summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author James O'Leary <jamesoleary@google.com> 2021-06-09 22:23:24 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2021-06-09 22:23:24 +0000
commite0970984c86fb362cbbd792ec6da9ea8c8551355 (patch)
treea2ecbb59dde845bf2c8648c942d7b19dcc7e296b
parent2eda3de20458ab028b0e78d68c582ee4a6a89d31 (diff)
parentbf1b65be0e0b989290b5de17c7123c130138abe9 (diff)
Merge changes from topics "quantizer_improved_logic", "reintroduce_scaling_fixes" into sc-dev
* changes: Quantize image to 128 colors instead of 5 Quantizer improvements Sort colors from high count => low count Don't interpolate image input to quantizer Cache set wallpaper as PNG instead of JPEG Only rescale wallpaper if its > display height
-rw-r--r--core/java/android/app/WallpaperColors.java114
-rw-r--r--core/java/com/android/internal/graphics/palette/CelebiQuantizer.java28
-rw-r--r--core/java/com/android/internal/graphics/palette/LABPointProvider.java (renamed from core/java/com/android/internal/graphics/palette/LABCentroid.java)8
-rw-r--r--core/java/com/android/internal/graphics/palette/Mean.java44
-rw-r--r--core/java/com/android/internal/graphics/palette/MeanBucket.java42
-rw-r--r--core/java/com/android/internal/graphics/palette/PointProvider.java (renamed from core/java/com/android/internal/graphics/palette/CentroidProvider.java)23
-rw-r--r--core/java/com/android/internal/graphics/palette/QuantizerMap.java62
-rw-r--r--core/java/com/android/internal/graphics/palette/WSMeansQuantizer.java398
-rw-r--r--core/java/com/android/internal/graphics/palette/WuQuantizer.java716
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperManagerService.java4
10 files changed, 753 insertions, 686 deletions
diff --git a/core/java/android/app/WallpaperColors.java b/core/java/android/app/WallpaperColors.java
index edd60473c522..cd82deb13e9b 100644
--- a/core/java/android/app/WallpaperColors.java
+++ b/core/java/android/app/WallpaperColors.java
@@ -30,6 +30,7 @@ import android.util.Log;
import android.util.Size;
import com.android.internal.graphics.ColorUtils;
+import com.android.internal.graphics.cam.Cam;
import com.android.internal.graphics.palette.CelebiQuantizer;
import com.android.internal.graphics.palette.Palette;
import com.android.internal.graphics.palette.VariationalKMeansQuantizer;
@@ -43,7 +44,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.stream.Collectors;
+import java.util.Set;
/**
* Provides information about the colors of a wallpaper.
@@ -176,7 +177,7 @@ public final class WallpaperColors implements Parcelable {
shouldRecycle = true;
Size optimalSize = calculateOptimalSize(bitmap.getWidth(), bitmap.getHeight());
bitmap = Bitmap.createScaledBitmap(bitmap, optimalSize.getWidth(),
- optimalSize.getHeight(), true /* filter */);
+ optimalSize.getHeight(), false /* filter */);
}
final Palette palette;
@@ -189,7 +190,7 @@ public final class WallpaperColors implements Parcelable {
} else {
palette = Palette
.from(bitmap, new CelebiQuantizer())
- .maximumColorCount(5)
+ .maximumColorCount(128)
.resizeBitmapArea(MAX_WALLPAPER_EXTRACTION_AREA)
.generate();
}
@@ -278,7 +279,7 @@ public final class WallpaperColors implements Parcelable {
/**
* 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
+ * @param colorToPopulation Map with keys of colors, and value representing the number of
* occurrences of color in the wallpaper.
* @param colorHints A combination of color hints.
* @hide
@@ -286,20 +287,105 @@ public final class WallpaperColors implements Parcelable {
* @see WallpaperColors#fromBitmap(Bitmap)
* @see WallpaperColors#fromDrawable(Drawable)
*/
- public WallpaperColors(@NonNull Map<Integer, Integer> populationByColor,
+ public WallpaperColors(@NonNull Map<Integer, Integer> colorToPopulation,
@ColorsHints 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());
+ mAllColors = colorToPopulation;
+
+ final Map<Integer, Cam> colorToCam = new HashMap<>();
+ for (int color : colorToPopulation.keySet()) {
+ colorToCam.put(color, Cam.fromInt(color));
+ }
+ final double[] hueProportions = hueProportions(colorToCam, colorToPopulation);
+ final Map<Integer, Double> colorToHueProportion = colorToHueProportion(
+ colorToPopulation.keySet(), colorToCam, hueProportions);
+
+ final Map<Integer, Double> colorToScore = new HashMap<>();
+ for (Map.Entry<Integer, Double> mapEntry : colorToHueProportion.entrySet()) {
+ int color = mapEntry.getKey();
+ double proportion = mapEntry.getValue();
+ double score = score(colorToCam.get(color), proportion);
+ colorToScore.put(color, score);
+ }
+ ArrayList<Map.Entry<Integer, Double>> mapEntries = new ArrayList(colorToScore.entrySet());
+ mapEntries.sort((a, b) -> b.getValue().compareTo(a.getValue()));
+
+ List<Integer> colorsByScoreDescending = new ArrayList<>();
+ for (Map.Entry<Integer, Double> colorToScoreEntry : mapEntries) {
+ colorsByScoreDescending.add(colorToScoreEntry.getKey());
+ }
+
+ List<Integer> mainColorInts = new ArrayList<>();
+ findSeedColorLoop:
+ for (int color : colorsByScoreDescending) {
+ Cam cam = colorToCam.get(color);
+ for (int otherColor : mainColorInts) {
+ Cam otherCam = colorToCam.get(otherColor);
+ if (hueDiff(cam, otherCam) < 15) {
+ continue findSeedColorLoop;
+ }
+ }
+ mainColorInts.add(color);
+ }
+ List<Color> mainColors = new ArrayList<>();
+ for (int colorInt : mainColorInts) {
+ mainColors.add(Color.valueOf(colorInt));
+ }
+ mMainColors = mainColors;
mColorHints = colorHints;
}
+ private static double hueDiff(Cam a, Cam b) {
+ return (180f - Math.abs(Math.abs(a.getHue() - b.getHue()) - 180f));
+ }
+
+ private static double score(Cam cam, double proportion) {
+ return cam.getChroma() + (proportion * 100);
+ }
+
+ private static Map<Integer, Double> colorToHueProportion(Set<Integer> colors,
+ Map<Integer, Cam> colorToCam, double[] hueProportions) {
+ Map<Integer, Double> colorToHueProportion = new HashMap<>();
+ for (int color : colors) {
+ final int hue = wrapDegrees(Math.round(colorToCam.get(color).getHue()));
+ double proportion = 0.0;
+ for (int i = hue - 15; i < hue + 15; i++) {
+ proportion += hueProportions[wrapDegrees(i)];
+ }
+ colorToHueProportion.put(color, proportion);
+ }
+ return colorToHueProportion;
+ }
+
+ private static int wrapDegrees(int degrees) {
+ if (degrees < 0) {
+ return (degrees % 360) + 360;
+ } else if (degrees >= 360) {
+ return degrees % 360;
+ } else {
+ return degrees;
+ }
+ }
+
+ private static double[] hueProportions(@NonNull Map<Integer, Cam> colorToCam,
+ Map<Integer, Integer> colorToPopulation) {
+ final double[] proportions = new double[360];
+
+ double totalPopulation = 0;
+ for (Map.Entry<Integer, Integer> entry : colorToPopulation.entrySet()) {
+ totalPopulation += entry.getValue();
+ }
+
+ for (Map.Entry<Integer, Integer> entry : colorToPopulation.entrySet()) {
+ final int color = (int) entry.getKey();
+ final int population = colorToPopulation.get(color);
+ final Cam cam = colorToCam.get(color);
+ final int hue = wrapDegrees(Math.round(cam.getHue()));
+ proportions[hue] = proportions[hue] + ((double) population / totalPopulation);
+ }
+
+ return proportions;
+ }
+
public static final @android.annotation.NonNull Creator<WallpaperColors> CREATOR = new Creator<WallpaperColors>() {
@Override
public WallpaperColors createFromParcel(Parcel in) {
diff --git a/core/java/com/android/internal/graphics/palette/CelebiQuantizer.java b/core/java/com/android/internal/graphics/palette/CelebiQuantizer.java
index de6bf2044dbc..454fd00a841c 100644
--- a/core/java/com/android/internal/graphics/palette/CelebiQuantizer.java
+++ b/core/java/com/android/internal/graphics/palette/CelebiQuantizer.java
@@ -19,26 +19,32 @@ 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.
- *
+ * An implementation of Celebi's quantization method.
* See Celebi 2011, “Improving the Performance of K-Means for Color Quantization”
+ *
+ * First, Wu's quantizer runs. The results are used as starting points for a subsequent Kmeans
+ * run. Using Wu's quantizer ensures 100% reproducible quantization results, because the starting
+ * centroids are always the same. It also ensures high quality results, Wu is a box-cutting
+ * quantization algorithm, much like medican color cut. It minimizes variance, much like Kmeans.
+ * Wu is shown to be the highest quality box-cutting quantization algorithm.
+ *
+ * Second, a Kmeans quantizer tweaked for performance is run. Celebi calls this a weighted
+ * square means quantizer, or WSMeans. Optimizations include operating on a map of image pixels
+ * rather than all image pixels, and avoiding excess color distance calculations by using a
+ * matrix and geometrical properties to know when there won't be any cluster closer to a pixel.
*/
public class CelebiQuantizer implements Quantizer {
private List<Palette.Swatch> mSwatches;
- public CelebiQuantizer() { }
+ public CelebiQuantizer() {
+ }
@Override
public void quantize(int[] pixels, int maxColors) {
- WuQuantizer wu = new WuQuantizer(pixels, maxColors);
+ WuQuantizer wu = new WuQuantizer();
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);
+ WSMeansQuantizer kmeans = new WSMeansQuantizer(wu.getColors(), new LABPointProvider(),
+ wu.inputPixelToCount());
kmeans.quantize(pixels, maxColors);
mSwatches = kmeans.getQuantizedColors();
}
diff --git a/core/java/com/android/internal/graphics/palette/LABCentroid.java b/core/java/com/android/internal/graphics/palette/LABPointProvider.java
index 408cf1fe9193..21a2212d6c77 100644
--- a/core/java/com/android/internal/graphics/palette/LABCentroid.java
+++ b/core/java/com/android/internal/graphics/palette/LABPointProvider.java
@@ -26,11 +26,11 @@ import android.graphics.ColorSpace;
* in L*a*b* space, also known as deltaE, is a universally accepted standard across industries
* and worldwide.
*/
-public class LABCentroid implements CentroidProvider {
+public class LABPointProvider implements PointProvider {
final ColorSpace.Connector mRgbToLab;
final ColorSpace.Connector mLabToRgb;
- public LABCentroid() {
+ public LABPointProvider() {
mRgbToLab = ColorSpace.connect(
ColorSpace.get(ColorSpace.Named.SRGB),
ColorSpace.get(ColorSpace.Named.CIE_LAB));
@@ -39,7 +39,7 @@ public class LABCentroid implements CentroidProvider {
}
@Override
- public float[] getCentroid(int color) {
+ public float[] fromInt(int color) {
float r = Color.red(color) / 255.f;
float g = Color.green(color) / 255.f;
float b = Color.blue(color) / 255.f;
@@ -49,7 +49,7 @@ public class LABCentroid implements CentroidProvider {
}
@Override
- public int getColor(float[] centroid) {
+ public int toInt(float[] centroid) {
float[] rgb = mLabToRgb.transform(centroid);
int color = Color.rgb(rgb[0], rgb[1], rgb[2]);
return color;
diff --git a/core/java/com/android/internal/graphics/palette/Mean.java b/core/java/com/android/internal/graphics/palette/Mean.java
deleted file mode 100644
index bde036349d3b..000000000000
--- a/core/java/com/android/internal/graphics/palette/Mean.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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 {
- public float[] center;
-
- /**
- * Constructor.
- *
- * @param upperBound maximum value of a dimension in the space Kmeans is optimizing in
- * @param random used to generate a random center
- */
- Mean(int upperBound, Random random) {
- 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
deleted file mode 100644
index ae8858a8107c..000000000000
--- a/core/java/com/android/internal/graphics/palette/MeanBucket.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * 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/CentroidProvider.java b/core/java/com/android/internal/graphics/palette/PointProvider.java
index 5fcfcbab3159..017adeb3ef27 100644
--- a/core/java/com/android/internal/graphics/palette/CentroidProvider.java
+++ b/core/java/com/android/internal/graphics/palette/PointProvider.java
@@ -18,21 +18,18 @@ package com.android.internal.graphics.palette;
import android.annotation.ColorInt;
-interface CentroidProvider {
- /**
- * @return 3 dimensions representing the color
- */
- float[] getCentroid(@ColorInt int color);
+/**
+ * Interface that allows quantizers to have a plug-and-play interface for experimenting with
+ * quantization in different color spaces.
+ */
+public interface PointProvider {
+ /** Convert a color to 3 coordinates representing the color in a color space. */
+ float[] fromInt(@ColorInt int argb);
- /**
- * @param centroid 3 dimensions representing the color
- * @return 32-bit ARGB representation
- */
+ /** Convert 3 coordinates in the color space into a color */
@ColorInt
- int getColor(float[] centroid);
+ int toInt(float[] point);
- /**
- * Distance between two centroids.
- */
+ /** Find the distance between two colosrin the color space */
float distance(float[] a, float[] b);
}
diff --git a/core/java/com/android/internal/graphics/palette/QuantizerMap.java b/core/java/com/android/internal/graphics/palette/QuantizerMap.java
new file mode 100644
index 000000000000..6b60f61b91f5
--- /dev/null
+++ b/core/java/com/android/internal/graphics/palette/QuantizerMap.java
@@ -0,0 +1,62 @@
+/*
+ * 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.NonNull;
+import android.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Converts a set of pixels/colors into a map with keys of unique colors, and values of the count
+ * of the unique color in the original set of pixels.
+ *
+ * This allows other quantizers to get a significant speed boost by simply running this quantizer,
+ * and then performing operations using the map, rather than for each pixel.
+ */
+public final class QuantizerMap implements Quantizer {
+ private HashMap<Integer, Integer> mColorToCount;
+ private Palette mPalette;
+
+ @Override
+ public void quantize(@NonNull int[] pixels, int colorCount) {
+ final HashMap<Integer, Integer> colorToCount = new HashMap<>();
+ for (int pixel : pixels) {
+ colorToCount.merge(pixel, 1, Integer::sum);
+ }
+ mColorToCount = colorToCount;
+
+ List<Palette.Swatch> swatches = new ArrayList<>();
+ for (Map.Entry<Integer, Integer> entry : colorToCount.entrySet()) {
+ swatches.add(new Palette.Swatch(entry.getKey(), entry.getValue()));
+ }
+ mPalette = Palette.from(swatches);
+ }
+
+ @Override
+ public List<Palette.Swatch> getQuantizedColors() {
+ return mPalette.getSwatches();
+ }
+
+ @Nullable
+ public Map<Integer, Integer> getColorToCount() {
+ return mColorToCount;
+ }
+}
diff --git a/core/java/com/android/internal/graphics/palette/WSMeansQuantizer.java b/core/java/com/android/internal/graphics/palette/WSMeansQuantizer.java
index 1d865c2513cf..19ed8d07394e 100644
--- a/core/java/com/android/internal/graphics/palette/WSMeansQuantizer.java
+++ b/core/java/com/android/internal/graphics/palette/WSMeansQuantizer.java
@@ -16,18 +16,20 @@
package com.android.internal.graphics.palette;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
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.Random;
import java.util.Set;
-
/**
- * A color quantizer based on the Kmeans algorithm.
+ * A color quantizer based on the Kmeans algorithm. Prefer using QuantizerCelebi.
*
* 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
@@ -36,253 +38,237 @@ import java.util.Set;
* 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.
+ * from Wu's quantization algorithm. See QuantizerCelebi 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]);
- }
+public final class WSMeansQuantizer implements Quantizer {
+ private static final String TAG = "QuantizerWsmeans";
+ private static final boolean DEBUG = false;
+ private static final int MAX_ITERATIONS = 10;
+ // Points won't be moved to a closer cluster, if the closer cluster is within
+ // this distance. 3.0 used because L*a*b* delta E < 3 is considered imperceptible.
+ private static final float MIN_MOVEMENT_DISTANCE = 3.0f;
- if (maxColors > means.length) {
- // Always initialize Random with the same seed. Ensures the results of quantization
- // are consistent, even when random centroids are required.
- Random random = new Random(0x42688);
- int randomMeansToCreate = maxColors - means.length;
- for (int i = 0; i < randomMeansToCreate; i++) {
- mMeans[means.length + i] = new Mean(100, random);
- }
- }
+ private final PointProvider mPointProvider;
+ private @Nullable Map<Integer, Integer> mInputPixelToCount;
+ private float[][] mClusters;
+ private int[] mClusterPopulations;
+ private float[][] mPoints;
+ private int[] mPixels;
+ private int[] mClusterIndices;
+ private int[][] mIndexMatrix = {};
+ private float[][] mDistanceMatrix = {};
- for (int pixel : pixels) {
- // These are pixels from the bitmap that is being quantized.
- // Depending on the bitmap & downscaling, it may have pixels that are less than opaque
- // Ignore those pixels.
- ///
- // Note: they don't _have_ to be ignored, for example, we could instead turn them
- // opaque. Traditionally, including outside Android, quantizers ignore transparent
- // pixels, so that strategy was chosen.
- int alpha = (pixel >> 24) & 0xff;
- if (alpha < 255) {
- continue;
- }
- 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);
- }
+ private Palette mPalette;
- if (pixels.length == 0) {
- return;
+ public WSMeansQuantizer(int[] inClusters, PointProvider pointProvider,
+ @Nullable Map<Integer, Integer> inputPixelToCount) {
+ mPointProvider = pointProvider;
+
+ mClusters = new float[inClusters.length][3];
+ int index = 0;
+ for (int cluster : inClusters) {
+ float[] point = pointProvider.fromInt(cluster);
+ mClusters[index++] = point;
}
- predict(maxColors, 0);
+ mInputPixelToCount = inputPixelToCount;
}
- /** 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;
+ @Override
+ public List<Palette.Swatch> getQuantizedColors() {
+ return mPalette.getSwatches();
}
- /** Create random starting centroids for K-means. */
- public static float[][] randomMeans(int maxColors, int upperBound) {
- float[][] means = new float[maxColors][];
+ @Override
+ public void quantize(@NonNull int[] pixels, int maxColors) {
+ assert (pixels.length > 0);
- // Always initialize Random with the same seed. Ensures the results of quantization
- // are consistent, even when random centroids are required.
- Random random = new Random(0x42688);
+ if (mInputPixelToCount == null) {
+ QuantizerMap mapQuantizer = new QuantizerMap();
+ mapQuantizer.quantize(pixels, maxColors);
+ mInputPixelToCount = mapQuantizer.getColorToCount();
+ }
+
+ mPoints = new float[mInputPixelToCount.size()][3];
+ mPixels = new int[mInputPixelToCount.size()];
+ int index = 0;
+ for (int pixel : mInputPixelToCount.keySet()) {
+ mPixels[index] = pixel;
+ mPoints[index] = mPointProvider.fromInt(pixel);
+ index++;
+ }
+ if (mClusters.length > 0) {
+ // This implies that the constructor was provided starting clusters. If that was the
+ // case, we limit the number of clusters to the number of starting clusters and don't
+ // initialize random clusters.
+ maxColors = Math.min(maxColors, mClusters.length);
+ }
+ maxColors = Math.min(maxColors, mPoints.length);
+
+ initializeClusters(maxColors);
+ for (int i = 0; i < MAX_ITERATIONS; i++) {
+ calculateClusterDistances(maxColors);
+ if (!reassignPoints(maxColors)) {
+ break;
+ }
+ recalculateClusterCenters(maxColors);
+ }
+
+ List<Palette.Swatch> swatches = new ArrayList<>();
for (int i = 0; i < maxColors; i++) {
- means[i] = new Mean(upperBound, random).center;
+ float[] cluster = mClusters[i];
+ int colorInt = mPointProvider.toInt(cluster);
+ swatches.add(new Palette.Swatch(colorInt, mClusterPopulations[i]));
}
- return means;
+ mPalette = Palette.from(swatches);
}
- @Override
- public void quantize(int[] pixels, int maxColors) {
+ private void initializeClusters(int maxColors) {
+ boolean hadInputClusters = mClusters.length > 0;
+ if (!hadInputClusters) {
+ int additionalClustersNeeded = maxColors - mClusters.length;
+ if (DEBUG) {
+ Log.d(TAG, "have " + mClusters.length + " clusters, want " + maxColors
+ + " results, so need " + additionalClustersNeeded + " additional clusters");
+ }
- }
+ Random random = new Random(0x42688);
+ List<float[]> additionalClusters = new ArrayList<>(additionalClustersNeeded);
+ Set<Integer> clusterIndicesUsed = new HashSet<>();
+ for (int i = 0; i < additionalClustersNeeded; i++) {
+ int index = random.nextInt(mPoints.length);
+ while (clusterIndicesUsed.contains(index)
+ && clusterIndicesUsed.size() < mPoints.length) {
+ index = random.nextInt(mPoints.length);
+ }
+ clusterIndicesUsed.add(index);
+ additionalClusters.add(mPoints[index]);
+ }
- @Override
- public List<Palette.Swatch> getQuantizedColors() {
- return mSwatches;
+ float[][] newClusters = (float[][]) additionalClusters.toArray();
+ float[][] clusters = Arrays.copyOf(mClusters, maxColors);
+ System.arraycopy(newClusters, 0, clusters, clusters.length, newClusters.length);
+ mClusters = clusters;
+ }
+
+ mClusterIndices = new int[mPixels.length];
+ mClusterPopulations = new int[mPixels.length];
+ Random random = new Random(0x42688);
+ for (int i = 0; i < mPixels.length; i++) {
+ int clusterIndex = random.nextInt(maxColors);
+ mClusterIndices[i] = clusterIndex;
+ mClusterPopulations[i] = mInputPixelToCount.get(mPixels[i]);
+ }
}
- private void predict(int maxColors, int iterationsCompleted) {
- double[][] centroidDistance = new double[maxColors][maxColors];
+ void calculateClusterDistances(int maxColors) {
+ if (mDistanceMatrix.length != maxColors) {
+ mDistanceMatrix = new float[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;
+ float distance = mPointProvider.distance(mClusters[i], mClusters[j]);
+ mDistanceMatrix[j][i] = distance;
+ mDistanceMatrix[i][j] = 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];
+ if (mIndexMatrix.length != maxColors) {
+ mIndexMatrix = 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];
+ ArrayList<Distance> distances = new ArrayList<>(maxColors);
+ for (int index = 0; index < maxColors; index++) {
+ distances.add(new Distance(index, mDistanceMatrix[i][index]));
+ }
+ distances.sort(
+ (a, b) -> Float.compare(a.getDistance(), b.getDistance()));
+
for (int j = 0; j < maxColors; j++) {
- outputRow[j] = findIndex(distancesFromIToAnotherMean, sortedByDistanceAscending[j]);
+ mIndexMatrix[i][j] = distances.get(j).getIndex();
}
- distanceMatrix[i] = outputRow;
}
+ }
+
+ boolean reassignPoints(int maxColors) {
+ boolean colorMoved = false;
+ for (int i = 0; i < mPoints.length; i++) {
+ float[] point = mPoints[i];
+ int previousClusterIndex = mClusterIndices[i];
+ float[] previousCluster = mClusters[previousClusterIndex];
+ float previousDistance = mPointProvider.distance(point, previousCluster);
- // 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);
+ float minimumDistance = previousDistance;
+ int newClusterIndex = -1;
for (int j = 1; j < maxColors; j++) {
- int indexOfClusterFromCurrentToJ = distanceMatrix[indexOfCurrentMean][j];
- double distanceBetweenJAndCurrent =
- centroidDistance[indexOfCurrentMean][indexOfClusterFromCurrentToJ];
- if (distanceBetweenJAndCurrent >= (4 * minDistance)) {
+ int t = mIndexMatrix[previousClusterIndex][j];
+ if (mDistanceMatrix[previousClusterIndex][t] >= 4 * previousDistance) {
+ // Triangle inequality proves there's can be no closer center.
break;
}
- double distanceBetweenJAndColor = mCentroidProvider.distance(mMeans[j].center,
- color);
- if (distanceBetweenJAndColor < minDistance) {
- minDistance = distanceBetweenJAndColor;
- mMeanIndexByColor.remove(intColor);
- mMeanIndexByColor.put(intColor, j);
- anyColorMoved = true;
+ float distance = mPointProvider.distance(point, mClusters[t]);
+ if (distance < minimumDistance) {
+ minimumDistance = distance;
+ newClusterIndex = t;
}
}
- }
-
- 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);
+ if (newClusterIndex != -1) {
+ float distanceChange = (float)
+ Math.abs((Math.sqrt(minimumDistance) - Math.sqrt(previousDistance)));
+ if (distanceChange > MIN_MOVEMENT_DISTANCE) {
+ colorMoved = true;
+ mClusterIndices[i] = newClusterIndex;
}
}
+ }
+ return colorMoved;
+ }
- for (MeanBucket bucket : buckets) {
- float[] centroid = bucket.getCentroid();
- if (centroid == null) {
- continue;
- }
+ void recalculateClusterCenters(int maxColors) {
+ mClusterPopulations = new int[maxColors];
+ float[] aSums = new float[maxColors];
+ float[] bSums = new float[maxColors];
+ float[] cSums = new float[maxColors];
+ for (int i = 0; i < mPoints.length; i++) {
+ int clusterIndex = mClusterIndices[i];
+ float[] point = mPoints[i];
+ int pixel = mPixels[i];
+ int count = mInputPixelToCount.get(pixel);
+ mClusterPopulations[clusterIndex] += count;
+ aSums[clusterIndex] += point[0] * count;
+ bSums[clusterIndex] += point[1] * count;
+ cSums[clusterIndex] += point[2] * count;
- 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--);
- }
- }
- }
- }
+ }
+ for (int i = 0; i < maxColors; i++) {
+ int count = mClusterPopulations[i];
+ float aSum = aSums[i];
+ float bSum = bSums[i];
+ float cSum = cSums[i];
+ mClusters[i][0] = aSum / count;
+ mClusters[i][1] = bSum / count;
+ mClusters[i][2] = cSum / count;
+ }
+ }
- 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);
- }
+ private static class Distance {
+ private final int mIndex;
+ private final float mDistance;
- predict(buckets.size(), iterationsCompleted + 1);
+ int getIndex() {
+ return mIndex;
}
- }
+ float getDistance() {
+ return mDistance;
+ }
- private static int findIndex(double[] list, double element) {
- for (int i = 0; i < list.length; i++) {
- if (list[i] == element) {
- return i;
- }
+ Distance(int index, float distance) {
+ mIndex = index;
+ mDistance = distance;
}
- 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
index a2652ea6d5e1..1cd0d721bce4 100644
--- a/core/java/com/android/internal/graphics/palette/WuQuantizer.java
+++ b/core/java/com/android/internal/graphics/palette/WuQuantizer.java
@@ -16,431 +16,447 @@
package com.android.internal.graphics.palette;
+import static java.lang.System.arraycopy;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Color;
+
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
-// 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
+ * Wu's quantization algorithm is a box-cut quantizer that minimizes variance. It takes longer to
+ * run than, say, median color cut, but provides the highest quality results currently known.
+ *
+ * Prefer `QuantizerCelebi`: coupled with Kmeans, this provides the best-known results for image
+ * quantization.
*
- * <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.
+ * Seemingly all Wu implementations are based off of one C code snippet that cites a book from 1992
+ * Graphics Gems vol. II, pp. 126-133. As a result, it is very hard to understand the mechanics of
+ * the algorithm, beyond the commentary provided in the C code. Comments on the methods of this
+ * class are avoided in favor of finding another implementation and reading the commentary there,
+ * avoiding perpetuating the same incomplete and somewhat confusing commentary here.
*/
-public 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<>();
+public final class WuQuantizer implements Quantizer {
+ // A histogram of all the input colors is constructed. It has the shape of a
+ // cube. The cube would be too large if it contained all 16 million colors:
+ // historical best practice is to use 5 bits of the 8 in each channel,
+ // reducing the histogram to a volume of ~32,000.
+ private static final int BITS = 5;
+ private static final int MAX_INDEX = 32;
+ private static final int SIDE_LENGTH = 33;
+ private static final int TOTAL_SIZE = 35937;
+
+ private int[] mWeights;
+ private int[] mMomentsR;
+ private int[] mMomentsG;
+ private int[] mMomentsB;
+ private double[] mMoments;
+ private Box[] mCubes;
+ private Palette mPalette;
+ private int[] mColors;
+ private Map<Integer, Integer> mInputPixelToCount;
@Override
public List<Palette.Swatch> getQuantizedColors() {
- return mSwatches;
+ return mPalette.getSwatches();
}
- 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;
+ @Override
+ public void quantize(@NonNull int[] pixels, int colorCount) {
+ assert (pixels.length > 0);
+
+ QuantizerMap quantizerMap = new QuantizerMap();
+ quantizerMap.quantize(pixels, colorCount);
+ mInputPixelToCount = quantizerMap.getColorToCount();
+ // Extraction should not be run on using a color count higher than the number of colors
+ // in the pixels. The algorithm doesn't expect that to be the case, unexpected results and
+ // exceptions may occur.
+ Set<Integer> uniqueColors = mInputPixelToCount.keySet();
+ if (uniqueColors.size() <= colorCount) {
+ mColors = new int[mInputPixelToCount.keySet().size()];
+ int index = 0;
+ for (int color : uniqueColors) {
+ mColors[index++] = color;
+ }
+ } else {
+ constructHistogram(mInputPixelToCount);
+ createMoments();
+ CreateBoxesResult createBoxesResult = createBoxes(colorCount);
+ mColors = createResult(createBoxesResult.mResultCount);
+ }
+
+ List<Palette.Swatch> swatches = new ArrayList<>();
+ for (int color : mColors) {
+ swatches.add(new Palette.Swatch(color, 0));
+ }
+ mPalette = Palette.from(swatches);
}
- private final int mSize; /* image size, in bytes. */
- private int mMaxColors;
- private int[] mQadd;
- private final int[] mPixels;
+ @Nullable
+ public int[] getColors() {
+ return mColors;
+ }
- 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];
+ /** Keys are color ints, values are the number of pixels in the image matching that color int */
+ @Nullable
+ public Map<Integer, Integer> inputPixelToCount() {
+ return mInputPixelToCount;
+ }
- public WuQuantizer(int[] pixels, int maxColorCount) {
- if (pixels == null) {
- pixels = new int[]{};
- }
- this.mPixels = pixels;
- this.mSize = pixels.length;
+ private static int getIndex(int r, int g, int b) {
+ return (r << 10) + (r << 6) + (g << 5) + r + g + b;
}
- @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.
- //
- // Also, the number of maximum colors should be capped at the number of pixels - otherwise,
- // If extraction is run on a set of pixels whose count is less than max colors,
- // then colors.length < max colors, and accesses to colors[index] throw an
- // ArrayOutOfBoundsException.
- this.mMaxColors = Math.min(Math.min(MAX_COLORS, maxColorCount), colors.length);
- 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();
+ private void constructHistogram(Map<Integer, Integer> pixels) {
+ mWeights = new int[TOTAL_SIZE];
+ mMomentsR = new int[TOTAL_SIZE];
+ mMomentsG = new int[TOTAL_SIZE];
+ mMomentsB = new int[TOTAL_SIZE];
+ mMoments = new double[TOTAL_SIZE];
+
+ for (Map.Entry<Integer, Integer> pair : pixels.entrySet()) {
+ int pixel = pair.getKey();
+ int count = pair.getValue();
+ int red = Color.red(pixel);
+ int green = Color.green(pixel);
+ int blue = Color.blue(pixel);
+ int bitsToRemove = 8 - BITS;
+ int iR = (red >> bitsToRemove) + 1;
+ int iG = (green >> bitsToRemove) + 1;
+ int iB = (blue >> bitsToRemove) + 1;
+ int index = getIndex(iR, iG, iB);
+ mWeights[index] += count;
+ mMomentsR[index] += (red * count);
+ mMomentsG[index] += (green * count);
+ mMomentsB[index] += (blue * count);
+ mMoments[index] += (count * ((red * red) + (green * green) + (blue * blue)));
}
+ }
- cube[0].mR0 = cube[0].mG0 = cube[0].mB0 = 0;
- cube[0].mR1 = cube[0].mG1 = cube[0].mB1 = QUANT_SIZE - 1;
- next = 0;
+ private void createMoments() {
+ for (int r = 1; r < SIDE_LENGTH; ++r) {
+ int[] area = new int[SIDE_LENGTH];
+ int[] areaR = new int[SIDE_LENGTH];
+ int[] areaG = new int[SIDE_LENGTH];
+ int[] areaB = new int[SIDE_LENGTH];
+ double[] area2 = new double[SIDE_LENGTH];
+
+ for (int g = 1; g < SIDE_LENGTH; ++g) {
+ int line = 0;
+ int lineR = 0;
+ int lineG = 0;
+ int lineB = 0;
+
+ double line2 = 0.0;
+ for (int b = 1; b < SIDE_LENGTH; ++b) {
+ int index = getIndex(r, g, b);
+ line += mWeights[index];
+ lineR += mMomentsR[index];
+ lineG += mMomentsG[index];
+ lineB += mMomentsB[index];
+ line2 += mMoments[index];
+
+ area[b] += line;
+ areaR[b] += lineR;
+ areaG[b] += lineG;
+ areaB[b] += lineB;
+ area2[b] += line2;
+
+ int previousIndex = getIndex(r - 1, g, b);
+ mWeights[index] = mWeights[previousIndex] + area[b];
+ mMomentsR[index] = mMomentsR[previousIndex] + areaR[b];
+ mMomentsG[index] = mMomentsG[previousIndex] + areaG[b];
+ mMomentsB[index] = mMomentsB[previousIndex] + areaB[b];
+ mMoments[index] = mMoments[previousIndex] + area2[b];
+ }
+ }
+ }
+ }
- 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;
+ private CreateBoxesResult createBoxes(int maxColorCount) {
+ mCubes = new Box[maxColorCount];
+ for (int i = 0; i < maxColorCount; i++) {
+ mCubes[i] = new Box();
+ }
+ double[] volumeVariance = new double[maxColorCount];
+ Box firstBox = mCubes[0];
+ firstBox.r1 = MAX_INDEX;
+ firstBox.g1 = MAX_INDEX;
+ firstBox.b1 = MAX_INDEX;
+
+ int generatedColorCount = 0;
+ int next = 0;
+
+ for (int i = 1; i < maxColorCount; i++) {
+ if (cut(mCubes[next], mCubes[i])) {
+ volumeVariance[next] = (mCubes[next].vol > 1) ? variance(mCubes[next]) : 0.0;
+ volumeVariance[i] = (mCubes[i].vol > 1) ? variance(mCubes[i]) : 0.0;
} else {
- vv[next] = 0.0f;
+ volumeVariance[next] = 0.0;
i--;
}
+
next = 0;
- temp = vv[0];
- for (k = 1; k <= i; ++k) {
- if (vv[k] > temp) {
- temp = vv[k];
+
+ double temp = volumeVariance[0];
+ for (int k = 1; k <= i; k++) {
+ if (volumeVariance[k] > temp) {
+ temp = volumeVariance[k];
next = k;
}
}
- if (temp <= 0.0f) {
+ generatedColorCount = i + 1;
+ if (temp <= 0.0) {
break;
}
}
- for (k = 0; k < mMaxColors; ++k) {
- weight = getVolume(cube[k], mWt);
+ return new CreateBoxesResult(maxColorCount, generatedColorCount);
+ }
+
+ private int[] createResult(int colorCount) {
+ int[] colors = new int[colorCount];
+ int nextAvailableIndex = 0;
+ for (int i = 0; i < colorCount; ++i) {
+ Box cube = mCubes[i];
+ int weight = volume(cube, mWeights);
if (weight > 0) {
- red = (int) (getVolume(cube[k], mMr) / weight);
- green = (int) (getVolume(cube[k], mMg) / weight);
- blue = (int) (getVolume(cube[k], mMb) / weight);
- colors[k] = (255 << 24) | (red << 16) | (green << 8) | blue;
- } else {
- colors[k] = 0;
+ int r = (volume(cube, mMomentsR) / weight);
+ int g = (volume(cube, mMomentsG) / weight);
+ int b = (volume(cube, mMomentsB) / weight);
+ int color = Color.rgb(r, g, b);
+ colors[nextAvailableIndex++] = color;
}
}
+ int[] resultArray = new int[nextAvailableIndex];
+ arraycopy(colors, 0, resultArray, 0, nextAvailableIndex);
+ return resultArray;
+ }
- int bitsPerPixel = 0;
- while ((1 << bitsPerPixel) < mMaxColors) {
- bitsPerPixel++;
- }
+ private double variance(Box cube) {
+ int dr = volume(cube, mMomentsR);
+ int dg = volume(cube, mMomentsG);
+ int db = volume(cube, mMomentsB);
+ double xx =
+ mMoments[getIndex(cube.r1, cube.g1, cube.b1)]
+ - mMoments[getIndex(cube.r1, cube.g1, cube.b0)]
+ - mMoments[getIndex(cube.r1, cube.g0, cube.b1)]
+ + mMoments[getIndex(cube.r1, cube.g0, cube.b0)]
+ - mMoments[getIndex(cube.r0, cube.g1, cube.b1)]
+ + mMoments[getIndex(cube.r0, cube.g1, cube.b0)]
+ + mMoments[getIndex(cube.r0, cube.g0, cube.b1)]
+ - mMoments[getIndex(cube.r0, cube.g0, cube.b0)];
+
+ int hypotenuse = (dr * dr + dg * dg + db * db);
+ int volume2 = volume(cube, mWeights);
+ double variance2 = xx - ((double) hypotenuse / (double) volume2);
+ return variance2;
+ }
- List<Palette.Swatch> swatches = new ArrayList<>();
- for (int l = 0; l < k; l++) {
- int pixel = colors[l];
- if (pixel == 0) {
- continue;
+ private boolean cut(Box one, Box two) {
+ int wholeR = volume(one, mMomentsR);
+ int wholeG = volume(one, mMomentsG);
+ int wholeB = volume(one, mMomentsB);
+ int wholeW = volume(one, mWeights);
+
+ MaximizeResult maxRResult =
+ maximize(one, Direction.RED, one.r0 + 1, one.r1, wholeR, wholeG, wholeB, wholeW);
+ MaximizeResult maxGResult =
+ maximize(one, Direction.GREEN, one.g0 + 1, one.g1, wholeR, wholeG, wholeB, wholeW);
+ MaximizeResult maxBResult =
+ maximize(one, Direction.BLUE, one.b0 + 1, one.b1, wholeR, wholeG, wholeB, wholeW);
+ Direction cutDirection;
+ double maxR = maxRResult.mMaximum;
+ double maxG = maxGResult.mMaximum;
+ double maxB = maxBResult.mMaximum;
+ if (maxR >= maxG && maxR >= maxB) {
+ if (maxRResult.mCutLocation < 0) {
+ return false;
}
- swatches.add(new Palette.Swatch(pixel, 0));
+ cutDirection = Direction.RED;
+ } else if (maxG >= maxR && maxG >= maxB) {
+ cutDirection = Direction.GREEN;
+ } else {
+ cutDirection = Direction.BLUE;
}
- 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;
+ two.r1 = one.r1;
+ two.g1 = one.g1;
+ two.b1 = one.b1;
+
+ switch (cutDirection) {
+ case RED:
+ one.r1 = maxRResult.mCutLocation;
+ two.r0 = one.r1;
+ two.g0 = one.g0;
+ two.b0 = one.b0;
+ break;
+ case GREEN:
+ one.g1 = maxGResult.mCutLocation;
+ two.r0 = one.r0;
+ two.g0 = one.g1;
+ two.b0 = one.b0;
+ break;
+ case BLUE:
+ one.b1 = maxBResult.mCutLocation;
+ two.r0 = one.r0;
+ two.g0 = one.g0;
+ two.b0 = one.b1;
+ break;
+ default:
+ throw new IllegalArgumentException("unexpected direction " + cutDirection);
}
- mQadd = new int[mSize];
+ one.vol = (one.r1 - one.r0) * (one.g1 - one.g0) * (one.b1 - one.b0);
+ two.vol = (two.r1 - two.r0) * (two.g1 - two.g0) * (two.b1 - two.b0);
- 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];
- }
+ return true;
}
- /* 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;
+ private MaximizeResult maximize(
+ Box cube,
+ Direction direction,
+ int first,
+ int last,
+ int wholeR,
+ int wholeG,
+ int wholeB,
+ int wholeW) {
+ int baseR = bottom(cube, direction, mMomentsR);
+ int baseG = bottom(cube, direction, mMomentsG);
+ int baseB = bottom(cube, direction, mMomentsB);
+ int baseW = bottom(cube, direction, mWeights);
+
+ double max = 0.0;
+ int cut = -1;
+ for (int i = first; i < last; i++) {
+ int halfR = baseR + top(cube, direction, i, mMomentsR);
+ int halfG = baseG + top(cube, direction, i, mMomentsG);
+ int halfB = baseB + top(cube, direction, i, mMomentsB);
+ int halfW = baseW + top(cube, direction, i, mWeights);
+
+ if (halfW == 0) {
+ continue;
+ }
+ double tempNumerator = halfR * halfR + halfG * halfG + halfB * halfB;
+ double tempDenominator = halfW;
+ double temp = tempNumerator / tempDenominator;
+
+ halfR = wholeR - halfR;
+ halfG = wholeG - halfG;
+ halfB = wholeB - halfB;
+ halfW = wholeW - halfW;
+ if (halfW == 0) {
+ continue;
}
- 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];
- }
+ tempNumerator = halfR * halfR + halfG * halfG + halfB * halfB;
+ tempDenominator = halfW;
+ temp += (tempNumerator / tempDenominator);
+ if (temp > max) {
+ max = temp;
+ cut = i;
}
}
+ return new MaximizeResult(cut, max);
}
- private 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]);
+ private static int volume(Box cube, int[] moment) {
+ return (moment[getIndex(cube.r1, cube.g1, cube.b1)]
+ - moment[getIndex(cube.r1, cube.g1, cube.b0)]
+ - moment[getIndex(cube.r1, cube.g0, cube.b1)]
+ + moment[getIndex(cube.r1, cube.g0, cube.b0)]
+ - moment[getIndex(cube.r0, cube.g1, cube.b1)]
+ + moment[getIndex(cube.r0, cube.g1, cube.b0)]
+ + moment[getIndex(cube.r0, cube.g0, cube.b1)]
+ - moment[getIndex(cube.r0, cube.g0, cube.b0)]);
}
- /* 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) {
+ private static int bottom(Box cube, Direction direction, int[] moment) {
+ switch (direction) {
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]);
+ return -moment[getIndex(cube.r0, cube.g1, cube.b1)]
+ + moment[getIndex(cube.r0, cube.g1, cube.b0)]
+ + moment[getIndex(cube.r0, cube.g0, cube.b1)]
+ - moment[getIndex(cube.r0, cube.g0, cube.b0)];
case GREEN:
- return (-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]);
+ return -moment[getIndex(cube.r1, cube.g0, cube.b1)]
+ + moment[getIndex(cube.r1, cube.g0, cube.b0)]
+ + moment[getIndex(cube.r0, cube.g0, cube.b1)]
+ - moment[getIndex(cube.r0, cube.g0, cube.b0)];
case BLUE:
- return (-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]);
+ return -moment[getIndex(cube.r1, cube.g1, cube.b0)]
+ + moment[getIndex(cube.r1, cube.g0, cube.b0)]
+ + moment[getIndex(cube.r0, cube.g1, cube.b0)]
+ - moment[getIndex(cube.r0, cube.g0, cube.b0)];
default:
- return 0;
+ throw new IllegalArgumentException("unexpected direction " + direction);
}
}
- 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) {
+ private static int top(Box cube, Direction direction, int position, int[] moment) {
+ switch (direction) {
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]);
+ return (moment[getIndex(position, cube.g1, cube.b1)]
+ - moment[getIndex(position, cube.g1, cube.b0)]
+ - moment[getIndex(position, cube.g0, cube.b1)]
+ + moment[getIndex(position, cube.g0, cube.b0)]);
case GREEN:
- return (mmt[cube.mR1][pos][cube.mB1]
- - mmt[cube.mR1][pos][cube.mB0]
- - mmt[cube.mR0][pos][cube.mB1]
- + mmt[cube.mR0][pos][cube.mB0]);
+ return (moment[getIndex(cube.r1, position, cube.b1)]
+ - moment[getIndex(cube.r1, position, cube.b0)]
+ - moment[getIndex(cube.r0, position, cube.b1)]
+ + moment[getIndex(cube.r0, position, cube.b0)]);
case BLUE:
- return (mmt[cube.mR1][cube.mG1][pos]
- - mmt[cube.mR1][cube.mG0][pos]
- - mmt[cube.mR0][cube.mG1][pos]
- + mmt[cube.mR0][cube.mG0][pos]);
+ return (moment[getIndex(cube.r1, cube.g1, position)]
+ - moment[getIndex(cube.r1, cube.g0, position)]
+ - moment[getIndex(cube.r0, cube.g1, position)]
+ + moment[getIndex(cube.r0, cube.g0, position)]);
default:
- return 0;
+ throw new IllegalArgumentException("unexpected direction " + direction);
}
}
- 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);
+ private enum Direction {
+ RED,
+ GREEN,
+ BLUE
}
- /* 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;
+ private static class MaximizeResult {
+ // < 0 if cut impossible
+ final int mCutLocation;
+ final double mMaximum;
- if (temp > max) {
- max = temp;
- cut[0] = i;
- }
+ MaximizeResult(int cut, double max) {
+ mCutLocation = cut;
+ mMaximum = max;
}
-
- 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;
+ private static class CreateBoxesResult {
+ final int mRequestedCount;
+ final int mResultCount;
- 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;
+ CreateBoxesResult(int requestedCount, int resultCount) {
+ mRequestedCount = requestedCount;
+ mResultCount = resultCount;
}
- 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;
+ private static class Box {
+ public int r0 = 0;
+ public int r1 = 0;
+ public int g0 = 0;
+ public int g1 = 0;
+ public int b0 = 0;
+ public int b1 = 0;
+ public int vol = 0;
}
}
+
+
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index c888e545b24e..53f1035ee422 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -628,7 +628,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
}
// scale if the crop height winds up not matching the recommended metrics
- needScale = wpData.mHeight != cropHint.height()
+ needScale = cropHint.height() > wpData.mHeight
|| cropHint.height() > GLHelper.getMaxTextureSize()
|| cropHint.width() > GLHelper.getMaxTextureSize();
@@ -752,7 +752,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
f = new FileOutputStream(wallpaper.cropFile);
bos = new BufferedOutputStream(f, 32*1024);
- finalCrop.compress(Bitmap.CompressFormat.JPEG, 100, bos);
+ finalCrop.compress(Bitmap.CompressFormat.PNG, 100, bos);
bos.flush(); // don't rely on the implicit flush-at-close when noting success
success = true;
}