Merge "Revert "[framework] Integrate new quantizers"" into sc-dev
This commit is contained in:
@@ -30,17 +30,14 @@ 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.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.util.ContrastColorUtil;
|
import com.android.internal.util.ContrastColorUtil;
|
||||||
|
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides information about the colors of a wallpaper.
|
* Provides information about the colors of a wallpaper.
|
||||||
@@ -97,21 +94,16 @@ public final class WallpaperColors implements Parcelable {
|
|||||||
private static final float DARK_PIXEL_CONTRAST = 6f;
|
private static final float DARK_PIXEL_CONTRAST = 6f;
|
||||||
private static final float MAX_DARK_AREA = 0.025f;
|
private static final float MAX_DARK_AREA = 0.025f;
|
||||||
|
|
||||||
private final List<Color> mMainColors;
|
private final ArrayList<Color> mMainColors;
|
||||||
private final Map<Integer, Integer> mAllColors;
|
|
||||||
private int mColorHints;
|
private int mColorHints;
|
||||||
|
|
||||||
public WallpaperColors(Parcel parcel) {
|
public WallpaperColors(Parcel parcel) {
|
||||||
mMainColors = new ArrayList<>();
|
mMainColors = new ArrayList<>();
|
||||||
mAllColors = new HashMap<>();
|
|
||||||
final int count = parcel.readInt();
|
final int count = parcel.readInt();
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
final int colorInt = parcel.readInt();
|
final int colorInt = parcel.readInt();
|
||||||
Color color = Color.valueOf(colorInt);
|
Color color = Color.valueOf(colorInt);
|
||||||
mMainColors.add(color);
|
mMainColors.add(color);
|
||||||
|
|
||||||
final int population = parcel.readInt();
|
|
||||||
mAllColors.put(colorInt, population);
|
|
||||||
}
|
}
|
||||||
mColorHints = parcel.readInt();
|
mColorHints = parcel.readInt();
|
||||||
}
|
}
|
||||||
@@ -174,22 +166,39 @@ public final class WallpaperColors implements Parcelable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final Palette palette = Palette
|
final Palette palette = Palette
|
||||||
.from(bitmap, new CelebiQuantizer())
|
.from(bitmap)
|
||||||
.maximumColorCount(256)
|
.setQuantizer(new VariationalKMeansQuantizer())
|
||||||
|
.maximumColorCount(5)
|
||||||
|
.clearFilters()
|
||||||
.resizeBitmapArea(MAX_WALLPAPER_EXTRACTION_AREA)
|
.resizeBitmapArea(MAX_WALLPAPER_EXTRACTION_AREA)
|
||||||
.generate();
|
.generate();
|
||||||
|
|
||||||
// Remove insignificant colors and sort swatches by population
|
// Remove insignificant colors and sort swatches by population
|
||||||
final ArrayList<Palette.Swatch> swatches = new ArrayList<>(palette.getSwatches());
|
final ArrayList<Palette.Swatch> swatches = new ArrayList<>(palette.getSwatches());
|
||||||
|
final float minColorArea = bitmap.getWidth() * bitmap.getHeight() * MIN_COLOR_OCCURRENCE;
|
||||||
|
swatches.removeIf(s -> s.getPopulation() < minColorArea);
|
||||||
swatches.sort((a, b) -> b.getPopulation() - a.getPopulation());
|
swatches.sort((a, b) -> b.getPopulation() - a.getPopulation());
|
||||||
|
|
||||||
final int swatchesSize = swatches.size();
|
final int swatchesSize = swatches.size();
|
||||||
|
Color primary = null, secondary = null, tertiary = null;
|
||||||
|
|
||||||
final Map<Integer, Integer> populationByColor = new HashMap<>();
|
swatchLoop:
|
||||||
for (int i = 0; i < swatchesSize; i++) {
|
for (int i = 0; i < swatchesSize; i++) {
|
||||||
Palette.Swatch swatch = swatches.get(i);
|
Color color = Color.valueOf(swatches.get(i).getRgb());
|
||||||
int colorInt = swatch.getInt();
|
switch (i) {
|
||||||
populationByColor.put(colorInt, swatch.getPopulation());
|
case 0:
|
||||||
|
primary = color;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
secondary = color;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
tertiary = color;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// out of bounds
|
||||||
|
break swatchLoop;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int hints = calculateDarkHints(bitmap);
|
int hints = calculateDarkHints(bitmap);
|
||||||
@@ -198,7 +207,7 @@ public final class WallpaperColors implements Parcelable {
|
|||||||
bitmap.recycle();
|
bitmap.recycle();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WallpaperColors(populationByColor, HINT_FROM_BITMAP | hints);
|
return new WallpaperColors(primary, secondary, tertiary, HINT_FROM_BITMAP | hints);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -244,13 +253,9 @@ public final class WallpaperColors implements Parcelable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mMainColors = new ArrayList<>(3);
|
mMainColors = new ArrayList<>(3);
|
||||||
mAllColors = new HashMap<>();
|
|
||||||
|
|
||||||
mMainColors.add(primaryColor);
|
mMainColors.add(primaryColor);
|
||||||
mAllColors.put(primaryColor.toArgb(), 0);
|
|
||||||
if (secondaryColor != null) {
|
if (secondaryColor != null) {
|
||||||
mMainColors.add(secondaryColor);
|
mMainColors.add(secondaryColor);
|
||||||
mAllColors.put(secondaryColor.toArgb(), 0);
|
|
||||||
}
|
}
|
||||||
if (tertiaryColor != null) {
|
if (tertiaryColor != null) {
|
||||||
if (secondaryColor == null) {
|
if (secondaryColor == null) {
|
||||||
@@ -258,32 +263,8 @@ public final class WallpaperColors implements Parcelable {
|
|||||||
+ "secondaryColor is null");
|
+ "secondaryColor is null");
|
||||||
}
|
}
|
||||||
mMainColors.add(tertiaryColor);
|
mMainColors.add(tertiaryColor);
|
||||||
mAllColors.put(tertiaryColor.toArgb(), 0);
|
|
||||||
}
|
|
||||||
mColorHints = colorHints;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
* occurrences of color in the wallpaper.
|
|
||||||
* @param colorHints A combination of WallpaperColor hints.
|
|
||||||
* @hide
|
|
||||||
* @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT
|
|
||||||
* @see WallpaperColors#fromBitmap(Bitmap)
|
|
||||||
* @see WallpaperColors#fromDrawable(Drawable)
|
|
||||||
*/
|
|
||||||
public WallpaperColors(@NonNull Map<Integer, Integer> populationByColor, int colorHints) {
|
|
||||||
mAllColors = populationByColor;
|
|
||||||
|
|
||||||
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());
|
|
||||||
mColorHints = colorHints;
|
mColorHints = colorHints;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,9 +293,6 @@ public final class WallpaperColors implements Parcelable {
|
|||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
Color color = mainColors.get(i);
|
Color color = mainColors.get(i);
|
||||||
dest.writeInt(color.toArgb());
|
dest.writeInt(color.toArgb());
|
||||||
Integer population = mAllColors.get(color.toArgb());
|
|
||||||
int populationInt = (population != null) ? population : 0;
|
|
||||||
dest.writeInt(populationInt);
|
|
||||||
}
|
}
|
||||||
dest.writeInt(mColorHints);
|
dest.writeInt(mColorHints);
|
||||||
}
|
}
|
||||||
@@ -358,17 +336,6 @@ public final class WallpaperColors implements Parcelable {
|
|||||||
return Collections.unmodifiableList(mMainColors);
|
return Collections.unmodifiableList(mMainColors);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Map of all colors. Key is rgb integer, value is importance of color.
|
|
||||||
*
|
|
||||||
* @return List of colors.
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
public @NonNull Map<Integer, Integer> getAllColors() {
|
|
||||||
return Collections.unmodifiableMap(mAllColors);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(@Nullable Object o) {
|
public boolean equals(@Nullable Object o) {
|
||||||
if (o == null || getClass() != o.getClass()) {
|
if (o == null || getClass() != o.getClass()) {
|
||||||
|
|||||||
@@ -1,50 +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.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.
|
|
||||||
*
|
|
||||||
* See Celebi 2011, “Improving the Performance of K-Means for Color Quantization”
|
|
||||||
*/
|
|
||||||
public class CelebiQuantizer implements Quantizer {
|
|
||||||
private List<Palette.Swatch> mSwatches;
|
|
||||||
|
|
||||||
public CelebiQuantizer() { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void quantize(int[] pixels, int maxColors) {
|
|
||||||
WuQuantizer wu = new WuQuantizer(pixels, maxColors);
|
|
||||||
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);
|
|
||||||
kmeans.quantize(pixels, maxColors);
|
|
||||||
mSwatches = kmeans.getQuantizedColors();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Palette.Swatch> getQuantizedColors() {
|
|
||||||
return mSwatches;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +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 android.annotation.ColorInt;
|
|
||||||
|
|
||||||
interface CentroidProvider {
|
|
||||||
/**
|
|
||||||
* @return 3 dimensions representing the color
|
|
||||||
*/
|
|
||||||
float[] getCentroid(@ColorInt int color);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param centroid 3 dimensions representing the color
|
|
||||||
* @return 32-bit ARGB representation
|
|
||||||
*/
|
|
||||||
@ColorInt
|
|
||||||
int getColor(float[] centroid);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Distance between two centroids.
|
|
||||||
*/
|
|
||||||
float distance(float[] a, float[] b);
|
|
||||||
}
|
|
||||||
@@ -35,8 +35,6 @@ package com.android.internal.graphics.palette;
|
|||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.util.TimingLogger;
|
import android.util.TimingLogger;
|
||||||
|
|
||||||
import com.android.internal.graphics.palette.Palette.Swatch;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -44,6 +42,9 @@ import java.util.Comparator;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.PriorityQueue;
|
import java.util.PriorityQueue;
|
||||||
|
|
||||||
|
import com.android.internal.graphics.ColorUtils;
|
||||||
|
import com.android.internal.graphics.palette.Palette.Swatch;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copied from: frameworks/support/v7/palette/src/main/java/android/support/v7/
|
* Copied from: frameworks/support/v7/palette/src/main/java/android/support/v7/
|
||||||
* graphics/ColorCutQuantizer.java
|
* graphics/ColorCutQuantizer.java
|
||||||
@@ -76,6 +77,7 @@ final class ColorCutQuantizer implements Quantizer {
|
|||||||
int[] mHistogram;
|
int[] mHistogram;
|
||||||
List<Swatch> mQuantizedColors;
|
List<Swatch> mQuantizedColors;
|
||||||
TimingLogger mTimingLogger;
|
TimingLogger mTimingLogger;
|
||||||
|
Palette.Filter[] mFilters;
|
||||||
|
|
||||||
private final float[] mTempHsl = new float[3];
|
private final float[] mTempHsl = new float[3];
|
||||||
|
|
||||||
@@ -84,9 +86,11 @@ final class ColorCutQuantizer implements Quantizer {
|
|||||||
*
|
*
|
||||||
* @param pixels histogram representing an image's pixel data
|
* @param pixels histogram representing an image's pixel data
|
||||||
* @param maxColors The maximum number of colors that should be in the result palette.
|
* @param maxColors The maximum number of colors that should be in the result palette.
|
||||||
|
* @param filters Set of filters to use in the quantization stage
|
||||||
*/
|
*/
|
||||||
public void quantize(final int[] pixels, final int maxColors) {
|
public void quantize(final int[] pixels, final int maxColors, final Palette.Filter[] filters) {
|
||||||
mTimingLogger = LOG_TIMINGS ? new TimingLogger(LOG_TAG, "Creation") : null;
|
mTimingLogger = LOG_TIMINGS ? new TimingLogger(LOG_TAG, "Creation") : null;
|
||||||
|
mFilters = filters;
|
||||||
|
|
||||||
final int[] hist = mHistogram = new int[1 << (QUANTIZE_WORD_WIDTH * 3)];
|
final int[] hist = mHistogram = new int[1 << (QUANTIZE_WORD_WIDTH * 3)];
|
||||||
for (int i = 0; i < pixels.length; i++) {
|
for (int i = 0; i < pixels.length; i++) {
|
||||||
@@ -104,6 +108,10 @@ final class ColorCutQuantizer implements Quantizer {
|
|||||||
// Now let's count the number of distinct colors
|
// Now let's count the number of distinct colors
|
||||||
int distinctColorCount = 0;
|
int distinctColorCount = 0;
|
||||||
for (int color = 0; color < hist.length; color++) {
|
for (int color = 0; color < hist.length; color++) {
|
||||||
|
if (hist[color] > 0 && shouldIgnoreColor(color)) {
|
||||||
|
// If we should ignore the color, set the population to 0
|
||||||
|
hist[color] = 0;
|
||||||
|
}
|
||||||
if (hist[color] > 0) {
|
if (hist[color] > 0) {
|
||||||
// If the color has population, increase the distinct color count
|
// If the color has population, increase the distinct color count
|
||||||
distinctColorCount++;
|
distinctColorCount++;
|
||||||
@@ -208,8 +216,12 @@ final class ColorCutQuantizer implements Quantizer {
|
|||||||
ArrayList<Swatch> colors = new ArrayList<>(vboxes.size());
|
ArrayList<Swatch> colors = new ArrayList<>(vboxes.size());
|
||||||
for (Vbox vbox : vboxes) {
|
for (Vbox vbox : vboxes) {
|
||||||
Swatch swatch = vbox.getAverageColor();
|
Swatch swatch = vbox.getAverageColor();
|
||||||
|
if (!shouldIgnoreColor(swatch)) {
|
||||||
|
// As we're averaging a color box, we can still get colors which we do not want, so
|
||||||
|
// we check again here
|
||||||
colors.add(swatch);
|
colors.add(swatch);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return colors;
|
return colors;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +230,7 @@ final class ColorCutQuantizer implements Quantizer {
|
|||||||
*/
|
*/
|
||||||
private class Vbox {
|
private class Vbox {
|
||||||
// lower and upper index are inclusive
|
// lower and upper index are inclusive
|
||||||
private final int mLowerIndex;
|
private int mLowerIndex;
|
||||||
private int mUpperIndex;
|
private int mUpperIndex;
|
||||||
// Population of colors within this box
|
// Population of colors within this box
|
||||||
private int mPopulation;
|
private int mPopulation;
|
||||||
@@ -435,6 +447,27 @@ final class ColorCutQuantizer implements Quantizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean shouldIgnoreColor(int color565) {
|
||||||
|
final int rgb = approximateToRgb888(color565);
|
||||||
|
ColorUtils.colorToHSL(rgb, mTempHsl);
|
||||||
|
return shouldIgnoreColor(rgb, mTempHsl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldIgnoreColor(Swatch color) {
|
||||||
|
return shouldIgnoreColor(color.getRgb(), color.getHsl());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldIgnoreColor(int rgb, float[] hsl) {
|
||||||
|
if (mFilters != null && mFilters.length > 0) {
|
||||||
|
for (int i = 0, count = mFilters.length; i < count; i++) {
|
||||||
|
if (!mFilters[i].isAllowed(rgb, hsl)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Comparator which sorts {@link Vbox} instances based on their volume, in descending order
|
* Comparator which sorts {@link Vbox} instances based on their volume, in descending order
|
||||||
*/
|
*/
|
||||||
@@ -465,8 +498,7 @@ final class ColorCutQuantizer implements Quantizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static int approximateToRgb888(int color) {
|
private static int approximateToRgb888(int color) {
|
||||||
return approximateToRgb888(quantizedRed(color), quantizedGreen(color),
|
return approximateToRgb888(quantizedRed(color), quantizedGreen(color), quantizedBlue(color));
|
||||||
quantizedBlue(color));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,103 +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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper methods for determining contrast between two colors, either via the colors themselves
|
|
||||||
* or components in different color spaces.
|
|
||||||
*/
|
|
||||||
public class Contrast {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param y Y in XYZ that contrasts with the returned Y value
|
|
||||||
* @param contrast contrast ratio between color argument and returned Y value. Must be >= 1
|
|
||||||
* or an exception will be thrown
|
|
||||||
* @return the lower Y coordinate in XYZ space that contrasts with color, or -1 if reaching
|
|
||||||
* no Y coordinate reaches contrast with color.
|
|
||||||
*/
|
|
||||||
public static float lighterY(float y, float contrast) {
|
|
||||||
assert (contrast >= 1);
|
|
||||||
float answer = -5 + contrast * (5 + y);
|
|
||||||
if (answer > 100.0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return answer;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param y Y in XYZ that contrasts with the returned Y value
|
|
||||||
* @param contrast contrast ratio between color argument and returned Y value. Must be >= 1
|
|
||||||
* or an exception will be thrown
|
|
||||||
* @return the lower Y coordinate in XYZ space that contrasts with color, or -1 if reaching
|
|
||||||
* no Y coordinate reaches contrast with color.
|
|
||||||
*/
|
|
||||||
public static float darkerY(float y, float contrast) {
|
|
||||||
assert (contrast >= 1);
|
|
||||||
float answer = (5 - 5 * contrast + y) / contrast;
|
|
||||||
if (answer < 0.0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return answer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert L* in L*a*b* to Y in XYZ.
|
|
||||||
*
|
|
||||||
* @param lstar L* in L*a*b*
|
|
||||||
* @return Y in XYZ
|
|
||||||
*/
|
|
||||||
public static float lstarToY(float lstar) {
|
|
||||||
// http://www.brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html
|
|
||||||
float ke = 8.0f;
|
|
||||||
if (lstar > ke) {
|
|
||||||
return (float) (Math.pow(((lstar + 16.0) / 116.0), 3) * 100.0);
|
|
||||||
} else {
|
|
||||||
return (float) (lstar / (24389 / 27) * 100.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert Y in XYZ to L* in L*a*b*.
|
|
||||||
*
|
|
||||||
* @param y Y in XYZ
|
|
||||||
* @return L* in L*a*b*
|
|
||||||
*/
|
|
||||||
public static float yToLstar(float y) {
|
|
||||||
y = y / 100.0f;
|
|
||||||
float e = 216.0f / 24389.0f;
|
|
||||||
float y_intermediate;
|
|
||||||
if (y <= e) {
|
|
||||||
y_intermediate = (24389.f / 27.f) * y;
|
|
||||||
// If y < e, can skip consecutive steps of / 116 + 16 followed by * 116 - 16.
|
|
||||||
return y_intermediate;
|
|
||||||
} else {
|
|
||||||
y_intermediate = (float) Math.cbrt(y);
|
|
||||||
}
|
|
||||||
return 116.f * y_intermediate - 16.f;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Contrast ratio between two Y values in XYZ space.
|
|
||||||
*/
|
|
||||||
public static float contrastYs(float y1, float y2) {
|
|
||||||
final float lighter = Math.max(y1, y2);
|
|
||||||
final float darker = (lighter == y1) ? y2 : y1;
|
|
||||||
return (lighter + 5) / (darker + 5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +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 android.graphics.Color;
|
|
||||||
import android.graphics.ColorSpace;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows quantizers to operate in the L*a*b* colorspace.
|
|
||||||
* L*a*b* is a good choice for measuring distance between colors.
|
|
||||||
* Better spaces, and better distance calculations even in L*a*b* exist, but measuring distance
|
|
||||||
* in L*a*b* space, also known as deltaE, is a universally accepted standard across industries
|
|
||||||
* and worldwide.
|
|
||||||
*/
|
|
||||||
public class LABCentroid implements CentroidProvider {
|
|
||||||
final ColorSpace.Connector mRgbToLab;
|
|
||||||
final ColorSpace.Connector mLabToRgb;
|
|
||||||
|
|
||||||
public LABCentroid() {
|
|
||||||
mRgbToLab = ColorSpace.connect(
|
|
||||||
ColorSpace.get(ColorSpace.Named.SRGB),
|
|
||||||
ColorSpace.get(ColorSpace.Named.CIE_LAB));
|
|
||||||
mLabToRgb = ColorSpace.connect(ColorSpace.get(ColorSpace.Named.CIE_LAB),
|
|
||||||
ColorSpace.get(ColorSpace.Named.SRGB));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float[] getCentroid(int color) {
|
|
||||||
float r = Color.red(color) / 255.f;
|
|
||||||
float g = Color.green(color) / 255.f;
|
|
||||||
float b = Color.blue(color) / 255.f;
|
|
||||||
|
|
||||||
float[] transform = mRgbToLab.transform(r, g, b);
|
|
||||||
return transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getColor(float[] centroid) {
|
|
||||||
float[] rgb = mLabToRgb.transform(centroid);
|
|
||||||
int color = Color.rgb(rgb[0], rgb[1], rgb[2]);
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float distance(float[] a, float[] b) {
|
|
||||||
// Standard v1 CIELAB deltaE formula, 1976 - easily improved upon, however,
|
|
||||||
// improvements do not significantly impact the Palette algorithm's results.
|
|
||||||
double dL = a[0] - b[0];
|
|
||||||
double dA = a[1] - b[1];
|
|
||||||
double dB = a[2] - b[2];
|
|
||||||
return (float) (Math.pow(dL, 2) + Math.pow(dA, 2) + Math.pow(dB, 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +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 {
|
|
||||||
private static final Random RANDOM = new Random(0);
|
|
||||||
|
|
||||||
public float[] center;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor.
|
|
||||||
*
|
|
||||||
* @param upperBound maximum value of a dimension in the space Kmeans is optimizing in
|
|
||||||
*/
|
|
||||||
Mean(int upperBound) {
|
|
||||||
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};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -22,15 +22,6 @@ import java.util.List;
|
|||||||
* Definition of an algorithm that receives pixels and outputs a list of colors.
|
* Definition of an algorithm that receives pixels and outputs a list of colors.
|
||||||
*/
|
*/
|
||||||
public interface Quantizer {
|
public interface Quantizer {
|
||||||
/**
|
void quantize(final int[] pixels, final int maxColors, final Palette.Filter[] filters);
|
||||||
* Create colors representative of the colors present in pixels.
|
|
||||||
* @param pixels Set of ARGB representation of a color.
|
|
||||||
* @param maxColors number of colors to generate
|
|
||||||
*/
|
|
||||||
void quantize(int[] pixels, int maxColors);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of colors generated by previous call to quantize.
|
|
||||||
*/
|
|
||||||
List<Palette.Swatch> getQuantizedColors();
|
List<Palette.Swatch> getQuantizedColors();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2021 The Android Open Source Project
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -16,234 +16,368 @@
|
|||||||
|
|
||||||
package com.android.internal.graphics.palette;
|
package com.android.internal.graphics.palette;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright 2015 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.
|
||||||
|
*/
|
||||||
|
|
||||||
import android.annotation.FloatRange;
|
import android.annotation.FloatRange;
|
||||||
import android.annotation.IntRange;
|
|
||||||
import android.annotation.NonNull;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class which allows custom selection of colors in a {@link Palette}'s generation. Instances can
|
* Copied from: frameworks/support/v7/palette/src/main/java/android/support/v7/graphics/Target.java
|
||||||
* be created via the {@link Builder} class.
|
*
|
||||||
|
* A class which allows custom selection of colors in a {@link Palette}'s generation. Instances
|
||||||
|
* can be created via the {@link android.support.v7.graphics.Target.Builder} class.
|
||||||
|
*
|
||||||
|
* <p>To use the target, use the {@link Palette.Builder#addTarget(Target)} API when building a
|
||||||
|
* Palette.</p>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public final class Target {
|
public final class Target {
|
||||||
private static final float WEIGHT_CHROMA = 0.5f;
|
|
||||||
private static final float WEIGHT_RELATIVE_LUMINANCE = 0.5f;
|
|
||||||
private static final float WEIGHT_POPULATION = 0.3f;
|
|
||||||
private static final float WEIGHT_HUE = 0.2f;
|
|
||||||
|
|
||||||
// Arbitrarily chosen, except max - CAM16 chroma has a ceiling of 130, based on unit testing.
|
private static final float TARGET_DARK_LUMA = 0.26f;
|
||||||
private static final float DEFAULT_CHROMA_MIN = 0.f;
|
private static final float MAX_DARK_LUMA = 0.45f;
|
||||||
private static final float DEFAULT_CHROMA_MAX = 130.f;
|
|
||||||
private static final float DEFAULT_CHROMA_TARGET = 30.f;
|
|
||||||
|
|
||||||
private float mTargetRelativeLuminance = -1.0f;
|
private static final float MIN_LIGHT_LUMA = 0.55f;
|
||||||
private float mChromaWeight;
|
private static final float TARGET_LIGHT_LUMA = 0.74f;
|
||||||
private float mChromaTarget;
|
|
||||||
private float mChromaMin;
|
private static final float MIN_NORMAL_LUMA = 0.3f;
|
||||||
private float mChromaMax;
|
private static final float TARGET_NORMAL_LUMA = 0.5f;
|
||||||
private float mRelativeLuminanceWeight;
|
private static final float MAX_NORMAL_LUMA = 0.7f;
|
||||||
private float mPopulationWeight;
|
|
||||||
private float mHueWeight;
|
private static final float TARGET_MUTED_SATURATION = 0.3f;
|
||||||
private float mTargetHue;
|
private static final float MAX_MUTED_SATURATION = 0.4f;
|
||||||
|
|
||||||
|
private static final float TARGET_VIBRANT_SATURATION = 1f;
|
||||||
|
private static final float MIN_VIBRANT_SATURATION = 0.35f;
|
||||||
|
|
||||||
|
private static final float WEIGHT_SATURATION = 0.24f;
|
||||||
|
private static final float WEIGHT_LUMA = 0.52f;
|
||||||
|
private static final float WEIGHT_POPULATION = 0.24f;
|
||||||
|
|
||||||
|
static final int INDEX_MIN = 0;
|
||||||
|
static final int INDEX_TARGET = 1;
|
||||||
|
static final int INDEX_MAX = 2;
|
||||||
|
|
||||||
|
static final int INDEX_WEIGHT_SAT = 0;
|
||||||
|
static final int INDEX_WEIGHT_LUMA = 1;
|
||||||
|
static final int INDEX_WEIGHT_POP = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A target which has the characteristics of a vibrant color which is light in luminance.
|
||||||
|
*/
|
||||||
|
public static final Target LIGHT_VIBRANT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A target which has the characteristics of a vibrant color which is neither light or dark.
|
||||||
|
*/
|
||||||
|
public static final Target VIBRANT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A target which has the characteristics of a vibrant color which is dark in luminance.
|
||||||
|
*/
|
||||||
|
public static final Target DARK_VIBRANT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A target which has the characteristics of a muted color which is light in luminance.
|
||||||
|
*/
|
||||||
|
public static final Target LIGHT_MUTED;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A target which has the characteristics of a muted color which is neither light or dark.
|
||||||
|
*/
|
||||||
|
public static final Target MUTED;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A target which has the characteristics of a muted color which is dark in luminance.
|
||||||
|
*/
|
||||||
|
public static final Target DARK_MUTED;
|
||||||
|
|
||||||
|
static {
|
||||||
|
LIGHT_VIBRANT = new Target();
|
||||||
|
setDefaultLightLightnessValues(LIGHT_VIBRANT);
|
||||||
|
setDefaultVibrantSaturationValues(LIGHT_VIBRANT);
|
||||||
|
|
||||||
|
VIBRANT = new Target();
|
||||||
|
setDefaultNormalLightnessValues(VIBRANT);
|
||||||
|
setDefaultVibrantSaturationValues(VIBRANT);
|
||||||
|
|
||||||
|
DARK_VIBRANT = new Target();
|
||||||
|
setDefaultDarkLightnessValues(DARK_VIBRANT);
|
||||||
|
setDefaultVibrantSaturationValues(DARK_VIBRANT);
|
||||||
|
|
||||||
|
LIGHT_MUTED = new Target();
|
||||||
|
setDefaultLightLightnessValues(LIGHT_MUTED);
|
||||||
|
setDefaultMutedSaturationValues(LIGHT_MUTED);
|
||||||
|
|
||||||
|
MUTED = new Target();
|
||||||
|
setDefaultNormalLightnessValues(MUTED);
|
||||||
|
setDefaultMutedSaturationValues(MUTED);
|
||||||
|
|
||||||
|
DARK_MUTED = new Target();
|
||||||
|
setDefaultDarkLightnessValues(DARK_MUTED);
|
||||||
|
setDefaultMutedSaturationValues(DARK_MUTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
final float[] mSaturationTargets = new float[3];
|
||||||
|
final float[] mLightnessTargets = new float[3];
|
||||||
|
final float[] mWeights = new float[3];
|
||||||
|
boolean mIsExclusive = true; // default to true
|
||||||
|
|
||||||
Target() {
|
Target() {
|
||||||
mChromaMax = DEFAULT_CHROMA_MAX;
|
setTargetDefaultValues(mSaturationTargets);
|
||||||
mChromaMin = DEFAULT_CHROMA_MIN;
|
setTargetDefaultValues(mLightnessTargets);
|
||||||
mChromaTarget = DEFAULT_CHROMA_TARGET;
|
setDefaultWeights();
|
||||||
mChromaWeight = WEIGHT_CHROMA;
|
|
||||||
mRelativeLuminanceWeight = WEIGHT_RELATIVE_LUMINANCE;
|
|
||||||
mPopulationWeight = WEIGHT_POPULATION;
|
|
||||||
mHueWeight = WEIGHT_HUE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Target(@NonNull Target from) {
|
Target(Target from) {
|
||||||
mTargetRelativeLuminance = from.mTargetRelativeLuminance;
|
System.arraycopy(from.mSaturationTargets, 0, mSaturationTargets, 0,
|
||||||
mChromaWeight = from.mChromaWeight;
|
mSaturationTargets.length);
|
||||||
mRelativeLuminanceWeight = from.mRelativeLuminanceWeight;
|
System.arraycopy(from.mLightnessTargets, 0, mLightnessTargets, 0,
|
||||||
mPopulationWeight = from.mPopulationWeight;
|
mLightnessTargets.length);
|
||||||
mHueWeight = from.mHueWeight;
|
System.arraycopy(from.mWeights, 0, mWeights, 0, mWeights.length);
|
||||||
mChromaTarget = from.mChromaTarget;
|
|
||||||
mChromaMin = from.mChromaMin;
|
|
||||||
mChromaMax = from.mChromaMax;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The relative luminance value for this target. */
|
|
||||||
@FloatRange(from = 0, to = 100)
|
|
||||||
public float getTargetRelativeLuminance() {
|
|
||||||
return mTargetRelativeLuminance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The relative luminance value for this target. */
|
|
||||||
@FloatRange(from = 0, to = 100)
|
|
||||||
public float getTargetPerceptualLuminance() {
|
|
||||||
return Contrast.yToLstar(mTargetRelativeLuminance);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The minimum chroma value for this target. */
|
|
||||||
@FloatRange(from = 0, to = 100)
|
|
||||||
public float getMinimumChroma() {
|
|
||||||
return mChromaMin;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The target chroma value for this target. */
|
|
||||||
@FloatRange(from = 0, to = 100)
|
|
||||||
public float getTargetChroma() {
|
|
||||||
return mChromaTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The maximum chroma value for this target. */
|
|
||||||
@FloatRange(from = 0, to = 130)
|
|
||||||
public float getMaximumChroma() {
|
|
||||||
return mChromaMax;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The target hue value for this target. */
|
|
||||||
@FloatRange(from = 0, to = 100)
|
|
||||||
public float getTargetHue() {
|
|
||||||
return mTargetHue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the weight of importance that this target places on a color's chroma within the
|
* The minimum saturation value for this target.
|
||||||
* image.
|
|
||||||
*
|
|
||||||
* <p>The larger the weight, relative to the other weights, the more important that a color
|
|
||||||
* being
|
|
||||||
* close to the target value has on selection.
|
|
||||||
*
|
|
||||||
* @see #getTargetChroma()
|
|
||||||
*/
|
*/
|
||||||
public float getChromaWeight() {
|
@FloatRange(from = 0, to = 1)
|
||||||
return mChromaWeight;
|
public float getMinimumSaturation() {
|
||||||
|
return mSaturationTargets[INDEX_MIN];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the weight of importance that this target places on a color's lightness within the
|
* The target saturation value for this target.
|
||||||
* image.
|
*/
|
||||||
|
@FloatRange(from = 0, to = 1)
|
||||||
|
public float getTargetSaturation() {
|
||||||
|
return mSaturationTargets[INDEX_TARGET];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum saturation value for this target.
|
||||||
|
*/
|
||||||
|
@FloatRange(from = 0, to = 1)
|
||||||
|
public float getMaximumSaturation() {
|
||||||
|
return mSaturationTargets[INDEX_MAX];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimum lightness value for this target.
|
||||||
|
*/
|
||||||
|
@FloatRange(from = 0, to = 1)
|
||||||
|
public float getMinimumLightness() {
|
||||||
|
return mLightnessTargets[INDEX_MIN];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The target lightness value for this target.
|
||||||
|
*/
|
||||||
|
@FloatRange(from = 0, to = 1)
|
||||||
|
public float getTargetLightness() {
|
||||||
|
return mLightnessTargets[INDEX_TARGET];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum lightness value for this target.
|
||||||
|
*/
|
||||||
|
@FloatRange(from = 0, to = 1)
|
||||||
|
public float getMaximumLightness() {
|
||||||
|
return mLightnessTargets[INDEX_MAX];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the weight of importance that this target places on a color's saturation within
|
||||||
|
* the image.
|
||||||
*
|
*
|
||||||
* <p>The larger the weight, relative to the other weights, the more important that a color
|
* <p>The larger the weight, relative to the other weights, the more important that a color
|
||||||
* being
|
* being close to the target value has on selection.</p>
|
||||||
* close to the target value has on selection.
|
|
||||||
*
|
*
|
||||||
* @see #getTargetRelativeLuminance()
|
* @see #getTargetSaturation()
|
||||||
|
*/
|
||||||
|
public float getSaturationWeight() {
|
||||||
|
return mWeights[INDEX_WEIGHT_SAT];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the weight of importance that this target places on a color's lightness within
|
||||||
|
* the image.
|
||||||
|
*
|
||||||
|
* <p>The larger the weight, relative to the other weights, the more important that a color
|
||||||
|
* being close to the target value has on selection.</p>
|
||||||
|
*
|
||||||
|
* @see #getTargetLightness()
|
||||||
*/
|
*/
|
||||||
public float getLightnessWeight() {
|
public float getLightnessWeight() {
|
||||||
return mRelativeLuminanceWeight;
|
return mWeights[INDEX_WEIGHT_LUMA];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the weight of importance that this target places on a color's population within the
|
* Returns the weight of importance that this target places on a color's population within
|
||||||
* image.
|
* the image.
|
||||||
*
|
*
|
||||||
* <p>The larger the weight, relative to the other weights, the more important that a color's
|
* <p>The larger the weight, relative to the other weights, the more important that a
|
||||||
* population being close to the most populous has on selection.
|
* color's population being close to the most populous has on selection.</p>
|
||||||
*/
|
*/
|
||||||
public float getPopulationWeight() {
|
public float getPopulationWeight() {
|
||||||
return mPopulationWeight;
|
return mWeights[INDEX_WEIGHT_POP];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the weight of importance that this target places on a color's hue.
|
* Returns whether any color selected for this target is exclusive for this target only.
|
||||||
*
|
*
|
||||||
* <p>The larger the weight, relative to the other weights, the more important that a color's
|
* <p>If false, then the color can be selected for other targets.</p>
|
||||||
* hue being close to the desired hue has on selection.
|
|
||||||
*/
|
*/
|
||||||
public float getHueWeight() {
|
public boolean isExclusive() {
|
||||||
return mHueWeight;
|
return mIsExclusive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void setTargetDefaultValues(final float[] values) {
|
||||||
|
values[INDEX_MIN] = 0f;
|
||||||
|
values[INDEX_TARGET] = 0.5f;
|
||||||
|
values[INDEX_MAX] = 1f;
|
||||||
|
}
|
||||||
|
|
||||||
/** Builder class for generating custom {@link Target} instances. */
|
private void setDefaultWeights() {
|
||||||
public static class Builder {
|
mWeights[INDEX_WEIGHT_SAT] = WEIGHT_SATURATION;
|
||||||
|
mWeights[INDEX_WEIGHT_LUMA] = WEIGHT_LUMA;
|
||||||
|
mWeights[INDEX_WEIGHT_POP] = WEIGHT_POPULATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
void normalizeWeights() {
|
||||||
|
float sum = 0;
|
||||||
|
for (int i = 0, z = mWeights.length; i < z; i++) {
|
||||||
|
float weight = mWeights[i];
|
||||||
|
if (weight > 0) {
|
||||||
|
sum += weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sum != 0) {
|
||||||
|
for (int i = 0, z = mWeights.length; i < z; i++) {
|
||||||
|
if (mWeights[i] > 0) {
|
||||||
|
mWeights[i] /= sum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setDefaultDarkLightnessValues(Target target) {
|
||||||
|
target.mLightnessTargets[INDEX_TARGET] = TARGET_DARK_LUMA;
|
||||||
|
target.mLightnessTargets[INDEX_MAX] = MAX_DARK_LUMA;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setDefaultNormalLightnessValues(Target target) {
|
||||||
|
target.mLightnessTargets[INDEX_MIN] = MIN_NORMAL_LUMA;
|
||||||
|
target.mLightnessTargets[INDEX_TARGET] = TARGET_NORMAL_LUMA;
|
||||||
|
target.mLightnessTargets[INDEX_MAX] = MAX_NORMAL_LUMA;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setDefaultLightLightnessValues(Target target) {
|
||||||
|
target.mLightnessTargets[INDEX_MIN] = MIN_LIGHT_LUMA;
|
||||||
|
target.mLightnessTargets[INDEX_TARGET] = TARGET_LIGHT_LUMA;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setDefaultVibrantSaturationValues(Target target) {
|
||||||
|
target.mSaturationTargets[INDEX_MIN] = MIN_VIBRANT_SATURATION;
|
||||||
|
target.mSaturationTargets[INDEX_TARGET] = TARGET_VIBRANT_SATURATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setDefaultMutedSaturationValues(Target target) {
|
||||||
|
target.mSaturationTargets[INDEX_TARGET] = TARGET_MUTED_SATURATION;
|
||||||
|
target.mSaturationTargets[INDEX_MAX] = MAX_MUTED_SATURATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builder class for generating custom {@link Target} instances.
|
||||||
|
*/
|
||||||
|
public final static class Builder {
|
||||||
private final Target mTarget;
|
private final Target mTarget;
|
||||||
|
|
||||||
/** Create a new {@link Target} builder from scratch. */
|
/**
|
||||||
|
* Create a new {@link Target} builder from scratch.
|
||||||
|
*/
|
||||||
public Builder() {
|
public Builder() {
|
||||||
mTarget = new Target();
|
mTarget = new Target();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a new builder based on an existing {@link Target}. */
|
/**
|
||||||
public Builder(@NonNull Target target) {
|
* Create a new builder based on an existing {@link Target}.
|
||||||
|
*/
|
||||||
|
public Builder(Target target) {
|
||||||
mTarget = new Target(target);
|
mTarget = new Target(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Set the minimum chroma value for this target. */
|
|
||||||
@NonNull
|
|
||||||
public Builder setMinimumChroma(@FloatRange(from = 0, to = 100) float value) {
|
|
||||||
mTarget.mChromaMin = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Set the target/ideal chroma value for this target. */
|
|
||||||
@NonNull
|
|
||||||
public Builder setTargetChroma(@FloatRange(from = 0, to = 100) float value) {
|
|
||||||
mTarget.mChromaTarget = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Set the maximum chroma value for this target. */
|
|
||||||
@NonNull
|
|
||||||
public Builder setMaximumChroma(@FloatRange(from = 0, to = 100) float value) {
|
|
||||||
mTarget.mChromaMax = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Set the minimum lightness value for this target, using Y in XYZ color space. */
|
|
||||||
@NonNull
|
|
||||||
public Builder setTargetRelativeLuminance(@FloatRange(from = 0, to = 100) float value) {
|
|
||||||
mTarget.mTargetRelativeLuminance = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Set the minimum lightness value for this target, using L* in LAB color space. */
|
|
||||||
@NonNull
|
|
||||||
public Builder setTargetPerceptualLuminance(@FloatRange(from = 0, to = 100) float value) {
|
|
||||||
mTarget.mTargetRelativeLuminance = Contrast.lstarToY(value);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the hue desired from the target. This hue is not enforced, the only consequence
|
* Set the minimum saturation value for this target.
|
||||||
* is points will be awarded to seed colors the closer they are to this hue.
|
|
||||||
*/
|
*/
|
||||||
@NonNull
|
public Target.Builder setMinimumSaturation(@FloatRange(from = 0, to = 1) float value) {
|
||||||
public Builder setTargetHue(@IntRange(from = 0, to = 360) int hue) {
|
mTarget.mSaturationTargets[INDEX_MIN] = value;
|
||||||
mTarget.mTargetHue = hue;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets lightness value for this target. */
|
|
||||||
@NonNull
|
|
||||||
public Builder setContrastRatio(
|
|
||||||
@FloatRange(from = 1, to = 21) float value,
|
|
||||||
@FloatRange(from = 0, to = 100) float relativeLuminance) {
|
|
||||||
float counterpartY = relativeLuminance;
|
|
||||||
float lstar = Contrast.yToLstar(counterpartY);
|
|
||||||
|
|
||||||
float targetY;
|
|
||||||
if (lstar < 50) {
|
|
||||||
targetY = Contrast.lighterY(counterpartY, value);
|
|
||||||
} else {
|
|
||||||
targetY = Contrast.darkerY(counterpartY, value);
|
|
||||||
}
|
|
||||||
mTarget.mTargetRelativeLuminance = targetY;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the weight of importance that this target will place on chroma values.
|
* Set the target/ideal saturation value for this target.
|
||||||
|
*/
|
||||||
|
public Target.Builder setTargetSaturation(@FloatRange(from = 0, to = 1) float value) {
|
||||||
|
mTarget.mSaturationTargets[INDEX_TARGET] = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the maximum saturation value for this target.
|
||||||
|
*/
|
||||||
|
public Target.Builder setMaximumSaturation(@FloatRange(from = 0, to = 1) float value) {
|
||||||
|
mTarget.mSaturationTargets[INDEX_MAX] = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the minimum lightness value for this target.
|
||||||
|
*/
|
||||||
|
public Target.Builder setMinimumLightness(@FloatRange(from = 0, to = 1) float value) {
|
||||||
|
mTarget.mLightnessTargets[INDEX_MIN] = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the target/ideal lightness value for this target.
|
||||||
|
*/
|
||||||
|
public Target.Builder setTargetLightness(@FloatRange(from = 0, to = 1) float value) {
|
||||||
|
mTarget.mLightnessTargets[INDEX_TARGET] = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the maximum lightness value for this target.
|
||||||
|
*/
|
||||||
|
public Target.Builder setMaximumLightness(@FloatRange(from = 0, to = 1) float value) {
|
||||||
|
mTarget.mLightnessTargets[INDEX_MAX] = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the weight of importance that this target will place on saturation values.
|
||||||
*
|
*
|
||||||
* <p>The larger the weight, relative to the other weights, the more important that a color
|
* <p>The larger the weight, relative to the other weights, the more important that a color
|
||||||
* being close to the target value has on selection.
|
* being close to the target value has on selection.</p>
|
||||||
*
|
*
|
||||||
* <p>A weight of 0 means that it has no weight, and thus has no bearing on the selection.
|
* <p>A weight of 0 means that it has no weight, and thus has no
|
||||||
|
* bearing on the selection.</p>
|
||||||
*
|
*
|
||||||
* @see #setTargetChroma(float)
|
* @see #setTargetSaturation(float)
|
||||||
*/
|
*/
|
||||||
@NonNull
|
public Target.Builder setSaturationWeight(@FloatRange(from = 0) float weight) {
|
||||||
public Builder setChromaWeight(@FloatRange(from = 0) float weight) {
|
mTarget.mWeights[INDEX_WEIGHT_SAT] = weight;
|
||||||
mTarget.mChromaWeight = weight;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,40 +385,51 @@ public final class Target {
|
|||||||
* Set the weight of importance that this target will place on lightness values.
|
* Set the weight of importance that this target will place on lightness values.
|
||||||
*
|
*
|
||||||
* <p>The larger the weight, relative to the other weights, the more important that a color
|
* <p>The larger the weight, relative to the other weights, the more important that a color
|
||||||
* being close to the target value has on selection.
|
* being close to the target value has on selection.</p>
|
||||||
*
|
*
|
||||||
* <p>A weight of 0 means that it has no weight, and thus has no bearing on the selection.
|
* <p>A weight of 0 means that it has no weight, and thus has no
|
||||||
|
* bearing on the selection.</p>
|
||||||
*
|
*
|
||||||
* @see #setTargetRelativeLuminance(float)
|
* @see #setTargetLightness(float)
|
||||||
*/
|
*/
|
||||||
@NonNull
|
public Target.Builder setLightnessWeight(@FloatRange(from = 0) float weight) {
|
||||||
public Builder setLightnessWeight(@FloatRange(from = 0) float weight) {
|
mTarget.mWeights[INDEX_WEIGHT_LUMA] = weight;
|
||||||
mTarget.mRelativeLuminanceWeight = weight;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the weight of importance that this target will place on a color's population within
|
* Set the weight of importance that this target will place on a color's population within
|
||||||
* the
|
* the image.
|
||||||
* image.
|
|
||||||
*
|
*
|
||||||
* <p>The larger the weight, relative to the other weights, the more important that a
|
* <p>The larger the weight, relative to the other weights, the more important that a
|
||||||
* color's
|
* color's population being close to the most populous has on selection.</p>
|
||||||
* population being close to the most populous has on selection.
|
|
||||||
*
|
*
|
||||||
* <p>A weight of 0 means that it has no weight, and thus has no bearing on the selection.
|
* <p>A weight of 0 means that it has no weight, and thus has no
|
||||||
|
* bearing on the selection.</p>
|
||||||
*/
|
*/
|
||||||
@NonNull
|
public Target.Builder setPopulationWeight(@FloatRange(from = 0) float weight) {
|
||||||
public Builder setPopulationWeight(@FloatRange(from = 0) float weight) {
|
mTarget.mWeights[INDEX_WEIGHT_POP] = weight;
|
||||||
mTarget.mPopulationWeight = weight;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether any color selected for this target is exclusive to this target only.
|
||||||
|
* Defaults to true.
|
||||||
|
*
|
||||||
|
* @param exclusive true if any the color is exclusive to this target, or false is the
|
||||||
|
* color can be selected for other targets.
|
||||||
|
*/
|
||||||
|
public Target.Builder setExclusive(boolean exclusive) {
|
||||||
|
mTarget.mIsExclusive = exclusive;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/** Builds and returns the resulting {@link Target}. */
|
/**
|
||||||
@NonNull
|
* Builds and returns the resulting {@link Target}.
|
||||||
|
*/
|
||||||
public Target build() {
|
public Target build() {
|
||||||
return mTarget;
|
return mTarget;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -70,9 +70,10 @@ public class VariationalKMeansQuantizer implements Quantizer {
|
|||||||
*
|
*
|
||||||
* @param pixels Pixels to quantize.
|
* @param pixels Pixels to quantize.
|
||||||
* @param maxColors Maximum number of clusters to extract.
|
* @param maxColors Maximum number of clusters to extract.
|
||||||
|
* @param filters Colors that should be ignored
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void quantize(int[] pixels, int maxColors) {
|
public void quantize(int[] pixels, int maxColors, Palette.Filter[] filters) {
|
||||||
// Start by converting all colors to HSL.
|
// Start by converting all colors to HSL.
|
||||||
// HLS is way more meaningful for clustering than RGB.
|
// HLS is way more meaningful for clustering than RGB.
|
||||||
final float[] hsl = {0, 0, 0};
|
final float[] hsl = {0, 0, 0};
|
||||||
@@ -110,18 +111,16 @@ public class VariationalKMeansQuantizer implements Quantizer {
|
|||||||
|
|
||||||
// Convert data to final format, de-normalizing the hue.
|
// Convert data to final format, de-normalizing the hue.
|
||||||
mQuantizedColors = new ArrayList<>();
|
mQuantizedColors = new ArrayList<>();
|
||||||
float[] mHsl = new float[3];
|
|
||||||
for (KMeans.Mean mean : optimalMeans) {
|
for (KMeans.Mean mean : optimalMeans) {
|
||||||
if (mean.getItems().size() == 0) {
|
if (mean.getItems().size() == 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
float[] centroid = mean.getCentroid();
|
float[] centroid = mean.getCentroid();
|
||||||
|
mQuantizedColors.add(new Palette.Swatch(new float[]{
|
||||||
mHsl[0] = centroid[0] * 360f;
|
centroid[0] * 360f,
|
||||||
mHsl[1] = centroid[1];
|
centroid[1],
|
||||||
mHsl[2] = centroid[2];
|
centroid[2]
|
||||||
int color = ColorUtils.HSLToColor(mHsl);
|
}, mean.getItems().size()));
|
||||||
mQuantizedColors.add(new Palette.Swatch(color, mean.getItems().size()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,269 +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.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A color quantizer based on the Kmeans algorithm.
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
* referred to as "WSMeans", or, "Weighted Square Means" The main advantages of this Kmeans
|
|
||||||
* implementation are taking advantage of triangle properties to avoid distance calculations, as
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
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 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]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxColors > means.length) {
|
|
||||||
int randomMeansToCreate = maxColors - means.length;
|
|
||||||
for (int i = 0; i < randomMeansToCreate; i++) {
|
|
||||||
mMeans[means.length + i] = new Mean(100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int pixel : pixels) {
|
|
||||||
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][];
|
|
||||||
for (int i = 0; i < maxColors; i++) {
|
|
||||||
means[i] = new Mean(upperBound).center;
|
|
||||||
}
|
|
||||||
return means;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void quantize(int[] pixels, int maxColors) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Palette.Swatch> getQuantizedColors() {
|
|
||||||
return mSwatches;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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<>();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int findIndex(double[] list, double element) {
|
|
||||||
for (int i = 0; i < list.length; i++) {
|
|
||||||
if (list[i] == element) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new IllegalArgumentException("Element not in list");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,442 +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.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
// 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
|
|
||||||
*
|
|
||||||
* <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.
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
|
|
||||||
private static final int QUANT_SIZE = 33;
|
|
||||||
private final List<Palette.Swatch> mSwatches = new ArrayList<>();
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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.
|
|
||||||
this.mMaxColors = Math.min(MAX_COLORS, maxColorCount);
|
|
||||||
Box[] cube = new Box[mMaxColors];
|
|
||||||
int red, green, blue;
|
|
||||||
|
|
||||||
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--;
|
|
||||||
}
|
|
||||||
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] = ((red & 0x0ff) << 16) | ((green & 0x0ff) << 8) | (blue & 0x0ff);
|
|
||||||
} else {
|
|
||||||
colors[k] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int bitsPerPixel = 0;
|
|
||||||
while ((1 << bitsPerPixel) < mMaxColors) {
|
|
||||||
bitsPerPixel++;
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
mSwatches.clear();
|
|
||||||
mSwatches.addAll(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];
|
|
||||||
|
|
||||||
for (i = 0; i < 256; i++) {
|
|
||||||
table[i] = i * i;
|
|
||||||
}
|
|
||||||
|
|
||||||
mQadd = new int[mSize];
|
|
||||||
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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];
|
|
||||||
|
|
||||||
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];
|
|
||||||
|
|
||||||
area[b] += line;
|
|
||||||
area_r[b] += line_r;
|
|
||||||
area_g[b] += line_g;
|
|
||||||
area_b[b] += line_b;
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private double getVariance(Box cube) {
|
|
||||||
/* Compute the weighted variance of a box */
|
|
||||||
/* NB: as with the raw statistics, this is really the variance * size */
|
|
||||||
double dr, dg, db, xx;
|
|
||||||
dr = getVolume(cube, mMr);
|
|
||||||
dg = getVolume(cube, mMg);
|
|
||||||
db = getVolume(cube, mMb);
|
|
||||||
xx =
|
|
||||||
mM2[cube.mR1][cube.mG1][cube.mB1]
|
|
||||||
- mM2[cube.mR1][cube.mG1][cube.mB0]
|
|
||||||
- mM2[cube.mR1][cube.mG0][cube.mB1]
|
|
||||||
+ mM2[cube.mR1][cube.mG0][cube.mB0]
|
|
||||||
- mM2[cube.mR0][cube.mG1][cube.mB1]
|
|
||||||
+ mM2[cube.mR0][cube.mG1][cube.mB0]
|
|
||||||
+ mM2[cube.mR0][cube.mG0][cube.mB1]
|
|
||||||
- mM2[cube.mR0][cube.mG0][cube.mB0];
|
|
||||||
return xx - (dr * dr + dg * dg + db * db) / getVolume(cube, mWt);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* We want to minimize the sum of the variances of two subboxes.
|
|
||||||
* The sum(c^2) terms can be ignored since their sum over both subboxes
|
|
||||||
* is the same (the sum for the whole box) no matter where we split.
|
|
||||||
* The remaining terms have a minus sign in the variance formula,
|
|
||||||
* so we drop the minus sign and MAXIMIZE the sum of the two terms.
|
|
||||||
*/
|
|
||||||
private double maximize(
|
|
||||||
Box cube,
|
|
||||||
int dir,
|
|
||||||
int first,
|
|
||||||
int last,
|
|
||||||
int[] cut,
|
|
||||||
long wholeR,
|
|
||||||
long wholeG,
|
|
||||||
long wholeB,
|
|
||||||
long wholeW) {
|
|
||||||
long half_r, half_g, half_b, half_w;
|
|
||||||
long base_r, base_g, base_b, base_w;
|
|
||||||
int i;
|
|
||||||
double temp, max;
|
|
||||||
|
|
||||||
base_r = getBottom(cube, dir, mMr);
|
|
||||||
base_g = getBottom(cube, dir, mMg);
|
|
||||||
base_b = getBottom(cube, dir, mMb);
|
|
||||||
base_w = getBottom(cube, dir, mWt);
|
|
||||||
|
|
||||||
max = 0.0f;
|
|
||||||
cut[0] = -1;
|
|
||||||
|
|
||||||
for (i = first; i < last; ++i) {
|
|
||||||
half_r = base_r + getTop(cube, dir, i, mMr);
|
|
||||||
half_g = base_g + getTop(cube, dir, i, mMg);
|
|
||||||
half_b = base_b + getTop(cube, dir, i, mMb);
|
|
||||||
half_w = base_w + getTop(cube, dir, i, mWt);
|
|
||||||
/* now half_x is sum over lower half of box, if split at i */
|
|
||||||
if (half_w == 0) /* subbox could be empty of pixels! */ {
|
|
||||||
continue; /* never split into an empty box */
|
|
||||||
}
|
|
||||||
temp = (half_r * half_r + half_g * half_g + half_b * half_b) / (double) half_w;
|
|
||||||
half_r = wholeR - half_r;
|
|
||||||
half_g = wholeG - half_g;
|
|
||||||
half_b = wholeB - half_b;
|
|
||||||
half_w = wholeW - half_w;
|
|
||||||
if (half_w == 0) /* subbox could be empty of pixels! */ {
|
|
||||||
continue; /* never split into an empty box */
|
|
||||||
}
|
|
||||||
temp += (half_r * half_r + half_g * half_g + half_b * half_b) / (double) half_w;
|
|
||||||
|
|
||||||
if (temp > max) {
|
|
||||||
max = temp;
|
|
||||||
cut[0] = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return max;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean cut(Box set1, Box set2) {
|
|
||||||
int dir;
|
|
||||||
int[] cutr = new int[1];
|
|
||||||
int[] cutg = new int[1];
|
|
||||||
int[] cutb = new int[1];
|
|
||||||
double maxr, maxg, maxb;
|
|
||||||
long whole_r, whole_g, whole_b, whole_w;
|
|
||||||
|
|
||||||
whole_r = getVolume(set1, mMr);
|
|
||||||
whole_g = getVolume(set1, mMg);
|
|
||||||
whole_b = getVolume(set1, mMb);
|
|
||||||
whole_w = getVolume(set1, mWt);
|
|
||||||
|
|
||||||
maxr = maximize(set1, RED, set1.mR0 + 1, set1.mR1, cutr, whole_r, whole_g, whole_b,
|
|
||||||
whole_w);
|
|
||||||
maxg = maximize(set1, GREEN, set1.mG0 + 1, set1.mG1, cutg, whole_r, whole_g, whole_b,
|
|
||||||
whole_w);
|
|
||||||
maxb = maximize(set1, BLUE, set1.mB0 + 1, set1.mB1, cutb, whole_r, whole_g, whole_b,
|
|
||||||
whole_w);
|
|
||||||
|
|
||||||
if (maxr >= maxg && maxr >= maxb) {
|
|
||||||
dir = RED;
|
|
||||||
if (cutr[0] < 0) return false; /* can't split the box */
|
|
||||||
} else if (maxg >= maxr && maxg >= maxb) {
|
|
||||||
dir = GREEN;
|
|
||||||
} else {
|
|
||||||
dir = BLUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
set2.mR1 = set1.mR1;
|
|
||||||
set2.mG1 = set1.mG1;
|
|
||||||
set2.mB1 = set1.mB1;
|
|
||||||
|
|
||||||
switch (dir) {
|
|
||||||
case RED:
|
|
||||||
set2.mR0 = set1.mR1 = cutr[0];
|
|
||||||
set2.mG0 = set1.mG0;
|
|
||||||
set2.mB0 = set1.mB0;
|
|
||||||
break;
|
|
||||||
case GREEN:
|
|
||||||
set2.mG0 = set1.mG1 = cutg[0];
|
|
||||||
set2.mR0 = set1.mR0;
|
|
||||||
set2.mB0 = set1.mB0;
|
|
||||||
break;
|
|
||||||
case BLUE:
|
|
||||||
set2.mB0 = set1.mB1 = cutb[0];
|
|
||||||
set2.mR0 = set1.mR0;
|
|
||||||
set2.mG0 = set1.mG0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -43,7 +43,6 @@ import java.util.List;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Util class to create the view for a splash screen content.
|
* Util class to create the view for a splash screen content.
|
||||||
*
|
|
||||||
* @hide
|
* @hide
|
||||||
*/
|
*/
|
||||||
public class SplashscreenContentDrawer {
|
public class SplashscreenContentDrawer {
|
||||||
@@ -434,11 +433,8 @@ public class SplashscreenContentDrawer {
|
|||||||
*/
|
*/
|
||||||
private interface ColorTester {
|
private interface ColorTester {
|
||||||
float nonTransparentRatio();
|
float nonTransparentRatio();
|
||||||
|
|
||||||
boolean isComplexColor();
|
boolean isComplexColor();
|
||||||
|
|
||||||
int getDominantColor();
|
int getDominantColor();
|
||||||
|
|
||||||
boolean isGrayscale();
|
boolean isGrayscale();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,17 +511,14 @@ public class SplashscreenContentDrawer {
|
|||||||
// restore to original bounds
|
// restore to original bounds
|
||||||
drawable.setBounds(initialBounds);
|
drawable.setBounds(initialBounds);
|
||||||
|
|
||||||
final Palette.Builder builder;
|
final Palette.Builder builder = new Palette.Builder(bitmap)
|
||||||
|
.maximumColorCount(5).clearFilters();
|
||||||
// The Palette API will ignore Alpha, so it cannot handle transparent pixels, but
|
// The Palette API will ignore Alpha, so it cannot handle transparent pixels, but
|
||||||
// sometimes we will need this information to know if this Drawable object is
|
// sometimes we will need this information to know if this Drawable object is
|
||||||
// transparent.
|
// transparent.
|
||||||
mFilterTransparent = filterTransparent;
|
mFilterTransparent = filterTransparent;
|
||||||
if (mFilterTransparent) {
|
if (mFilterTransparent) {
|
||||||
builder = new Palette.Builder(bitmap, TRANSPARENT_FILTER_QUANTIZER)
|
builder.setQuantizer(TRANSPARENT_FILTER_QUANTIZER);
|
||||||
.maximumColorCount(5);
|
|
||||||
} else {
|
|
||||||
builder = new Palette.Builder(bitmap, null)
|
|
||||||
.maximumColorCount(5);
|
|
||||||
}
|
}
|
||||||
mPalette = builder.generate();
|
mPalette = builder.generate();
|
||||||
bitmap.recycle();
|
bitmap.recycle();
|
||||||
@@ -545,7 +538,7 @@ public class SplashscreenContentDrawer {
|
|||||||
public int getDominantColor() {
|
public int getDominantColor() {
|
||||||
final Palette.Swatch mainSwatch = mPalette.getDominantSwatch();
|
final Palette.Swatch mainSwatch = mPalette.getDominantSwatch();
|
||||||
if (mainSwatch != null) {
|
if (mainSwatch != null) {
|
||||||
return mainSwatch.getInt();
|
return mainSwatch.getRgb();
|
||||||
}
|
}
|
||||||
return Color.BLACK;
|
return Color.BLACK;
|
||||||
}
|
}
|
||||||
@@ -556,7 +549,7 @@ public class SplashscreenContentDrawer {
|
|||||||
if (swatches != null) {
|
if (swatches != null) {
|
||||||
for (int i = swatches.size() - 1; i >= 0; i--) {
|
for (int i = swatches.size() - 1; i >= 0; i--) {
|
||||||
Palette.Swatch swatch = swatches.get(i);
|
Palette.Swatch swatch = swatches.get(i);
|
||||||
if (!isGrayscaleColor(swatch.getInt())) {
|
if (!isGrayscaleColor(swatch.getRgb())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -568,9 +561,9 @@ public class SplashscreenContentDrawer {
|
|||||||
private static final int NON_TRANSPARENT = 0xFF000000;
|
private static final int NON_TRANSPARENT = 0xFF000000;
|
||||||
private final Quantizer mInnerQuantizer = new VariationalKMeansQuantizer();
|
private final Quantizer mInnerQuantizer = new VariationalKMeansQuantizer();
|
||||||
private float mNonTransparentRatio;
|
private float mNonTransparentRatio;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void quantize(final int[] pixels, final int maxColors) {
|
public void quantize(final int[] pixels, final int maxColors,
|
||||||
|
final Palette.Filter[] filters) {
|
||||||
mNonTransparentRatio = 0;
|
mNonTransparentRatio = 0;
|
||||||
int realSize = 0;
|
int realSize = 0;
|
||||||
for (int i = pixels.length - 1; i > 0; i--) {
|
for (int i = pixels.length - 1; i > 0; i--) {
|
||||||
@@ -582,7 +575,7 @@ public class SplashscreenContentDrawer {
|
|||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Slog.d(TAG, "quantize: this is pure transparent image");
|
Slog.d(TAG, "quantize: this is pure transparent image");
|
||||||
}
|
}
|
||||||
mInnerQuantizer.quantize(pixels, maxColors);
|
mInnerQuantizer.quantize(pixels, maxColors, filters);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mNonTransparentRatio = (float) realSize / pixels.length;
|
mNonTransparentRatio = (float) realSize / pixels.length;
|
||||||
@@ -594,7 +587,7 @@ public class SplashscreenContentDrawer {
|
|||||||
rowIndex++;
|
rowIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mInnerQuantizer.quantize(samplePixels, maxColors);
|
mInnerQuantizer.quantize(samplePixels, maxColors, filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Reference in New Issue
Block a user