From 00f4c53592204f7b1f79e6324da570af00f0458a Mon Sep 17 00:00:00 2001 From: James O'Leary Date: Thu, 18 Feb 2021 16:47:21 -0500 Subject: [PATCH] [framework] Integrate new quantizers - WuQuantizer, CelebiQuantizer, and WSMeansQuantizer for implementing Celebi's weighted squared means algorithm for color extraction - CentroidProvider interface added; allows easy switching of what colorspace/dimensions is used for clustering during color extraction Test: Run, switch between a bunch of wallpapers, switch dark/light theme, repeat ad nauseum. Unit tests in separate Android Studio project hosting the library pass, will add those to Android in separate CL. Fixes: 180641035 Change-Id: I0fc60a134f411845dab9d6c7d8553907849d0f8a --- core/java/android/app/WallpaperColors.java | 87 +- .../graphics/palette/CelebiQuantizer.java | 50 ++ .../graphics/palette/CentroidProvider.java | 38 + .../graphics/palette/ColorCutQuantizer.java | 52 +- .../internal/graphics/palette/Contrast.java | 103 +++ .../graphics/palette/LABCentroid.java | 67 ++ .../internal/graphics/palette/Mean.java | 45 + .../internal/graphics/palette/MeanBucket.java | 42 + .../internal/graphics/palette/Palette.java | 806 ++++-------------- .../internal/graphics/palette/Quantizer.java | 11 +- .../internal/graphics/palette/Target.java | 509 ++++------- .../palette/VariationalKMeansQuantizer.java | 15 +- .../graphics/palette/WSMeansQuantizer.java | 269 ++++++ .../graphics/palette/WuQuantizer.java | 442 ++++++++++ .../SplashscreenContentDrawer.java | 27 +- 15 files changed, 1498 insertions(+), 1065 deletions(-) create mode 100644 core/java/com/android/internal/graphics/palette/CelebiQuantizer.java create mode 100644 core/java/com/android/internal/graphics/palette/CentroidProvider.java create mode 100644 core/java/com/android/internal/graphics/palette/Contrast.java create mode 100644 core/java/com/android/internal/graphics/palette/LABCentroid.java create mode 100644 core/java/com/android/internal/graphics/palette/Mean.java create mode 100644 core/java/com/android/internal/graphics/palette/MeanBucket.java create mode 100644 core/java/com/android/internal/graphics/palette/WSMeansQuantizer.java create mode 100644 core/java/com/android/internal/graphics/palette/WuQuantizer.java diff --git a/core/java/android/app/WallpaperColors.java b/core/java/android/app/WallpaperColors.java index 2d203f57a66f6..3abba43ae0a83 100644 --- a/core/java/android/app/WallpaperColors.java +++ b/core/java/android/app/WallpaperColors.java @@ -30,14 +30,17 @@ import android.util.Log; import android.util.Size; import com.android.internal.graphics.ColorUtils; +import com.android.internal.graphics.palette.CelebiQuantizer; import com.android.internal.graphics.palette.Palette; -import com.android.internal.graphics.palette.VariationalKMeansQuantizer; import com.android.internal.util.ContrastColorUtil; import java.io.FileOutputStream; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; /** * Provides information about the colors of a wallpaper. @@ -94,16 +97,21 @@ public final class WallpaperColors implements Parcelable { private static final float DARK_PIXEL_CONTRAST = 6f; private static final float MAX_DARK_AREA = 0.025f; - private final ArrayList mMainColors; + private final List mMainColors; + private final Map mAllColors; private int mColorHints; public WallpaperColors(Parcel parcel) { mMainColors = new ArrayList<>(); + mAllColors = new HashMap<>(); final int count = parcel.readInt(); for (int i = 0; i < count; i++) { final int colorInt = parcel.readInt(); Color color = Color.valueOf(colorInt); mMainColors.add(color); + + final int population = parcel.readInt(); + mAllColors.put(colorInt, population); } mColorHints = parcel.readInt(); } @@ -166,39 +174,22 @@ public final class WallpaperColors implements Parcelable { } final Palette palette = Palette - .from(bitmap) - .setQuantizer(new VariationalKMeansQuantizer()) - .maximumColorCount(5) - .clearFilters() + .from(bitmap, new CelebiQuantizer()) + .maximumColorCount(256) .resizeBitmapArea(MAX_WALLPAPER_EXTRACTION_AREA) .generate(); - // Remove insignificant colors and sort swatches by population final ArrayList swatches = new ArrayList<>(palette.getSwatches()); - final float minColorArea = bitmap.getWidth() * bitmap.getHeight() * MIN_COLOR_OCCURRENCE; - swatches.removeIf(s -> s.getPopulation() < minColorArea); swatches.sort((a, b) -> b.getPopulation() - a.getPopulation()); final int swatchesSize = swatches.size(); - Color primary = null, secondary = null, tertiary = null; - swatchLoop: + final Map populationByColor = new HashMap<>(); for (int i = 0; i < swatchesSize; i++) { - Color color = Color.valueOf(swatches.get(i).getRgb()); - switch (i) { - case 0: - primary = color; - break; - case 1: - secondary = color; - break; - case 2: - tertiary = color; - break; - default: - // out of bounds - break swatchLoop; - } + Palette.Swatch swatch = swatches.get(i); + int colorInt = swatch.getInt(); + populationByColor.put(colorInt, swatch.getPopulation()); + } int hints = calculateDarkHints(bitmap); @@ -207,7 +198,7 @@ public final class WallpaperColors implements Parcelable { bitmap.recycle(); } - return new WallpaperColors(primary, secondary, tertiary, HINT_FROM_BITMAP | hints); + return new WallpaperColors(populationByColor, HINT_FROM_BITMAP | hints); } /** @@ -253,9 +244,13 @@ public final class WallpaperColors implements Parcelable { } mMainColors = new ArrayList<>(3); + mAllColors = new HashMap<>(); + mMainColors.add(primaryColor); + mAllColors.put(primaryColor.toArgb(), 0); if (secondaryColor != null) { mMainColors.add(secondaryColor); + mAllColors.put(secondaryColor.toArgb(), 0); } if (tertiaryColor != null) { if (secondaryColor == null) { @@ -263,8 +258,32 @@ public final class WallpaperColors implements Parcelable { + "secondaryColor is null"); } mMainColors.add(tertiaryColor); + mAllColors.put(tertiaryColor.toArgb(), 0); } + mColorHints = colorHints; + } + /** + * Constructs a new object from a set of colors, where hints can be specified. + * + * @param populationByColor Map with keys of colors, and value representing the number of + * occurrences of color in the wallpaper. + * @param colorHints A combination of WallpaperColor hints. + * @hide + * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT + * @see WallpaperColors#fromBitmap(Bitmap) + * @see WallpaperColors#fromDrawable(Drawable) + */ + public WallpaperColors(@NonNull Map populationByColor, int colorHints) { + mAllColors = populationByColor; + + ArrayList> mapEntries = new ArrayList( + populationByColor.entrySet()); + mapEntries.sort((a, b) -> + a.getValue().compareTo(b.getValue()) + ); + mMainColors = mapEntries.stream().map(entry -> Color.valueOf(entry.getKey())).collect( + Collectors.toList()); mColorHints = colorHints; } @@ -293,6 +312,9 @@ public final class WallpaperColors implements Parcelable { for (int i = 0; i < count; i++) { Color color = mainColors.get(i); dest.writeInt(color.toArgb()); + Integer population = mAllColors.get(color.toArgb()); + int populationInt = (population != null) ? population : 0; + dest.writeInt(populationInt); } dest.writeInt(mColorHints); } @@ -336,6 +358,17 @@ public final class WallpaperColors implements Parcelable { return Collections.unmodifiableList(mMainColors); } + /** + * Map of all colors. Key is rgb integer, value is importance of color. + * + * @return List of colors. + * @hide + */ + public @NonNull Map getAllColors() { + return Collections.unmodifiableMap(mAllColors); + } + + @Override public boolean equals(@Nullable Object o) { if (o == null || getClass() != o.getClass()) { diff --git a/core/java/com/android/internal/graphics/palette/CelebiQuantizer.java b/core/java/com/android/internal/graphics/palette/CelebiQuantizer.java new file mode 100644 index 0000000000000..de6bf2044dbce --- /dev/null +++ b/core/java/com/android/internal/graphics/palette/CelebiQuantizer.java @@ -0,0 +1,50 @@ +/* + * 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 java.util.List; + +/** + * An implementation of Celebi's WSM quantizer, or, a Kmeans quantizer that starts with centroids + * from a Wu quantizer to ensure 100% reproducible and quality results, and has some optimizations + * to the Kmeans algorithm. + * + * See Celebi 2011, “Improving the Performance of K-Means for Color Quantization” + */ +public class CelebiQuantizer implements Quantizer { + private List mSwatches; + + public CelebiQuantizer() { } + + @Override + public void quantize(int[] pixels, int maxColors) { + WuQuantizer wu = new WuQuantizer(pixels, maxColors); + wu.quantize(pixels, maxColors); + List wuSwatches = wu.getQuantizedColors(); + LABCentroid labCentroidProvider = new LABCentroid(); + WSMeansQuantizer kmeans = + new WSMeansQuantizer(WSMeansQuantizer.createStartingCentroids(labCentroidProvider, + wuSwatches), labCentroidProvider, pixels, maxColors); + kmeans.quantize(pixels, maxColors); + mSwatches = kmeans.getQuantizedColors(); + } + + @Override + public List getQuantizedColors() { + return mSwatches; + } +} diff --git a/core/java/com/android/internal/graphics/palette/CentroidProvider.java b/core/java/com/android/internal/graphics/palette/CentroidProvider.java new file mode 100644 index 0000000000000..5fcfcbab31596 --- /dev/null +++ b/core/java/com/android/internal/graphics/palette/CentroidProvider.java @@ -0,0 +1,38 @@ +/* + * 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 android.annotation.ColorInt; + +interface CentroidProvider { + /** + * @return 3 dimensions representing the color + */ + float[] getCentroid(@ColorInt int color); + + /** + * @param centroid 3 dimensions representing the color + * @return 32-bit ARGB representation + */ + @ColorInt + int getColor(float[] centroid); + + /** + * Distance between two centroids. + */ + float distance(float[] a, float[] b); +} diff --git a/core/java/com/android/internal/graphics/palette/ColorCutQuantizer.java b/core/java/com/android/internal/graphics/palette/ColorCutQuantizer.java index 9ac753b6d6ce1..777949434c36b 100644 --- a/core/java/com/android/internal/graphics/palette/ColorCutQuantizer.java +++ b/core/java/com/android/internal/graphics/palette/ColorCutQuantizer.java @@ -35,6 +35,8 @@ package com.android.internal.graphics.palette; import android.graphics.Color; import android.util.TimingLogger; +import com.android.internal.graphics.palette.Palette.Swatch; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -42,9 +44,6 @@ import java.util.Comparator; import java.util.List; import java.util.PriorityQueue; -import com.android.internal.graphics.ColorUtils; -import com.android.internal.graphics.palette.Palette.Swatch; - /** * Copied from: frameworks/support/v7/palette/src/main/java/android/support/v7/ * graphics/ColorCutQuantizer.java @@ -77,20 +76,17 @@ final class ColorCutQuantizer implements Quantizer { int[] mHistogram; List mQuantizedColors; TimingLogger mTimingLogger; - Palette.Filter[] mFilters; private final float[] mTempHsl = new float[3]; /** * Execute color quantization. * - * @param pixels histogram representing an image's pixel data + * @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 */ - public void quantize(final int[] pixels, final int maxColors, final Palette.Filter[] filters) { + public void quantize(final int[] pixels, final int maxColors) { mTimingLogger = LOG_TIMINGS ? new TimingLogger(LOG_TAG, "Creation") : null; - mFilters = filters; final int[] hist = mHistogram = new int[1 << (QUANTIZE_WORD_WIDTH * 3)]; for (int i = 0; i < pixels.length; i++) { @@ -108,10 +104,6 @@ final class ColorCutQuantizer implements Quantizer { // 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++; @@ -186,7 +178,7 @@ final class ColorCutQuantizer implements Quantizer { * and splitting them. Once split, the new box and the remaining box are offered back to the * queue. * - * @param queue {@link java.util.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 queue, final int maxSize) { @@ -216,11 +208,7 @@ final class ColorCutQuantizer implements Quantizer { ArrayList colors = new ArrayList<>(vboxes.size()); for (Vbox vbox : vboxes) { 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(swatch); - } + colors.add(swatch); } return colors; } @@ -230,7 +218,7 @@ final class ColorCutQuantizer implements Quantizer { */ private class Vbox { // lower and upper index are inclusive - private int mLowerIndex; + private final int mLowerIndex; private int mUpperIndex; // Population of colors within this box private int mPopulation; @@ -373,7 +361,7 @@ final class ColorCutQuantizer implements Quantizer { modifySignificantOctet(colors, longestDimension, mLowerIndex, mUpperIndex); final int midPoint = mPopulation / 2; - for (int i = mLowerIndex, count = 0; i <= mUpperIndex; i++) { + for (int i = mLowerIndex, count = 0; i <= mUpperIndex; i++) { count += hist[colors[i]]; if (count >= midPoint) { // we never want to split on the upperIndex, as this will result in the same @@ -447,27 +435,6 @@ final class ColorCutQuantizer implements Quantizer { } } - private boolean shouldIgnoreColor(int color565) { - final int rgb = approximateToRgb888(color565); - ColorUtils.colorToHSL(rgb, mTempHsl); - return shouldIgnoreColor(rgb, mTempHsl); - } - - private boolean shouldIgnoreColor(Swatch color) { - return shouldIgnoreColor(color.getRgb(), color.getHsl()); - } - - 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; - } - /** * Comparator which sorts {@link Vbox} instances based on their volume, in descending order */ @@ -498,7 +465,8 @@ final class ColorCutQuantizer implements Quantizer { } private static int approximateToRgb888(int color) { - return approximateToRgb888(quantizedRed(color), quantizedGreen(color), quantizedBlue(color)); + return approximateToRgb888(quantizedRed(color), quantizedGreen(color), + quantizedBlue(color)); } /** diff --git a/core/java/com/android/internal/graphics/palette/Contrast.java b/core/java/com/android/internal/graphics/palette/Contrast.java new file mode 100644 index 0000000000000..3dd1b8d8117c4 --- /dev/null +++ b/core/java/com/android/internal/graphics/palette/Contrast.java @@ -0,0 +1,103 @@ +/* + * 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; + +/** + * Helper methods for determining contrast between two colors, either via the colors themselves + * or components in different color spaces. + */ +public class Contrast { + /** + * + * @param y Y in XYZ that contrasts with the returned Y value + * @param contrast contrast ratio between color argument and returned Y value. Must be >= 1 + * or an exception will be thrown + * @return the lower Y coordinate in XYZ space that contrasts with color, or -1 if reaching + * no Y coordinate reaches contrast with color. + */ + public static float lighterY(float y, float contrast) { + assert (contrast >= 1); + float answer = -5 + contrast * (5 + y); + if (answer > 100.0) { + return -1; + } + return answer; + } + + + /** + * @param y Y in XYZ that contrasts with the returned Y value + * @param contrast contrast ratio between color argument and returned Y value. Must be >= 1 + * or an exception will be thrown + * @return the lower Y coordinate in XYZ space that contrasts with color, or -1 if reaching + * no Y coordinate reaches contrast with color. + */ + public static float darkerY(float y, float contrast) { + assert (contrast >= 1); + float answer = (5 - 5 * contrast + y) / contrast; + if (answer < 0.0) { + return -1; + } + return answer; + } + + /** + * Convert L* in L*a*b* to Y in XYZ. + * + * @param lstar L* in L*a*b* + * @return Y in XYZ + */ + public static float lstarToY(float lstar) { + // http://www.brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html + float ke = 8.0f; + if (lstar > ke) { + return (float) (Math.pow(((lstar + 16.0) / 116.0), 3) * 100.0); + } else { + return (float) (lstar / (24389 / 27) * 100.0); + } + } + + /** + * Convert Y in XYZ to L* in L*a*b*. + * + * @param y Y in XYZ + * @return L* in L*a*b* + */ + public static float yToLstar(float y) { + y = y / 100.0f; + float e = 216.0f / 24389.0f; + float y_intermediate; + if (y <= e) { + y_intermediate = (24389.f / 27.f) * y; + // If y < e, can skip consecutive steps of / 116 + 16 followed by * 116 - 16. + return y_intermediate; + } else { + y_intermediate = (float) Math.cbrt(y); + } + return 116.f * y_intermediate - 16.f; + } + + + /** + * @return Contrast ratio between two Y values in XYZ space. + */ + public static float contrastYs(float y1, float y2) { + final float lighter = Math.max(y1, y2); + final float darker = (lighter == y1) ? y2 : y1; + return (lighter + 5) / (darker + 5); + } +} diff --git a/core/java/com/android/internal/graphics/palette/LABCentroid.java b/core/java/com/android/internal/graphics/palette/LABCentroid.java new file mode 100644 index 0000000000000..98d5d26848575 --- /dev/null +++ b/core/java/com/android/internal/graphics/palette/LABCentroid.java @@ -0,0 +1,67 @@ +/* + * 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 android.graphics.Color; +import android.graphics.ColorSpace; + +/** + * Allows quantizers to operate in the L*a*b* colorspace. + * L*a*b* is a good choice for measuring distance between colors. + * Better spaces, and better distance calculations even in L*a*b* exist, but measuring distance + * in L*a*b* space, also known as deltaE, is a universally accepted standard across industries + * and worldwide. + */ +public class LABCentroid implements CentroidProvider { + final ColorSpace.Connector mRgbToLab; + final ColorSpace.Connector mLabToRgb; + + public LABCentroid() { + mRgbToLab = ColorSpace.connect( + ColorSpace.get(ColorSpace.Named.SRGB), + ColorSpace.get(ColorSpace.Named.CIE_LAB)); + mLabToRgb = ColorSpace.connect(ColorSpace.get(ColorSpace.Named.CIE_LAB), + ColorSpace.get(ColorSpace.Named.SRGB)); + } + + @Override + public float[] getCentroid(int color) { + float r = Color.red(color) / 255.f; + float g = Color.green(color) / 255.f; + float b = Color.blue(color) / 255.f; + + float[] transform = mRgbToLab.transform(r, g, b); + return transform; + } + + @Override + public int getColor(float[] centroid) { + float[] rgb = mLabToRgb.transform(centroid); + int color = Color.rgb(rgb[0], rgb[1], rgb[2]); + return color; + } + + @Override + public float distance(float[] a, float[] b) { + // Standard v1 CIELAB deltaE formula, 1976 - easily improved upon, however, + // improvements do not significantly impact the Palette algorithm's results. + double dL = a[0] - b[0]; + double dA = a[1] - b[1]; + double dB = a[2] - b[2]; + return (float) (Math.pow(dL, 2) + Math.pow(dA, 2) + Math.pow(dB, 2)); + } +} diff --git a/core/java/com/android/internal/graphics/palette/Mean.java b/core/java/com/android/internal/graphics/palette/Mean.java new file mode 100644 index 0000000000000..894f91b6261c1 --- /dev/null +++ b/core/java/com/android/internal/graphics/palette/Mean.java @@ -0,0 +1,45 @@ +/* + * 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 java.util.Random; + +/** + * Represents a centroid in Kmeans algorithms. + */ +public class Mean { + private static final Random RANDOM = new Random(0); + + public float[] center; + + /** + * Constructor. + * + * @param upperBound maximum value of a dimension in the space Kmeans is optimizing in + */ + Mean(int upperBound) { + center = + new float[]{ + RANDOM.nextInt(upperBound + 1), RANDOM.nextInt(upperBound + 1), + RANDOM.nextInt(upperBound + 1) + }; + } + + Mean(float[] center) { + this.center = center; + } +} diff --git a/core/java/com/android/internal/graphics/palette/MeanBucket.java b/core/java/com/android/internal/graphics/palette/MeanBucket.java new file mode 100644 index 0000000000000..ae8858a8107cf --- /dev/null +++ b/core/java/com/android/internal/graphics/palette/MeanBucket.java @@ -0,0 +1,42 @@ +/* + * 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 java.util.HashSet; +import java.util.Set; + +class MeanBucket { + float[] mTotal = {0.f, 0.f, 0.f}; + int mCount = 0; + Set mColors = new HashSet<>(); + + void add(float[] colorAsDoubles, int color, int colorCount) { + assert (colorAsDoubles.length == 3); + mColors.add(color); + mTotal[0] += (colorAsDoubles[0] * colorCount); + mTotal[1] += (colorAsDoubles[1] * colorCount); + mTotal[2] += (colorAsDoubles[2] * colorCount); + mCount += colorCount; + } + + float[] getCentroid() { + if (mCount == 0) { + return null; + } + return new float[]{mTotal[0] / mCount, mTotal[1] / mCount, mTotal[2] / mCount}; + } +} diff --git a/core/java/com/android/internal/graphics/palette/Palette.java b/core/java/com/android/internal/graphics/palette/Palette.java index a4f9a596050c2..8b1137d7de7c0 100644 --- a/core/java/com/android/internal/graphics/palette/Palette.java +++ b/core/java/com/android/internal/graphics/palette/Palette.java @@ -19,48 +19,24 @@ package com.android.internal.graphics.palette; import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.Px; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Rect; -import android.os.AsyncTask; -import android.util.ArrayMap; import android.util.Log; -import android.util.SparseBooleanArray; -import android.util.TimingLogger; -import com.android.internal.graphics.ColorUtils; - -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Map; /** - * Copied from: /frameworks/support/v7/palette/src/main/java/android/support/v7/ - * graphics/Palette.java - * * A helper class to extract prominent colors from an image. - *

- * A number of colors with different profiles are extracted from the image: - *

    - *
  • Vibrant
  • - *
  • Vibrant Dark
  • - *
  • Vibrant Light
  • - *
  • Muted
  • - *
  • Muted Dark
  • - *
  • Muted Light
  • - *
- * These can be retrieved from the appropriate getter method. * - *

- * Instances are created with a {@link Palette.Builder} which supports several options to tweak the + *

Instances are created with a {@link Builder} which supports several options to tweak the * generated Palette. See that class' documentation for more information. - *

- * Generation should always be completed on a background thread, ideally the one in - * which you load your image on. {@link Palette.Builder} supports both synchronous and asynchronous - * generation: + * + *

Generation should always be completed on a background thread, ideally the one in which you + * load your image on. {@link Builder} supports both synchronous and asynchronous generation: * *

  * // Synchronous
@@ -85,346 +61,59 @@ public final class Palette {
         /**
          * Called when the {@link Palette} has been generated.
          */
-        void onGenerated(Palette palette);
+        void onGenerated(@Nullable Palette palette);
     }
 
     static final int DEFAULT_RESIZE_BITMAP_AREA = 112 * 112;
     static final int DEFAULT_CALCULATE_NUMBER_COLORS = 16;
-
-    static final float MIN_CONTRAST_TITLE_TEXT = 3.0f;
-    static final float MIN_CONTRAST_BODY_TEXT = 4.5f;
-
     static final String LOG_TAG = "Palette";
-    static final boolean LOG_TIMINGS = false;
 
-    /**
-     * Start generating a {@link Palette} with the returned {@link Palette.Builder} instance.
-     */
-    public static Palette.Builder from(Bitmap bitmap) {
-        return new Palette.Builder(bitmap);
+    /** Start generating a {@link Palette} with the returned {@link Builder} instance. */
+    @NonNull
+    public static Builder from(@NonNull Bitmap bitmap, @NonNull Quantizer quantizer) {
+        return new Builder(bitmap, quantizer);
     }
 
     /**
      * Generate a {@link Palette} from the pre-generated list of {@link Palette.Swatch} swatches.
-     * This is useful for testing, or if you want to resurrect a {@link Palette} instance from a
-     * list of swatches. Will return null if the {@code swatches} is null.
+     * This
+     * is useful for testing, or if you want to resurrect a {@link Palette} instance from a list of
+     * swatches. Will return null if the {@code swatches} is null.
      */
-    public static Palette from(List swatches) {
-        return new Palette.Builder(swatches).generate();
+    @NonNull
+    public static Palette from(@NonNull List swatches) {
+        return new Builder(swatches).generate();
     }
 
-    /**
-     * @deprecated Use {@link Palette.Builder} to generate the Palette.
-     */
-    @Deprecated
-    public static Palette generate(Bitmap bitmap) {
-        return from(bitmap).generate();
-    }
+    private final List mSwatches;
 
-    /**
-     * @deprecated Use {@link Palette.Builder} to generate the Palette.
-     */
-    @Deprecated
-    public static Palette generate(Bitmap bitmap, int numColors) {
-        return from(bitmap).maximumColorCount(numColors).generate();
-    }
 
-    /**
-     * @deprecated Use {@link Palette.Builder} to generate the Palette.
-     */
-    @Deprecated
-    public static AsyncTask generateAsync(
-            Bitmap bitmap, Palette.PaletteAsyncListener listener) {
-        return from(bitmap).generate(listener);
-    }
+    @Nullable
+    private final Swatch mDominantSwatch;
 
-    /**
-     * @deprecated Use {@link Palette.Builder} to generate the Palette.
-     */
-    @Deprecated
-    public static AsyncTask generateAsync(
-            final Bitmap bitmap, final int numColors, final Palette.PaletteAsyncListener listener) {
-        return from(bitmap).maximumColorCount(numColors).generate(listener);
-    }
-
-    private final List mSwatches;
-    private final List mTargets;
-
-    private final Map mSelectedSwatches;
-    private final SparseBooleanArray mUsedColors;
-
-    private final Palette.Swatch mDominantSwatch;
-
-    Palette(List swatches, List targets) {
+    Palette(List swatches) {
         mSwatches = swatches;
-        mTargets = targets;
-
-        mUsedColors = new SparseBooleanArray();
-        mSelectedSwatches = new ArrayMap<>();
-
         mDominantSwatch = findDominantSwatch();
     }
 
-    /**
-     * Returns all of the swatches which make up the palette.
-     */
+    /** Returns all of the swatches which make up the palette. */
     @NonNull
-    public List getSwatches() {
+    public List getSwatches() {
         return Collections.unmodifiableList(mSwatches);
     }
 
-    /**
-     * Returns the targets used to generate this palette.
-     */
-    @NonNull
-    public List getTargets() {
-        return Collections.unmodifiableList(mTargets);
-    }
-
-    /**
-     * Returns the most vibrant swatch in the palette. Might be null.
-     *
-     * @see Target#VIBRANT
-     */
+    /** Returns the swatch with the highest population, or null if there are no swatches. */
     @Nullable
-    public Palette.Swatch getVibrantSwatch() {
-        return getSwatchForTarget(Target.VIBRANT);
-    }
-
-    /**
-     * Returns a light and vibrant swatch from the palette. Might be null.
-     *
-     * @see Target#LIGHT_VIBRANT
-     */
-    @Nullable
-    public Palette.Swatch getLightVibrantSwatch() {
-        return getSwatchForTarget(Target.LIGHT_VIBRANT);
-    }
-
-    /**
-     * Returns a dark and vibrant swatch from the palette. Might be null.
-     *
-     * @see Target#DARK_VIBRANT
-     */
-    @Nullable
-    public Palette.Swatch getDarkVibrantSwatch() {
-        return getSwatchForTarget(Target.DARK_VIBRANT);
-    }
-
-    /**
-     * Returns a muted swatch from the palette. Might be null.
-     *
-     * @see Target#MUTED
-     */
-    @Nullable
-    public Palette.Swatch getMutedSwatch() {
-        return getSwatchForTarget(Target.MUTED);
-    }
-
-    /**
-     * Returns a muted and light swatch from the palette. Might be null.
-     *
-     * @see Target#LIGHT_MUTED
-     */
-    @Nullable
-    public Palette.Swatch getLightMutedSwatch() {
-        return getSwatchForTarget(Target.LIGHT_MUTED);
-    }
-
-    /**
-     * Returns a muted and dark swatch from the palette. Might be null.
-     *
-     * @see Target#DARK_MUTED
-     */
-    @Nullable
-    public Palette.Swatch getDarkMutedSwatch() {
-        return getSwatchForTarget(Target.DARK_MUTED);
-    }
-
-    /**
-     * Returns the most vibrant color in the palette as an RGB packed int.
-     *
-     * @param defaultColor value to return if the swatch isn't available
-     * @see #getVibrantSwatch()
-     */
-    @ColorInt
-    public int getVibrantColor(@ColorInt final int defaultColor) {
-        return getColorForTarget(Target.VIBRANT, defaultColor);
-    }
-
-    /**
-     * Returns a light and vibrant color from the palette as an RGB packed int.
-     *
-     * @param defaultColor value to return if the swatch isn't available
-     * @see #getLightVibrantSwatch()
-     */
-    @ColorInt
-    public int getLightVibrantColor(@ColorInt final int defaultColor) {
-        return getColorForTarget(Target.LIGHT_VIBRANT, defaultColor);
-    }
-
-    /**
-     * Returns a dark and vibrant color from the palette as an RGB packed int.
-     *
-     * @param defaultColor value to return if the swatch isn't available
-     * @see #getDarkVibrantSwatch()
-     */
-    @ColorInt
-    public int getDarkVibrantColor(@ColorInt final int defaultColor) {
-        return getColorForTarget(Target.DARK_VIBRANT, defaultColor);
-    }
-
-    /**
-     * Returns a muted color from the palette as an RGB packed int.
-     *
-     * @param defaultColor value to return if the swatch isn't available
-     * @see #getMutedSwatch()
-     */
-    @ColorInt
-    public int getMutedColor(@ColorInt final int defaultColor) {
-        return getColorForTarget(Target.MUTED, defaultColor);
-    }
-
-    /**
-     * Returns a muted and light color from the palette as an RGB packed int.
-     *
-     * @param defaultColor value to return if the swatch isn't available
-     * @see #getLightMutedSwatch()
-     */
-    @ColorInt
-    public int getLightMutedColor(@ColorInt final int defaultColor) {
-        return getColorForTarget(Target.LIGHT_MUTED, defaultColor);
-    }
-
-    /**
-     * Returns a muted and dark color from the palette as an RGB packed int.
-     *
-     * @param defaultColor value to return if the swatch isn't available
-     * @see #getDarkMutedSwatch()
-     */
-    @ColorInt
-    public int getDarkMutedColor(@ColorInt final int defaultColor) {
-        return getColorForTarget(Target.DARK_MUTED, defaultColor);
-    }
-
-    /**
-     * Returns the selected swatch for the given target from the palette, or {@code null} if one
-     * could not be found.
-     */
-    @Nullable
-    public Palette.Swatch getSwatchForTarget(@NonNull final Target target) {
-        return mSelectedSwatches.get(target);
-    }
-
-    /**
-     * Returns the selected color for the given target from the palette as an RGB packed int.
-     *
-     * @param defaultColor value to return if the swatch isn't available
-     */
-    @ColorInt
-    public int getColorForTarget(@NonNull final Target target, @ColorInt final int defaultColor) {
-        Palette.Swatch swatch = getSwatchForTarget(target);
-        return swatch != null ? swatch.getRgb() : defaultColor;
-    }
-
-    /**
-     * Returns the dominant swatch from the palette.
-     *
-     * 

The dominant swatch is defined as the swatch with the greatest population (frequency) - * within the palette.

- */ - @Nullable - public Palette.Swatch getDominantSwatch() { + public Swatch getDominantSwatch() { return mDominantSwatch; } - /** - * Returns the color of the dominant swatch from the palette, as an RGB packed int. - * - * @param defaultColor value to return if the swatch isn't available - * @see #getDominantSwatch() - */ - @ColorInt - public int getDominantColor(@ColorInt int defaultColor) { - return mDominantSwatch != null ? mDominantSwatch.getRgb() : defaultColor; - } - - void generate() { - // We need to make sure that the scored targets are generated first. This is so that - // inherited targets have something to inherit from - for (int i = 0, count = mTargets.size(); i < count; i++) { - final Target target = mTargets.get(i); - target.normalizeWeights(); - mSelectedSwatches.put(target, generateScoredTarget(target)); - } - // We now clear out the used colors - mUsedColors.clear(); - } - - private Palette.Swatch generateScoredTarget(final Target target) { - final Palette.Swatch maxScoreSwatch = getMaxScoredSwatchForTarget(target); - if (maxScoreSwatch != null && target.isExclusive()) { - // If we have a swatch, and the target is exclusive, add the color to the used list - mUsedColors.append(maxScoreSwatch.getRgb(), true); - } - return maxScoreSwatch; - } - - private Palette.Swatch getMaxScoredSwatchForTarget(final Target target) { - float maxScore = 0; - Palette.Swatch maxScoreSwatch = null; - for (int i = 0, count = mSwatches.size(); i < count; i++) { - final Palette.Swatch swatch = mSwatches.get(i); - if (shouldBeScoredForTarget(swatch, target)) { - final float score = generateScore(swatch, target); - if (maxScoreSwatch == null || score > maxScore) { - maxScoreSwatch = swatch; - maxScore = score; - } - } - } - return maxScoreSwatch; - } - - private boolean shouldBeScoredForTarget(final Palette.Swatch swatch, final Target target) { - // Check whether the HSL values are within the correct ranges, and this color hasn't - // been used yet. - final float hsl[] = swatch.getHsl(); - return hsl[1] >= target.getMinimumSaturation() && hsl[1] <= target.getMaximumSaturation() - && hsl[2] >= target.getMinimumLightness() && hsl[2] <= target.getMaximumLightness() - && !mUsedColors.get(swatch.getRgb()); - } - - private float generateScore(Palette.Swatch swatch, Target target) { - final float[] hsl = swatch.getHsl(); - - float saturationScore = 0; - float luminanceScore = 0; - float populationScore = 0; - - final int maxPopulation = mDominantSwatch != null ? mDominantSwatch.getPopulation() : 1; - - if (target.getSaturationWeight() > 0) { - saturationScore = target.getSaturationWeight() - * (1f - Math.abs(hsl[1] - target.getTargetSaturation())); - } - if (target.getLightnessWeight() > 0) { - luminanceScore = target.getLightnessWeight() - * (1f - Math.abs(hsl[2] - target.getTargetLightness())); - } - if (target.getPopulationWeight() > 0) { - populationScore = target.getPopulationWeight() - * (swatch.getPopulation() / (float) maxPopulation); - } - - return saturationScore + luminanceScore + populationScore; - } - - private Palette.Swatch findDominantSwatch() { + @Nullable + private Swatch findDominantSwatch() { int maxPop = Integer.MIN_VALUE; - Palette.Swatch maxSwatch = null; + Swatch maxSwatch = null; for (int i = 0, count = mSwatches.size(); i < count; i++) { - Palette.Swatch swatch = mSwatches.get(i); + Swatch swatch = mSwatches.get(i); if (swatch.getPopulation() > maxPop) { maxSwatch = swatch; maxPop = swatch.getPopulation(); @@ -433,148 +122,42 @@ public final class Palette { return maxSwatch; } - private static float[] copyHslValues(Palette.Swatch color) { - final float[] newHsl = new float[3]; - System.arraycopy(color.getHsl(), 0, newHsl, 0, 3); - return newHsl; - } - /** * Represents a color swatch generated from an image's palette. The RGB color can be retrieved - * by calling {@link #getRgb()}. + * by + * calling {@link #getInt()}. */ - public static final class Swatch { - private final int mRed, mGreen, mBlue; - private final int mRgb; + public static class Swatch { + private final Color mColor; private final int mPopulation; - private boolean mGeneratedTextColors; - private int mTitleTextColor; - private int mBodyTextColor; - private float[] mHsl; - - public Swatch(@ColorInt int color, int population) { - mRed = Color.red(color); - mGreen = Color.green(color); - mBlue = Color.blue(color); - mRgb = color; + public Swatch(@ColorInt int colorInt, int population) { + mColor = Color.valueOf(colorInt); mPopulation = population; } - Swatch(int red, int green, int blue, int population) { - mRed = red; - mGreen = green; - mBlue = blue; - mRgb = Color.rgb(red, green, blue); - mPopulation = population; - } - - Swatch(float[] hsl, int population) { - this(ColorUtils.HSLToColor(hsl), population); - mHsl = hsl; - } - - /** - * @return this swatch's RGB color value - */ + /** @return this swatch's RGB color value */ @ColorInt - public int getRgb() { - return mRgb; + public int getInt() { + return mColor.toArgb(); } - /** - * Return this swatch's HSL values. - * hsv[0] is Hue [0 .. 360) - * hsv[1] is Saturation [0...1] - * hsv[2] is Lightness [0...1] - */ - public float[] getHsl() { - if (mHsl == null) { - mHsl = new float[3]; - } - ColorUtils.RGBToHSL(mRed, mGreen, mBlue, mHsl); - return mHsl; - } - - /** - * @return the number of pixels represented by this swatch - */ + /** @return the number of pixels represented by this swatch */ public int getPopulation() { return mPopulation; } - /** - * Returns an appropriate color to use for any 'title' text which is displayed over this - * {@link Palette.Swatch}'s color. This color is guaranteed to have sufficient contrast. - */ - @ColorInt - public int getTitleTextColor() { - ensureTextColorsGenerated(); - return mTitleTextColor; - } - - /** - * Returns an appropriate color to use for any 'body' text which is displayed over this - * {@link Palette.Swatch}'s color. This color is guaranteed to have sufficient contrast. - */ - @ColorInt - public int getBodyTextColor() { - ensureTextColorsGenerated(); - return mBodyTextColor; - } - - private void ensureTextColorsGenerated() { - if (!mGeneratedTextColors) { - // First check white, as most colors will be dark - final int lightBodyAlpha = ColorUtils.calculateMinimumAlpha( - Color.WHITE, mRgb, MIN_CONTRAST_BODY_TEXT); - final int lightTitleAlpha = ColorUtils.calculateMinimumAlpha( - Color.WHITE, mRgb, MIN_CONTRAST_TITLE_TEXT); - - if (lightBodyAlpha != -1 && lightTitleAlpha != -1) { - // If we found valid light values, use them and return - mBodyTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha); - mTitleTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha); - mGeneratedTextColors = true; - return; - } - - final int darkBodyAlpha = ColorUtils.calculateMinimumAlpha( - Color.BLACK, mRgb, MIN_CONTRAST_BODY_TEXT); - final int darkTitleAlpha = ColorUtils.calculateMinimumAlpha( - Color.BLACK, mRgb, MIN_CONTRAST_TITLE_TEXT); - - if (darkBodyAlpha != -1 && darkTitleAlpha != -1) { - // If we found valid dark values, use them and return - mBodyTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha); - mTitleTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha); - mGeneratedTextColors = true; - return; - } - - // If we reach here then we can not find title and body values which use the same - // lightness, we need to use mismatched values - mBodyTextColor = lightBodyAlpha != -1 - ? ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha) - : ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha); - mTitleTextColor = lightTitleAlpha != -1 - ? ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha) - : ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha); - mGeneratedTextColors = true; - } - } - @Override public String toString() { return new StringBuilder(getClass().getSimpleName()) - .append(" [RGB: #").append(Integer.toHexString(getRgb())).append(']') - .append(" [HSL: ").append(Arrays.toString(getHsl())).append(']') - .append(" [Population: ").append(mPopulation).append(']') - .append(" [Title Text: #").append(Integer.toHexString(getTitleTextColor())) + .append(" [") + .append(mColor) .append(']') - .append(" [Body Text: #").append(Integer.toHexString(getBodyTextColor())) - .append(']').toString(); + .append(" [Population: ") + .append(mPopulation) + .append(']') + .toString(); } @Override @@ -586,243 +169,168 @@ public final class Palette { return false; } - Palette.Swatch - swatch = (Palette.Swatch) o; - return mPopulation == swatch.mPopulation && mRgb == swatch.mRgb; + Swatch swatch = (Swatch) o; + return mPopulation == swatch.mPopulation && mColor.toArgb() == swatch.mColor.toArgb(); } @Override public int hashCode() { - return 31 * mRgb + mPopulation; + return 31 * mColor.toArgb() + mPopulation; } } - /** - * Builder class for generating {@link Palette} instances. - */ - public static final class Builder { - private final List mSwatches; + /** Builder class for generating {@link Palette} instances. */ + public static class Builder { + @Nullable + private final List mSwatches; + @Nullable private final Bitmap mBitmap; + @Nullable + private Quantizer mQuantizer = new ColorCutQuantizer(); - private final List mTargets = new ArrayList<>(); private int mMaxColors = DEFAULT_CALCULATE_NUMBER_COLORS; private int mResizeArea = DEFAULT_RESIZE_BITMAP_AREA; private int mResizeMaxDimension = -1; - private final List mFilters = new ArrayList<>(); + @Nullable private Rect mRegion; - private Quantizer mQuantizer; - - /** - * Construct a new {@link Palette.Builder} using a source {@link Bitmap} - */ - public Builder(Bitmap bitmap) { + /** Construct a new {@link Builder} using a source {@link Bitmap} */ + public Builder(@NonNull Bitmap bitmap, @NonNull Quantizer quantizer) { if (bitmap == null || bitmap.isRecycled()) { throw new IllegalArgumentException("Bitmap is not valid"); } - mFilters.add(DEFAULT_FILTER); - mBitmap = bitmap; mSwatches = null; - - // Add the default targets - mTargets.add(Target.LIGHT_VIBRANT); - mTargets.add(Target.VIBRANT); - mTargets.add(Target.DARK_VIBRANT); - mTargets.add(Target.LIGHT_MUTED); - mTargets.add(Target.MUTED); - mTargets.add(Target.DARK_MUTED); + mBitmap = bitmap; + mQuantizer = quantizer == null ? new ColorCutQuantizer() : quantizer; } /** - * Construct a new {@link Palette.Builder} using a list of {@link Palette.Swatch} instances. - * Typically only used for testing. + * Construct a new {@link Builder} using a list of {@link Swatch} instances. Typically only + * used + * for testing. */ - public Builder(List swatches) { + public Builder(@NonNull List swatches) { if (swatches == null || swatches.isEmpty()) { throw new IllegalArgumentException("List of Swatches is not valid"); } - mFilters.add(DEFAULT_FILTER); mSwatches = swatches; mBitmap = null; + mQuantizer = null; } /** - * Set the maximum number of colors to use in the quantization step when using a - * {@link android.graphics.Bitmap} as the source. - *

- * Good values for depend on the source image type. For landscapes, good values are in - * the range 10-16. For images which are largely made up of people's faces then this - * value should be increased to ~24. + * Set the maximum number of colors to use in the quantization step when using a {@link + * android.graphics.Bitmap} as the source. + * + *

Good values for depend on the source image type. For landscapes, good values are in + * the + * range 10-16. For images which are largely made up of people's faces then this value + * should be + * increased to ~24. */ @NonNull - public Palette.Builder maximumColorCount(int colors) { + public Builder maximumColorCount(int colors) { mMaxColors = colors; return this; } /** - * Set the resize value when using a {@link android.graphics.Bitmap} as the source. - * If the bitmap's largest dimension is greater than the value specified, then the bitmap - * will be resized so that its largest dimension matches {@code maxDimension}. If the - * bitmap is smaller or equal, the original is used as-is. - * - * @deprecated Using {@link #resizeBitmapArea(int)} is preferred since it can handle - * abnormal aspect ratios more gracefully. + * Set the resize value when using a {@link android.graphics.Bitmap} as the source. If the + * bitmap's largest dimension is greater than the value specified, then the bitmap will be + * resized so that its largest dimension matches {@code maxDimension}. If the bitmap is + * smaller + * or equal, the original is used as-is. * * @param maxDimension the number of pixels that the max dimension should be scaled down to, - * or any value <= 0 to disable resizing. + * or + * any value <= 0 to disable resizing. + * @deprecated Using {@link #resizeBitmapArea(int)} is preferred since it can handle + * abnormal + * aspect ratios more gracefully. */ @NonNull @Deprecated - public Palette.Builder resizeBitmapSize(final int maxDimension) { + public Builder resizeBitmapSize(int maxDimension) { mResizeMaxDimension = maxDimension; mResizeArea = -1; return this; } /** - * Set the resize value when using a {@link android.graphics.Bitmap} as the source. - * If the bitmap's area is greater than the value specified, then the bitmap - * will be resized so that its area matches {@code area}. If the - * bitmap is smaller or equal, the original is used as-is. - *

- * This value has a large effect on the processing time. The larger the resized image is, - * the greater time it will take to generate the palette. The smaller the image is, the - * more detail is lost in the resulting image and thus less precision for color selection. + * Set the resize value when using a {@link android.graphics.Bitmap} as the source. If the + * bitmap's area is greater than the value specified, then the bitmap will be resized so + * that + * its area matches {@code area}. If the bitmap is smaller or equal, the original is used + * as-is. + * + *

This value has a large effect on the processing time. The larger the resized image is, + * the + * greater time it will take to generate the palette. The smaller the image is, the more + * detail + * is lost in the resulting image and thus less precision for color selection. * * @param area the number of pixels that the intermediary scaled down Bitmap should cover, - * or any value <= 0 to disable resizing. + * or + * any value <= 0 to disable resizing. */ @NonNull - public Palette.Builder resizeBitmapArea(final int area) { + public Builder resizeBitmapArea(int area) { mResizeArea = area; mResizeMaxDimension = -1; return this; } - /** - * Clear all added filters. This includes any default filters added automatically by - * {@link Palette}. - */ - @NonNull - public Palette.Builder clearFilters() { - mFilters.clear(); - return this; - } - - /** - * Add a filter to be able to have fine grained control over which colors are - * allowed in the resulting palette. - * - * @param filter filter to add. - */ - @NonNull - public Palette.Builder addFilter( - Palette.Filter filter) { - if (filter != null) { - mFilters.add(filter); - } - 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. - *

This only works when the original input is a {@link Bitmap}.

* - * @param left The left side of the rectangle used for the region. - * @param top The top of the rectangle used for the region. - * @param right The right side of the rectangle used for the region. + *

This only works when the original input is a {@link Bitmap}. + * + * @param left The left side of the rectangle used for the region. + * @param top The top of the rectangle used for the region. + * @param right The right side of the rectangle used for the region. * @param bottom The bottom of the rectangle used for the region. */ @NonNull - public Palette.Builder setRegion(int left, int top, int right, int bottom) { + public Builder setRegion(@Px int left, @Px int top, @Px int right, @Px int bottom) { if (mBitmap != null) { if (mRegion == null) mRegion = new Rect(); // Set the Rect to be initially the whole Bitmap mRegion.set(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); // Now just get the intersection with the region if (!mRegion.intersect(left, top, right, bottom)) { - throw new IllegalArgumentException("The given region must intersect with " - + "the Bitmap's dimensions."); + throw new IllegalArgumentException( + "The given region must intersect with " + "the Bitmap's dimensions."); } } return this; } - /** - * Clear any previously region set via {@link #setRegion(int, int, int, int)}. - */ + /** Clear any previously region set via {@link #setRegion(int, int, int, int)}. */ @NonNull - public Palette.Builder clearRegion() { + public Builder clearRegion() { mRegion = null; return this; } - /** - * Add a target profile to be generated in the palette. - * - *

You can retrieve the result via {@link Palette#getSwatchForTarget(Target)}.

- */ - @NonNull - public Palette.Builder addTarget(@NonNull final Target target) { - if (!mTargets.contains(target)) { - mTargets.add(target); - } - return this; - } - /** - * Clear all added targets. This includes any default targets added automatically by - * {@link Palette}. - */ - @NonNull - public Palette.Builder clearTargets() { - if (mTargets != null) { - mTargets.clear(); - } - return this; - } - - /** - * Generate and return the {@link Palette} synchronously. - */ + /** Generate and return the {@link Palette} synchronously. */ @NonNull public Palette generate() { - final TimingLogger logger = LOG_TIMINGS - ? new TimingLogger(LOG_TAG, "Generation") - : null; - - List swatches; + List swatches; if (mBitmap != null) { // We have a Bitmap so we need to use quantization to reduce the number of colors // First we'll scale down the bitmap if needed - final Bitmap bitmap = scaleBitmapDown(mBitmap); + Bitmap bitmap = scaleBitmapDown(mBitmap); - if (logger != null) { - logger.addSplit("Processed Bitmap"); - } - - final Rect region = mRegion; + Rect region = mRegion; if (bitmap != mBitmap && region != null) { // If we have a scaled bitmap and a selected region, we need to scale down the // region to match the new scale - final double scale = bitmap.getWidth() / (double) mBitmap.getWidth(); + double scale = bitmap.getWidth() / (double) mBitmap.getWidth(); region.left = (int) Math.floor(region.left * scale); region.top = (int) Math.floor(region.top * scale); region.right = Math.min((int) Math.ceil(region.right * scale), @@ -832,54 +340,47 @@ public final class Palette { } // Now generate a quantizer from the Bitmap - if (mQuantizer == null) { - mQuantizer = new ColorCutQuantizer(); - } - mQuantizer.quantize(getPixelsFromBitmap(bitmap), - mMaxColors, mFilters.isEmpty() ? null : - mFilters.toArray(new Palette.Filter[mFilters.size()])); + mQuantizer.quantize( + getPixelsFromBitmap(bitmap), + mMaxColors); // If created a new bitmap, recycle it if (bitmap != mBitmap) { bitmap.recycle(); } - swatches = mQuantizer.getQuantizedColors(); - - if (logger != null) { - logger.addSplit("Color quantization completed"); - } - } else { + } else if (mSwatches != null) { // Else we're using the provided swatches swatches = mSwatches; + } else { + // The constructors enforce either a bitmap or swatches are present. + throw new AssertionError(); } // Now create a Palette instance - final Palette p = new Palette(swatches, mTargets); + Palette p = new Palette(swatches); // And make it generate itself - p.generate(); - - if (logger != null) { - logger.addSplit("Created Palette"); - logger.dumpToLog(); - } return p; } /** - * Generate the {@link Palette} asynchronously. The provided listener's - * {@link Palette.PaletteAsyncListener#onGenerated} method will be called with the palette when - * generated. + * Generate the {@link Palette} asynchronously. The provided listener's {@link + * PaletteAsyncListener#onGenerated} method will be called with the palette when generated. + * + * @deprecated Use the standard java.util.concurrent or Kotlin + * concurrency utilities to call {@link #generate()} instead. */ @NonNull - public AsyncTask generate(final Palette.PaletteAsyncListener listener) { - if (listener == null) { - throw new IllegalArgumentException("listener can not be null"); - } + @Deprecated + public android.os.AsyncTask generate( + @NonNull PaletteAsyncListener listener) { + assert (listener != null); - return new AsyncTask() { + return new android.os.AsyncTask() { @Override + @Nullable protected Palette doInBackground(Bitmap... params) { try { return generate(); @@ -890,16 +391,16 @@ public final class Palette { } @Override - protected void onPostExecute(Palette colorExtractor) { + protected void onPostExecute(@Nullable Palette colorExtractor) { listener.onGenerated(colorExtractor); } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mBitmap); + }.executeOnExecutor(android.os.AsyncTask.THREAD_POOL_EXECUTOR, mBitmap); } private int[] getPixelsFromBitmap(Bitmap bitmap) { - final int bitmapWidth = bitmap.getWidth(); - final int bitmapHeight = bitmap.getHeight(); - final int[] pixels = new int[bitmapWidth * bitmapHeight]; + int bitmapWidth = bitmap.getWidth(); + int bitmapHeight = bitmap.getHeight(); + int[] pixels = new int[bitmapWidth * bitmapHeight]; bitmap.getPixels(pixels, 0, bitmapWidth, 0, 0, bitmapWidth, bitmapHeight); if (mRegion == null) { @@ -908,32 +409,34 @@ public final class Palette { } else { // If we do have a region, lets create a subset array containing only the region's // pixels - final int regionWidth = mRegion.width(); - final int regionHeight = mRegion.height(); + int regionWidth = mRegion.width(); + int regionHeight = mRegion.height(); // pixels contains all of the pixels, so we need to iterate through each row and // copy the regions pixels into a new smaller array - final int[] subsetPixels = new int[regionWidth * regionHeight]; + int[] subsetPixels = new int[regionWidth * regionHeight]; for (int row = 0; row < regionHeight; row++) { - System.arraycopy(pixels, ((row + mRegion.top) * bitmapWidth) + mRegion.left, - subsetPixels, row * regionWidth, regionWidth); + System.arraycopy( + pixels, + ((row + mRegion.top) * bitmapWidth) + mRegion.left, + subsetPixels, + row * regionWidth, + regionWidth); } return subsetPixels; } } - /** - * Scale the bitmap down as needed. - */ - private Bitmap scaleBitmapDown(final Bitmap bitmap) { + /** Scale the bitmap down as needed. */ + private Bitmap scaleBitmapDown(Bitmap bitmap) { double scaleRatio = -1; if (mResizeArea > 0) { - final int bitmapArea = bitmap.getWidth() * bitmap.getHeight(); + int bitmapArea = bitmap.getWidth() * bitmap.getHeight(); if (bitmapArea > mResizeArea) { scaleRatio = Math.sqrt(mResizeArea / (double) bitmapArea); } } else if (mResizeMaxDimension > 0) { - final int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight()); + int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight()); if (maxDimension > mResizeMaxDimension) { scaleRatio = mResizeMaxDimension / (double) maxDimension; } @@ -944,11 +447,13 @@ public final class Palette { return bitmap; } - return Bitmap.createScaledBitmap(bitmap, + return Bitmap.createScaledBitmap( + bitmap, (int) Math.ceil(bitmap.getWidth() * scaleRatio), (int) Math.ceil(bitmap.getHeight() * scaleRatio), false); } + } /** @@ -961,9 +466,7 @@ public final class Palette { * * @param rgb the color in RGB888. * @param hsl HSL representation of the color. - * * @return true if the color is allowed, false if not. - * * @see Palette.Builder#addFilter(Palette.Filter) */ boolean isAllowed(int rgb, float[] hsl); @@ -1004,3 +507,4 @@ public final class Palette { } }; } + diff --git a/core/java/com/android/internal/graphics/palette/Quantizer.java b/core/java/com/android/internal/graphics/palette/Quantizer.java index db60f2e9dc69a..a219ea3aa7d07 100644 --- a/core/java/com/android/internal/graphics/palette/Quantizer.java +++ b/core/java/com/android/internal/graphics/palette/Quantizer.java @@ -22,6 +22,15 @@ 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); + /** + * Create colors representative of the colors present in pixels. + * @param pixels Set of ARGB representation of a color. + * @param maxColors number of colors to generate + */ + void quantize(int[] pixels, int maxColors); + + /** + * List of colors generated by previous call to quantize. + */ List getQuantizedColors(); } diff --git a/core/java/com/android/internal/graphics/palette/Target.java b/core/java/com/android/internal/graphics/palette/Target.java index 0540d80ef6f05..96e7faa81c3ae 100644 --- a/core/java/com/android/internal/graphics/palette/Target.java +++ b/core/java/com/android/internal/graphics/palette/Target.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * 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. @@ -16,368 +16,234 @@ package com.android.internal.graphics.palette; -/* - * Copyright 2015 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. - */ import android.annotation.FloatRange; +import android.annotation.IntRange; +import android.annotation.NonNull; /** - * Copied from: frameworks/support/v7/palette/src/main/java/android/support/v7/graphics/Target.java - * - * A class which allows custom selection of colors in a {@link Palette}'s generation. Instances - * can be created via the {@link android.support.v7.graphics.Target.Builder} class. - * - *

To use the target, use the {@link Palette.Builder#addTarget(Target)} API when building a - * Palette.

+ * A class which allows custom selection of colors in a {@link Palette}'s generation. Instances can + * be created via the {@link Builder} class. */ + public final class Target { + private static final float WEIGHT_CHROMA = 0.5f; + private static final float WEIGHT_RELATIVE_LUMINANCE = 0.5f; + private static final float WEIGHT_POPULATION = 0.3f; + private static final float WEIGHT_HUE = 0.2f; - private static final float TARGET_DARK_LUMA = 0.26f; - private static final float MAX_DARK_LUMA = 0.45f; + // Arbitrarily chosen, except max - CAM16 chroma has a ceiling of 130, based on unit testing. + private static final float DEFAULT_CHROMA_MIN = 0.f; + private static final float DEFAULT_CHROMA_MAX = 130.f; + private static final float DEFAULT_CHROMA_TARGET = 30.f; - private static final float MIN_LIGHT_LUMA = 0.55f; - private static final float TARGET_LIGHT_LUMA = 0.74f; - - private static final float MIN_NORMAL_LUMA = 0.3f; - private static final float TARGET_NORMAL_LUMA = 0.5f; - private static final float MAX_NORMAL_LUMA = 0.7f; - - private static final float TARGET_MUTED_SATURATION = 0.3f; - private static final float MAX_MUTED_SATURATION = 0.4f; - - private static final float TARGET_VIBRANT_SATURATION = 1f; - private static final float MIN_VIBRANT_SATURATION = 0.35f; - - private static final float WEIGHT_SATURATION = 0.24f; - private static final float WEIGHT_LUMA = 0.52f; - private static final float WEIGHT_POPULATION = 0.24f; - - static final int INDEX_MIN = 0; - static final int INDEX_TARGET = 1; - static final int INDEX_MAX = 2; - - static final int INDEX_WEIGHT_SAT = 0; - static final int INDEX_WEIGHT_LUMA = 1; - static final int INDEX_WEIGHT_POP = 2; - - /** - * A target which has the characteristics of a vibrant color which is light in luminance. - */ - public static final Target LIGHT_VIBRANT; - - /** - * A target which has the characteristics of a vibrant color which is neither light or dark. - */ - public static final Target VIBRANT; - - /** - * A target which has the characteristics of a vibrant color which is dark in luminance. - */ - public static final Target DARK_VIBRANT; - - /** - * A target which has the characteristics of a muted color which is light in luminance. - */ - public static final Target LIGHT_MUTED; - - /** - * A target which has the characteristics of a muted color which is neither light or dark. - */ - public static final Target MUTED; - - /** - * A target which has the characteristics of a muted color which is dark in luminance. - */ - public static final Target DARK_MUTED; - - static { - LIGHT_VIBRANT = new Target(); - setDefaultLightLightnessValues(LIGHT_VIBRANT); - setDefaultVibrantSaturationValues(LIGHT_VIBRANT); - - VIBRANT = new Target(); - setDefaultNormalLightnessValues(VIBRANT); - setDefaultVibrantSaturationValues(VIBRANT); - - DARK_VIBRANT = new Target(); - setDefaultDarkLightnessValues(DARK_VIBRANT); - setDefaultVibrantSaturationValues(DARK_VIBRANT); - - LIGHT_MUTED = new Target(); - setDefaultLightLightnessValues(LIGHT_MUTED); - setDefaultMutedSaturationValues(LIGHT_MUTED); - - MUTED = new Target(); - setDefaultNormalLightnessValues(MUTED); - setDefaultMutedSaturationValues(MUTED); - - DARK_MUTED = new Target(); - setDefaultDarkLightnessValues(DARK_MUTED); - setDefaultMutedSaturationValues(DARK_MUTED); - } - - final float[] mSaturationTargets = new float[3]; - final float[] mLightnessTargets = new float[3]; - final float[] mWeights = new float[3]; - boolean mIsExclusive = true; // default to true + private float mTargetRelativeLuminance = -1.0f; + private float mChromaWeight; + private float mChromaTarget; + private float mChromaMin; + private float mChromaMax; + private float mRelativeLuminanceWeight; + private float mPopulationWeight; + private float mHueWeight; + private float mTargetHue; Target() { - setTargetDefaultValues(mSaturationTargets); - setTargetDefaultValues(mLightnessTargets); - setDefaultWeights(); + mChromaMax = DEFAULT_CHROMA_MAX; + mChromaMin = DEFAULT_CHROMA_MIN; + mChromaTarget = DEFAULT_CHROMA_TARGET; + mChromaWeight = WEIGHT_CHROMA; + mRelativeLuminanceWeight = WEIGHT_RELATIVE_LUMINANCE; + mPopulationWeight = WEIGHT_POPULATION; + mHueWeight = WEIGHT_HUE; } - Target(Target from) { - System.arraycopy(from.mSaturationTargets, 0, mSaturationTargets, 0, - mSaturationTargets.length); - System.arraycopy(from.mLightnessTargets, 0, mLightnessTargets, 0, - mLightnessTargets.length); - System.arraycopy(from.mWeights, 0, mWeights, 0, mWeights.length); + Target(@NonNull Target from) { + mTargetRelativeLuminance = from.mTargetRelativeLuminance; + mChromaWeight = from.mChromaWeight; + mRelativeLuminanceWeight = from.mRelativeLuminanceWeight; + mPopulationWeight = from.mPopulationWeight; + mHueWeight = from.mHueWeight; + mChromaTarget = from.mChromaTarget; + mChromaMin = from.mChromaMin; + mChromaMax = from.mChromaMax; + } + + /** The relative luminance value for this target. */ + @FloatRange(from = 0, to = 100) + public float getTargetRelativeLuminance() { + return mTargetRelativeLuminance; + } + + /** The relative luminance value for this target. */ + @FloatRange(from = 0, to = 100) + public float getTargetPerceptualLuminance() { + return Contrast.yToLstar(mTargetRelativeLuminance); + } + + /** The minimum chroma value for this target. */ + @FloatRange(from = 0, to = 100) + public float getMinimumChroma() { + return mChromaMin; + } + + /** The target chroma value for this target. */ + @FloatRange(from = 0, to = 100) + public float getTargetChroma() { + return mChromaTarget; + } + + /** The maximum chroma value for this target. */ + @FloatRange(from = 0, to = 130) + public float getMaximumChroma() { + return mChromaMax; + } + + /** The target hue value for this target. */ + @FloatRange(from = 0, to = 100) + public float getTargetHue() { + return mTargetHue; } /** - * The minimum saturation value for this target. - */ - @FloatRange(from = 0, to = 1) - public float getMinimumSaturation() { - return mSaturationTargets[INDEX_MIN]; - } - - /** - * The target saturation value for this target. - */ - @FloatRange(from = 0, to = 1) - public float getTargetSaturation() { - return mSaturationTargets[INDEX_TARGET]; - } - - /** - * The maximum saturation value for this target. - */ - @FloatRange(from = 0, to = 1) - public float getMaximumSaturation() { - return mSaturationTargets[INDEX_MAX]; - } - - /** - * The minimum lightness value for this target. - */ - @FloatRange(from = 0, to = 1) - public float getMinimumLightness() { - return mLightnessTargets[INDEX_MIN]; - } - - /** - * The target lightness value for this target. - */ - @FloatRange(from = 0, to = 1) - public float getTargetLightness() { - return mLightnessTargets[INDEX_TARGET]; - } - - /** - * The maximum lightness value for this target. - */ - @FloatRange(from = 0, to = 1) - public float getMaximumLightness() { - return mLightnessTargets[INDEX_MAX]; - } - - /** - * Returns the weight of importance that this target places on a color's saturation within - * the image. + * Returns the weight of importance that this target places on a color's chroma within the + * image. * *

The larger the weight, relative to the other weights, the more important that a color - * being close to the target value has on selection.

+ * being + * close to the target value has on selection. * - * @see #getTargetSaturation() + * @see #getTargetChroma() */ - public float getSaturationWeight() { - return mWeights[INDEX_WEIGHT_SAT]; + public float getChromaWeight() { + return mChromaWeight; } /** - * Returns the weight of importance that this target places on a color's lightness within - * the image. + * Returns the weight of importance that this target places on a color's lightness within the + * image. * *

The larger the weight, relative to the other weights, the more important that a color - * being close to the target value has on selection.

+ * being + * close to the target value has on selection. * - * @see #getTargetLightness() + * @see #getTargetRelativeLuminance() */ public float getLightnessWeight() { - return mWeights[INDEX_WEIGHT_LUMA]; + return mRelativeLuminanceWeight; } /** - * Returns the weight of importance that this target places on a color's population within - * the image. + * Returns the weight of importance that this target places on a color's population within the + * image. * - *

The larger the weight, relative to the other weights, the more important that a - * color's population being close to the most populous has on selection.

+ *

The larger the weight, relative to the other weights, the more important that a color's + * population being close to the most populous has on selection. */ public float getPopulationWeight() { - return mWeights[INDEX_WEIGHT_POP]; + return mPopulationWeight; } /** - * Returns whether any color selected for this target is exclusive for this target only. + * Returns the weight of importance that this target places on a color's hue. * - *

If false, then the color can be selected for other targets.

+ *

The larger the weight, relative to the other weights, the more important that a color's + * hue being close to the desired hue has on selection. */ - public boolean isExclusive() { - return mIsExclusive; + public float getHueWeight() { + return mHueWeight; } - private static void setTargetDefaultValues(final float[] values) { - values[INDEX_MIN] = 0f; - values[INDEX_TARGET] = 0.5f; - values[INDEX_MAX] = 1f; - } - private void setDefaultWeights() { - mWeights[INDEX_WEIGHT_SAT] = WEIGHT_SATURATION; - mWeights[INDEX_WEIGHT_LUMA] = WEIGHT_LUMA; - mWeights[INDEX_WEIGHT_POP] = WEIGHT_POPULATION; - } - - void normalizeWeights() { - float sum = 0; - for (int i = 0, z = mWeights.length; i < z; i++) { - float weight = mWeights[i]; - if (weight > 0) { - sum += weight; - } - } - if (sum != 0) { - for (int i = 0, z = mWeights.length; i < z; i++) { - if (mWeights[i] > 0) { - mWeights[i] /= sum; - } - } - } - } - - private static void setDefaultDarkLightnessValues(Target target) { - target.mLightnessTargets[INDEX_TARGET] = TARGET_DARK_LUMA; - target.mLightnessTargets[INDEX_MAX] = MAX_DARK_LUMA; - } - - private static void setDefaultNormalLightnessValues(Target target) { - target.mLightnessTargets[INDEX_MIN] = MIN_NORMAL_LUMA; - target.mLightnessTargets[INDEX_TARGET] = TARGET_NORMAL_LUMA; - target.mLightnessTargets[INDEX_MAX] = MAX_NORMAL_LUMA; - } - - private static void setDefaultLightLightnessValues(Target target) { - target.mLightnessTargets[INDEX_MIN] = MIN_LIGHT_LUMA; - target.mLightnessTargets[INDEX_TARGET] = TARGET_LIGHT_LUMA; - } - - private static void setDefaultVibrantSaturationValues(Target target) { - target.mSaturationTargets[INDEX_MIN] = MIN_VIBRANT_SATURATION; - target.mSaturationTargets[INDEX_TARGET] = TARGET_VIBRANT_SATURATION; - } - - private static void setDefaultMutedSaturationValues(Target target) { - target.mSaturationTargets[INDEX_TARGET] = TARGET_MUTED_SATURATION; - target.mSaturationTargets[INDEX_MAX] = MAX_MUTED_SATURATION; - } - - /** - * Builder class for generating custom {@link Target} instances. - */ - public final static class Builder { + /** Builder class for generating custom {@link Target} instances. */ + public static class Builder { private final Target mTarget; - /** - * Create a new {@link Target} builder from scratch. - */ + /** Create a new {@link Target} builder from scratch. */ public Builder() { mTarget = new Target(); } - /** - * Create a new builder based on an existing {@link Target}. - */ - public Builder(Target target) { + /** Create a new builder based on an existing {@link Target}. */ + public Builder(@NonNull Target target) { mTarget = new Target(target); } - /** - * Set the minimum saturation value for this target. - */ - public Target.Builder setMinimumSaturation(@FloatRange(from = 0, to = 1) float value) { - mTarget.mSaturationTargets[INDEX_MIN] = value; + /** Set the minimum chroma value for this target. */ + @NonNull + public Builder setMinimumChroma(@FloatRange(from = 0, to = 100) float value) { + mTarget.mChromaMin = value; + return this; + } + + /** Set the target/ideal chroma value for this target. */ + @NonNull + public Builder setTargetChroma(@FloatRange(from = 0, to = 100) float value) { + mTarget.mChromaTarget = value; + return this; + } + + /** Set the maximum chroma value for this target. */ + @NonNull + public Builder setMaximumChroma(@FloatRange(from = 0, to = 100) float value) { + mTarget.mChromaMax = value; + return this; + } + + /** Set the minimum lightness value for this target, using Y in XYZ color space. */ + @NonNull + public Builder setTargetRelativeLuminance(@FloatRange(from = 0, to = 100) float value) { + mTarget.mTargetRelativeLuminance = value; + return this; + } + + /** Set the minimum lightness value for this target, using L* in LAB color space. */ + @NonNull + public Builder setTargetPerceptualLuminance(@FloatRange(from = 0, to = 100) float value) { + mTarget.mTargetRelativeLuminance = Contrast.lstarToY(value); return this; } /** - * Set the target/ideal saturation value for this target. + * Set the hue desired from the target. This hue is not enforced, the only consequence + * is points will be awarded to seed colors the closer they are to this hue. */ - public Target.Builder setTargetSaturation(@FloatRange(from = 0, to = 1) float value) { - mTarget.mSaturationTargets[INDEX_TARGET] = value; + @NonNull + public Builder setTargetHue(@IntRange(from = 0, to = 360) int hue) { + mTarget.mTargetHue = hue; + return this; + } + + /** Sets lightness value for this target. */ + @NonNull + public Builder setContrastRatio( + @FloatRange(from = 1, to = 21) float value, + @FloatRange(from = 0, to = 100) float relativeLuminance) { + float counterpartY = relativeLuminance; + float lstar = Contrast.yToLstar(counterpartY); + + float targetY; + if (lstar < 50) { + targetY = Contrast.lighterY(counterpartY, value); + } else { + targetY = Contrast.darkerY(counterpartY, value); + } + mTarget.mTargetRelativeLuminance = targetY; return this; } /** - * Set the maximum saturation value for this target. - */ - public Target.Builder setMaximumSaturation(@FloatRange(from = 0, to = 1) float value) { - mTarget.mSaturationTargets[INDEX_MAX] = value; - return this; - } - - /** - * Set the minimum lightness value for this target. - */ - public Target.Builder setMinimumLightness(@FloatRange(from = 0, to = 1) float value) { - mTarget.mLightnessTargets[INDEX_MIN] = value; - return this; - } - - /** - * Set the target/ideal lightness value for this target. - */ - public Target.Builder setTargetLightness(@FloatRange(from = 0, to = 1) float value) { - mTarget.mLightnessTargets[INDEX_TARGET] = value; - return this; - } - - /** - * Set the maximum lightness value for this target. - */ - public Target.Builder setMaximumLightness(@FloatRange(from = 0, to = 1) float value) { - mTarget.mLightnessTargets[INDEX_MAX] = value; - return this; - } - - /** - * Set the weight of importance that this target will place on saturation values. + * Set the weight of importance that this target will place on chroma values. * *

The larger the weight, relative to the other weights, the more important that a color - * being close to the target value has on selection.

+ * being close to the target value has on selection. * - *

A weight of 0 means that it has no weight, and thus has no - * bearing on the selection.

+ *

A weight of 0 means that it has no weight, and thus has no bearing on the selection. * - * @see #setTargetSaturation(float) + * @see #setTargetChroma(float) */ - public Target.Builder setSaturationWeight(@FloatRange(from = 0) float weight) { - mTarget.mWeights[INDEX_WEIGHT_SAT] = weight; + @NonNull + public Builder setChromaWeight(@FloatRange(from = 0) float weight) { + mTarget.mChromaWeight = weight; return this; } @@ -385,51 +251,40 @@ public final class Target { * Set the weight of importance that this target will place on lightness values. * *

The larger the weight, relative to the other weights, the more important that a color - * being close to the target value has on selection.

+ * being close to the target value has on selection. * - *

A weight of 0 means that it has no weight, and thus has no - * bearing on the selection.

+ *

A weight of 0 means that it has no weight, and thus has no bearing on the selection. * - * @see #setTargetLightness(float) + * @see #setTargetRelativeLuminance(float) */ - public Target.Builder setLightnessWeight(@FloatRange(from = 0) float weight) { - mTarget.mWeights[INDEX_WEIGHT_LUMA] = weight; + @NonNull + public Builder setLightnessWeight(@FloatRange(from = 0) float weight) { + mTarget.mRelativeLuminanceWeight = weight; return this; } /** * Set the weight of importance that this target will place on a color's population within - * the image. + * the + * image. * *

The larger the weight, relative to the other weights, the more important that a - * color's population being close to the most populous has on selection.

+ * color's + * population being close to the most populous has on selection. * - *

A weight of 0 means that it has no weight, and thus has no - * bearing on the selection.

+ *

A weight of 0 means that it has no weight, and thus has no bearing on the selection. */ - public Target.Builder setPopulationWeight(@FloatRange(from = 0) float weight) { - mTarget.mWeights[INDEX_WEIGHT_POP] = weight; + @NonNull + public Builder setPopulationWeight(@FloatRange(from = 0) float weight) { + mTarget.mPopulationWeight = weight; return this; } - /** - * Set whether any color selected for this target is exclusive to this target only. - * Defaults to true. - * - * @param exclusive true if any the color is exclusive to this target, or false is the - * color can be selected for other targets. - */ - public Target.Builder setExclusive(boolean exclusive) { - mTarget.mIsExclusive = exclusive; - return this; - } - /** - * Builds and returns the resulting {@link Target}. - */ + /** Builds and returns the resulting {@link Target}. */ + @NonNull public Target build() { return mTarget; } } - -} \ No newline at end of file +} diff --git a/core/java/com/android/internal/graphics/palette/VariationalKMeansQuantizer.java b/core/java/com/android/internal/graphics/palette/VariationalKMeansQuantizer.java index b0355350dc158..d791f7b3e6bee 100644 --- a/core/java/com/android/internal/graphics/palette/VariationalKMeansQuantizer.java +++ b/core/java/com/android/internal/graphics/palette/VariationalKMeansQuantizer.java @@ -70,10 +70,9 @@ public class VariationalKMeansQuantizer implements 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) { + public void quantize(int[] pixels, int maxColors) { // Start by converting all colors to HSL. // HLS is way more meaningful for clustering than RGB. final float[] hsl = {0, 0, 0}; @@ -111,16 +110,18 @@ public class VariationalKMeansQuantizer implements Quantizer { // Convert data to final format, de-normalizing the hue. mQuantizedColors = new ArrayList<>(); + float[] mHsl = new float[3]; 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())); + + mHsl[0] = centroid[0] * 360f; + mHsl[1] = centroid[1]; + mHsl[2] = centroid[2]; + int color = ColorUtils.HSLToColor(mHsl); + mQuantizedColors.add(new Palette.Swatch(color, mean.getItems().size())); } } diff --git a/core/java/com/android/internal/graphics/palette/WSMeansQuantizer.java b/core/java/com/android/internal/graphics/palette/WSMeansQuantizer.java new file mode 100644 index 0000000000000..a87a34f4ae110 --- /dev/null +++ b/core/java/com/android/internal/graphics/palette/WSMeansQuantizer.java @@ -0,0 +1,269 @@ +/* + * 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 java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +/** + * A color quantizer based on the Kmeans algorithm. + * + * This is an implementation of Kmeans based on Celebi's 2011 paper, + * "Improving the Performance of K-Means for Color Quantization". In the paper, this algorithm is + * referred to as "WSMeans", or, "Weighted Square Means" The main advantages of this Kmeans + * implementation are taking advantage of triangle properties to avoid distance calculations, as + * well as indexing colors by their count, thus minimizing the number of points to move around. + * + * Celebi's paper also stabilizes results and guarantees high quality by using starting centroids + * from Wu's quantization algorithm. See CelebiQuantizer for more info. + */ +public class WSMeansQuantizer implements Quantizer { + Mean[] mMeans; + private final Map mCountByColor = new HashMap<>(); + private final Map mMeanIndexByColor = new HashMap<>(); + private final Set mUniqueColors = new HashSet<>(); + private final List mSwatches = new ArrayList<>(); + private final CentroidProvider mCentroidProvider; + + public WSMeansQuantizer( + float[][] means, CentroidProvider centroidProvider, int[] pixels, int maxColors) { + if (pixels == null) { + pixels = new int[]{}; + } + mCentroidProvider = centroidProvider; + mMeans = new Mean[maxColors]; + for (int i = 0; i < means.length; i++) { + mMeans[i] = new Mean(means[i]); + } + + if (maxColors > means.length) { + int randomMeansToCreate = maxColors - means.length; + for (int i = 0; i < randomMeansToCreate; i++) { + mMeans[means.length + i] = new Mean(100); + } + } + + for (int pixel : pixels) { + Integer currentCount = mCountByColor.get(pixel); + if (currentCount == null) { + currentCount = 0; + mUniqueColors.add(pixel); + } + mCountByColor.put(pixel, currentCount + 1); + } + for (int color : mUniqueColors) { + int closestMeanIndex = -1; + double closestMeanDistance = -1; + float[] centroid = mCentroidProvider.getCentroid(color); + for (int i = 0; i < mMeans.length; i++) { + double distance = mCentroidProvider.distance(centroid, mMeans[i].center); + if (closestMeanIndex == -1 || distance < closestMeanDistance) { + closestMeanIndex = i; + closestMeanDistance = distance; + } + } + mMeanIndexByColor.put(color, closestMeanIndex); + } + + if (pixels.length == 0) { + return; + } + + predict(maxColors, 0); + } + + /** Create starting centroids for K-means from a set of colors. */ + public static float[][] createStartingCentroids(CentroidProvider centroidProvider, + List swatches) { + float[][] startingCentroids = new float[swatches.size()][]; + for (int i = 0; i < swatches.size(); i++) { + startingCentroids[i] = centroidProvider.getCentroid(swatches.get(i).getInt()); + } + return startingCentroids; + } + + /** Create random starting centroids for K-means. */ + public static float[][] randomMeans(int maxColors, int upperBound) { + float[][] means = new float[maxColors][]; + for (int i = 0; i < maxColors; i++) { + means[i] = new Mean(upperBound).center; + } + return means; + } + + + @Override + public void quantize(int[] pixels, int maxColors) { + + } + + @Override + public List getQuantizedColors() { + return mSwatches; + } + + private void predict(int maxColors, int iterationsCompleted) { + double[][] centroidDistance = new double[maxColors][maxColors]; + for (int i = 0; i <= maxColors; i++) { + for (int j = i + 1; j < maxColors; j++) { + float[] meanI = mMeans[i].center; + float[] meanJ = mMeans[j].center; + double distance = mCentroidProvider.distance(meanI, meanJ); + centroidDistance[i][j] = distance; + centroidDistance[j][i] = distance; + } + } + + // Construct a K×K matrix M in which row i is a permutation of + // 1,2,…,K that represents the clusters in increasing order of + // distance of their centers from ci; + int[][] distanceMatrix = new int[maxColors][maxColors]; + for (int i = 0; i < maxColors; i++) { + double[] distancesFromIToAnotherMean = centroidDistance[i]; + double[] sortedByDistanceAscending = distancesFromIToAnotherMean.clone(); + Arrays.sort(sortedByDistanceAscending); + int[] outputRow = new int[maxColors]; + for (int j = 0; j < maxColors; j++) { + outputRow[j] = findIndex(distancesFromIToAnotherMean, sortedByDistanceAscending[j]); + } + distanceMatrix[i] = outputRow; + } + + // for (i=1;i≤N′;i=i+ 1) do + // Let Sp be the cluster that xi was assigned to in the previous + // iteration; + // p=m[i]; + // min_dist=prev_dist=jjxi−cpjj2; + boolean anyColorMoved = false; + for (int intColor : mUniqueColors) { + float[] color = mCentroidProvider.getCentroid(intColor); + int indexOfCurrentMean = mMeanIndexByColor.get(intColor); + Mean currentMean = mMeans[indexOfCurrentMean]; + double minDistance = mCentroidProvider.distance(color, currentMean.center); + for (int j = 1; j < maxColors; j++) { + int indexOfClusterFromCurrentToJ = distanceMatrix[indexOfCurrentMean][j]; + double distanceBetweenJAndCurrent = + centroidDistance[indexOfCurrentMean][indexOfClusterFromCurrentToJ]; + if (distanceBetweenJAndCurrent >= (4 * minDistance)) { + break; + } + double distanceBetweenJAndColor = mCentroidProvider.distance(mMeans[j].center, + color); + if (distanceBetweenJAndColor < minDistance) { + minDistance = distanceBetweenJAndColor; + mMeanIndexByColor.remove(intColor); + mMeanIndexByColor.put(intColor, j); + anyColorMoved = true; + } + } + } + + List buckets = new ArrayList<>(); + for (int i = 0; i < maxColors; i++) { + buckets.add(new MeanBucket()); + } + + for (int intColor : mUniqueColors) { + int meanIndex = mMeanIndexByColor.get(intColor); + MeanBucket meanBucket = buckets.get(meanIndex); + meanBucket.add(mCentroidProvider.getCentroid(intColor), intColor, + mCountByColor.get(intColor)); + } + + List swatches = new ArrayList<>(); + boolean done = !anyColorMoved && iterationsCompleted > 0 || iterationsCompleted >= 100; + if (done) { + for (int i = 0; i < buckets.size(); i++) { + MeanBucket a = buckets.get(i); + if (a.mCount <= 0) { + continue; + } + List bucketsToMerge = new ArrayList<>(); + for (int j = i + 1; j < buckets.size(); j++) { + MeanBucket b = buckets.get(j); + if (b.mCount == 0) { + continue; + } + float[] bCentroid = b.getCentroid(); + assert (a.mCount > 0); + assert (a.getCentroid() != null); + + assert (bCentroid != null); + if (mCentroidProvider.distance(a.getCentroid(), b.getCentroid()) < 5) { + bucketsToMerge.add(b); + } + } + + for (MeanBucket bucketToMerge : bucketsToMerge) { + float[] centroid = bucketToMerge.getCentroid(); + a.add(centroid, mCentroidProvider.getColor(centroid), bucketToMerge.mCount); + buckets.remove(bucketToMerge); + } + } + + for (MeanBucket bucket : buckets) { + float[] centroid = bucket.getCentroid(); + if (centroid == null) { + continue; + } + + int rgb = mCentroidProvider.getColor(centroid); + swatches.add(new Palette.Swatch(rgb, bucket.mCount)); + mSwatches.clear(); + mSwatches.addAll(swatches); + } + } else { + List emptyBuckets = new ArrayList<>(); + for (int i = 0; i < buckets.size(); i++) { + MeanBucket bucket = buckets.get(i); + if ((bucket.getCentroid() == null) || (bucket.mCount == 0)) { + emptyBuckets.add(bucket); + for (Integer color : mUniqueColors) { + int meanIndex = mMeanIndexByColor.get(color); + if (meanIndex > i) { + mMeanIndexByColor.put(color, meanIndex--); + } + } + } + } + + Mean[] newMeans = new Mean[buckets.size()]; + for (int i = 0; i < buckets.size(); i++) { + float[] centroid = buckets.get(i).getCentroid(); + newMeans[i] = new Mean(centroid); + } + + predict(buckets.size(), iterationsCompleted + 1); + } + + } + + private static int findIndex(double[] list, double element) { + for (int i = 0; i < list.length; i++) { + if (list[i] == element) { + return i; + } + } + throw new IllegalArgumentException("Element not in list"); + } +} diff --git a/core/java/com/android/internal/graphics/palette/WuQuantizer.java b/core/java/com/android/internal/graphics/palette/WuQuantizer.java new file mode 100644 index 0000000000000..01e45f6139864 --- /dev/null +++ b/core/java/com/android/internal/graphics/palette/WuQuantizer.java @@ -0,0 +1,442 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; + +// All reference Wu implementations are based on the original C code by Wu. +// Comments on methods are the same as in the original implementation, and the comment below +// is the original class header. + +/** + * Wu's Color Quantizer (v. 2) (see Graphics Gems vol. II, pp. 126-133) Author: Xiaolin Wu + * + *

Algorithm: Greedy orthogonal bipartition of RGB space for variance minimization aided by + * inclusion-exclusion tricks. For speed no nearest neighbor search is done. Slightly better + * performance can be expected by more sophisticated but more expensive versions. + */ +public class WuQuantizer implements Quantizer { + private static final int MAX_COLORS = 256; + private static final int RED = 2; + private static final int GREEN = 1; + private static final int BLUE = 0; + + private static final int QUANT_SIZE = 33; + private final List mSwatches = new ArrayList<>(); + + @Override + public List getQuantizedColors() { + return mSwatches; + } + + private static final class Box { + int mR0; /* min value, exclusive */ + int mR1; /* max value, inclusive */ + int mG0; + int mG1; + int mB0; + int mB1; + int mVol; + } + + private final int mSize; /* image size, in bytes. */ + private int mMaxColors; + private int[] mQadd; + private final int[] mPixels; + + private final double[][][] mM2 = new double[QUANT_SIZE][QUANT_SIZE][QUANT_SIZE]; + private final long[][][] mWt = new long[QUANT_SIZE][QUANT_SIZE][QUANT_SIZE]; + private final long[][][] mMr = new long[QUANT_SIZE][QUANT_SIZE][QUANT_SIZE]; + private final long[][][] mMg = new long[QUANT_SIZE][QUANT_SIZE][QUANT_SIZE]; + private final long[][][] mMb = new long[QUANT_SIZE][QUANT_SIZE][QUANT_SIZE]; + + public WuQuantizer(int[] pixels, int maxColorCount) { + if (pixels == null) { + pixels = new int[]{}; + } + this.mPixels = pixels; + this.mSize = pixels.length; + } + + @Override + public void quantize(int[] colors, int maxColorCount) { + // All of the sample Wu implementations are reimplementations of a snippet of C code from + // the early 90s. They all cap the maximum # of colors at 256, and it is impossible to tell + // if this is a requirement, a consequence of QUANT_SIZE, or arbitrary. + this.mMaxColors = Math.min(MAX_COLORS, maxColorCount); + Box[] cube = new Box[mMaxColors]; + int red, green, blue; + + int next, i, k; + long weight; + double[] vv = new double[mMaxColors]; + double temp; + + compute3DHistogram(mWt, mMr, mMg, mMb, mM2); + computeMoments(mWt, mMr, mMg, mMb, mM2); + + for (i = 0; i < mMaxColors; i++) { + cube[i] = new Box(); + } + + cube[0].mR0 = cube[0].mG0 = cube[0].mB0 = 0; + cube[0].mR1 = cube[0].mG1 = cube[0].mB1 = QUANT_SIZE - 1; + next = 0; + + for (i = 1; i < mMaxColors; ++i) { + if (cut(cube[next], cube[i])) { + vv[next] = (cube[next].mVol > 1) ? getVariance(cube[next]) : 0.0f; + vv[i] = (cube[i].mVol > 1) ? getVariance(cube[i]) : 0.0f; + } else { + vv[next] = 0.0f; + i--; + } + next = 0; + temp = vv[0]; + for (k = 1; k <= i; ++k) { + if (vv[k] > temp) { + temp = vv[k]; + next = k; + } + } + if (temp <= 0.0f) { + break; + } + } + + for (k = 0; k < mMaxColors; ++k) { + weight = getVolume(cube[k], mWt); + if (weight > 0) { + red = (int) (getVolume(cube[k], mMr) / weight); + green = (int) (getVolume(cube[k], mMg) / weight); + blue = (int) (getVolume(cube[k], mMb) / weight); + colors[k] = ((red & 0x0ff) << 16) | ((green & 0x0ff) << 8) | (blue & 0x0ff); + } else { + colors[k] = 0; + } + } + + int bitsPerPixel = 0; + while ((1 << bitsPerPixel) < mMaxColors) { + bitsPerPixel++; + } + + List swatches = new ArrayList<>(); + for (int l = 0; l < k; l++) { + int pixel = colors[l]; + if (pixel == 0) { + continue; + } + swatches.add(new Palette.Swatch(pixel, 0)); + } + mSwatches.clear(); + mSwatches.addAll(swatches); + } + + /* Histogram is in elements 1..HISTSIZE along each axis, + * element 0 is for base or marginal value + * NB: these must start out 0! + */ + private void compute3DHistogram( + long[][][] vwt, long[][][] vmr, long[][][] vmg, long[][][] vmb, double[][][] m2) { + // build 3-D color histogram of counts, r/g/b, and c^2 + int r, g, b; + int i; + int inr; + int ing; + int inb; + int[] table = new int[256]; + + for (i = 0; i < 256; i++) { + table[i] = i * i; + } + + mQadd = new int[mSize]; + + for (i = 0; i < mSize; ++i) { + int rgb = mPixels[i]; + // Skip less than opaque pixels. They're not meaningful in the context of palette + // generation for UI schemes. + if ((rgb >>> 24) < 0xff) { + continue; + } + r = ((rgb >> 16) & 0xff); + g = ((rgb >> 8) & 0xff); + b = (rgb & 0xff); + inr = (r >> 3) + 1; + ing = (g >> 3) + 1; + inb = (b >> 3) + 1; + mQadd[i] = (inr << 10) + (inr << 6) + inr + (ing << 5) + ing + inb; + /*[inr][ing][inb]*/ + ++vwt[inr][ing][inb]; + vmr[inr][ing][inb] += r; + vmg[inr][ing][inb] += g; + vmb[inr][ing][inb] += b; + m2[inr][ing][inb] += table[r] + table[g] + table[b]; + } + } + + /* At conclusion of the histogram step, we can interpret + * wt[r][g][b] = sum over voxel of P(c) + * mr[r][g][b] = sum over voxel of r*P(c) , similarly for mg, mb + * m2[r][g][b] = sum over voxel of c^2*P(c) + * Actually each of these should be divided by 'size' to give the usual + * interpretation of P() as ranging from 0 to 1, but we needn't do that here. + * + * We now convert histogram into moments so that we can rapidly calculate + * the sums of the above quantities over any desired box. + */ + private void computeMoments( + long[][][] vwt, long[][][] vmr, long[][][] vmg, long[][][] vmb, double[][][] m2) { + /* compute cumulative moments. */ + int i, r, g, b; + int line, line_r, line_g, line_b; + int[] area = new int[QUANT_SIZE]; + int[] area_r = new int[QUANT_SIZE]; + int[] area_g = new int[QUANT_SIZE]; + int[] area_b = new int[QUANT_SIZE]; + double line2; + double[] area2 = new double[QUANT_SIZE]; + + for (r = 1; r < QUANT_SIZE; ++r) { + for (i = 0; i < QUANT_SIZE; ++i) { + area2[i] = area[i] = area_r[i] = area_g[i] = area_b[i] = 0; + } + for (g = 1; g < QUANT_SIZE; ++g) { + line2 = line = line_r = line_g = line_b = 0; + for (b = 1; b < QUANT_SIZE; ++b) { + line += vwt[r][g][b]; + line_r += vmr[r][g][b]; + line_g += vmg[r][g][b]; + line_b += vmb[r][g][b]; + line2 += m2[r][g][b]; + + area[b] += line; + area_r[b] += line_r; + area_g[b] += line_g; + area_b[b] += line_b; + area2[b] += line2; + + vwt[r][g][b] = vwt[r - 1][g][b] + area[b]; + vmr[r][g][b] = vmr[r - 1][g][b] + area_r[b]; + vmg[r][g][b] = vmg[r - 1][g][b] + area_g[b]; + vmb[r][g][b] = vmb[r - 1][g][b] + area_b[b]; + m2[r][g][b] = m2[r - 1][g][b] + area2[b]; + } + } + } + } + + private long getVolume(Box cube, long[][][] mmt) { + /* Compute sum over a box of any given statistic */ + return (mmt[cube.mR1][cube.mG1][cube.mB1] + - mmt[cube.mR1][cube.mG1][cube.mB0] + - mmt[cube.mR1][cube.mG0][cube.mB1] + + mmt[cube.mR1][cube.mG0][cube.mB0] + - mmt[cube.mR0][cube.mG1][cube.mB1] + + mmt[cube.mR0][cube.mG1][cube.mB0] + + mmt[cube.mR0][cube.mG0][cube.mB1] + - mmt[cube.mR0][cube.mG0][cube.mB0]); + } + + /* The next two routines allow a slightly more efficient calculation + * of Vol() for a proposed subbox of a given box. The sum of Top() + * and Bottom() is the Vol() of a subbox split in the given direction + * and with the specified new upper bound. + */ + private long getBottom(Box cube, int dir, long[][][] mmt) { + /* Compute part of Vol(cube, mmt) that doesn't depend on r1, g1, or b1 */ + /* (depending on dir) */ + switch (dir) { + case RED: + return (-mmt[cube.mR0][cube.mG1][cube.mB1] + + mmt[cube.mR0][cube.mG1][cube.mB0] + + mmt[cube.mR0][cube.mG0][cube.mB1] + - mmt[cube.mR0][cube.mG0][cube.mB0]); + case GREEN: + return (-mmt[cube.mR1][cube.mG0][cube.mB1] + + mmt[cube.mR1][cube.mG0][cube.mB0] + + mmt[cube.mR0][cube.mG0][cube.mB1] + - mmt[cube.mR0][cube.mG0][cube.mB0]); + case BLUE: + return (-mmt[cube.mR1][cube.mG1][cube.mB0] + + mmt[cube.mR1][cube.mG0][cube.mB0] + + mmt[cube.mR0][cube.mG1][cube.mB0] + - mmt[cube.mR0][cube.mG0][cube.mB0]); + default: + return 0; + } + } + + private long getTop(Box cube, int dir, int pos, long[][][] mmt) { + /* Compute remainder of Vol(cube, mmt), substituting pos for */ + /* r1, g1, or b1 (depending on dir) */ + switch (dir) { + case RED: + return (mmt[pos][cube.mG1][cube.mB1] + - mmt[pos][cube.mG1][cube.mB0] + - mmt[pos][cube.mG0][cube.mB1] + + mmt[pos][cube.mG0][cube.mB0]); + case GREEN: + return (mmt[cube.mR1][pos][cube.mB1] + - mmt[cube.mR1][pos][cube.mB0] + - mmt[cube.mR0][pos][cube.mB1] + + mmt[cube.mR0][pos][cube.mB0]); + case BLUE: + return (mmt[cube.mR1][cube.mG1][pos] + - mmt[cube.mR1][cube.mG0][pos] + - mmt[cube.mR0][cube.mG1][pos] + + mmt[cube.mR0][cube.mG0][pos]); + default: + return 0; + } + } + + private double getVariance(Box cube) { + /* Compute the weighted variance of a box */ + /* NB: as with the raw statistics, this is really the variance * size */ + double dr, dg, db, xx; + dr = getVolume(cube, mMr); + dg = getVolume(cube, mMg); + db = getVolume(cube, mMb); + xx = + mM2[cube.mR1][cube.mG1][cube.mB1] + - mM2[cube.mR1][cube.mG1][cube.mB0] + - mM2[cube.mR1][cube.mG0][cube.mB1] + + mM2[cube.mR1][cube.mG0][cube.mB0] + - mM2[cube.mR0][cube.mG1][cube.mB1] + + mM2[cube.mR0][cube.mG1][cube.mB0] + + mM2[cube.mR0][cube.mG0][cube.mB1] + - mM2[cube.mR0][cube.mG0][cube.mB0]; + return xx - (dr * dr + dg * dg + db * db) / getVolume(cube, mWt); + } + + /* We want to minimize the sum of the variances of two subboxes. + * The sum(c^2) terms can be ignored since their sum over both subboxes + * is the same (the sum for the whole box) no matter where we split. + * The remaining terms have a minus sign in the variance formula, + * so we drop the minus sign and MAXIMIZE the sum of the two terms. + */ + private double maximize( + Box cube, + int dir, + int first, + int last, + int[] cut, + long wholeR, + long wholeG, + long wholeB, + long wholeW) { + long half_r, half_g, half_b, half_w; + long base_r, base_g, base_b, base_w; + int i; + double temp, max; + + base_r = getBottom(cube, dir, mMr); + base_g = getBottom(cube, dir, mMg); + base_b = getBottom(cube, dir, mMb); + base_w = getBottom(cube, dir, mWt); + + max = 0.0f; + cut[0] = -1; + + for (i = first; i < last; ++i) { + half_r = base_r + getTop(cube, dir, i, mMr); + half_g = base_g + getTop(cube, dir, i, mMg); + half_b = base_b + getTop(cube, dir, i, mMb); + half_w = base_w + getTop(cube, dir, i, mWt); + /* now half_x is sum over lower half of box, if split at i */ + if (half_w == 0) /* subbox could be empty of pixels! */ { + continue; /* never split into an empty box */ + } + temp = (half_r * half_r + half_g * half_g + half_b * half_b) / (double) half_w; + half_r = wholeR - half_r; + half_g = wholeG - half_g; + half_b = wholeB - half_b; + half_w = wholeW - half_w; + if (half_w == 0) /* subbox could be empty of pixels! */ { + continue; /* never split into an empty box */ + } + temp += (half_r * half_r + half_g * half_g + half_b * half_b) / (double) half_w; + + if (temp > max) { + max = temp; + cut[0] = i; + } + } + + return max; + } + + private boolean cut(Box set1, Box set2) { + int dir; + int[] cutr = new int[1]; + int[] cutg = new int[1]; + int[] cutb = new int[1]; + double maxr, maxg, maxb; + long whole_r, whole_g, whole_b, whole_w; + + whole_r = getVolume(set1, mMr); + whole_g = getVolume(set1, mMg); + whole_b = getVolume(set1, mMb); + whole_w = getVolume(set1, mWt); + + maxr = maximize(set1, RED, set1.mR0 + 1, set1.mR1, cutr, whole_r, whole_g, whole_b, + whole_w); + maxg = maximize(set1, GREEN, set1.mG0 + 1, set1.mG1, cutg, whole_r, whole_g, whole_b, + whole_w); + maxb = maximize(set1, BLUE, set1.mB0 + 1, set1.mB1, cutb, whole_r, whole_g, whole_b, + whole_w); + + if (maxr >= maxg && maxr >= maxb) { + dir = RED; + if (cutr[0] < 0) return false; /* can't split the box */ + } else if (maxg >= maxr && maxg >= maxb) { + dir = GREEN; + } else { + dir = BLUE; + } + + set2.mR1 = set1.mR1; + set2.mG1 = set1.mG1; + set2.mB1 = set1.mB1; + + switch (dir) { + case RED: + set2.mR0 = set1.mR1 = cutr[0]; + set2.mG0 = set1.mG0; + set2.mB0 = set1.mB0; + break; + case GREEN: + set2.mG0 = set1.mG1 = cutg[0]; + set2.mR0 = set1.mR0; + set2.mB0 = set1.mB0; + break; + case BLUE: + set2.mB0 = set1.mB1 = cutb[0]; + set2.mR0 = set1.mR0; + set2.mG0 = set1.mG0; + break; + } + set1.mVol = (set1.mR1 - set1.mR0) * (set1.mG1 - set1.mG0) * (set1.mB1 - set1.mB0); + set2.mVol = (set2.mR1 - set2.mR0) * (set2.mG1 - set2.mG0) * (set2.mB1 - set2.mB0); + + return true; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java index 45d5515289408..76497706ce4f1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java @@ -43,6 +43,7 @@ import java.util.List; /** * Util class to create the view for a splash screen content. + * * @hide */ public class SplashscreenContentDrawer { @@ -349,7 +350,7 @@ public class SplashscreenContentDrawer { // Calculate the difference between two colors based on the HSV dimensions. final float normalizeH = minAngle / 180f; - final double square = Math.pow(normalizeH, 2) + final double square = Math.pow(normalizeH, 2) + Math.pow(aHsv[1] - bHsv[1], 2) + Math.pow(aHsv[2] - bHsv[2], 2); final double mean = square / 3; @@ -433,8 +434,11 @@ public class SplashscreenContentDrawer { */ private interface ColorTester { float nonTransparentRatio(); + boolean isComplexColor(); + int getDominantColor(); + boolean isGrayscale(); } @@ -511,14 +515,17 @@ public class SplashscreenContentDrawer { // restore to original bounds drawable.setBounds(initialBounds); - final Palette.Builder builder = new Palette.Builder(bitmap) - .maximumColorCount(5).clearFilters(); + final Palette.Builder builder; // The Palette API will ignore Alpha, so it cannot handle transparent pixels, but // sometimes we will need this information to know if this Drawable object is // transparent. mFilterTransparent = filterTransparent; if (mFilterTransparent) { - builder.setQuantizer(TRANSPARENT_FILTER_QUANTIZER); + builder = new Palette.Builder(bitmap, TRANSPARENT_FILTER_QUANTIZER) + .maximumColorCount(5); + } else { + builder = new Palette.Builder(bitmap, null) + .maximumColorCount(5); } mPalette = builder.generate(); bitmap.recycle(); @@ -538,7 +545,7 @@ public class SplashscreenContentDrawer { public int getDominantColor() { final Palette.Swatch mainSwatch = mPalette.getDominantSwatch(); if (mainSwatch != null) { - return mainSwatch.getRgb(); + return mainSwatch.getInt(); } return Color.BLACK; } @@ -549,7 +556,7 @@ public class SplashscreenContentDrawer { if (swatches != null) { for (int i = swatches.size() - 1; i >= 0; i--) { Palette.Swatch swatch = swatches.get(i); - if (!isGrayscaleColor(swatch.getRgb())) { + if (!isGrayscaleColor(swatch.getInt())) { return false; } } @@ -561,9 +568,9 @@ public class SplashscreenContentDrawer { private static final int NON_TRANSPARENT = 0xFF000000; private final Quantizer mInnerQuantizer = new VariationalKMeansQuantizer(); private float mNonTransparentRatio; + @Override - public void quantize(final int[] pixels, final int maxColors, - final Palette.Filter[] filters) { + public void quantize(final int[] pixels, final int maxColors) { mNonTransparentRatio = 0; int realSize = 0; for (int i = pixels.length - 1; i > 0; i--) { @@ -575,7 +582,7 @@ public class SplashscreenContentDrawer { if (DEBUG) { Slog.d(TAG, "quantize: this is pure transparent image"); } - mInnerQuantizer.quantize(pixels, maxColors, filters); + mInnerQuantizer.quantize(pixels, maxColors); return; } mNonTransparentRatio = (float) realSize / pixels.length; @@ -587,7 +594,7 @@ public class SplashscreenContentDrawer { rowIndex++; } } - mInnerQuantizer.quantize(samplePixels, maxColors, filters); + mInnerQuantizer.quantize(samplePixels, maxColors); } @Override