diff options
Diffstat (limited to 'score/Score.java')
-rw-r--r-- | score/Score.java | 176 |
1 files changed, 176 insertions, 0 deletions
diff --git a/score/Score.java b/score/Score.java new file mode 100644 index 0000000..ca28d62 --- /dev/null +++ b/score/Score.java @@ -0,0 +1,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); + } + } +} |