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

Commit 7b2d2103 authored by Lucas Dupin's avatar Lucas Dupin Committed by android-build-merger
Browse files

Merge "K-Means color clustering" into oc-dr1-dev am: 11cc2600

am: af26f72f

Change-Id: I1985de652f8d4643a8a5a170fd7f43c33c6c0bd7
parents a7c46d24 af26f72f
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import android.os.Parcelable;
import android.util.Size;

import com.android.internal.graphics.palette.Palette;
import com.android.internal.graphics.palette.VariationalKMeansQuantizer;

import java.util.ArrayList;
import java.util.Collections;
@@ -142,6 +143,8 @@ public final class WallpaperColors implements Parcelable {

        final Palette palette = Palette
                .from(bitmap)
                .setQuantizer(new VariationalKMeansQuantizer())
                .maximumColorCount(5)
                .clearFilters()
                .resizeBitmapArea(MAX_WALLPAPER_EXTRACTION_AREA)
                .generate();
+9 −9
Original line number Diff line number Diff line
@@ -61,7 +61,7 @@ import com.android.internal.graphics.palette.Palette.Swatch;
 * This means that the color space is divided into distinct colors, rather than representative
 * colors.
 */
final class ColorCutQuantizer {
final class ColorCutQuantizer implements Quantizer {

    private static final String LOG_TAG = "ColorCutQuantizer";
    private static final boolean LOG_TIMINGS = false;
@@ -73,22 +73,22 @@ final class ColorCutQuantizer {
    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;
    int[] mColors;
    int[] mHistogram;
    List<Swatch> mQuantizedColors;
    TimingLogger mTimingLogger;
    Palette.Filter[] mFilters;

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

    /**
     * Constructor.
     * Execute color quantization.
     *
     * @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
     */
    ColorCutQuantizer(final int[] pixels, final int maxColors, final Palette.Filter[] filters) {
    public void quantize(final int[] pixels, final int maxColors, final Palette.Filter[] filters) {
        mTimingLogger = LOG_TIMINGS ? new TimingLogger(LOG_TAG, "Creation") : null;
        mFilters = filters;

@@ -160,7 +160,7 @@ final class ColorCutQuantizer {
    /**
     * @return the list of quantized colors
     */
    List<Swatch> getQuantizedColors() {
    public List<Swatch> getQuantizedColors() {
        return mQuantizedColors;
    }

+21 −5
Original line number Diff line number Diff line
@@ -613,6 +613,8 @@ public final class Palette {
        private final List<Palette.Filter> mFilters = new ArrayList<>();
        private Rect mRegion;

        private Quantizer mQuantizer;

        /**
         * Construct a new {@link Palette.Builder} using a source {@link Bitmap}
         */
@@ -725,6 +727,18 @@ public final class Palette {
            return this;
        }

        /**
         * Set a specific quantization algorithm. {@link ColorCutQuantizer} will
         * be used if unspecified.
         *
         * @param quantizer Quantizer implementation.
         */
        @NonNull
        public Palette.Builder setQuantizer(Quantizer quantizer) {
            mQuantizer = quantizer;
            return this;
        }

        /**
         * Set a region of the bitmap to be used exclusively when calculating the palette.
         * <p>This only works when the original input is a {@link Bitmap}.</p>
@@ -818,17 +832,19 @@ public final class Palette {
                }

                // Now generate a quantizer from the Bitmap
                final ColorCutQuantizer quantizer = new ColorCutQuantizer(
                        getPixelsFromBitmap(bitmap),
                        mMaxColors,
                        mFilters.isEmpty() ? null : mFilters.toArray(new Palette.Filter[mFilters.size()]));
                if (mQuantizer == null) {
                    mQuantizer = new ColorCutQuantizer();
                }
                mQuantizer.quantize(getPixelsFromBitmap(bitmap),
                            mMaxColors, mFilters.isEmpty() ? null :
                            mFilters.toArray(new Palette.Filter[mFilters.size()]));

                // If created a new bitmap, recycle it
                if (bitmap != mBitmap) {
                    bitmap.recycle();
                }

                swatches = quantizer.getQuantizedColors();
                swatches = mQuantizer.getQuantizedColors();

                if (logger != null) {
                    logger.addSplit("Color quantization completed");
+27 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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 java.util.List;

/**
 * Definition of an algorithm that receives pixels and outputs a list of colors.
 */
public interface Quantizer {
    void quantize(final int[] pixels, final int maxColors, final Palette.Filter[] filters);
    List<Palette.Swatch> getQuantizedColors();
}
+154 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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 android.util.Log;

import com.android.internal.graphics.ColorUtils;
import com.android.internal.ml.clustering.KMeans;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * A quantizer that uses k-means
 */
public class VariationalKMeansQuantizer implements Quantizer {

    private static final String TAG = "KMeansQuantizer";
    private static final boolean DEBUG = false;

    /**
     * Clusters closer than this value will me merged.
     */
    private final float mMinClusterSqDistance;

    /**
     * K-means can get stuck in local optima, this can be avoided by
     * repeating it and getting the "best" execution.
     */
    private final int mInitializations;

    /**
     * Initialize KMeans with a fixed random state to have
     * consistent results across multiple runs.
     */
    private final KMeans mKMeans = new KMeans(new Random(0), 30, 0);

    private List<Palette.Swatch> mQuantizedColors;

    public VariationalKMeansQuantizer() {
        this(0.25f /* cluster distance */);
    }

    public VariationalKMeansQuantizer(float minClusterDistance) {
        this(minClusterDistance, 1 /* initializations */);
    }

    public VariationalKMeansQuantizer(float minClusterDistance, int initializations) {
        mMinClusterSqDistance = minClusterDistance * minClusterDistance;
        mInitializations = initializations;
    }

    /**
     * K-Means quantizer.
     *
     * @param pixels Pixels to quantize.
     * @param maxColors Maximum number of clusters to extract.
     * @param filters Colors that should be ignored
     */
    @Override
    public void quantize(int[] pixels, int maxColors, Palette.Filter[] filters) {
        // Start by converting all colors to HSL.
        // HLS is way more meaningful for clustering than RGB.
        final float[] hsl = {0, 0, 0};
        final float[][] hslPixels = new float[pixels.length][3];
        for (int i = 0; i < pixels.length; i++) {
            ColorUtils.colorToHSL(pixels[i], hsl);
            // Normalize hue so all values go from 0 to 1.
            hslPixels[i][0] = hsl[0] / 360f;
            hslPixels[i][1] = hsl[1];
            hslPixels[i][2] = hsl[2];
        }

        final List<KMeans.Mean> optimalMeans = getOptimalKMeans(maxColors, hslPixels);

        // Ideally we should run k-means again to merge clusters but it would be too expensive,
        // instead we just merge all clusters that are closer than a threshold.
        for (int i = 0; i < optimalMeans.size(); i++) {
            KMeans.Mean current = optimalMeans.get(i);
            float[] currentCentroid = current.getCentroid();
            for (int j = i + 1; j < optimalMeans.size(); j++) {
                KMeans.Mean compareTo = optimalMeans.get(j);
                float[] compareToCentroid = compareTo.getCentroid();
                float sqDistance = KMeans.sqDistance(currentCentroid, compareToCentroid);
                // Merge them
                if (sqDistance < mMinClusterSqDistance) {
                    optimalMeans.remove(compareTo);
                    current.getItems().addAll(compareTo.getItems());
                    for (int k = 0; k < currentCentroid.length; k++) {
                        currentCentroid[k] += (compareToCentroid[k] - currentCentroid[k]) / 2.0;
                    }
                    j--;
                }
            }
        }

        // Convert data to final format, de-normalizing the hue.
        mQuantizedColors = new ArrayList<>();
        for (KMeans.Mean mean : optimalMeans) {
            if (mean.getItems().size() == 0) {
                continue;
            }
            float[] centroid = mean.getCentroid();
            mQuantizedColors.add(new Palette.Swatch(new float[]{
                    centroid[0] * 360f,
                    centroid[1],
                    centroid[2]
            }, mean.getItems().size()));
        }
    }

    private List<KMeans.Mean> getOptimalKMeans(int k, float[][] inputData) {
        List<KMeans.Mean> optimal = null;
        double optimalScore = -Double.MAX_VALUE;
        int runs = mInitializations;
        while (runs > 0) {
            if (DEBUG) {
                Log.d(TAG, "k-means run: " + runs);
            }
            List<KMeans.Mean> means = mKMeans.predict(k, inputData);
            double score = KMeans.score(means);
            if (optimal == null || score > optimalScore) {
                if (DEBUG) {
                    Log.d(TAG, "\tnew optimal score: " + score);
                }
                optimalScore = score;
                optimal = means;
            }
            runs--;
        }

        return optimal;
    }

    @Override
    public List<Palette.Swatch> getQuantizedColors() {
        return mQuantizedColors;
    }
}
Loading