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 com.android.internal.graphics.ColorUtils;
|
||||
import com.android.internal.graphics.cam.Cam;
|
||||
import com.android.internal.graphics.palette.CelebiQuantizer;
|
||||
import com.android.internal.graphics.palette.Palette;
|
||||
import com.android.internal.graphics.palette.VariationalKMeansQuantizer;
|
||||
@@ -43,7 +44,7 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Provides information about the colors of a wallpaper.
|
||||
@@ -176,7 +177,7 @@ public final class WallpaperColors implements Parcelable {
|
||||
shouldRecycle = true;
|
||||
Size optimalSize = calculateOptimalSize(bitmap.getWidth(), bitmap.getHeight());
|
||||
bitmap = Bitmap.createScaledBitmap(bitmap, optimalSize.getWidth(),
|
||||
optimalSize.getHeight(), true /* filter */);
|
||||
optimalSize.getHeight(), false /* filter */);
|
||||
}
|
||||
|
||||
final Palette palette;
|
||||
@@ -189,7 +190,7 @@ public final class WallpaperColors implements Parcelable {
|
||||
} else {
|
||||
palette = Palette
|
||||
.from(bitmap, new CelebiQuantizer())
|
||||
.maximumColorCount(5)
|
||||
.maximumColorCount(128)
|
||||
.resizeBitmapArea(MAX_WALLPAPER_EXTRACTION_AREA)
|
||||
.generate();
|
||||
}
|
||||
@@ -278,7 +279,7 @@ public final class WallpaperColors implements Parcelable {
|
||||
/**
|
||||
* Constructs a new object from a set of colors, where hints can be specified.
|
||||
*
|
||||
* @param populationByColor Map with keys of colors, and value representing the number of
|
||||
* @param colorToPopulation Map with keys of colors, and value representing the number of
|
||||
* occurrences of color in the wallpaper.
|
||||
* @param colorHints A combination of color hints.
|
||||
* @hide
|
||||
@@ -286,20 +287,105 @@ public final class WallpaperColors implements Parcelable {
|
||||
* @see WallpaperColors#fromBitmap(Bitmap)
|
||||
* @see WallpaperColors#fromDrawable(Drawable)
|
||||
*/
|
||||
public WallpaperColors(@NonNull Map<Integer, Integer> populationByColor,
|
||||
public WallpaperColors(@NonNull Map<Integer, Integer> colorToPopulation,
|
||||
@ColorsHints int colorHints) {
|
||||
mAllColors = populationByColor;
|
||||
mAllColors = colorToPopulation;
|
||||
|
||||
ArrayList<Map.Entry<Integer, Integer>> mapEntries = new ArrayList(
|
||||
populationByColor.entrySet());
|
||||
mapEntries.sort((a, b) ->
|
||||
a.getValue().compareTo(b.getValue())
|
||||
);
|
||||
mMainColors = mapEntries.stream().map(entry -> Color.valueOf(entry.getKey())).collect(
|
||||
Collectors.toList());
|
||||
final Map<Integer, Cam> colorToCam = new HashMap<>();
|
||||
for (int color : colorToPopulation.keySet()) {
|
||||
colorToCam.put(color, Cam.fromInt(color));
|
||||
}
|
||||
final double[] hueProportions = hueProportions(colorToCam, colorToPopulation);
|
||||
final Map<Integer, Double> colorToHueProportion = colorToHueProportion(
|
||||
colorToPopulation.keySet(), colorToCam, hueProportions);
|
||||
|
||||
final Map<Integer, Double> colorToScore = new HashMap<>();
|
||||
for (Map.Entry<Integer, Double> mapEntry : colorToHueProportion.entrySet()) {
|
||||
int color = mapEntry.getKey();
|
||||
double proportion = mapEntry.getValue();
|
||||
double score = score(colorToCam.get(color), proportion);
|
||||
colorToScore.put(color, score);
|
||||
}
|
||||
ArrayList<Map.Entry<Integer, Double>> mapEntries = new ArrayList(colorToScore.entrySet());
|
||||
mapEntries.sort((a, b) -> b.getValue().compareTo(a.getValue()));
|
||||
|
||||
List<Integer> colorsByScoreDescending = new ArrayList<>();
|
||||
for (Map.Entry<Integer, Double> colorToScoreEntry : mapEntries) {
|
||||
colorsByScoreDescending.add(colorToScoreEntry.getKey());
|
||||
}
|
||||
|
||||
List<Integer> mainColorInts = new ArrayList<>();
|
||||
findSeedColorLoop:
|
||||
for (int color : colorsByScoreDescending) {
|
||||
Cam cam = colorToCam.get(color);
|
||||
for (int otherColor : mainColorInts) {
|
||||
Cam otherCam = colorToCam.get(otherColor);
|
||||
if (hueDiff(cam, otherCam) < 15) {
|
||||
continue findSeedColorLoop;
|
||||
}
|
||||
}
|
||||
mainColorInts.add(color);
|
||||
}
|
||||
List<Color> mainColors = new ArrayList<>();
|
||||
for (int colorInt : mainColorInts) {
|
||||
mainColors.add(Color.valueOf(colorInt));
|
||||
}
|
||||
mMainColors = mainColors;
|
||||
mColorHints = colorHints;
|
||||
}
|
||||
|
||||
private static double hueDiff(Cam a, Cam b) {
|
||||
return (180f - Math.abs(Math.abs(a.getHue() - b.getHue()) - 180f));
|
||||
}
|
||||
|
||||
private static double score(Cam cam, double proportion) {
|
||||
return cam.getChroma() + (proportion * 100);
|
||||
}
|
||||
|
||||
private static Map<Integer, Double> colorToHueProportion(Set<Integer> colors,
|
||||
Map<Integer, Cam> colorToCam, double[] hueProportions) {
|
||||
Map<Integer, Double> colorToHueProportion = new HashMap<>();
|
||||
for (int color : colors) {
|
||||
final int hue = wrapDegrees(Math.round(colorToCam.get(color).getHue()));
|
||||
double proportion = 0.0;
|
||||
for (int i = hue - 15; i < hue + 15; i++) {
|
||||
proportion += hueProportions[wrapDegrees(i)];
|
||||
}
|
||||
colorToHueProportion.put(color, proportion);
|
||||
}
|
||||
return colorToHueProportion;
|
||||
}
|
||||
|
||||
private static int wrapDegrees(int degrees) {
|
||||
if (degrees < 0) {
|
||||
return (degrees % 360) + 360;
|
||||
} else if (degrees >= 360) {
|
||||
return degrees % 360;
|
||||
} else {
|
||||
return degrees;
|
||||
}
|
||||
}
|
||||
|
||||
private static double[] hueProportions(@NonNull Map<Integer, Cam> colorToCam,
|
||||
Map<Integer, Integer> colorToPopulation) {
|
||||
final double[] proportions = new double[360];
|
||||
|
||||
double totalPopulation = 0;
|
||||
for (Map.Entry<Integer, Integer> entry : colorToPopulation.entrySet()) {
|
||||
totalPopulation += entry.getValue();
|
||||
}
|
||||
|
||||
for (Map.Entry<Integer, Integer> entry : colorToPopulation.entrySet()) {
|
||||
final int color = (int) entry.getKey();
|
||||
final int population = colorToPopulation.get(color);
|
||||
final Cam cam = colorToCam.get(color);
|
||||
final int hue = wrapDegrees(Math.round(cam.getHue()));
|
||||
proportions[hue] = proportions[hue] + ((double) population / totalPopulation);
|
||||
}
|
||||
|
||||
return proportions;
|
||||
}
|
||||
|
||||
public static final @android.annotation.NonNull Creator<WallpaperColors> CREATOR = new Creator<WallpaperColors>() {
|
||||
@Override
|
||||
public WallpaperColors createFromParcel(Parcel in) {
|
||||
|
||||
@@ -19,26 +19,32 @@ package com.android.internal.graphics.palette;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An implementation of Celebi's WSM quantizer, or, a Kmeans quantizer that starts with centroids
|
||||
* from a Wu quantizer to ensure 100% reproducible and quality results, and has some optimizations
|
||||
* to the Kmeans algorithm.
|
||||
*
|
||||
* An implementation of Celebi's quantization method.
|
||||
* See Celebi 2011, “Improving the Performance of K-Means for Color Quantization”
|
||||
*
|
||||
* First, Wu's quantizer runs. The results are used as starting points for a subsequent Kmeans
|
||||
* run. Using Wu's quantizer ensures 100% reproducible quantization results, because the starting
|
||||
* centroids are always the same. It also ensures high quality results, Wu is a box-cutting
|
||||
* quantization algorithm, much like medican color cut. It minimizes variance, much like Kmeans.
|
||||
* Wu is shown to be the highest quality box-cutting quantization algorithm.
|
||||
*
|
||||
* Second, a Kmeans quantizer tweaked for performance is run. Celebi calls this a weighted
|
||||
* square means quantizer, or WSMeans. Optimizations include operating on a map of image pixels
|
||||
* rather than all image pixels, and avoiding excess color distance calculations by using a
|
||||
* matrix and geometrical properties to know when there won't be any cluster closer to a pixel.
|
||||
*/
|
||||
public class CelebiQuantizer implements Quantizer {
|
||||
private List<Palette.Swatch> mSwatches;
|
||||
|
||||
public CelebiQuantizer() { }
|
||||
public CelebiQuantizer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void quantize(int[] pixels, int maxColors) {
|
||||
WuQuantizer wu = new WuQuantizer(pixels, maxColors);
|
||||
WuQuantizer wu = new WuQuantizer();
|
||||
wu.quantize(pixels, maxColors);
|
||||
List<Palette.Swatch> wuSwatches = wu.getQuantizedColors();
|
||||
LABCentroid labCentroidProvider = new LABCentroid();
|
||||
WSMeansQuantizer kmeans =
|
||||
new WSMeansQuantizer(WSMeansQuantizer.createStartingCentroids(labCentroidProvider,
|
||||
wuSwatches), labCentroidProvider, pixels, maxColors);
|
||||
WSMeansQuantizer kmeans = new WSMeansQuantizer(wu.getColors(), new LABPointProvider(),
|
||||
wu.inputPixelToCount());
|
||||
kmeans.quantize(pixels, maxColors);
|
||||
mSwatches = kmeans.getQuantizedColors();
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@ import android.graphics.ColorSpace;
|
||||
* in L*a*b* space, also known as deltaE, is a universally accepted standard across industries
|
||||
* and worldwide.
|
||||
*/
|
||||
public class LABCentroid implements CentroidProvider {
|
||||
public class LABPointProvider implements PointProvider {
|
||||
final ColorSpace.Connector mRgbToLab;
|
||||
final ColorSpace.Connector mLabToRgb;
|
||||
|
||||
public LABCentroid() {
|
||||
public LABPointProvider() {
|
||||
mRgbToLab = ColorSpace.connect(
|
||||
ColorSpace.get(ColorSpace.Named.SRGB),
|
||||
ColorSpace.get(ColorSpace.Named.CIE_LAB));
|
||||
@@ -39,7 +39,7 @@ public class LABCentroid implements CentroidProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] getCentroid(int color) {
|
||||
public float[] fromInt(int color) {
|
||||
float r = Color.red(color) / 255.f;
|
||||
float g = Color.green(color) / 255.f;
|
||||
float b = Color.blue(color) / 255.f;
|
||||
@@ -49,7 +49,7 @@ public class LABCentroid implements CentroidProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColor(float[] centroid) {
|
||||
public int toInt(float[] centroid) {
|
||||
float[] rgb = mLabToRgb.transform(centroid);
|
||||
int color = Color.rgb(rgb[0], rgb[1], rgb[2]);
|
||||
return color;
|
||||
@@ -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;
|
||||
|
||||
interface CentroidProvider {
|
||||
/**
|
||||
* @return 3 dimensions representing the color
|
||||
*/
|
||||
float[] getCentroid(@ColorInt int color);
|
||||
/**
|
||||
* Interface that allows quantizers to have a plug-and-play interface for experimenting with
|
||||
* quantization in different color spaces.
|
||||
*/
|
||||
public interface PointProvider {
|
||||
/** Convert a color to 3 coordinates representing the color in a color space. */
|
||||
float[] fromInt(@ColorInt int argb);
|
||||
|
||||
/**
|
||||
* @param centroid 3 dimensions representing the color
|
||||
* @return 32-bit ARGB representation
|
||||
*/
|
||||
/** Convert 3 coordinates in the color space into a color */
|
||||
@ColorInt
|
||||
int getColor(float[] centroid);
|
||||
int toInt(float[] point);
|
||||
|
||||
/**
|
||||
* Distance between two centroids.
|
||||
*/
|
||||
/** Find the distance between two colosrin the color space */
|
||||
float distance(float[] a, float[] b);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
/**
|
||||
* A color quantizer based on the Kmeans algorithm.
|
||||
* A color quantizer based on the Kmeans algorithm. Prefer using QuantizerCelebi.
|
||||
*
|
||||
* This is an implementation of Kmeans based on Celebi's 2011 paper,
|
||||
* "Improving the Performance of K-Means for Color Quantization". In the paper, this algorithm is
|
||||
@@ -36,253 +38,237 @@ import java.util.Set;
|
||||
* well as indexing colors by their count, thus minimizing the number of points to move around.
|
||||
*
|
||||
* Celebi's paper also stabilizes results and guarantees high quality by using starting centroids
|
||||
* from Wu's quantization algorithm. See CelebiQuantizer for more info.
|
||||
* from Wu's quantization algorithm. See QuantizerCelebi for more info.
|
||||
*/
|
||||
public class WSMeansQuantizer implements Quantizer {
|
||||
Mean[] mMeans;
|
||||
private final Map<Integer, Integer> mCountByColor = new HashMap<>();
|
||||
private final Map<Integer, Integer> mMeanIndexByColor = new HashMap<>();
|
||||
private final Set<Integer> mUniqueColors = new HashSet<>();
|
||||
private final List<Palette.Swatch> mSwatches = new ArrayList<>();
|
||||
private final CentroidProvider mCentroidProvider;
|
||||
public final class WSMeansQuantizer implements Quantizer {
|
||||
private static final String TAG = "QuantizerWsmeans";
|
||||
private static final boolean DEBUG = false;
|
||||
private static final int MAX_ITERATIONS = 10;
|
||||
// Points won't be moved to a closer cluster, if the closer cluster is within
|
||||
// this distance. 3.0 used because L*a*b* delta E < 3 is considered imperceptible.
|
||||
private static final float MIN_MOVEMENT_DISTANCE = 3.0f;
|
||||
|
||||
public WSMeansQuantizer(
|
||||
float[][] means, CentroidProvider centroidProvider, int[] pixels, int maxColors) {
|
||||
if (pixels == null) {
|
||||
pixels = new int[]{};
|
||||
}
|
||||
mCentroidProvider = centroidProvider;
|
||||
mMeans = new Mean[maxColors];
|
||||
for (int i = 0; i < means.length; i++) {
|
||||
mMeans[i] = new Mean(means[i]);
|
||||
private final PointProvider mPointProvider;
|
||||
private @Nullable Map<Integer, Integer> mInputPixelToCount;
|
||||
private float[][] mClusters;
|
||||
private int[] mClusterPopulations;
|
||||
private float[][] mPoints;
|
||||
private int[] mPixels;
|
||||
private int[] mClusterIndices;
|
||||
private int[][] mIndexMatrix = {};
|
||||
private float[][] mDistanceMatrix = {};
|
||||
|
||||
private Palette mPalette;
|
||||
|
||||
public WSMeansQuantizer(int[] inClusters, PointProvider pointProvider,
|
||||
@Nullable Map<Integer, Integer> inputPixelToCount) {
|
||||
mPointProvider = pointProvider;
|
||||
|
||||
mClusters = new float[inClusters.length][3];
|
||||
int index = 0;
|
||||
for (int cluster : inClusters) {
|
||||
float[] point = pointProvider.fromInt(cluster);
|
||||
mClusters[index++] = point;
|
||||
}
|
||||
|
||||
if (maxColors > means.length) {
|
||||
// Always initialize Random with the same seed. Ensures the results of quantization
|
||||
// are consistent, even when random centroids are required.
|
||||
Random random = new Random(0x42688);
|
||||
int randomMeansToCreate = maxColors - means.length;
|
||||
for (int i = 0; i < randomMeansToCreate; i++) {
|
||||
mMeans[means.length + i] = new Mean(100, random);
|
||||
}
|
||||
}
|
||||
|
||||
for (int pixel : pixels) {
|
||||
// These are pixels from the bitmap that is being quantized.
|
||||
// Depending on the bitmap & downscaling, it may have pixels that are less than opaque
|
||||
// Ignore those pixels.
|
||||
///
|
||||
// Note: they don't _have_ to be ignored, for example, we could instead turn them
|
||||
// opaque. Traditionally, including outside Android, quantizers ignore transparent
|
||||
// pixels, so that strategy was chosen.
|
||||
int alpha = (pixel >> 24) & 0xff;
|
||||
if (alpha < 255) {
|
||||
continue;
|
||||
}
|
||||
Integer currentCount = mCountByColor.get(pixel);
|
||||
if (currentCount == null) {
|
||||
currentCount = 0;
|
||||
mUniqueColors.add(pixel);
|
||||
}
|
||||
mCountByColor.put(pixel, currentCount + 1);
|
||||
}
|
||||
for (int color : mUniqueColors) {
|
||||
int closestMeanIndex = -1;
|
||||
double closestMeanDistance = -1;
|
||||
float[] centroid = mCentroidProvider.getCentroid(color);
|
||||
for (int i = 0; i < mMeans.length; i++) {
|
||||
double distance = mCentroidProvider.distance(centroid, mMeans[i].center);
|
||||
if (closestMeanIndex == -1 || distance < closestMeanDistance) {
|
||||
closestMeanIndex = i;
|
||||
closestMeanDistance = distance;
|
||||
}
|
||||
}
|
||||
mMeanIndexByColor.put(color, closestMeanIndex);
|
||||
}
|
||||
|
||||
if (pixels.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
predict(maxColors, 0);
|
||||
}
|
||||
|
||||
/** Create starting centroids for K-means from a set of colors. */
|
||||
public static float[][] createStartingCentroids(CentroidProvider centroidProvider,
|
||||
List<Palette.Swatch> swatches) {
|
||||
float[][] startingCentroids = new float[swatches.size()][];
|
||||
for (int i = 0; i < swatches.size(); i++) {
|
||||
startingCentroids[i] = centroidProvider.getCentroid(swatches.get(i).getInt());
|
||||
}
|
||||
return startingCentroids;
|
||||
}
|
||||
|
||||
/** Create random starting centroids for K-means. */
|
||||
public static float[][] randomMeans(int maxColors, int upperBound) {
|
||||
float[][] means = new float[maxColors][];
|
||||
|
||||
// Always initialize Random with the same seed. Ensures the results of quantization
|
||||
// are consistent, even when random centroids are required.
|
||||
Random random = new Random(0x42688);
|
||||
for (int i = 0; i < maxColors; i++) {
|
||||
means[i] = new Mean(upperBound, random).center;
|
||||
}
|
||||
return means;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void quantize(int[] pixels, int maxColors) {
|
||||
|
||||
mInputPixelToCount = inputPixelToCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Palette.Swatch> getQuantizedColors() {
|
||||
return mSwatches;
|
||||
return mPalette.getSwatches();
|
||||
}
|
||||
|
||||
private void predict(int maxColors, int iterationsCompleted) {
|
||||
double[][] centroidDistance = new double[maxColors][maxColors];
|
||||
for (int i = 0; i <= maxColors; i++) {
|
||||
for (int j = i + 1; j < maxColors; j++) {
|
||||
float[] meanI = mMeans[i].center;
|
||||
float[] meanJ = mMeans[j].center;
|
||||
double distance = mCentroidProvider.distance(meanI, meanJ);
|
||||
centroidDistance[i][j] = distance;
|
||||
centroidDistance[j][i] = distance;
|
||||
@Override
|
||||
public void quantize(@NonNull int[] pixels, int maxColors) {
|
||||
assert (pixels.length > 0);
|
||||
|
||||
if (mInputPixelToCount == null) {
|
||||
QuantizerMap mapQuantizer = new QuantizerMap();
|
||||
mapQuantizer.quantize(pixels, maxColors);
|
||||
mInputPixelToCount = mapQuantizer.getColorToCount();
|
||||
}
|
||||
|
||||
mPoints = new float[mInputPixelToCount.size()][3];
|
||||
mPixels = new int[mInputPixelToCount.size()];
|
||||
int index = 0;
|
||||
for (int pixel : mInputPixelToCount.keySet()) {
|
||||
mPixels[index] = pixel;
|
||||
mPoints[index] = mPointProvider.fromInt(pixel);
|
||||
index++;
|
||||
}
|
||||
if (mClusters.length > 0) {
|
||||
// This implies that the constructor was provided starting clusters. If that was the
|
||||
// case, we limit the number of clusters to the number of starting clusters and don't
|
||||
// initialize random clusters.
|
||||
maxColors = Math.min(maxColors, mClusters.length);
|
||||
}
|
||||
maxColors = Math.min(maxColors, mPoints.length);
|
||||
|
||||
initializeClusters(maxColors);
|
||||
for (int i = 0; i < MAX_ITERATIONS; i++) {
|
||||
calculateClusterDistances(maxColors);
|
||||
if (!reassignPoints(maxColors)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Construct a K×K matrix M in which row i is a permutation of
|
||||
// 1,2,…,K that represents the clusters in increasing order of
|
||||
// distance of their centers from ci;
|
||||
int[][] distanceMatrix = new int[maxColors][maxColors];
|
||||
for (int i = 0; i < maxColors; i++) {
|
||||
double[] distancesFromIToAnotherMean = centroidDistance[i];
|
||||
double[] sortedByDistanceAscending = distancesFromIToAnotherMean.clone();
|
||||
Arrays.sort(sortedByDistanceAscending);
|
||||
int[] outputRow = new int[maxColors];
|
||||
for (int j = 0; j < maxColors; j++) {
|
||||
outputRow[j] = findIndex(distancesFromIToAnotherMean, sortedByDistanceAscending[j]);
|
||||
}
|
||||
distanceMatrix[i] = outputRow;
|
||||
}
|
||||
|
||||
// for (i=1;i≤N′;i=i+ 1) do
|
||||
// Let Sp be the cluster that xi was assigned to in the previous
|
||||
// iteration;
|
||||
// p=m[i];
|
||||
// min_dist=prev_dist=jjxi−cpjj2;
|
||||
boolean anyColorMoved = false;
|
||||
for (int intColor : mUniqueColors) {
|
||||
float[] color = mCentroidProvider.getCentroid(intColor);
|
||||
int indexOfCurrentMean = mMeanIndexByColor.get(intColor);
|
||||
Mean currentMean = mMeans[indexOfCurrentMean];
|
||||
double minDistance = mCentroidProvider.distance(color, currentMean.center);
|
||||
for (int j = 1; j < maxColors; j++) {
|
||||
int indexOfClusterFromCurrentToJ = distanceMatrix[indexOfCurrentMean][j];
|
||||
double distanceBetweenJAndCurrent =
|
||||
centroidDistance[indexOfCurrentMean][indexOfClusterFromCurrentToJ];
|
||||
if (distanceBetweenJAndCurrent >= (4 * minDistance)) {
|
||||
break;
|
||||
}
|
||||
double distanceBetweenJAndColor = mCentroidProvider.distance(mMeans[j].center,
|
||||
color);
|
||||
if (distanceBetweenJAndColor < minDistance) {
|
||||
minDistance = distanceBetweenJAndColor;
|
||||
mMeanIndexByColor.remove(intColor);
|
||||
mMeanIndexByColor.put(intColor, j);
|
||||
anyColorMoved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<MeanBucket> buckets = new ArrayList<>();
|
||||
for (int i = 0; i < maxColors; i++) {
|
||||
buckets.add(new MeanBucket());
|
||||
}
|
||||
|
||||
for (int intColor : mUniqueColors) {
|
||||
int meanIndex = mMeanIndexByColor.get(intColor);
|
||||
MeanBucket meanBucket = buckets.get(meanIndex);
|
||||
meanBucket.add(mCentroidProvider.getCentroid(intColor), intColor,
|
||||
mCountByColor.get(intColor));
|
||||
recalculateClusterCenters(maxColors);
|
||||
}
|
||||
|
||||
List<Palette.Swatch> swatches = new ArrayList<>();
|
||||
boolean done = !anyColorMoved && iterationsCompleted > 0 || iterationsCompleted >= 100;
|
||||
if (done) {
|
||||
for (int i = 0; i < buckets.size(); i++) {
|
||||
MeanBucket a = buckets.get(i);
|
||||
if (a.mCount <= 0) {
|
||||
continue;
|
||||
}
|
||||
List<MeanBucket> bucketsToMerge = new ArrayList<>();
|
||||
for (int j = i + 1; j < buckets.size(); j++) {
|
||||
MeanBucket b = buckets.get(j);
|
||||
if (b.mCount == 0) {
|
||||
continue;
|
||||
}
|
||||
float[] bCentroid = b.getCentroid();
|
||||
assert (a.mCount > 0);
|
||||
assert (a.getCentroid() != null);
|
||||
|
||||
assert (bCentroid != null);
|
||||
if (mCentroidProvider.distance(a.getCentroid(), b.getCentroid()) < 5) {
|
||||
bucketsToMerge.add(b);
|
||||
}
|
||||
}
|
||||
|
||||
for (MeanBucket bucketToMerge : bucketsToMerge) {
|
||||
float[] centroid = bucketToMerge.getCentroid();
|
||||
a.add(centroid, mCentroidProvider.getColor(centroid), bucketToMerge.mCount);
|
||||
buckets.remove(bucketToMerge);
|
||||
}
|
||||
}
|
||||
|
||||
for (MeanBucket bucket : buckets) {
|
||||
float[] centroid = bucket.getCentroid();
|
||||
if (centroid == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int rgb = mCentroidProvider.getColor(centroid);
|
||||
swatches.add(new Palette.Swatch(rgb, bucket.mCount));
|
||||
mSwatches.clear();
|
||||
mSwatches.addAll(swatches);
|
||||
}
|
||||
} else {
|
||||
List<MeanBucket> emptyBuckets = new ArrayList<>();
|
||||
for (int i = 0; i < buckets.size(); i++) {
|
||||
MeanBucket bucket = buckets.get(i);
|
||||
if ((bucket.getCentroid() == null) || (bucket.mCount == 0)) {
|
||||
emptyBuckets.add(bucket);
|
||||
for (Integer color : mUniqueColors) {
|
||||
int meanIndex = mMeanIndexByColor.get(color);
|
||||
if (meanIndex > i) {
|
||||
mMeanIndexByColor.put(color, meanIndex--);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Mean[] newMeans = new Mean[buckets.size()];
|
||||
for (int i = 0; i < buckets.size(); i++) {
|
||||
float[] centroid = buckets.get(i).getCentroid();
|
||||
newMeans[i] = new Mean(centroid);
|
||||
}
|
||||
|
||||
predict(buckets.size(), iterationsCompleted + 1);
|
||||
for (int i = 0; i < maxColors; i++) {
|
||||
float[] cluster = mClusters[i];
|
||||
int colorInt = mPointProvider.toInt(cluster);
|
||||
swatches.add(new Palette.Swatch(colorInt, mClusterPopulations[i]));
|
||||
}
|
||||
|
||||
mPalette = Palette.from(swatches);
|
||||
}
|
||||
|
||||
private static int findIndex(double[] list, double element) {
|
||||
for (int i = 0; i < list.length; i++) {
|
||||
if (list[i] == element) {
|
||||
return i;
|
||||
|
||||
private void initializeClusters(int maxColors) {
|
||||
boolean hadInputClusters = mClusters.length > 0;
|
||||
if (!hadInputClusters) {
|
||||
int additionalClustersNeeded = maxColors - mClusters.length;
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "have " + mClusters.length + " clusters, want " + maxColors
|
||||
+ " results, so need " + additionalClustersNeeded + " additional clusters");
|
||||
}
|
||||
|
||||
Random random = new Random(0x42688);
|
||||
List<float[]> additionalClusters = new ArrayList<>(additionalClustersNeeded);
|
||||
Set<Integer> clusterIndicesUsed = new HashSet<>();
|
||||
for (int i = 0; i < additionalClustersNeeded; i++) {
|
||||
int index = random.nextInt(mPoints.length);
|
||||
while (clusterIndicesUsed.contains(index)
|
||||
&& clusterIndicesUsed.size() < mPoints.length) {
|
||||
index = random.nextInt(mPoints.length);
|
||||
}
|
||||
clusterIndicesUsed.add(index);
|
||||
additionalClusters.add(mPoints[index]);
|
||||
}
|
||||
|
||||
float[][] newClusters = (float[][]) additionalClusters.toArray();
|
||||
float[][] clusters = Arrays.copyOf(mClusters, maxColors);
|
||||
System.arraycopy(newClusters, 0, clusters, clusters.length, newClusters.length);
|
||||
mClusters = clusters;
|
||||
}
|
||||
|
||||
mClusterIndices = new int[mPixels.length];
|
||||
mClusterPopulations = new int[mPixels.length];
|
||||
Random random = new Random(0x42688);
|
||||
for (int i = 0; i < mPixels.length; i++) {
|
||||
int clusterIndex = random.nextInt(maxColors);
|
||||
mClusterIndices[i] = clusterIndex;
|
||||
mClusterPopulations[i] = mInputPixelToCount.get(mPixels[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void calculateClusterDistances(int maxColors) {
|
||||
if (mDistanceMatrix.length != maxColors) {
|
||||
mDistanceMatrix = new float[maxColors][maxColors];
|
||||
}
|
||||
|
||||
for (int i = 0; i <= maxColors; i++) {
|
||||
for (int j = i + 1; j < maxColors; j++) {
|
||||
float distance = mPointProvider.distance(mClusters[i], mClusters[j]);
|
||||
mDistanceMatrix[j][i] = distance;
|
||||
mDistanceMatrix[i][j] = distance;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Element not in list");
|
||||
|
||||
if (mIndexMatrix.length != maxColors) {
|
||||
mIndexMatrix = new int[maxColors][maxColors];
|
||||
}
|
||||
|
||||
for (int i = 0; i < maxColors; i++) {
|
||||
ArrayList<Distance> distances = new ArrayList<>(maxColors);
|
||||
for (int index = 0; index < maxColors; index++) {
|
||||
distances.add(new Distance(index, mDistanceMatrix[i][index]));
|
||||
}
|
||||
distances.sort(
|
||||
(a, b) -> Float.compare(a.getDistance(), b.getDistance()));
|
||||
|
||||
for (int j = 0; j < maxColors; j++) {
|
||||
mIndexMatrix[i][j] = distances.get(j).getIndex();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean reassignPoints(int maxColors) {
|
||||
boolean colorMoved = false;
|
||||
for (int i = 0; i < mPoints.length; i++) {
|
||||
float[] point = mPoints[i];
|
||||
int previousClusterIndex = mClusterIndices[i];
|
||||
float[] previousCluster = mClusters[previousClusterIndex];
|
||||
float previousDistance = mPointProvider.distance(point, previousCluster);
|
||||
|
||||
float minimumDistance = previousDistance;
|
||||
int newClusterIndex = -1;
|
||||
for (int j = 1; j < maxColors; j++) {
|
||||
int t = mIndexMatrix[previousClusterIndex][j];
|
||||
if (mDistanceMatrix[previousClusterIndex][t] >= 4 * previousDistance) {
|
||||
// Triangle inequality proves there's can be no closer center.
|
||||
break;
|
||||
}
|
||||
float distance = mPointProvider.distance(point, mClusters[t]);
|
||||
if (distance < minimumDistance) {
|
||||
minimumDistance = distance;
|
||||
newClusterIndex = t;
|
||||
}
|
||||
}
|
||||
if (newClusterIndex != -1) {
|
||||
float distanceChange = (float)
|
||||
Math.abs((Math.sqrt(minimumDistance) - Math.sqrt(previousDistance)));
|
||||
if (distanceChange > MIN_MOVEMENT_DISTANCE) {
|
||||
colorMoved = true;
|
||||
mClusterIndices[i] = newClusterIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
return colorMoved;
|
||||
}
|
||||
|
||||
void recalculateClusterCenters(int maxColors) {
|
||||
mClusterPopulations = new int[maxColors];
|
||||
float[] aSums = new float[maxColors];
|
||||
float[] bSums = new float[maxColors];
|
||||
float[] cSums = new float[maxColors];
|
||||
for (int i = 0; i < mPoints.length; i++) {
|
||||
int clusterIndex = mClusterIndices[i];
|
||||
float[] point = mPoints[i];
|
||||
int pixel = mPixels[i];
|
||||
int count = mInputPixelToCount.get(pixel);
|
||||
mClusterPopulations[clusterIndex] += count;
|
||||
aSums[clusterIndex] += point[0] * count;
|
||||
bSums[clusterIndex] += point[1] * count;
|
||||
cSums[clusterIndex] += point[2] * count;
|
||||
|
||||
}
|
||||
for (int i = 0; i < maxColors; i++) {
|
||||
int count = mClusterPopulations[i];
|
||||
float aSum = aSums[i];
|
||||
float bSum = bSums[i];
|
||||
float cSum = cSums[i];
|
||||
mClusters[i][0] = aSum / count;
|
||||
mClusters[i][1] = bSum / count;
|
||||
mClusters[i][2] = cSum / count;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Distance {
|
||||
private final int mIndex;
|
||||
private final float mDistance;
|
||||
|
||||
int getIndex() {
|
||||
return mIndex;
|
||||
}
|
||||
|
||||
float getDistance() {
|
||||
return mDistance;
|
||||
}
|
||||
|
||||
Distance(int index, float distance) {
|
||||
mIndex = index;
|
||||
mDistance = distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,431 +16,447 @@
|
||||
|
||||
package com.android.internal.graphics.palette;
|
||||
|
||||
import static java.lang.System.arraycopy;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.graphics.Color;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
// All reference Wu implementations are based on the original C code by Wu.
|
||||
// Comments on methods are the same as in the original implementation, and the comment below
|
||||
// is the original class header.
|
||||
|
||||
/**
|
||||
* Wu's Color Quantizer (v. 2) (see Graphics Gems vol. II, pp. 126-133) Author: Xiaolin Wu
|
||||
* Wu's quantization algorithm is a box-cut quantizer that minimizes variance. It takes longer to
|
||||
* run than, say, median color cut, but provides the highest quality results currently known.
|
||||
*
|
||||
* <p>Algorithm: Greedy orthogonal bipartition of RGB space for variance minimization aided by
|
||||
* inclusion-exclusion tricks. For speed no nearest neighbor search is done. Slightly better
|
||||
* performance can be expected by more sophisticated but more expensive versions.
|
||||
* Prefer `QuantizerCelebi`: coupled with Kmeans, this provides the best-known results for image
|
||||
* quantization.
|
||||
*
|
||||
* Seemingly all Wu implementations are based off of one C code snippet that cites a book from 1992
|
||||
* Graphics Gems vol. II, pp. 126-133. As a result, it is very hard to understand the mechanics of
|
||||
* the algorithm, beyond the commentary provided in the C code. Comments on the methods of this
|
||||
* class are avoided in favor of finding another implementation and reading the commentary there,
|
||||
* avoiding perpetuating the same incomplete and somewhat confusing commentary here.
|
||||
*/
|
||||
public class WuQuantizer implements Quantizer {
|
||||
private static final int MAX_COLORS = 256;
|
||||
private static final int RED = 2;
|
||||
private static final int GREEN = 1;
|
||||
private static final int BLUE = 0;
|
||||
public final class WuQuantizer implements Quantizer {
|
||||
// A histogram of all the input colors is constructed. It has the shape of a
|
||||
// cube. The cube would be too large if it contained all 16 million colors:
|
||||
// historical best practice is to use 5 bits of the 8 in each channel,
|
||||
// reducing the histogram to a volume of ~32,000.
|
||||
private static final int BITS = 5;
|
||||
private static final int MAX_INDEX = 32;
|
||||
private static final int SIDE_LENGTH = 33;
|
||||
private static final int TOTAL_SIZE = 35937;
|
||||
|
||||
private static final int QUANT_SIZE = 33;
|
||||
private final List<Palette.Swatch> mSwatches = new ArrayList<>();
|
||||
private int[] mWeights;
|
||||
private int[] mMomentsR;
|
||||
private int[] mMomentsG;
|
||||
private int[] mMomentsB;
|
||||
private double[] mMoments;
|
||||
private Box[] mCubes;
|
||||
private Palette mPalette;
|
||||
private int[] mColors;
|
||||
private Map<Integer, Integer> mInputPixelToCount;
|
||||
|
||||
@Override
|
||||
public List<Palette.Swatch> getQuantizedColors() {
|
||||
return mSwatches;
|
||||
}
|
||||
|
||||
private static final class Box {
|
||||
int mR0; /* min value, exclusive */
|
||||
int mR1; /* max value, inclusive */
|
||||
int mG0;
|
||||
int mG1;
|
||||
int mB0;
|
||||
int mB1;
|
||||
int mVol;
|
||||
}
|
||||
|
||||
private final int mSize; /* image size, in bytes. */
|
||||
private int mMaxColors;
|
||||
private int[] mQadd;
|
||||
private final int[] mPixels;
|
||||
|
||||
private final double[][][] mM2 = new double[QUANT_SIZE][QUANT_SIZE][QUANT_SIZE];
|
||||
private final long[][][] mWt = new long[QUANT_SIZE][QUANT_SIZE][QUANT_SIZE];
|
||||
private final long[][][] mMr = new long[QUANT_SIZE][QUANT_SIZE][QUANT_SIZE];
|
||||
private final long[][][] mMg = new long[QUANT_SIZE][QUANT_SIZE][QUANT_SIZE];
|
||||
private final long[][][] mMb = new long[QUANT_SIZE][QUANT_SIZE][QUANT_SIZE];
|
||||
|
||||
public WuQuantizer(int[] pixels, int maxColorCount) {
|
||||
if (pixels == null) {
|
||||
pixels = new int[]{};
|
||||
}
|
||||
this.mPixels = pixels;
|
||||
this.mSize = pixels.length;
|
||||
return mPalette.getSwatches();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void quantize(int[] colors, int maxColorCount) {
|
||||
// All of the sample Wu implementations are reimplementations of a snippet of C code from
|
||||
// the early 90s. They all cap the maximum # of colors at 256, and it is impossible to tell
|
||||
// if this is a requirement, a consequence of QUANT_SIZE, or arbitrary.
|
||||
//
|
||||
// Also, the number of maximum colors should be capped at the number of pixels - otherwise,
|
||||
// If extraction is run on a set of pixels whose count is less than max colors,
|
||||
// then colors.length < max colors, and accesses to colors[index] throw an
|
||||
// ArrayOutOfBoundsException.
|
||||
this.mMaxColors = Math.min(Math.min(MAX_COLORS, maxColorCount), colors.length);
|
||||
Box[] cube = new Box[mMaxColors];
|
||||
int red, green, blue;
|
||||
public void quantize(@NonNull int[] pixels, int colorCount) {
|
||||
assert (pixels.length > 0);
|
||||
|
||||
int next, i, k;
|
||||
long weight;
|
||||
double[] vv = new double[mMaxColors];
|
||||
double temp;
|
||||
|
||||
compute3DHistogram(mWt, mMr, mMg, mMb, mM2);
|
||||
computeMoments(mWt, mMr, mMg, mMb, mM2);
|
||||
|
||||
for (i = 0; i < mMaxColors; i++) {
|
||||
cube[i] = new Box();
|
||||
}
|
||||
|
||||
cube[0].mR0 = cube[0].mG0 = cube[0].mB0 = 0;
|
||||
cube[0].mR1 = cube[0].mG1 = cube[0].mB1 = QUANT_SIZE - 1;
|
||||
next = 0;
|
||||
|
||||
for (i = 1; i < mMaxColors; ++i) {
|
||||
if (cut(cube[next], cube[i])) {
|
||||
vv[next] = (cube[next].mVol > 1) ? getVariance(cube[next]) : 0.0f;
|
||||
vv[i] = (cube[i].mVol > 1) ? getVariance(cube[i]) : 0.0f;
|
||||
} else {
|
||||
vv[next] = 0.0f;
|
||||
i--;
|
||||
QuantizerMap quantizerMap = new QuantizerMap();
|
||||
quantizerMap.quantize(pixels, colorCount);
|
||||
mInputPixelToCount = quantizerMap.getColorToCount();
|
||||
// Extraction should not be run on using a color count higher than the number of colors
|
||||
// in the pixels. The algorithm doesn't expect that to be the case, unexpected results and
|
||||
// exceptions may occur.
|
||||
Set<Integer> uniqueColors = mInputPixelToCount.keySet();
|
||||
if (uniqueColors.size() <= colorCount) {
|
||||
mColors = new int[mInputPixelToCount.keySet().size()];
|
||||
int index = 0;
|
||||
for (int color : uniqueColors) {
|
||||
mColors[index++] = color;
|
||||
}
|
||||
next = 0;
|
||||
temp = vv[0];
|
||||
for (k = 1; k <= i; ++k) {
|
||||
if (vv[k] > temp) {
|
||||
temp = vv[k];
|
||||
next = k;
|
||||
}
|
||||
}
|
||||
if (temp <= 0.0f) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (k = 0; k < mMaxColors; ++k) {
|
||||
weight = getVolume(cube[k], mWt);
|
||||
if (weight > 0) {
|
||||
red = (int) (getVolume(cube[k], mMr) / weight);
|
||||
green = (int) (getVolume(cube[k], mMg) / weight);
|
||||
blue = (int) (getVolume(cube[k], mMb) / weight);
|
||||
colors[k] = (255 << 24) | (red << 16) | (green << 8) | blue;
|
||||
} else {
|
||||
colors[k] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int bitsPerPixel = 0;
|
||||
while ((1 << bitsPerPixel) < mMaxColors) {
|
||||
bitsPerPixel++;
|
||||
} else {
|
||||
constructHistogram(mInputPixelToCount);
|
||||
createMoments();
|
||||
CreateBoxesResult createBoxesResult = createBoxes(colorCount);
|
||||
mColors = createResult(createBoxesResult.mResultCount);
|
||||
}
|
||||
|
||||
List<Palette.Swatch> swatches = new ArrayList<>();
|
||||
for (int l = 0; l < k; l++) {
|
||||
int pixel = colors[l];
|
||||
if (pixel == 0) {
|
||||
continue;
|
||||
}
|
||||
swatches.add(new Palette.Swatch(pixel, 0));
|
||||
for (int color : mColors) {
|
||||
swatches.add(new Palette.Swatch(color, 0));
|
||||
}
|
||||
mSwatches.clear();
|
||||
mSwatches.addAll(swatches);
|
||||
mPalette = Palette.from(swatches);
|
||||
}
|
||||
|
||||
/* Histogram is in elements 1..HISTSIZE along each axis,
|
||||
* element 0 is for base or marginal value
|
||||
* NB: these must start out 0!
|
||||
*/
|
||||
private void compute3DHistogram(
|
||||
long[][][] vwt, long[][][] vmr, long[][][] vmg, long[][][] vmb, double[][][] m2) {
|
||||
// build 3-D color histogram of counts, r/g/b, and c^2
|
||||
int r, g, b;
|
||||
int i;
|
||||
int inr;
|
||||
int ing;
|
||||
int inb;
|
||||
int[] table = new int[256];
|
||||
@Nullable
|
||||
public int[] getColors() {
|
||||
return mColors;
|
||||
}
|
||||
|
||||
for (i = 0; i < 256; i++) {
|
||||
table[i] = i * i;
|
||||
}
|
||||
/** Keys are color ints, values are the number of pixels in the image matching that color int */
|
||||
@Nullable
|
||||
public Map<Integer, Integer> inputPixelToCount() {
|
||||
return mInputPixelToCount;
|
||||
}
|
||||
|
||||
mQadd = new int[mSize];
|
||||
private static int getIndex(int r, int g, int b) {
|
||||
return (r << 10) + (r << 6) + (g << 5) + r + g + b;
|
||||
}
|
||||
|
||||
for (i = 0; i < mSize; ++i) {
|
||||
int rgb = mPixels[i];
|
||||
// Skip less than opaque pixels. They're not meaningful in the context of palette
|
||||
// generation for UI schemes.
|
||||
if ((rgb >>> 24) < 0xff) {
|
||||
continue;
|
||||
}
|
||||
r = ((rgb >> 16) & 0xff);
|
||||
g = ((rgb >> 8) & 0xff);
|
||||
b = (rgb & 0xff);
|
||||
inr = (r >> 3) + 1;
|
||||
ing = (g >> 3) + 1;
|
||||
inb = (b >> 3) + 1;
|
||||
mQadd[i] = (inr << 10) + (inr << 6) + inr + (ing << 5) + ing + inb;
|
||||
/*[inr][ing][inb]*/
|
||||
++vwt[inr][ing][inb];
|
||||
vmr[inr][ing][inb] += r;
|
||||
vmg[inr][ing][inb] += g;
|
||||
vmb[inr][ing][inb] += b;
|
||||
m2[inr][ing][inb] += table[r] + table[g] + table[b];
|
||||
private void constructHistogram(Map<Integer, Integer> pixels) {
|
||||
mWeights = new int[TOTAL_SIZE];
|
||||
mMomentsR = new int[TOTAL_SIZE];
|
||||
mMomentsG = new int[TOTAL_SIZE];
|
||||
mMomentsB = new int[TOTAL_SIZE];
|
||||
mMoments = new double[TOTAL_SIZE];
|
||||
|
||||
for (Map.Entry<Integer, Integer> pair : pixels.entrySet()) {
|
||||
int pixel = pair.getKey();
|
||||
int count = pair.getValue();
|
||||
int red = Color.red(pixel);
|
||||
int green = Color.green(pixel);
|
||||
int blue = Color.blue(pixel);
|
||||
int bitsToRemove = 8 - BITS;
|
||||
int iR = (red >> bitsToRemove) + 1;
|
||||
int iG = (green >> bitsToRemove) + 1;
|
||||
int iB = (blue >> bitsToRemove) + 1;
|
||||
int index = getIndex(iR, iG, iB);
|
||||
mWeights[index] += count;
|
||||
mMomentsR[index] += (red * count);
|
||||
mMomentsG[index] += (green * count);
|
||||
mMomentsB[index] += (blue * count);
|
||||
mMoments[index] += (count * ((red * red) + (green * green) + (blue * blue)));
|
||||
}
|
||||
}
|
||||
|
||||
/* At conclusion of the histogram step, we can interpret
|
||||
* wt[r][g][b] = sum over voxel of P(c)
|
||||
* mr[r][g][b] = sum over voxel of r*P(c) , similarly for mg, mb
|
||||
* m2[r][g][b] = sum over voxel of c^2*P(c)
|
||||
* Actually each of these should be divided by 'size' to give the usual
|
||||
* interpretation of P() as ranging from 0 to 1, but we needn't do that here.
|
||||
*
|
||||
* We now convert histogram into moments so that we can rapidly calculate
|
||||
* the sums of the above quantities over any desired box.
|
||||
*/
|
||||
private void computeMoments(
|
||||
long[][][] vwt, long[][][] vmr, long[][][] vmg, long[][][] vmb, double[][][] m2) {
|
||||
/* compute cumulative moments. */
|
||||
int i, r, g, b;
|
||||
int line, line_r, line_g, line_b;
|
||||
int[] area = new int[QUANT_SIZE];
|
||||
int[] area_r = new int[QUANT_SIZE];
|
||||
int[] area_g = new int[QUANT_SIZE];
|
||||
int[] area_b = new int[QUANT_SIZE];
|
||||
double line2;
|
||||
double[] area2 = new double[QUANT_SIZE];
|
||||
private void createMoments() {
|
||||
for (int r = 1; r < SIDE_LENGTH; ++r) {
|
||||
int[] area = new int[SIDE_LENGTH];
|
||||
int[] areaR = new int[SIDE_LENGTH];
|
||||
int[] areaG = new int[SIDE_LENGTH];
|
||||
int[] areaB = new int[SIDE_LENGTH];
|
||||
double[] area2 = new double[SIDE_LENGTH];
|
||||
|
||||
for (r = 1; r < QUANT_SIZE; ++r) {
|
||||
for (i = 0; i < QUANT_SIZE; ++i) {
|
||||
area2[i] = area[i] = area_r[i] = area_g[i] = area_b[i] = 0;
|
||||
}
|
||||
for (g = 1; g < QUANT_SIZE; ++g) {
|
||||
line2 = line = line_r = line_g = line_b = 0;
|
||||
for (b = 1; b < QUANT_SIZE; ++b) {
|
||||
line += vwt[r][g][b];
|
||||
line_r += vmr[r][g][b];
|
||||
line_g += vmg[r][g][b];
|
||||
line_b += vmb[r][g][b];
|
||||
line2 += m2[r][g][b];
|
||||
for (int g = 1; g < SIDE_LENGTH; ++g) {
|
||||
int line = 0;
|
||||
int lineR = 0;
|
||||
int lineG = 0;
|
||||
int lineB = 0;
|
||||
|
||||
double line2 = 0.0;
|
||||
for (int b = 1; b < SIDE_LENGTH; ++b) {
|
||||
int index = getIndex(r, g, b);
|
||||
line += mWeights[index];
|
||||
lineR += mMomentsR[index];
|
||||
lineG += mMomentsG[index];
|
||||
lineB += mMomentsB[index];
|
||||
line2 += mMoments[index];
|
||||
|
||||
area[b] += line;
|
||||
area_r[b] += line_r;
|
||||
area_g[b] += line_g;
|
||||
area_b[b] += line_b;
|
||||
areaR[b] += lineR;
|
||||
areaG[b] += lineG;
|
||||
areaB[b] += lineB;
|
||||
area2[b] += line2;
|
||||
|
||||
vwt[r][g][b] = vwt[r - 1][g][b] + area[b];
|
||||
vmr[r][g][b] = vmr[r - 1][g][b] + area_r[b];
|
||||
vmg[r][g][b] = vmg[r - 1][g][b] + area_g[b];
|
||||
vmb[r][g][b] = vmb[r - 1][g][b] + area_b[b];
|
||||
m2[r][g][b] = m2[r - 1][g][b] + area2[b];
|
||||
int previousIndex = getIndex(r - 1, g, b);
|
||||
mWeights[index] = mWeights[previousIndex] + area[b];
|
||||
mMomentsR[index] = mMomentsR[previousIndex] + areaR[b];
|
||||
mMomentsG[index] = mMomentsG[previousIndex] + areaG[b];
|
||||
mMomentsB[index] = mMomentsB[previousIndex] + areaB[b];
|
||||
mMoments[index] = mMoments[previousIndex] + area2[b];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long getVolume(Box cube, long[][][] mmt) {
|
||||
/* Compute sum over a box of any given statistic */
|
||||
return (mmt[cube.mR1][cube.mG1][cube.mB1]
|
||||
- mmt[cube.mR1][cube.mG1][cube.mB0]
|
||||
- mmt[cube.mR1][cube.mG0][cube.mB1]
|
||||
+ mmt[cube.mR1][cube.mG0][cube.mB0]
|
||||
- mmt[cube.mR0][cube.mG1][cube.mB1]
|
||||
+ mmt[cube.mR0][cube.mG1][cube.mB0]
|
||||
+ mmt[cube.mR0][cube.mG0][cube.mB1]
|
||||
- mmt[cube.mR0][cube.mG0][cube.mB0]);
|
||||
}
|
||||
|
||||
/* The next two routines allow a slightly more efficient calculation
|
||||
* of Vol() for a proposed subbox of a given box. The sum of Top()
|
||||
* and Bottom() is the Vol() of a subbox split in the given direction
|
||||
* and with the specified new upper bound.
|
||||
*/
|
||||
private long getBottom(Box cube, int dir, long[][][] mmt) {
|
||||
/* Compute part of Vol(cube, mmt) that doesn't depend on r1, g1, or b1 */
|
||||
/* (depending on dir) */
|
||||
switch (dir) {
|
||||
case RED:
|
||||
return (-mmt[cube.mR0][cube.mG1][cube.mB1]
|
||||
+ mmt[cube.mR0][cube.mG1][cube.mB0]
|
||||
+ mmt[cube.mR0][cube.mG0][cube.mB1]
|
||||
- mmt[cube.mR0][cube.mG0][cube.mB0]);
|
||||
case GREEN:
|
||||
return (-mmt[cube.mR1][cube.mG0][cube.mB1]
|
||||
+ mmt[cube.mR1][cube.mG0][cube.mB0]
|
||||
+ mmt[cube.mR0][cube.mG0][cube.mB1]
|
||||
- mmt[cube.mR0][cube.mG0][cube.mB0]);
|
||||
case BLUE:
|
||||
return (-mmt[cube.mR1][cube.mG1][cube.mB0]
|
||||
+ mmt[cube.mR1][cube.mG0][cube.mB0]
|
||||
+ mmt[cube.mR0][cube.mG1][cube.mB0]
|
||||
- mmt[cube.mR0][cube.mG0][cube.mB0]);
|
||||
default:
|
||||
return 0;
|
||||
private CreateBoxesResult createBoxes(int maxColorCount) {
|
||||
mCubes = new Box[maxColorCount];
|
||||
for (int i = 0; i < maxColorCount; i++) {
|
||||
mCubes[i] = new Box();
|
||||
}
|
||||
}
|
||||
double[] volumeVariance = new double[maxColorCount];
|
||||
Box firstBox = mCubes[0];
|
||||
firstBox.r1 = MAX_INDEX;
|
||||
firstBox.g1 = MAX_INDEX;
|
||||
firstBox.b1 = MAX_INDEX;
|
||||
|
||||
private long getTop(Box cube, int dir, int pos, long[][][] mmt) {
|
||||
/* Compute remainder of Vol(cube, mmt), substituting pos for */
|
||||
/* r1, g1, or b1 (depending on dir) */
|
||||
switch (dir) {
|
||||
case RED:
|
||||
return (mmt[pos][cube.mG1][cube.mB1]
|
||||
- mmt[pos][cube.mG1][cube.mB0]
|
||||
- mmt[pos][cube.mG0][cube.mB1]
|
||||
+ mmt[pos][cube.mG0][cube.mB0]);
|
||||
case GREEN:
|
||||
return (mmt[cube.mR1][pos][cube.mB1]
|
||||
- mmt[cube.mR1][pos][cube.mB0]
|
||||
- mmt[cube.mR0][pos][cube.mB1]
|
||||
+ mmt[cube.mR0][pos][cube.mB0]);
|
||||
case BLUE:
|
||||
return (mmt[cube.mR1][cube.mG1][pos]
|
||||
- mmt[cube.mR1][cube.mG0][pos]
|
||||
- mmt[cube.mR0][cube.mG1][pos]
|
||||
+ mmt[cube.mR0][cube.mG0][pos]);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
int generatedColorCount = 0;
|
||||
int next = 0;
|
||||
|
||||
private double getVariance(Box cube) {
|
||||
/* Compute the weighted variance of a box */
|
||||
/* NB: as with the raw statistics, this is really the variance * size */
|
||||
double dr, dg, db, xx;
|
||||
dr = getVolume(cube, mMr);
|
||||
dg = getVolume(cube, mMg);
|
||||
db = getVolume(cube, mMb);
|
||||
xx =
|
||||
mM2[cube.mR1][cube.mG1][cube.mB1]
|
||||
- mM2[cube.mR1][cube.mG1][cube.mB0]
|
||||
- mM2[cube.mR1][cube.mG0][cube.mB1]
|
||||
+ mM2[cube.mR1][cube.mG0][cube.mB0]
|
||||
- mM2[cube.mR0][cube.mG1][cube.mB1]
|
||||
+ mM2[cube.mR0][cube.mG1][cube.mB0]
|
||||
+ mM2[cube.mR0][cube.mG0][cube.mB1]
|
||||
- mM2[cube.mR0][cube.mG0][cube.mB0];
|
||||
return xx - (dr * dr + dg * dg + db * db) / getVolume(cube, mWt);
|
||||
}
|
||||
|
||||
/* We want to minimize the sum of the variances of two subboxes.
|
||||
* The sum(c^2) terms can be ignored since their sum over both subboxes
|
||||
* is the same (the sum for the whole box) no matter where we split.
|
||||
* The remaining terms have a minus sign in the variance formula,
|
||||
* so we drop the minus sign and MAXIMIZE the sum of the two terms.
|
||||
*/
|
||||
private double maximize(
|
||||
Box cube,
|
||||
int dir,
|
||||
int first,
|
||||
int last,
|
||||
int[] cut,
|
||||
long wholeR,
|
||||
long wholeG,
|
||||
long wholeB,
|
||||
long wholeW) {
|
||||
long half_r, half_g, half_b, half_w;
|
||||
long base_r, base_g, base_b, base_w;
|
||||
int i;
|
||||
double temp, max;
|
||||
|
||||
base_r = getBottom(cube, dir, mMr);
|
||||
base_g = getBottom(cube, dir, mMg);
|
||||
base_b = getBottom(cube, dir, mMb);
|
||||
base_w = getBottom(cube, dir, mWt);
|
||||
|
||||
max = 0.0f;
|
||||
cut[0] = -1;
|
||||
|
||||
for (i = first; i < last; ++i) {
|
||||
half_r = base_r + getTop(cube, dir, i, mMr);
|
||||
half_g = base_g + getTop(cube, dir, i, mMg);
|
||||
half_b = base_b + getTop(cube, dir, i, mMb);
|
||||
half_w = base_w + getTop(cube, dir, i, mWt);
|
||||
/* now half_x is sum over lower half of box, if split at i */
|
||||
if (half_w == 0) /* subbox could be empty of pixels! */ {
|
||||
continue; /* never split into an empty box */
|
||||
for (int i = 1; i < maxColorCount; i++) {
|
||||
if (cut(mCubes[next], mCubes[i])) {
|
||||
volumeVariance[next] = (mCubes[next].vol > 1) ? variance(mCubes[next]) : 0.0;
|
||||
volumeVariance[i] = (mCubes[i].vol > 1) ? variance(mCubes[i]) : 0.0;
|
||||
} else {
|
||||
volumeVariance[next] = 0.0;
|
||||
i--;
|
||||
}
|
||||
temp = (half_r * half_r + half_g * half_g + half_b * half_b) / (double) half_w;
|
||||
half_r = wholeR - half_r;
|
||||
half_g = wholeG - half_g;
|
||||
half_b = wholeB - half_b;
|
||||
half_w = wholeW - half_w;
|
||||
if (half_w == 0) /* subbox could be empty of pixels! */ {
|
||||
continue; /* never split into an empty box */
|
||||
}
|
||||
temp += (half_r * half_r + half_g * half_g + half_b * half_b) / (double) half_w;
|
||||
|
||||
if (temp > max) {
|
||||
max = temp;
|
||||
cut[0] = i;
|
||||
next = 0;
|
||||
|
||||
double temp = volumeVariance[0];
|
||||
for (int k = 1; k <= i; k++) {
|
||||
if (volumeVariance[k] > temp) {
|
||||
temp = volumeVariance[k];
|
||||
next = k;
|
||||
}
|
||||
}
|
||||
generatedColorCount = i + 1;
|
||||
if (temp <= 0.0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return max;
|
||||
return new CreateBoxesResult(maxColorCount, generatedColorCount);
|
||||
}
|
||||
|
||||
private boolean cut(Box set1, Box set2) {
|
||||
int dir;
|
||||
int[] cutr = new int[1];
|
||||
int[] cutg = new int[1];
|
||||
int[] cutb = new int[1];
|
||||
double maxr, maxg, maxb;
|
||||
long whole_r, whole_g, whole_b, whole_w;
|
||||
private int[] createResult(int colorCount) {
|
||||
int[] colors = new int[colorCount];
|
||||
int nextAvailableIndex = 0;
|
||||
for (int i = 0; i < colorCount; ++i) {
|
||||
Box cube = mCubes[i];
|
||||
int weight = volume(cube, mWeights);
|
||||
if (weight > 0) {
|
||||
int r = (volume(cube, mMomentsR) / weight);
|
||||
int g = (volume(cube, mMomentsG) / weight);
|
||||
int b = (volume(cube, mMomentsB) / weight);
|
||||
int color = Color.rgb(r, g, b);
|
||||
colors[nextAvailableIndex++] = color;
|
||||
}
|
||||
}
|
||||
int[] resultArray = new int[nextAvailableIndex];
|
||||
arraycopy(colors, 0, resultArray, 0, nextAvailableIndex);
|
||||
return resultArray;
|
||||
}
|
||||
|
||||
whole_r = getVolume(set1, mMr);
|
||||
whole_g = getVolume(set1, mMg);
|
||||
whole_b = getVolume(set1, mMb);
|
||||
whole_w = getVolume(set1, mWt);
|
||||
private double variance(Box cube) {
|
||||
int dr = volume(cube, mMomentsR);
|
||||
int dg = volume(cube, mMomentsG);
|
||||
int db = volume(cube, mMomentsB);
|
||||
double xx =
|
||||
mMoments[getIndex(cube.r1, cube.g1, cube.b1)]
|
||||
- mMoments[getIndex(cube.r1, cube.g1, cube.b0)]
|
||||
- mMoments[getIndex(cube.r1, cube.g0, cube.b1)]
|
||||
+ mMoments[getIndex(cube.r1, cube.g0, cube.b0)]
|
||||
- mMoments[getIndex(cube.r0, cube.g1, cube.b1)]
|
||||
+ mMoments[getIndex(cube.r0, cube.g1, cube.b0)]
|
||||
+ mMoments[getIndex(cube.r0, cube.g0, cube.b1)]
|
||||
- mMoments[getIndex(cube.r0, cube.g0, cube.b0)];
|
||||
|
||||
maxr = maximize(set1, RED, set1.mR0 + 1, set1.mR1, cutr, whole_r, whole_g, whole_b,
|
||||
whole_w);
|
||||
maxg = maximize(set1, GREEN, set1.mG0 + 1, set1.mG1, cutg, whole_r, whole_g, whole_b,
|
||||
whole_w);
|
||||
maxb = maximize(set1, BLUE, set1.mB0 + 1, set1.mB1, cutb, whole_r, whole_g, whole_b,
|
||||
whole_w);
|
||||
int hypotenuse = (dr * dr + dg * dg + db * db);
|
||||
int volume2 = volume(cube, mWeights);
|
||||
double variance2 = xx - ((double) hypotenuse / (double) volume2);
|
||||
return variance2;
|
||||
}
|
||||
|
||||
if (maxr >= maxg && maxr >= maxb) {
|
||||
dir = RED;
|
||||
if (cutr[0] < 0) return false; /* can't split the box */
|
||||
} else if (maxg >= maxr && maxg >= maxb) {
|
||||
dir = GREEN;
|
||||
private boolean cut(Box one, Box two) {
|
||||
int wholeR = volume(one, mMomentsR);
|
||||
int wholeG = volume(one, mMomentsG);
|
||||
int wholeB = volume(one, mMomentsB);
|
||||
int wholeW = volume(one, mWeights);
|
||||
|
||||
MaximizeResult maxRResult =
|
||||
maximize(one, Direction.RED, one.r0 + 1, one.r1, wholeR, wholeG, wholeB, wholeW);
|
||||
MaximizeResult maxGResult =
|
||||
maximize(one, Direction.GREEN, one.g0 + 1, one.g1, wholeR, wholeG, wholeB, wholeW);
|
||||
MaximizeResult maxBResult =
|
||||
maximize(one, Direction.BLUE, one.b0 + 1, one.b1, wholeR, wholeG, wholeB, wholeW);
|
||||
Direction cutDirection;
|
||||
double maxR = maxRResult.mMaximum;
|
||||
double maxG = maxGResult.mMaximum;
|
||||
double maxB = maxBResult.mMaximum;
|
||||
if (maxR >= maxG && maxR >= maxB) {
|
||||
if (maxRResult.mCutLocation < 0) {
|
||||
return false;
|
||||
}
|
||||
cutDirection = Direction.RED;
|
||||
} else if (maxG >= maxR && maxG >= maxB) {
|
||||
cutDirection = Direction.GREEN;
|
||||
} else {
|
||||
dir = BLUE;
|
||||
cutDirection = Direction.BLUE;
|
||||
}
|
||||
|
||||
set2.mR1 = set1.mR1;
|
||||
set2.mG1 = set1.mG1;
|
||||
set2.mB1 = set1.mB1;
|
||||
two.r1 = one.r1;
|
||||
two.g1 = one.g1;
|
||||
two.b1 = one.b1;
|
||||
|
||||
switch (dir) {
|
||||
switch (cutDirection) {
|
||||
case RED:
|
||||
set2.mR0 = set1.mR1 = cutr[0];
|
||||
set2.mG0 = set1.mG0;
|
||||
set2.mB0 = set1.mB0;
|
||||
one.r1 = maxRResult.mCutLocation;
|
||||
two.r0 = one.r1;
|
||||
two.g0 = one.g0;
|
||||
two.b0 = one.b0;
|
||||
break;
|
||||
case GREEN:
|
||||
set2.mG0 = set1.mG1 = cutg[0];
|
||||
set2.mR0 = set1.mR0;
|
||||
set2.mB0 = set1.mB0;
|
||||
one.g1 = maxGResult.mCutLocation;
|
||||
two.r0 = one.r0;
|
||||
two.g0 = one.g1;
|
||||
two.b0 = one.b0;
|
||||
break;
|
||||
case BLUE:
|
||||
set2.mB0 = set1.mB1 = cutb[0];
|
||||
set2.mR0 = set1.mR0;
|
||||
set2.mG0 = set1.mG0;
|
||||
one.b1 = maxBResult.mCutLocation;
|
||||
two.r0 = one.r0;
|
||||
two.g0 = one.g0;
|
||||
two.b0 = one.b1;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("unexpected direction " + cutDirection);
|
||||
}
|
||||
set1.mVol = (set1.mR1 - set1.mR0) * (set1.mG1 - set1.mG0) * (set1.mB1 - set1.mB0);
|
||||
set2.mVol = (set2.mR1 - set2.mR0) * (set2.mG1 - set2.mG0) * (set2.mB1 - set2.mB0);
|
||||
|
||||
one.vol = (one.r1 - one.r0) * (one.g1 - one.g0) * (one.b1 - one.b0);
|
||||
two.vol = (two.r1 - two.r0) * (two.g1 - two.g0) * (two.b1 - two.b0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private MaximizeResult maximize(
|
||||
Box cube,
|
||||
Direction direction,
|
||||
int first,
|
||||
int last,
|
||||
int wholeR,
|
||||
int wholeG,
|
||||
int wholeB,
|
||||
int wholeW) {
|
||||
int baseR = bottom(cube, direction, mMomentsR);
|
||||
int baseG = bottom(cube, direction, mMomentsG);
|
||||
int baseB = bottom(cube, direction, mMomentsB);
|
||||
int baseW = bottom(cube, direction, mWeights);
|
||||
|
||||
double max = 0.0;
|
||||
int cut = -1;
|
||||
for (int i = first; i < last; i++) {
|
||||
int halfR = baseR + top(cube, direction, i, mMomentsR);
|
||||
int halfG = baseG + top(cube, direction, i, mMomentsG);
|
||||
int halfB = baseB + top(cube, direction, i, mMomentsB);
|
||||
int halfW = baseW + top(cube, direction, i, mWeights);
|
||||
|
||||
if (halfW == 0) {
|
||||
continue;
|
||||
}
|
||||
double tempNumerator = halfR * halfR + halfG * halfG + halfB * halfB;
|
||||
double tempDenominator = halfW;
|
||||
double temp = tempNumerator / tempDenominator;
|
||||
|
||||
halfR = wholeR - halfR;
|
||||
halfG = wholeG - halfG;
|
||||
halfB = wholeB - halfB;
|
||||
halfW = wholeW - halfW;
|
||||
if (halfW == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tempNumerator = halfR * halfR + halfG * halfG + halfB * halfB;
|
||||
tempDenominator = halfW;
|
||||
temp += (tempNumerator / tempDenominator);
|
||||
if (temp > max) {
|
||||
max = temp;
|
||||
cut = i;
|
||||
}
|
||||
}
|
||||
return new MaximizeResult(cut, max);
|
||||
}
|
||||
|
||||
private static int volume(Box cube, int[] moment) {
|
||||
return (moment[getIndex(cube.r1, cube.g1, cube.b1)]
|
||||
- moment[getIndex(cube.r1, cube.g1, cube.b0)]
|
||||
- moment[getIndex(cube.r1, cube.g0, cube.b1)]
|
||||
+ moment[getIndex(cube.r1, cube.g0, cube.b0)]
|
||||
- moment[getIndex(cube.r0, cube.g1, cube.b1)]
|
||||
+ moment[getIndex(cube.r0, cube.g1, cube.b0)]
|
||||
+ moment[getIndex(cube.r0, cube.g0, cube.b1)]
|
||||
- moment[getIndex(cube.r0, cube.g0, cube.b0)]);
|
||||
}
|
||||
|
||||
private static int bottom(Box cube, Direction direction, int[] moment) {
|
||||
switch (direction) {
|
||||
case RED:
|
||||
return -moment[getIndex(cube.r0, cube.g1, cube.b1)]
|
||||
+ moment[getIndex(cube.r0, cube.g1, cube.b0)]
|
||||
+ moment[getIndex(cube.r0, cube.g0, cube.b1)]
|
||||
- moment[getIndex(cube.r0, cube.g0, cube.b0)];
|
||||
case GREEN:
|
||||
return -moment[getIndex(cube.r1, cube.g0, cube.b1)]
|
||||
+ moment[getIndex(cube.r1, cube.g0, cube.b0)]
|
||||
+ moment[getIndex(cube.r0, cube.g0, cube.b1)]
|
||||
- moment[getIndex(cube.r0, cube.g0, cube.b0)];
|
||||
case BLUE:
|
||||
return -moment[getIndex(cube.r1, cube.g1, cube.b0)]
|
||||
+ moment[getIndex(cube.r1, cube.g0, cube.b0)]
|
||||
+ moment[getIndex(cube.r0, cube.g1, cube.b0)]
|
||||
- moment[getIndex(cube.r0, cube.g0, cube.b0)];
|
||||
default:
|
||||
throw new IllegalArgumentException("unexpected direction " + direction);
|
||||
}
|
||||
}
|
||||
|
||||
private static int top(Box cube, Direction direction, int position, int[] moment) {
|
||||
switch (direction) {
|
||||
case RED:
|
||||
return (moment[getIndex(position, cube.g1, cube.b1)]
|
||||
- moment[getIndex(position, cube.g1, cube.b0)]
|
||||
- moment[getIndex(position, cube.g0, cube.b1)]
|
||||
+ moment[getIndex(position, cube.g0, cube.b0)]);
|
||||
case GREEN:
|
||||
return (moment[getIndex(cube.r1, position, cube.b1)]
|
||||
- moment[getIndex(cube.r1, position, cube.b0)]
|
||||
- moment[getIndex(cube.r0, position, cube.b1)]
|
||||
+ moment[getIndex(cube.r0, position, cube.b0)]);
|
||||
case BLUE:
|
||||
return (moment[getIndex(cube.r1, cube.g1, position)]
|
||||
- moment[getIndex(cube.r1, cube.g0, position)]
|
||||
- moment[getIndex(cube.r0, cube.g1, position)]
|
||||
+ moment[getIndex(cube.r0, cube.g0, position)]);
|
||||
default:
|
||||
throw new IllegalArgumentException("unexpected direction " + direction);
|
||||
}
|
||||
}
|
||||
|
||||
private enum Direction {
|
||||
RED,
|
||||
GREEN,
|
||||
BLUE
|
||||
}
|
||||
|
||||
private static class MaximizeResult {
|
||||
// < 0 if cut impossible
|
||||
final int mCutLocation;
|
||||
final double mMaximum;
|
||||
|
||||
MaximizeResult(int cut, double max) {
|
||||
mCutLocation = cut;
|
||||
mMaximum = max;
|
||||
}
|
||||
}
|
||||
|
||||
private static class CreateBoxesResult {
|
||||
final int mRequestedCount;
|
||||
final int mResultCount;
|
||||
|
||||
CreateBoxesResult(int requestedCount, int resultCount) {
|
||||
mRequestedCount = requestedCount;
|
||||
mResultCount = resultCount;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Box {
|
||||
public int r0 = 0;
|
||||
public int r1 = 0;
|
||||
public int g0 = 0;
|
||||
public int g1 = 0;
|
||||
public int b0 = 0;
|
||||
public int b1 = 0;
|
||||
public int vol = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -628,7 +628,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
|
||||
}
|
||||
|
||||
// scale if the crop height winds up not matching the recommended metrics
|
||||
needScale = wpData.mHeight != cropHint.height()
|
||||
needScale = cropHint.height() > wpData.mHeight
|
||||
|| cropHint.height() > GLHelper.getMaxTextureSize()
|
||||
|| cropHint.width() > GLHelper.getMaxTextureSize();
|
||||
|
||||
@@ -752,7 +752,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
|
||||
|
||||
f = new FileOutputStream(wallpaper.cropFile);
|
||||
bos = new BufferedOutputStream(f, 32*1024);
|
||||
finalCrop.compress(Bitmap.CompressFormat.JPEG, 100, bos);
|
||||
finalCrop.compress(Bitmap.CompressFormat.PNG, 100, bos);
|
||||
bos.flush(); // don't rely on the implicit flush-at-close when noting success
|
||||
success = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user