1 /* 2 * Copyright (C) 2017 The Android Open Source Project 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.android.internal.graphics.palette; 18 19 /* 20 * Copyright 2014 The Android Open Source Project 21 * 22 * Licensed under the Apache License, Version 2.0 (the "License"); 23 * you may not use this file except in compliance with the License. 24 * You may obtain a copy of the License at 25 * 26 * http://www.apache.org/licenses/LICENSE-2.0 27 * 28 * Unless required by applicable law or agreed to in writing, software 29 * distributed under the License is distributed on an "AS IS" BASIS, 30 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31 * See the License for the specific language governing permissions and 32 * limitations under the License. 33 */ 34 35 import android.graphics.Color; 36 import android.util.TimingLogger; 37 38 import com.android.internal.graphics.palette.Palette.Swatch; 39 40 import java.util.ArrayList; 41 import java.util.Arrays; 42 import java.util.Collection; 43 import java.util.Comparator; 44 import java.util.List; 45 import java.util.PriorityQueue; 46 47 /** 48 * Copied from: frameworks/support/v7/palette/src/main/java/android/support/v7/ 49 * graphics/ColorCutQuantizer.java 50 * 51 * An color quantizer based on the Median-cut algorithm, but optimized for picking out distinct 52 * colors rather than representation colors. 53 * 54 * The color space is represented as a 3-dimensional cube with each dimension being an RGB 55 * component. The cube is then repeatedly divided until we have reduced the color space to the 56 * requested number of colors. An average color is then generated from each cube. 57 * 58 * What makes this different to median-cut is that median-cut divided cubes so that all of the cubes 59 * have roughly the same population, where this quantizer divides boxes based on their color volume. 60 * This means that the color space is divided into distinct colors, rather than representative 61 * colors. 62 */ 63 final class ColorCutQuantizer implements Quantizer { 64 65 private static final String LOG_TAG = "ColorCutQuantizer"; 66 private static final boolean LOG_TIMINGS = false; 67 68 static final int COMPONENT_RED = -3; 69 static final int COMPONENT_GREEN = -2; 70 static final int COMPONENT_BLUE = -1; 71 72 private static final int QUANTIZE_WORD_WIDTH = 5; 73 private static final int QUANTIZE_WORD_MASK = (1 << QUANTIZE_WORD_WIDTH) - 1; 74 75 int[] mColors; 76 int[] mHistogram; 77 List<Swatch> mQuantizedColors; 78 TimingLogger mTimingLogger; 79 80 private final float[] mTempHsl = new float[3]; 81 82 /** 83 * Execute color quantization. 84 * 85 * @param pixels histogram representing an image's pixel data 86 * @param maxColors The maximum number of colors that should be in the result palette. 87 */ quantize(final int[] pixels, final int maxColors)88 public void quantize(final int[] pixels, final int maxColors) { 89 mTimingLogger = LOG_TIMINGS ? new TimingLogger(LOG_TAG, "Creation") : null; 90 91 final int[] hist = mHistogram = new int[1 << (QUANTIZE_WORD_WIDTH * 3)]; 92 for (int i = 0; i < pixels.length; i++) { 93 final int quantizedColor = quantizeFromRgb888(pixels[i]); 94 // Now update the pixel value to the quantized value 95 pixels[i] = quantizedColor; 96 // And update the histogram 97 hist[quantizedColor]++; 98 } 99 100 if (LOG_TIMINGS) { 101 mTimingLogger.addSplit("Histogram created"); 102 } 103 104 // Now let's count the number of distinct colors 105 int distinctColorCount = 0; 106 for (int color = 0; color < hist.length; color++) { 107 if (hist[color] > 0) { 108 // If the color has population, increase the distinct color count 109 distinctColorCount++; 110 } 111 } 112 113 if (LOG_TIMINGS) { 114 mTimingLogger.addSplit("Filtered colors and distinct colors counted"); 115 } 116 117 // Now lets go through create an array consisting of only distinct colors 118 final int[] colors = mColors = new int[distinctColorCount]; 119 int distinctColorIndex = 0; 120 for (int color = 0; color < hist.length; color++) { 121 if (hist[color] > 0) { 122 colors[distinctColorIndex++] = color; 123 } 124 } 125 126 if (LOG_TIMINGS) { 127 mTimingLogger.addSplit("Distinct colors copied into array"); 128 } 129 130 if (distinctColorCount <= maxColors) { 131 // The image has fewer colors than the maximum requested, so just return the colors 132 mQuantizedColors = new ArrayList<>(); 133 for (int color : colors) { 134 mQuantizedColors.add(new Swatch(approximateToRgb888(color), hist[color])); 135 } 136 137 if (LOG_TIMINGS) { 138 mTimingLogger.addSplit("Too few colors present. Copied to Swatches"); 139 mTimingLogger.dumpToLog(); 140 } 141 } else { 142 // We need use quantization to reduce the number of colors 143 mQuantizedColors = quantizePixels(maxColors); 144 145 if (LOG_TIMINGS) { 146 mTimingLogger.addSplit("Quantized colors computed"); 147 mTimingLogger.dumpToLog(); 148 } 149 } 150 } 151 152 /** 153 * @return the list of quantized colors 154 */ getQuantizedColors()155 public List<Swatch> getQuantizedColors() { 156 return mQuantizedColors; 157 } 158 quantizePixels(int maxColors)159 private List<Swatch> quantizePixels(int maxColors) { 160 // Create the priority queue which is sorted by volume descending. This means we always 161 // split the largest box in the queue 162 final PriorityQueue<Vbox> pq = new PriorityQueue<>(maxColors, VBOX_COMPARATOR_VOLUME); 163 164 // To start, offer a box which contains all of the colors 165 pq.offer(new Vbox(0, mColors.length - 1)); 166 167 // Now go through the boxes, splitting them until we have reached maxColors or there are no 168 // more boxes to split 169 splitBoxes(pq, maxColors); 170 171 // Finally, return the average colors of the color boxes 172 return generateAverageColors(pq); 173 } 174 175 /** 176 * Iterate through the {@link java.util.Queue}, popping 177 * {@link ColorCutQuantizer.Vbox} objects from the queue 178 * and splitting them. Once split, the new box and the remaining box are offered back to the 179 * queue. 180 * 181 * @param queue {@link java.util.PriorityQueue} to poll for boxes 182 * @param maxSize Maximum amount of boxes to split 183 */ splitBoxes(final PriorityQueue<Vbox> queue, final int maxSize)184 private void splitBoxes(final PriorityQueue<Vbox> queue, final int maxSize) { 185 while (queue.size() < maxSize) { 186 final Vbox vbox = queue.poll(); 187 188 if (vbox != null && vbox.canSplit()) { 189 // First split the box, and offer the result 190 queue.offer(vbox.splitBox()); 191 192 if (LOG_TIMINGS) { 193 mTimingLogger.addSplit("Box split"); 194 } 195 // Then offer the box back 196 queue.offer(vbox); 197 } else { 198 if (LOG_TIMINGS) { 199 mTimingLogger.addSplit("All boxes split"); 200 } 201 // If we get here then there are no more boxes to split, so return 202 return; 203 } 204 } 205 } 206 generateAverageColors(Collection<Vbox> vboxes)207 private List<Swatch> generateAverageColors(Collection<Vbox> vboxes) { 208 ArrayList<Swatch> colors = new ArrayList<>(vboxes.size()); 209 for (Vbox vbox : vboxes) { 210 Swatch swatch = vbox.getAverageColor(); 211 colors.add(swatch); 212 } 213 return colors; 214 } 215 216 /** 217 * Represents a tightly fitting box around a color space. 218 */ 219 private class Vbox { 220 // lower and upper index are inclusive 221 private final int mLowerIndex; 222 private int mUpperIndex; 223 // Population of colors within this box 224 private int mPopulation; 225 226 private int mMinRed, mMaxRed; 227 private int mMinGreen, mMaxGreen; 228 private int mMinBlue, mMaxBlue; 229 Vbox(int lowerIndex, int upperIndex)230 Vbox(int lowerIndex, int upperIndex) { 231 mLowerIndex = lowerIndex; 232 mUpperIndex = upperIndex; 233 fitBox(); 234 } 235 getVolume()236 final int getVolume() { 237 return (mMaxRed - mMinRed + 1) * (mMaxGreen - mMinGreen + 1) * 238 (mMaxBlue - mMinBlue + 1); 239 } 240 canSplit()241 final boolean canSplit() { 242 return getColorCount() > 1; 243 } 244 getColorCount()245 final int getColorCount() { 246 return 1 + mUpperIndex - mLowerIndex; 247 } 248 249 /** 250 * Recomputes the boundaries of this box to tightly fit the colors within the box. 251 */ fitBox()252 final void fitBox() { 253 final int[] colors = mColors; 254 final int[] hist = mHistogram; 255 256 // Reset the min and max to opposite values 257 int minRed, minGreen, minBlue; 258 minRed = minGreen = minBlue = Integer.MAX_VALUE; 259 int maxRed, maxGreen, maxBlue; 260 maxRed = maxGreen = maxBlue = Integer.MIN_VALUE; 261 int count = 0; 262 263 for (int i = mLowerIndex; i <= mUpperIndex; i++) { 264 final int color = colors[i]; 265 count += hist[color]; 266 267 final int r = quantizedRed(color); 268 final int g = quantizedGreen(color); 269 final int b = quantizedBlue(color); 270 if (r > maxRed) { 271 maxRed = r; 272 } 273 if (r < minRed) { 274 minRed = r; 275 } 276 if (g > maxGreen) { 277 maxGreen = g; 278 } 279 if (g < minGreen) { 280 minGreen = g; 281 } 282 if (b > maxBlue) { 283 maxBlue = b; 284 } 285 if (b < minBlue) { 286 minBlue = b; 287 } 288 } 289 290 mMinRed = minRed; 291 mMaxRed = maxRed; 292 mMinGreen = minGreen; 293 mMaxGreen = maxGreen; 294 mMinBlue = minBlue; 295 mMaxBlue = maxBlue; 296 mPopulation = count; 297 } 298 299 /** 300 * Split this color box at the mid-point along its longest dimension 301 * 302 * @return the new ColorBox 303 */ splitBox()304 final Vbox splitBox() { 305 if (!canSplit()) { 306 throw new IllegalStateException("Can not split a box with only 1 color"); 307 } 308 309 // find median along the longest dimension 310 final int splitPoint = findSplitPoint(); 311 312 Vbox newBox = new Vbox(splitPoint + 1, mUpperIndex); 313 314 // Now change this box's upperIndex and recompute the color boundaries 315 mUpperIndex = splitPoint; 316 fitBox(); 317 318 return newBox; 319 } 320 321 /** 322 * @return the dimension which this box is largest in 323 */ getLongestColorDimension()324 final int getLongestColorDimension() { 325 final int redLength = mMaxRed - mMinRed; 326 final int greenLength = mMaxGreen - mMinGreen; 327 final int blueLength = mMaxBlue - mMinBlue; 328 329 if (redLength >= greenLength && redLength >= blueLength) { 330 return COMPONENT_RED; 331 } else if (greenLength >= redLength && greenLength >= blueLength) { 332 return COMPONENT_GREEN; 333 } else { 334 return COMPONENT_BLUE; 335 } 336 } 337 338 /** 339 * Finds the point within this box's lowerIndex and upperIndex index of where to split. 340 * 341 * This is calculated by finding the longest color dimension, and then sorting the 342 * sub-array based on that dimension value in each color. The colors are then iterated over 343 * until a color is found with at least the midpoint of the whole box's dimension midpoint. 344 * 345 * @return the index of the colors array to split from 346 */ findSplitPoint()347 final int findSplitPoint() { 348 final int longestDimension = getLongestColorDimension(); 349 final int[] colors = mColors; 350 final int[] hist = mHistogram; 351 352 // We need to sort the colors in this box based on the longest color dimension. 353 // As we can't use a Comparator to define the sort logic, we modify each color so that 354 // its most significant is the desired dimension 355 modifySignificantOctet(colors, longestDimension, mLowerIndex, mUpperIndex); 356 357 // Now sort... Arrays.sort uses a exclusive toIndex so we need to add 1 358 Arrays.sort(colors, mLowerIndex, mUpperIndex + 1); 359 360 // Now revert all of the colors so that they are packed as RGB again 361 modifySignificantOctet(colors, longestDimension, mLowerIndex, mUpperIndex); 362 363 final int midPoint = mPopulation / 2; 364 for (int i = mLowerIndex, count = 0; i <= mUpperIndex; i++) { 365 count += hist[colors[i]]; 366 if (count >= midPoint) { 367 // we never want to split on the upperIndex, as this will result in the same 368 // box 369 return Math.min(mUpperIndex - 1, i); 370 } 371 } 372 373 return mLowerIndex; 374 } 375 376 /** 377 * @return the average color of this box. 378 */ getAverageColor()379 final Swatch getAverageColor() { 380 final int[] colors = mColors; 381 final int[] hist = mHistogram; 382 int redSum = 0; 383 int greenSum = 0; 384 int blueSum = 0; 385 int totalPopulation = 0; 386 387 for (int i = mLowerIndex; i <= mUpperIndex; i++) { 388 final int color = colors[i]; 389 final int colorPopulation = hist[color]; 390 391 totalPopulation += colorPopulation; 392 redSum += colorPopulation * quantizedRed(color); 393 greenSum += colorPopulation * quantizedGreen(color); 394 blueSum += colorPopulation * quantizedBlue(color); 395 } 396 397 final int redMean = Math.round(redSum / (float) totalPopulation); 398 final int greenMean = Math.round(greenSum / (float) totalPopulation); 399 final int blueMean = Math.round(blueSum / (float) totalPopulation); 400 401 return new Swatch(approximateToRgb888(redMean, greenMean, blueMean), totalPopulation); 402 } 403 } 404 405 /** 406 * Modify the significant octet in a packed color int. Allows sorting based on the value of a 407 * single color component. This relies on all components being the same word size. 408 * 409 * @see Vbox#findSplitPoint() 410 */ modifySignificantOctet(final int[] a, final int dimension, final int lower, final int upper)411 static void modifySignificantOctet(final int[] a, final int dimension, 412 final int lower, final int upper) { 413 switch (dimension) { 414 case COMPONENT_RED: 415 // Already in RGB, no need to do anything 416 break; 417 case COMPONENT_GREEN: 418 // We need to do a RGB to GRB swap, or vice-versa 419 for (int i = lower; i <= upper; i++) { 420 final int color = a[i]; 421 a[i] = quantizedGreen(color) << (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH) 422 | quantizedRed(color) << QUANTIZE_WORD_WIDTH 423 | quantizedBlue(color); 424 } 425 break; 426 case COMPONENT_BLUE: 427 // We need to do a RGB to BGR swap, or vice-versa 428 for (int i = lower; i <= upper; i++) { 429 final int color = a[i]; 430 a[i] = quantizedBlue(color) << (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH) 431 | quantizedGreen(color) << QUANTIZE_WORD_WIDTH 432 | quantizedRed(color); 433 } 434 break; 435 } 436 } 437 438 /** 439 * Comparator which sorts {@link Vbox} instances based on their volume, in descending order 440 */ 441 private static final Comparator<Vbox> VBOX_COMPARATOR_VOLUME = new Comparator<Vbox>() { 442 @Override 443 public int compare(Vbox lhs, Vbox rhs) { 444 return rhs.getVolume() - lhs.getVolume(); 445 } 446 }; 447 448 /** 449 * Quantized a RGB888 value to have a word width of {@value #QUANTIZE_WORD_WIDTH}. 450 */ quantizeFromRgb888(int color)451 private static int quantizeFromRgb888(int color) { 452 int r = modifyWordWidth(Color.red(color), 8, QUANTIZE_WORD_WIDTH); 453 int g = modifyWordWidth(Color.green(color), 8, QUANTIZE_WORD_WIDTH); 454 int b = modifyWordWidth(Color.blue(color), 8, QUANTIZE_WORD_WIDTH); 455 return r << (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH) | g << QUANTIZE_WORD_WIDTH | b; 456 } 457 458 /** 459 * Quantized RGB888 values to have a word width of {@value #QUANTIZE_WORD_WIDTH}. 460 */ approximateToRgb888(int r, int g, int b)461 static int approximateToRgb888(int r, int g, int b) { 462 return Color.rgb(modifyWordWidth(r, QUANTIZE_WORD_WIDTH, 8), 463 modifyWordWidth(g, QUANTIZE_WORD_WIDTH, 8), 464 modifyWordWidth(b, QUANTIZE_WORD_WIDTH, 8)); 465 } 466 approximateToRgb888(int color)467 private static int approximateToRgb888(int color) { 468 return approximateToRgb888(quantizedRed(color), quantizedGreen(color), 469 quantizedBlue(color)); 470 } 471 472 /** 473 * @return red component of the quantized color 474 */ quantizedRed(int color)475 static int quantizedRed(int color) { 476 return (color >> (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH)) & QUANTIZE_WORD_MASK; 477 } 478 479 /** 480 * @return green component of a quantized color 481 */ quantizedGreen(int color)482 static int quantizedGreen(int color) { 483 return (color >> QUANTIZE_WORD_WIDTH) & QUANTIZE_WORD_MASK; 484 } 485 486 /** 487 * @return blue component of a quantized color 488 */ quantizedBlue(int color)489 static int quantizedBlue(int color) { 490 return color & QUANTIZE_WORD_MASK; 491 } 492 modifyWordWidth(int value, int currentWidth, int targetWidth)493 private static int modifyWordWidth(int value, int currentWidth, int targetWidth) { 494 final int newValue; 495 if (targetWidth > currentWidth) { 496 // If we're approximating up in word width, we'll shift up 497 newValue = value << (targetWidth - currentWidth); 498 } else { 499 // Else, we will just shift and keep the MSB 500 newValue = value >> (currentWidth - targetWidth); 501 } 502 return newValue & ((1 << targetWidth) - 1); 503 } 504 505 }