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

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

Powered by Google App Engine
This is Rietveld 408576698