All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.com.android.internal.graphics.palette.WuQuantizer Maven / Gradle / Ivy

Go to download

A library jar that provides APIs for Applications written for the Google Android Platform.

There is a newer version: 15-robolectric-12650502
Show newest version
/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.graphics.palette;

import static java.lang.System.arraycopy;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Color;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;


/**
 * Wu's quantization algorithm is a box-cut quantizer that minimizes variance. It takes longer to
 * run than, say, median color cut, but provides the highest quality results currently known.
 *
 * Prefer `QuantizerCelebi`: coupled with Kmeans, this provides the best-known results for image
 * quantization.
 *
 * Seemingly all Wu implementations are based off of one C code snippet that cites a book from 1992
 * Graphics Gems vol. II, pp. 126-133. As a result, it is very hard to understand the mechanics of
 * the algorithm, beyond the commentary provided in the C code. Comments on the methods of this
 * class are avoided in favor of finding another implementation and reading the commentary there,
 * avoiding perpetuating the same incomplete and somewhat confusing commentary here.
 */
public final class WuQuantizer implements Quantizer {
    // A histogram of all the input colors is constructed. It has the shape of a
    // cube. The cube would be too large if it contained all 16 million colors:
    // historical best practice is to use 5 bits  of the 8 in each channel,
    // reducing the histogram to a volume of ~32,000.
    private static final int BITS = 5;
    private static final int MAX_INDEX = 32;
    private static final int SIDE_LENGTH = 33;
    private static final int TOTAL_SIZE = 35937;

    private int[] mWeights;
    private int[] mMomentsR;
    private int[] mMomentsG;
    private int[] mMomentsB;
    private double[] mMoments;
    private Box[] mCubes;
    private Palette mPalette;
    private int[] mColors;
    private Map mInputPixelToCount;

    @Override
    public List getQuantizedColors() {
        return mPalette.getSwatches();
    }

    @Override
    public void quantize(@NonNull int[] pixels, int colorCount) {
        assert (pixels.length > 0);

        QuantizerMap quantizerMap = new QuantizerMap();
        quantizerMap.quantize(pixels, colorCount);
        mInputPixelToCount = quantizerMap.getColorToCount();
        // Extraction should not be run on using a color count higher than the number of colors
        // in the pixels. The algorithm doesn't expect that to be the case, unexpected results and
        // exceptions may occur.
        Set uniqueColors = mInputPixelToCount.keySet();
        if (uniqueColors.size() <= colorCount) {
            mColors = new int[mInputPixelToCount.keySet().size()];
            int index = 0;
            for (int color : uniqueColors) {
                mColors[index++] = color;
            }
        } else {
            constructHistogram(mInputPixelToCount);
            createMoments();
            CreateBoxesResult createBoxesResult = createBoxes(colorCount);
            mColors = createResult(createBoxesResult.mResultCount);
        }

        List swatches = new ArrayList<>();
        for (int color : mColors) {
            swatches.add(new Palette.Swatch(color, 0));
        }
        mPalette = Palette.from(swatches);
    }

    @Nullable
    public int[] getColors() {
        return mColors;
    }

    /** Keys are color ints, values are the number of pixels in the image matching that color int */
    @Nullable
    public Map inputPixelToCount() {
        return mInputPixelToCount;
    }

    private static int getIndex(int r, int g, int b) {
        return (r << 10) + (r << 6) + (g << 5) + r + g + b;
    }

    private void constructHistogram(Map pixels) {
        mWeights = new int[TOTAL_SIZE];
        mMomentsR = new int[TOTAL_SIZE];
        mMomentsG = new int[TOTAL_SIZE];
        mMomentsB = new int[TOTAL_SIZE];
        mMoments = new double[TOTAL_SIZE];

        for (Map.Entry pair : pixels.entrySet()) {
            int pixel = pair.getKey();
            int count = pair.getValue();
            int red = Color.red(pixel);
            int green = Color.green(pixel);
            int blue = Color.blue(pixel);
            int bitsToRemove = 8 - BITS;
            int iR = (red >> bitsToRemove) + 1;
            int iG = (green >> bitsToRemove) + 1;
            int iB = (blue >> bitsToRemove) + 1;
            int index = getIndex(iR, iG, iB);
            mWeights[index] += count;
            mMomentsR[index] += (red * count);
            mMomentsG[index] += (green * count);
            mMomentsB[index] += (blue * count);
            mMoments[index] += (count * ((red * red) + (green * green) + (blue * blue)));
        }
    }

    private void createMoments() {
        for (int r = 1; r < SIDE_LENGTH; ++r) {
            int[] area = new int[SIDE_LENGTH];
            int[] areaR = new int[SIDE_LENGTH];
            int[] areaG = new int[SIDE_LENGTH];
            int[] areaB = new int[SIDE_LENGTH];
            double[] area2 = new double[SIDE_LENGTH];

            for (int g = 1; g < SIDE_LENGTH; ++g) {
                int line = 0;
                int lineR = 0;
                int lineG = 0;
                int lineB = 0;

                double line2 = 0.0;
                for (int b = 1; b < SIDE_LENGTH; ++b) {
                    int index = getIndex(r, g, b);
                    line += mWeights[index];
                    lineR += mMomentsR[index];
                    lineG += mMomentsG[index];
                    lineB += mMomentsB[index];
                    line2 += mMoments[index];

                    area[b] += line;
                    areaR[b] += lineR;
                    areaG[b] += lineG;
                    areaB[b] += lineB;
                    area2[b] += line2;

                    int previousIndex = getIndex(r - 1, g, b);
                    mWeights[index] = mWeights[previousIndex] + area[b];
                    mMomentsR[index] = mMomentsR[previousIndex] + areaR[b];
                    mMomentsG[index] = mMomentsG[previousIndex] + areaG[b];
                    mMomentsB[index] = mMomentsB[previousIndex] + areaB[b];
                    mMoments[index] = mMoments[previousIndex] + area2[b];
                }
            }
        }
    }

    private CreateBoxesResult createBoxes(int maxColorCount) {
        mCubes = new Box[maxColorCount];
        for (int i = 0; i < maxColorCount; i++) {
            mCubes[i] = new Box();
        }
        double[] volumeVariance = new double[maxColorCount];
        Box firstBox = mCubes[0];
        firstBox.r1 = MAX_INDEX;
        firstBox.g1 = MAX_INDEX;
        firstBox.b1 = MAX_INDEX;

        int generatedColorCount = 0;
        int next = 0;

        for (int i = 1; i < maxColorCount; i++) {
            if (cut(mCubes[next], mCubes[i])) {
                volumeVariance[next] = (mCubes[next].vol > 1) ? variance(mCubes[next]) : 0.0;
                volumeVariance[i] = (mCubes[i].vol > 1) ? variance(mCubes[i]) : 0.0;
            } else {
                volumeVariance[next] = 0.0;
                i--;
            }

            next = 0;

            double temp = volumeVariance[0];
            for (int k = 1; k <= i; k++) {
                if (volumeVariance[k] > temp) {
                    temp = volumeVariance[k];
                    next = k;
                }
            }
            generatedColorCount = i + 1;
            if (temp <= 0.0) {
                break;
            }
        }

        return new CreateBoxesResult(maxColorCount, generatedColorCount);
    }

    private int[] createResult(int colorCount) {
        int[] colors = new int[colorCount];
        int nextAvailableIndex = 0;
        for (int i = 0; i < colorCount; ++i) {
            Box cube = mCubes[i];
            int weight = volume(cube, mWeights);
            if (weight > 0) {
                int r = (volume(cube, mMomentsR) / weight);
                int g = (volume(cube, mMomentsG) / weight);
                int b = (volume(cube, mMomentsB) / weight);
                int color = Color.rgb(r, g, b);
                colors[nextAvailableIndex++] = color;
            }
        }
        int[] resultArray = new int[nextAvailableIndex];
        arraycopy(colors, 0, resultArray, 0, nextAvailableIndex);
        return resultArray;
    }

    private double variance(Box cube) {
        int dr = volume(cube, mMomentsR);
        int dg = volume(cube, mMomentsG);
        int db = volume(cube, mMomentsB);
        double xx =
                mMoments[getIndex(cube.r1, cube.g1, cube.b1)]
                        - mMoments[getIndex(cube.r1, cube.g1, cube.b0)]
                        - mMoments[getIndex(cube.r1, cube.g0, cube.b1)]
                        + mMoments[getIndex(cube.r1, cube.g0, cube.b0)]
                        - mMoments[getIndex(cube.r0, cube.g1, cube.b1)]
                        + mMoments[getIndex(cube.r0, cube.g1, cube.b0)]
                        + mMoments[getIndex(cube.r0, cube.g0, cube.b1)]
                        - mMoments[getIndex(cube.r0, cube.g0, cube.b0)];

        int hypotenuse = (dr * dr + dg * dg + db * db);
        int volume2 = volume(cube, mWeights);
        double variance2 = xx - ((double) hypotenuse / (double) volume2);
        return variance2;
    }

    private boolean cut(Box one, Box two) {
        int wholeR = volume(one, mMomentsR);
        int wholeG = volume(one, mMomentsG);
        int wholeB = volume(one, mMomentsB);
        int wholeW = volume(one, mWeights);

        MaximizeResult maxRResult =
                maximize(one, Direction.RED, one.r0 + 1, one.r1, wholeR, wholeG, wholeB, wholeW);
        MaximizeResult maxGResult =
                maximize(one, Direction.GREEN, one.g0 + 1, one.g1, wholeR, wholeG, wholeB, wholeW);
        MaximizeResult maxBResult =
                maximize(one, Direction.BLUE, one.b0 + 1, one.b1, wholeR, wholeG, wholeB, wholeW);
        Direction cutDirection;
        double maxR = maxRResult.mMaximum;
        double maxG = maxGResult.mMaximum;
        double maxB = maxBResult.mMaximum;
        if (maxR >= maxG && maxR >= maxB) {
            if (maxRResult.mCutLocation < 0) {
                return false;
            }
            cutDirection = Direction.RED;
        } else if (maxG >= maxR && maxG >= maxB) {
            cutDirection = Direction.GREEN;
        } else {
            cutDirection = Direction.BLUE;
        }

        two.r1 = one.r1;
        two.g1 = one.g1;
        two.b1 = one.b1;

        switch (cutDirection) {
            case RED:
                one.r1 = maxRResult.mCutLocation;
                two.r0 = one.r1;
                two.g0 = one.g0;
                two.b0 = one.b0;
                break;
            case GREEN:
                one.g1 = maxGResult.mCutLocation;
                two.r0 = one.r0;
                two.g0 = one.g1;
                two.b0 = one.b0;
                break;
            case BLUE:
                one.b1 = maxBResult.mCutLocation;
                two.r0 = one.r0;
                two.g0 = one.g0;
                two.b0 = one.b1;
                break;
            default:
                throw new IllegalArgumentException("unexpected direction " + cutDirection);
        }

        one.vol = (one.r1 - one.r0) * (one.g1 - one.g0) * (one.b1 - one.b0);
        two.vol = (two.r1 - two.r0) * (two.g1 - two.g0) * (two.b1 - two.b0);

        return true;
    }

    private MaximizeResult maximize(
            Box cube,
            Direction direction,
            int first,
            int last,
            int wholeR,
            int wholeG,
            int wholeB,
            int wholeW) {
        int baseR = bottom(cube, direction, mMomentsR);
        int baseG = bottom(cube, direction, mMomentsG);
        int baseB = bottom(cube, direction, mMomentsB);
        int baseW = bottom(cube, direction, mWeights);

        double max = 0.0;
        int cut = -1;
        for (int i = first; i < last; i++) {
            int halfR = baseR + top(cube, direction, i, mMomentsR);
            int halfG = baseG + top(cube, direction, i, mMomentsG);
            int halfB = baseB + top(cube, direction, i, mMomentsB);
            int halfW = baseW + top(cube, direction, i, mWeights);

            if (halfW == 0) {
                continue;
            }
            double tempNumerator = halfR * halfR + halfG * halfG + halfB * halfB;
            double tempDenominator = halfW;
            double temp = tempNumerator / tempDenominator;

            halfR = wholeR - halfR;
            halfG = wholeG - halfG;
            halfB = wholeB - halfB;
            halfW = wholeW - halfW;
            if (halfW == 0) {
                continue;
            }

            tempNumerator = halfR * halfR + halfG * halfG + halfB * halfB;
            tempDenominator = halfW;
            temp += (tempNumerator / tempDenominator);
            if (temp > max) {
                max = temp;
                cut = i;
            }
        }
        return new MaximizeResult(cut, max);
    }

    private static int volume(Box cube, int[] moment) {
        return (moment[getIndex(cube.r1, cube.g1, cube.b1)]
                - moment[getIndex(cube.r1, cube.g1, cube.b0)]
                - moment[getIndex(cube.r1, cube.g0, cube.b1)]
                + moment[getIndex(cube.r1, cube.g0, cube.b0)]
                - moment[getIndex(cube.r0, cube.g1, cube.b1)]
                + moment[getIndex(cube.r0, cube.g1, cube.b0)]
                + moment[getIndex(cube.r0, cube.g0, cube.b1)]
                - moment[getIndex(cube.r0, cube.g0, cube.b0)]);
    }

    private static int bottom(Box cube, Direction direction, int[] moment) {
        switch (direction) {
            case RED:
                return -moment[getIndex(cube.r0, cube.g1, cube.b1)]
                        + moment[getIndex(cube.r0, cube.g1, cube.b0)]
                        + moment[getIndex(cube.r0, cube.g0, cube.b1)]
                        - moment[getIndex(cube.r0, cube.g0, cube.b0)];
            case GREEN:
                return -moment[getIndex(cube.r1, cube.g0, cube.b1)]
                        + moment[getIndex(cube.r1, cube.g0, cube.b0)]
                        + moment[getIndex(cube.r0, cube.g0, cube.b1)]
                        - moment[getIndex(cube.r0, cube.g0, cube.b0)];
            case BLUE:
                return -moment[getIndex(cube.r1, cube.g1, cube.b0)]
                        + moment[getIndex(cube.r1, cube.g0, cube.b0)]
                        + moment[getIndex(cube.r0, cube.g1, cube.b0)]
                        - moment[getIndex(cube.r0, cube.g0, cube.b0)];
            default:
                throw new IllegalArgumentException("unexpected direction " + direction);
        }
    }

    private static int top(Box cube, Direction direction, int position, int[] moment) {
        switch (direction) {
            case RED:
                return (moment[getIndex(position, cube.g1, cube.b1)]
                        - moment[getIndex(position, cube.g1, cube.b0)]
                        - moment[getIndex(position, cube.g0, cube.b1)]
                        + moment[getIndex(position, cube.g0, cube.b0)]);
            case GREEN:
                return (moment[getIndex(cube.r1, position, cube.b1)]
                        - moment[getIndex(cube.r1, position, cube.b0)]
                        - moment[getIndex(cube.r0, position, cube.b1)]
                        + moment[getIndex(cube.r0, position, cube.b0)]);
            case BLUE:
                return (moment[getIndex(cube.r1, cube.g1, position)]
                        - moment[getIndex(cube.r1, cube.g0, position)]
                        - moment[getIndex(cube.r0, cube.g1, position)]
                        + moment[getIndex(cube.r0, cube.g0, position)]);
            default:
                throw new IllegalArgumentException("unexpected direction " + direction);
        }
    }

    private enum Direction {
        RED,
        GREEN,
        BLUE
    }

    private static class MaximizeResult {
        // < 0 if cut impossible
        final int mCutLocation;
        final double mMaximum;

        MaximizeResult(int cut, double max) {
            mCutLocation = cut;
            mMaximum = max;
        }
    }

    private static class CreateBoxesResult {
        final int mRequestedCount;
        final int mResultCount;

        CreateBoxesResult(int requestedCount, int resultCount) {
            mRequestedCount = requestedCount;
            mResultCount = resultCount;
        }
    }

    private static class Box {
        public int r0 = 0;
        public int r1 = 0;
        public int g0 = 0;
        public int g1 = 0;
        public int b0 = 0;
        public int b1 = 0;
        public int vol = 0;
    }
}






© 2015 - 2024 Weber Informatics LLC | Privacy Policy