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