Revert^2 "[framework] Integrate new quantizers"
96169052c42ed682ea110730e8902eecd194ec96
Change-Id: Idefcf279b9fedeeaa29f75e705adfb60f0ad5c8a
diff --git a/core/java/android/app/WallpaperColors.java b/core/java/android/app/WallpaperColors.java
index 2d203f57..3abba43 100644
--- a/core/java/android/app/WallpaperColors.java
+++ b/core/java/android/app/WallpaperColors.java
@@ -30,14 +30,17 @@
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 @@
private static final float DARK_PIXEL_CONTRAST = 6f;
private static final float MAX_DARK_AREA = 0.025f;
- private final ArrayList<Color> mMainColors;
+ private final List<Color> mMainColors;
+ private final Map<Integer, Integer> 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 @@
}
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<Palette.Swatch> 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<Integer, Integer> 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 @@
bitmap.recycle();
}
- return new WallpaperColors(primary, secondary, tertiary, HINT_FROM_BITMAP | hints);
+ return new WallpaperColors(populationByColor, HINT_FROM_BITMAP | hints);
}
/**
@@ -253,9 +244,13 @@
}
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 @@
+ "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<Integer, Integer> populationByColor, int colorHints) {
+ mAllColors = populationByColor;
+
+ ArrayList<Map.Entry<Integer, Integer>> 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 @@
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 @@
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<Integer, Integer> 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 0000000..de6bf20
--- /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<Palette.Swatch> mSwatches;
+
+ public CelebiQuantizer() { }
+
+ @Override
+ public void quantize(int[] pixels, int maxColors) {
+ WuQuantizer wu = new WuQuantizer(pixels, maxColors);
+ wu.quantize(pixels, maxColors);
+ List<Palette.Swatch> 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<Palette.Swatch> 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 0000000..5fcfcba
--- /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 9ac753b..7779494 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 @@
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.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 @@
int[] mHistogram;
List<Swatch> 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 @@
// 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 @@
* 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<Vbox> queue, final int maxSize) {
@@ -216,11 +208,7 @@
ArrayList<Swatch> 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 @@
*/
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 @@
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 @@
}
}
- 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 @@
}
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 0000000..3dd1b8d
--- /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 0000000..98d5d26
--- /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 0000000..894f91b
--- /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 0000000..ae8858a
--- /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<Integer> 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 a4f9a59..8b1137d 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 @@
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.
- * <p>
- * A number of colors with different profiles are extracted from the image:
- * <ul>
- * <li>Vibrant</li>
- * <li>Vibrant Dark</li>
- * <li>Vibrant Light</li>
- * <li>Muted</li>
- * <li>Muted Dark</li>
- * <li>Muted Light</li>
- * </ul>
- * These can be retrieved from the appropriate getter method.
*
- * <p>
- * Instances are created with a {@link Palette.Builder} which supports several options to tweak the
+ * <p>Instances are created with a {@link Builder} which supports several options to tweak the
* generated Palette. See that class' documentation for more information.
- * <p>
- * 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:
+ *
+ * <p>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:
*
* <pre>
* // Synchronous
@@ -85,346 +61,59 @@
/**
* 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<Palette.Swatch> swatches) {
- return new Palette.Builder(swatches).generate();
+ @NonNull
+ public static Palette from(@NonNull List<Swatch> 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<Swatch> 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<Bitmap, Void, Palette> 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<Bitmap, Void, Palette> generateAsync(
- final Bitmap bitmap, final int numColors, final Palette.PaletteAsyncListener listener) {
- return from(bitmap).maximumColorCount(numColors).generate(listener);
- }
-
- private final List<Palette.Swatch> mSwatches;
- private final List<Target> mTargets;
-
- private final Map<Target, Palette.Swatch> mSelectedSwatches;
- private final SparseBooleanArray mUsedColors;
-
- private final Palette.Swatch mDominantSwatch;
-
- Palette(List<Palette.Swatch> swatches, List<Target> targets) {
+ Palette(List<Swatch> 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<Palette.Swatch> getSwatches() {
+ public List<Swatch> getSwatches() {
return Collections.unmodifiableList(mSwatches);
}
- /**
- * Returns the targets used to generate this palette.
- */
- @NonNull
- public List<Target> 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.
- *
- * <p>The dominant swatch is defined as the swatch with the greatest population (frequency)
- * within the palette.</p>
- */
- @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 @@
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 @@
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<Palette.Swatch> mSwatches;
+ /** Builder class for generating {@link Palette} instances. */
+ public static class Builder {
+ @Nullable
+ private final List<Swatch> mSwatches;
+ @Nullable
private final Bitmap mBitmap;
+ @Nullable
+ private Quantizer mQuantizer = new ColorCutQuantizer();
- private final List<Target> mTargets = new ArrayList<>();
private int mMaxColors = DEFAULT_CALCULATE_NUMBER_COLORS;
private int mResizeArea = DEFAULT_RESIZE_BITMAP_AREA;
private int mResizeMaxDimension = -1;
- private final List<Palette.Filter> 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<Palette.Swatch> swatches) {
+ public Builder(@NonNull List<Swatch> 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.
- * <p>
- * 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.
+ *
+ * <p>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.
- * <p>
- * 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.
+ *
+ * <p>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.
- * <p>This only works when the original input is a {@link Bitmap}.</p>
*
- * @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.
+ * <p>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.
- *
- * <p>You can retrieve the result via {@link Palette#getSwatchForTarget(Target)}.</p>
- */
- @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<Palette.Swatch> swatches;
+ List<Swatch> 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 @@
}
// 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 <code>java.util.concurrent</code> or <a
+ * href="https://developer.android.com/topic/libraries/architecture/coroutines">Kotlin
+ * concurrency utilities</a> to call {@link #generate()} instead.
*/
@NonNull
- public AsyncTask<Bitmap, Void, Palette> generate(final Palette.PaletteAsyncListener listener) {
- if (listener == null) {
- throw new IllegalArgumentException("listener can not be null");
- }
+ @Deprecated
+ public android.os.AsyncTask<Bitmap, Void, Palette> generate(
+ @NonNull PaletteAsyncListener listener) {
+ assert (listener != null);
- return new AsyncTask<Bitmap, Void, Palette>() {
+ return new android.os.AsyncTask<Bitmap, Void, Palette>() {
@Override
+ @Nullable
protected Palette doInBackground(Bitmap... params) {
try {
return generate();
@@ -890,16 +391,16 @@
}
@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 @@
} 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 @@
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 @@
*
* @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 @@
}
};
}
+
diff --git a/core/java/com/android/internal/graphics/palette/Quantizer.java b/core/java/com/android/internal/graphics/palette/Quantizer.java
index db60f2e..a219ea3 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 @@
* 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<Palette.Swatch> getQuantizedColors();
}
diff --git a/core/java/com/android/internal/graphics/palette/Target.java b/core/java/com/android/internal/graphics/palette/Target.java
index 0540d80..96e7faa 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.
- *
- * <p>To use the target, use the {@link Palette.Builder#addTarget(Target)} API when building a
- * Palette.</p>
+ * 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.
*
* <p>The larger the weight, relative to the other weights, the more important that a color
- * being close to the target value has on selection.</p>
+ * 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.
*
* <p>The larger the weight, relative to the other weights, the more important that a color
- * being close to the target value has on selection.</p>
+ * 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.
*
- * <p>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.</p>
+ * <p>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.
*
- * <p>If false, then the color can be selected for other targets.</p>
+ * <p>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.
*
* <p>The larger the weight, relative to the other weights, the more important that a color
- * being close to the target value has on selection.</p>
+ * being close to the target value has on selection.
*
- * <p>A weight of 0 means that it has no weight, and thus has no
- * bearing on the selection.</p>
+ * <p>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 @@
* Set the weight of importance that this target will place on lightness values.
*
* <p>The larger the weight, relative to the other weights, the more important that a color
- * being close to the target value has on selection.</p>
+ * being close to the target value has on selection.
*
- * <p>A weight of 0 means that it has no weight, and thus has no
- * bearing on the selection.</p>
+ * <p>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.
*
* <p>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.</p>
+ * color's
+ * population being close to the most populous has on selection.
*
- * <p>A weight of 0 means that it has no weight, and thus has no
- * bearing on the selection.</p>
+ * <p>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 b035535..d791f7b 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 @@
*
* @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 @@
// 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 0000000..a87a34f
--- /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<Integer, Integer> mCountByColor = new HashMap<>();
+ private final Map<Integer, Integer> mMeanIndexByColor = new HashMap<>();
+ private final Set<Integer> mUniqueColors = new HashSet<>();
+ private final List<Palette.Swatch> 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<Palette.Swatch> 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<Palette.Swatch> 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<MeanBucket> 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<Palette.Swatch> 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<MeanBucket> 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<MeanBucket> 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 0000000..01e45f6
--- /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
+ *
+ * <p>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<Palette.Swatch> mSwatches = new ArrayList<>();
+
+ @Override
+ public List<Palette.Swatch> 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<Palette.Swatch> 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 45d5515..7649770 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 @@
/**
* Util class to create the view for a splash screen content.
+ *
* @hide
*/
public class SplashscreenContentDrawer {
@@ -349,7 +350,7 @@
// 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 @@
*/
private interface ColorTester {
float nonTransparentRatio();
+
boolean isComplexColor();
+
int getDominantColor();
+
boolean isGrayscale();
}
@@ -511,14 +515,17 @@
// 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 int getDominantColor() {
final Palette.Swatch mainSwatch = mPalette.getDominantSwatch();
if (mainSwatch != null) {
- return mainSwatch.getRgb();
+ return mainSwatch.getInt();
}
return Color.BLACK;
}
@@ -549,7 +556,7 @@
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 @@
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 @@
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 @@
rowIndex++;
}
}
- mInnerQuantizer.quantize(samplePixels, maxColors, filters);
+ mInnerQuantizer.quantize(samplePixels, maxColors);
}
@Override