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 android.util.Size;
import com.android.internal.graphics.ColorUtils; 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.CelebiQuantizer;
import com.android.internal.graphics.palette.Palette; import com.android.internal.graphics.palette.Palette;
import com.android.internal.graphics.palette.VariationalKMeansQuantizer; import com.android.internal.graphics.palette.VariationalKMeansQuantizer;
@@ -43,7 +44,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.Set;
/** /**
* Provides information about the colors of a wallpaper. * Provides information about the colors of a wallpaper.
@@ -176,7 +177,7 @@ public final class WallpaperColors implements Parcelable {
shouldRecycle = true; shouldRecycle = true;
Size optimalSize = calculateOptimalSize(bitmap.getWidth(), bitmap.getHeight()); Size optimalSize = calculateOptimalSize(bitmap.getWidth(), bitmap.getHeight());
bitmap = Bitmap.createScaledBitmap(bitmap, optimalSize.getWidth(), bitmap = Bitmap.createScaledBitmap(bitmap, optimalSize.getWidth(),
optimalSize.getHeight(), true /* filter */); optimalSize.getHeight(), false /* filter */);
} }
final Palette palette; final Palette palette;
@@ -189,7 +190,7 @@ public final class WallpaperColors implements Parcelable {
} else { } else {
palette = Palette palette = Palette
.from(bitmap, new CelebiQuantizer()) .from(bitmap, new CelebiQuantizer())
.maximumColorCount(5) .maximumColorCount(128)
.resizeBitmapArea(MAX_WALLPAPER_EXTRACTION_AREA) .resizeBitmapArea(MAX_WALLPAPER_EXTRACTION_AREA)
.generate(); .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. * 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. * occurrences of color in the wallpaper.
* @param colorHints A combination of color hints. * @param colorHints A combination of color hints.
* @hide * @hide
@@ -286,20 +287,105 @@ public final class WallpaperColors implements Parcelable {
* @see WallpaperColors#fromBitmap(Bitmap) * @see WallpaperColors#fromBitmap(Bitmap)
* @see WallpaperColors#fromDrawable(Drawable) * @see WallpaperColors#fromDrawable(Drawable)
*/ */
public WallpaperColors(@NonNull Map<Integer, Integer> populationByColor, public WallpaperColors(@NonNull Map<Integer, Integer> colorToPopulation,
@ColorsHints int colorHints) { @ColorsHints int colorHints) {
mAllColors = populationByColor; mAllColors = colorToPopulation;
ArrayList<Map.Entry<Integer, Integer>> mapEntries = new ArrayList( final Map<Integer, Cam> colorToCam = new HashMap<>();
populationByColor.entrySet()); for (int color : colorToPopulation.keySet()) {
mapEntries.sort((a, b) -> colorToCam.put(color, Cam.fromInt(color));
a.getValue().compareTo(b.getValue()) }
); final double[] hueProportions = hueProportions(colorToCam, colorToPopulation);
mMainColors = mapEntries.stream().map(entry -> Color.valueOf(entry.getKey())).collect( final Map<Integer, Double> colorToHueProportion = colorToHueProportion(
Collectors.toList()); 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; 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>() { public static final @android.annotation.NonNull Creator<WallpaperColors> CREATOR = new Creator<WallpaperColors>() {
@Override @Override
public WallpaperColors createFromParcel(Parcel in) { public WallpaperColors createFromParcel(Parcel in) {

View File

@@ -19,26 +19,32 @@ package com.android.internal.graphics.palette;
import java.util.List; import java.util.List;
/** /**
* An implementation of Celebi's WSM quantizer, or, a Kmeans quantizer that starts with centroids * An implementation of Celebi's quantization method.
* from a Wu quantizer to ensure 100% reproducible and quality results, and has some optimizations
* to the Kmeans algorithm.
*
* See Celebi 2011, “Improving the Performance of K-Means for Color Quantization” * 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 { public class CelebiQuantizer implements Quantizer {
private List<Palette.Swatch> mSwatches; private List<Palette.Swatch> mSwatches;
public CelebiQuantizer() { } public CelebiQuantizer() {
}
@Override @Override
public void quantize(int[] pixels, int maxColors) { public void quantize(int[] pixels, int maxColors) {
WuQuantizer wu = new WuQuantizer(pixels, maxColors); WuQuantizer wu = new WuQuantizer();
wu.quantize(pixels, maxColors); wu.quantize(pixels, maxColors);
List<Palette.Swatch> wuSwatches = wu.getQuantizedColors(); WSMeansQuantizer kmeans = new WSMeansQuantizer(wu.getColors(), new LABPointProvider(),
LABCentroid labCentroidProvider = new LABCentroid(); wu.inputPixelToCount());
WSMeansQuantizer kmeans =
new WSMeansQuantizer(WSMeansQuantizer.createStartingCentroids(labCentroidProvider,
wuSwatches), labCentroidProvider, pixels, maxColors);
kmeans.quantize(pixels, maxColors); kmeans.quantize(pixels, maxColors);
mSwatches = kmeans.getQuantizedColors(); 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 * in L*a*b* space, also known as deltaE, is a universally accepted standard across industries
* and worldwide. * and worldwide.
*/ */
public class LABCentroid implements CentroidProvider { public class LABPointProvider implements PointProvider {
final ColorSpace.Connector mRgbToLab; final ColorSpace.Connector mRgbToLab;
final ColorSpace.Connector mLabToRgb; final ColorSpace.Connector mLabToRgb;
public LABCentroid() { public LABPointProvider() {
mRgbToLab = ColorSpace.connect( mRgbToLab = ColorSpace.connect(
ColorSpace.get(ColorSpace.Named.SRGB), ColorSpace.get(ColorSpace.Named.SRGB),
ColorSpace.get(ColorSpace.Named.CIE_LAB)); ColorSpace.get(ColorSpace.Named.CIE_LAB));
@@ -39,7 +39,7 @@ public class LABCentroid implements CentroidProvider {
} }
@Override @Override
public float[] getCentroid(int color) { public float[] fromInt(int color) {
float r = Color.red(color) / 255.f; float r = Color.red(color) / 255.f;
float g = Color.green(color) / 255.f; float g = Color.green(color) / 255.f;
float b = Color.blue(color) / 255.f; float b = Color.blue(color) / 255.f;
@@ -49,7 +49,7 @@ public class LABCentroid implements CentroidProvider {
} }
@Override @Override
public int getColor(float[] centroid) { public int toInt(float[] centroid) {
float[] rgb = mLabToRgb.transform(centroid); float[] rgb = mLabToRgb.transform(centroid);
int color = Color.rgb(rgb[0], rgb[1], rgb[2]); int color = Color.rgb(rgb[0], rgb[1], rgb[2]);
return color; 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; import android.annotation.ColorInt;
interface CentroidProvider {
/** /**
* @return 3 dimensions representing the color * Interface that allows quantizers to have a plug-and-play interface for experimenting with
* quantization in different color spaces.
*/ */
float[] getCentroid(@ColorInt int color); public interface PointProvider {
/** Convert a color to 3 coordinates representing the color in a color space. */
float[] fromInt(@ColorInt int argb);
/** /** Convert 3 coordinates in the color space into a color */
* @param centroid 3 dimensions representing the color
* @return 32-bit ARGB representation
*/
@ColorInt @ColorInt
int getColor(float[] centroid); int toInt(float[] point);
/** /** Find the distance between two colosrin the color space */
* Distance between two centroids.
*/
float distance(float[] a, float[] b); 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; 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.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.Set; 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, * 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 * "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. * 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 * 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 { public final class WSMeansQuantizer implements Quantizer {
Mean[] mMeans; private static final String TAG = "QuantizerWsmeans";
private final Map<Integer, Integer> mCountByColor = new HashMap<>(); private static final boolean DEBUG = false;
private final Map<Integer, Integer> mMeanIndexByColor = new HashMap<>(); private static final int MAX_ITERATIONS = 10;
private final Set<Integer> mUniqueColors = new HashSet<>(); // Points won't be moved to a closer cluster, if the closer cluster is within
private final List<Palette.Swatch> mSwatches = new ArrayList<>(); // this distance. 3.0 used because L*a*b* delta E < 3 is considered imperceptible.
private final CentroidProvider mCentroidProvider; private static final float MIN_MOVEMENT_DISTANCE = 3.0f;
public WSMeansQuantizer( private final PointProvider mPointProvider;
float[][] means, CentroidProvider centroidProvider, int[] pixels, int maxColors) { private @Nullable Map<Integer, Integer> mInputPixelToCount;
if (pixels == null) { private float[][] mClusters;
pixels = new int[]{}; private int[] mClusterPopulations;
} private float[][] mPoints;
mCentroidProvider = centroidProvider; private int[] mPixels;
mMeans = new Mean[maxColors]; private int[] mClusterIndices;
for (int i = 0; i < means.length; i++) { private int[][] mIndexMatrix = {};
mMeans[i] = new Mean(means[i]); 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) { mInputPixelToCount = inputPixelToCount;
// 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) {
} }
@Override @Override
public List<Palette.Swatch> getQuantizedColors() { public List<Palette.Swatch> getQuantizedColors() {
return mSwatches; return mPalette.getSwatches();
} }
private void predict(int maxColors, int iterationsCompleted) { @Override
double[][] centroidDistance = new double[maxColors][maxColors]; public void quantize(@NonNull int[] pixels, int maxColors) {
for (int i = 0; i <= maxColors; i++) { assert (pixels.length > 0);
for (int j = i + 1; j < maxColors; j++) {
float[] meanI = mMeans[i].center; if (mInputPixelToCount == null) {
float[] meanJ = mMeans[j].center; QuantizerMap mapQuantizer = new QuantizerMap();
double distance = mCentroidProvider.distance(meanI, meanJ); mapQuantizer.quantize(pixels, maxColors);
centroidDistance[i][j] = distance; mInputPixelToCount = mapQuantizer.getColorToCount();
centroidDistance[j][i] = distance;
}
} }
// Construct a K×K matrix M in which row i is a permutation of mPoints = new float[mInputPixelToCount.size()][3];
// 1,2,…,K that represents the clusters in increasing order of mPixels = new int[mInputPixelToCount.size()];
// distance of their centers from ci; int index = 0;
int[][] distanceMatrix = new int[maxColors][maxColors]; for (int pixel : mInputPixelToCount.keySet()) {
for (int i = 0; i < maxColors; i++) { mPixels[index] = pixel;
double[] distancesFromIToAnotherMean = centroidDistance[i]; mPoints[index] = mPointProvider.fromInt(pixel);
double[] sortedByDistanceAscending = distancesFromIToAnotherMean.clone(); index++;
Arrays.sort(sortedByDistanceAscending);
int[] outputRow = new int[maxColors];
for (int j = 0; j < maxColors; j++) {
outputRow[j] = findIndex(distancesFromIToAnotherMean, sortedByDistanceAscending[j]);
} }
distanceMatrix[i] = outputRow; 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);
// for (i=1;i≤N′;i=i+ 1) do initializeClusters(maxColors);
// Let Sp be the cluster that xi was assigned to in the previous for (int i = 0; i < MAX_ITERATIONS; i++) {
// iteration; calculateClusterDistances(maxColors);
// p=m[i]; if (!reassignPoints(maxColors)) {
// 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; break;
} }
double distanceBetweenJAndColor = mCentroidProvider.distance(mMeans[j].center, recalculateClusterCenters(maxColors);
color);
if (distanceBetweenJAndColor < minDistance) {
minDistance = distanceBetweenJAndColor;
mMeanIndexByColor.remove(intColor);
mMeanIndexByColor.put(intColor, j);
anyColorMoved = true;
}
}
}
List<MeanBucket> buckets = new ArrayList<>();
for (int i = 0; i < maxColors; i++) {
buckets.add(new MeanBucket());
}
for (int intColor : mUniqueColors) {
int meanIndex = mMeanIndexByColor.get(intColor);
MeanBucket meanBucket = buckets.get(meanIndex);
meanBucket.add(mCentroidProvider.getCentroid(intColor), intColor,
mCountByColor.get(intColor));
} }
List<Palette.Swatch> swatches = new ArrayList<>(); List<Palette.Swatch> swatches = new ArrayList<>();
boolean done = !anyColorMoved && iterationsCompleted > 0 || iterationsCompleted >= 100; for (int i = 0; i < maxColors; i++) {
if (done) { float[] cluster = mClusters[i];
for (int i = 0; i < buckets.size(); i++) { int colorInt = mPointProvider.toInt(cluster);
MeanBucket a = buckets.get(i); swatches.add(new Palette.Swatch(colorInt, mClusterPopulations[i]));
if (a.mCount <= 0) {
continue;
} }
List<MeanBucket> bucketsToMerge = new ArrayList<>(); mPalette = Palette.from(swatches);
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) { private void initializeClusters(int maxColors) {
bucketsToMerge.add(b); 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]);
} }
} }
for (MeanBucket bucketToMerge : bucketsToMerge) { void calculateClusterDistances(int maxColors) {
float[] centroid = bucketToMerge.getCentroid(); if (mDistanceMatrix.length != maxColors) {
a.add(centroid, mCentroidProvider.getColor(centroid), bucketToMerge.mCount); mDistanceMatrix = new float[maxColors][maxColors];
buckets.remove(bucketToMerge); }
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;
} }
} }
for (MeanBucket bucket : buckets) { if (mIndexMatrix.length != maxColors) {
float[] centroid = bucket.getCentroid(); mIndexMatrix = new int[maxColors][maxColors];
if (centroid == null) {
continue;
} }
int rgb = mCentroidProvider.getColor(centroid); for (int i = 0; i < maxColors; i++) {
swatches.add(new Palette.Swatch(rgb, bucket.mCount)); ArrayList<Distance> distances = new ArrayList<>(maxColors);
mSwatches.clear(); for (int index = 0; index < maxColors; index++) {
mSwatches.addAll(swatches); distances.add(new Distance(index, mDistanceMatrix[i][index]));
}
} 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--);
} }
distances.sort(
(a, b) -> Float.compare(a.getDistance(), b.getDistance()));
for (int j = 0; j < maxColors; j++) {
mIndexMatrix[i][j] = distances.get(j).getIndex();
} }
} }
} }
Mean[] newMeans = new Mean[buckets.size()]; boolean reassignPoints(int maxColors) {
for (int i = 0; i < buckets.size(); i++) { boolean colorMoved = false;
float[] centroid = buckets.get(i).getCentroid(); for (int i = 0; i < mPoints.length; i++) {
newMeans[i] = new Mean(centroid); 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;
} }
predict(buckets.size(), iterationsCompleted + 1); 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 int findIndex(double[] list, double element) { private static class Distance {
for (int i = 0; i < list.length; i++) { private final int mIndex;
if (list[i] == element) { private final float mDistance;
return i;
int getIndex() {
return mIndex;
}
float getDistance() {
return mDistance;
}
Distance(int index, float distance) {
mIndex = index;
mDistance = distance;
} }
} }
throw new IllegalArgumentException("Element not in list");
}
} }

View File

@@ -16,431 +16,447 @@
package com.android.internal.graphics.palette; 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.ArrayList;
import java.util.List; 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 * Prefer `QuantizerCelebi`: coupled with Kmeans, this provides the best-known results for image
* inclusion-exclusion tricks. For speed no nearest neighbor search is done. Slightly better * quantization.
* 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 { public final class WuQuantizer implements Quantizer {
private static final int MAX_COLORS = 256; // A histogram of all the input colors is constructed. It has the shape of a
private static final int RED = 2; // cube. The cube would be too large if it contained all 16 million colors:
private static final int GREEN = 1; // historical best practice is to use 5 bits of the 8 in each channel,
private static final int BLUE = 0; // 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 int[] mWeights;
private final List<Palette.Swatch> mSwatches = new ArrayList<>(); 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 @Override
public List<Palette.Swatch> getQuantizedColors() { 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;
}
private final int mSize; /* image size, in bytes. */
private int mMaxColors;
private int[] mQadd;
private final int[] mPixels;
private final double[][][] mM2 = new double[QUANT_SIZE][QUANT_SIZE][QUANT_SIZE];
private final long[][][] mWt = new long[QUANT_SIZE][QUANT_SIZE][QUANT_SIZE];
private final long[][][] mMr = new long[QUANT_SIZE][QUANT_SIZE][QUANT_SIZE];
private final long[][][] mMg = new long[QUANT_SIZE][QUANT_SIZE][QUANT_SIZE];
private final long[][][] mMb = new long[QUANT_SIZE][QUANT_SIZE][QUANT_SIZE];
public WuQuantizer(int[] pixels, int maxColorCount) {
if (pixels == null) {
pixels = new int[]{};
}
this.mPixels = pixels;
this.mSize = pixels.length;
} }
@Override @Override
public void quantize(int[] colors, int maxColorCount) { public void quantize(@NonNull int[] pixels, int colorCount) {
// All of the sample Wu implementations are reimplementations of a snippet of C code from assert (pixels.length > 0);
// 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; QuantizerMap quantizerMap = new QuantizerMap();
long weight; quantizerMap.quantize(pixels, colorCount);
double[] vv = new double[mMaxColors]; mInputPixelToCount = quantizerMap.getColorToCount();
double temp; // 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
compute3DHistogram(mWt, mMr, mMg, mMb, mM2); // exceptions may occur.
computeMoments(mWt, mMr, mMg, mMb, mM2); Set<Integer> uniqueColors = mInputPixelToCount.keySet();
if (uniqueColors.size() <= colorCount) {
for (i = 0; i < mMaxColors; i++) { mColors = new int[mInputPixelToCount.keySet().size()];
cube[i] = new Box(); int index = 0;
for (int color : uniqueColors) {
mColors[index++] = color;
} }
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 { } else {
vv[next] = 0.0f; constructHistogram(mInputPixelToCount);
i--; createMoments();
} CreateBoxesResult createBoxesResult = createBoxes(colorCount);
next = 0; mColors = createResult(createBoxesResult.mResultCount);
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++;
} }
List<Palette.Swatch> swatches = new ArrayList<>(); List<Palette.Swatch> swatches = new ArrayList<>();
for (int l = 0; l < k; l++) { for (int color : mColors) {
int pixel = colors[l]; swatches.add(new Palette.Swatch(color, 0));
if (pixel == 0) {
continue;
} }
swatches.add(new Palette.Swatch(pixel, 0)); mPalette = Palette.from(swatches);
}
mSwatches.clear();
mSwatches.addAll(swatches);
} }
/* Histogram is in elements 1..HISTSIZE along each axis, @Nullable
* element 0 is for base or marginal value public int[] getColors() {
* NB: these must start out 0! return mColors;
*/
private void compute3DHistogram(
long[][][] vwt, long[][][] vmr, long[][][] vmg, long[][][] vmb, double[][][] m2) {
// build 3-D color histogram of counts, r/g/b, and c^2
int r, g, b;
int i;
int inr;
int ing;
int inb;
int[] table = new int[256];
for (i = 0; i < 256; i++) {
table[i] = i * i;
} }
mQadd = new int[mSize]; /** Keys are color ints, values are the number of pixels in the image matching that color int */
@Nullable
for (i = 0; i < mSize; ++i) { public Map<Integer, Integer> inputPixelToCount() {
int rgb = mPixels[i]; return mInputPixelToCount;
// 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); private static int getIndex(int r, int g, int b) {
b = (rgb & 0xff); return (r << 10) + (r << 6) + (g << 5) + r + g + b;
inr = (r >> 3) + 1; }
ing = (g >> 3) + 1;
inb = (b >> 3) + 1; private void constructHistogram(Map<Integer, Integer> pixels) {
mQadd[i] = (inr << 10) + (inr << 6) + inr + (ing << 5) + ing + inb; mWeights = new int[TOTAL_SIZE];
/*[inr][ing][inb]*/ mMomentsR = new int[TOTAL_SIZE];
++vwt[inr][ing][inb]; mMomentsG = new int[TOTAL_SIZE];
vmr[inr][ing][inb] += r; mMomentsB = new int[TOTAL_SIZE];
vmg[inr][ing][inb] += g; mMoments = new double[TOTAL_SIZE];
vmb[inr][ing][inb] += b;
m2[inr][ing][inb] += table[r] + table[g] + table[b]; 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 private void createMoments() {
* wt[r][g][b] = sum over voxel of P(c) for (int r = 1; r < SIDE_LENGTH; ++r) {
* mr[r][g][b] = sum over voxel of r*P(c) , similarly for mg, mb int[] area = new int[SIDE_LENGTH];
* m2[r][g][b] = sum over voxel of c^2*P(c) int[] areaR = new int[SIDE_LENGTH];
* Actually each of these should be divided by 'size' to give the usual int[] areaG = new int[SIDE_LENGTH];
* interpretation of P() as ranging from 0 to 1, but we needn't do that here. int[] areaB = new int[SIDE_LENGTH];
* double[] area2 = new double[SIDE_LENGTH];
* 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 (int g = 1; g < SIDE_LENGTH; ++g) {
for (i = 0; i < QUANT_SIZE; ++i) { int line = 0;
area2[i] = area[i] = area_r[i] = area_g[i] = area_b[i] = 0; int lineR = 0;
} int lineG = 0;
for (g = 1; g < QUANT_SIZE; ++g) { int lineB = 0;
line2 = line = line_r = line_g = line_b = 0;
for (b = 1; b < QUANT_SIZE; ++b) { double line2 = 0.0;
line += vwt[r][g][b]; for (int b = 1; b < SIDE_LENGTH; ++b) {
line_r += vmr[r][g][b]; int index = getIndex(r, g, b);
line_g += vmg[r][g][b]; line += mWeights[index];
line_b += vmb[r][g][b]; lineR += mMomentsR[index];
line2 += m2[r][g][b]; lineG += mMomentsG[index];
lineB += mMomentsB[index];
line2 += mMoments[index];
area[b] += line; area[b] += line;
area_r[b] += line_r; areaR[b] += lineR;
area_g[b] += line_g; areaG[b] += lineG;
area_b[b] += line_b; areaB[b] += lineB;
area2[b] += line2; area2[b] += line2;
vwt[r][g][b] = vwt[r - 1][g][b] + area[b]; int previousIndex = getIndex(r - 1, g, b);
vmr[r][g][b] = vmr[r - 1][g][b] + area_r[b]; mWeights[index] = mWeights[previousIndex] + area[b];
vmg[r][g][b] = vmg[r - 1][g][b] + area_g[b]; mMomentsR[index] = mMomentsR[previousIndex] + areaR[b];
vmb[r][g][b] = vmb[r - 1][g][b] + area_b[b]; mMomentsG[index] = mMomentsG[previousIndex] + areaG[b];
m2[r][g][b] = m2[r - 1][g][b] + area2[b]; mMomentsB[index] = mMomentsB[previousIndex] + areaB[b];
mMoments[index] = mMoments[previousIndex] + area2[b];
} }
} }
} }
} }
private long getVolume(Box cube, long[][][] mmt) { private CreateBoxesResult createBoxes(int maxColorCount) {
/* Compute sum over a box of any given statistic */ mCubes = new Box[maxColorCount];
return (mmt[cube.mR1][cube.mG1][cube.mB1] for (int i = 0; i < maxColorCount; i++) {
- mmt[cube.mR1][cube.mG1][cube.mB0] mCubes[i] = new Box();
- 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]);
} }
double[] volumeVariance = new double[maxColorCount];
Box firstBox = mCubes[0];
firstBox.r1 = MAX_INDEX;
firstBox.g1 = MAX_INDEX;
firstBox.b1 = MAX_INDEX;
/* The next two routines allow a slightly more efficient calculation int generatedColorCount = 0;
* of Vol() for a proposed subbox of a given box. The sum of Top() int next = 0;
* and Bottom() is the Vol() of a subbox split in the given direction
* and with the specified new upper bound.
*/
private long getBottom(Box cube, int dir, long[][][] mmt) {
/* Compute part of Vol(cube, mmt) that doesn't depend on r1, g1, or b1 */
/* (depending on dir) */
switch (dir) {
case RED:
return (-mmt[cube.mR0][cube.mG1][cube.mB1]
+ mmt[cube.mR0][cube.mG1][cube.mB0]
+ mmt[cube.mR0][cube.mG0][cube.mB1]
- mmt[cube.mR0][cube.mG0][cube.mB0]);
case GREEN:
return (-mmt[cube.mR1][cube.mG0][cube.mB1]
+ mmt[cube.mR1][cube.mG0][cube.mB0]
+ mmt[cube.mR0][cube.mG0][cube.mB1]
- mmt[cube.mR0][cube.mG0][cube.mB0]);
case BLUE:
return (-mmt[cube.mR1][cube.mG1][cube.mB0]
+ mmt[cube.mR1][cube.mG0][cube.mB0]
+ mmt[cube.mR0][cube.mG1][cube.mB0]
- mmt[cube.mR0][cube.mG0][cube.mB0]);
default:
return 0;
}
}
private long getTop(Box cube, int dir, int pos, long[][][] mmt) { for (int i = 1; i < maxColorCount; i++) {
/* Compute remainder of Vol(cube, mmt), substituting pos for */ if (cut(mCubes[next], mCubes[i])) {
/* r1, g1, or b1 (depending on dir) */ volumeVariance[next] = (mCubes[next].vol > 1) ? variance(mCubes[next]) : 0.0;
switch (dir) { volumeVariance[i] = (mCubes[i].vol > 1) ? variance(mCubes[i]) : 0.0;
case RED:
return (mmt[pos][cube.mG1][cube.mB1]
- mmt[pos][cube.mG1][cube.mB0]
- mmt[pos][cube.mG0][cube.mB1]
+ mmt[pos][cube.mG0][cube.mB0]);
case GREEN:
return (mmt[cube.mR1][pos][cube.mB1]
- mmt[cube.mR1][pos][cube.mB0]
- mmt[cube.mR0][pos][cube.mB1]
+ mmt[cube.mR0][pos][cube.mB0]);
case BLUE:
return (mmt[cube.mR1][cube.mG1][pos]
- mmt[cube.mR1][cube.mG0][pos]
- mmt[cube.mR0][cube.mG1][pos]
+ mmt[cube.mR0][cube.mG0][pos]);
default:
return 0;
}
}
private double getVariance(Box cube) {
/* Compute the weighted variance of a box */
/* NB: as with the raw statistics, this is really the variance * size */
double dr, dg, db, xx;
dr = getVolume(cube, mMr);
dg = getVolume(cube, mMg);
db = getVolume(cube, mMb);
xx =
mM2[cube.mR1][cube.mG1][cube.mB1]
- mM2[cube.mR1][cube.mG1][cube.mB0]
- mM2[cube.mR1][cube.mG0][cube.mB1]
+ mM2[cube.mR1][cube.mG0][cube.mB0]
- mM2[cube.mR0][cube.mG1][cube.mB1]
+ mM2[cube.mR0][cube.mG1][cube.mB0]
+ mM2[cube.mR0][cube.mG0][cube.mB1]
- mM2[cube.mR0][cube.mG0][cube.mB0];
return xx - (dr * dr + dg * dg + db * db) / getVolume(cube, mWt);
}
/* We want to minimize the sum of the variances of two subboxes.
* The sum(c^2) terms can be ignored since their sum over both subboxes
* is the same (the sum for the whole box) no matter where we split.
* The remaining terms have a minus sign in the variance formula,
* so we drop the minus sign and MAXIMIZE the sum of the two terms.
*/
private double maximize(
Box cube,
int dir,
int first,
int last,
int[] cut,
long wholeR,
long wholeG,
long wholeB,
long wholeW) {
long half_r, half_g, half_b, half_w;
long base_r, base_g, base_b, base_w;
int i;
double temp, max;
base_r = getBottom(cube, dir, mMr);
base_g = getBottom(cube, dir, mMg);
base_b = getBottom(cube, dir, mMb);
base_w = getBottom(cube, dir, mWt);
max = 0.0f;
cut[0] = -1;
for (i = first; i < last; ++i) {
half_r = base_r + getTop(cube, dir, i, mMr);
half_g = base_g + getTop(cube, dir, i, mMg);
half_b = base_b + getTop(cube, dir, i, mMb);
half_w = base_w + getTop(cube, dir, i, mWt);
/* now half_x is sum over lower half of box, if split at i */
if (half_w == 0) /* subbox could be empty of pixels! */ {
continue; /* never split into an empty box */
}
temp = (half_r * half_r + half_g * half_g + half_b * half_b) / (double) half_w;
half_r = wholeR - half_r;
half_g = wholeG - half_g;
half_b = wholeB - half_b;
half_w = wholeW - half_w;
if (half_w == 0) /* subbox could be empty of pixels! */ {
continue; /* never split into an empty box */
}
temp += (half_r * half_r + half_g * half_g + half_b * half_b) / (double) half_w;
if (temp > max) {
max = temp;
cut[0] = i;
}
}
return max;
}
private boolean cut(Box set1, Box set2) {
int dir;
int[] cutr = new int[1];
int[] cutg = new int[1];
int[] cutb = new int[1];
double maxr, maxg, maxb;
long whole_r, whole_g, whole_b, whole_w;
whole_r = getVolume(set1, mMr);
whole_g = getVolume(set1, mMg);
whole_b = getVolume(set1, mMb);
whole_w = getVolume(set1, mWt);
maxr = maximize(set1, RED, set1.mR0 + 1, set1.mR1, cutr, whole_r, whole_g, whole_b,
whole_w);
maxg = maximize(set1, GREEN, set1.mG0 + 1, set1.mG1, cutg, whole_r, whole_g, whole_b,
whole_w);
maxb = maximize(set1, BLUE, set1.mB0 + 1, set1.mB1, cutb, whole_r, whole_g, whole_b,
whole_w);
if (maxr >= maxg && maxr >= maxb) {
dir = RED;
if (cutr[0] < 0) return false; /* can't split the box */
} else if (maxg >= maxr && maxg >= maxb) {
dir = GREEN;
} else { } else {
dir = BLUE; volumeVariance[next] = 0.0;
i--;
} }
set2.mR1 = set1.mR1; next = 0;
set2.mG1 = set1.mG1;
set2.mB1 = set1.mB1;
switch (dir) { 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 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) {
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;
}
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;
}
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 {
cutDirection = Direction.BLUE;
}
two.r1 = one.r1;
two.g1 = one.g1;
two.b1 = one.b1;
switch (cutDirection) {
case RED: case RED:
set2.mR0 = set1.mR1 = cutr[0]; one.r1 = maxRResult.mCutLocation;
set2.mG0 = set1.mG0; two.r0 = one.r1;
set2.mB0 = set1.mB0; two.g0 = one.g0;
two.b0 = one.b0;
break; break;
case GREEN: case GREEN:
set2.mG0 = set1.mG1 = cutg[0]; one.g1 = maxGResult.mCutLocation;
set2.mR0 = set1.mR0; two.r0 = one.r0;
set2.mB0 = set1.mB0; two.g0 = one.g1;
two.b0 = one.b0;
break; break;
case BLUE: case BLUE:
set2.mB0 = set1.mB1 = cutb[0]; one.b1 = maxBResult.mCutLocation;
set2.mR0 = set1.mR0; two.r0 = one.r0;
set2.mG0 = set1.mG0; two.g0 = one.g0;
two.b0 = one.b1;
break; 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; 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 // 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.height() > GLHelper.getMaxTextureSize()
|| cropHint.width() > GLHelper.getMaxTextureSize(); || cropHint.width() > GLHelper.getMaxTextureSize();
@@ -752,7 +752,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
f = new FileOutputStream(wallpaper.cropFile); f = new FileOutputStream(wallpaper.cropFile);
bos = new BufferedOutputStream(f, 32*1024); 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 bos.flush(); // don't rely on the implicit flush-at-close when noting success
success = true; success = true;
} }