From 2a75b45bff598ca0e5df868fb2474365f2c883b4 Mon Sep 17 00:00:00 2001 From: James O'Leary Date: Thu, 11 Mar 2021 11:27:38 -0500 Subject: [PATCH 1/4] Fix initializing clusters with random randoms During the quantization process, an instance of a Quantizer is created. When quantization is run, the Quantizer may need to create starting points that are random. To keep results stable, meaning multiple runs of a quantizer output the same results, those starting points must be created using a Random with the same starting seed. Without specifying a starting seed, or if an instance of Random is reused across runs, the random starting points will vary across quantizer runs. With this patch, the Random used to create starting points is no longer used across multiple runs (i.e. it isn't a static variable anymore). Additionally, a starting seed is specified. Test: Add log dumps after the quantization process. Verify that results from Kmeans remain consistent as long as the input is consistent. Bug: 182333325 Change-Id: Ia0f984349b7885ae1b44536aaa1b286a28e50587 --- .../com/android/internal/graphics/palette/Mean.java | 9 ++++----- .../internal/graphics/palette/WSMeansQuantizer.java | 12 ++++++++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/core/java/com/android/internal/graphics/palette/Mean.java b/core/java/com/android/internal/graphics/palette/Mean.java index 894f91b6261c1..bde036349d3b9 100644 --- a/core/java/com/android/internal/graphics/palette/Mean.java +++ b/core/java/com/android/internal/graphics/palette/Mean.java @@ -22,20 +22,19 @@ 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 + * @param random used to generate a random center */ - Mean(int upperBound) { + Mean(int upperBound, Random random) { center = new float[]{ - RANDOM.nextInt(upperBound + 1), RANDOM.nextInt(upperBound + 1), - RANDOM.nextInt(upperBound + 1) + random.nextInt(upperBound + 1), random.nextInt(upperBound + 1), + random.nextInt(upperBound + 1) }; } diff --git a/core/java/com/android/internal/graphics/palette/WSMeansQuantizer.java b/core/java/com/android/internal/graphics/palette/WSMeansQuantizer.java index a87a34f4ae110..b7c03d6a0b0ab 100644 --- a/core/java/com/android/internal/graphics/palette/WSMeansQuantizer.java +++ b/core/java/com/android/internal/graphics/palette/WSMeansQuantizer.java @@ -22,6 +22,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Random; import java.util.Set; @@ -57,9 +58,12 @@ public class WSMeansQuantizer implements Quantizer { } 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); + mMeans[means.length + i] = new Mean(100, random); } } @@ -105,8 +109,12 @@ public class WSMeansQuantizer implements Quantizer { /** Create random starting centroids for K-means. */ public static float[][] randomMeans(int maxColors, int upperBound) { float[][] means = new float[maxColors][]; + + // 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); for (int i = 0; i < maxColors; i++) { - means[i] = new Mean(upperBound).center; + means[i] = new Mean(upperBound, random).center; } return means; } From fbbb5dd5cade7c9f775bff25a20dbc99ec74303e Mon Sep 17 00:00:00 2001 From: James O'Leary Date: Thu, 11 Mar 2021 11:41:35 -0500 Subject: [PATCH 2/4] Fix create colors in Wu quantizer This line is suspiciously complicated compared to other 3 ints -> int representing color functions. This patch ensures the colors produced have an explicitly opaque alpha channel (transparent pixels are filtered out earlier in the quantization process), and ensures we're not unnecessarily masking the red/green/blue channels. Test: Add log dump to output hex for all colors created, immediately after the Wu quantization finishes, thus isolating it from the larger quantization process. Test a variety of wallpapers, make sure hex codes make sense, verify that the fix holds by converting Bug: 182333325 Change-Id: Ic0bd4afcd095845e00eb6d10d05aab336266d6c3 --- .../java/com/android/internal/graphics/palette/WuQuantizer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/java/com/android/internal/graphics/palette/WuQuantizer.java b/core/java/com/android/internal/graphics/palette/WuQuantizer.java index 66206bf8297a4..6d08dc88cb20e 100644 --- a/core/java/com/android/internal/graphics/palette/WuQuantizer.java +++ b/core/java/com/android/internal/graphics/palette/WuQuantizer.java @@ -129,7 +129,7 @@ public class WuQuantizer implements Quantizer { 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); + colors[k] = (255 << 24) | (red << 16) | (green << 8) | blue; } else { colors[k] = 0; } From 9b9276849b3bb9b57900f21f90e98e6f330536ef Mon Sep 17 00:00:00 2001 From: James O'Leary Date: Thu, 11 Mar 2021 13:43:20 -0500 Subject: [PATCH 3/4] Ignore transparent pixels during WSMeans The WSMeans quantizer uses starting clusters from a Wu quantizer as starting points for an optimized Kmeans algorithm. The first thing the quantizer does is assign pixels from the image being quantized to those starting clusters. Either due to the image source or downscaling, transparent pixels may be in the pixels. Filter those out: they create misleading results, such as there being two colors in the image, semi-transparent black and green, in a wallpaper that is solid green by all accounts. Fixes: 182333325 Test: atest CtsAppTestCases:android.app.cts.WallpaperColorsTest#fromDrawableTest passes locally on sunfish. link: http://ab/I26300007868608112 Change-Id: Icaebd98eeba27d3dc59a698282dd3be03fce1bf9 --- .../internal/graphics/palette/WSMeansQuantizer.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/java/com/android/internal/graphics/palette/WSMeansQuantizer.java b/core/java/com/android/internal/graphics/palette/WSMeansQuantizer.java index b7c03d6a0b0ab..b4f216ba87fa6 100644 --- a/core/java/com/android/internal/graphics/palette/WSMeansQuantizer.java +++ b/core/java/com/android/internal/graphics/palette/WSMeansQuantizer.java @@ -68,6 +68,17 @@ public class WSMeansQuantizer implements Quantizer { } 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); + if (alpha < 255) { + continue; + } Integer currentCount = mCountByColor.get(pixel); if (currentCount == null) { currentCount = 0; From 7ef64c0787c0f98cc0273d9f20e9ed70ef33b145 Mon Sep 17 00:00:00 2001 From: James O'Leary Date: Thu, 11 Mar 2021 11:50:40 -0500 Subject: [PATCH 4/4] Move Wu check for max colors earlier. This has no impact. However, after going through the quantizers line by line for other bug fixes, after getting a break from looking at them in-depth for a couple weeks, it was much more readable/felt safer to adjust mMaxColors at the top. Bug: 182333325 Test: Verify output remains constant, atest CtsAppTestCases:android.app.cts.WallpaperColorsTest#fromDrawableTest passes. Change-Id: I561ba44bca392634a956992fd946f3310aa1c8ee --- .../internal/graphics/palette/WuQuantizer.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/java/com/android/internal/graphics/palette/WuQuantizer.java b/core/java/com/android/internal/graphics/palette/WuQuantizer.java index 6d08dc88cb20e..a2652ea6d5e1a 100644 --- a/core/java/com/android/internal/graphics/palette/WuQuantizer.java +++ b/core/java/com/android/internal/graphics/palette/WuQuantizer.java @@ -78,7 +78,12 @@ public class WuQuantizer implements Quantizer { // 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); + // + // 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; @@ -119,11 +124,7 @@ public class WuQuantizer implements Quantizer { } } - // If extraction is run on a set of pixels whose count is less than the - // number of max colors, then colors.length < max colors, and accesses - // to colors[index] inside the for loop throw an ArrayOutOfBoundsException. - int numColorsToCreate = (int) Math.min(mMaxColors, colors.length); - for (k = 0; k < numColorsToCreate; ++k) { + for (k = 0; k < mMaxColors; ++k) { weight = getVolume(cube[k], mWt); if (weight > 0) { red = (int) (getVolume(cube[k], mMr) / weight);