aboutsummaryrefslogtreecommitdiff
path: root/score/Score.java
blob: ca28d6269769223eaacdb837ea0d1f66d57456dc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
/*
 * Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.ux.material.libmonet.score;

import com.google.ux.material.libmonet.hct.Hct;
import com.google.ux.material.libmonet.utils.MathUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

/**
 * Given a large set of colors, remove colors that are unsuitable for a UI theme, and rank the rest
 * based on suitability.
 *
 * <p>Enables use of a high cluster count for image quantization, thus ensuring colors aren't
 * muddied, while curating the high cluster count to a much smaller number of appropriate choices.
 */
public final class Score {
  private static final double TARGET_CHROMA = 48.; // A1 Chroma
  private static final double WEIGHT_PROPORTION = 0.7;
  private static final double WEIGHT_CHROMA_ABOVE = 0.3;
  private static final double WEIGHT_CHROMA_BELOW = 0.1;
  private static final double CUTOFF_CHROMA = 5.;
  private static final double CUTOFF_EXCITED_PROPORTION = 0.01;

  private Score() {}

  public static List<Integer> score(Map<Integer, Integer> colorsToPopulation) {
    // Fallback color is Google Blue.
    return score(colorsToPopulation, 4, 0xff4285f4, true);
  }

  public static List<Integer> score(Map<Integer, Integer> colorsToPopulation, int desired) {
    return score(colorsToPopulation, desired, 0xff4285f4, true);
  }

  public static List<Integer> score(
      Map<Integer, Integer> colorsToPopulation, int desired, int fallbackColorArgb) {
    return score(colorsToPopulation, desired, fallbackColorArgb, true);
  }

  /**
   * Given a map with keys of colors and values of how often the color appears, rank the colors
   * based on suitability for being used for a UI theme.
   *
   * @param colorsToPopulation map with keys of colors and values of how often the color appears,
   *     usually from a source image.
   * @param desired max count of colors to be returned in the list.
   * @param fallbackColorArgb color to be returned if no other options available.
   * @param filter whether to filter out undesireable combinations.
   * @return Colors sorted by suitability for a UI theme. The most suitable color is the first item,
   *     the least suitable is the last. There will always be at least one color returned. If all
   *     the input colors were not suitable for a theme, a default fallback color will be provided,
   *     Google Blue.
   */
  public static List<Integer> score(
      Map<Integer, Integer> colorsToPopulation,
      int desired,
      int fallbackColorArgb,
      boolean filter) {

    // Get the HCT color for each Argb value, while finding the per hue count and
    // total count.
    List<Hct> colorsHct = new ArrayList<>();
    int[] huePopulation = new int[360];
    double populationSum = 0.;
    for (Map.Entry<Integer, Integer> entry : colorsToPopulation.entrySet()) {
      Hct hct = Hct.fromInt(entry.getKey());
      colorsHct.add(hct);
      int hue = (int) Math.floor(hct.getHue());
      huePopulation[hue] += entry.getValue();
      populationSum += entry.getValue();
    }

    // Hues with more usage in neighboring 30 degree slice get a larger number.
    double[] hueExcitedProportions = new double[360];
    for (int hue = 0; hue < 360; hue++) {
      double proportion = huePopulation[hue] / populationSum;
      for (int i = hue - 14; i < hue + 16; i++) {
        int neighborHue = MathUtils.sanitizeDegreesInt(i);
        hueExcitedProportions[neighborHue] += proportion;
      }
    }

    // Scores each HCT color based on usage and chroma, while optionally
    // filtering out values that do not have enough chroma or usage.
    List<ScoredHCT> scoredHcts = new ArrayList<>();
    for (Hct hct : colorsHct) {
      int hue = MathUtils.sanitizeDegreesInt((int) Math.round(hct.getHue()));
      double proportion = hueExcitedProportions[hue];
      if (filter && (hct.getChroma() < CUTOFF_CHROMA || proportion <= CUTOFF_EXCITED_PROPORTION)) {
        continue;
      }

      double proportionScore = proportion * 100.0 * WEIGHT_PROPORTION;
      double chromaWeight =
          hct.getChroma() < TARGET_CHROMA ? WEIGHT_CHROMA_BELOW : WEIGHT_CHROMA_ABOVE;
      double chromaScore = (hct.getChroma() - TARGET_CHROMA) * chromaWeight;
      double score = proportionScore + chromaScore;
      scoredHcts.add(new ScoredHCT(hct, score));
    }
    // Sorted so that colors with higher scores come first.
    Collections.sort(scoredHcts, new ScoredComparator());

    // Iterates through potential hue differences in degrees in order to select
    // the colors with the largest distribution of hues possible. Starting at
    // 90 degrees(maximum difference for 4 colors) then decreasing down to a
    // 15 degree minimum.
    List<Hct> chosenColors = new ArrayList<>();
    for (int differenceDegrees = 90; differenceDegrees >= 15; differenceDegrees--) {
      chosenColors.clear();
      for (ScoredHCT entry : scoredHcts) {
        Hct hct = entry.hct;
        boolean hasDuplicateHue = false;
        for (Hct chosenHct : chosenColors) {
          if (MathUtils.differenceDegrees(hct.getHue(), chosenHct.getHue()) < differenceDegrees) {
            hasDuplicateHue = true;
            break;
          }
        }
        if (!hasDuplicateHue) {
          chosenColors.add(hct);
        }
        if (chosenColors.size() >= desired) {
          break;
        }
      }
      if (chosenColors.size() >= desired) {
        break;
      }
    }
    List<Integer> colors = new ArrayList<>();
    if (chosenColors.isEmpty()) {
      colors.add(fallbackColorArgb);
    }
    for (Hct chosenHct : chosenColors) {
      colors.add(chosenHct.toInt());
    }
    return colors;
  }

  private static class ScoredHCT {
    public final Hct hct;
    public final double score;

    public ScoredHCT(Hct hct, double score) {
      this.hct = hct;
      this.score = score;
    }
  }

  private static class ScoredComparator implements Comparator<ScoredHCT> {
    public ScoredComparator() {}

    @Override
    public int compare(ScoredHCT entry1, ScoredHCT entry2) {
      return Double.compare(entry2.score, entry1.score);
    }
  }
}