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