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

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: Created 3 years, 10 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
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 (equivalent to the popularity of that
154 // color).
155 struct WeightedColor {
156 SkColor color;
157 int weight;
158 };
159
160 // A |ColorBox| represents a region in a color space (an ordered set of colors).
161 // It is a range in the ordered set, with a low index and a high index. The
162 // diversity (volume) of the box is computed by looking at the range of color
163 // values it spans, where r, g, and b components are considered separately.
164 class ColorBox {
165 public:
166 explicit ColorBox(std::vector<SkColor>* color_space)
167 : ColorBox(color_space, gfx::Range(0, color_space->size() - 1)) {}
168 ColorBox(const ColorBox& other) = default;
169 ColorBox& operator=(const ColorBox& other) = default;
170 ~ColorBox() {}
171
172 // Can't split if there's only one color in the box.
173 bool CanSplit() { return !color_range_.is_empty(); }
sadrul 2017/02/10 19:36:44 const
Evan Stade 2017/02/13 19:01:13 Done.
174
175 // Splits |this| in two and returns the other half.
176 ColorBox Split() {
sadrul 2017/02/10 19:36:44 What happens if Split() is called when this has ju
Evan Stade 2017/02/13 19:01:13 It's an error to do that. You'd hit the dcheck in
177 // Calculate which component has the largest range...
178 int r_dimension = max_r_ - min_r_;
179 int g_dimension = max_g_ - min_g_;
180 int b_dimension = max_b_ - min_b_;
181 int long_dimension = std::max({r_dimension, g_dimension, b_dimension});
182
183 // ... and sort along that axis. Split at the median of that axis.
184 std::function<bool(SkColor, SkColor)> sort_function = [](
185 SkColor a, SkColor b) { return SkColorGetR(a) < SkColorGetR(b); };
sadrul 2017/02/10 19:36:44 https://chromium-cpp.appspot.com/ claims this is b
Evan Stade 2017/02/13 19:01:13 well that's pretty annoying as std::sort doesn't t
186 int median = (min_r_ + max_r_) / 2;
187 if (g_dimension == long_dimension) {
188 sort_function = [](SkColor a, SkColor b) {
189 return SkColorGetG(a) < SkColorGetG(b);
190 };
191 median = (min_g_ + max_g_) / 2;
192 } else if (b_dimension == long_dimension) {
193 sort_function = [](SkColor a, SkColor b) {
194 return SkColorGetB(a) < SkColorGetB(b);
195 };
196 median = (min_b_ + max_b_) / 2;
197 }
198
199 // Just the portion of |color_space_| that's covered by this box should be
200 // sorted.
201 std::sort(color_space_->begin() + color_range_.start(),
202 color_space_->begin() + color_range_.end(), sort_function);
203
204 // Split at the first color value that's not less than the median.
205 uint32_t split_point = color_range_.end();
206 for (auto i = color_range_.start() + 1; i < color_range_.end(); ++i) {
sadrul 2017/02/10 19:36:44 Just use uint32_t here?
Evan Stade 2017/02/13 19:01:13 Done.
207 if (!sort_function(color_space_->at(i),
208 SkColorSetRGB(median, median, median))) {
209 split_point = i;
210 break;
211 }
212 }
213
214 // Break off half and return it.
215 gfx::Range other_range = color_range_;
216 other_range.set_end(split_point - 1);
217 ColorBox other_box(color_space_, other_range);
218
219 // Keep the other half in |this| and recalculate our color bounds.
220 color_range_.set_start(split_point);
221 RecomputeBounds();
222 return other_box;
223 }
224
225 // Returns the average color of this box, weighted by its popularity in
226 // |color_counts|.
227 WeightedColor GetWeightedAverageColor(
228 const std::map<SkColor, int>& color_counts) const {
229 int sum_r = 0;
230 int sum_g = 0;
231 int sum_b = 0;
232 int total_count_in_box = 0;
233
234 for (auto i = color_range_.start(); i <= color_range_.end(); ++i) {
sadrul 2017/02/10 19:36:44 uint32_t
Evan Stade 2017/02/13 19:01:13 Done.
235 const SkColor color = color_space_->at(i);
236 auto color_count_iter = color_counts.find(color);
237 DCHECK(color_count_iter != color_counts.end());
238 const int color_count = color_count_iter->second;
239
240 total_count_in_box += color_count;
241 sum_r += color_count * SkColorGetR(color);
242 sum_g += color_count * SkColorGetG(color);
243 sum_b += color_count * SkColorGetB(color);
244 }
245
246 WeightedColor weighted_color;
247 weighted_color.weight = total_count_in_box;
248 weighted_color.color = SkColorSetRGB(
249 std::round(static_cast<float>(sum_r) / total_count_in_box),
250 std::round(static_cast<float>(sum_g) / total_count_in_box),
251 std::round(static_cast<float>(sum_b) / total_count_in_box));
252 return weighted_color;
253 }
254
255 // Comparisons are done by volume.
256 bool operator<(const ColorBox& other) const {
257 return volume_ < other.volume_;
258 }
259
260 private:
261 ColorBox(std::vector<SkColor>* color_space, const gfx::Range& color_range)
262 : color_space_(color_space), color_range_(color_range) {
263 RecomputeBounds();
264 }
265
266 void RecomputeBounds() {
267 DCHECK(!color_range_.is_reversed());
268 DCHECK_LT(color_range_.end(), color_space_->size());
269
270 min_r_ = 0xFF;
271 min_g_ = 0xFF;
272 min_b_ = 0xFF;
273 max_r_ = 0;
274 max_g_ = 0;
275 max_b_ = 0;
276
277 for (auto i = color_range_.start(); i < color_range_.end(); ++i) {
sadrul 2017/02/10 19:36:44 uint32_t
Evan Stade 2017/02/13 19:01:13 Done.
278 SkColor color = color_space_->at(i);
279 min_r_ = std::min<uint8_t>(SkColorGetR(color), min_r_);
280 min_g_ = std::min<uint8_t>(SkColorGetG(color), min_g_);
281 min_b_ = std::min<uint8_t>(SkColorGetB(color), min_b_);
282 max_r_ = std::max<uint8_t>(SkColorGetR(color), max_r_);
283 max_g_ = std::max<uint8_t>(SkColorGetG(color), max_g_);
284 max_b_ = std::max<uint8_t>(SkColorGetB(color), max_b_);
285 }
286
287 volume_ =
288 (max_r_ - min_r_ + 1) * (max_g_ - min_g_ + 1) * (max_b_ - min_b_ + 1);
289 }
290
291 // The set of colors of which this box captures a subset. This vector is not
292 // owned but may be modified during the split operation.
293 std::vector<SkColor>* color_space_;
294
295 // The range of indexes into |color_space_| that are part of this box.
296 gfx::Range color_range_;
297
298 // Cached min and max color component values for the colors in this box.
299 uint8_t min_r_ = 0;
300 uint8_t min_g_ = 0;
301 uint8_t min_b_ = 0;
302 uint8_t max_r_ = 0;
303 uint8_t max_g_ = 0;
304 uint8_t max_b_ = 0;
305
306 // Cached volume value, which is the product of the range of each color
307 // component.
308 int volume_ = 0;
309 };
310
311 // Some color values should be ignored for the purposes of determining prominent
312 // colors.
313 bool IsInterestingColor(SkColor color) {
314 float average_channel_value =
315 (SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color)) / 3.0f;
316 // If a color is too close to white or black, ignore it.
317 if (average_channel_value >= 237 || average_channel_value <= 22)
318 return false;
319
320 // Also rule out skin tones.
321 HSL hsl;
322 SkColorToHSL(color, &hsl);
323 return !(hsl.h >= 0.028f && hsl.h <= 0.10f && hsl.s <= 0.82f);
324 }
325
326 // This algorithm is a port of Android's Palette API. Compare to package
327 // android.support.v7.graphics and see that code for additional high-level
328 // explanation of this algorithm. There are some minor differences:
329 // * This code doesn't exclude the same color from being used for
330 // different color profiles.
331 // * This code doesn't try to heuristically derive missing colors from
332 // existing colors.
333 SkColor CalculateProminentColorOfBuffer(uint8_t* decoded_data,
334 int img_width,
335 int img_height,
336 const HSL& lower_bound,
337 const HSL& upper_bound,
338 const HSL& goal) {
339 DCHECK_GT(img_width, 0);
340 DCHECK_GT(img_height, 0);
341 std::map<SkColor, int> color_counts;
342
343 // First extract all colors.
344 for (int i = 0; i < img_width * img_height; ++i) {
345 // TODO(port): This code assumes the CPU architecture is little-endian.
346 uint8_t b = decoded_data[i * 4];
347 uint8_t g = decoded_data[i * 4 + 1];
348 uint8_t r = decoded_data[i * 4 + 2];
349 uint8_t a = decoded_data[i * 4 + 3];
350 if (a == SK_AlphaTRANSPARENT)
351 continue;
352
353 SkColor pixel = SkColorSetRGB(r, g, b);
354 color_counts[pixel]++;
355 }
356
357 // Now throw out some uninteresting colors.
358 std::vector<SkColor> interesting_colors;
359 for (auto color_count : color_counts) {
360 SkColor color = color_count.first;
361 if (IsInterestingColor(color))
362 interesting_colors.push_back(color);
363 }
364
365 if (interesting_colors.empty())
366 return SK_ColorTRANSPARENT;
367
368 // Group the colors into "boxes" and repeatedly split the most voluminous box.
369 // We stop the process when a box can no longer be split (there's only one
370 // color in it) or when the number of color boxes reaches 12. As per the
371 // Android docs,
372 //
373 // For landscapes, good values are in the range 12-16. For images which
374 // are largely made up of people's faces then this value should be increased
375 // to 24-32.
376 const int kMaxColors = 12;
377 // Boxes are sorted by volume with the most voluminous at the front of the PQ.
378 std::priority_queue<ColorBox> boxes;
379 boxes.emplace(&interesting_colors);
380 while (boxes.size() < kMaxColors) {
381 auto box = boxes.top();
382 if (!box.CanSplit())
383 break;
384 boxes.pop();
385 boxes.push(box.Split());
386 boxes.push(box);
387 }
388
389 // Now extract a single color to represent each box. This is the average color
390 // in the box, weighted by the frequency of that color in the source image.
391 std::vector<WeightedColor> box_colors;
392 int max_weight = 0;
393 while (!boxes.empty()) {
394 box_colors.push_back(boxes.top().GetWeightedAverageColor(color_counts));
395 boxes.pop();
396 max_weight = std::max(max_weight, box_colors.back().weight);
397 }
398
399 // Given these box average colors, find the best one for the desired color
400 // profile. "Best" in this case means the color which fits in the provided
401 // bounds and comes closest to |goal|. It's possible that no color will fit in
402 // the provided bounds, in which case we'll return an empty color.
403 double best_suitability = 0;
404 SkColor best_color = SK_ColorTRANSPARENT;
405 for (const auto& box_color : box_colors) {
406 HSL hsl;
407 SkColorToHSL(box_color.color, &hsl);
408 if (!IsWithinHSLRange(hsl, lower_bound, upper_bound))
409 continue;
410
411 double suitability =
412 (1 - std::abs(hsl.s - goal.s)) * 3 +
413 (1 - std::abs(hsl.l - goal.l)) * 6.5 +
414 (box_color.weight / static_cast<float>(max_weight)) * 0.5;
415 if (suitability > best_suitability) {
416 best_suitability = suitability;
417 best_color = box_color.color;
418 }
419 }
420
421 return best_color;
422 }
423
146 } // namespace 424 } // namespace
147 425
148 KMeanImageSampler::KMeanImageSampler() { 426 KMeanImageSampler::KMeanImageSampler() {
149 } 427 }
150 428
151 KMeanImageSampler::~KMeanImageSampler() { 429 KMeanImageSampler::~KMeanImageSampler() {
152 } 430 }
153 431
154 GridSampler::GridSampler() : calls_(0) { 432 GridSampler::GridSampler() : calls_(0) {
155 } 433 }
(...skipping 238 matching lines...) Expand 10 before | Expand all | Expand 10 after
394 const HSL& upper_bound, 672 const HSL& upper_bound,
395 KMeanImageSampler* sampler) { 673 KMeanImageSampler* sampler) {
396 // SkBitmap uses pre-multiplied alpha but the KMean clustering function 674 // SkBitmap uses pre-multiplied alpha but the KMean clustering function
397 // above uses non-pre-multiplied alpha. Transform the bitmap before we 675 // above uses non-pre-multiplied alpha. Transform the bitmap before we
398 // analyze it because the function reads each pixel multiple times. 676 // analyze it because the function reads each pixel multiple times.
399 int pixel_count = bitmap.width() * bitmap.height(); 677 int pixel_count = bitmap.width() * bitmap.height();
400 std::unique_ptr<uint32_t[]> image(new uint32_t[pixel_count]); 678 std::unique_ptr<uint32_t[]> image(new uint32_t[pixel_count]);
401 UnPreMultiply(bitmap, image.get(), pixel_count); 679 UnPreMultiply(bitmap, image.get(), pixel_count);
402 680
403 return CalculateKMeanColorOfBuffer(reinterpret_cast<uint8_t*>(image.get()), 681 return CalculateKMeanColorOfBuffer(reinterpret_cast<uint8_t*>(image.get()),
404 bitmap.width(), 682 bitmap.width(), bitmap.height(),
405 bitmap.height(), 683 lower_bound, upper_bound, sampler);
406 lower_bound,
407 upper_bound,
408 sampler);
409 } 684 }
410 685
411 SkColor CalculateKMeanColorOfBitmap(const SkBitmap& bitmap) { 686 SkColor CalculateKMeanColorOfBitmap(const SkBitmap& bitmap) {
412 GridSampler sampler; 687 GridSampler sampler;
413 return CalculateKMeanColorOfBitmap( 688 return CalculateKMeanColorOfBitmap(
414 bitmap, kDefaultLowerHSLBound, kDefaultUpperHSLBound, &sampler); 689 bitmap, kDefaultLowerHSLBound, kDefaultUpperHSLBound, &sampler);
415 } 690 }
416 691
692 SkColor CalculateProminentColorOfBitmap(const SkBitmap& bitmap,
693 int color_profile) {
694 // SkBitmap uses pre-multiplied alpha but the prominent color algorithm
695 // above uses non-pre-multiplied alpha. Transform the bitmap before we
696 // analyze it because the function reads each pixel multiple times.
697 int pixel_count = bitmap.width() * bitmap.height();
698 if (pixel_count == 0)
699 return SK_ColorTRANSPARENT;
700
701 std::unique_ptr<uint32_t[]> image(new uint32_t[pixel_count]);
702 UnPreMultiply(bitmap, image.get(), pixel_count);
703
704 // The hue is not relevant to our bounds or goal colors.
705 HSL lower_bound = {
706 -1,
707 };
708 HSL upper_bound = {
709 -1,
710 };
711 HSL goal = {
712 -1,
713 };
714
715 // Set the luma.
716 if (color_profile & LIGHT) {
717 lower_bound.l = 0.55f;
718 upper_bound.l = 1;
719 goal.l = 0.74f;
720 } else if (color_profile & NORMAL) {
721 lower_bound.l = 0.3f;
722 upper_bound.l = 0.7f;
723 goal.l = 0.5f;
724 } else {
725 DCHECK(color_profile & DARK);
726 lower_bound.l = 0;
727 upper_bound.l = 0.45f;
728 goal.l = 0.26f;
729 }
730
731 // Set the saturation.
732 if (color_profile & VIBRANT) {
733 lower_bound.s = 0.35f;
734 upper_bound.s = 1;
735 goal.s = 1;
736 } else {
737 DCHECK(color_profile & MUTED);
738 lower_bound.s = 0;
739 upper_bound.s = 0.4f;
740 goal.s = 0.3f;
741 }
742
743 return CalculateProminentColorOfBuffer(
744 reinterpret_cast<uint8_t*>(image.get()), bitmap.width(), bitmap.height(),
745 lower_bound, upper_bound, goal);
746 }
747
417 gfx::Matrix3F ComputeColorCovariance(const SkBitmap& bitmap) { 748 gfx::Matrix3F ComputeColorCovariance(const SkBitmap& bitmap) {
418 // First need basic stats to normalize each channel separately. 749 // First need basic stats to normalize each channel separately.
419 SkAutoLockPixels bitmap_lock(bitmap); 750 SkAutoLockPixels bitmap_lock(bitmap);
420 gfx::Matrix3F covariance = gfx::Matrix3F::Zeros(); 751 gfx::Matrix3F covariance = gfx::Matrix3F::Zeros();
421 if (!bitmap.getPixels()) 752 if (!bitmap.getPixels())
422 return covariance; 753 return covariance;
423 754
424 // Assume ARGB_8888 format. 755 // Assume ARGB_8888 format.
425 DCHECK(bitmap.colorType() == kN32_SkColorType); 756 DCHECK(bitmap.colorType() == kN32_SkColorType);
426 757
(...skipping 149 matching lines...) Expand 10 before | Expand all | Expand 10 after
576 gfx::Matrix3F covariance = ComputeColorCovariance(source_bitmap); 907 gfx::Matrix3F covariance = ComputeColorCovariance(source_bitmap);
577 gfx::Matrix3F eigenvectors = gfx::Matrix3F::Zeros(); 908 gfx::Matrix3F eigenvectors = gfx::Matrix3F::Zeros();
578 gfx::Vector3dF eigenvals = covariance.SolveEigenproblem(&eigenvectors); 909 gfx::Vector3dF eigenvals = covariance.SolveEigenproblem(&eigenvectors);
579 gfx::Vector3dF principal = eigenvectors.get_column(0); 910 gfx::Vector3dF principal = eigenvectors.get_column(0);
580 if (eigenvals == gfx::Vector3dF() || principal == gfx::Vector3dF()) 911 if (eigenvals == gfx::Vector3dF() || principal == gfx::Vector3dF())
581 return false; // This may happen for some edge cases. 912 return false; // This may happen for some edge cases.
582 return ApplyColorReduction(source_bitmap, principal, true, target_bitmap); 913 return ApplyColorReduction(source_bitmap, principal, true, target_bitmap);
583 } 914 }
584 915
585 } // color_utils 916 } // color_utils
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698