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 }