hue查找:整体偏差不会很大,但是对于亮度较高存在误差,精准度不够
lab查找:整体一般,但是精准度较好,不过算法复杂,增加耗时
hue色相查找存在误差,在有限的256色中,匹配的规则需要调整
这里使用lab算法提高精准度
RGB转Lab

fun rGBToLab(r: Int, g: Int, b: Int): DoubleArray { // 处理负值(如-2563864) val labR = if (r < 0) 0f else r / 255f val labG = if (g < 0) 0f else g / 255f val labB = if (b < 0) 0f else b / 255f // 线性化处理(sRGB转换) val rLin = if (labR <= 0.04045) labR / 12.92 else ((labR + 0.055) / 1.055).pow(2.4) val gLin = if (labG <= 0.04045) labG / 12.92 else ((labG + 0.055) / 1.055).pow(2.4) val bLin = if (labB <= 0.04045) labB / 12.92 else ((labB + 0.055) / 1.055).pow(2.4) // XYZ转换(D65标准光源) val x = 0.4124564 * rLin + 0.3575761 * gLin + 0.1804375 * bLin val y = 0.2126729 * rLin + 0.7151522 * gLin + 0.0721750 * bLin val z = 0.0193339 * rLin + 0.1191920 * gLin + 0.9503041 * bLin // Lab转换 val xn = 0.95047f val yn = 1.0f val zn = 1.08883f val fX = x / xn val fY = y / yn val fZ = z / zn val l = 116f * fY.pow(1 / 3.0) - 16 val a = 500f * (fX.pow(1 / 3.0) - fY.pow(1 / 3.0)) val b = 200f * (fY.pow(1 / 3.0) - fZ.pow(1 / 3.0)) return doubleArrayOf(l, a, b) }
对rgb进行转换,这里是简化版,如果需要精确要求极高,需要完善
处理流程跟方案一一样,使用集合缓存
接着使用 ΔE76色差公式 查找色值(另外还有ΔEab欧式距离算法,CIEDE2000色差公式,CIE76)

fun deltaE76(lab1: DoubleArray, lab2: DoubleArray): Double { val (l1, a1, b1) = lab1 val (l2, a2, b2) = lab2 // 计算中间变量 val c1 = sqrt(a1.pow(2) + b1.pow(2)) val c2 = sqrt(a2.pow(2) + b2.pow(2)) val cAvg = (c1 + c2) / 2 val g = 0.5 * (1 - sqrt(cAvg.pow(7) / (cAvg.pow(7) + 25f.pow(7)))) val a1p = a1 * (1 + g) val a2p = a2 * (1 + g) val c1p = sqrt(a1p.pow(2) + b1.pow(2)) val c2p = sqrt(a2p.pow(2) + b2.pow(2)) val h1p = Math.toDegrees(kotlin.math.atan2(b1, a1p)).let { if (it < 0) it + 360 else it } val h2p = Math.toDegrees(kotlin.math.atan2(b2, a2p)).let { if (it < 0) it + 360 else it } // 色差计算 val deltaLp = l2 - l1 val deltaCp = c2p - c1p val deltaHp = when { c1p * c2p == 0.0 -> 0.0 abs(h2p - h1p) <= 180 -> h2p - h1p h2p <= h1p -> h2p - h1p + 360 else -> h2p - h1p - 360 } val deltaHpS = 2 * sqrt(c1p * c2p) * sin(Math.toRadians(deltaHp) / 2) // 加权计算 val lAvg = (l1 + l2) / 2 val cpAvg = (c1p + c2p) / 2 val hpAvg = when { c1p * c2p == 0.0 -> h1p + h2p abs(h1p - h2p) > 180 -> (h1p + h2p + 360) / 2 else -> (h1p + h2p) / 2 }.let { if (it >= 360) it - 360 else it } val t = 1 - 0.17 * cos(Math.toRadians(hpAvg - 30)) + 0.24 * cos(Math.toRadians(2 * hpAvg)) + 0.32 * cos(Math.toRadians(3 * hpAvg + 6)) - 0.20 * cos(Math.toRadians(4 * hpAvg - 63)) val sl = 1 + (0.015 * (lAvg - 50).pow(2)) / sqrt(20 + (lAvg - 50).pow(2)) val sc = 1 + 0.045 * cpAvg val sh = 1 + 0.015 * cpAvg * t val deltaTheta = 30 * exp(-((hpAvg - 275) / 25).pow(2)) val rc = 2 * sqrt(cpAvg.pow(7) / (cpAvg.pow(7) + 25f.pow(7))) val rt = -rc * sin(2 * Math.toRadians(deltaTheta)) return sqrt( (deltaLp / sl).pow(2) + (deltaCp / sc).pow(2) + (deltaHpS / sh).pow(2) + rt * (deltaCp / sc) * (deltaHpS / sh) ) }

var minIndex = 0 var minDistance = Double.MAX_VALUE var minLab = doubleArrayOf() tableLabPalette.forEachIndexed { index, lab -> val distance = ColorEabLabUtils.deltaE76(lab, list) if (distance < minDistance) { minDistance = distance minLab = lab minIndex = index } }
从上面图中结果可以明显看到,大部分图片中,hue原方案精准度对比lab查找,是有差距的
但是缺点也明显,在某些图片上表现不好,可以看到原方案hue更精准
经过大量调整算法,优化算法,测试下来发现,如果在不通过底层显示屏直接转换色值情况下,都存在误差
而刚好这两种查找结果有一定互补作用,那是否可以取各自的优势,结合结果,达到一个平衡

private fun findLabColor(list: DoubleArray, hue: Float): Int { // 寻找最近颜色(简化版) var minIndex = 0 var minDistance = Double.MAX_VALUE var minLab = doubleArrayOf() tableLabPalette.forEachIndexed { index, lab -> val distance = ColorEabLabUtils.deltaE76(lab, list) if (distance < minDistance) { minDistance = distance minLab = lab minIndex = index } } return if (minDistance < 26) { tableColorList[minIndex] } else { findColor(hue).colorTip } }
在lab精度不够时,可能结果偏差较大,但是hue偏差不会很大,hue只是精度不够,所以这里兼容两种方案
在lab误差较大时,直接采用hue方案计算结果,这样直接弥补了自身的缺点,比如图一中误差小,采用lab方案,提高了精准度,而在图二精度不够时,采用hue,避免误差偏差较大
最终方案是融合方案,不过精度任然存在误差,但本身转换过程结果只有256色,所以不可能达到完美,相比原图取色结果精度是较高

import android.graphics.Bitmap import android.graphics.Color import androidx.palette.graphics.Palette import androidx.palette.graphics.Palette.Swatch import java.lang.Math.toDegrees import java.lang.Math.toRadians import kotlin.math.abs import kotlin.math.atan2 import kotlin.math.cos import kotlin.math.exp import kotlin.math.pow import kotlin.math.sin import kotlin.math.sqrt object ColorEabLabUtils { fun getPerceptuallyDominantColor(bitmap: Bitmap): Int { val palette = Palette.from(bitmap).maximumColorCount(24).clearFilters().generate() val swatches = palette.swatches if (swatches.isEmpty()) return Color.WHITE var bestSwatch: Swatch? = null var maxScore = 0f for (swatch in swatches) { val hsl = swatch.getHsl() val saturation = hsl[1] // 饱和度 (0-1) val luminance = hsl[2] // 亮度 (0-1) val population = swatch.population // 评分公式:人口占比 * 饱和度 * 亮度因子 // 亮度因子确保避免过暗或过亮的颜色(0.1-0.9为理想范围) val luminanceFactor = 1f - abs(luminance - 0.5f) * 1.8f val score = population * saturation * luminanceFactor if (score > maxScore) { maxScore = score bestSwatch = swatch } } return bestSwatch?.rgb ?: palette.getDominantColor(Color.WHITE) } // RGB转Lab ********* fun rGBToLab(r: Int, g: Int, b: Int): DoubleArray { // 处理负值(如-2563864) val labR = if (r < 0) 0f else r / 255f val labG = if (g < 0) 0f else g / 255f val labB = if (b < 0) 0f else b / 255f // 线性化处理(sRGB转换) val rLin = if (labR <= 0.04045) labR / 12.92 else ((labR + 0.055) / 1.055).pow(2.4) val gLin = if (labG <= 0.04045) labG / 12.92 else ((labG + 0.055) / 1.055).pow(2.4) val bLin = if (labB <= 0.04045) labB / 12.92 else ((labB + 0.055) / 1.055).pow(2.4) // XYZ转换(D65标准光源) val x = 0.4124564 * rLin + 0.3575761 * gLin + 0.1804375 * bLin val y = 0.2126729 * rLin + 0.7151522 * gLin + 0.0721750 * bLin val z = 0.0193339 * rLin + 0.1191920 * gLin + 0.9503041 * bLin // Lab转换 val xn = 0.95047f val yn = 1.0f val zn = 1.08883f val fX = x / xn val fY = y / yn val fZ = z / zn val l = 116f * fY.pow(1 / 3.0) - 16 val a = 500f * (fX.pow(1 / 3.0) - fY.pow(1 / 3.0)) val b = 200f * (fY.pow(1 / 3.0) - fZ.pow(1 / 3.0)) return doubleArrayOf(l, a, b) } // ΔEab算法 fun deltaEab(lab1: DoubleArray, lab2: DoubleArray): Double { val dL = lab1[0] - lab2[0] val da = lab1[1] - lab2[1] val db = lab1[2] - lab2[2] return sqrt(dL.pow(2) + da.pow(2) + db.pow(2)) } // 改进的RGB转Lab(包含伽马校正和D65白点) fun rgbToLab(r: Int, g: Int, b: Int): DoubleArray { // 处理负值(如-2563864) val labR = if (r < 0) 0f else r / 255f val labG = if (g < 0) 0f else g / 255f val labB = if (b < 0) 0f else b / 255f // sRGB伽马校正 val rLin = if (labR <= 0.04045) labR / 12.92 else ((labR + 0.055) / 1.055).pow(2.4) val gLin = if (labG <= 0.04045) labG / 12.92 else ((labG + 0.055) / 1.055).pow(2.4) val bLin = if (labB <= 0.04045) labB / 12.92 else ((labB + 0.055) / 1.055).pow(2.4) // D65标准光源XYZ转换 val x = 0.4124564 * rLin + 0.3575761 * gLin + 0.1804375 * bLin val y = 0.2126729 * rLin + 0.7151522 * gLin + 0.0721750 * bLin val z = 0.0193339 * rLin + 0.1191920 * gLin + 0.9503041 * bLin // Lab转换(包含阈值处理) val xn = 0.95047 val yn = 1.0 val zn = 1.08883 val fX = x / xn val fY = y / yn val fZ = z / zn val l = if (fY > 0.008856) 116 * fY.pow(1 / 3.0) - 16 else 903.3 * fY val a = 500 * (fX.pow(1 / 3.0) - fY.pow(1 / 3.0)) val b = 200 * (fY.pow(1 / 3.0) - fZ.pow(1 / 3.0)) return doubleArrayOf(l, a, b) } // 改进的ΔE76色差公式(包含亮度权重) ********* fun deltaE76(lab1: DoubleArray, lab2: DoubleArray): Double { val (l1, a1, b1) = lab1 val (l2, a2, b2) = lab2 // 计算中间变量 val c1 = sqrt(a1.pow(2) + b1.pow(2)) val c2 = sqrt(a2.pow(2) + b2.pow(2)) val cAvg = (c1 + c2) / 2 val g = 0.5 * (1 - sqrt(cAvg.pow(7) / (cAvg.pow(7) + 25f.pow(7)))) val a1p = a1 * (1 + g) val a2p = a2 * (1 + g) val c1p = sqrt(a1p.pow(2) + b1.pow(2)) val c2p = sqrt(a2p.pow(2) + b2.pow(2)) val h1p = Math.toDegrees(kotlin.math.atan2(b1, a1p)).let { if (it < 0) it + 360 else it } val h2p = Math.toDegrees(kotlin.math.atan2(b2, a2p)).let { if (it < 0) it + 360 else it } // 色差计算 val deltaLp = l2 - l1 val deltaCp = c2p - c1p val deltaHp = when { c1p * c2p == 0.0 -> 0.0 abs(h2p - h1p) <= 180 -> h2p - h1p h2p <= h1p -> h2p - h1p + 360 else -> h2p - h1p - 360 } val deltaHpS = 2 * sqrt(c1p * c2p) * sin(Math.toRadians(deltaHp) / 2) // 加权计算 val lAvg = (l1 + l2) / 2 val cpAvg = (c1p + c2p) / 2 val hpAvg = when { c1p * c2p == 0.0 -> h1p + h2p abs(h1p - h2p) > 180 -> (h1p + h2p + 360) / 2 else -> (h1p + h2p) / 2 }.let { if (it >= 360) it - 360 else it } val t = 1 - 0.17 * cos(Math.toRadians(hpAvg - 30)) + 0.24 * cos(Math.toRadians(2 * hpAvg)) + 0.32 * cos(Math.toRadians(3 * hpAvg + 6)) - 0.20 * cos(Math.toRadians(4 * hpAvg - 63)) val sl = 1 + (0.015 * (lAvg - 50).pow(2)) / sqrt(20 + (lAvg - 50).pow(2)) val sc = 1 + 0.045 * cpAvg val sh = 1 + 0.015 * cpAvg * t val deltaTheta = 30 * exp(-((hpAvg - 275) / 25).pow(2)) val rc = 2 * sqrt(cpAvg.pow(7) / (cpAvg.pow(7) + 25f.pow(7))) val rt = -rc * sin(2 * Math.toRadians(deltaTheta)) return sqrt( (deltaLp / sl).pow(2) + (deltaCp / sc).pow(2) + (deltaHpS / sh).pow(2) + rt * (deltaCp / sc) * (deltaHpS / sh) ) } /** * CIEDE2000色差公式实现 * 更符合人眼感知的颜色差异计算 * @param lab1 第一个颜色的Lab值 * @param lab2 第二个颜色的Lab值 * @return Double 色差值(ΔE越小颜色越接近) */ fun deltaE2000(lab1: DoubleArray, lab2: DoubleArray): Double { val (l1, a1, b1) = lab1 val (l2, a2, b2) = lab2 // 步骤1:计算色度 val c1 = sqrt(a1.pow(2) + b1.pow(2)) val c2 = sqrt(a2.pow(2) + b2.pow(2)) val cAvg = (c1 + c2) / 2.0 // 步骤2:计算补偿因子 val g = 0.5 * (1.0 - sqrt(cAvg.pow(7) / (cAvg.pow(7) + 25.0.pow(7)))) val a1Prime = a1 * (1.0 + g) val a2Prime = a2 * (1.0 + g) val c1Prime = sqrt(a1Prime.pow(2) + b1.pow(2)) val c2Prime = sqrt(a2Prime.pow(2) + b2.pow(2)) val cBarPrime = (c1Prime + c2Prime) / 2.0 // 步骤3:计算色调角 val h1Prime = toDegrees(atan2(b1, a1Prime)).let { if (it < 0) it + 360.0 else it } val h2Prime = toDegrees(atan2(b2, a2Prime)).let { if (it < 0) it + 360.0 else it } // 步骤4:计算基本差异 val deltaLPrime = l2 - l1 val deltaCPrime = c2Prime - c1Prime // 步骤5:计算色调差 var deltaHPrime = 0.0 if (c1Prime * c2Prime != 0.0) { val deltaHPrimeRaw = when { abs(h2Prime - h1Prime) <= 180.0 -> h2Prime - h1Prime h2Prime <= h1Prime -> h2Prime - h1Prime + 360.0 else -> h2Prime - h1Prime - 360.0 } deltaHPrime = 2.0 * sqrt(c1Prime * c2Prime) * sin(toRadians(deltaHPrimeRaw) / 2.0) } // 步骤6:计算加权平均值 val lBar = (l1 + l2) / 2.0 val cBar = (c1 + c2) / 2.0 // 步骤7:计算色调平均值 val hBarPrime = when { abs(h1Prime - h2Prime) > 180.0 -> (h1Prime + h2Prime + 360.0) / 2.0 else -> (h1Prime + h2Prime) / 2.0 } // 步骤8:计算补偿项 val t = 1.0 - 0.17 * cos(toRadians(hBarPrime - 30.0)) + 0.24 * cos(toRadians(2.0 * hBarPrime)) + 0.32 * cos(toRadians(3.0 * hBarPrime + 6.0)) - 0.20 * cos(toRadians(4.0 * hBarPrime - 63.0)) // 步骤9:计算旋转项 val deltaTheta = 30.0 * exp(-((hBarPrime - 275.0) / 25.0).pow(2)) val rc = 2.0 * sqrt(cBarPrime.pow(7) / (cBarPrime.pow(7) + 25.0.pow(7))) val rt = -rc * sin(2.0 * toRadians(deltaTheta)) // 步骤10:计算权重因子 val sl = 1.0 + (0.015 * (lBar - 50.0).pow(2) / sqrt(20.0 + (lBar - 50.0).pow(2))) val sc = 1.0 + 0.045 * cBarPrime val sh = 1.0 + 0.015 * cBarPrime * t // 步骤11:最终色差计算 val term1 = (deltaLPrime / sl).pow(2) val term2 = (deltaCPrime / sc).pow(2) val term3 = (deltaHPrime / sh).pow(2) val term4 = rt * (deltaCPrime / sc) * (deltaHPrime / sh) return sqrt(term1 + term2 + term3 + term4) } // 优化的伽马校正函数 fun preciseGammaExpand(c: Double) = if (c <= 0.04045) { c / 12.92 } else { ((c + 0.055) / 1.055).pow(2.4) } // 优化的Lab转换函数 fun preciseF(t: Double) = if (t > 0.008856) { t.pow(1.0 / 3.0) } else { (7.787 * t) + (16.0 / 116.0) } /** * 优化的RGB转Lab转换方法 * 使用更精确的浮点数计算和标准化处理 */ fun rgbToLabOptimized(r: Int, g: Int, b: Int): DoubleArray { // 使用更精确的归一化 val rNormalized = r / 255.0 val gNormalized = g / 255.0 val bNormalized = b / 255.0 // 应用伽马校正 val rLinear = preciseGammaExpand(rNormalized) val gLinear = preciseGammaExpand(gNormalized) val bLinear = preciseGammaExpand(bNormalized) // 使用D65标准光源的精确XYZ转换矩阵 val x = 0.4124564 * rLinear + 0.3575761 * gLinear + 0.1804375 * bLinear val y = 0.2126729 * rLinear + 0.7151522 * gLinear + 0.0721750 * bLinear val z = 0.0193339 * rLinear + 0.1191920 * gLinear + 0.9503041 * bLinear // 使用CIE标准参考白点 val refX = 0.95047 val refY = 1.00000 val refZ = 1.08883 // 计算相对值 val xRelative = x / refX val yRelative = y / refY val zRelative = z / refZ val fx = preciseF(xRelative) val fy = preciseF(yRelative) val fz = preciseF(zRelative) // 计算Lab值 val l = (116.0 * fy) - 16.0 val a = 500.0 * (fx - fy) val bLab = 200.0 * (fy - fz) return doubleArrayOf(l, a, bLab) } /** * 完整的CIEDE2000色差公式实现 * 包含所有补偿项和旋转项 */ fun deltaE2000Complete(lab1: DoubleArray, lab2: DoubleArray): Double { val (l1, a1, b1) = lab1 val (l2, a2, b2) = lab2 // 步骤1:计算色度值 val c1 = sqrt(a1.pow(2) + b1.pow(2)) val c2 = sqrt(a2.pow(2) + b2.pow(2)) // 步骤2:计算平均值 val lBar = (l1 + l2) / 2.0 val cBar = (c1 + c2) / 2.0 // 步骤3:计算补偿因子 val g = 0.5 * (1.0 - sqrt(cBar.pow(7) / (cBar.pow(7) + 25.0.pow(7)))) // 步骤4:计算调整后的a'值 val a1Prime = a1 * (1.0 + g) val a2Prime = a2 * (1.0 + g) // 步骤5:计算调整后的色度值 val c1Prime = sqrt(a1Prime.pow(2) + b1.pow(2)) val c2Prime = sqrt(a2Prime.pow(2) + b2.pow(2)) val cBarPrime = (c1Prime + c2Prime) / 2.0 // 步骤6:计算色调角 val h1Prime = toDegrees(atan2(b1, a1Prime)).let { if (it < 0) it + 360.0 else it } val h2Prime = toDegrees(atan2(b2, a2Prime)).let { if (it < 0) it + 360.0 else it } // 步骤7:计算基本差异 val deltaLPrime = l2 - l1 val deltaCPrime = c2Prime - c1Prime // 步骤8:计算色调差 var deltaHPrime = 0.0 if (c1Prime != 0.0 && c2Prime != 0.0) { val deltaHPrimeRaw = when { abs(h2Prime - h1Prime) <= 180.0 -> h2Prime - h1Prime h2Prime <= h1Prime -> h2Prime - h1Prime + 360.0 else -> h2Prime - h1Prime - 360.0 } deltaHPrime = 2.0 * sqrt(c1Prime * c2Prime) * sin(toRadians(deltaHPrimeRaw) / 2.0) } // 步骤9:计算色调平均值 val hBarPrime = when { abs(h1Prime - h2Prime) > 180.0 -> (h1Prime + h2Prime + 360.0) / 2.0 else -> (h1Prime + h2Prime) / 2.0 } // 步骤10:计算补偿项 val t = 1.0 - 0.17 * cos(toRadians(hBarPrime - 30.0)) + 0.24 * cos(toRadians(2.0 * hBarPrime)) + 0.32 * cos(toRadians(3.0 * hBarPrime + 6.0)) - 0.20 * cos(toRadians(4.0 * hBarPrime - 63.0)) // 步骤11:计算旋转项 val deltaTheta = 30.0 * exp(-((hBarPrime - 275.0) / 25.0).pow(2)) val rc = 2.0 * sqrt(cBarPrime.pow(7) / (cBarPrime.pow(7) + 25.0.pow(7))) val rt = -rc * sin(2.0 * toRadians(deltaTheta)) // 步骤12:计算权重因子 val sl = 1.0 + (0.015 * (lBar - 50.0).pow(2) / sqrt(20.0 + (lBar - 50.0).pow(2))) val sc = 1.0 + 0.045 * cBarPrime val sh = 1.0 + 0.015 * cBarPrime * t // 步骤13:最终色差计算 val term1 = (deltaLPrime / sl).pow(2) val term2 = (deltaCPrime / sc).pow(2) val term3 = (deltaHPrime / sh).pow(2) val term4 = rt * (deltaCPrime / sc) * (deltaHPrime / sh) return sqrt(term1 + term2 + term3 + term4) } }

{
"list": [
{
"r": 0,
"g": 84,
"b": 255
},
{
"r": 18,
"g": 92,
"b": 255
},
{
"r": 65,
"g": 82,
"b": 255
},
{
"r": 90,
"g": 81,
"b": 255
},
{
"r": 110,
"g": 80,
"b": 255
},
{
"r": 127,
"g": 78,
"b": 255
},
{
"r": 142,
"g": 77,
"b": 255
},
{
"r": 157,
"g": 76,
"b": 255
},
{
"r": 170,
"g": 74,
"b": 255
},
{
"r": 197,
"g": 70,
"b": 255
},
{
"r": 209,
"g": 68,
"b": 255
},
{
"r": 237,
"g": 62,
"b": 255
},
{
"r": 255,
"g": 51,
"b": 244
},
{
"r": 255,
"g": 35,
"b": 219
},
{
"r": 255,
"g": 11,
"b": 196
},
{
"r": 255,
"g": 0,
"b": 176
},
{
"r": 255,
"g": 48,
"b": 158
},
{
"r": 255,
"g": 19,
"b": 142
},
{
"r": 255,
"g": 15,
"b": 122
},
{
"r": 255,
"g": 0,
"b": 99
},
{
"r": 255,
"g": 6,
"b": 75
},
{
"r": 255,
"g": 9,
"b": 55
},
{
"r": 255,
"g": 6,
"b": 0
},
{
"r": 255,
"g": 28,
"b": 0
},
{
"r": 255,
"g": 62,
"b": 0
},
{
"r": 255,
"g": 85,
"b": 0
},
{
"r": 255,
"g": 105,
"b": 0
},
{
"r": 255,
"g": 125,
"b": 0
},
{
"r": 255,
"g": 155,
"b": 5
},
{
"r": 255,
"g": 171,
"b": 41
},
{
"r": 255,
"g": 180,
"b": 15
},
{
"r": 255,
"g": 192,
"b": 19
},
{
"r": 255,
"g": 206,
"b": 45
},
{
"r": 255,
"g": 218,
"b": 27
},
{
"r": 255,
"g": 231,
"b": 30
},
{
"r": 255,
"g": 244,
"b": 34
},
{
"r": 251,
"g": 255,
"b": 37
},
{
"r": 235,
"g": 255,
"b": 38
},
{
"r": 219,
"g": 255,
"b": 39
},
{
"r": 203,
"g": 255,
"b": 40
},
{
"r": 185,
"g": 255,
"b": 41
},
{
"r": 165,
"g": 255,
"b": 42
},
{
"r": 142,
"g": 255,
"b": 42
},
{
"r": 109,
"g": 255,
"b": 29
},
{
"r": 69,
"g": 255,
"b": 44
},
{
"r": 24,
"g": 255,
"b": 46
},
{
"r": 22,
"g": 255,
"b": 32
},
{
"r": 0,
"g": 255,
"b": 102
},
{
"r": 0,
"g": 255,
"b": 136
},
{
"r": 6,
"g": 209,
"b": 128
},
{
"r": 0,
"g": 255,
"b": 189
},
{
"r": 0,
"g": 255,
"b": 212
},
{
"r": 0,
"g": 255,
"b": 234
},
{
"r": 0,
"g": 254,
"b": 255
},
{
"r": 0,
"g": 234,
"b": 255
},
{
"r": 0,
"g": 200,
"b": 255
},
{
"r": 0,
"g": 186,
"b": 255
},
{
"r": 0,
"g": 173,
"b": 255
},
{
"r": 72,
"g": 164,
"b": 237
},
{
"r": 2,
"g": 146,
"b": 255
},
{
"r": 0,
"g": 138,
"b": 255
},
{
"r": 0,
"g": 127,
"b": 255
},
{
"r": 0,
"g": 106,
"b": 255
},
{
"r": 255,
"g": 255,
"b": 255
},
{
"r": 78,
"g": 85,
"b": 255
},
{
"r": 100,
"g": 150,
"b": 255
},
{
"r": 98,
"g": 204,
"b": 255
},
{
"r": 103,
"g": 172,
"b": 156
},
{
"r": 84,
"g": 255,
"b": 167
},
{
"r": 58,
"g": 255,
"b": 112
},
{
"r": 52,
"g": 255,
"b": 88
},
{
"r": 149,
"g": 95,
"b": 255
},
{
"r": 162,
"g": 152,
"b": 255
},
{
"r": 171,
"g": 228,
"b": 255
},
{
"r": 159,
"g": 255,
"b": 201
},
{
"r": 130,
"g": 255,
"b": 142
},
{
"r": 108,
"g": 255,
"b": 99
},
{
"r": 97,
"g": 255,
"b": 74
},
{
"r": 79,
"g": 255,
"b": 41
},
{
"r": 236,
"g": 101,
"b": 254
},
{
"r": 254,
"g": 169,
"b": 255
},
{
"r": 225,
"g": 255,
"b": 170
},
{
"r": 184,
"g": 255,
"b": 155
},
{
"r": 155,
"g": 255,
"b": 75
},
{
"r": 133,
"g": 255,
"b": 45
},
{
"r": 255,
"g": 86,
"b": 196
},
{
"r": 255,
"g": 130,
"b": 171
},
{
"r": 255,
"g": 175,
"b": 146
},
{
"r": 255,
"g": 221,
"b": 119
},
{
"r": 235,
"g": 255,
"b": 95
},
{
"r": 203,
"g": 255,
"b": 51
},
{
"r": 255,
"g": 68,
"b": 138
},
{
"r": 255,
"g": 104,
"b": 117
},
{
"r": 255,
"g": 140,
"b": 95
},
{
"r": 255,
"g": 187,
"b": 85
},
{
"r": 255,
"g": 217,
"b": 49
},
{
"r": 255,
"g": 25,
"b": 115
},
{
"r": 255,
"g": 40,
"b": 96
},
{
"r": 255,
"g": 85,
"b": 78
},
{
"r": 255,
"g": 116,
"b": 59
},
{
"r": 255,
"g": 148,
"b": 40
},
{
"r": 255,
"g": 45,
"b": 62
},
{
"r": 255,
"g": 81,
"b": 54
},
{
"r": 255,
"g": 98,
"b": 33
},
{
"r": 255,
"g": 22,
"b": 36
},
{
"r": 255,
"g": 49,
"b": 35
},
{
"r": 255,
"g": 32,
"b": 23
},
{
"r": 35,
"g": 55,
"b": 255
},
{
"r": 35,
"g": 73,
"b": 255
},
{
"r": 35,
"g": 94,
"b": 255
},
{
"r": 35,
"g": 116,
"b": 255
},
{
"r": 65,
"g": 145,
"b": 255
},
{
"r": 35,
"g": 182,
"b": 255
},
{
"r": 35,
"g": 214,
"b": 255
},
{
"r": 2,
"g": 251,
"b": 255
},
{
"r": 0,
"g": 255,
"b": 223
},
{
"r": 0,
"g": 255,
"b": 203
},
{
"r": 22,
"g": 255,
"b": 168
},
{
"r": 54,
"g": 255,
"b": 144
},
{
"r": 58,
"g": 55,
"b": 255
},
{
"r": 59,
"g": 86,
"b": 255
},
{
"r": 61,
"g": 120,
"b": 255
},
{
"r": 64,
"g": 175,
"b": 255
},
{
"r": 66,
"g": 225,
"b": 255
},
{
"r": 65,
"g": 255,
"b": 246
},
{
"r": 83,
"g": 255,
"b": 211
},
{
"r": 51,
"g": 255,
"b": 181
},
{
"r": 46,
"g": 255,
"b": 156
},
{
"r": 41,
"g": 255,
"b": 134
},
{
"r": 37,
"g": 255,
"b": 115
},
{
"r": 83,
"g": 56,
"b": 255
},
{
"r": 84,
"g": 67,
"b": 255
},
{
"r": 85,
"g": 77,
"b": 255
},
{
"r": 86,
"g": 104,
"b": 255
},
{
"r": 89,
"g": 113,
"b": 255
},
{
"r": 90,
"g": 126,
"b": 255
},
{
"r": 94,
"g": 170,
"b": 255
},
{
"r": 102,
"g": 242,
"b": 255
},
{
"r": 84,
"g": 255,
"b": 193
},
{
"r": 68,
"g": 255,
"b": 140
},
{
"r": 56,
"g": 255,
"b": 101
},
{
"r": 110,
"g": 58,
"b": 255
},
{
"r": 112,
"g": 68,
"b": 255
},
{
"r": 116,
"g": 91,
"b": 255
},
{
"r": 120,
"g": 117,
"b": 255
},
{
"r": 124,
"g": 145,
"b": 255
},
{
"r": 130,
"g": 176,
"b": 255
},
{
"r": 149,
"g": 222,
"b": 255
},
{
"r": 142,
"g": 253,
"b": 255
},
{
"r": 127,
"g": 255,
"b": 216
},
{
"r": 114,
"g": 255,
"b": 183
},
{
"r": 103,
"g": 255,
"b": 156
},
{
"r": 94,
"g": 255,
"b": 132
},
{
"r": 86,
"g": 255,
"b": 111
},
{
"r": 78,
"g": 255,
"b": 93
},
{
"r": 72,
"g": 255,
"b": 77
},
{
"r": 87,
"g": 255,
"b": 71
},
{
"r": 62,
"g": 255,
"b": 51
},
{
"r": 143,
"g": 70,
"b": 255
},
{
"r": 140,
"g": 112,
"b": 255
},
{
"r": 170,
"g": 186,
"b": 255
},
{
"r": 178,
"g": 255,
"b": 240
},
{
"r": 143,
"g": 255,
"b": 169
},
{
"r": 118,
"g": 255,
"b": 119
},
{
"r": 100,
"g": 255,
"b": 82
},
{
"r": 85,
"g": 255,
"b": 53
},
{
"r": 74,
"g": 255,
"b": 30
},
{
"r": 178,
"g": 72,
"b": 255
},
{
"r": 192,
"g": 105,
"b": 255
},
{
"r": 195,
"g": 127,
"b": 255
},
{
"r": 205,
"g": 160,
"b": 255
},
{
"r": 216,
"g": 198,
"b": 255
},
{
"r": 191,
"g": 255,
"b": 186
},
{
"r": 173,
"g": 255,
"b": 155
},
{
"r": 162,
"g": 255,
"b": 125
},
{
"r": 143,
"g": 255,
"b": 106
},
{
"r": 130,
"g": 255,
"b": 91
},
{
"r": 121,
"g": 255,
"b": 71
},
{
"r": 112,
"g": 255,
"b": 56
},
{
"r": 104,
"g": 255,
"b": 43
},
{
"r": 93,
"g": 255,
"b": 37
},
{
"r": 217,
"g": 74,
"b": 255
},
{
"r": 242,
"g": 140,
"b": 255
},
{
"r": 255,
"g": 199,
"b": 240
},
{
"r": 203,
"g": 255,
"b": 140
},
{
"r": 169,
"g": 255,
"b": 94
},
{
"r": 143,
"g": 255,
"b": 59
},
{
"r": 124,
"g": 255,
"b": 33
},
{
"r": 255,
"g": 84,
"b": 255
},
{
"r": 255,
"g": 112,
"b": 255
},
{
"r": 255,
"g": 123,
"b": 222
},
{
"r": 255,
"g": 148,
"b": 208
},
{
"r": 255,
"g": 166,
"b": 205
},
{
"r": 255,
"g": 194,
"b": 204
},
{
"r": 255,
"g": 224,
"b": 166
},
{
"r": 255,
"g": 251,
"b": 151
},
{
"r": 233,
"g": 255,
"b": 125
},
{
"r": 210,
"g": 255,
"b": 91
},
{
"r": 194,
"g": 255,
"b": 81
},
{
"r": 179,
"g": 255,
"b": 63
},
{
"r": 154,
"g": 255,
"b": 32
},
{
"r": 158,
"g": 255,
"b": 37
},
{
"r": 255,
"g": 65,
"b": 208
},
{
"r": 255,
"g": 108,
"b": 184
},
{
"r": 255,
"g": 152,
"b": 159
},
{
"r": 255,
"g": 198,
"b": 133
},
{
"r": 255,
"g": 253,
"b": 95
},
{
"r": 220,
"g": 255,
"b": 67
},
{
"r": 188,
"g": 255,
"b": 36
},
{
"r": 255,
"g": 57,
"b": 176
},
{
"r": 255,
"g": 76,
"b": 164
},
{
"r": 255,
"g": 96,
"b": 153
},
{
"r": 255,
"g": 125,
"b": 150
},
{
"r": 255,
"g": 136,
"b": 130
},
{
"r": 255,
"g": 166,
"b": 126
},
{
"r": 255,
"g": 176,
"b": 106
},
{
"r": 255,
"g": 197,
"b": 94
},
{
"r": 255,
"g": 223,
"b": 88
},
{
"r": 255,
"g": 240,
"b": 69
},
{
"r": 248,
"g": 255,
"b": 56
},
{
"r": 228,
"g": 255,
"b": 38
},
{
"r": 255,
"g": 36,
"b": 167
},
{
"r": 255,
"g": 88,
"b": 134
},
{
"r": 255,
"g": 122,
"b": 106
},
{
"r": 255,
"g": 160,
"b": 77
},
{
"r": 255,
"g": 207,
"b": 86
},
{
"r": 255,
"g": 237,
"b": 37
},
{
"r": 255,
"g": 41,
"b": 143
},
{
"r": 255,
"g": 61,
"b": 116
},
{
"r": 255,
"g": 84,
"b": 111
},
{
"r": 255,
"g": 94,
"b": 96
},
{
"r": 255,
"g": 110,
"b": 86
},
{
"r": 255,
"g": 127,
"b": 76
},
{
"r": 255,
"g": 144,
"b": 65
},
{
"r": 255,
"g": 162,
"b": 55
},
{
"r": 255,
"g": 179,
"b": 44
},
{
"r": 255,
"g": 197,
"b": 33
},
{
"r": 255,
"g": 70,
"b": 88
},
{
"r": 254,
"g": 107,
"b": 64
},
{
"r": 255,
"g": 132,
"b": 50
},
{
"r": 255,
"g": 164,
"b": 30
},
{
"r": 255,
"g": 22,
"b": 98
},
{
"r": 255,
"g": 50,
"b": 80
},
{
"r": 254,
"g": 89,
"b": 64
},
{
"r": 255,
"g": 92,
"b": 54
},
{
"r": 255,
"g": 121,
"b": 36
},
{
"r": 255,
"g": 138,
"b": 37
},
{
"r": 255,
"g": 111,
"b": 25
},
{
"r": 255,
"g": 41,
"b": 54
},
{
"r": 255,
"g": 53,
"b": 46
},
{
"r": 255,
"g": 78,
"b": 30
},
{
"r": 255,
"g": 61,
"b": 28
},
{
"r": 255,
"g": 39,
"b": 37
},
{
"r": 255,
"g": 57,
"b": 18
},
{
"r": 255,
"g": 29,
"b": 15
},
{
"r": 255,
"g": 14,
"b": 8
}
]
}

import android.graphics.Bitmap import android.graphics.Color import android.util.Log import com.blankj.utilcode.util.GsonUtils import com.blankj.utilcode.util.ScreenUtils import com.blankj.utilcode.util.Utils import com.google.gson.Gson import com.google.gson.reflect.TypeToken import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import java.util.concurrent.CopyOnWriteArrayList import kotlin.math.abs object AmbientLightColorPickManager { private const val TAG = "AmbientLightColorPickManager" private var scope = MainScope() private val mWidth = ScreenUtils.getScreenWidth() / 2 private val mHeight = ScreenUtils.getScreenHeight() / 2 // 256色 private val tableList = mutableListOf<ColorTableBean>() // 原图色 private val originalList = CopyOnWriteArrayList<ColorTableBean>() var test1Listener: ((Int) -> Unit)? = null var test2Listener: ((Int, Int, Int) -> Unit)? = null @JvmStatic fun init() { log("$TAG init") scope.launch(Dispatchers.IO) { initHsvColor() } } private fun initHsvColor() { tableList.clear() runCatching { val json = SharedPreferencesUtils.getRGB256HsvColor(Utils.getApp()) val listType = object : TypeToken<MutableList<ColorTableBean>>() {}.type Gson().fromJson<MutableList<ColorTableBean>>(json, listType)?.let { tableList.addAll(it) log("initHsvColor xml list size=${tableList.size}") } }.getOrElse { Log.e(TAG, "initHsvColor Exception ${it.message}") } if (tableList.isEmpty()) { saveHsvColor().let { if (it.isNotEmpty()) { tableList.addAll(it) } } log("initHsvColor json list size=${tableList.size}") } } /** 将本地rgb色值转换成hsv保存到本地 */ private fun saveHsvColor(): MutableList<ColorTableBean> { log("saveHsvColor") val hsvList = mutableListOf<ColorTableBean>() runCatching { val assetManager = Utils.getApp().assets val file = assetManager.open("color_rgb_256.json") val jsonStr = file.bufferedReader().readText() file.close() val bean = Gson().fromJson(jsonStr, AmbientLightList::class.java) for (i in 0 until bean.list.size) { bean.list[i].apply { val myColor = Color.rgb(r, g, b) val hsvColors = FloatArray(3) Color.colorToHSV(myColor, hsvColors) val lab = ColorEabLabUtils.rGBToLab(r, g, b) val bean = ColorTableBean(hsvColors, lab, myColor) hsvList.add(bean) } } val json = Gson().toJson(hsvList) log("saveHsvColor hsvListSize=${hsvList.size}") SharedPreferencesUtils.setRGB256HsvColor(Utils.getApp(), json) }.getOrElse { Log.e(TAG, "saveHsvColor Exception ${it.message}") } return hsvList } /** 设置氛围灯 */ @JvmStatic fun setAmbientLight(displayId: Int, index: Int) { if (displayId != DisplayParameter.DISPLAY_CSD.displayId) return log("setAmbientLight displayId=$displayId") scope.launch(Dispatchers.IO) { if (originalList.isEmpty()) { Log.w(TAG, "setAmbientLight hueList is null") return@launch } if (index < 0 || index >= originalList.size) { Log.w(TAG, "setAmbientLight 索引异常") return@launch } // 氛围灯取色 setBytesFunctionValue(index) } } @JvmStatic fun switchLight(isOn: Boolean) { log("switchLight isOn=$isOn") } /** 初始化资源 */ @JvmStatic fun loadData(displayId: Int, pictures: List<String>) { if (displayId != DisplayParameter.DISPLAY_CSD.displayId) return log("loadData pictures size=${pictures.size} pictures $pictures") originalList.clear() for ((_, picture) in pictures.withIndex()) { runCatching { val bitmap = GlideCacheUtils.loadImageAsBitmap(picture, mWidth, mHeight) originalList.add(generate(bitmap)) }.getOrElse { Log.e(TAG, "loadData exception ${it.message}") } } log("loadData hueList size=${originalList.size}") } private fun setFunctionValue(functionId: Int, value: Int, zone: Int) { } private fun setBytesFunctionValue(index: Int) { try { originalList[index].let { test1Listener?.invoke(it.color) test2Listener?.invoke( findColor(it.hue()).colorTip, findLabColor(it), findTestColor(it.labArray) ) } } catch (e: Exception) { Log.e(TAG, "setBytesFunctionValue Exception $e") } } private fun findColor(bgHue: Float): ColorTipBean { if (tableList.isEmpty()) { Log.w(TAG, "findColor hsvList is null") return ColorTipBean(Color.WHITE) } var result = tableList[0] var minDiff = abs(result.hue() - bgHue) for (i in 0 until tableList.size) { val currentDiff = abs(tableList[i].hue() - bgHue) if (currentDiff < minDiff) { minDiff = currentDiff result = tableList[i] } } log("findColor bgHue=$bgHue,minDiff=$minDiff,result=$result") return ColorTipBean(result.color) } private fun findLabColor(bean: ColorTableBean): Int { var color = Color.WHITE var minDiff = Double.MAX_VALUE tableList.forEachIndexed { index, it -> val distance = ColorEabLabUtils.deltaE76(it.labArray, bean.labArray) if (distance < minDiff) { minDiff = distance color = it.color } } log("findLabColor minDiff=$minDiff,color=[${bean.color},$color]") return if (minDiff < 26) color else findColor(bean.hue()).colorTip } private fun findTestColor(list: DoubleArray): Int { var color = Color.WHITE var minDiff = Double.MAX_VALUE tableList.forEachIndexed { index, it -> val distance = ColorEabLabUtils.deltaE76(it.labArray, list) if (distance < minDiff) { minDiff = distance color = it.color } } return color } private fun getColors(bean: ColorTableBean): ByteArray { val result = mutableListOf<ColorTipBean>() val colorBean = ColorTipBean(findLabColor(bean)) result.add(colorBean) result.add(colorBean) result.add(colorBean) val json = GsonUtils.toJson(ColorLightBean(result).list) log("setBytesFunctionValue json=$json") return json.toByteArray() } private fun generate(newMap: Bitmap): ColorTableBean { val dominantColor = ColorEabLabUtils.getPerceptuallyDominantColor(newMap) val hsvArray = FloatArray(3) Color.colorToHSV(dominantColor, hsvArray) val labArray = ColorEabLabUtils.rGBToLab(dominantColor) return ColorTableBean(hsvArray, labArray, dominantColor) } private fun log(str: String) = Log.d(TAG, str) }