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

Unified Diff: ui/gfx/color_analysis.cc

Issue 2690513002: Port Android palette API for deriving a prominent color from an image. (Closed)
Patch Set: git cl try 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 side-by-side diff with in-line comments
Download patch
Index: ui/gfx/color_analysis.cc
diff --git a/ui/gfx/color_analysis.cc b/ui/gfx/color_analysis.cc
index d3d7b4b6b7e4072ace26a6fc2b286b72b3028ef8..f635c5a5604eb4c059ff65128d91ec01c15528b1 100644
--- a/ui/gfx/color_analysis.cc
+++ b/ui/gfx/color_analysis.cc
@@ -8,15 +8,20 @@
#include <stdint.h>
#include <algorithm>
+#include <cmath>
#include <limits>
+#include <map>
#include <memory>
+#include <queue>
#include <vector>
#include "base/logging.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkUnPreMultiply.h"
#include "ui/gfx/codec/png_codec.h"
+#include "ui/gfx/color_palette.h"
#include "ui/gfx/color_utils.h"
+#include "ui/gfx/range/range.h"
namespace color_utils {
namespace {
@@ -143,6 +148,298 @@ void UnPreMultiply(const SkBitmap& bitmap, uint32_t* buffer, int buffer_size) {
*out++ = SkUnPreMultiply::PMColorToColor(*in++);
}
+// Prominent color utilities ---------------------------------------------------
Nico 2017/02/17 21:37:05 kind of looks long enough that it could go into it
Evan Stade 2017/02/17 23:23:18 I like having all the moving parts to this algorit
+
+// A color value with an associated weight (equivalent to the popularity of that
+// color).
+struct WeightedColor {
+ SkColor color;
+ int weight;
sky 2017/02/17 21:42:19 Initialize color and weight?
sky 2017/02/17 21:42:19 Document what the range for this is. Always positi
Evan Stade 2017/02/17 23:23:18 done
Evan Stade 2017/02/17 23:23:18 changed it to use an explicit constructor
+};
+
+// A |ColorBox| represents a region in a color space (an ordered set of colors).
sky 2017/02/17 21:42:19 Did you consider the name OrderedColorSet? IMO tha
Evan Stade 2017/02/17 23:23:18 no, I didn't consider that name. I drew the names
sky 2017/02/18 00:01:34 I actually think 'box' is misleading in this conte
+// It is a range in the ordered set, with a low index and a high index. The
+// diversity (volume) of the box is computed by looking at the range of color
+// values it spans, where r, g, and b components are considered separately.
+class ColorBox {
+ public:
+ explicit ColorBox(std::vector<SkColor>* color_space)
+ : ColorBox(color_space, gfx::Range(0, color_space->size() - 1)) {}
sky 2017/02/17 21:42:20 DCHECK color_space !empty. That said, CanSplit() s
Evan Stade 2017/02/17 23:23:18 An empty color_space would trip the second DCHECK
+ ColorBox(const ColorBox& other) = default;
+ ColorBox& operator=(const ColorBox& other) = default;
+ ~ColorBox() {}
+
+ // Can't split if there's only one color in the box.
+ bool CanSplit() const { return !color_range_.is_empty(); }
+
+ // Splits |this| in two and returns the other half.
+ ColorBox Split() {
sky 2017/02/17 21:42:19 This also sorts. Maybe SortAndSplit?
Evan Stade 2017/02/17 23:26:26 oops, missed this one. It sorts in an unpredictabl
+ // Calculate which component has the largest range...
+ int r_dimension = max_r_ - min_r_;
sky 2017/02/17 21:42:19 As you have min/max as uint8_t, why did you use in
Evan Stade 2017/02/17 23:23:18 updated these all to uint8_t. int8_t could overflo
+ int g_dimension = max_g_ - min_g_;
+ int b_dimension = max_b_ - min_b_;
+ int long_dimension = std::max({r_dimension, g_dimension, b_dimension});
+ enum ColorChannel {
+ RED,
+ GREEN,
+ BLUE,
+ };
Nico 2017/02/17 21:37:05 optional nit: Since you don't use the name, I'd do
Evan Stade 2017/02/17 23:23:18 I like it, done.
+ ColorChannel channel = long_dimension == r_dimension
+ ? RED
+ : long_dimension == g_dimension ? GREEN : BLUE;
+
+ // ... and sort along that axis.
+ auto sort_function = [channel](SkColor a, SkColor b) {
+ switch (channel) {
+ case RED:
+ return SkColorGetR(a) < SkColorGetR(b);
+ case GREEN:
+ return SkColorGetG(a) < SkColorGetG(b);
+ case BLUE:
+ return SkColorGetB(a) < SkColorGetB(b);
+ }
+ NOTREACHED();
+ return SkColorGetB(a) < SkColorGetB(b);
+ };
+ // Just the portion of |color_space_| that's covered by this box should be
+ // sorted.
+ std::sort(color_space_->begin() + color_range_.start(),
+ color_space_->begin() + color_range_.end(), sort_function);
+
+ // Split at the first color value that's not less than the median.
Nico 2017/02/17 21:37:06 You mean "mean" not "median" here, right? Else you
Evan Stade 2017/02/17 23:23:18 hmm, I copied this terminology without thinking mu
+ uint32_t split_point = color_range_.end();
+ for (uint32_t i = color_range_.start() + 1; i < color_range_.end(); ++i) {
+ bool past_median = false;
+ switch (channel) {
+ case RED:
+ past_median =
+ SkColorGetR(color_space_->at(i)) > (min_r_ + max_r_) / 2;
sky 2017/02/17 21:42:19 optional: at -> (*color_space_)[i]
Evan Stade 2017/02/17 23:23:18 Done.
+ break;
+ case GREEN:
+ past_median =
+ SkColorGetG(color_space_->at(i)) > (min_g_ + max_g_) / 2;
+ break;
+ case BLUE:
+ past_median =
+ SkColorGetB(color_space_->at(i)) > (min_b_ + max_b_) / 2;
+ break;
+ }
+ if (past_median) {
+ split_point = i;
+ break;
+ }
+ }
+
+ // Break off half and return it.
+ gfx::Range other_range = color_range_;
sky 2017/02/17 21:42:19 Does this do the right thing with 1 color? If ther
Evan Stade 2017/02/17 23:23:18 it's an error to call this function in that case (
+ other_range.set_end(split_point - 1);
+ ColorBox other_box(color_space_, other_range);
+
+ // Keep the other half in |this| and recalculate our color bounds.
+ color_range_.set_start(split_point);
+ RecomputeBounds();
+ return other_box;
+ }
+
+ // Returns the average color of this box, weighted by its popularity in
+ // |color_counts|.
+ WeightedColor GetWeightedAverageColor(
+ const std::map<SkColor, int>& color_counts) const {
+ int sum_r = 0;
Nico 2017/02/17 21:37:05 For a 4096x4096 all-red image I think this will ov
Evan Stade 2017/02/17 23:23:18 that's bad. I've changed this to uint64_t. I do e
+ int sum_g = 0;
+ int sum_b = 0;
+ int total_count_in_box = 0;
+
+ for (uint32_t i = color_range_.start(); i <= color_range_.end(); ++i) {
+ const SkColor color = color_space_->at(i);
sky 2017/02/17 21:42:19 If you're going to use const (which I like) use it
Evan Stade 2017/02/17 23:23:18 Done.
+ auto color_count_iter = color_counts.find(color);
+ DCHECK(color_count_iter != color_counts.end());
+ const int color_count = color_count_iter->second;
+
+ total_count_in_box += color_count;
+ sum_r += color_count * SkColorGetR(color);
+ sum_g += color_count * SkColorGetG(color);
+ sum_b += color_count * SkColorGetB(color);
+ }
+
+ WeightedColor weighted_color;
+ weighted_color.weight = total_count_in_box;
+ weighted_color.color = SkColorSetRGB(
+ std::round(static_cast<float>(sum_r) / total_count_in_box),
+ std::round(static_cast<float>(sum_g) / total_count_in_box),
+ std::round(static_cast<float>(sum_b) / total_count_in_box));
+ return weighted_color;
+ }
+
+ // Comparisons are done by volume.
sky 2017/02/17 21:42:19 Using operator < to mean compare by volume is obsc
Evan Stade 2017/02/17 23:23:18 done. (If there's a more elegant way to do this, p
+ bool operator<(const ColorBox& other) const {
+ return volume_ < other.volume_;
+ }
+
+ private:
+ ColorBox(std::vector<SkColor>* color_space, const gfx::Range& color_range)
+ : color_space_(color_space), color_range_(color_range) {
+ RecomputeBounds();
+ }
+
+ void RecomputeBounds() {
sky 2017/02/17 21:42:19 optional: There are no bounds in here, and the nam
Evan Stade 2017/02/17 23:23:18 min_* and max_* describe upper and lower bounds fo
+ DCHECK(!color_range_.is_reversed());
+ DCHECK_LT(color_range_.end(), color_space_->size());
+
+ min_r_ = 0xFF;
+ min_g_ = 0xFF;
+ min_b_ = 0xFF;
+ max_r_ = 0;
+ max_g_ = 0;
+ max_b_ = 0;
+
+ for (uint32_t i = color_range_.start(); i < color_range_.end(); ++i) {
+ SkColor color = color_space_->at(i);
+ min_r_ = std::min<uint8_t>(SkColorGetR(color), min_r_);
+ min_g_ = std::min<uint8_t>(SkColorGetG(color), min_g_);
+ min_b_ = std::min<uint8_t>(SkColorGetB(color), min_b_);
+ max_r_ = std::max<uint8_t>(SkColorGetR(color), max_r_);
+ max_g_ = std::max<uint8_t>(SkColorGetG(color), max_g_);
+ max_b_ = std::max<uint8_t>(SkColorGetB(color), max_b_);
+ }
+
+ volume_ =
+ (max_r_ - min_r_ + 1) * (max_g_ - min_g_ + 1) * (max_b_ - min_b_ + 1);
+ }
+
+ // The set of colors of which this box captures a subset. This vector is not
+ // owned but may be modified during the split operation.
+ std::vector<SkColor>* color_space_;
+
+ // The range of indexes into |color_space_| that are part of this box.
sky 2017/02/17 21:42:19 Why did you make this inclusive vs exclusive? Incl
Evan Stade 2017/02/17 23:23:18 changed. In fact I think there was a bug in the st
+ gfx::Range color_range_;
+
+ // Cached min and max color component values for the colors in this box.
+ uint8_t min_r_ = 0;
+ uint8_t min_g_ = 0;
+ uint8_t min_b_ = 0;
+ uint8_t max_r_ = 0;
+ uint8_t max_g_ = 0;
+ uint8_t max_b_ = 0;
+
+ // Cached volume value, which is the product of the range of each color
+ // component.
+ int volume_ = 0;
+};
+
+// Some color values should be ignored for the purposes of determining prominent
+// colors.
+bool IsInterestingColor(SkColor color) {
+ float average_channel_value =
+ (SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color)) / 3.0f;
+ // If a color is too close to white or black, ignore it.
+ if (average_channel_value >= 237 || average_channel_value <= 22)
+ return false;
+
+ // Also rule out skin tones.
+ HSL hsl;
+ SkColorToHSL(color, &hsl);
+ return !(hsl.h >= 0.028f && hsl.h <= 0.10f && hsl.s <= 0.82f);
+}
+
+// This algorithm is a port of Android's Palette API. Compare to package
+// android.support.v7.graphics and see that code for additional high-level
+// explanation of this algorithm. There are some minor differences:
+// * This code doesn't exclude the same color from being used for
+// different color profiles.
+// * This code doesn't try to heuristically derive missing colors from
+// existing colors.
+SkColor CalculateProminentColorOfBuffer(uint8_t* decoded_data,
+ int img_width,
+ int img_height,
+ const HSL& lower_bound,
+ const HSL& upper_bound,
+ const HSL& goal) {
+ DCHECK_GT(img_width, 0);
+ DCHECK_GT(img_height, 0);
+ std::map<SkColor, int> color_counts;
+
+ // First extract all colors.
+ for (int i = 0; i < img_width * img_height; ++i) {
+ // TODO(port): This code assumes the CPU architecture is little-endian.
sky 2017/02/17 21:42:19 Can you DCHECK on the image type? That it's 32bit?
Evan Stade 2017/02/17 23:23:18 I was kind of just blindly copying this bit. I've
+ uint8_t b = decoded_data[i * 4];
+ uint8_t g = decoded_data[i * 4 + 1];
+ uint8_t r = decoded_data[i * 4 + 2];
+ uint8_t a = decoded_data[i * 4 + 3];
+ if (a == SK_AlphaTRANSPARENT)
+ continue;
+
+ SkColor pixel = SkColorSetRGB(r, g, b);
+ color_counts[pixel]++;
+ }
+
+ // Now throw out some uninteresting colors.
+ std::vector<SkColor> interesting_colors;
+ for (auto color_count : color_counts) {
+ SkColor color = color_count.first;
+ if (IsInterestingColor(color))
+ interesting_colors.push_back(color);
+ }
+
+ if (interesting_colors.empty())
sky 2017/02/17 21:42:19 If there is only one color, is it the prominent co
Evan Stade 2017/02/17 23:23:18 it could be, but only if it's "interesting" and wi
+ return SK_ColorTRANSPARENT;
+
+ // Group the colors into "boxes" and repeatedly split the most voluminous box.
+ // We stop the process when a box can no longer be split (there's only one
+ // color in it) or when the number of color boxes reaches 12. As per the
+ // Android docs,
+ //
+ // For landscapes, good values are in the range 12-16. For images which
+ // are largely made up of people's faces then this value should be increased
+ // to 24-32.
+ const int kMaxColors = 12;
+ // Boxes are sorted by volume with the most voluminous at the front of the PQ.
+ std::priority_queue<ColorBox> boxes;
+ boxes.emplace(&interesting_colors);
+ while (boxes.size() < kMaxColors) {
+ auto box = boxes.top();
+ if (!box.CanSplit())
+ break;
+ boxes.pop();
+ boxes.push(box.Split());
+ boxes.push(box);
+ }
+
+ // Now extract a single color to represent each box. This is the average color
+ // in the box, weighted by the frequency of that color in the source image.
+ std::vector<WeightedColor> box_colors;
+ int max_weight = 0;
+ while (!boxes.empty()) {
+ box_colors.push_back(boxes.top().GetWeightedAverageColor(color_counts));
+ boxes.pop();
+ max_weight = std::max(max_weight, box_colors.back().weight);
+ }
+
+ // Given these box average colors, find the best one for the desired color
+ // profile. "Best" in this case means the color which fits in the provided
+ // bounds and comes closest to |goal|. It's possible that no color will fit in
+ // the provided bounds, in which case we'll return an empty color.
+ double best_suitability = 0;
+ SkColor best_color = SK_ColorTRANSPARENT;
+ for (const auto& box_color : box_colors) {
+ HSL hsl;
+ SkColorToHSL(box_color.color, &hsl);
+ if (!IsWithinHSLRange(hsl, lower_bound, upper_bound))
+ continue;
+
+ double suitability =
+ (1 - std::abs(hsl.s - goal.s)) * 3 +
+ (1 - std::abs(hsl.l - goal.l)) * 6.5 +
+ (box_color.weight / static_cast<float>(max_weight)) * 0.5;
+ if (suitability > best_suitability) {
+ best_suitability = suitability;
+ best_color = box_color.color;
+ }
+ }
+
+ return best_color;
+}
+
} // namespace
KMeanImageSampler::KMeanImageSampler() {
@@ -401,11 +698,8 @@ SkColor CalculateKMeanColorOfBitmap(const SkBitmap& bitmap,
UnPreMultiply(bitmap, image.get(), pixel_count);
return CalculateKMeanColorOfBuffer(reinterpret_cast<uint8_t*>(image.get()),
- bitmap.width(),
- bitmap.height(),
- lower_bound,
- upper_bound,
- sampler);
+ bitmap.width(), bitmap.height(),
+ lower_bound, upper_bound, sampler);
}
SkColor CalculateKMeanColorOfBitmap(const SkBitmap& bitmap) {
@@ -414,6 +708,66 @@ SkColor CalculateKMeanColorOfBitmap(const SkBitmap& bitmap) {
bitmap, kDefaultLowerHSLBound, kDefaultUpperHSLBound, &sampler);
}
+SkColor CalculateProminentColorOfBitmap(const SkBitmap& bitmap,
+ LumaRange luma,
+ SaturationRange saturation) {
+ // SkBitmap uses pre-multiplied alpha but the prominent color algorithm
+ // above uses non-pre-multiplied alpha. Transform the bitmap before we
+ // analyze it because the function reads each pixel multiple times.
+ int pixel_count = bitmap.width() * bitmap.height();
+ if (pixel_count == 0)
+ return SK_ColorTRANSPARENT;
+
+ std::unique_ptr<uint32_t[]> image(new uint32_t[pixel_count]);
sky 2017/02/17 21:42:19 image_data? bitmap_data?
Evan Stade 2017/02/17 23:23:18 removed
+ UnPreMultiply(bitmap, image.get(), pixel_count);
+
+ // The hue is not relevant to our bounds or goal colors.
+ HSL lower_bound = {
+ -1,
+ };
+ HSL upper_bound = {
+ -1,
+ };
+ HSL goal = {
+ -1,
+ };
+
+ switch (luma) {
+ case LumaRange::LIGHT:
+ lower_bound.l = 0.55f;
+ upper_bound.l = 1;
+ goal.l = 0.74f;
+ break;
+ case LumaRange::NORMAL:
+ lower_bound.l = 0.3f;
+ upper_bound.l = 0.7f;
+ goal.l = 0.5f;
+ break;
+ case LumaRange::DARK:
+ lower_bound.l = 0;
+ upper_bound.l = 0.45f;
+ goal.l = 0.26f;
+ break;
+ }
+
+ switch (saturation) {
+ case SaturationRange::VIBRANT:
+ lower_bound.s = 0.35f;
+ upper_bound.s = 1;
+ goal.s = 1;
+ break;
+ case SaturationRange::MUTED:
+ lower_bound.s = 0;
+ upper_bound.s = 0.4f;
+ goal.s = 0.3f;
+ break;
+ }
+
+ return CalculateProminentColorOfBuffer(
+ reinterpret_cast<uint8_t*>(image.get()), bitmap.width(), bitmap.height(),
+ lower_bound, upper_bound, goal);
+}
+
gfx::Matrix3F ComputeColorCovariance(const SkBitmap& bitmap) {
// First need basic stats to normalize each channel separately.
SkAutoLockPixels bitmap_lock(bitmap);

Powered by Google App Engine
This is Rietveld 408576698