Skip to content

Commit 804c95f

Browse files
Material Engcopybara-github
authored andcommitted
Align color score libraries between implementations.
PiperOrigin-RevId: 531160188
1 parent e276211 commit 804c95f

File tree

8 files changed

+920
-461
lines changed

8 files changed

+920
-461
lines changed

cpp/score/score.cc

Lines changed: 71 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -18,135 +18,106 @@
1818

1919
#include <algorithm>
2020
#include <cmath>
21-
#include <cstdlib>
2221
#include <map>
22+
#include <utility>
2323
#include <vector>
2424

25-
#include "cpp/cam/cam.h"
25+
#include "cpp/cam/hct.h"
2626
#include "cpp/utils/utils.h"
2727

2828
namespace material_color_utilities {
2929

30-
constexpr double kCutoffChroma = 15.0;
31-
constexpr double kCutoffExcitedProportion = 0.01;
32-
constexpr double kCutoffTone = 10.0;
3330
constexpr double kTargetChroma = 48.0; // A1 Chroma
3431
constexpr double kWeightProportion = 0.7;
3532
constexpr double kWeightChromaAbove = 0.3;
3633
constexpr double kWeightChromaBelow = 0.1;
34+
constexpr double kCutoffChroma = 5.0;
35+
constexpr double kCutoffExcitedProportion = 0.01;
3736

38-
struct AnnotatedColor {
39-
Argb argb = 0;
40-
Cam cam;
41-
double excited_proportion = 0.0;
42-
double score = 0.0;
43-
};
44-
45-
bool ArgbAndScoreComparator(const AnnotatedColor& a, const AnnotatedColor& b) {
46-
return a.score > b.score;
47-
}
48-
49-
bool IsAcceptableColor(const AnnotatedColor& color) {
50-
return color.cam.chroma >= kCutoffChroma &&
51-
LstarFromArgb(color.argb) >= kCutoffTone &&
52-
color.excited_proportion >= kCutoffExcitedProportion;
53-
}
54-
55-
bool ColorsAreTooClose(const AnnotatedColor& color_one,
56-
const AnnotatedColor& color_two) {
57-
return DiffDegrees(color_one.cam.hue, color_two.cam.hue) < 15;
37+
bool CompareScoredHCT(const std::pair<Hct, double>& a,
38+
const std::pair<Hct, double>& b) {
39+
return a.second > b.second;
5840
}
5941

6042
std::vector<Argb> RankedSuggestions(
61-
const std::map<Argb, int>& argb_to_population) {
43+
const std::map<Argb, int>& argb_to_population,
44+
const ScoreOptions& options) {
45+
// Get the HCT color for each Argb value, while finding the per hue count and
46+
// total count.
47+
std::vector<Hct> colors_hct;
48+
std::vector<int> hue_population(360, 0);
6249
double population_sum = 0;
63-
int input_size = argb_to_population.size();
64-
65-
std::vector<Argb> argbs;
66-
std::vector<int> populations;
67-
68-
argbs.reserve(input_size);
69-
populations.reserve(input_size);
70-
71-
for (auto const& pair : argb_to_population) {
72-
argbs.push_back(pair.first);
73-
populations.push_back(pair.second);
74-
}
75-
76-
for (int i = 0; i < input_size; i++) {
77-
population_sum += populations[i];
50+
for (const auto& [argb, population] : argb_to_population) {
51+
Hct hct(argb);
52+
colors_hct.push_back(hct);
53+
int hue = floor(hct.get_hue());
54+
hue_population[hue] += population;
55+
population_sum += population;
7856
}
7957

80-
double hue_proportions[361] = {};
81-
std::vector<AnnotatedColor> colors;
82-
83-
for (int i = 0; i < input_size; i++) {
84-
double proportion = populations[i] / population_sum;
85-
86-
Cam cam = CamFromInt(argbs[i]);
87-
88-
int hue = SanitizeDegreesInt(round(cam.hue));
89-
hue_proportions[hue] += proportion;
90-
91-
colors.push_back({argbs[i], cam, 0, -1});
92-
}
93-
94-
for (int i = 0; i < input_size; i++) {
95-
int hue = round(colors[i].cam.hue);
96-
for (int j = (hue - 15); j < (hue + 15); j++) {
97-
int sanitized_hue = SanitizeDegreesInt(j);
98-
colors[i].excited_proportion += hue_proportions[sanitized_hue];
58+
// Hues with more usage in neighboring 30 degree slice get a larger number.
59+
std::vector<double> hue_excited_proportions(360, 0.0);
60+
for (int hue = 0; hue < 360; hue++) {
61+
double proportion = hue_population[hue] / population_sum;
62+
for (int i = hue - 14; i < hue + 16; i++) {
63+
int neighbor_hue = SanitizeDegreesInt(i);
64+
hue_excited_proportions[neighbor_hue] += proportion;
9965
}
10066
}
10167

102-
for (int i = 0; i < input_size; i++) {
103-
double proportion_score =
104-
colors[i].excited_proportion * 100.0 * kWeightProportion;
105-
106-
double chroma = colors[i].cam.chroma;
107-
double chroma_weight =
108-
(chroma > kTargetChroma ? kWeightChromaAbove : kWeightChromaBelow);
109-
double chroma_score = (chroma - kTargetChroma) * chroma_weight;
110-
111-
colors[i].score = chroma_score + proportion_score;
112-
}
113-
114-
std::sort(colors.begin(), colors.end(), ArgbAndScoreComparator);
115-
116-
std::vector<AnnotatedColor> selected_colors;
117-
118-
for (int i = 0; i < input_size; i++) {
119-
if (!IsAcceptableColor(colors[i])) {
68+
// Scores each HCT color based on usage and chroma, while optionally
69+
// filtering out values that do not have enough chroma or usage.
70+
std::vector<std::pair<Hct, double>> scored_hcts;
71+
for (Hct hct : colors_hct) {
72+
int hue = SanitizeDegreesInt(round(hct.get_hue()));
73+
double proportion = hue_excited_proportions[hue];
74+
if (options.filter && (hct.get_chroma() < kCutoffChroma ||
75+
proportion <= kCutoffExcitedProportion)) {
12076
continue;
12177
}
12278

123-
bool is_duplicate_color = false;
124-
for (size_t j = 0; j < selected_colors.size(); j++) {
125-
if (ColorsAreTooClose(selected_colors[j], colors[i])) {
126-
is_duplicate_color = true;
127-
break;
79+
double proportion_score = proportion * 100.0 * kWeightProportion;
80+
double chroma_weight = hct.get_chroma() < kTargetChroma
81+
? kWeightChromaBelow
82+
: kWeightChromaAbove;
83+
double chroma_score = (hct.get_chroma() - kTargetChroma) * chroma_weight;
84+
double score = proportion_score + chroma_score;
85+
scored_hcts.push_back({hct, score});
86+
}
87+
// Sorted so that colors with higher scores come first.
88+
sort(scored_hcts.begin(), scored_hcts.end(), CompareScoredHCT);
89+
90+
// Iterates through potential hue differences in degrees in order to select
91+
// the colors with the largest distribution of hues possible. Starting at
92+
// 90 degrees(maximum difference for 4 colors) then decreasing down to a
93+
// 15 degree minimum.
94+
std::vector<Hct> chosen_colors;
95+
for (int difference_degrees = 90; difference_degrees >= 15;
96+
difference_degrees--) {
97+
chosen_colors.clear();
98+
for (auto entry : scored_hcts) {
99+
Hct hct = entry.first;
100+
auto duplicate_hue = std::find_if(
101+
chosen_colors.begin(), chosen_colors.end(),
102+
[&hct, difference_degrees](Hct chosen_hct) {
103+
return DiffDegrees(hct.get_hue(), chosen_hct.get_hue()) <
104+
difference_degrees;
105+
});
106+
if (duplicate_hue == chosen_colors.end()) {
107+
chosen_colors.push_back(hct);
108+
if (chosen_colors.size() >= options.desired) break;
128109
}
129110
}
130-
131-
if (is_duplicate_color) {
132-
continue;
133-
}
134-
135-
selected_colors.push_back(colors[i]);
111+
if (chosen_colors.size() >= options.desired) break;
136112
}
137-
138-
// Use google blue if no colors are selected.
139-
if (selected_colors.empty()) {
140-
selected_colors.push_back({0xFF4285F4, {}, 0.0, 0.0});
113+
std::vector<Argb> colors;
114+
if (chosen_colors.empty()) {
115+
colors.push_back(options.fallback_color_argb);
141116
}
142-
143-
std::vector<Argb> return_value(selected_colors.size());
144-
145-
for (size_t j = 0; j < selected_colors.size(); j++) {
146-
return_value[j] = selected_colors[j].argb;
117+
for (auto chosen_hct : chosen_colors) {
118+
colors.push_back(chosen_hct.ToInt());
147119
}
148-
149-
return return_value;
120+
return colors;
150121
}
151122

152123
} // namespace material_color_utilities

cpp/score/score.h

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,43 @@
1717
#ifndef CPP_SCORE_SCORE_H_
1818
#define CPP_SCORE_SCORE_H_
1919

20+
#include <cstdlib>
2021
#include <map>
2122
#include <vector>
2223

2324
#include "cpp/utils/utils.h"
2425

2526
namespace material_color_utilities {
2627

28+
/**
29+
* Default options for ranking colors based on usage counts.
30+
* `desired`: is the max count of the colors returned.
31+
* `fallback_color_argb`: Is the default color that should be used if no
32+
* other colors are suitable.
33+
* `filter`: controls if the resulting colors should be filtered to not include
34+
* hues that are not used often enough, and colors that are effectively
35+
* grayscale.
36+
*/
37+
struct ScoreOptions {
38+
size_t desired = 4; // 4 colors matches the Android wallpaper picker.
39+
int fallback_color_argb = 0xff4285f4; // Google Blue.
40+
bool filter = true; // Avoid unsuitable colors.
41+
};
42+
43+
/**
44+
* Given a map with keys of colors and values of how often the color appears,
45+
* rank the colors based on suitability for being used for a UI theme.
46+
*
47+
* The list returned is of length <= [desired]. The recommended color is the
48+
* first item, the least suitable is the last. There will always be at least
49+
* one color returned. If all the input colors were not suitable for a theme,
50+
* a default fallback color will be provided, Google Blue, or supplied fallback
51+
* color. The default number of colors returned is 4, simply because that's the
52+
* # of colors display in Android 12's wallpaper picker.
53+
*/
2754
std::vector<Argb> RankedSuggestions(
28-
const std::map<Argb, int>& argb_to_population);
55+
const std::map<Argb, int>& argb_to_population,
56+
const ScoreOptions& options = {});
2957
} // namespace material_color_utilities
3058

3159
#endif // CPP_SCORE_SCORE_H_

0 commit comments

Comments
 (0)