Home | History | Annotate | Download | only in palette
      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      */
     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      */
    163     public List<Swatch> getQuantizedColors() {
    164         return mQuantizedColors;
    165     }
    166 
    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      */
    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 
    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 
    242         Vbox(int lowerIndex, int upperIndex) {
    243             mLowerIndex = lowerIndex;
    244             mUpperIndex = upperIndex;
    245             fitBox();
    246         }
    247 
    248         final int getVolume() {
    249             return (mMaxRed - mMinRed + 1) * (mMaxGreen - mMinGreen + 1) *
    250                     (mMaxBlue - mMinBlue + 1);
    251         }
    252 
    253         final boolean canSplit() {
    254             return getColorCount() > 1;
    255         }
    256 
    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          */
    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          */
    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          */
    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          */
    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          */
    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      */
    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 
    450     private boolean shouldIgnoreColor(int color565) {
    451         final int rgb = approximateToRgb888(color565);
    452         ColorUtils.colorToHSL(rgb, mTempHsl);
    453         return shouldIgnoreColor(rgb, mTempHsl);
    454     }
    455 
    456     private boolean shouldIgnoreColor(Swatch color) {
    457         return shouldIgnoreColor(color.getRgb(), color.getHsl());
    458     }
    459 
    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      */
    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      */
    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 
    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      */
    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      */
    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      */
    521     static int quantizedBlue(int color) {
    522         return color & QUANTIZE_WORD_MASK;
    523     }
    524 
    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 }