OLD | NEW |
---|---|
(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 | |
OLD | NEW |