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 "chrome/browser/thumbnails/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 "skia/ext/recursive_gaussian_convolution.h"
18 #include "third_party/skia/include/core/SkBitmap.h"
19 #include "third_party/skia/include/core/SkSize.h"
20 #include "ui/gfx/color_analysis.h"
21
22 namespace {
23
24 const float kSigmaThresholdForRecursive = 1.5f;
25 const float kAspectRatioToleranceFactor = 1.02f;
26
27 template<class InputIterator, class OutputIterator, class Compare>
SlidingWindowMinMax(InputIterator first,InputIterator last,OutputIterator output,int window_size,Compare cmp)28 void SlidingWindowMinMax(InputIterator first,
29 InputIterator last,
30 OutputIterator output,
31 int window_size,
32 Compare cmp) {
33 typedef std::deque<
34 std::pair<typename std::iterator_traits<InputIterator>::value_type, int> >
35 deque_type;
36 deque_type slider;
37 int front_tail_length = window_size / 2;
38 int i = 0;
39 DCHECK_LT(front_tail_length, last - first);
40 // This min-max filter functions the way image filters do. The min/max we
41 // compute is placed in the center of the window. Thus, first we need to
42 // 'pre-load' the window with the slider with right-tail of the filter.
43 for (; first < last && i < front_tail_length; ++i, ++first)
44 slider.push_back(std::make_pair(*first, i));
45
46 for (; first < last; ++i, ++first, ++output) {
47 while (!slider.empty() && !cmp(slider.back().first, *first))
48 slider.pop_back();
49 slider.push_back(std::make_pair(*first, i));
50
51 while (slider.front().second <= i - window_size)
52 slider.pop_front();
53 *output = slider.front().first;
54 }
55
56 // Now at the tail-end we will simply need to use whatever value is left of
57 // the filter to compute the remaining front_tail_length taps in the output.
58
59 // If input shorter than window, remainder length needs to be adjusted.
60 front_tail_length = std::min(front_tail_length, i);
61 for (; front_tail_length >= 0; --front_tail_length, ++i) {
62 while (slider.front().second <= i - window_size)
63 slider.pop_front();
64 *output = slider.front().first;
65 }
66 }
67
FindOtsuThresholdingIndex(const std::vector<int> & histogram)68 size_t FindOtsuThresholdingIndex(const std::vector<int>& histogram) {
69 // Otsu's method seeks to maximize variance between two classes of pixels
70 // correspondng to valleys and peaks of the profile.
71 double w1 = histogram[0]; // Total weight of the first class.
72 double t1 = 0.5 * w1;
73 double w2 = 0.0;
74 double t2 = 0.0;
75 for (size_t i = 1; i < histogram.size(); ++i) {
76 w2 += histogram[i];
77 t2 += (0.5 + i) * histogram[i];
78 }
79
80 size_t max_index = 0;
81 double m1 = t1 / w1;
82 double m2 = t2 / w2;
83 double max_variance_score = w1 * w2 * (m1 - m2) * (m1 - m2);
84 // Iterate through all possible ways of splitting the histogram.
85 for (size_t i = 1; i < histogram.size() - 1; i++) {
86 double bin_volume = (0.5 + i) * histogram[i];
87 w1 += histogram[i];
88 w2 -= histogram[i];
89 t2 -= bin_volume;
90 t1 += bin_volume;
91 m1 = t1 / w1;
92 m2 = t2 / w2;
93 double variance_score = w1 * w2 * (m1 - m2) * (m1 - m2);
94 if (variance_score >= max_variance_score) {
95 max_variance_score = variance_score;
96 max_index = i;
97 }
98 }
99
100 return max_index;
101 }
102
ComputeScaledHistogram(const std::vector<float> & source,std::vector<int> * histogram,std::pair<float,float> * minmax)103 bool ComputeScaledHistogram(const std::vector<float>& source,
104 std::vector<int>* histogram,
105 std::pair<float, float>* minmax) {
106 DCHECK(histogram);
107 DCHECK(minmax);
108 histogram->clear();
109 histogram->resize(256);
110 float value_min = std::numeric_limits<float>::max();
111 float value_max = 0.0f;
112
113 std::vector<float>::const_iterator it;
114 for (it = source.begin(); it < source.end(); ++it) {
115 value_min = std::min(value_min, *it);
116 value_max = std::max(value_max, *it);
117 }
118
119 *minmax = std::make_pair(value_min, value_max);
120
121 if (value_max - value_min <= std::numeric_limits<float>::epsilon() * 100.0f) {
122 // Scaling won't work and there is nothing really to segment anyway.
123 return false;
124 }
125
126 float value_span = value_max - value_min;
127 float scale = 255.0f / value_span;
128 for (it = source.begin(); it < source.end(); ++it) {
129 float scaled_value = (*it - value_min) * scale;
130 (*histogram)[static_cast<int>(scaled_value)] += 1;
131 }
132 return true;
133 }
134
ConstrainedProfileThresholding(const std::vector<float> & profile,const std::vector<int> & histogram,int current_clip_index,float current_threshold,const std::pair<float,float> & range,int size_for_threshold,int target_size,std::vector<bool> * result)135 void ConstrainedProfileThresholding(const std::vector<float>& profile,
136 const std::vector<int>& histogram,
137 int current_clip_index,
138 float current_threshold,
139 const std::pair<float, float>& range,
140 int size_for_threshold,
141 int target_size,
142 std::vector<bool>* result) {
143 DCHECK(!profile.empty());
144 DCHECK_EQ(histogram.size(), 256U);
145 DCHECK(result);
146
147 // A subroutine performing thresholding on the |profile|.
148 if (size_for_threshold != target_size) {
149 // Find a cut-off point (on the histogram) closest to the desired size.
150 int candidate_size = profile.size();
151 int candidate_clip_index = 0;
152 for (std::vector<int>::const_iterator it = histogram.begin();
153 it != histogram.end(); ++it, ++candidate_clip_index) {
154 if (std::abs(candidate_size - target_size) <
155 std::abs(candidate_size - *it - target_size)) {
156 break;
157 }
158 candidate_size -= *it;
159 }
160
161 if (std::abs(candidate_size - target_size) <
162 std::abs(candidate_size -size_for_threshold)) {
163 current_clip_index = candidate_clip_index;
164 current_threshold = (range.second - range.first) *
165 current_clip_index / 255.0f + range.first;
166 // Recount, rather than assume. One-offs due to rounding can be very
167 // harmful when eroding / dilating the result.
168 size_for_threshold = std::count_if(
169 profile.begin(), profile.end(),
170 std::bind2nd(std::greater<float>(), current_threshold));
171 }
172 }
173
174 result->resize(profile.size());
175 for (size_t i = 0; i < profile.size(); ++i)
176 (*result)[i] = profile[i] > current_threshold;
177
178 while (size_for_threshold > target_size) {
179 // If the current size is larger than target size, erode exactly as needed.
180 std::vector<bool>::iterator mod_it = result->begin();
181 std::vector<bool>::const_iterator lead_it = result->begin();
182 bool prev_value = true;
183 for (++lead_it;
184 lead_it < result->end() && size_for_threshold > target_size;
185 ++lead_it, ++mod_it) {
186 bool value = *mod_it;
187 // If any neighbour is false, switch the element off.
188 if (!prev_value || !*lead_it) {
189 *mod_it = false;
190 --size_for_threshold;
191 }
192 prev_value = value;
193 }
194
195 if (lead_it == result->end() && !prev_value) {
196 *mod_it = false;
197 --size_for_threshold;
198 }
199 }
200
201 while (size_for_threshold < target_size) {
202 std::vector<bool>::iterator mod_it = result->begin();
203 std::vector<bool>::const_iterator lead_it = result->begin();
204 bool prev_value = false;
205 for (++lead_it;
206 lead_it < result->end() && size_for_threshold < target_size;
207 ++lead_it, ++mod_it) {
208 bool value = *mod_it;
209 // If any neighbour is false, switch the element off.
210 if (!prev_value || !*lead_it) {
211 *mod_it = true;
212 ++size_for_threshold;
213 }
214 prev_value = value;
215 }
216
217 if (lead_it == result->end() && !prev_value) {
218 *mod_it = true;
219 ++size_for_threshold;
220 }
221 }
222 }
223
224 } // namespace
225
226 namespace thumbnailing_utils {
227
ApplyGaussianGradientMagnitudeFilter(SkBitmap * input_bitmap,float kernel_sigma)228 void ApplyGaussianGradientMagnitudeFilter(SkBitmap* input_bitmap,
229 float kernel_sigma) {
230 // The purpose of this function is to highlight salient
231 // (attention-attracting?) features of the image for use in image
232 // retargeting.
233 SkAutoLockPixels source_lock(*input_bitmap);
234 DCHECK(input_bitmap);
235 DCHECK(input_bitmap->getPixels());
236 DCHECK_EQ(SkBitmap::kA8_Config, input_bitmap->config());
237
238 // To perform computations we will need one intermediate buffer. It can
239 // very well be just another bitmap.
240 const SkISize image_size = SkISize::Make(input_bitmap->width(),
241 input_bitmap->height());
242 SkBitmap intermediate;
243 intermediate.setConfig(
244 input_bitmap->config(), image_size.width(), image_size.height());
245 intermediate.allocPixels();
246
247 SkBitmap intermediate2;
248 intermediate2.setConfig(
249 input_bitmap->config(), image_size.width(), image_size.height());
250 intermediate2.allocPixels();
251
252
253 if (kernel_sigma <= kSigmaThresholdForRecursive) {
254 // For small kernels classic implementation is faster.
255 skia::ConvolutionFilter1D smoothing_filter;
256 skia::SetUpGaussianConvolutionKernel(
257 &smoothing_filter, kernel_sigma, false);
258 skia::SingleChannelConvolveX1D(
259 input_bitmap->getAddr8(0, 0),
260 static_cast<int>(input_bitmap->rowBytes()),
261 0, input_bitmap->bytesPerPixel(),
262 smoothing_filter,
263 image_size,
264 intermediate.getAddr8(0, 0),
265 static_cast<int>(intermediate.rowBytes()),
266 0, intermediate.bytesPerPixel(), false);
267 skia::SingleChannelConvolveY1D(
268 intermediate.getAddr8(0, 0),
269 static_cast<int>(intermediate.rowBytes()),
270 0, intermediate.bytesPerPixel(),
271 smoothing_filter,
272 image_size,
273 input_bitmap->getAddr8(0, 0),
274 static_cast<int>(input_bitmap->rowBytes()),
275 0, input_bitmap->bytesPerPixel(), false);
276
277 skia::ConvolutionFilter1D gradient_filter;
278 skia::SetUpGaussianConvolutionKernel(&gradient_filter, kernel_sigma, true);
279 skia::SingleChannelConvolveX1D(
280 input_bitmap->getAddr8(0, 0),
281 static_cast<int>(input_bitmap->rowBytes()),
282 0, input_bitmap->bytesPerPixel(),
283 gradient_filter,
284 image_size,
285 intermediate.getAddr8(0, 0),
286 static_cast<int>(intermediate.rowBytes()),
287 0, intermediate.bytesPerPixel(), true);
288 skia::SingleChannelConvolveY1D(
289 input_bitmap->getAddr8(0, 0),
290 static_cast<int>(input_bitmap->rowBytes()),
291 0, input_bitmap->bytesPerPixel(),
292 gradient_filter,
293 image_size,
294 intermediate2.getAddr8(0, 0),
295 static_cast<int>(intermediate2.rowBytes()),
296 0, intermediate2.bytesPerPixel(), true);
297 } else {
298 // For larger sigma values use the recursive filter.
299 skia::RecursiveFilter smoothing_filter(kernel_sigma,
300 skia::RecursiveFilter::FUNCTION);
301 skia::SingleChannelRecursiveGaussianX(
302 input_bitmap->getAddr8(0, 0),
303 static_cast<int>(input_bitmap->rowBytes()),
304 0, input_bitmap->bytesPerPixel(),
305 smoothing_filter,
306 image_size,
307 intermediate.getAddr8(0, 0),
308 static_cast<int>(intermediate.rowBytes()),
309 0, intermediate.bytesPerPixel(), false);
310 unsigned char smoothed_max = skia::SingleChannelRecursiveGaussianY(
311 intermediate.getAddr8(0, 0),
312 static_cast<int>(intermediate.rowBytes()),
313 0, intermediate.bytesPerPixel(),
314 smoothing_filter,
315 image_size,
316 input_bitmap->getAddr8(0, 0),
317 static_cast<int>(input_bitmap->rowBytes()),
318 0, input_bitmap->bytesPerPixel(), false);
319 if (smoothed_max < 127) {
320 int bit_shift = 8 - static_cast<int>(
321 std::log10(static_cast<float>(smoothed_max)) / std::log10(2.0f));
322 for (int r = 0; r < image_size.height(); ++r) {
323 uint8* row = input_bitmap->getAddr8(0, r);
324 for (int c = 0; c < image_size.width(); ++c, ++row) {
325 *row <<= bit_shift;
326 }
327 }
328 }
329
330 skia::RecursiveFilter gradient_filter(
331 kernel_sigma, skia::RecursiveFilter::FIRST_DERIVATIVE);
332 skia::SingleChannelRecursiveGaussianX(
333 input_bitmap->getAddr8(0, 0),
334 static_cast<int>(input_bitmap->rowBytes()),
335 0, input_bitmap->bytesPerPixel(),
336 gradient_filter,
337 image_size,
338 intermediate.getAddr8(0, 0),
339 static_cast<int>(intermediate.rowBytes()),
340 0, intermediate.bytesPerPixel(), true);
341 skia::SingleChannelRecursiveGaussianY(
342 input_bitmap->getAddr8(0, 0),
343 static_cast<int>(input_bitmap->rowBytes()),
344 0, input_bitmap->bytesPerPixel(),
345 gradient_filter,
346 image_size,
347 intermediate2.getAddr8(0, 0),
348 static_cast<int>(intermediate2.rowBytes()),
349 0, intermediate2.bytesPerPixel(), true);
350 }
351
352 unsigned grad_max = 0;
353 for (int r = 0; r < image_size.height(); ++r) {
354 const uint8* grad_x_row = intermediate.getAddr8(0, r);
355 const uint8* grad_y_row = intermediate2.getAddr8(0, r);
356 for (int c = 0; c < image_size.width(); ++c) {
357 unsigned grad_x = grad_x_row[c];
358 unsigned grad_y = grad_y_row[c];
359 grad_max = std::max(grad_max, grad_x * grad_x + grad_y * grad_y);
360 }
361 }
362
363 int bit_shift = 0;
364 if (grad_max > 255)
365 bit_shift = static_cast<int>(
366 std::log10(static_cast<float>(grad_max)) / std::log10(2.0f)) - 7;
367 for (int r = 0; r < image_size.height(); ++r) {
368 const uint8* grad_x_row = intermediate.getAddr8(0, r);
369 const uint8* grad_y_row = intermediate2.getAddr8(0, r);
370 uint8* target_row = input_bitmap->getAddr8(0, r);
371 for (int c = 0; c < image_size.width(); ++c) {
372 unsigned grad_x = grad_x_row[c];
373 unsigned grad_y = grad_y_row[c];
374 target_row[c] = (grad_x * grad_x + grad_y * grad_y) >> bit_shift;
375 }
376 }
377 }
378
ExtractImageProfileInformation(const SkBitmap & input_bitmap,const gfx::Rect & area,const gfx::Size & target_size,bool apply_log,std::vector<float> * rows,std::vector<float> * columns)379 void ExtractImageProfileInformation(const SkBitmap& input_bitmap,
380 const gfx::Rect& area,
381 const gfx::Size& target_size,
382 bool apply_log,
383 std::vector<float>* rows,
384 std::vector<float>* columns) {
385 SkAutoLockPixels source_lock(input_bitmap);
386 DCHECK(rows);
387 DCHECK(columns);
388 DCHECK(input_bitmap.getPixels());
389 DCHECK_EQ(SkBitmap::kA8_Config, input_bitmap.config());
390 DCHECK_GE(area.x(), 0);
391 DCHECK_GE(area.y(), 0);
392 DCHECK_LE(area.right(), input_bitmap.width());
393 DCHECK_LE(area.bottom(), input_bitmap.height());
394
395 // Make sure rows and columns are allocated and initialized to 0.
396 rows->clear();
397 columns->clear();
398 rows->resize(area.height(), 0);
399 columns->resize(area.width(), 0);
400
401 for (int r = 0; r < area.height(); ++r) {
402 // Points to the first byte of the row in the rectangle.
403 const uint8* image_row = input_bitmap.getAddr8(area.x(), r + area.y());
404 unsigned row_sum = 0;
405 for (int c = 0; c < area.width(); ++c, ++image_row) {
406 row_sum += *image_row;
407 (*columns)[c] += *image_row;
408 }
409 (*rows)[r] = row_sum;
410 }
411
412 if (apply_log) {
413 // Generally for processing we will need to take logarithm of this data.
414 // The option not to apply it is left principally as a test seam.
415 std::vector<float>::iterator it;
416 for (it = columns->begin(); it < columns->end(); ++it)
417 *it = std::log(1.0f + *it);
418
419 for (it = rows->begin(); it < rows->end(); ++it)
420 *it = std::log(1.0f + *it);
421 }
422
423 if (!target_size.IsEmpty()) {
424 // If the target size is given, profiles should be further processed through
425 // morphological closing. The idea is to close valleys smaller than what
426 // can be seen after scaling down to avoid deforming noticable features
427 // when profiles are used.
428 // Morphological closing is defined as dilation followed by errosion. In
429 // normal-speak: sliding-window maximum followed by minimum.
430 int column_window_size = 1 + 2 *
431 static_cast<int>(0.5f * area.width() / target_size.width() + 0.5f);
432 int row_window_size = 1 + 2 *
433 static_cast<int>(0.5f * area.height() / target_size.height() + 0.5f);
434
435 // Dilate and erode each profile with the given window size.
436 if (column_window_size >= 3) {
437 SlidingWindowMinMax(columns->begin(),
438 columns->end(),
439 columns->begin(),
440 column_window_size,
441 std::greater<float>());
442 SlidingWindowMinMax(columns->begin(),
443 columns->end(),
444 columns->begin(),
445 column_window_size,
446 std::less<float>());
447 }
448
449 if (row_window_size >= 3) {
450 SlidingWindowMinMax(rows->begin(),
451 rows->end(),
452 rows->begin(),
453 row_window_size,
454 std::greater<float>());
455 SlidingWindowMinMax(rows->begin(),
456 rows->end(),
457 rows->begin(),
458 row_window_size,
459 std::less<float>());
460 }
461 }
462 }
463
AutoSegmentPeaks(const std::vector<float> & input)464 float AutoSegmentPeaks(const std::vector<float>& input) {
465 // This is a thresholding operation based on Otsu's method.
466 std::vector<int> histogram;
467 std::pair<float, float> minmax;
468 if (!ComputeScaledHistogram(input, &histogram, &minmax))
469 return minmax.first;
470
471 // max_index refers to the bin *after* which we need to split. The sought
472 // threshold is the centre of this bin, scaled back to the original range.
473 size_t max_index = FindOtsuThresholdingIndex(histogram);
474 return (minmax.second - minmax.first) * (max_index + 0.5f) / 255.0f +
475 minmax.first;
476 }
477
AdjustClippingSizeToAspectRatio(const gfx::Size & target_size,const gfx::Size & image_size,const gfx::Size & computed_size)478 gfx::Size AdjustClippingSizeToAspectRatio(const gfx::Size& target_size,
479 const gfx::Size& image_size,
480 const gfx::Size& computed_size) {
481 DCHECK_GT(target_size.width(), 0);
482 DCHECK_GT(target_size.height(), 0);
483 // If the computed thumbnail would be too wide or to tall, we shall attempt
484 // to fix it. Generally the idea is to re-add content to the part which has
485 // been more aggressively shrunk unless there is nothing to add there or if
486 // adding there won't fix anything. Should that be the case, we will
487 // (reluctantly) take away more from the other dimension.
488 float desired_aspect =
489 static_cast<float>(target_size.width()) / target_size.height();
490 int computed_width = std::max(computed_size.width(), target_size.width());
491 int computed_height = std::max(computed_size.height(), target_size.height());
492 float computed_aspect = static_cast<float>(computed_width) / computed_height;
493 float aspect_change_delta = std::abs(computed_aspect - desired_aspect);
494 float prev_aspect_change_delta = 1000.0f;
495 const float kAspectChangeEps = 0.01f;
496 const float kLargeEffect = 2.0f;
497
498 while ((prev_aspect_change_delta - aspect_change_delta > kAspectChangeEps) &&
499 (computed_aspect / desired_aspect > kAspectRatioToleranceFactor ||
500 desired_aspect / computed_aspect > kAspectRatioToleranceFactor)) {
501 int new_computed_width = computed_width;
502 int new_computed_height = computed_height;
503 float row_dimension_shrink =
504 static_cast<float>(image_size.height()) / computed_height;
505 float column_dimension_shrink =
506 static_cast<float>(image_size.width()) / computed_width;
507
508 if (computed_aspect / desired_aspect > kAspectRatioToleranceFactor) {
509 // Too wide.
510 if (row_dimension_shrink > column_dimension_shrink) {
511 // Bring the computed_height to the least of:
512 // (1) image height (2) the number of lines that would
513 // make up the desired aspect or (3) number of lines we would get
514 // at the same 'aggressivity' level as width or.
515 new_computed_height = std::min(
516 static_cast<int>(image_size.height()),
517 static_cast<int>(computed_width / desired_aspect + 0.5f));
518 new_computed_height = std::min(
519 new_computed_height,
520 static_cast<int>(
521 image_size.height() / column_dimension_shrink + 0.5f));
522 } else if (row_dimension_shrink >= kLargeEffect ||
523 new_computed_width <= target_size.width()) {
524 // Even though rows were resized less, we will generally rather add than
525 // remove (or there is nothing to remove in x already).
526 new_computed_height = std::min(
527 static_cast<int>(image_size.height()),
528 static_cast<int>(computed_width / desired_aspect + 0.5f));
529 } else {
530 // Rows were already shrunk less aggressively. This means there is
531 // simply no room left too expand. Cut columns to get the desired
532 // aspect ratio.
533 new_computed_width = desired_aspect * computed_height + 0.5f;
534 }
535 } else {
536 // Too tall.
537 if (column_dimension_shrink > row_dimension_shrink) {
538 // Columns were shrunk more aggressively. Try to relax the same way as
539 // above.
540 new_computed_width = std::min(
541 static_cast<int>(image_size.width()),
542 static_cast<int>(desired_aspect * computed_height + 0.5f));
543 new_computed_width = std::min(
544 new_computed_width,
545 static_cast<int>(
546 image_size.width() / row_dimension_shrink + 0.5f));
547 } else if (column_dimension_shrink >= kLargeEffect ||
548 new_computed_height <= target_size.height()) {
549 new_computed_width = std::min(
550 static_cast<int>(image_size.width()),
551 static_cast<int>(desired_aspect * computed_height + 0.5f));
552 } else {
553 new_computed_height = computed_width / desired_aspect + 0.5f;
554 }
555 }
556
557 new_computed_width = std::max(new_computed_width, target_size.width());
558 new_computed_height = std::max(new_computed_height, target_size.height());
559
560 // Update loop control variables.
561 float new_computed_aspect =
562 static_cast<float>(new_computed_width) / new_computed_height;
563
564 if (std::abs(new_computed_aspect - desired_aspect) >
565 std::abs(computed_aspect - desired_aspect)) {
566 // Do not take inferior results.
567 break;
568 }
569
570 computed_width = new_computed_width;
571 computed_height = new_computed_height;
572 computed_aspect = new_computed_aspect;
573 prev_aspect_change_delta = aspect_change_delta;
574 aspect_change_delta = std::abs(new_computed_aspect - desired_aspect);
575 }
576
577 return gfx::Size(computed_width, computed_height);
578 }
579
ConstrainedProfileSegmentation(const std::vector<float> & row_profile,const std::vector<float> & column_profile,const gfx::Size & target_size,std::vector<bool> * included_rows,std::vector<bool> * included_columns)580 void ConstrainedProfileSegmentation(const std::vector<float>& row_profile,
581 const std::vector<float>& column_profile,
582 const gfx::Size& target_size,
583 std::vector<bool>* included_rows,
584 std::vector<bool>* included_columns) {
585 DCHECK(included_rows);
586 DCHECK(included_columns);
587
588 std::vector<int> histogram_rows;
589 std::pair<float, float> minmax_rows;
590 bool rows_well_behaved = ComputeScaledHistogram(
591 row_profile, &histogram_rows, &minmax_rows);
592
593 float row_threshold = minmax_rows.first;
594 size_t clip_index_rows = 0;
595
596 if (rows_well_behaved) {
597 clip_index_rows = FindOtsuThresholdingIndex(histogram_rows);
598 row_threshold = (minmax_rows.second - minmax_rows.first) *
599 (clip_index_rows + 0.5f) / 255.0f + minmax_rows.first;
600 }
601
602 std::vector<int> histogram_columns;
603 std::pair<float, float> minmax_columns;
604 bool columns_well_behaved = ComputeScaledHistogram(column_profile,
605 &histogram_columns,
606 &minmax_columns);
607 float column_threshold = minmax_columns.first;
608 size_t clip_index_columns = 0;
609
610 if (columns_well_behaved) {
611 clip_index_columns = FindOtsuThresholdingIndex(histogram_columns);
612 column_threshold = (minmax_columns.second - minmax_columns.first) *
613 (clip_index_columns + 0.5f) / 255.0f + minmax_columns.first;
614 }
615
616 int auto_segmented_width = count_if(
617 column_profile.begin(), column_profile.end(),
618 std::bind2nd(std::greater<float>(), column_threshold));
619 int auto_segmented_height = count_if(
620 row_profile.begin(), row_profile.end(),
621 std::bind2nd(std::greater<float>(), row_threshold));
622
623 gfx::Size computed_size = AdjustClippingSizeToAspectRatio(
624 target_size,
625 gfx::Size(column_profile.size(), row_profile.size()),
626 gfx::Size(auto_segmented_width, auto_segmented_height));
627
628 // Apply thresholding.
629 if (rows_well_behaved) {
630 ConstrainedProfileThresholding(row_profile,
631 histogram_rows,
632 clip_index_rows,
633 row_threshold,
634 minmax_rows,
635 auto_segmented_height,
636 computed_size.height(),
637 included_rows);
638 } else {
639 // This is essentially an error condition, invoked when no segmentation was
640 // possible. This will result in applying a very low threshold and likely
641 // in producing a thumbnail which should get rejected.
642 included_rows->resize(row_profile.size());
643 for (size_t i = 0; i < row_profile.size(); ++i)
644 (*included_rows)[i] = row_profile[i] > row_threshold;
645 }
646
647 if (columns_well_behaved) {
648 ConstrainedProfileThresholding(column_profile,
649 histogram_columns,
650 clip_index_columns,
651 column_threshold,
652 minmax_columns,
653 auto_segmented_width,
654 computed_size.width(),
655 included_columns);
656 } else {
657 included_columns->resize(column_profile.size());
658 for (size_t i = 0; i < column_profile.size(); ++i)
659 (*included_columns)[i] = column_profile[i] > column_threshold;
660 }
661 }
662
ComputeDecimatedImage(const SkBitmap & bitmap,const std::vector<bool> & rows,const std::vector<bool> & columns)663 SkBitmap ComputeDecimatedImage(const SkBitmap& bitmap,
664 const std::vector<bool>& rows,
665 const std::vector<bool>& columns) {
666 SkAutoLockPixels source_lock(bitmap);
667 DCHECK(bitmap.getPixels());
668 DCHECK_GT(bitmap.bytesPerPixel(), 0);
669 DCHECK_EQ(bitmap.width(), static_cast<int>(columns.size()));
670 DCHECK_EQ(bitmap.height(), static_cast<int>(rows.size()));
671
672 unsigned target_row_count = std::count(rows.begin(), rows.end(), true);
673 unsigned target_column_count = std::count(
674 columns.begin(), columns.end(), true);
675
676 if (target_row_count == 0 || target_column_count == 0)
677 return SkBitmap(); // Not quite an error, so no DCHECK. Just return empty.
678
679 if (target_row_count == rows.size() && target_column_count == columns.size())
680 return SkBitmap(); // Equivalent of the situation above (empty target).
681
682 // Allocate the target image.
683 SkBitmap target;
684 target.setConfig(bitmap.config(), target_column_count, target_row_count);
685 target.allocPixels();
686
687 int target_row = 0;
688 for (int r = 0; r < bitmap.height(); ++r) {
689 if (!rows[r])
690 continue; // We can just skip this one.
691 uint8* src_row =
692 static_cast<uint8*>(bitmap.getPixels()) + r * bitmap.rowBytes();
693 uint8* insertion_target = static_cast<uint8*>(target.getPixels()) +
694 target_row * target.rowBytes();
695 int left_copy_pixel = -1;
696 for (int c = 0; c < bitmap.width(); ++c) {
697 if (left_copy_pixel < 0 && columns[c]) {
698 left_copy_pixel = c; // Next time we will start copying from here.
699 } else if (left_copy_pixel >= 0 && !columns[c]) {
700 // This closes a fragment we want to copy. We do it now.
701 size_t bytes_to_copy = (c - left_copy_pixel) * bitmap.bytesPerPixel();
702 memcpy(insertion_target,
703 src_row + left_copy_pixel * bitmap.bytesPerPixel(),
704 bytes_to_copy);
705 left_copy_pixel = -1;
706 insertion_target += bytes_to_copy;
707 }
708 }
709 // We can still have the tail end to process here.
710 if (left_copy_pixel >= 0) {
711 size_t bytes_to_copy =
712 (bitmap.width() - left_copy_pixel) * bitmap.bytesPerPixel();
713 memcpy(insertion_target,
714 src_row + left_copy_pixel * bitmap.bytesPerPixel(),
715 bytes_to_copy);
716 }
717 target_row++;
718 }
719
720 return target;
721 }
722
CreateRetargetedThumbnailImage(const SkBitmap & source_bitmap,const gfx::Size & target_size,float kernel_sigma)723 SkBitmap CreateRetargetedThumbnailImage(
724 const SkBitmap& source_bitmap,
725 const gfx::Size& target_size,
726 float kernel_sigma) {
727 // First thing we need for this method is to color-reduce the source_bitmap.
728 SkBitmap reduced_color;
729 reduced_color.setConfig(
730 SkBitmap::kA8_Config, source_bitmap.width(), source_bitmap.height());
731 reduced_color.allocPixels();
732
733 if (!color_utils::ComputePrincipalComponentImage(source_bitmap,
734 &reduced_color)) {
735 // CCIR601 luminance conversion vector.
736 gfx::Vector3dF transform(0.299f, 0.587f, 0.114f);
737 if (!color_utils::ApplyColorReduction(
738 source_bitmap, transform, true, &reduced_color)) {
739 DLOG(WARNING) << "Failed to compute luminance image from a screenshot. "
740 << "Cannot compute retargeted thumbnail.";
741 return SkBitmap();
742 }
743 DLOG(WARNING) << "Could not compute principal color image for a thumbnail. "
744 << "Using luminance instead.";
745 }
746
747 // Turn 'color-reduced' image into the 'energy' image.
748 ApplyGaussianGradientMagnitudeFilter(&reduced_color, kernel_sigma);
749
750 // Extract vertical and horizontal projection of image features.
751 std::vector<float> row_profile;
752 std::vector<float> column_profile;
753 ExtractImageProfileInformation(reduced_color,
754 gfx::Rect(reduced_color.width(),
755 reduced_color.height()),
756 target_size,
757 true,
758 &row_profile,
759 &column_profile);
760
761 std::vector<bool> included_rows, included_columns;
762 ConstrainedProfileSegmentation(row_profile,
763 column_profile,
764 target_size,
765 &included_rows,
766 &included_columns);
767
768 // Use the original image and computed inclusion vectors to create a resized
769 // image.
770 return ComputeDecimatedImage(source_bitmap, included_rows, included_columns);
771 }
772
773 } // thumbnailing_utils
774