1 /* 2 * Copyright (C) 2017 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 package com.android.launcher3.graphics; 17 18 import android.graphics.Bitmap; 19 import android.graphics.Color; 20 import android.util.SparseArray; 21 22 /** 23 * Utility class for extracting colors from a bitmap. 24 */ 25 public class ColorExtractor { 26 27 public static int findDominantColorByHue(Bitmap bitmap) { 28 return findDominantColorByHue(bitmap, 20); 29 } 30 31 /** 32 * This picks a dominant color, looking for high-saturation, high-value, repeated hues. 33 * @param bitmap The bitmap to scan 34 * @param samples The approximate max number of samples to use. 35 */ 36 public static int findDominantColorByHue(Bitmap bitmap, int samples) { 37 final int height = bitmap.getHeight(); 38 final int width = bitmap.getWidth(); 39 int sampleStride = (int) Math.sqrt((height * width) / samples); 40 if (sampleStride < 1) { 41 sampleStride = 1; 42 } 43 44 // This is an out-param, for getting the hsv values for an rgb 45 float[] hsv = new float[3]; 46 47 // First get the best hue, by creating a histogram over 360 hue buckets, 48 // where each pixel contributes a score weighted by saturation, value, and alpha. 49 float[] hueScoreHistogram = new float[360]; 50 float highScore = -1; 51 int bestHue = -1; 52 53 int[] pixels = new int[samples]; 54 int pixelCount = 0; 55 56 for (int y = 0; y < height; y += sampleStride) { 57 for (int x = 0; x < width; x += sampleStride) { 58 int argb = bitmap.getPixel(x, y); 59 int alpha = 0xFF & (argb >> 24); 60 if (alpha < 0x80) { 61 // Drop mostly-transparent pixels. 62 continue; 63 } 64 // Remove the alpha channel. 65 int rgb = argb | 0xFF000000; 66 Color.colorToHSV(rgb, hsv); 67 // Bucket colors by the 360 integer hues. 68 int hue = (int) hsv[0]; 69 if (hue < 0 || hue >= hueScoreHistogram.length) { 70 // Defensively avoid array bounds violations. 71 continue; 72 } 73 if (pixelCount < samples) { 74 pixels[pixelCount++] = rgb; 75 } 76 float score = hsv[1] * hsv[2]; 77 hueScoreHistogram[hue] += score; 78 if (hueScoreHistogram[hue] > highScore) { 79 highScore = hueScoreHistogram[hue]; 80 bestHue = hue; 81 } 82 } 83 } 84 85 SparseArray<Float> rgbScores = new SparseArray<>(); 86 int bestColor = 0xff000000; 87 highScore = -1; 88 // Go back over the RGB colors that match the winning hue, 89 // creating a histogram of weighted s*v scores, for up to 100*100 [s,v] buckets. 90 // The highest-scoring RGB color wins. 91 for (int i = 0; i < pixelCount; i++) { 92 int rgb = pixels[i]; 93 Color.colorToHSV(rgb, hsv); 94 int hue = (int) hsv[0]; 95 if (hue == bestHue) { 96 float s = hsv[1]; 97 float v = hsv[2]; 98 int bucket = (int) (s * 100) + (int) (v * 10000); 99 // Score by cumulative saturation * value. 100 float score = s * v; 101 Float oldTotal = rgbScores.get(bucket); 102 float newTotal = oldTotal == null ? score : oldTotal + score; 103 rgbScores.put(bucket, newTotal); 104 if (newTotal > highScore) { 105 highScore = newTotal; 106 // All the colors in the winning bucket are very similar. Last in wins. 107 bestColor = rgb; 108 } 109 } 110 } 111 return bestColor; 112 } 113 } 114