Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e0970984 authored by James O'Leary's avatar James O'Leary Committed by Android (Google) Code Review
Browse files

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
parents 2eda3de2 bf1b65be
Loading
Loading
Loading
Loading
+100 −14
Original line number Diff line number Diff line
@@ -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;

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

        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) {
+17 −11
Original line number Diff line number Diff line
@@ -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();
    }
+4 −4
Original line number Diff line number Diff line
@@ -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;
+0 −44
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+10 −13
Original line number Diff line number Diff line
@@ -18,21 +18,18 @@ package com.android.internal.graphics.palette;

import android.annotation.ColorInt;

interface CentroidProvider {
/**
     * @return 3 dimensions representing the color
 * Interface that allows quantizers to have a plug-and-play interface for experimenting with
 * quantization in different color spaces.
 */
    float[] getCentroid(@ColorInt int color);
public interface PointProvider {
    /** Convert a color to 3 coordinates representing the color in a color space. */
    float[] fromInt(@ColorInt int argb);

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