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:
committed by
Android (Google) Code Review
commit
e0970984c8
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,21 +18,18 @@ package com.android.internal.graphics.palette;
|
|||||||
|
|
||||||
import android.annotation.ColorInt;
|
import android.annotation.ColorInt;
|
||||||
|
|
||||||
interface CentroidProvider {
|
/**
|
||||||
/**
|
* Interface that allows quantizers to have a plug-and-play interface for experimenting with
|
||||||
* @return 3 dimensions representing the color
|
* 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);
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
}
|
||||||
|
|
||||||
|
mPoints = new float[mInputPixelToCount.size()][3];
|
||||||
|
mPixels = new int[mInputPixelToCount.size()];
|
||||||
|
int index = 0;
|
||||||
|
for (int pixel : mInputPixelToCount.keySet()) {
|
||||||
|
mPixels[index] = pixel;
|
||||||
|
mPoints[index] = mPointProvider.fromInt(pixel);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
if (mClusters.length > 0) {
|
||||||
|
// This implies that the constructor was provided starting clusters. If that was the
|
||||||
|
// case, we limit the number of clusters to the number of starting clusters and don't
|
||||||
|
// initialize random clusters.
|
||||||
|
maxColors = Math.min(maxColors, mClusters.length);
|
||||||
|
}
|
||||||
|
maxColors = Math.min(maxColors, mPoints.length);
|
||||||
|
|
||||||
|
initializeClusters(maxColors);
|
||||||
|
for (int i = 0; i < MAX_ITERATIONS; i++) {
|
||||||
|
calculateClusterDistances(maxColors);
|
||||||
|
if (!reassignPoints(maxColors)) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
recalculateClusterCenters(maxColors);
|
||||||
|
|
||||||
// Construct a K×K matrix M in which row i is a permutation of
|
|
||||||
// 1,2,…,K that represents the clusters in increasing order of
|
|
||||||
// distance of their centers from ci;
|
|
||||||
int[][] distanceMatrix = new int[maxColors][maxColors];
|
|
||||||
for (int i = 0; i < maxColors; i++) {
|
|
||||||
double[] distancesFromIToAnotherMean = centroidDistance[i];
|
|
||||||
double[] sortedByDistanceAscending = distancesFromIToAnotherMean.clone();
|
|
||||||
Arrays.sort(sortedByDistanceAscending);
|
|
||||||
int[] outputRow = new int[maxColors];
|
|
||||||
for (int j = 0; j < maxColors; j++) {
|
|
||||||
outputRow[j] = findIndex(distancesFromIToAnotherMean, sortedByDistanceAscending[j]);
|
|
||||||
}
|
|
||||||
distanceMatrix[i] = outputRow;
|
|
||||||
}
|
|
||||||
|
|
||||||
// for (i=1;i≤N′;i=i+ 1) do
|
|
||||||
// Let Sp be the cluster that xi was assigned to in the previous
|
|
||||||
// iteration;
|
|
||||||
// p=m[i];
|
|
||||||
// min_dist=prev_dist=jjxi−cpjj2;
|
|
||||||
boolean anyColorMoved = false;
|
|
||||||
for (int intColor : mUniqueColors) {
|
|
||||||
float[] color = mCentroidProvider.getCentroid(intColor);
|
|
||||||
int indexOfCurrentMean = mMeanIndexByColor.get(intColor);
|
|
||||||
Mean currentMean = mMeans[indexOfCurrentMean];
|
|
||||||
double minDistance = mCentroidProvider.distance(color, currentMean.center);
|
|
||||||
for (int j = 1; j < maxColors; j++) {
|
|
||||||
int indexOfClusterFromCurrentToJ = distanceMatrix[indexOfCurrentMean][j];
|
|
||||||
double distanceBetweenJAndCurrent =
|
|
||||||
centroidDistance[indexOfCurrentMean][indexOfClusterFromCurrentToJ];
|
|
||||||
if (distanceBetweenJAndCurrent >= (4 * minDistance)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
double distanceBetweenJAndColor = mCentroidProvider.distance(mMeans[j].center,
|
|
||||||
color);
|
|
||||||
if (distanceBetweenJAndColor < minDistance) {
|
|
||||||
minDistance = distanceBetweenJAndColor;
|
|
||||||
mMeanIndexByColor.remove(intColor);
|
|
||||||
mMeanIndexByColor.put(intColor, j);
|
|
||||||
anyColorMoved = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<MeanBucket> buckets = new ArrayList<>();
|
|
||||||
for (int i = 0; i < maxColors; i++) {
|
|
||||||
buckets.add(new MeanBucket());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int intColor : mUniqueColors) {
|
|
||||||
int meanIndex = mMeanIndexByColor.get(intColor);
|
|
||||||
MeanBucket meanBucket = buckets.get(meanIndex);
|
|
||||||
meanBucket.add(mCentroidProvider.getCentroid(intColor), intColor,
|
|
||||||
mCountByColor.get(intColor));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Palette.Swatch> swatches = new ArrayList<>();
|
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<>();
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
mPalette = Palette.from(swatches);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int findIndex(double[] list, double element) {
|
|
||||||
for (int i = 0; i < list.length; i++) {
|
private void initializeClusters(int maxColors) {
|
||||||
if (list[i] == element) {
|
boolean hadInputClusters = mClusters.length > 0;
|
||||||
return i;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
|
||||||
vv[next] = 0.0f;
|
|
||||||
i--;
|
|
||||||
}
|
}
|
||||||
next = 0;
|
} else {
|
||||||
temp = vv[0];
|
constructHistogram(mInputPixelToCount);
|
||||||
for (k = 1; k <= i; ++k) {
|
createMoments();
|
||||||
if (vv[k] > temp) {
|
CreateBoxesResult createBoxesResult = createBoxes(colorCount);
|
||||||
temp = vv[k];
|
mColors = createResult(createBoxesResult.mResultCount);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
mSwatches.clear();
|
mPalette = Palette.from(swatches);
|
||||||
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++) {
|
/** Keys are color ints, values are the number of pixels in the image matching that color int */
|
||||||
table[i] = i * i;
|
@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) {
|
private void constructHistogram(Map<Integer, Integer> pixels) {
|
||||||
int rgb = mPixels[i];
|
mWeights = new int[TOTAL_SIZE];
|
||||||
// Skip less than opaque pixels. They're not meaningful in the context of palette
|
mMomentsR = new int[TOTAL_SIZE];
|
||||||
// generation for UI schemes.
|
mMomentsG = new int[TOTAL_SIZE];
|
||||||
if ((rgb >>> 24) < 0xff) {
|
mMomentsB = new int[TOTAL_SIZE];
|
||||||
continue;
|
mMoments = new double[TOTAL_SIZE];
|
||||||
}
|
|
||||||
r = ((rgb >> 16) & 0xff);
|
for (Map.Entry<Integer, Integer> pair : pixels.entrySet()) {
|
||||||
g = ((rgb >> 8) & 0xff);
|
int pixel = pair.getKey();
|
||||||
b = (rgb & 0xff);
|
int count = pair.getValue();
|
||||||
inr = (r >> 3) + 1;
|
int red = Color.red(pixel);
|
||||||
ing = (g >> 3) + 1;
|
int green = Color.green(pixel);
|
||||||
inb = (b >> 3) + 1;
|
int blue = Color.blue(pixel);
|
||||||
mQadd[i] = (inr << 10) + (inr << 6) + inr + (ing << 5) + ing + inb;
|
int bitsToRemove = 8 - BITS;
|
||||||
/*[inr][ing][inb]*/
|
int iR = (red >> bitsToRemove) + 1;
|
||||||
++vwt[inr][ing][inb];
|
int iG = (green >> bitsToRemove) + 1;
|
||||||
vmr[inr][ing][inb] += r;
|
int iB = (blue >> bitsToRemove) + 1;
|
||||||
vmg[inr][ing][inb] += g;
|
int index = getIndex(iR, iG, iB);
|
||||||
vmb[inr][ing][inb] += b;
|
mWeights[index] += count;
|
||||||
m2[inr][ing][inb] += table[r] + table[g] + table[b];
|
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]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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;
|
|
||||||
}
|
}
|
||||||
}
|
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) {
|
int generatedColorCount = 0;
|
||||||
/* Compute remainder of Vol(cube, mmt), substituting pos for */
|
int next = 0;
|
||||||
/* r1, g1, or b1 (depending on dir) */
|
|
||||||
switch (dir) {
|
|
||||||
case RED:
|
|
||||||
return (mmt[pos][cube.mG1][cube.mB1]
|
|
||||||
- mmt[pos][cube.mG1][cube.mB0]
|
|
||||||
- mmt[pos][cube.mG0][cube.mB1]
|
|
||||||
+ mmt[pos][cube.mG0][cube.mB0]);
|
|
||||||
case GREEN:
|
|
||||||
return (mmt[cube.mR1][pos][cube.mB1]
|
|
||||||
- mmt[cube.mR1][pos][cube.mB0]
|
|
||||||
- mmt[cube.mR0][pos][cube.mB1]
|
|
||||||
+ mmt[cube.mR0][pos][cube.mB0]);
|
|
||||||
case BLUE:
|
|
||||||
return (mmt[cube.mR1][cube.mG1][pos]
|
|
||||||
- mmt[cube.mR1][cube.mG0][pos]
|
|
||||||
- mmt[cube.mR0][cube.mG1][pos]
|
|
||||||
+ mmt[cube.mR0][cube.mG0][pos]);
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private double getVariance(Box cube) {
|
for (int i = 1; i < maxColorCount; i++) {
|
||||||
/* Compute the weighted variance of a box */
|
if (cut(mCubes[next], mCubes[i])) {
|
||||||
/* NB: as with the raw statistics, this is really the variance * size */
|
volumeVariance[next] = (mCubes[next].vol > 1) ? variance(mCubes[next]) : 0.0;
|
||||||
double dr, dg, db, xx;
|
volumeVariance[i] = (mCubes[i].vol > 1) ? variance(mCubes[i]) : 0.0;
|
||||||
dr = getVolume(cube, mMr);
|
} else {
|
||||||
dg = getVolume(cube, mMg);
|
volumeVariance[next] = 0.0;
|
||||||
db = getVolume(cube, mMb);
|
i--;
|
||||||
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) {
|
next = 0;
|
||||||
max = temp;
|
|
||||||
cut[0] = i;
|
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) {
|
private int[] createResult(int colorCount) {
|
||||||
int dir;
|
int[] colors = new int[colorCount];
|
||||||
int[] cutr = new int[1];
|
int nextAvailableIndex = 0;
|
||||||
int[] cutg = new int[1];
|
for (int i = 0; i < colorCount; ++i) {
|
||||||
int[] cutb = new int[1];
|
Box cube = mCubes[i];
|
||||||
double maxr, maxg, maxb;
|
int weight = volume(cube, mWeights);
|
||||||
long whole_r, whole_g, whole_b, whole_w;
|
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);
|
private double variance(Box cube) {
|
||||||
whole_g = getVolume(set1, mMg);
|
int dr = volume(cube, mMomentsR);
|
||||||
whole_b = getVolume(set1, mMb);
|
int dg = volume(cube, mMomentsG);
|
||||||
whole_w = getVolume(set1, mWt);
|
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,
|
int hypotenuse = (dr * dr + dg * dg + db * db);
|
||||||
whole_w);
|
int volume2 = volume(cube, mWeights);
|
||||||
maxg = maximize(set1, GREEN, set1.mG0 + 1, set1.mG1, cutg, whole_r, whole_g, whole_b,
|
double variance2 = xx - ((double) hypotenuse / (double) volume2);
|
||||||
whole_w);
|
return variance2;
|
||||||
maxb = maximize(set1, BLUE, set1.mB0 + 1, set1.mB1, cutb, whole_r, whole_g, whole_b,
|
}
|
||||||
whole_w);
|
|
||||||
|
|
||||||
if (maxr >= maxg && maxr >= maxb) {
|
private boolean cut(Box one, Box two) {
|
||||||
dir = RED;
|
int wholeR = volume(one, mMomentsR);
|
||||||
if (cutr[0] < 0) return false; /* can't split the box */
|
int wholeG = volume(one, mMomentsG);
|
||||||
} else if (maxg >= maxr && maxg >= maxb) {
|
int wholeB = volume(one, mMomentsB);
|
||||||
dir = GREEN;
|
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 {
|
} else {
|
||||||
dir = BLUE;
|
cutDirection = Direction.BLUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
set2.mR1 = set1.mR1;
|
two.r1 = one.r1;
|
||||||
set2.mG1 = set1.mG1;
|
two.g1 = one.g1;
|
||||||
set2.mB1 = set1.mB1;
|
two.b1 = one.b1;
|
||||||
|
|
||||||
switch (dir) {
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user