| Index: ui/gfx/color_analysis.cc
|
| diff --git a/ui/gfx/color_analysis.cc b/ui/gfx/color_analysis.cc
|
| index d3d7b4b6b7e4072ace26a6fc2b286b72b3028ef8..6679ac446422a2acdff94c9dc02e898a74782cc1 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,308 @@ void UnPreMultiply(const SkBitmap& bitmap, uint32_t* buffer, int buffer_size) {
|
| *out++ = SkUnPreMultiply::PMColorToColor(*in++);
|
| }
|
|
|
| +// Prominent color utilities ---------------------------------------------------
|
| +
|
| +// A color value with an associated weight.
|
| +struct WeightedColor {
|
| + WeightedColor(SkColor color, uint64_t weight)
|
| + : color(color), weight(weight) {}
|
| +
|
| + SkColor color;
|
| +
|
| + // The weight correlates to a count, so it should be 1 or greater.
|
| + uint64_t weight;
|
| +};
|
| +
|
| +// A |ColorBox| represents a 3-dimensional region in a color space (an ordered
|
| +// set of colors). 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())) {}
|
| + 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_.length() > 1; }
|
| +
|
| + // Splits |this| in two and returns the other half.
|
| + ColorBox Split() {
|
| + // Calculate which component has the largest range...
|
| + const uint8_t r_dimension = max_r_ - min_r_;
|
| + const uint8_t g_dimension = max_g_ - min_g_;
|
| + const uint8_t b_dimension = max_b_ - min_b_;
|
| + const uint8_t long_dimension =
|
| + std::max({r_dimension, g_dimension, b_dimension});
|
| + const enum {
|
| + RED,
|
| + GREEN,
|
| + BLUE,
|
| + } 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 midpoint (mean of
|
| + // the start and values).
|
| + uint32_t split_point = color_range_.end() - 1;
|
| + for (uint32_t i = color_range_.start() + 1; i < color_range_.end() - 1;
|
| + ++i) {
|
| + bool past_midpoint = false;
|
| + switch (channel) {
|
| + case RED:
|
| + past_midpoint =
|
| + static_cast<uint8_t>(SkColorGetR((*color_space_)[i])) >
|
| + (min_r_ + max_r_) / 2;
|
| + break;
|
| + case GREEN:
|
| + past_midpoint =
|
| + static_cast<uint8_t>(SkColorGetG((*color_space_)[i])) >
|
| + (min_g_ + max_g_) / 2;
|
| + break;
|
| + case BLUE:
|
| + past_midpoint =
|
| + static_cast<uint8_t>(SkColorGetB((*color_space_)[i])) >
|
| + (min_b_ + max_b_) / 2;
|
| + break;
|
| + }
|
| + if (past_midpoint) {
|
| + split_point = i;
|
| + break;
|
| + }
|
| + }
|
| +
|
| + // Break off half and return it.
|
| + gfx::Range other_range = color_range_;
|
| + other_range.set_end(split_point);
|
| + 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 {
|
| + uint64_t sum_r = 0;
|
| + uint64_t sum_g = 0;
|
| + uint64_t sum_b = 0;
|
| + uint64_t total_count_in_box = 0;
|
| +
|
| + for (uint32_t i = color_range_.start(); i < color_range_.end(); ++i) {
|
| + const SkColor color = (*color_space_)[i];
|
| + const 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);
|
| + }
|
| +
|
| + return WeightedColor(
|
| + SkColorSetRGB(
|
| + std::round(static_cast<double>(sum_r) / total_count_in_box),
|
| + std::round(static_cast<double>(sum_g) / total_count_in_box),
|
| + std::round(static_cast<double>(sum_b) / total_count_in_box)),
|
| + total_count_in_box);
|
| + }
|
| +
|
| + static bool CompareByVolume(const ColorBox& a, const ColorBox& b) {
|
| + return a.volume_ < b.volume_;
|
| + }
|
| +
|
| + private:
|
| + ColorBox(std::vector<SkColor>* color_space, const gfx::Range& color_range)
|
| + : color_space_(color_space), color_range_(color_range) {
|
| + RecomputeBounds();
|
| + }
|
| +
|
| + void RecomputeBounds() {
|
| + DCHECK(!color_range_.is_reversed());
|
| + DCHECK(!color_range_.is_empty());
|
| + DCHECK_LE(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_)[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.
|
| + 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) {
|
| + const 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;
|
| +
|
| + 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 CalculateProminentColor(const SkBitmap& bitmap,
|
| + const HSL& lower_bound,
|
| + const HSL& upper_bound,
|
| + const HSL& goal) {
|
| + DCHECK(!bitmap.empty());
|
| + DCHECK(!bitmap.isNull());
|
| +
|
| + SkAutoLockPixels auto_lock(bitmap);
|
| + const uint32_t* pixels = static_cast<uint32_t*>(bitmap.getPixels());
|
| + int pixel_count = bitmap.width() * bitmap.height();
|
| + std::map<SkColor, int> color_counts;
|
| +
|
| + // First extract all colors into counts.
|
| + for (int i = 0; i < pixel_count; ++i) {
|
| + // SkBitmap uses pre-multiplied alpha but the prominent color algorithm
|
| + // needs non-pre-multiplied alpha.
|
| + const SkColor pixel = SkUnPreMultiply::PMColorToColor(pixels[i]);
|
| + if (SkColorGetA(pixel) == SK_AlphaTRANSPARENT)
|
| + continue;
|
| +
|
| + color_counts[pixel]++;
|
| + }
|
| +
|
| + // Now throw out some uninteresting colors.
|
| + std::vector<SkColor> interesting_colors;
|
| + interesting_colors.reserve(color_counts.size());
|
| + for (auto color_count : color_counts) {
|
| + SkColor color = color_count.first;
|
| + if (IsInterestingColor(color))
|
| + interesting_colors.push_back(color);
|
| + }
|
| +
|
| + if (interesting_colors.empty())
|
| + 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, std::vector<ColorBox>,
|
| + bool (*)(const ColorBox&, const ColorBox&)>
|
| + boxes(&ColorBox::CompareByVolume);
|
| + 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;
|
| + uint64_t 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 +708,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 +718,57 @@ SkColor CalculateKMeanColorOfBitmap(const SkBitmap& bitmap) {
|
| bitmap, kDefaultLowerHSLBound, kDefaultUpperHSLBound, &sampler);
|
| }
|
|
|
| +SkColor CalculateProminentColorOfBitmap(const SkBitmap& bitmap,
|
| + LumaRange luma,
|
| + SaturationRange saturation) {
|
| + if (bitmap.empty() || bitmap.isNull())
|
| + return SK_ColorTRANSPARENT;
|
| +
|
| + // 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 CalculateProminentColor(bitmap, lower_bound, upper_bound, goal);
|
| +}
|
| +
|
| gfx::Matrix3F ComputeColorCovariance(const SkBitmap& bitmap) {
|
| // First need basic stats to normalize each channel separately.
|
| SkAutoLockPixels bitmap_lock(bitmap);
|
|
|