Palette.java revision b14fc7c928307b6758688ed38590bf674c62a01b
1/*
2 * Copyright 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *       http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.v7.graphics;
18
19import android.graphics.Bitmap;
20import android.graphics.Color;
21import android.os.AsyncTask;
22import android.support.v4.os.AsyncTaskCompat;
23
24import java.util.Arrays;
25import java.util.Collections;
26import java.util.List;
27
28/**
29 * A helper class to extract prominent colors from an image.
30 * <p>
31 * A number of colors with different profiles are extracted from the image:
32 * <ul>
33 *     <li>Vibrant</li>
34 *     <li>Vibrant Dark</li>
35 *     <li>Vibrant Light</li>
36 *     <li>Muted</li>
37 *     <li>Muted Dark</li>
38 *     <li>Muted Light</li>
39 * </ul>
40 * These can be retrieved from the appropriate getter method.
41 *
42 * <p>
43 * Instances can be created with the synchronous factory methods {@link #generate(Bitmap)} and
44 * {@link #generate(Bitmap, int)}.
45 * <p>
46 * These should be called on a background thread, ideally the one in
47 * which you load your images on. Sometimes that is not possible, so asynchronous factory methods
48 * have also been provided: {@link #generateAsync(Bitmap, PaletteAsyncListener)} and
49 * {@link #generateAsync(Bitmap, int, PaletteAsyncListener)}. These can be used as so:
50 *
51 * <pre>
52 * Palette.generateAsync(bitmap, new Palette.PaletteAsyncListener() {
53 *     public void onGenerated(Palette palette) {
54 *         // Do something with colors...
55 *     }
56 * });
57 * </pre>
58 */
59public final class Palette {
60
61    /**
62     * Listener to be used with {@link #generateAsync(Bitmap, PaletteAsyncListener)} or
63     * {@link #generateAsync(Bitmap, int, PaletteAsyncListener)}
64     */
65    public interface PaletteAsyncListener {
66
67        /**
68         * Called when the {@link Palette} has been generated.
69         */
70        void onGenerated(Palette palette);
71    }
72
73    private static final int CALCULATE_BITMAP_MIN_DIMENSION = 100;
74    private static final int DEFAULT_CALCULATE_NUMBER_COLORS = 16;
75
76    private static final float TARGET_DARK_LUMA = 0.26f;
77    private static final float MAX_DARK_LUMA = 0.45f;
78
79    private static final float MIN_LIGHT_LUMA = 0.55f;
80    private static final float TARGET_LIGHT_LUMA = 0.74f;
81
82    private static final float MIN_NORMAL_LUMA = 0.3f;
83    private static final float TARGET_NORMAL_LUMA = 0.5f;
84    private static final float MAX_NORMAL_LUMA = 0.7f;
85
86    private static final float TARGET_MUTED_SATURATION = 0.3f;
87    private static final float MAX_MUTED_SATURATION = 0.4f;
88
89    private static final float TARGET_VIBRANT_SATURATION = 1f;
90    private static final float MIN_VIBRANT_SATURATION = 0.35f;
91
92    private final List<Swatch> mSwatches;
93    private final int mHighestPopulation;
94
95    private Swatch mVibrantSwatch;
96    private Swatch mMutedSwatch;
97
98    private Swatch mDarkVibrantSwatch;
99    private Swatch mDarkMutedSwatch;
100
101    private Swatch mLightVibrantSwatch;
102    private Swatch mLightMutedColor;
103
104    /**
105     * Generate a {@link Palette} from a {@link Bitmap} using the default number of colors.
106     */
107    public static Palette generate(Bitmap bitmap) {
108        return generate(bitmap, DEFAULT_CALCULATE_NUMBER_COLORS);
109    }
110
111    /**
112     * Generate a {@link Palette} from a {@link Bitmap} using the specified {@code numColors}.
113     * Good values for {@code numColors} depend on the source image type.
114     * For landscapes, a good values are in the range 12-16. For images which are largely made up
115     * of people's faces then this value should be increased to 24-32.
116     *
117     * @param numColors The maximum number of colors in the generated palette. Increasing this
118     *                  number will increase the time needed to compute the values.
119     */
120    public static Palette generate(Bitmap bitmap, int numColors) {
121        checkBitmapParam(bitmap);
122        checkNumberColorsParam(numColors);
123
124        // First we'll scale down the bitmap so it's shortest dimension is 100px
125        final Bitmap scaledBitmap = scaleBitmapDown(bitmap);
126
127        // Now generate a quantizer from the Bitmap
128        ColorCutQuantizer quantizer = ColorCutQuantizer.fromBitmap(scaledBitmap, numColors);
129
130        // If created a new bitmap, recycle it
131        if (scaledBitmap != bitmap) {
132            scaledBitmap.recycle();
133        }
134
135        // Now return a ColorExtractor instance
136        return new Palette(quantizer.getQuantizedColors());
137    }
138
139    /**
140     * Generate a {@link Palette} asynchronously. {@link PaletteAsyncListener#onGenerated(Palette)}
141     * will be called with the created instance. The resulting {@link Palette} is the same as
142     * what would be created by calling {@link #generate(Bitmap)}.
143     *
144     * @param listener Listener to be invoked when the {@link Palette} has been generated.
145     *
146     * @return the {@link android.os.AsyncTask} used to asynchronously generate the instance.
147     */
148    public static AsyncTask<Bitmap, Void, Palette> generateAsync(
149            Bitmap bitmap, PaletteAsyncListener listener) {
150        return generateAsync(bitmap, DEFAULT_CALCULATE_NUMBER_COLORS, listener);
151    }
152
153    /**
154     * Generate a {@link Palette} asynchronously. {@link PaletteAsyncListener#onGenerated(Palette)}
155     * will be called with the created instance. The resulting {@link Palette} is the same as what
156     * would be created by calling {@link #generate(Bitmap, int)}.
157     *
158     * @param listener Listener to be invoked when the {@link Palette} has been generated.
159     *
160     * @return the {@link android.os.AsyncTask} used to asynchronously generate the instance.
161     */
162    public static AsyncTask<Bitmap, Void, Palette> generateAsync(
163            final Bitmap bitmap, final int numColors, final PaletteAsyncListener listener) {
164        checkBitmapParam(bitmap);
165        checkNumberColorsParam(numColors);
166        checkAsyncListenerParam(listener);
167
168        return AsyncTaskCompat.executeParallel(
169                new AsyncTask<Bitmap, Void, Palette>() {
170                    @Override
171                    protected Palette doInBackground(Bitmap... params) {
172                        return generate(params[0], numColors);
173                    }
174
175                    @Override
176                    protected void onPostExecute(Palette colorExtractor) {
177                        listener.onGenerated(colorExtractor);
178                    }
179                }, bitmap);
180    }
181
182    private Palette(List<Swatch> swatches) {
183        mSwatches = swatches;
184        mHighestPopulation = findMaxPopulation();
185
186        mVibrantSwatch = findColor(TARGET_NORMAL_LUMA, MIN_NORMAL_LUMA, MAX_NORMAL_LUMA,
187                TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1f);
188
189        mLightVibrantSwatch = findColor(TARGET_LIGHT_LUMA, MIN_LIGHT_LUMA, 1f,
190                TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1f);
191
192        mDarkVibrantSwatch = findColor(TARGET_DARK_LUMA, 0f, MAX_DARK_LUMA,
193                TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1f);
194
195        mMutedSwatch = findColor(TARGET_NORMAL_LUMA, MIN_NORMAL_LUMA, MAX_NORMAL_LUMA,
196                TARGET_MUTED_SATURATION, 0f, MAX_MUTED_SATURATION);
197
198        mLightMutedColor = findColor(TARGET_LIGHT_LUMA, MIN_LIGHT_LUMA, 1f,
199                TARGET_MUTED_SATURATION, 0f, MAX_MUTED_SATURATION);
200
201        mDarkMutedSwatch = findColor(TARGET_DARK_LUMA, 0f, MAX_DARK_LUMA,
202                TARGET_MUTED_SATURATION, 0f, MAX_MUTED_SATURATION);
203
204        // Now try and generate any missing colors
205        generateEmptySwatches();
206    }
207
208    /**
209     * Returns all of the swatches which make up the palette.
210     */
211    public List<Swatch> getSwatches() {
212        return Collections.unmodifiableList(mSwatches);
213    }
214
215    /**
216     * Returns the most vibrant swatch in the palette. Might be null.
217     */
218    public Swatch getVibrantSwatch() {
219        return mVibrantSwatch;
220    }
221
222    /**
223     * Returns a light and vibrant swatch from the palette. Might be null.
224     */
225    public Swatch getLightVibrantSwatch() {
226        return mLightVibrantSwatch;
227    }
228
229    /**
230     * Returns a dark and vibrant swatch from the palette. Might be null.
231     */
232    public Swatch getDarkVibrantSwatch() {
233        return mDarkVibrantSwatch;
234    }
235
236    /**
237     * Returns a muted swatch from the palette. Might be null.
238     */
239    public Swatch getMutedSwatch() {
240        return mMutedSwatch;
241    }
242
243    /**
244     * Returns a muted and light swatch from the palette. Might be null.
245     */
246    public Swatch getLightMutedSwatch() {
247        return mLightMutedColor;
248    }
249
250    /**
251     * Returns a muted and dark swatch from the palette. Might be null.
252     */
253    public Swatch getDarkMutedSwatch() {
254        return mDarkMutedSwatch;
255    }
256
257    /**
258     * Returns the most vibrant color in the palette as an RGB packed int.
259     *
260     * @param defaultColor value to return if the swatch isn't available
261     */
262    public int getVibrantColor(int defaultColor) {
263        return mVibrantSwatch != null ? mVibrantSwatch.getRgb() : defaultColor;
264    }
265
266    /**
267     * Returns a light and vibrant color from the palette as an RGB packed int.
268     *
269     * @param defaultColor value to return if the swatch isn't available
270     */
271    public int getLightVibrantColor(int defaultColor) {
272        return mLightVibrantSwatch != null ? mLightVibrantSwatch.getRgb() : defaultColor;
273    }
274
275    /**
276     * Returns a dark and vibrant color from the palette as an RGB packed int.
277     *
278     * @param defaultColor value to return if the swatch isn't available
279     */
280    public int getDarkVibrantColor(int defaultColor) {
281        return mDarkVibrantSwatch != null ? mDarkVibrantSwatch.getRgb() : defaultColor;
282    }
283
284    /**
285     * Returns a muted color from the palette as an RGB packed int.
286     *
287     * @param defaultColor value to return if the swatch isn't available
288     */
289    public int getMutedColor(int defaultColor) {
290        return mMutedSwatch != null ? mMutedSwatch.getRgb() : defaultColor;
291    }
292
293    /**
294     * Returns a muted and light color from the palette as an RGB packed int.
295     *
296     * @param defaultColor value to return if the swatch isn't available
297     */
298    public int getLightMutedColor(int defaultColor) {
299        return mLightMutedColor != null ? mLightMutedColor.getRgb() : defaultColor;
300    }
301
302    /**
303     * Returns a muted and dark color from the palette as an RGB packed int.
304     *
305     * @param defaultColor value to return if the swatch isn't available
306     */
307    public int getDarkMutedColor(int defaultColor) {
308        return mDarkMutedSwatch != null ? mDarkMutedSwatch.getRgb() : defaultColor;
309    }
310
311    /**
312     * @return true if we have already selected {@code swatch}
313     */
314    private boolean isAlreadySelected(Swatch swatch) {
315        return mVibrantSwatch == swatch || mDarkVibrantSwatch == swatch ||
316                mLightVibrantSwatch == swatch || mMutedSwatch == swatch ||
317                mDarkMutedSwatch == swatch || mLightMutedColor == swatch;
318    }
319
320    private Swatch findColor(float targetLuma, float minLuma, float maxLuma,
321                             float targetSaturation, float minSaturation, float maxSaturation) {
322        Swatch max = null;
323        float maxValue = 0f;
324
325        for (Swatch swatch : mSwatches) {
326            final float sat = swatch.getHsl()[1];
327            final float luma = swatch.getHsl()[2];
328
329            if (sat >= minSaturation && sat <= maxSaturation &&
330                    luma >= minLuma && luma <= maxLuma &&
331                    !isAlreadySelected(swatch)) {
332                float thisValue = createComparisonValue(sat, targetSaturation, luma, targetLuma,
333                        swatch.getPopulation(), mHighestPopulation);
334                if (max == null || thisValue > maxValue) {
335                    max = swatch;
336                    maxValue = thisValue;
337                }
338            }
339        }
340
341        return max;
342    }
343
344    /**
345     * Try and generate any missing swatches from the swatches we did find.
346     */
347    private void generateEmptySwatches() {
348        if (mVibrantSwatch == null) {
349            // If we do not have a vibrant color...
350            if (mDarkVibrantSwatch != null) {
351                // ...but we do have a dark vibrant, generate the value by modifying the luma
352                final float[] newHsl = copyHslValues(mDarkVibrantSwatch);
353                newHsl[2] = TARGET_NORMAL_LUMA;
354                mVibrantSwatch = new Swatch(ColorUtils.HSLtoRGB(newHsl), 0);
355            }
356        }
357
358        if (mDarkVibrantSwatch == null) {
359            // If we do not have a dark vibrant color...
360            if (mVibrantSwatch != null) {
361                // ...but we do have a vibrant, generate the value by modifying the luma
362                final float[] newHsl = copyHslValues(mVibrantSwatch);
363                newHsl[2] = TARGET_DARK_LUMA;
364                mDarkVibrantSwatch = new Swatch(ColorUtils.HSLtoRGB(newHsl), 0);
365            }
366        }
367    }
368
369    /**
370     * Find the {@link Swatch} with the highest population value and return the population.
371     */
372    private int findMaxPopulation() {
373        int population = 0;
374        for (Swatch swatch : mSwatches) {
375            population = Math.max(population, swatch.getPopulation());
376        }
377        return population;
378    }
379
380    /**
381     * Scale the bitmap down so that it's smallest dimension is
382     * {@value #CALCULATE_BITMAP_MIN_DIMENSION}px. If {@code bitmap} is smaller than this, than it
383     * is returned.
384     */
385    private static Bitmap scaleBitmapDown(Bitmap bitmap) {
386        final int minDimension = Math.min(bitmap.getWidth(), bitmap.getHeight());
387
388        if (minDimension <= CALCULATE_BITMAP_MIN_DIMENSION) {
389            // If the bitmap is small enough already, just return it
390            return bitmap;
391        }
392
393        final float scaleRatio = CALCULATE_BITMAP_MIN_DIMENSION / (float) minDimension;
394        return Bitmap.createScaledBitmap(bitmap,
395                Math.round(bitmap.getWidth() * scaleRatio),
396                Math.round(bitmap.getHeight() * scaleRatio),
397                false);
398    }
399
400    private static float createComparisonValue(float saturation, float targetSaturation,
401            float luma, float targetLuma,
402            int population, int highestPopulation) {
403        return weightedMean(
404                invertDiff(saturation, targetSaturation), 3f,
405                invertDiff(luma, targetLuma), 6.5f,
406                population / (float) highestPopulation, 0.5f
407        );
408    }
409
410    /**
411     * Copy a {@link Swatch}'s HSL values into a new float[].
412     */
413    private static float[] copyHslValues(Swatch color) {
414        final float[] newHsl = new float[3];
415        System.arraycopy(color.getHsl(), 0, newHsl, 0, 3);
416        return newHsl;
417    }
418
419    /**
420     * Returns a value in the range 0-1. 1 is returned when {@code value} equals the
421     * {@code targetValue} and then decreases as the absolute difference between {@code value} and
422     * {@code targetValue} increases.
423     *
424     * @param value the item's value
425     * @param targetValue the value which we desire
426     */
427    private static float invertDiff(float value, float targetValue) {
428        return 1f - Math.abs(value - targetValue);
429    }
430
431    private static float weightedMean(float... values) {
432        float sum = 0f;
433        float sumWeight = 0f;
434
435        for (int i = 0; i < values.length; i += 2) {
436            float value = values[i];
437            float weight = values[i + 1];
438
439            sum += (value * weight);
440            sumWeight += weight;
441        }
442
443        return sum / sumWeight;
444    }
445
446    private static void checkBitmapParam(Bitmap bitmap) {
447        if (bitmap == null) {
448            throw new IllegalArgumentException("bitmap can not be null");
449        }
450        if (bitmap.isRecycled()) {
451            throw new IllegalArgumentException("bitmap can not be recycled");
452        }
453    }
454
455    private static void checkNumberColorsParam(int numColors) {
456        if (numColors < 1) {
457            throw new IllegalArgumentException("numColors must be 1 of greater");
458        }
459    }
460
461    private static void checkAsyncListenerParam(PaletteAsyncListener listener) {
462        if (listener == null) {
463            throw new IllegalArgumentException("listener can not be null");
464        }
465    }
466
467    /**
468     * Represents a color swatch generated from an image's palette. The RGB color can be retrieved
469     * by calling {@link #getRgb()}.
470     */
471    public static final class Swatch {
472
473        final int mRed, mGreen, mBlue;
474        final int mRgb;
475        final int mPopulation;
476
477        private float[] mHsl;
478
479        Swatch(int rgbColor, int population) {
480            mRed = Color.red(rgbColor);
481            mGreen = Color.green(rgbColor);
482            mBlue = Color.blue(rgbColor);
483            mRgb = rgbColor;
484            mPopulation = population;
485        }
486
487        Swatch(int red, int green, int blue, int population) {
488            mRed = red;
489            mGreen = green;
490            mBlue = blue;
491            mRgb = Color.rgb(red, green, blue);
492            mPopulation = population;
493        }
494
495        /**
496         * @return this swatch's RGB color value
497         */
498        public int getRgb() {
499            return mRgb;
500        }
501
502        /**
503         * Return this swatch's HSL values.
504         *     hsv[0] is Hue [0 .. 360)
505         *     hsv[1] is Saturation [0...1]
506         *     hsv[2] is Lightness [0...1]
507         */
508        public float[] getHsl() {
509            if (mHsl == null) {
510                // Lazily generate HSL values from RGB
511                mHsl = new float[3];
512                ColorUtils.RGBtoHSL(mRed, mGreen, mBlue, mHsl);
513            }
514            return mHsl;
515        }
516
517        /**
518         * @return the number of pixels represented by this swatch
519         */
520        public int getPopulation() {
521            return mPopulation;
522        }
523
524        @Override
525        public String toString() {
526            return new StringBuilder(getClass().getSimpleName()).append(" ")
527                    .append("[").append(Integer.toHexString(getRgb())).append(']')
528                    .append("[HSL: ").append(Arrays.toString(getHsl())).append(']')
529                    .append("[Population: ").append(mPopulation).append(']').toString();
530        }
531    }
532
533}
534