1 /* 2 * Copyright 2021 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.ux.material.libmonet.score; 18 19 import com.google.ux.material.libmonet.hct.Hct; 20 import com.google.ux.material.libmonet.utils.MathUtils; 21 import java.util.ArrayList; 22 import java.util.Collections; 23 import java.util.Comparator; 24 import java.util.List; 25 import java.util.Map; 26 27 /** 28 * Given a large set of colors, remove colors that are unsuitable for a UI theme, and rank the rest 29 * based on suitability. 30 * 31 * <p>Enables use of a high cluster count for image quantization, thus ensuring colors aren't 32 * muddied, while curating the high cluster count to a much smaller number of appropriate choices. 33 */ 34 public final class Score { 35 private static final double TARGET_CHROMA = 48.; // A1 Chroma 36 private static final double WEIGHT_PROPORTION = 0.7; 37 private static final double WEIGHT_CHROMA_ABOVE = 0.3; 38 private static final double WEIGHT_CHROMA_BELOW = 0.1; 39 private static final double CUTOFF_CHROMA = 5.; 40 private static final double CUTOFF_EXCITED_PROPORTION = 0.01; 41 Score()42 private Score() {} 43 score(Map<Integer, Integer> colorsToPopulation)44 public static List<Integer> score(Map<Integer, Integer> colorsToPopulation) { 45 // Fallback color is Google Blue. 46 return score(colorsToPopulation, 4, 0xff4285f4, true); 47 } 48 score(Map<Integer, Integer> colorsToPopulation, int desired)49 public static List<Integer> score(Map<Integer, Integer> colorsToPopulation, int desired) { 50 return score(colorsToPopulation, desired, 0xff4285f4, true); 51 } 52 score( Map<Integer, Integer> colorsToPopulation, int desired, int fallbackColorArgb)53 public static List<Integer> score( 54 Map<Integer, Integer> colorsToPopulation, int desired, int fallbackColorArgb) { 55 return score(colorsToPopulation, desired, fallbackColorArgb, true); 56 } 57 58 /** 59 * Given a map with keys of colors and values of how often the color appears, rank the colors 60 * based on suitability for being used for a UI theme. 61 * 62 * @param colorsToPopulation map with keys of colors and values of how often the color appears, 63 * usually from a source image. 64 * @param desired max count of colors to be returned in the list. 65 * @param fallbackColorArgb color to be returned if no other options available. 66 * @param filter whether to filter out undesireable combinations. 67 * @return Colors sorted by suitability for a UI theme. The most suitable color is the first item, 68 * the least suitable is the last. There will always be at least one color returned. If all 69 * the input colors were not suitable for a theme, a default fallback color will be provided, 70 * Google Blue. 71 */ score( Map<Integer, Integer> colorsToPopulation, int desired, int fallbackColorArgb, boolean filter)72 public static List<Integer> score( 73 Map<Integer, Integer> colorsToPopulation, 74 int desired, 75 int fallbackColorArgb, 76 boolean filter) { 77 78 // Get the HCT color for each Argb value, while finding the per hue count and 79 // total count. 80 List<Hct> colorsHct = new ArrayList<>(); 81 int[] huePopulation = new int[360]; 82 double populationSum = 0.; 83 for (Map.Entry<Integer, Integer> entry : colorsToPopulation.entrySet()) { 84 Hct hct = Hct.fromInt(entry.getKey()); 85 colorsHct.add(hct); 86 int hue = (int) Math.floor(hct.getHue()); 87 huePopulation[hue] += entry.getValue(); 88 populationSum += entry.getValue(); 89 } 90 91 // Hues with more usage in neighboring 30 degree slice get a larger number. 92 double[] hueExcitedProportions = new double[360]; 93 for (int hue = 0; hue < 360; hue++) { 94 double proportion = huePopulation[hue] / populationSum; 95 for (int i = hue - 14; i < hue + 16; i++) { 96 int neighborHue = MathUtils.sanitizeDegreesInt(i); 97 hueExcitedProportions[neighborHue] += proportion; 98 } 99 } 100 101 // Scores each HCT color based on usage and chroma, while optionally 102 // filtering out values that do not have enough chroma or usage. 103 List<ScoredHCT> scoredHcts = new ArrayList<>(); 104 for (Hct hct : colorsHct) { 105 int hue = MathUtils.sanitizeDegreesInt((int) Math.round(hct.getHue())); 106 double proportion = hueExcitedProportions[hue]; 107 if (filter && (hct.getChroma() < CUTOFF_CHROMA || proportion <= CUTOFF_EXCITED_PROPORTION)) { 108 continue; 109 } 110 111 double proportionScore = proportion * 100.0 * WEIGHT_PROPORTION; 112 double chromaWeight = 113 hct.getChroma() < TARGET_CHROMA ? WEIGHT_CHROMA_BELOW : WEIGHT_CHROMA_ABOVE; 114 double chromaScore = (hct.getChroma() - TARGET_CHROMA) * chromaWeight; 115 double score = proportionScore + chromaScore; 116 scoredHcts.add(new ScoredHCT(hct, score)); 117 } 118 // Sorted so that colors with higher scores come first. 119 Collections.sort(scoredHcts, new ScoredComparator()); 120 121 // Iterates through potential hue differences in degrees in order to select 122 // the colors with the largest distribution of hues possible. Starting at 123 // 90 degrees(maximum difference for 4 colors) then decreasing down to a 124 // 15 degree minimum. 125 List<Hct> chosenColors = new ArrayList<>(); 126 for (int differenceDegrees = 90; differenceDegrees >= 15; differenceDegrees--) { 127 chosenColors.clear(); 128 for (ScoredHCT entry : scoredHcts) { 129 Hct hct = entry.hct; 130 boolean hasDuplicateHue = false; 131 for (Hct chosenHct : chosenColors) { 132 if (MathUtils.differenceDegrees(hct.getHue(), chosenHct.getHue()) < differenceDegrees) { 133 hasDuplicateHue = true; 134 break; 135 } 136 } 137 if (!hasDuplicateHue) { 138 chosenColors.add(hct); 139 } 140 if (chosenColors.size() >= desired) { 141 break; 142 } 143 } 144 if (chosenColors.size() >= desired) { 145 break; 146 } 147 } 148 List<Integer> colors = new ArrayList<>(); 149 if (chosenColors.isEmpty()) { 150 colors.add(fallbackColorArgb); 151 } 152 for (Hct chosenHct : chosenColors) { 153 colors.add(chosenHct.toInt()); 154 } 155 return colors; 156 } 157 158 private static class ScoredHCT { 159 public final Hct hct; 160 public final double score; 161 ScoredHCT(Hct hct, double score)162 public ScoredHCT(Hct hct, double score) { 163 this.hct = hct; 164 this.score = score; 165 } 166 } 167 168 private static class ScoredComparator implements Comparator<ScoredHCT> { ScoredComparator()169 public ScoredComparator() {} 170 171 @Override compare(ScoredHCT entry1, ScoredHCT entry2)172 public int compare(ScoredHCT entry1, ScoredHCT entry2) { 173 return Double.compare(entry2.score, entry1.score); 174 } 175 } 176 } 177