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
This commit is contained in:
James O'Leary
2021-06-09 22:23:24 +00:00
committed by Android (Google) Code Review
10 changed files with 790 additions and 723 deletions

View File

@@ -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;
mAllColors = colorToPopulation;
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());
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) {

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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};
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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 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;
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]);
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 = {};
private Palette mPalette;
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;
}
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);
}
}
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);
}
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][];
// 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, random).center;
}
return means;
}
@Override
public void quantize(int[] pixels, int maxColors) {
mInputPixelToCount = inputPixelToCount;
}
@Override
public List<Palette.Swatch> getQuantizedColors() {
return mSwatches;
return mPalette.getSwatches();
}
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;
@Override
public void quantize(@NonNull int[] pixels, int maxColors) {
assert (pixels.length > 0);
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;
}
}
// 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));
recalculateClusterCenters(maxColors);
}
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);
for (int i = 0; i < maxColors; i++) {
float[] cluster = mClusters[i];
int colorInt = mPointProvider.toInt(cluster);
swatches.add(new Palette.Swatch(colorInt, mClusterPopulations[i]));
}
mPalette = Palette.from(swatches);
}
private static int findIndex(double[] list, double element) {
for (int i = 0; i < list.length; i++) {
if (list[i] == element) {
return i;
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]);
}
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]);
}
}
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 distance = mPointProvider.distance(mClusters[i], mClusters[j]);
mDistanceMatrix[j][i] = distance;
mDistanceMatrix[i][j] = distance;
}
}
throw new IllegalArgumentException("Element not in list");
if (mIndexMatrix.length != maxColors) {
mIndexMatrix = new int[maxColors][maxColors];
}
for (int i = 0; i < maxColors; i++) {
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++) {
mIndexMatrix[i][j] = distances.get(j).getIndex();
}
}
}
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);
float minimumDistance = previousDistance;
int newClusterIndex = -1;
for (int j = 1; j < maxColors; j++) {
int t = mIndexMatrix[previousClusterIndex][j];
if (mDistanceMatrix[previousClusterIndex][t] >= 4 * previousDistance) {
// Triangle inequality proves there's can be no closer center.
break;
}
float distance = mPointProvider.distance(point, mClusters[t]);
if (distance < minimumDistance) {
minimumDistance = distance;
newClusterIndex = t;
}
}
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;
}
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;
}
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;
}
}
private static class Distance {
private final int mIndex;
private final float mDistance;
int getIndex() {
return mIndex;
}
float getDistance() {
return mDistance;
}
Distance(int index, float distance) {
mIndex = index;
mDistance = distance;
}
}
}

View File

@@ -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.
*
* <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.
* Prefer `QuantizerCelebi`: coupled with Kmeans, this provides the best-known results for image
* quantization.
*
* 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;
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 static final int QUANT_SIZE = 33;
private final List<Palette.Swatch> mSwatches = new ArrayList<>();
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;
}
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;
return mPalette.getSwatches();
}
@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;
public void quantize(@NonNull int[] pixels, int colorCount) {
assert (pixels.length > 0);
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--;
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;
}
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] = (255 << 24) | (red << 16) | (green << 8) | blue;
} else {
colors[k] = 0;
}
}
int bitsPerPixel = 0;
while ((1 << bitsPerPixel) < mMaxColors) {
bitsPerPixel++;
} else {
constructHistogram(mInputPixelToCount);
createMoments();
CreateBoxesResult createBoxesResult = createBoxes(colorCount);
mColors = createResult(createBoxesResult.mResultCount);
}
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));
for (int color : mColors) {
swatches.add(new Palette.Swatch(color, 0));
}
mSwatches.clear();
mSwatches.addAll(swatches);
mPalette = Palette.from(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];
@Nullable
public int[] getColors() {
return mColors;
}
for (i = 0; i < 256; i++) {
table[i] = i * i;
}
/** 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;
}
mQadd = new int[mSize];
private static int getIndex(int r, int g, int b) {
return (r << 10) + (r << 6) + (g << 5) + r + g + b;
}
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];
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)));
}
}
/* 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];
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 (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];
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;
area_r[b] += line_r;
area_g[b] += line_g;
area_b[b] += line_b;
areaR[b] += lineR;
areaG[b] += lineG;
areaB[b] += lineB;
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];
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];
}
}
}
}
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 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;
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;
}
}
int generatedColorCount = 0;
int next = 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 */
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 {
volumeVariance[next] = 0.0;
i--;
}
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;
next = 0;
double temp = volumeVariance[0];
for (int k = 1; k <= i; k++) {
if (volumeVariance[k] > temp) {
temp = volumeVariance[k];
next = k;
}
}
generatedColorCount = i + 1;
if (temp <= 0.0) {
break;
}
}
return max;
return new CreateBoxesResult(maxColorCount, generatedColorCount);
}
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;
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) {
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;
}
whole_r = getVolume(set1, mMr);
whole_g = getVolume(set1, mMg);
whole_b = getVolume(set1, mMb);
whole_w = getVolume(set1, mWt);
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)];
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);
int hypotenuse = (dr * dr + dg * dg + db * db);
int volume2 = volume(cube, mWeights);
double variance2 = xx - ((double) hypotenuse / (double) volume2);
return variance2;
}
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;
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;
}
cutDirection = Direction.RED;
} else if (maxG >= maxR && maxG >= maxB) {
cutDirection = Direction.GREEN;
} else {
dir = BLUE;
cutDirection = Direction.BLUE;
}
set2.mR1 = set1.mR1;
set2.mG1 = set1.mG1;
set2.mB1 = set1.mB1;
two.r1 = one.r1;
two.g1 = one.g1;
two.b1 = one.b1;
switch (dir) {
switch (cutDirection) {
case RED:
set2.mR0 = set1.mR1 = cutr[0];
set2.mG0 = set1.mG0;
set2.mB0 = set1.mB0;
one.r1 = maxRResult.mCutLocation;
two.r0 = one.r1;
two.g0 = one.g0;
two.b0 = one.b0;
break;
case GREEN:
set2.mG0 = set1.mG1 = cutg[0];
set2.mR0 = set1.mR0;
set2.mB0 = set1.mB0;
one.g1 = maxGResult.mCutLocation;
two.r0 = one.r0;
two.g0 = one.g1;
two.b0 = one.b0;
break;
case BLUE:
set2.mB0 = set1.mB1 = cutb[0];
set2.mR0 = set1.mR0;
set2.mG0 = set1.mG0;
one.b1 = maxBResult.mCutLocation;
two.r0 = one.r0;
two.g0 = one.g0;
two.b0 = one.b1;
break;
default:
throw new IllegalArgumentException("unexpected direction " + cutDirection);
}
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);
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);
return true;
}
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;
}
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 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)]);
}
private static int bottom(Box cube, Direction direction, int[] moment) {
switch (direction) {
case RED:
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 -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 -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:
throw new IllegalArgumentException("unexpected direction " + direction);
}
}
private static int top(Box cube, Direction direction, int position, int[] moment) {
switch (direction) {
case RED:
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 (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 (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:
throw new IllegalArgumentException("unexpected direction " + direction);
}
}
private enum Direction {
RED,
GREEN,
BLUE
}
private static class MaximizeResult {
// < 0 if cut impossible
final int mCutLocation;
final double mMaximum;
MaximizeResult(int cut, double max) {
mCutLocation = cut;
mMaximum = max;
}
}
private static class CreateBoxesResult {
final int mRequestedCount;
final int mResultCount;
CreateBoxesResult(int requestedCount, int resultCount) {
mRequestedCount = requestedCount;
mResultCount = resultCount;
}
}
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;
}
}

View File

@@ -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;
}