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

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

Issue 2690513002: Port Android palette API for deriving a prominent color from an image. (Closed)
Patch Set: another comment update Created 3 years, 9 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
« no previous file with comments | « ui/gfx/color_analysis.h ('k') | ui/gfx/color_analysis_unittest.cc » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "ui/gfx/color_analysis.h" 5 #include "ui/gfx/color_analysis.h"
6 6
7 #include <limits.h> 7 #include <limits.h>
8 #include <stdint.h> 8 #include <stdint.h>
9 9
10 #include <algorithm> 10 #include <algorithm>
11 #include <cmath>
11 #include <limits> 12 #include <limits>
13 #include <map>
12 #include <memory> 14 #include <memory>
15 #include <queue>
13 #include <vector> 16 #include <vector>
14 17
15 #include "base/logging.h" 18 #include "base/logging.h"
16 #include "third_party/skia/include/core/SkBitmap.h" 19 #include "third_party/skia/include/core/SkBitmap.h"
17 #include "third_party/skia/include/core/SkUnPreMultiply.h" 20 #include "third_party/skia/include/core/SkUnPreMultiply.h"
18 #include "ui/gfx/codec/png_codec.h" 21 #include "ui/gfx/codec/png_codec.h"
22 #include "ui/gfx/color_palette.h"
19 #include "ui/gfx/color_utils.h" 23 #include "ui/gfx/color_utils.h"
24 #include "ui/gfx/range/range.h"
20 25
21 namespace color_utils { 26 namespace color_utils {
22 namespace { 27 namespace {
23 28
24 // RGBA KMean Constants 29 // RGBA KMean Constants
25 const uint32_t kNumberOfClusters = 4; 30 const uint32_t kNumberOfClusters = 4;
26 const int kNumberOfIterations = 50; 31 const int kNumberOfIterations = 50;
27 32
28 const HSL kDefaultLowerHSLBound = {-1, -1, 0.15}; 33 const HSL kDefaultLowerHSLBound = {-1, -1, 0.15};
29 const HSL kDefaultUpperHSLBound = {-1, -1, 0.85}; 34 const HSL kDefaultUpperHSLBound = {-1, -1, 0.85};
(...skipping 106 matching lines...) Expand 10 before | Expand all | Expand 10 after
136 // approximately 10 microseconds for a 16x16 icon on an Intel Core i5. 141 // approximately 10 microseconds for a 16x16 icon on an Intel Core i5.
137 void UnPreMultiply(const SkBitmap& bitmap, uint32_t* buffer, int buffer_size) { 142 void UnPreMultiply(const SkBitmap& bitmap, uint32_t* buffer, int buffer_size) {
138 SkAutoLockPixels auto_lock(bitmap); 143 SkAutoLockPixels auto_lock(bitmap);
139 uint32_t* in = static_cast<uint32_t*>(bitmap.getPixels()); 144 uint32_t* in = static_cast<uint32_t*>(bitmap.getPixels());
140 uint32_t* out = buffer; 145 uint32_t* out = buffer;
141 int pixel_count = std::min(bitmap.width() * bitmap.height(), buffer_size); 146 int pixel_count = std::min(bitmap.width() * bitmap.height(), buffer_size);
142 for (int i = 0; i < pixel_count; ++i) 147 for (int i = 0; i < pixel_count; ++i)
143 *out++ = SkUnPreMultiply::PMColorToColor(*in++); 148 *out++ = SkUnPreMultiply::PMColorToColor(*in++);
144 } 149 }
145 150
151 // Prominent color utilities ---------------------------------------------------
152
153 // A color value with an associated weight.
154 struct WeightedColor {
155 WeightedColor(SkColor color, uint64_t weight)
156 : color(color), weight(weight) {}
157
158 SkColor color;
159
160 // The weight correlates to a count, so it should be 1 or greater.
161 uint64_t weight;
162 };
163
164 // A |ColorBox| represents a 3-dimensional region in a color space (an ordered
165 // set of colors). It is a range in the ordered set, with a low index and a high
166 // index. The diversity (volume) of the box is computed by looking at the range
167 // of color values it spans, where r, g, and b components are considered
168 // separately.
169 class ColorBox {
170 public:
171 explicit ColorBox(std::vector<SkColor>* color_space)
172 : ColorBox(color_space, gfx::Range(0, color_space->size())) {}
173 ColorBox(const ColorBox& other) = default;
174 ColorBox& operator=(const ColorBox& other) = default;
175 ~ColorBox() {}
176
177 // Can't split if there's only one color in the box.
178 bool CanSplit() const { return color_range_.length() > 1; }
179
180 // Splits |this| in two and returns the other half.
181 ColorBox Split() {
182 // Calculate which component has the largest range...
183 const uint8_t r_dimension = max_r_ - min_r_;
184 const uint8_t g_dimension = max_g_ - min_g_;
185 const uint8_t b_dimension = max_b_ - min_b_;
186 const uint8_t long_dimension =
187 std::max({r_dimension, g_dimension, b_dimension});
188 const enum {
189 RED,
190 GREEN,
191 BLUE,
192 } channel = long_dimension == r_dimension
193 ? RED
194 : long_dimension == g_dimension ? GREEN : BLUE;
195
196 // ... and sort along that axis.
197 auto sort_function = [channel](SkColor a, SkColor b) {
198 switch (channel) {
199 case RED:
200 return SkColorGetR(a) < SkColorGetR(b);
201 case GREEN:
202 return SkColorGetG(a) < SkColorGetG(b);
203 case BLUE:
204 return SkColorGetB(a) < SkColorGetB(b);
205 }
206 NOTREACHED();
207 return SkColorGetB(a) < SkColorGetB(b);
208 };
209 // Just the portion of |color_space_| that's covered by this box should be
210 // sorted.
211 std::sort(color_space_->begin() + color_range_.start(),
212 color_space_->begin() + color_range_.end(), sort_function);
213
214 // Split at the first color value that's not less than the midpoint (mean of
215 // the start and values).
216 uint32_t split_point = color_range_.end() - 1;
217 for (uint32_t i = color_range_.start() + 1; i < color_range_.end() - 1;
218 ++i) {
219 bool past_midpoint = false;
220 switch (channel) {
221 case RED:
222 past_midpoint =
223 static_cast<uint8_t>(SkColorGetR((*color_space_)[i])) >
224 (min_r_ + max_r_) / 2;
225 break;
226 case GREEN:
227 past_midpoint =
228 static_cast<uint8_t>(SkColorGetG((*color_space_)[i])) >
229 (min_g_ + max_g_) / 2;
230 break;
231 case BLUE:
232 past_midpoint =
233 static_cast<uint8_t>(SkColorGetB((*color_space_)[i])) >
234 (min_b_ + max_b_) / 2;
235 break;
236 }
237 if (past_midpoint) {
238 split_point = i;
239 break;
240 }
241 }
242
243 // Break off half and return it.
244 gfx::Range other_range = color_range_;
245 other_range.set_end(split_point);
246 ColorBox other_box(color_space_, other_range);
247
248 // Keep the other half in |this| and recalculate our color bounds.
249 color_range_.set_start(split_point);
250 RecomputeBounds();
251 return other_box;
252 }
253
254 // Returns the average color of this box, weighted by its popularity in
255 // |color_counts|.
256 WeightedColor GetWeightedAverageColor(
257 const std::map<SkColor, int>& color_counts) const {
258 uint64_t sum_r = 0;
259 uint64_t sum_g = 0;
260 uint64_t sum_b = 0;
261 uint64_t total_count_in_box = 0;
262
263 for (uint32_t i = color_range_.start(); i < color_range_.end(); ++i) {
264 const SkColor color = (*color_space_)[i];
265 const auto color_count_iter = color_counts.find(color);
266 DCHECK(color_count_iter != color_counts.end());
267 const int color_count = color_count_iter->second;
268
269 total_count_in_box += color_count;
270 sum_r += color_count * SkColorGetR(color);
271 sum_g += color_count * SkColorGetG(color);
272 sum_b += color_count * SkColorGetB(color);
273 }
274
275 return WeightedColor(
276 SkColorSetRGB(
277 std::round(static_cast<double>(sum_r) / total_count_in_box),
278 std::round(static_cast<double>(sum_g) / total_count_in_box),
279 std::round(static_cast<double>(sum_b) / total_count_in_box)),
280 total_count_in_box);
281 }
282
283 static bool CompareByVolume(const ColorBox& a, const ColorBox& b) {
284 return a.volume_ < b.volume_;
285 }
286
287 private:
288 ColorBox(std::vector<SkColor>* color_space, const gfx::Range& color_range)
289 : color_space_(color_space), color_range_(color_range) {
290 RecomputeBounds();
291 }
292
293 void RecomputeBounds() {
294 DCHECK(!color_range_.is_reversed());
295 DCHECK(!color_range_.is_empty());
296 DCHECK_LE(color_range_.end(), color_space_->size());
297
298 min_r_ = 0xFF;
299 min_g_ = 0xFF;
300 min_b_ = 0xFF;
301 max_r_ = 0;
302 max_g_ = 0;
303 max_b_ = 0;
304
305 for (uint32_t i = color_range_.start(); i < color_range_.end(); ++i) {
306 SkColor color = (*color_space_)[i];
307 min_r_ = std::min<uint8_t>(SkColorGetR(color), min_r_);
308 min_g_ = std::min<uint8_t>(SkColorGetG(color), min_g_);
309 min_b_ = std::min<uint8_t>(SkColorGetB(color), min_b_);
310 max_r_ = std::max<uint8_t>(SkColorGetR(color), max_r_);
311 max_g_ = std::max<uint8_t>(SkColorGetG(color), max_g_);
312 max_b_ = std::max<uint8_t>(SkColorGetB(color), max_b_);
313 }
314
315 volume_ =
316 (max_r_ - min_r_ + 1) * (max_g_ - min_g_ + 1) * (max_b_ - min_b_ + 1);
317 }
318
319 // The set of colors of which this box captures a subset. This vector is not
320 // owned but may be modified during the split operation.
321 std::vector<SkColor>* color_space_;
322
323 // The range of indexes into |color_space_| that are part of this box.
324 gfx::Range color_range_;
325
326 // Cached min and max color component values for the colors in this box.
327 uint8_t min_r_ = 0;
328 uint8_t min_g_ = 0;
329 uint8_t min_b_ = 0;
330 uint8_t max_r_ = 0;
331 uint8_t max_g_ = 0;
332 uint8_t max_b_ = 0;
333
334 // Cached volume value, which is the product of the range of each color
335 // component.
336 int volume_ = 0;
337 };
338
339 // Some color values should be ignored for the purposes of determining prominent
340 // colors.
341 bool IsInterestingColor(SkColor color) {
342 const float average_channel_value =
343 (SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color)) / 3.0f;
344 // If a color is too close to white or black, ignore it.
345 if (average_channel_value >= 237 || average_channel_value <= 22)
346 return false;
347
348 HSL hsl;
349 SkColorToHSL(color, &hsl);
350 return !(hsl.h >= 0.028f && hsl.h <= 0.10f && hsl.s <= 0.82f);
351 }
352
353 // This algorithm is a port of Android's Palette API. Compare to package
354 // android.support.v7.graphics and see that code for additional high-level
355 // explanation of this algorithm. There are some minor differences:
356 // * This code doesn't exclude the same color from being used for
357 // different color profiles.
358 // * This code doesn't try to heuristically derive missing colors from
359 // existing colors.
360 SkColor CalculateProminentColor(const SkBitmap& bitmap,
361 const HSL& lower_bound,
362 const HSL& upper_bound,
363 const HSL& goal) {
364 DCHECK(!bitmap.empty());
365 DCHECK(!bitmap.isNull());
366
367 SkAutoLockPixels auto_lock(bitmap);
368 const uint32_t* pixels = static_cast<uint32_t*>(bitmap.getPixels());
369 int pixel_count = bitmap.width() * bitmap.height();
370 std::map<SkColor, int> color_counts;
371
372 // First extract all colors into counts.
373 for (int i = 0; i < pixel_count; ++i) {
374 // SkBitmap uses pre-multiplied alpha but the prominent color algorithm
375 // needs non-pre-multiplied alpha.
376 const SkColor pixel = SkUnPreMultiply::PMColorToColor(pixels[i]);
377 if (SkColorGetA(pixel) == SK_AlphaTRANSPARENT)
378 continue;
379
380 color_counts[pixel]++;
381 }
382
383 // Now throw out some uninteresting colors.
384 std::vector<SkColor> interesting_colors;
385 interesting_colors.reserve(color_counts.size());
386 for (auto color_count : color_counts) {
387 SkColor color = color_count.first;
388 if (IsInterestingColor(color))
389 interesting_colors.push_back(color);
390 }
391
392 if (interesting_colors.empty())
393 return SK_ColorTRANSPARENT;
394
395 // Group the colors into "boxes" and repeatedly split the most voluminous box.
396 // We stop the process when a box can no longer be split (there's only one
397 // color in it) or when the number of color boxes reaches 12. As per the
398 // Android docs,
399 //
400 // For landscapes, good values are in the range 12-16. For images which
401 // are largely made up of people's faces then this value should be increased
402 // to 24-32.
403 const int kMaxColors = 12;
404 // Boxes are sorted by volume with the most voluminous at the front of the PQ.
405 std::priority_queue<ColorBox, std::vector<ColorBox>,
406 bool (*)(const ColorBox&, const ColorBox&)>
407 boxes(&ColorBox::CompareByVolume);
408 boxes.emplace(&interesting_colors);
409 while (boxes.size() < kMaxColors) {
410 auto box = boxes.top();
411 if (!box.CanSplit())
412 break;
413 boxes.pop();
414 boxes.push(box.Split());
415 boxes.push(box);
416 }
417
418 // Now extract a single color to represent each box. This is the average color
419 // in the box, weighted by the frequency of that color in the source image.
420 std::vector<WeightedColor> box_colors;
421 uint64_t max_weight = 0;
422 while (!boxes.empty()) {
423 box_colors.push_back(boxes.top().GetWeightedAverageColor(color_counts));
424 boxes.pop();
425 max_weight = std::max(max_weight, box_colors.back().weight);
426 }
427
428 // Given these box average colors, find the best one for the desired color
429 // profile. "Best" in this case means the color which fits in the provided
430 // bounds and comes closest to |goal|. It's possible that no color will fit in
431 // the provided bounds, in which case we'll return an empty color.
432 double best_suitability = 0;
433 SkColor best_color = SK_ColorTRANSPARENT;
434 for (const auto& box_color : box_colors) {
435 HSL hsl;
436 SkColorToHSL(box_color.color, &hsl);
437 if (!IsWithinHSLRange(hsl, lower_bound, upper_bound))
438 continue;
439
440 double suitability =
441 (1 - std::abs(hsl.s - goal.s)) * 3 +
442 (1 - std::abs(hsl.l - goal.l)) * 6.5 +
443 (box_color.weight / static_cast<float>(max_weight)) * 0.5;
444 if (suitability > best_suitability) {
445 best_suitability = suitability;
446 best_color = box_color.color;
447 }
448 }
449
450 return best_color;
451 }
452
146 } // namespace 453 } // namespace
147 454
148 KMeanImageSampler::KMeanImageSampler() { 455 KMeanImageSampler::KMeanImageSampler() {
149 } 456 }
150 457
151 KMeanImageSampler::~KMeanImageSampler() { 458 KMeanImageSampler::~KMeanImageSampler() {
152 } 459 }
153 460
154 GridSampler::GridSampler() : calls_(0) { 461 GridSampler::GridSampler() : calls_(0) {
155 } 462 }
(...skipping 238 matching lines...) Expand 10 before | Expand all | Expand 10 after
394 const HSL& upper_bound, 701 const HSL& upper_bound,
395 KMeanImageSampler* sampler) { 702 KMeanImageSampler* sampler) {
396 // SkBitmap uses pre-multiplied alpha but the KMean clustering function 703 // SkBitmap uses pre-multiplied alpha but the KMean clustering function
397 // above uses non-pre-multiplied alpha. Transform the bitmap before we 704 // above uses non-pre-multiplied alpha. Transform the bitmap before we
398 // analyze it because the function reads each pixel multiple times. 705 // analyze it because the function reads each pixel multiple times.
399 int pixel_count = bitmap.width() * bitmap.height(); 706 int pixel_count = bitmap.width() * bitmap.height();
400 std::unique_ptr<uint32_t[]> image(new uint32_t[pixel_count]); 707 std::unique_ptr<uint32_t[]> image(new uint32_t[pixel_count]);
401 UnPreMultiply(bitmap, image.get(), pixel_count); 708 UnPreMultiply(bitmap, image.get(), pixel_count);
402 709
403 return CalculateKMeanColorOfBuffer(reinterpret_cast<uint8_t*>(image.get()), 710 return CalculateKMeanColorOfBuffer(reinterpret_cast<uint8_t*>(image.get()),
404 bitmap.width(), 711 bitmap.width(), bitmap.height(),
405 bitmap.height(), 712 lower_bound, upper_bound, sampler);
406 lower_bound,
407 upper_bound,
408 sampler);
409 } 713 }
410 714
411 SkColor CalculateKMeanColorOfBitmap(const SkBitmap& bitmap) { 715 SkColor CalculateKMeanColorOfBitmap(const SkBitmap& bitmap) {
412 GridSampler sampler; 716 GridSampler sampler;
413 return CalculateKMeanColorOfBitmap( 717 return CalculateKMeanColorOfBitmap(
414 bitmap, kDefaultLowerHSLBound, kDefaultUpperHSLBound, &sampler); 718 bitmap, kDefaultLowerHSLBound, kDefaultUpperHSLBound, &sampler);
415 } 719 }
416 720
721 SkColor CalculateProminentColorOfBitmap(const SkBitmap& bitmap,
722 LumaRange luma,
723 SaturationRange saturation) {
724 if (bitmap.empty() || bitmap.isNull())
725 return SK_ColorTRANSPARENT;
726
727 // The hue is not relevant to our bounds or goal colors.
728 HSL lower_bound = {
729 -1,
730 };
731 HSL upper_bound = {
732 -1,
733 };
734 HSL goal = {
735 -1,
736 };
737
738 switch (luma) {
739 case LumaRange::LIGHT:
740 lower_bound.l = 0.55f;
741 upper_bound.l = 1;
742 goal.l = 0.74f;
743 break;
744 case LumaRange::NORMAL:
745 lower_bound.l = 0.3f;
746 upper_bound.l = 0.7f;
747 goal.l = 0.5f;
748 break;
749 case LumaRange::DARK:
750 lower_bound.l = 0;
751 upper_bound.l = 0.45f;
752 goal.l = 0.26f;
753 break;
754 }
755
756 switch (saturation) {
757 case SaturationRange::VIBRANT:
758 lower_bound.s = 0.35f;
759 upper_bound.s = 1;
760 goal.s = 1;
761 break;
762 case SaturationRange::MUTED:
763 lower_bound.s = 0;
764 upper_bound.s = 0.4f;
765 goal.s = 0.3f;
766 break;
767 }
768
769 return CalculateProminentColor(bitmap, lower_bound, upper_bound, goal);
770 }
771
417 gfx::Matrix3F ComputeColorCovariance(const SkBitmap& bitmap) { 772 gfx::Matrix3F ComputeColorCovariance(const SkBitmap& bitmap) {
418 // First need basic stats to normalize each channel separately. 773 // First need basic stats to normalize each channel separately.
419 SkAutoLockPixels bitmap_lock(bitmap); 774 SkAutoLockPixels bitmap_lock(bitmap);
420 gfx::Matrix3F covariance = gfx::Matrix3F::Zeros(); 775 gfx::Matrix3F covariance = gfx::Matrix3F::Zeros();
421 if (!bitmap.getPixels()) 776 if (!bitmap.getPixels())
422 return covariance; 777 return covariance;
423 778
424 // Assume ARGB_8888 format. 779 // Assume ARGB_8888 format.
425 DCHECK(bitmap.colorType() == kN32_SkColorType); 780 DCHECK(bitmap.colorType() == kN32_SkColorType);
426 781
(...skipping 149 matching lines...) Expand 10 before | Expand all | Expand 10 after
576 gfx::Matrix3F covariance = ComputeColorCovariance(source_bitmap); 931 gfx::Matrix3F covariance = ComputeColorCovariance(source_bitmap);
577 gfx::Matrix3F eigenvectors = gfx::Matrix3F::Zeros(); 932 gfx::Matrix3F eigenvectors = gfx::Matrix3F::Zeros();
578 gfx::Vector3dF eigenvals = covariance.SolveEigenproblem(&eigenvectors); 933 gfx::Vector3dF eigenvals = covariance.SolveEigenproblem(&eigenvectors);
579 gfx::Vector3dF principal = eigenvectors.get_column(0); 934 gfx::Vector3dF principal = eigenvectors.get_column(0);
580 if (eigenvals == gfx::Vector3dF() || principal == gfx::Vector3dF()) 935 if (eigenvals == gfx::Vector3dF() || principal == gfx::Vector3dF())
581 return false; // This may happen for some edge cases. 936 return false; // This may happen for some edge cases.
582 return ApplyColorReduction(source_bitmap, principal, true, target_bitmap); 937 return ApplyColorReduction(source_bitmap, principal, true, target_bitmap);
583 } 938 }
584 939
585 } // color_utils 940 } // color_utils
OLDNEW
« no previous file with comments | « ui/gfx/color_analysis.h ('k') | ui/gfx/color_analysis_unittest.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698