Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(22)

Side by Side Diff: ui/gfx/content_analysis.cc

Issue 13947013: Complete (but inefficient) implementation of the image retargetting method. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 7 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "ui/gfx/content_analysis.h"
6
7 #include <algorithm>
8 #include <cmath>
9 #include <deque>
10 #include <functional>
11 #include <limits>
12 #include <numeric>
13 #include <vector>
14
15 #include "base/logging.h"
16 #include "skia/ext/convolver.h"
17 #include "third_party/skia/include/core/SkBitmap.h"
18 #include "third_party/skia/include/core/SkSize.h"
19 #include "ui/gfx/color_analysis.h"
20
21 namespace {
22
23 template<class InputIterator, class OutputIterator, class Compare>
24 void SlidingWindowMinMax(InputIterator first,
25 InputIterator last,
26 OutputIterator output,
27 int window_size,
28 Compare cmp) {
29 typedef std::deque<std::pair<typename InputIterator::value_type, int> >
30 deque_type;
31 deque_type slider;
32 int front_tail_length = window_size / 2;
33 int i = 0;
34 DCHECK(front_tail_length < last - first);
35 // This min-max filter functions the way image filters do. The min/max we
36 // compute is placed in the center of the window. Thus, first we need to
37 // 'pre-load' the window with the slider with right-tail of the filter.
38 for (; first < last && i < front_tail_length; ++i, ++first)
39 slider.push_back(std::make_pair(*first, i));
40
41 for (; first < last; ++i, ++first, ++output) {
42 while (!slider.empty() && !cmp(slider.back().first, *first))
43 slider.pop_back();
44 slider.push_back(std::make_pair(*first, i));
45
46 while (slider.front().second <= i - window_size)
47 slider.pop_front();
48 *output = slider.front().first;
49 }
50
51 // Now at the tail-end we will simply need to use whatever value is left of
52 // the filter to compute the remaining front_tail_length taps in the output.
53
54 // If input shorter than window, remainder length needs to be adjusted.
55 front_tail_length = std::min(front_tail_length, i);
56 for (;front_tail_length >= 0; --front_tail_length, ++i) {
57 while (slider.front().second <= i - window_size)
58 slider.pop_front();
59 *output = slider.front().first;
60 }
61 }
62
63 } // namespace
64
65 namespace color_utils {
66
67 void ApplyGaussianGradientMagnitudeFilter(SkBitmap* input_bitmap,
68 float kernel_sigma) {
69 // The purpose of this function is to highlight salient
70 // (attention-attracting?) features of the image for use in image
71 // retargetting.
72 SkAutoLockPixels source_lock(*input_bitmap);
73 DCHECK(input_bitmap);
74 DCHECK(input_bitmap->getPixels());
75 DCHECK_EQ(SkBitmap::kA8_Config, input_bitmap->config());
76
77 int tail_length = static_cast<int>(4.0f * kernel_sigma + 0.5f);
78 int kernel_size = tail_length * 2 + 1;
79 float smoother_weights[kernel_size];
80 float sigmasq = kernel_sigma * kernel_sigma;
81 float kernel_sum = 1.0f;
82
83 smoother_weights[tail_length] = 1.0f;
84
85 for (int ii = 1; ii <= tail_length; ++ii) {
86 float v = std::exp(-0.5f * ii * ii / sigmasq);
87 smoother_weights[tail_length + ii] = v;
88 smoother_weights[tail_length - ii] = v;
89 kernel_sum += 2.0f * v;
90 }
91
92 for (int i = 0; i <= kernel_size; ++i)
93 smoother_weights[i] /= kernel_sum;
94
95 float gradient_weights[kernel_size];
96 gradient_weights[tail_length] = 0.0;
97 for (int ii = 1; ii <= tail_length; ++ii) {
98 float v = sigmasq * smoother_weights[tail_length + ii] / ii;
99 gradient_weights[tail_length + ii] = v;
100 gradient_weights[tail_length - ii] = -v;
101 }
102
103 skia::ConvolutionFilter1D smoothing_filter;
104 skia::ConvolutionFilter1D gradient_filter;
105 smoothing_filter.AddFilter(0, smoother_weights, kernel_size);
106 gradient_filter.AddFilter(0, gradient_weights, kernel_size);
107
108 // To perform computations we will need one intermediate buffer. It can
109 // very well be just another bitmap.
110 const SkISize image_size = SkISize::Make(input_bitmap->width(),
111 input_bitmap->height());
112 SkBitmap intermediate;
113 intermediate.setConfig(
114 input_bitmap->config(), image_size.width(), image_size.height());
115 intermediate.allocPixels();
116
117 skia::SingleChannelConvolve1D_X(
118 static_cast<const uint8*>(input_bitmap->getPixels()),
Stephen White 2013/04/10 15:20:31 Suggestion: use input_bitmap->getAddr8(0, 0) to h
motek. 2013/04/11 16:18:38 Done.
119 static_cast<int>(input_bitmap->rowBytes()),
120 0, input_bitmap->bytesPerPixel(),
121 smoothing_filter,
122 image_size,
123 static_cast<uint8*>(intermediate.getPixels()),
124 static_cast<int>(intermediate.rowBytes()),
125 0, intermediate.bytesPerPixel(), false);
126 skia::SingleChannelConvolve1D_Y(
127 static_cast<const uint8*>(intermediate.getPixels()),
128 static_cast<int>(intermediate.rowBytes()),
129 0, intermediate.bytesPerPixel(),
130 smoothing_filter,
131 image_size,
132 static_cast<uint8*>(input_bitmap->getPixels()),
133 static_cast<int>(input_bitmap->rowBytes()),
134 0, input_bitmap->bytesPerPixel(), false);
135
136 // Now the gradient operator (we will need two buffers).
137 SkBitmap intermediate2;
138 intermediate2.setConfig(
139 input_bitmap->config(), image_size.width(), image_size.height());
140 intermediate2.allocPixels();
141
142 skia::SingleChannelConvolve1D_X(
143 static_cast<const uint8*>(input_bitmap->getPixels()),
144 static_cast<int>(input_bitmap->rowBytes()),
145 0, input_bitmap->bytesPerPixel(),
146 gradient_filter,
147 image_size,
148 static_cast<uint8*>(intermediate.getPixels()),
149 static_cast<int>(intermediate.rowBytes()),
150 0, intermediate.bytesPerPixel(), true);
151 skia::SingleChannelConvolve1D_Y(
152 static_cast<const uint8*>(input_bitmap->getPixels()),
153 static_cast<int>(input_bitmap->rowBytes()),
154 0, input_bitmap->bytesPerPixel(),
155 gradient_filter,
156 image_size,
157 static_cast<uint8*>(intermediate2.getPixels()),
158 static_cast<int>(intermediate2.rowBytes()),
159 0, intermediate2.bytesPerPixel(), true);
160
161 uint grad_max = 0;
162 for (int r = 0; r < image_size.height(); ++r) {
163 const uint8* grad_x_row = static_cast<const uint8*>(
164 intermediate.getPixels()) + r * intermediate.rowBytes();
165 const uint8* grad_y_row = static_cast<const uint8*>(
166 intermediate2.getPixels()) + r * intermediate2.rowBytes();
167 for (int c = 0; c < image_size.width(); ++c) {
168 uint grad_x = grad_x_row[c];
169 uint grad_y = grad_y_row[c];
170 grad_max = std::max(grad_max, grad_x * grad_x + grad_y * grad_y);
171 }
172 }
173
174 int bit_shift = 0;
175 if (grad_max > 255)
176 bit_shift = static_cast<int>(
177 std::log10(static_cast<float>(grad_max)) / std::log10(2.0f)) - 7;
Stephen White 2013/04/10 15:20:31 Not that it really matters, but these could probab
motek. 2013/04/11 16:18:38 They certainly could. I need log2 which is not ava
178 for (int r = 0; r < image_size.height(); ++r) {
179 const uint8* grad_x_row = static_cast<const uint8*>(
180 intermediate.getPixels()) + r * intermediate.rowBytes();
Stephen White 2013/04/10 15:20:31 intermediate.getAddr8(0, r) should work here, and
motek. 2013/04/11 16:18:38 Done.
181 const uint8* grad_y_row = static_cast<const uint8*>(
182 intermediate2.getPixels()) + r * intermediate2.rowBytes();
183 uint8* target_row = static_cast<uint8*>(
184 input_bitmap->getPixels()) + r * input_bitmap->rowBytes();
185 for (int c = 0; c < image_size.width(); ++c) {
186 uint grad_x = grad_x_row[c];
187 uint grad_y = grad_y_row[c];
188 target_row[c] = (grad_x * grad_x + grad_y * grad_y) >> bit_shift;
189 }
190 }
191 }
192
193 void ExtractImageProfileInformation(const SkBitmap& input_bitmap,
194 const gfx::Rect& area,
195 const gfx::Size& target_size,
196 bool apply_log,
197 std::vector<float>* rows,
198 std::vector<float>* columns) {
199 SkAutoLockPixels source_lock(input_bitmap);
200 DCHECK(rows);
201 DCHECK(columns);
202 DCHECK(input_bitmap.getPixels());
203 DCHECK_EQ(SkBitmap::kA8_Config, input_bitmap.config());
204 DCHECK_GE(area.x(), 0);
205 DCHECK_GE(area.y(), 0);
206 DCHECK_LE(area.right(), input_bitmap.width());
207 DCHECK_LE(area.bottom(), input_bitmap.height());
208
209 // Make sure rows and columns are allocated and initialized to 0.
210 rows->resize(0);
211 columns->resize(0);
212 rows->resize(area.height(), 0);
213 columns->resize(area.width(), 0);
214
215 for (int r = 0; r < area.height(); ++r) {
216 const uint8* image_row = static_cast<const uint8*>(
217 input_bitmap.getPixels()) +
218 (r + area.y()) * input_bitmap.rowBytes() +
219 area.x(); // Points to the first byte of the row in the rectangle.
220 unsigned row_sum = 0;
221 for (int c = 0; c < area.width(); ++c, ++image_row) {
222 row_sum += *image_row;
223 (*columns)[c] += *image_row;
224 }
225 (*rows)[r] = row_sum;
226 }
227
228 if (apply_log) {
229 // Generally for processing we will need to take logarithm of this data.
230 // The option not to apply it is left principally as a test seam.
231 std::vector<float>::iterator it;
232 for (it = columns->begin(); it < columns->end(); ++it)
233 *it = std::log(1.0f + *it);
234
235 for (it = rows->begin(); it < rows->end(); ++it)
236 *it = std::log(1.0f + *it);
237 }
238
239 if (!target_size.IsEmpty()) {
240 // If the target size is given, profiles should be further processed through
241 // morphological closing. The idea is to close valleys smaller than what
242 // can be seen after scaling down to avoid deforming noticable features
243 // when profiles are used.
244 // Morphological closing is defined as dilation followed by errosion. In
245 // normal-speak: sliding-window maximum followed by minimum.
246 int column_window_size = 1 +
247 2 * static_cast<int>(0.5f * area.width() / target_size.width() + 0.5);
Stephen White 2013/04/10 15:20:31 0.5 should probably be 0.5f, unless you really nee
motek. 2013/04/11 16:18:38 Done.
248 int row_window_size = 1 +
249 2 * static_cast<int>(0.5f * area.height() / target_size.height() + 0.5);
Stephen White 2013/04/10 15:20:31 Same here.
motek. 2013/04/11 16:18:38 Done.
250
251 // Dilate and erode each profile with the given window size.
252 if (column_window_size >= 3) {
253 SlidingWindowMinMax(columns->begin(),
254 columns->end(),
255 columns->begin(),
256 column_window_size,
257 std::greater<float>());
258 SlidingWindowMinMax(columns->begin(),
259 columns->end(),
260 columns->begin(),
261 column_window_size,
262 std::less<float>());
263 }
264
265 if (row_window_size >= 3) {
266 SlidingWindowMinMax(rows->begin(),
267 rows->end(),
268 rows->begin(),
269 row_window_size,
270 std::greater<float>());
271 SlidingWindowMinMax(rows->begin(),
272 rows->end(),
273 rows->begin(),
274 row_window_size,
275 std::less<float>());
276 }
277 }
278 }
279
280 float AutoSegmentPeaks(const std::vector<float>& input) {
281 // This is a thresholding operation based on Otsu's method.
282 std::vector<int> histogram(256, 0);
283 std::vector<float>::const_iterator it;
284
285 float value_min = std::numeric_limits<float>::max();
286 float value_max = std::numeric_limits<float>::min();
287
288 for (it = input.begin(); it < input.end(); ++it) {
Alexei Svitkine (slow) 2013/04/09 18:02:23 Be consistent whether you use iterators or indices
motek. 2013/04/11 16:18:38 I think I am consistent. I use iterators everywher
289 value_min = std::min(value_min, *it);
290 value_max = std::max(value_max, *it);
291 }
292
293 if (value_max - value_min <= std::numeric_limits<float>::epsilon() * 100) {
294 // Scaling won't work and there is nothing really to segment anyway.
295 return value_min;
296 }
297
298 float value_span = value_max - value_min;
299 for (it = input.begin(); it < input.end(); ++it) {
300 float scaled_value = (*it - value_min) / value_span * 255;
301 histogram[static_cast<int>(scaled_value)] += 1;
302 }
303
304 // Otsu's method seeks to maximize variance between two classes of pixels
305 // correspondng to valleys and peaks of the profile.
306 double w1 = histogram[0]; // Total weight of the first class.
307 double t1 = 0.5 * w1;
308 double w2 = 0;
309 double t2 = 0;
310 for (unsigned i = 1; i < histogram.size(); ++i) {
Alexei Svitkine (slow) 2013/04/09 18:02:23 Use size_t, here and throughout for the indices.
motek. 2013/04/11 16:18:38 Done.
311 w2 += histogram[i];
312 t2 += (0.5 + i) * histogram[i];
313 }
314
315 int max_index = 0;
316 double m1 = t1 / w1;
317 double m2 = t2 / w2;
318 double max_variance_score = w1 * w2 * (m1 - m2) * (m1 - m2);
319 // Iterate through all possible ways of splitting the histogram.
320 for (unsigned i = 1; i < histogram.size() - 1; i++) {
321 double bin_volume = (0.5 + i) * histogram[i];
322 w1 += histogram[i];
323 w2 -= histogram[i];
324 t2 -= bin_volume;
325 t1 += bin_volume;
326 m1 = t1 / w1;
327 m2 = t2 / w2;
328 double variance_score = w1 * w2 * (m1 - m2) * (m1 - m2);
329 if (variance_score >= max_variance_score) {
330 max_variance_score = variance_score;
331 max_index = i;
332 }
333 }
334
335 // max_index refers to the bin *after* which we need to split. The sought
336 // threshold is the centre of this bin, scaled back to the original range.
337 return value_span * (max_index + 0.5f) / 255.0f + value_min;
338 }
339
340 SkBitmap* ComputeDecimatedImage(const SkBitmap& bitmap,
341 const std::vector<bool>& rows,
342 const std::vector<bool>& columns) {
343 SkAutoLockPixels source_lock(bitmap);
344 DCHECK(bitmap.getPixels());
345 DCHECK_GT(bitmap.bytesPerPixel(), 0);
346 DCHECK_EQ(bitmap.width(), static_cast<int>(columns.size()));
347 DCHECK_EQ(bitmap.height(), static_cast<int>(rows.size()));
348
349 unsigned tgt_row_count = std::count(rows.begin(), rows.end(), true);
350 unsigned tgt_column_count = std::count(columns.begin(), columns.end(), true);
351
352 if (tgt_row_count == 0 || tgt_column_count == 0)
353 return NULL; // Not quite an error, so no DCHECK. Just return null.
354
355 if (tgt_row_count == rows.size() && tgt_column_count == columns.size())
356 return NULL; // Equivalent of the situation where the target is empty.
357
358 // Declare and allocate the target image.
359 SkBitmap target;
360 target.setConfig(bitmap.config(), tgt_column_count, tgt_row_count);
361 target.allocPixels();
362
363 int tgt_row = 0;
364 for (int r = 0; r < bitmap.height(); ++r) {
365 if (!rows[r])
366 continue; // We can just skip this one.
367 uint8* src_row =
368 static_cast<uint8*>(bitmap.getPixels()) + r * bitmap.rowBytes();
369 uint8* insertion_target =
370 static_cast<uint8*>(target.getPixels()) + tgt_row * target.rowBytes();
371 int left_copy_pixel = -1;
372 for (int c = 0; c < bitmap.width(); ++c) {
373 if (left_copy_pixel < 0 && columns[c]) {
374 left_copy_pixel = c; // Next time we will start copying from here.
375 } else if (left_copy_pixel >= 0 && !columns[c]) {
376 // This closes a fragment we want to copy. We do it now.
377 size_t bytes_to_copy = (c - left_copy_pixel) * bitmap.bytesPerPixel();
378 memcpy(insertion_target,
379 src_row + left_copy_pixel * bitmap.bytesPerPixel(),
380 bytes_to_copy);
381 left_copy_pixel = -1;
382 insertion_target += bytes_to_copy;
383 }
384 }
385 // We can still have the tail end to process here.
386 if (left_copy_pixel >= 0) {
387 size_t bytes_to_copy =
388 (bitmap.width() - left_copy_pixel) * bitmap.bytesPerPixel();
389 memcpy(insertion_target,
390 src_row + left_copy_pixel * bitmap.bytesPerPixel(),
391 bytes_to_copy);
392 }
393 tgt_row++;
394 }
395
396 return new SkBitmap(target);
397 }
398
399 SkBitmap* CreateRetargettedThumbnailImage(
400 const SkBitmap& source_bitmap,
401 const gfx::Size& target_size,
402 float kernel_sigma) {
403 // First thing we need for this method is to color-reduce the source_bitmap.
404 SkBitmap reduced_color;
405 reduced_color.setConfig(
406 SkBitmap::kA8_Config, source_bitmap.width(), source_bitmap.height());
407 reduced_color.allocPixels();
408
409 if (!ComputePrincipalComponentImage(source_bitmap, &reduced_color)) {
410 // CCIR601 luminance conversion vector.
411 gfx::Vector3dF transform(0.299f, 0.587f, 0.114f);
412 if (!ApplyColorReduction(source_bitmap, transform, true, &reduced_color)) {
413 DLOG(WARNING) << "Failed to compute luminance image from a screenshot. "
414 << "Cannot compute retargetted thumbnail.";
415 return NULL;
416 }
417 DLOG(WARNING) << "Could not compute principal color image for a thumbnail. "
418 << "Using luminance instead.";
419 }
420
421 // Turn 'color-reduced' image into the 'energy' image.
422 ApplyGaussianGradientMagnitudeFilter(&reduced_color, kernel_sigma);
423
424 // Extract vertical and horizontal projection of image features.
425 std::vector<float> row_profile;
426 std::vector<float> column_profile;
427 ExtractImageProfileInformation(reduced_color,
428 gfx::Rect(reduced_color.width(),
429 reduced_color.height()),
430 target_size,
431 true,
432 &row_profile,
433 &column_profile);
434 float threshold_rows = AutoSegmentPeaks(row_profile);
435 float threshold_columns = AutoSegmentPeaks(column_profile);
436
437 // Apply thresholding.
438 std::vector<bool> included_rows(row_profile.size(), false);
439 std::transform(row_profile.begin(),
440 row_profile.end(),
441 included_rows.begin(),
442 std::bind2nd(std::greater<float>(), threshold_rows));
443
444 std::vector<bool> included_columns(column_profile.size(), false);
445 std::transform(column_profile.begin(),
446 column_profile.end(),
447 included_columns.begin(),
448 std::bind2nd(std::greater<float>(), threshold_columns));
449
450 // Use the original image and computed inclusion vectors to create a resized
451 // image.
452 return ComputeDecimatedImage(source_bitmap, included_rows, included_columns);
453 }
454
455 } // color_utils
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698