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 }