Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d6d9ab50 authored by Steve Kondik's avatar Steve Kondik Committed by Steve Kondik
Browse files

Update private copy of Palette

Change-Id: I9eec47031fa8d11b62b38ae769acc9a4a177ff18
parent 80922ca9
Loading
Loading
Loading
Loading
+229 −162
Original line number Diff line number Diff line
@@ -16,11 +16,9 @@

package com.android.internal.util.cm.palette;

import android.graphics.Bitmap;
import android.graphics.Color;
import android.util.SparseIntArray;

import com.android.internal.util.cm.palette.Palette.Swatch;
import android.util.TimingLogger;

import java.util.ArrayList;
import java.util.Arrays;
@@ -46,74 +44,97 @@ import java.util.PriorityQueue;
 */
final class ColorCutQuantizer {

    private static final String LOG_TAG = ColorCutQuantizer.class.getSimpleName();

    private final float[] mTempHsl = new float[3];

    private static final float BLACK_MAX_LIGHTNESS = 0.05f;
    private static final float WHITE_MIN_LIGHTNESS = 0.95f;
    private static final String LOG_TAG = "ColorCutQuantizer";
    private static final boolean LOG_TIMINGS = false;

    private static final int COMPONENT_RED = -3;
    private static final int COMPONENT_GREEN = -2;
    private static final int COMPONENT_BLUE = -1;

    private final int[] mColors;
    private final SparseIntArray mColorPopulations;
    private static final int QUANTIZE_WORD_WIDTH = 5;
    private static final int QUANTIZE_WORD_MASK = (1 << QUANTIZE_WORD_WIDTH) - 1;

    final int[] mColors;
    final int[] mHistogram;
    final List<Swatch> mQuantizedColors;
    final TimingLogger mTimingLogger;
    final Palette.Filter[] mFilters;

    private final List<Swatch> mQuantizedColors;
    private final float[] mTempHsl = new float[3];

    /**
     * Factory-method to generate a {@link ColorCutQuantizer} from a {@link Bitmap} object.
     * Constructor.
     *
     * @param bitmap Bitmap to extract the pixel data from
     * @param pixels histogram representing an image's pixel data
     * @param maxColors The maximum number of colors that should be in the result palette.
     * @param filters Set of filters to use in the quantization stage
     */
    static ColorCutQuantizer fromBitmap(Bitmap bitmap, int maxColors) {
        final int width = bitmap.getWidth();
        final int height = bitmap.getHeight();
    ColorCutQuantizer(final int[] pixels, final int maxColors, final Palette.Filter[] filters) {
        mTimingLogger = LOG_TIMINGS ? new TimingLogger(LOG_TAG, "Creation") : null;
        mFilters = filters;

        final int[] pixels = new int[width * height];
        bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
        final int[] hist = mHistogram = new int[1 << (QUANTIZE_WORD_WIDTH * 3)];
        for (int i = 0; i < pixels.length; i++) {
            final int quantizedColor = quantizeFromRgb888(pixels[i]);
            // Now update the pixel value to the quantized value
            pixels[i] = quantizedColor;
            // And update the histogram
            hist[quantizedColor]++;
        }

        return new ColorCutQuantizer(new com.android.internal.util.cm.palette.ColorHistogram(pixels), maxColors);
        if (LOG_TIMINGS) {
            mTimingLogger.addSplit("Histogram created");
        }

    /**
     * Private constructor.
     *
     * @param colorHistogram histogram representing an image's pixel data
     * @param maxColors The maximum number of colors that should be in the result palette.
     */
    private ColorCutQuantizer(com.android.internal.util.cm.palette.ColorHistogram colorHistogram, int maxColors) {
        final int rawColorCount = colorHistogram.getNumberOfColors();
        final int[] rawColors = colorHistogram.getColors();
        final int[] rawColorCounts = colorHistogram.getColorCounts();
        // Now let's count the number of distinct colors
        int distinctColorCount = 0;
        for (int color = 0; color < hist.length; color++) {
            if (hist[color] > 0 && shouldIgnoreColor(color)) {
                // If we should ignore the color, set the population to 0
                hist[color] = 0;
            }
            if (hist[color] > 0) {
                // If the color has population, increase the distinct color count
                distinctColorCount++;
            }
        }

        // First, lets pack the populations into a SparseIntArray so that they can be easily
        // retrieved without knowing a color's index
        mColorPopulations = new SparseIntArray(rawColorCount);
        for (int i = 0; i < rawColors.length; i++) {
            mColorPopulations.append(rawColors[i], rawColorCounts[i]);
        if (LOG_TIMINGS) {
            mTimingLogger.addSplit("Filtered colors and distinct colors counted");
        }

        // Now go through all of the colors and keep those which we do not want to ignore
        mColors = new int[rawColorCount];
        int validColorCount = 0;
        for (int color : rawColors) {
            if (!shouldIgnoreColor(color)) {
                mColors[validColorCount++] = color;
        // Now lets go through create an array consisting of only distinct colors
        final int[] colors = mColors = new int[distinctColorCount];
        int distinctColorIndex = 0;
        for (int color = 0; color < hist.length; color++) {
            if (hist[color] > 0) {
                colors[distinctColorIndex++] = color;
            }
        }

        if (LOG_TIMINGS) {
            mTimingLogger.addSplit("Distinct colors copied into array");
        }

        if (validColorCount <= maxColors) {
        if (distinctColorCount <= maxColors) {
            // The image has fewer colors than the maximum requested, so just return the colors
            mQuantizedColors = new ArrayList<Swatch>();
            for (final int color : mColors) {
                mQuantizedColors.add(new Swatch(color, mColorPopulations.get(color)));
            mQuantizedColors = new ArrayList<>();
            for (int color : colors) {
                mQuantizedColors.add(new Swatch(approximateToRgb888(color), hist[color]));
            }

            if (LOG_TIMINGS) {
                mTimingLogger.addSplit("Too few colors present. Copied to Swatches");
                mTimingLogger.dumpToLog();
            }
        } else {
            // We need use quantization to reduce the number of colors
            mQuantizedColors = quantizePixels(validColorCount - 1, maxColors);
            mQuantizedColors = quantizePixels(maxColors);

            if (LOG_TIMINGS) {
                mTimingLogger.addSplit("Quantized colors computed");
                mTimingLogger.dumpToLog();
            }
        }
    }

@@ -124,13 +145,13 @@ final class ColorCutQuantizer {
        return mQuantizedColors;
    }

    private List<Swatch> quantizePixels(int maxColorIndex, int maxColors) {
    private List<Swatch> quantizePixels(int maxColors) {
        // Create the priority queue which is sorted by volume descending. This means we always
        // split the largest box in the queue
        final PriorityQueue<Vbox> pq = new PriorityQueue<Vbox>(maxColors, VBOX_COMPARATOR_VOLUME);
        final PriorityQueue<Vbox> pq = new PriorityQueue<>(maxColors, VBOX_COMPARATOR_VOLUME);

        // To start, offer a box which contains all of the colors
        pq.offer(new Vbox(0, maxColorIndex));
        pq.offer(new Vbox(0, mColors.length - 1));

        // Now go through the boxes, splitting them until we have reached maxColors or there are no
        // more boxes to split
@@ -146,7 +167,7 @@ final class ColorCutQuantizer {
     * and splitting them. Once split, the new box and the remaining box are offered back to the
     * queue.
     *
     * @param queue {@link PriorityQueue} to poll for boxes
     * @param queue {@link java.util.PriorityQueue} to poll for boxes
     * @param maxSize Maximum amount of boxes to split
     */
    private void splitBoxes(final PriorityQueue<Vbox> queue, final int maxSize) {
@@ -156,9 +177,16 @@ final class ColorCutQuantizer {
            if (vbox != null && vbox.canSplit()) {
                // First split the box, and offer the result
                queue.offer(vbox.splitBox());

                if (LOG_TIMINGS) {
                    mTimingLogger.addSplit("Box split");
                }
                // Then offer the box back
                queue.offer(vbox);
            } else {
                if (LOG_TIMINGS) {
                    mTimingLogger.addSplit("All boxes split");
                }
                // If we get here then there are no more boxes to split, so return
                return;
            }
@@ -166,13 +194,13 @@ final class ColorCutQuantizer {
    }

    private List<Swatch> generateAverageColors(Collection<Vbox> vboxes) {
        ArrayList<Swatch> colors = new ArrayList<Swatch>(vboxes.size());
        ArrayList<Swatch> colors = new ArrayList<>(vboxes.size());
        for (Vbox vbox : vboxes) {
            Swatch color = vbox.getAverageColor();
            if (!shouldIgnoreColor(color)) {
            Swatch swatch = vbox.getAverageColor();
            if (!shouldIgnoreColor(swatch)) {
                // As we're averaging a color box, we can still get colors which we do not want, so
                // we check again here
                colors.add(color);
                colors.add(swatch);
            }
        }
        return colors;
@@ -185,6 +213,8 @@ final class ColorCutQuantizer {
        // lower and upper index are inclusive
        private int mLowerIndex;
        private int mUpperIndex;
        // Population of colors within this box
        private int mPopulation;

        private int mMinRed, mMaxRed;
        private int mMinGreen, mMaxGreen;
@@ -196,51 +226,67 @@ final class ColorCutQuantizer {
            fitBox();
        }

        int getVolume() {
        final int getVolume() {
            return (mMaxRed - mMinRed + 1) * (mMaxGreen - mMinGreen + 1) *
                    (mMaxBlue - mMinBlue + 1);
        }

        boolean canSplit() {
        final boolean canSplit() {
            return getColorCount() > 1;
        }

        int getColorCount() {
            return mUpperIndex - mLowerIndex + 1;
        final int getColorCount() {
            return 1 + mUpperIndex - mLowerIndex;
        }

        /**
         * Recomputes the boundaries of this box to tightly fit the colors within the box.
         */
        void fitBox() {
        final void fitBox() {
            final int[] colors = mColors;
            final int[] hist = mHistogram;

            // Reset the min and max to opposite values
            mMinRed = mMinGreen = mMinBlue = 0xFF;
            mMaxRed = mMaxGreen = mMaxBlue = 0x0;
            int minRed, minGreen, minBlue;
            minRed = minGreen = minBlue = Integer.MAX_VALUE;
            int maxRed, maxGreen, maxBlue;
            maxRed = maxGreen = maxBlue = Integer.MIN_VALUE;
            int count = 0;

            for (int i = mLowerIndex; i <= mUpperIndex; i++) {
                final int color = mColors[i];
                final int r = Color.red(color);
                final int g = Color.green(color);
                final int b = Color.blue(color);
                if (r > mMaxRed) {
                    mMaxRed = r;
                final int color = colors[i];
                count += hist[color];

                final int r = quantizedRed(color);
                final int g = quantizedGreen(color);
                final int b = quantizedBlue(color);
                if (r > maxRed) {
                    maxRed = r;
                }
                if (r < mMinRed) {
                    mMinRed = r;
                if (r < minRed) {
                    minRed = r;
                }
                if (g > mMaxGreen) {
                    mMaxGreen = g;
                if (g > maxGreen) {
                    maxGreen = g;
                }
                if (g < mMinGreen) {
                    mMinGreen = g;
                if (g < minGreen) {
                    minGreen = g;
                }
                if (b > mMaxBlue) {
                    mMaxBlue = b;
                if (b > maxBlue) {
                    maxBlue = b;
                }
                if (b < mMinBlue) {
                    mMinBlue = b;
                if (b < minBlue) {
                    minBlue = b;
                }
            }

            mMinRed = minRed;
            mMaxRed = maxRed;
            mMinGreen = minGreen;
            mMaxGreen = maxGreen;
            mMinBlue = minBlue;
            mMaxBlue = maxBlue;
            mPopulation = count;
        }

        /**
@@ -248,7 +294,7 @@ final class ColorCutQuantizer {
         *
         * @return the new ColorBox
         */
        Vbox splitBox() {
        final Vbox splitBox() {
            if (!canSplit()) {
                throw new IllegalStateException("Can not split a box with only 1 color");
            }
@@ -268,7 +314,7 @@ final class ColorCutQuantizer {
        /**
         * @return the dimension which this box is largest in
         */
        int getLongestColorDimension() {
        final int getLongestColorDimension() {
            final int redLength = mMaxRed - mMinRed;
            final int greenLength = mMaxGreen - mMinGreen;
            final int blueLength = mMaxBlue - mMinBlue;
@@ -291,42 +337,28 @@ final class ColorCutQuantizer {
         *
         * @return the index of the colors array to split from
         */
        int findSplitPoint() {
        final int findSplitPoint() {
            final int longestDimension = getLongestColorDimension();
            final int[] colors = mColors;
            final int[] hist = mHistogram;

            // We need to sort the colors in this box based on the longest color dimension.
            // As we can't use a Comparator to define the sort logic, we modify each color so that
            // it's most significant is the desired dimension
            modifySignificantOctet(longestDimension, mLowerIndex, mUpperIndex);
            modifySignificantOctet(colors, longestDimension, mLowerIndex, mUpperIndex);

            // Now sort... Arrays.sort uses a exclusive toIndex so we need to add 1
            Arrays.sort(mColors, mLowerIndex, mUpperIndex + 1);
            Arrays.sort(colors, mLowerIndex, mUpperIndex + 1);

            // Now revert all of the colors so that they are packed as RGB again
            modifySignificantOctet(longestDimension, mLowerIndex, mUpperIndex);

            final int dimensionMidPoint = midPoint(longestDimension);
            modifySignificantOctet(colors, longestDimension, mLowerIndex, mUpperIndex);

            for (int i = mLowerIndex; i <= mUpperIndex; i++)  {
                final int color = mColors[i];

                switch (longestDimension) {
                    case COMPONENT_RED:
                        if (Color.red(color) >= dimensionMidPoint) {
            final int midPoint = mPopulation / 2;
            for (int i = mLowerIndex, count = 0; i <= mUpperIndex; i++)  {
                count += hist[colors[i]];
                if (count >= midPoint) {
                    return i;
                }
                        break;
                    case COMPONENT_GREEN:
                        if (Color.green(color) >= dimensionMidPoint) {
                            return i;
                        }
                        break;
                    case COMPONENT_BLUE:
                        if (Color.blue(color) > dimensionMidPoint) {
                            return i;
                        }
                        break;
                }
            }

            return mLowerIndex;
@@ -335,115 +367,150 @@ final class ColorCutQuantizer {
        /**
         * @return the average color of this box.
         */
        Swatch getAverageColor() {
        final Swatch getAverageColor() {
            final int[] colors = mColors;
            final int[] hist = mHistogram;
            int redSum = 0;
            int greenSum = 0;
            int blueSum = 0;
            int totalPopulation = 0;

            for (int i = mLowerIndex; i <= mUpperIndex; i++) {
                final int color = mColors[i];
                final int colorPopulation = mColorPopulations.get(color);
                final int color = colors[i];
                final int colorPopulation = hist[color];

                totalPopulation += colorPopulation;
                redSum += colorPopulation * Color.red(color);
                greenSum += colorPopulation * Color.green(color);
                blueSum += colorPopulation * Color.blue(color);
                redSum += colorPopulation * quantizedRed(color);
                greenSum += colorPopulation * quantizedGreen(color);
                blueSum += colorPopulation * quantizedBlue(color);
            }

            final int redAverage = Math.round(redSum / (float) totalPopulation);
            final int greenAverage = Math.round(greenSum / (float) totalPopulation);
            final int blueAverage = Math.round(blueSum / (float) totalPopulation);

            return new Swatch(redAverage, greenAverage, blueAverage, totalPopulation);
        }
            final int redMean = Math.round(redSum / (float) totalPopulation);
            final int greenMean = Math.round(greenSum / (float) totalPopulation);
            final int blueMean = Math.round(blueSum / (float) totalPopulation);

        /**
         * @return the midpoint of this box in the given {@code dimension}
         */
        int midPoint(int dimension) {
            switch (dimension) {
                case COMPONENT_RED:
                default:
                    return (mMinRed + mMaxRed) / 2;
                case COMPONENT_GREEN:
                    return (mMinGreen + mMaxGreen) / 2;
                case COMPONENT_BLUE:
                    return (mMinBlue + mMaxBlue) / 2;
            }
            return new Swatch(approximateToRgb888(redMean, greenMean, blueMean), totalPopulation);
        }
    }

    /**
     * Modify the significant octet in a packed color int. Allows sorting based on the value of a
     * single color component.
     * single color component. This relies on all components being the same word size.
     *
     * @see Vbox#findSplitPoint()
     */
    private void modifySignificantOctet(final int dimension, int lowerIndex, int upperIndex) {
    private static void modifySignificantOctet(final int[] a, final int dimension,
            final int lower, final int upper) {
        switch (dimension) {
            case COMPONENT_RED:
                // Already in RGB, no need to do anything
                break;
            case COMPONENT_GREEN:
                // We need to do a RGB to GRB swap, or vice-versa
                for (int i = lowerIndex; i <= upperIndex; i++) {
                    final int color = mColors[i];
                    mColors[i] = Color.rgb((color >> 8) & 0xFF, (color >> 16) & 0xFF, color & 0xFF);
                for (int i = lower; i <= upper; i++) {
                    final int color = a[i];
                    a[i] = quantizedGreen(color) << (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH)
                            | quantizedRed(color) << QUANTIZE_WORD_WIDTH
                            | quantizedBlue(color);
                }
                break;
            case COMPONENT_BLUE:
                // We need to do a RGB to BGR swap, or vice-versa
                for (int i = lowerIndex; i <= upperIndex; i++) {
                    final int color = mColors[i];
                    mColors[i] = Color.rgb(color & 0xFF, (color >> 8) & 0xFF, (color >> 16) & 0xFF);
                for (int i = lower; i <= upper; i++) {
                    final int color = a[i];
                    a[i] = quantizedBlue(color) << (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH)
                            | quantizedGreen(color) << QUANTIZE_WORD_WIDTH
                            | quantizedRed(color);
                }
                break;
        }
    }

    private boolean shouldIgnoreColor(int color) {
        com.android.internal.util.cm.palette.ColorUtils.RGBtoHSL(Color.red(color), Color.green(color), Color.blue(color), mTempHsl);
        return shouldIgnoreColor(mTempHsl);
    private boolean shouldIgnoreColor(int color565) {
        final int rgb = approximateToRgb888(color565);
        ColorUtils.colorToHSL(rgb, mTempHsl);
        return shouldIgnoreColor(rgb, mTempHsl);
    }

    private static boolean shouldIgnoreColor(Swatch color) {
        return shouldIgnoreColor(color.getHsl());
    private boolean shouldIgnoreColor(Swatch color) {
        return shouldIgnoreColor(color.getRgb(), color.getHsl());
    }

    private static boolean shouldIgnoreColor(float[] hslColor) {
        return isWhite(hslColor) || isBlack(hslColor) || isNearRedILine(hslColor);
    private boolean shouldIgnoreColor(int rgb, float[] hsl) {
        if (mFilters != null && mFilters.length > 0) {
            for (int i = 0, count = mFilters.length; i < count; i++) {
                if (!mFilters[i].isAllowed(rgb, hsl)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * @return true if the color represents a color which is close to black.
     * Comparator which sorts {@link Vbox} instances based on their volume, in descending order
     */
    private static boolean isBlack(float[] hslColor) {
        return hslColor[2] <= BLACK_MAX_LIGHTNESS;
    private static final Comparator<Vbox> VBOX_COMPARATOR_VOLUME = new Comparator<Vbox>() {
        @Override
        public int compare(Vbox lhs, Vbox rhs) {
            return rhs.getVolume() - lhs.getVolume();
        }
    };

    /**
     * @return true if the color represents a color which is close to white.
     * Quantized a RGB888 value to have a word width of {@value #QUANTIZE_WORD_WIDTH}.
     */
    private static boolean isWhite(float[] hslColor) {
        return hslColor[2] >= WHITE_MIN_LIGHTNESS;
    private static int quantizeFromRgb888(int color) {
        int r = modifyWordWidth(Color.red(color), 8, QUANTIZE_WORD_WIDTH);
        int g = modifyWordWidth(Color.green(color), 8, QUANTIZE_WORD_WIDTH);
        int b = modifyWordWidth(Color.blue(color), 8, QUANTIZE_WORD_WIDTH);
        return r << (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH) | g << QUANTIZE_WORD_WIDTH | b;
    }

    /**
     * @return true if the color lies close to the red side of the I line.
     * Quantized RGB888 values to have a word width of {@value #QUANTIZE_WORD_WIDTH}.
     */
    private static boolean isNearRedILine(float[] hslColor) {
        return hslColor[0] >= 10f && hslColor[0] <= 37f && hslColor[1] <= 0.82f;
    private static int approximateToRgb888(int r, int g, int b) {
        return Color.rgb(modifyWordWidth(r, QUANTIZE_WORD_WIDTH, 8),
                modifyWordWidth(g, QUANTIZE_WORD_WIDTH, 8),
                modifyWordWidth(b, QUANTIZE_WORD_WIDTH, 8));
    }

    private static int approximateToRgb888(int color) {
        return approximateToRgb888(quantizedRed(color), quantizedGreen(color), quantizedBlue(color));
    }

    /**
     * Comparator which sorts {@link Vbox} instances based on their volume, in descending order
     * @return red component of the quantized color
     */
    private static final Comparator<Vbox> VBOX_COMPARATOR_VOLUME = new Comparator<Vbox>() {
        @Override
        public int compare(Vbox lhs, Vbox rhs) {
            return rhs.getVolume() - lhs.getVolume();
    private static int quantizedRed(int color) {
        return (color >> (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH)) & QUANTIZE_WORD_MASK;
    }

    /**
     * @return green component of a quantized color
     */
    private static int quantizedGreen(int color) {
        return (color >> QUANTIZE_WORD_WIDTH) & QUANTIZE_WORD_MASK;
    }

    /**
     * @return blue component of a quantized color
     */
    private static int quantizedBlue(int color) {
        return color & QUANTIZE_WORD_MASK;
    }

    private static int modifyWordWidth(int value, int currentWidth, int targetWidth) {
        final int newValue;
        if (targetWidth > currentWidth) {
            // If we're approximating up in word width, we'll shift up
            newValue = value << (targetWidth - currentWidth);
        } else {
            // Else, we will just shift and keep the MSB
            newValue = value >> (currentWidth - targetWidth);
        }
        return newValue & ((1 << targetWidth) - 1);
    }
    };

}
+0 −127

File deleted.

Preview size limit exceeded, changes collapsed.

+111 −44

File changed.

Preview size limit exceeded, changes collapsed.

+244 −0

File added.

Preview size limit exceeded, changes collapsed.

+420 −368

File changed.

Preview size limit exceeded, changes collapsed.