找回密码
 立即注册
首页 业界区 安全 氛围灯动态屏保取色方案

氛围灯动态屏保取色方案

孙淼淼 昨天 16:25
这是屏保联动时,从屏保图片中动态获取主色,同步到氛围灯,切换时跟氛围灯联动效果
解析图片动态取色,耗时较长,需要预加载处理,所以在每次变更主题时便要开始取色,然后切换时同步到氛围灯
而氛围灯并不支持所有的颜色,只能支持256色,所以在取到图片颜色后需要根据结果颜色去跟氛围灯所支持的256色对比,取最接近的结果色,然后同步到氛围灯显示
取色流程

取色需要用到原生 Palette.from(bitmap).generate() 方法,通过量化算法分析位图的像素颜色分布,提取最具代表性的颜色组合,也有异步获取方法,下面方法都处于子线程,所以这里直接使用同步方法
查看 androidx.palette.graphics.Palette 源码可以得知,该方法默认提取16种颜色样本
1.png

需要确保取色精准度,16可能错过次要但视觉显著的颜色,过高又会导致耗时,所以这里使用24
针对原图还需要缩放处理,但是不宜过度,否则对准确度会有影响,这里对2560分辨率的图片缩小三分之一处理
2.gif
3.gif
  1. private val mWidth = ScreenUtils.getScreenWidth() / 2
  2. private val mHeight = ScreenUtils.getScreenHeight() / 2
  3. Glide.with(Utils.getApp())
  4.                     .asBitmap()
  5.                     .load(new File(path))
  6.                     .override(width, height)
  7.                     .centerCrop()
  8.                     .skipMemoryCache(true)
  9.                     .diskCacheStrategy(DiskCacheStrategy.NONE)
  10.                     .submit(width, height)
  11.                     .get();
复制代码
View Code对氛围灯的256色进行缓存处理,先新建 color_rgb_256.json 文件,将rgb色值保存,用于后续转换对比
4.png

初始化时解析成hsv缓存到本地集合中
5.gif
6.gif
  1.     private fun saveHsvColor(): MutableList<HsvColor> {
  2.         log("saveHsvColor")
  3.         val hsvList = mutableListOf<HsvColor>()
  4.         runCatching {
  5.             val assetManager = Utils.getApp().assets
  6.             val file = assetManager.open("color_rgb_256.json")
  7.             val jsonStr = file.bufferedReader().readText()
  8.             file.close()
  9.             val bean = Gson().fromJson(jsonStr, AmbientLightList::class.java)
  10.             val hsvColors = FloatArray(3)
  11.             for (i in 0 until bean.list.size) {
  12.                 bean.list[i].apply {
  13.                     val myColor = Color.rgb(r, g, b)
  14.                     Color.colorToHSV(myColor, hsvColors)
  15.                     hsvList.add(HsvColor(hsvColors[0], hsvColors[1], hsvColors[2]))
  16.                 }
  17.             }
  18.             val json = Gson().toJson(hsvList)
  19.             log("saveHsvColor hsvListSize=${hsvList.size}")
  20.             SharedPreferencesUtils.setRGB256HsvColor(Utils.getApp(), json)
  21.         }.getOrElse {
  22.             Log.e(TAG, "saveHsvColor Exception ${it.message}")
  23.         }
  24.         return hsvList
  25.     }
复制代码
View Code此文件颜色不会变,所以不用重复操作,判断首次转换就行
7.gif
8.gif
  1. private fun initHsvColor() {
  2.         if (hsvTableList.isEmpty()) {
  3.             runCatching {
  4.                 val json = SharedPreferencesUtils.getRGB256HsvColor(Utils.getApp())
  5.                 val listType = object : TypeToken<MutableList<HsvColor>>() {}.type
  6.                 Gson().fromJson<MutableList<HsvColor>>(json, listType)?.let {
  7.                     hsvTableList.addAll(it)
  8.                     log("initHsvColor xml list size=${hsvTableList.size}")
  9.                 }
  10.             }.getOrElse {
  11.                 Log.e(TAG, "initHsvColor Exception ${it.message}")
  12.             }
  13.         }
  14.         if (hsvTableList.isEmpty()) {
  15.             saveHsvColor().let {
  16.                 if (it.isNotEmpty()) {
  17.                     hsvTableList.addAll(it)
  18.                 }
  19.             }
  20.             log("initHsvColor json list size=${hsvTableList.size}")
  21.         }
  22.     }
复制代码
View Code耗时操作需要放在子线程
9.gif
10.gif
  1.     @JvmStatic
  2.     fun init() {
  3.         log("$TAG init")
  4.         scope.launch(Dispatchers.IO) {
  5.             hsvTableList.clear()
  6.             initHsvColor()
  7.         }
  8.     }
复制代码
View Code后面对图片进行取色,见下面方案
取色后,跟256色进行就近查找,所以需要转换成hsv,取 hue 进行对比
11.gif
12.gif
  1. private fun findColor(bgHue: Float): ColorTipBean {
  2.         if (hsvTableList.isEmpty()) {
  3.             Log.w(TAG, "findColor hsvList is null")
  4.             return ColorTipBean(Color.WHITE)
  5.         }
  6.         var result = hsvTableList[0]
  7.         var minDiff = abs(result.hue - bgHue)
  8.         for (i in 0 until hsvTableList.size) {
  9.             val currentDiff = abs(hsvTableList[i].hue - bgHue)
  10.             if (currentDiff < minDiff) {
  11.                 minDiff = currentDiff
  12.                 result = hsvTableList[i]
  13.             }
  14.         }
  15.         log("findColor bgHue=$bgHue,result=$result")
  16.         return ColorTipBean(
  17.             Color.HSVToColor(floatArrayOf(result.hue, result.saturation, result.value))
  18.         )
  19.     }
复制代码
View Code拿到结果后,通过信号下设到氛围灯显示
准确度

想要达到联动效果,需要确保取色结果的准确度,原生方案使用 getDominantColor 直接获取主色,但是大部分结果差异较大,下面提供了几种方案对比
方案一:

通过原生提供的方法直接获取图片主色
13.gif
14.gif
  1. Palette.from(newMap).generate().apply {
  2.             val dominantColor = getDominantColor(Color.WHITE)
  3.             val hsvColorArray = FloatArray(3)
  4.             val hsv = colorToHSV(dominantColor, hsvColorArray)
  5.             Log.d(TAG, "dominantColor $dominantColor hsv $hsv")
  6.             result.fill(hsv)
  7. }
复制代码
View CodegetDominantColor 方法直接取的 mDominantSwatch.getRgb
15.gif
16.gif
  1.     /**
  2.      * Returns the color of the dominant swatch from the palette, as an RGB packed int.
  3.      *
  4.      * @param defaultColor value to return if the swatch isn't available
  5.      * @see #getDominantSwatch()
  6.      */
  7.     @ColorInt
  8.     public int getDominantColor(@ColorInt int defaultColor) {
  9.         return mDominantSwatch != null ? mDominantSwatch.getRgb() : defaultColor;
  10.     }
复制代码
View Code而 mDominantSwatch 则根据色块 population 排序的结果
17.gif
18.gif
  1.     Palette(List<Swatch> swatches, List<Target> targets) {
  2.         mSwatches = swatches;
  3.         mTargets = targets;
  4.         mUsedColors = new SparseBooleanArray();
  5.         mSelectedSwatches = new ArrayMap<>();
  6.         mDominantSwatch = findDominantSwatch();
  7.     }
  8. @Nullable
  9.     private Swatch findDominantSwatch() {
  10.         int maxPop = Integer.MIN_VALUE;
  11.         Swatch maxSwatch = null;
  12.         for (int i = 0, count = mSwatches.size(); i < count; i++) {
  13.             Swatch swatch = mSwatches.get(i);
  14.             if (swatch.getPopulation() > maxPop) {
  15.                 maxSwatch = swatch;
  16.                 maxPop = swatch.getPopulation();
  17.             }
  18.         }
  19.         return maxSwatch;
  20.     }
复制代码
View Code假设氛围灯需要多个取色,可以直接从 mSwatches 颜色集合中按 population 排序获取
19.png

Swatch 代表的颜色在图片中的权重占比(多个小红点可能被聚类到同一个红色 Swatch)
经自测验证,改方案准确度不够,偏差较大,特别是在氛围灯所支持的256色中,查找出的相近结果出入较大,整体准确度不够
因为实际环境中无法看到氛围灯(车机上效果),所以在左上角显示测试结果,方便查看
20.png

图片中,左上角测试区域,中间上面是图片主色,下面是通过主色映射的氛围灯颜色,很显然跟图片差异较大
方案二:

在原生基础上使用饱和度跟亮度参与计算,避免过暗或过亮的颜色
21.gif
22.gif
  1. fun getPerceptuallyDominantColor(bitmap: Bitmap): Int {
  2.         val palette = Palette.from(bitmap).maximumColorCount(24).clearFilters().generate()
  3.         val swatches = palette.swatches
  4.         if (swatches.isEmpty()) return Color.WHITE
  5.         var bestSwatch: Swatch? = null
  6.         var maxScore = 0f
  7.         for (swatch in swatches) {
  8.             val hsl = swatch.getHsl()
  9.             val saturation = hsl[1] // 饱和度 (0-1)
  10.             val luminance = hsl[2] // 亮度 (0-1)
  11.             val population = swatch.population
  12.             // 评分公式:人口占比 * 饱和度 * 亮度因子
  13.             // 亮度因子确保避免过暗或过亮的颜色(0.1-0.9为理想范围)
  14.             val luminanceFactor = 1f - abs(luminance - 0.5f) * 1.8f
  15.             val score = population * saturation * luminanceFactor
  16.             if (score > maxScore) {
  17.                 maxScore = score
  18.                 bestSwatch = swatch
  19.             }
  20.         }
  21.         return bestSwatch?.rgb ?: palette.getDominantColor(Color.WHITE)
  22.     }
复制代码
View Code该方案将纯黑白色过滤(实际图片中纯黑白色占比很少,但是很印象色块,容易出现误差),同时避免了过亮的颜色,更突出我们肉眼看到的颜色
其它方案:

1、在方案二的基础上,加入色相,改进计算公式
2、调整图片,缩小区域,针对中心区域进行取色
3、自定义过滤器,针对业务情况单独处理某些图片
比如,可以针对纯黑白占比大于30%的进行过滤,否则不过滤
23.gif
24.gif
  1. private fun isClear(bitmap: Bitmap): Boolean {
  2.         val totalPixels = bitmap.width * bitmap.height
  3.         var blackCount = 0.0
  4.         var whiteCount = 0.0
  5.         for (x in 0 until bitmap.width) {
  6.             for (y in 0 until bitmap.height) {
  7.                 val pixel = bitmap[x, y]
  8.                 if (pixel == Color.BLACK) {
  9.                     blackCount++
  10.                 }
  11.                 if (pixel == Color.WHITE) {
  12.                     whiteCount++
  13.                 }
  14.             }
  15.         }
  16.         val blackRatio = blackCount / totalPixels
  17.         val whiteRatio = whiteCount / totalPixels
  18.         val isClear = blackRatio > 0.3 || whiteRatio > 0.3
  19.         Log.d(TAG, "isClear=$isClear totalPixels=$totalPixels,blackCount=$blackCount, blackRatio=${String.format("%.2f", blackRatio)},whiteRatio=${String.format("%.2f", whiteRatio)}")
  20.         return isClear
  21.     }
复制代码
View Code但需要慎重,会提高计算耗时
25.png

26.png

27.png

左上角,上面的方格代表直接从图片中读取的色值,下面的方格是映射后的色值,最左边的是方案二,中间的是方案一,右边的是替补方案
结论图片不多展示,经过大量图片验证,准确度最高的是方案二
28.gif
29.gif
  1. import android.graphics.Bitmapimport android.graphics.Colorimport android.util.Logimport androidx.palette.graphics.Paletteimport androidx.palette.graphics.Palette.Swatchimport com.blankj.utilcode.util.GsonUtilsimport com.blankj.utilcode.util.ScreenUtilsimport com.blankj.utilcode.util.Utilsimport com.google.gson.Gsonimport com.google.gson.annotations.SerializedNameimport com.google.gson.reflect.TypeTokenimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.MainScopeimport kotlinx.coroutines.launchimport java.util.Collectionsimport java.util.concurrent.CopyOnWriteArrayListimport kotlin.math.absimport kotlin.math.sqrtimport androidx.core.graphics.getobject AmbientLightColorPickManager {    private const val TAG = "AmbientLightColorPickManager"    private var scope = MainScope()    private val mWidth = ScreenUtils.getScreenWidth() / 2    private val mHeight = ScreenUtils.getScreenHeight() / 2    private val hsvTableList = mutableListOf()    private val hueList = CopyOnWriteArrayList()    private val test1List = CopyOnWriteArrayList()    private val test2List = CopyOnWriteArrayList()    private val test3List = CopyOnWriteArrayList()    var test1Listener: ((Int, Int, Int) -> Unit)? = null    var test2Listener: ((Int, Int, Int) -> Unit)? = null    @JvmStatic
  2.     fun init() {
  3.         log("$TAG init")
  4.         scope.launch(Dispatchers.IO) {
  5.             hsvTableList.clear()
  6.             initHsvColor()
  7.         }
  8.     }    private fun initHsvColor() {
  9.         if (hsvTableList.isEmpty()) {
  10.             runCatching {
  11.                 val json = SharedPreferencesUtils.getRGB256HsvColor(Utils.getApp())
  12.                 val listType = object : TypeToken<MutableList<HsvColor>>() {}.type
  13.                 Gson().fromJson<MutableList<HsvColor>>(json, listType)?.let {
  14.                     hsvTableList.addAll(it)
  15.                     log("initHsvColor xml list size=${hsvTableList.size}")
  16.                 }
  17.             }.getOrElse {
  18.                 Log.e(TAG, "initHsvColor Exception ${it.message}")
  19.             }
  20.         }
  21.         if (hsvTableList.isEmpty()) {
  22.             saveHsvColor().let {
  23.                 if (it.isNotEmpty()) {
  24.                     hsvTableList.addAll(it)
  25.                 }
  26.             }
  27.             log("initHsvColor json list size=${hsvTableList.size}")
  28.         }
  29.     }    /** 将本地rgb色值转换成hsv保存到本地 */    private fun saveHsvColor(): MutableList<HsvColor> {
  30.         log("saveHsvColor")
  31.         val hsvList = mutableListOf<HsvColor>()
  32.         runCatching {
  33.             val assetManager = Utils.getApp().assets
  34.             val file = assetManager.open("color_rgb_256.json")
  35.             val jsonStr = file.bufferedReader().readText()
  36.             file.close()
  37.             val bean = Gson().fromJson(jsonStr, AmbientLightList::class.java)
  38.             val hsvColors = FloatArray(3)
  39.             for (i in 0 until bean.list.size) {
  40.                 bean.list[i].apply {
  41.                     val myColor = Color.rgb(r, g, b)
  42.                     Color.colorToHSV(myColor, hsvColors)
  43.                     hsvList.add(HsvColor(hsvColors[0], hsvColors[1], hsvColors[2]))
  44.                 }
  45.             }
  46.             val json = Gson().toJson(hsvList)
  47.             log("saveHsvColor hsvListSize=${hsvList.size}")
  48.             SharedPreferencesUtils.setRGB256HsvColor(Utils.getApp(), json)
  49.         }.getOrElse {
  50.             Log.e(TAG, "saveHsvColor Exception ${it.message}")
  51.         }
  52.         return hsvList
  53.     }    /** 设置氛围灯 */    @JvmStatic    fun setAmbientLight(displayId: Int, index: Int) {        if (displayId != DisplayParameter.DISPLAY_CSD.displayId) return        log("setAmbientLight displayId=$displayId")        scope.launch(Dispatchers.IO) {            if (hueList.isEmpty()) {                Log.w(TAG, "setAmbientLight hueList is null")                return@launch            }            if (index < 0 || index >= hueList.size) {                Log.w(TAG, "setAmbientLight 索引异常")                return@launch            }            // 氛围灯取色            setBytesFunctionValue(index)        }    }    @JvmStatic    fun switchLight(isOn: Boolean) {        log("switchLight isOn=$isOn")    }    private fun findColor(bgHue: Float): ColorTipBean {
  54.         if (hsvTableList.isEmpty()) {
  55.             Log.w(TAG, "findColor hsvList is null")
  56.             return ColorTipBean(Color.WHITE)
  57.         }
  58.         var result = hsvTableList[0]
  59.         var minDiff = abs(result.hue - bgHue)
  60.         for (i in 0 until hsvTableList.size) {
  61.             val currentDiff = abs(hsvTableList[i].hue - bgHue)
  62.             if (currentDiff < minDiff) {
  63.                 minDiff = currentDiff
  64.                 result = hsvTableList[i]
  65.             }
  66.         }
  67.         log("findColor bgHue=$bgHue,result=$result")
  68.         return ColorTipBean(
  69.             Color.HSVToColor(floatArrayOf(result.hue, result.saturation, result.value))
  70.         )
  71.     }    /** 初始化资源 */    @JvmStatic    fun loadData(displayId: Int, pictures: List) {        if (displayId != DisplayParameter.DISPLAY_CSD.displayId) return        log("loadData pictures size=${pictures.size} pictures $pictures")            hueList.clear()        test1List.clear()        test2List.clear()        test3List.clear()            for ((index, picture) in pictures.withIndex()) {                runCatching {                    val bitmap = GlideCacheUtils.loadImageAsBitmap(picture, mWidth, mHeight)                    testGenerate(bitmap)                    val result = generate(bitmap)                    hueList.add(result)                    log("loadData add index=$index,colors=${GsonUtils.toJson(result)}")                }.getOrElse {                    Log.e(TAG, "loadData exception ${it.message}")                }            }            log("loadData hueList size=${hueList.size}")    }    private fun setFunctionValue(functionId: Int, value: Int, zone: Int) {        try {            AdapterCarManager.iCarFunction.setFunctionValue(functionId, zone, value)        } catch (e: Exception) {            Log.e(TAG, "setFunctionValue Exception $e")        }    }    private fun setBytesFunctionValue(index: Int) {        try {            test1Listener?.invoke(                Color.HSVToColor(test1List[index]),                Color.HSVToColor(test2List[index]),                Color.HSVToColor(test3List[index]),            )            test2Listener?.invoke(                findColor(test1List[index][0]).colorTip,                findColor(test2List[index][0]).colorTip,                findColor(test3List[index][0]).colorTip,            )        } catch (e: Exception) {            Log.e(TAG, "setBytesFunctionValue Exception $e")        }    }    private fun getColors(list: FloatArray): ByteArray {        val result = mutableListOf()        list.forEach {            result.add(findColor(it))        }        val json = GsonUtils.toJson(LightColorBean(result).list)        log("setBytesFunctionValue json=$json")        return json.toByteArray()    }    private fun generate(newMap: Bitmap): FloatArray {        val result = FloatArray(3)        Log.w(TAG, "------generate start")        val dominantColor = getPerceptuallyDominantColor(newMap)        val hsvColorArray = FloatArray(3)        val hsv = colorToHSV(dominantColor, hsvColorArray)        result.fill(hsv)        Log.d(TAG, "dominantColor $dominantColor, hsv ${GsonUtils.toJson(hsvColorArray)}")        return result    }    private fun testGenerate(newMap: Bitmap) {        // 评分公式        val dominantColor1 = getPerceptuallyDominantColor(newMap)        val hsvColorArray1 = FloatArray(3)        colorToHSV(dominantColor1, hsvColorArray1)        test1List.add(hsvColorArray1)        // 主色        Palette.from(newMap).maximumColorCount(24).clearFilters().generate().apply {            val hsvColorArray2 = FloatArray(3)            val dominantColor2 = getDominantColor(Color.WHITE)            colorToHSV(dominantColor2, hsvColorArray2)            test2List.add(hsvColorArray2)        }        // 评分优化公式        val dominantColor3 = getPerceptuallyDominantColor1(newMap)        val hsvColorArray3 = FloatArray(3)        colorToHSV(dominantColor3, hsvColorArray3)        test3List.add(hsvColorArray3)    }    fun getPerceptuallyDominantColor(bitmap: Bitmap): Int {
  72.         val palette = Palette.from(bitmap).maximumColorCount(24).clearFilters().generate()
  73.         val swatches = palette.swatches
  74.         if (swatches.isEmpty()) return Color.WHITE
  75.         var bestSwatch: Swatch? = null
  76.         var maxScore = 0f
  77.         for (swatch in swatches) {
  78.             val hsl = swatch.getHsl()
  79.             val saturation = hsl[1] // 饱和度 (0-1)
  80.             val luminance = hsl[2] // 亮度 (0-1)
  81.             val population = swatch.population
  82.             // 评分公式:人口占比 * 饱和度 * 亮度因子
  83.             // 亮度因子确保避免过暗或过亮的颜色(0.1-0.9为理想范围)
  84.             val luminanceFactor = 1f - abs(luminance - 0.5f) * 1.8f
  85.             val score = population * saturation * luminanceFactor
  86.             if (score > maxScore) {
  87.                 maxScore = score
  88.                 bestSwatch = swatch
  89.             }
  90.         }
  91.         return bestSwatch?.rgb ?: palette.getDominantColor(Color.WHITE)
  92.     }    private fun isClear(bitmap: Bitmap): Boolean {
  93.         val totalPixels = bitmap.width * bitmap.height
  94.         var blackCount = 0.0
  95.         var whiteCount = 0.0
  96.         for (x in 0 until bitmap.width) {
  97.             for (y in 0 until bitmap.height) {
  98.                 val pixel = bitmap[x, y]
  99.                 if (pixel == Color.BLACK) {
  100.                     blackCount++
  101.                 }
  102.                 if (pixel == Color.WHITE) {
  103.                     whiteCount++
  104.                 }
  105.             }
  106.         }
  107.         val blackRatio = blackCount / totalPixels
  108.         val whiteRatio = whiteCount / totalPixels
  109.         val isClear = blackRatio > 0.3 || whiteRatio > 0.3
  110.         Log.d(TAG, "isClear=$isClear totalPixels=$totalPixels,blackCount=$blackCount, blackRatio=${String.format("%.2f", blackRatio)},whiteRatio=${String.format("%.2f", whiteRatio)}")
  111.         return isClear
  112.     }    private fun calculateSwatchScore(        hue: Float,        saturation: Float,        luminance: Float,        population: Float    ): Float {        // 1. 人口权重 (标准化)        val populationWeight = population / 1000000f        // 2. 饱和度权重 - 适度重视但不过度        val saturationWeight = sqrt(saturation) // 使用平方根降低过高饱和度的优势        // 3. 亮度权重 - 偏好中等亮度范围        val luminanceWeight = when {            luminance < 0.15f -> 0.2f  // 太暗的惩罚            luminance > 0.85f -> 0.3f  // 太亮的惩罚            else -> 1.0f - abs(luminance - 0.5f) * 1.5f        }        // 4. 色相权重 - 可选:降低过于鲜艳的红色/蓝色的优势        val hueWeight = when {            // 红色范围 (330-30度)            (hue >= 330f || hue  0.8f            // 蓝色范围 (210-270度)            hue in 210f..270f -> 0.9f            else -> 1.0f        }        return populationWeight * saturationWeight * luminanceWeight * hueWeight    }    fun getPerceptuallyDominantColor1(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.hsl            val hue = hsl[0]        // 色相 (0-360)            val saturation = hsl[1] // 饱和度 (0-1)            val luminance = hsl[2] // 亮度 (0-1)            val population = swatch.population.toFloat()            // 改进的评分公式            val score = calculateSwatchScore(hue, saturation, luminance, population)            if (score > maxScore) {                maxScore = score                bestSwatch = swatch            }        }        return bestSwatch?.rgb ?: palette.getDominantColor(Color.WHITE)    }    private fun colorToHSV(rgb: Int, hsvColorArray: FloatArray): Float {        Color.colorToHSV(rgb, hsvColorArray)        return hsvColorArray[0]    }    private fun log(str: String) = Log.d(TAG, str)    data class LightColorBean(        val list: List    )    data class ColorTipBean(        @SerializedName("ColorTip")        var colorTip: Int,    )}
复制代码
View Code 

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册