Reland "[Media Notification] Derive background color from artwork"
This is a reland of e60e1e6e09
TBR=estade@chromium.org,sadrul@chromium.org
Original change's description:
> [Media Notification] Derive background color from artwork
>
> The media notification should derive its background
> color from prominent colors in the artwork. This
> uses as similar algorithm from Android.
>
> Also breaks out some of the logic of CalculateProminentColors
> so we can use it without the reset of the function. This
> is because we want uninteresting colors and do not want
> the color profile matching.
>
> BUG=944598
>
> Change-Id: I60df7f600f6fc881dfb38d2057c903a5679845df
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1534923
> Commit-Queue: Becca Hughes <beccahughes@chromium.org>
> Reviewed-by: Sadrul Chowdhury <sadrul@chromium.org>
> Reviewed-by: Evan Stade <estade@chromium.org>
> Reviewed-by: Tommy Steimel <steimel@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#644985}
Bug: 944598
Change-Id: I44f904ccfe2843cf2fbe9568e9121922fd012f03
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1543466
Commit-Queue: Becca Hughes <beccahughes@chromium.org>
Reviewed-by: Tommy Steimel <steimel@chromium.org>
Cr-Commit-Position: refs/heads/master@{#645507}
This commit is contained in:
@ -5,8 +5,12 @@
|
||||
#include "ash/media/media_notification_background.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
#include "skia/ext/image_operations.h"
|
||||
#include "ui/gfx/canvas.h"
|
||||
#include "ui/gfx/color_analysis.h"
|
||||
#include "ui/gfx/color_utils.h"
|
||||
#include "ui/gfx/scoped_canvas.h"
|
||||
#include "ui/views/view.h"
|
||||
|
||||
@ -16,7 +20,66 @@ namespace {
|
||||
|
||||
constexpr int kMediaImageGradientWidth = 40;
|
||||
|
||||
constexpr SkColor kMediaNotificationBackgroundColor = SK_ColorWHITE;
|
||||
constexpr SkColor kMediaNotificationDefaultBackgroundColor = SK_ColorWHITE;
|
||||
|
||||
// The ratio for a background color option to be considered very popular.
|
||||
constexpr double kMediaNotificationBackgroundColorVeryPopularRatio = 2.5;
|
||||
|
||||
bool IsNearlyWhiteOrBlack(SkColor color) {
|
||||
color_utils::HSL hsl;
|
||||
color_utils::SkColorToHSL(color, &hsl);
|
||||
return hsl.l >= 0.9 || hsl.l <= 0.08;
|
||||
}
|
||||
|
||||
base::Optional<SkColor> GetNotificationBackgroundColor(const SkBitmap* source) {
|
||||
if (!source || source->empty() || source->isNull())
|
||||
return base::nullopt;
|
||||
|
||||
std::vector<color_utils::Swatch> swatches =
|
||||
color_utils::CalculateColorSwatches(
|
||||
*source, 16, gfx::Rect(source->width() / 2, source->height()),
|
||||
false /* exclude_uninteresting */);
|
||||
|
||||
if (swatches.empty())
|
||||
return base::nullopt;
|
||||
|
||||
base::Optional<color_utils::Swatch> most_popular;
|
||||
base::Optional<color_utils::Swatch> non_white_black;
|
||||
|
||||
// Find the most popular color with the most weight and the color which
|
||||
// is the color with the most weight that is not white or black.
|
||||
for (auto& swatch : swatches) {
|
||||
if (!IsNearlyWhiteOrBlack(swatch.color) &&
|
||||
(!non_white_black || swatch.population > non_white_black->population)) {
|
||||
non_white_black = swatch;
|
||||
}
|
||||
|
||||
if (most_popular && swatch.population < most_popular->population)
|
||||
continue;
|
||||
|
||||
most_popular = swatch;
|
||||
}
|
||||
|
||||
DCHECK(most_popular);
|
||||
|
||||
// If the most popular color is not white or black then we should use that.
|
||||
if (!IsNearlyWhiteOrBlack(most_popular->color))
|
||||
return most_popular->color;
|
||||
|
||||
// If we could not find a color that is not white or black then we should
|
||||
// use the most popular color.
|
||||
if (!non_white_black)
|
||||
return most_popular->color;
|
||||
|
||||
// If the most popular color is very popular then we should use that color.
|
||||
if (static_cast<double>(most_popular->population) /
|
||||
non_white_black->population >
|
||||
kMediaNotificationBackgroundColorVeryPopularRatio) {
|
||||
return most_popular->color;
|
||||
}
|
||||
|
||||
return non_white_black->color;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -32,6 +95,8 @@ MediaNotificationBackground::MediaNotificationBackground(
|
||||
DCHECK(owner);
|
||||
}
|
||||
|
||||
MediaNotificationBackground::~MediaNotificationBackground() = default;
|
||||
|
||||
void MediaNotificationBackground::Paint(gfx::Canvas* canvas,
|
||||
views::View* view) const {
|
||||
DCHECK(view);
|
||||
@ -70,18 +135,16 @@ void MediaNotificationBackground::Paint(gfx::Canvas* canvas,
|
||||
|
||||
// Draw a filled rectangle which will act as the main background of the
|
||||
// notification. This may cover up some of the artwork.
|
||||
canvas->FillRect(GetFilledBackgroundBounds(bounds),
|
||||
kMediaNotificationBackgroundColor);
|
||||
const SkColor background_color =
|
||||
background_color_.value_or(kMediaNotificationDefaultBackgroundColor);
|
||||
canvas->FillRect(GetFilledBackgroundBounds(bounds), background_color);
|
||||
|
||||
{
|
||||
// Draw a gradient to fade the color background and the image together.
|
||||
gfx::Rect draw_bounds = GetGradientBounds(bounds);
|
||||
|
||||
const SkColor transparent =
|
||||
SkColorSetA(kMediaNotificationBackgroundColor, 0);
|
||||
|
||||
const SkColor colors[2] = {kMediaNotificationBackgroundColor, transparent};
|
||||
|
||||
const SkColor colors[2] = {
|
||||
background_color, SkColorSetA(background_color, SK_AlphaTRANSPARENT)};
|
||||
const SkPoint points[2] = {gfx::PointToSkPoint(draw_bounds.left_center()),
|
||||
gfx::PointToSkPoint(draw_bounds.right_center())};
|
||||
|
||||
@ -100,6 +163,7 @@ void MediaNotificationBackground::UpdateArtwork(const gfx::ImageSkia& image) {
|
||||
return;
|
||||
|
||||
artwork_ = image;
|
||||
background_color_ = GetNotificationBackgroundColor(artwork_.bitmap());
|
||||
owner_->SchedulePaint();
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
#define ASH_MEDIA_MEDIA_NOTIFICATION_BACKGROUND_H_
|
||||
|
||||
#include "ash/ash_export.h"
|
||||
#include "base/optional.h"
|
||||
#include "ui/gfx/image/image_skia.h"
|
||||
#include "ui/views/background.h"
|
||||
|
||||
@ -29,6 +30,7 @@ class ASH_EXPORT MediaNotificationBackground : public views::Background {
|
||||
int top_radius,
|
||||
int bottom_radius,
|
||||
double artwork_max_width_pct);
|
||||
~MediaNotificationBackground() override;
|
||||
|
||||
// views::Background
|
||||
void Paint(gfx::Canvas* canvas, views::View* view) const override;
|
||||
@ -38,6 +40,7 @@ class ASH_EXPORT MediaNotificationBackground : public views::Background {
|
||||
void UpdateArtworkMaxWidthPct(double max_width_pct);
|
||||
|
||||
private:
|
||||
friend class MediaNotificationBackgroundTest;
|
||||
friend class MediaNotificationViewTest;
|
||||
FRIEND_TEST_ALL_PREFIXES(MediaNotificationBackgroundTest, BoundsSanityCheck);
|
||||
|
||||
@ -56,6 +59,8 @@ class ASH_EXPORT MediaNotificationBackground : public views::Background {
|
||||
gfx::ImageSkia artwork_;
|
||||
double artwork_max_width_pct_;
|
||||
|
||||
base::Optional<SkColor> background_color_;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(MediaNotificationBackground);
|
||||
};
|
||||
|
||||
|
@ -11,53 +11,173 @@
|
||||
|
||||
namespace ash {
|
||||
|
||||
namespace {
|
||||
|
||||
gfx::ImageSkia CreateTestBackgroundImage(SkColor first_color,
|
||||
SkColor second_color,
|
||||
int second_height) {
|
||||
constexpr SkColor kRightHandSideColor = SK_ColorMAGENTA;
|
||||
|
||||
DCHECK_NE(kRightHandSideColor, first_color);
|
||||
DCHECK_NE(kRightHandSideColor, second_color);
|
||||
|
||||
SkBitmap bitmap;
|
||||
bitmap.allocN32Pixels(100, 100);
|
||||
|
||||
int first_height = bitmap.height() - second_height;
|
||||
int right_width = bitmap.width() / 2;
|
||||
|
||||
// Fill the right hand side of the image with a constant color. The color
|
||||
// derivation algorithm does not look at the right hand side so we should
|
||||
// never see |kRightHandSideColor|.
|
||||
bitmap.erase(kRightHandSideColor,
|
||||
{right_width, 0, bitmap.width(), bitmap.height()});
|
||||
|
||||
// Fill the left hand side with |first_color|.
|
||||
bitmap.erase(first_color, {0, 0, right_width, first_height});
|
||||
|
||||
// Fill the left hand side with |second_color|.
|
||||
bitmap.erase(second_color, {0, first_height, right_width, bitmap.height()});
|
||||
|
||||
return gfx::ImageSkia::CreateFrom1xBitmap(bitmap);
|
||||
}
|
||||
|
||||
gfx::ImageSkia CreateTestBackgroundImage(SkColor color) {
|
||||
return CreateTestBackgroundImage(color, SK_ColorTRANSPARENT, 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class MediaNotificationBackgroundTest : public AshTestBase {
|
||||
public:
|
||||
MediaNotificationBackgroundTest() = default;
|
||||
~MediaNotificationBackgroundTest() override = default;
|
||||
|
||||
void SetUp() override {
|
||||
AshTestBase::SetUp();
|
||||
|
||||
owner_ = std::make_unique<views::StaticSizedView>();
|
||||
background_ = std::make_unique<MediaNotificationBackground>(owner_.get(),
|
||||
10, 10, 0.1);
|
||||
|
||||
EXPECT_FALSE(GetBackgroundColor().has_value());
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
background_.reset();
|
||||
owner_.reset();
|
||||
|
||||
AshTestBase::TearDown();
|
||||
}
|
||||
|
||||
MediaNotificationBackground* background() const { return background_.get(); }
|
||||
|
||||
base::Optional<SkColor> GetBackgroundColor() const {
|
||||
return background_->background_color_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<views::StaticSizedView> owner_;
|
||||
std::unique_ptr<MediaNotificationBackground> background_;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(MediaNotificationBackgroundTest);
|
||||
};
|
||||
|
||||
TEST_F(MediaNotificationBackgroundTest, BoundsSanityCheck) {
|
||||
views::StaticSizedView owner;
|
||||
MediaNotificationBackground background(&owner, 10, 10, 0.1);
|
||||
|
||||
// The test notification will have a width of 200 and a height of 50.
|
||||
gfx::Rect bounds(0, 0, 200, 50);
|
||||
|
||||
// Check the artwork is not visible by default.
|
||||
EXPECT_EQ(0, background.GetArtworkWidth(bounds.size()));
|
||||
EXPECT_EQ(0, background.GetArtworkVisibleWidth(bounds.size()));
|
||||
EXPECT_EQ(gfx::Rect(200, 0, 0, 50), background.GetArtworkBounds(bounds));
|
||||
EXPECT_EQ(0, background()->GetArtworkWidth(bounds.size()));
|
||||
EXPECT_EQ(0, background()->GetArtworkVisibleWidth(bounds.size()));
|
||||
EXPECT_EQ(gfx::Rect(200, 0, 0, 50), background()->GetArtworkBounds(bounds));
|
||||
EXPECT_EQ(gfx::Rect(0, 0, 200, 50),
|
||||
background.GetFilledBackgroundBounds(bounds));
|
||||
EXPECT_EQ(gfx::Rect(0, 0, 0, 0), background.GetGradientBounds(bounds));
|
||||
background()->GetFilledBackgroundBounds(bounds));
|
||||
EXPECT_EQ(gfx::Rect(0, 0, 0, 0), background()->GetGradientBounds(bounds));
|
||||
|
||||
// The background artwork image will have an aspect ratio of 2:1.
|
||||
SkBitmap bitmap;
|
||||
bitmap.allocN32Pixels(20, 10);
|
||||
background.UpdateArtwork(gfx::ImageSkia::CreateFrom1xBitmap(bitmap));
|
||||
bitmap.eraseColor(SK_ColorWHITE);
|
||||
background()->UpdateArtwork(gfx::ImageSkia::CreateFrom1xBitmap(bitmap));
|
||||
|
||||
// The artwork width will be 2x the height of the notification and the visible
|
||||
// width will be limited to 10% the width of the notification.
|
||||
EXPECT_EQ(100, background.GetArtworkWidth(bounds.size()));
|
||||
EXPECT_EQ(20, background.GetArtworkVisibleWidth(bounds.size()));
|
||||
EXPECT_EQ(100, background()->GetArtworkWidth(bounds.size()));
|
||||
EXPECT_EQ(20, background()->GetArtworkVisibleWidth(bounds.size()));
|
||||
|
||||
// Update the visible width % to be greater than the width of the image.
|
||||
background.UpdateArtworkMaxWidthPct(0.6);
|
||||
EXPECT_EQ(100, background.GetArtworkVisibleWidth(bounds.size()));
|
||||
background()->UpdateArtworkMaxWidthPct(0.6);
|
||||
EXPECT_EQ(100, background()->GetArtworkVisibleWidth(bounds.size()));
|
||||
|
||||
// Check the artwork is positioned to the right.
|
||||
EXPECT_EQ(gfx::Rect(100, 0, 100, 50), background.GetArtworkBounds(bounds));
|
||||
EXPECT_EQ(gfx::Rect(100, 0, 100, 50), background()->GetArtworkBounds(bounds));
|
||||
|
||||
// Check the filled background is to the left of the image.
|
||||
EXPECT_EQ(gfx::Rect(0, 0, 100, 50),
|
||||
background.GetFilledBackgroundBounds(bounds));
|
||||
background()->GetFilledBackgroundBounds(bounds));
|
||||
|
||||
// Check the gradient is positioned above the artwork.
|
||||
EXPECT_EQ(gfx::Rect(100, 0, 40, 50), background.GetGradientBounds(bounds));
|
||||
EXPECT_EQ(gfx::Rect(100, 0, 40, 50), background()->GetGradientBounds(bounds));
|
||||
}
|
||||
|
||||
// If we have no artwork then we should use the default background color.
|
||||
TEST_F(MediaNotificationBackgroundTest, DeriveBackgroundColor_NoArtwork) {
|
||||
background()->UpdateArtwork(gfx::ImageSkia());
|
||||
EXPECT_FALSE(GetBackgroundColor().has_value());
|
||||
}
|
||||
|
||||
// If we have artwork with no popular color then we should use the default
|
||||
// background color.
|
||||
TEST_F(MediaNotificationBackgroundTest, DeriveBackgroundColor_NoPopularColor) {
|
||||
background()->UpdateArtwork(CreateTestBackgroundImage(SK_ColorTRANSPARENT));
|
||||
EXPECT_FALSE(GetBackgroundColor().has_value());
|
||||
}
|
||||
|
||||
// If the most popular color is not white or black then we should use that
|
||||
// color.
|
||||
TEST_F(MediaNotificationBackgroundTest,
|
||||
DeriveBackgroundColor_PopularNonWhiteBlackColor) {
|
||||
constexpr SkColor kTestColor = SK_ColorYELLOW;
|
||||
background()->UpdateArtwork(CreateTestBackgroundImage(kTestColor));
|
||||
EXPECT_EQ(kTestColor, GetBackgroundColor());
|
||||
}
|
||||
|
||||
// MediaNotificationBackgroundBlackWhiteTest will repeat these tests with a
|
||||
// parameter that is either black or white.
|
||||
class MediaNotificationBackgroundBlackWhiteTest
|
||||
: public MediaNotificationBackgroundTest,
|
||||
public testing::WithParamInterface<SkColor> {};
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(,
|
||||
MediaNotificationBackgroundBlackWhiteTest,
|
||||
testing::Values(SK_ColorBLACK, SK_ColorWHITE));
|
||||
|
||||
// If the most popular color is black or white but there is no secondary color
|
||||
// we should use the most popular color.
|
||||
TEST_P(MediaNotificationBackgroundBlackWhiteTest,
|
||||
DeriveBackgroundColor_PopularBlackWhiteNoSecondaryColor) {
|
||||
background()->UpdateArtwork(CreateTestBackgroundImage(GetParam()));
|
||||
EXPECT_EQ(GetParam(), GetBackgroundColor());
|
||||
}
|
||||
|
||||
// If the most popular color is black or white and there is a secondary color
|
||||
// that is very minor then we should use the most popular color.
|
||||
TEST_P(MediaNotificationBackgroundBlackWhiteTest,
|
||||
DeriveBackgroundColor_VeryPopularBlackWhite) {
|
||||
background()->UpdateArtwork(
|
||||
CreateTestBackgroundImage(GetParam(), SK_ColorYELLOW, 20));
|
||||
EXPECT_EQ(GetParam(), GetBackgroundColor());
|
||||
}
|
||||
|
||||
// If the most popular color is black or white but it is not that popular then
|
||||
// we should use the secondary color.
|
||||
TEST_P(MediaNotificationBackgroundBlackWhiteTest,
|
||||
DeriveBackgroundColor_NotVeryPopularBlackWhite) {
|
||||
constexpr SkColor kTestColor = SK_ColorYELLOW;
|
||||
background()->UpdateArtwork(
|
||||
CreateTestBackgroundImage(GetParam(), kTestColor, 40));
|
||||
EXPECT_EQ(kTestColor, GetBackgroundColor());
|
||||
}
|
||||
|
||||
} // namespace ash
|
||||
|
@ -687,6 +687,7 @@ TEST_F(MediaNotificationViewTest, UpdateArtworkFromItem) {
|
||||
|
||||
SkBitmap image;
|
||||
image.allocN32Pixels(10, 10);
|
||||
image.eraseColor(SK_ColorWHITE);
|
||||
|
||||
EXPECT_TRUE(GetArtworkImage().isNull());
|
||||
|
||||
|
@ -21,6 +21,7 @@
|
||||
#include "ui/gfx/codec/png_codec.h"
|
||||
#include "ui/gfx/color_palette.h"
|
||||
#include "ui/gfx/color_utils.h"
|
||||
#include "ui/gfx/geometry/rect.h"
|
||||
#include "ui/gfx/range/range.h"
|
||||
|
||||
namespace color_utils {
|
||||
@ -139,17 +140,6 @@ class KMeanCluster {
|
||||
|
||||
// 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
|
||||
@ -242,18 +232,18 @@ class ColorBox {
|
||||
|
||||
// Returns the average color of this box, weighted by its popularity in
|
||||
// |color_counts|.
|
||||
WeightedColor GetWeightedAverageColor(
|
||||
Swatch GetWeightedAverageColor(
|
||||
const std::unordered_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;
|
||||
size_t sum_r = 0;
|
||||
size_t sum_g = 0;
|
||||
size_t sum_b = 0;
|
||||
size_t total_count_in_box = 0;
|
||||
|
||||
for (uint32_t i = color_range_.start(); i < color_range_.end(); ++i) {
|
||||
for (size_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;
|
||||
const size_t color_count = color_count_iter->second;
|
||||
|
||||
total_count_in_box += color_count;
|
||||
sum_r += color_count * SkColorGetR(color);
|
||||
@ -261,7 +251,7 @@ class ColorBox {
|
||||
sum_b += color_count * SkColorGetB(color);
|
||||
}
|
||||
|
||||
return WeightedColor(
|
||||
return Swatch(
|
||||
SkColorSetRGB(
|
||||
std::round(static_cast<double>(sum_r) / total_count_in_box),
|
||||
std::round(static_cast<double>(sum_g) / total_count_in_box),
|
||||
@ -347,86 +337,23 @@ struct ColorBracket {
|
||||
HSL goal = {-1};
|
||||
};
|
||||
|
||||
// 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.
|
||||
std::vector<SkColor> CalculateProminentColors(
|
||||
const SkBitmap& bitmap,
|
||||
const std::vector<ColorBracket>& color_brackets) {
|
||||
DCHECK(!bitmap.empty());
|
||||
DCHECK(!bitmap.isNull());
|
||||
|
||||
const uint32_t* pixels = static_cast<uint32_t*>(bitmap.getPixels());
|
||||
const int pixel_count = bitmap.width() * bitmap.height();
|
||||
|
||||
// For better performance, only consider at most 10k pixels (evenly
|
||||
// distributed throughout the image). This has a very minor impact on the
|
||||
// outcome but improves runtime substantially for large images. 10,007 is a
|
||||
// prime number to reduce the chance of picking an unrepresentative sample.
|
||||
constexpr int kMaxConsideredPixels = 10007;
|
||||
const int pixel_increment = std::max(1, pixel_count / kMaxConsideredPixels);
|
||||
std::unordered_map<SkColor, int> color_counts(kMaxConsideredPixels);
|
||||
|
||||
// First extract all colors into counts.
|
||||
for (int i = 0; i < pixel_count; i += pixel_increment) {
|
||||
// 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);
|
||||
}
|
||||
std::vector<Swatch> box_colors = CalculateColorSwatches(
|
||||
bitmap, 12, gfx::Rect(bitmap.width(), bitmap.height()),
|
||||
true /* exclude_uninteresting */);
|
||||
|
||||
std::vector<SkColor> best_colors(color_brackets.size(), SK_ColorTRANSPARENT);
|
||||
if (interesting_colors.empty())
|
||||
if (box_colors.empty())
|
||||
return best_colors;
|
||||
|
||||
// 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);
|
||||
}
|
||||
size_t max_weight = 0;
|
||||
for (auto& weighted : box_colors)
|
||||
max_weight = std::max(max_weight, weighted.population);
|
||||
|
||||
// Given these box average colors, find the best one for each desired color
|
||||
// profile. "Best" in this case means the color which fits in the provided
|
||||
@ -445,7 +372,7 @@ std::vector<SkColor> CalculateProminentColors(
|
||||
double suitability =
|
||||
(1 - std::abs(hsl.s - color_brackets[i].goal.s)) * 3 +
|
||||
(1 - std::abs(hsl.l - color_brackets[i].goal.l)) * 6.5 +
|
||||
(box_color.weight / static_cast<float>(max_weight)) * 0.5;
|
||||
(box_color.population / static_cast<float>(max_weight)) * 0.5;
|
||||
if (suitability > best_suitability) {
|
||||
best_suitability = suitability;
|
||||
best_colors[i] = box_color.color;
|
||||
@ -733,6 +660,89 @@ SkColor CalculateKMeanColorOfBitmap(const SkBitmap& bitmap) {
|
||||
true);
|
||||
}
|
||||
|
||||
// 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.
|
||||
std::vector<Swatch> CalculateColorSwatches(const SkBitmap& bitmap,
|
||||
size_t max_swatches,
|
||||
const gfx::Rect& region,
|
||||
bool exclude_uninteresting) {
|
||||
DCHECK(!bitmap.empty());
|
||||
DCHECK(!bitmap.isNull());
|
||||
DCHECK(!region.IsEmpty());
|
||||
DCHECK_LE(region.width(), bitmap.width());
|
||||
DCHECK_LE(region.height(), bitmap.height());
|
||||
|
||||
const int pixel_count = region.width() * region.height();
|
||||
|
||||
// For better performance, only consider at most 10k pixels (evenly
|
||||
// distributed throughout the image). This has a very minor impact on the
|
||||
// outcome but improves runtime substantially for large images. 10,007 is a
|
||||
// prime number to reduce the chance of picking an unrepresentative sample.
|
||||
constexpr int kMaxConsideredPixels = 10007;
|
||||
const int pixel_increment = std::max(1, pixel_count / kMaxConsideredPixels);
|
||||
std::unordered_map<SkColor, int> color_counts(kMaxConsideredPixels);
|
||||
|
||||
// First extract all colors into counts.
|
||||
for (int i = 0; i < pixel_count; i += pixel_increment) {
|
||||
const int x = region.x() + (i % region.width());
|
||||
const int y = region.y() + (i / region.width());
|
||||
|
||||
const SkColor pixel = bitmap.getColor(x, y);
|
||||
if (SkColorGetA(pixel) == SK_AlphaTRANSPARENT)
|
||||
continue;
|
||||
|
||||
color_counts[pixel]++;
|
||||
}
|
||||
|
||||
// Now throw out some uninteresting colors if |exclude_uninteresting| is true.
|
||||
std::vector<SkColor> interesting_colors;
|
||||
interesting_colors.reserve(color_counts.size());
|
||||
for (auto color_count : color_counts) {
|
||||
SkColor color = color_count.first;
|
||||
if (!exclude_uninteresting || IsInterestingColor(color))
|
||||
interesting_colors.push_back(color);
|
||||
}
|
||||
|
||||
if (interesting_colors.empty())
|
||||
return {};
|
||||
|
||||
// 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 |max_colors|.
|
||||
//
|
||||
// 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() < max_swatches) {
|
||||
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.
|
||||
size_t max_weight = 0;
|
||||
std::vector<Swatch> box_colors;
|
||||
box_colors.reserve(max_swatches);
|
||||
while (!boxes.empty()) {
|
||||
box_colors.push_back(boxes.top().GetWeightedAverageColor(color_counts));
|
||||
boxes.pop();
|
||||
max_weight = std::max(max_weight, box_colors.back().population);
|
||||
}
|
||||
|
||||
return box_colors;
|
||||
}
|
||||
|
||||
std::vector<SkColor> CalculateProminentColorsOfBitmap(
|
||||
const SkBitmap& bitmap,
|
||||
const std::vector<ColorProfile>& color_profiles) {
|
||||
|
@ -16,6 +16,10 @@
|
||||
|
||||
class SkBitmap;
|
||||
|
||||
namespace gfx {
|
||||
class Rect;
|
||||
} // namespace gfx
|
||||
|
||||
namespace color_utils {
|
||||
|
||||
struct HSL;
|
||||
@ -136,6 +140,35 @@ struct ColorProfile {
|
||||
SaturationRange saturation = SaturationRange::MUTED;
|
||||
};
|
||||
|
||||
// A color value with an associated weight.
|
||||
struct Swatch {
|
||||
Swatch(SkColor color, size_t population)
|
||||
: color(color), population(population) {}
|
||||
|
||||
SkColor color;
|
||||
|
||||
// The population correlates to a count, so it should be 1 or greater.
|
||||
size_t population;
|
||||
|
||||
bool operator==(const Swatch& other) const {
|
||||
return color == other.color && population == other.population;
|
||||
}
|
||||
};
|
||||
|
||||
// Returns a vector of |Swatch| that represent the prominent colors of the
|
||||
// bitmap within |region|. The |max_swatches| is the maximum number of swatches.
|
||||
// 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. |exclude_uninteresting| will exclude colors that are not interesting
|
||||
// (e.g. too white or black).
|
||||
// This is an implementation of the Android Palette API:
|
||||
// https://developer.android.com/reference/android/support/v7/graphics/Palette
|
||||
GFX_EXPORT std::vector<Swatch> CalculateColorSwatches(
|
||||
const SkBitmap& bitmap,
|
||||
size_t max_swatches,
|
||||
const gfx::Rect& region,
|
||||
bool exclude_uninteresting);
|
||||
|
||||
// Returns a vector of RGB colors that represents the bitmap based on the
|
||||
// |color_profiles| provided. For each value, if a value is succesfully
|
||||
// calculated, the calculated value is fully opaque. For failure, the calculated
|
||||
|
@ -7,6 +7,7 @@
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <exception>
|
||||
#include <vector>
|
||||
|
||||
#include "skia/ext/platform_canvas.h"
|
||||
@ -546,4 +547,67 @@ TEST_F(ColorAnalysisTest, ComputeProminentColors) {
|
||||
EXPECT_EQ(expectations, computations);
|
||||
}
|
||||
|
||||
TEST_F(ColorAnalysisTest, ComputeColorSwatches) {
|
||||
SkBitmap bitmap;
|
||||
bitmap.allocN32Pixels(100, 100);
|
||||
bitmap.eraseColor(SK_ColorMAGENTA);
|
||||
bitmap.erase(SK_ColorGREEN, {10, 10, 90, 90});
|
||||
bitmap.erase(SK_ColorYELLOW, {40, 40, 60, 60});
|
||||
|
||||
const Swatch kYellowSwatch = Swatch(SK_ColorYELLOW, (20u * 20u));
|
||||
const Swatch kGreenSwatch =
|
||||
Swatch(SK_ColorGREEN, (80u * 80u) - kYellowSwatch.population);
|
||||
const Swatch kMagentaSwatch =
|
||||
Swatch(SK_ColorMAGENTA, (100u * 100u) - kGreenSwatch.population -
|
||||
kYellowSwatch.population);
|
||||
|
||||
{
|
||||
std::vector<Swatch> colors =
|
||||
CalculateColorSwatches(bitmap, 10, gfx::Rect(100, 100), false);
|
||||
EXPECT_EQ(3u, colors.size());
|
||||
EXPECT_EQ(kGreenSwatch, colors[0]);
|
||||
EXPECT_EQ(kMagentaSwatch, colors[1]);
|
||||
EXPECT_EQ(kYellowSwatch, colors[2]);
|
||||
}
|
||||
|
||||
{
|
||||
std::vector<Swatch> colors =
|
||||
CalculateColorSwatches(bitmap, 10, gfx::Rect(10, 10, 80, 80), false);
|
||||
EXPECT_EQ(2u, colors.size());
|
||||
EXPECT_EQ(kGreenSwatch, colors[0]);
|
||||
EXPECT_EQ(kYellowSwatch, colors[1]);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ColorAnalysisTest, ComputeColorSwatches_Uninteresting) {
|
||||
SkBitmap bitmap;
|
||||
bitmap.allocN32Pixels(100, 100);
|
||||
bitmap.eraseColor(SK_ColorMAGENTA);
|
||||
bitmap.erase(SK_ColorBLACK, {10, 10, 90, 90});
|
||||
bitmap.erase(SK_ColorWHITE, {40, 40, 60, 60});
|
||||
|
||||
const Swatch kWhiteSwatch = Swatch(SK_ColorWHITE, (20u * 20u));
|
||||
const Swatch kBlackSwatch =
|
||||
Swatch(SK_ColorBLACK, (80u * 80u) - kWhiteSwatch.population);
|
||||
const Swatch kMagentaSwatch =
|
||||
Swatch(SK_ColorMAGENTA,
|
||||
(100u * 100u) - kBlackSwatch.population - kWhiteSwatch.population);
|
||||
|
||||
{
|
||||
std::vector<Swatch> colors =
|
||||
CalculateColorSwatches(bitmap, 10, gfx::Rect(100, 100), true);
|
||||
EXPECT_EQ(1u, colors.size());
|
||||
EXPECT_EQ(kMagentaSwatch, colors[0]);
|
||||
}
|
||||
|
||||
{
|
||||
std::vector<Swatch> colors =
|
||||
CalculateColorSwatches(bitmap, 10, gfx::Rect(100, 100), false);
|
||||
EXPECT_EQ(3u, colors.size());
|
||||
EXPECT_EQ(kBlackSwatch, colors[0]);
|
||||
EXPECT_EQ(kMagentaSwatch, colors[1]);
|
||||
EXPECT_EQ(kWhiteSwatch, colors[2]);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace color_utils
|
||||
|
Reference in New Issue
Block a user