Chromium Code Reviews| 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 |