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