Loading core/java/android/app/WallpaperColors.java +3 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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(); Loading core/java/com/android/internal/graphics/palette/ColorCutQuantizer.java +9 −9 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -160,7 +160,7 @@ final class ColorCutQuantizer { /** * @return the list of quantized colors */ List<Swatch> getQuantizedColors() { public List<Swatch> getQuantizedColors() { return mQuantizedColors; } Loading core/java/com/android/internal/graphics/palette/Palette.java +21 −5 Original line number Diff line number Diff line Loading @@ -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} */ Loading Loading @@ -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> Loading Loading @@ -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"); Loading core/java/com/android/internal/graphics/palette/Quantizer.java 0 → 100644 +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(); } core/java/com/android/internal/graphics/palette/VariationalKMeansQuantizer.java 0 → 100644 +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
core/java/android/app/WallpaperColors.java +3 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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(); Loading
core/java/com/android/internal/graphics/palette/ColorCutQuantizer.java +9 −9 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -160,7 +160,7 @@ final class ColorCutQuantizer { /** * @return the list of quantized colors */ List<Swatch> getQuantizedColors() { public List<Swatch> getQuantizedColors() { return mQuantizedColors; } Loading
core/java/com/android/internal/graphics/palette/Palette.java +21 −5 Original line number Diff line number Diff line Loading @@ -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} */ Loading Loading @@ -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> Loading Loading @@ -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"); Loading
core/java/com/android/internal/graphics/palette/Quantizer.java 0 → 100644 +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(); }
core/java/com/android/internal/graphics/palette/VariationalKMeansQuantizer.java 0 → 100644 +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; } }