動態

詳情 返回 返回

氛圍燈動態屏保取色方案一 - 動態 詳情

氛圍燈並不支持所有的顏色,只能支持256色,所以在取到圖片顏色後需要根據結果顏色去跟氛圍燈所支持的256色對比,取最接近的結果色,然後同步到氛圍燈顯示

取色流程

取色需要用到原生 Palette.from(bitmap).generate() 方法,通過量化算法分析位圖的像素顏色分佈,提取最具代表性的顏色組合,也有異步獲取方法,下面方法都處於子線程,所以這裏直接使用同步方法

查看 androidx.palette.graphics.Palette 源碼可以得知,該方法默認提取16種顏色樣本

image

需要確保取色精準度,16可能錯過次要但視覺顯著的顏色,過高又會導致耗時,所以這裏使用24

針對原圖還需要縮放處理,但是不宜過度,否則對準確度會有影響,這裏對2560分辨率的圖片縮小三分之一處理

private val mWidth = ScreenUtils.getScreenWidth() / 2
private val mHeight = ScreenUtils.getScreenHeight() / 2

Glide.with(Utils.getApp())
                    .asBitmap()
                    .load(new File(path))
                    .override(width, height)
                    .centerCrop()
                    .skipMemoryCache(true)
                    .diskCacheStrategy(DiskCacheStrategy.NONE)
                    .submit(width, height)
                    .get();
View Code

對氛圍燈的256色進行緩存處理,先新建 color_rgb_256.json 文件,將rgb色值保存,用於後續轉換對比

image

初始化時解析成hsv緩存到本地集合中

    private fun saveHsvColor(): MutableList<HsvColor> {
        log("saveHsvColor")
        val hsvList = mutableListOf<HsvColor>()
        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)
            val hsvColors = FloatArray(3)
            for (i in 0 until bean.list.size) {
                bean.list[i].apply {
                    val myColor = Color.rgb(r, g, b)
                    Color.colorToHSV(myColor, hsvColors)
                    hsvList.add(HsvColor(hsvColors[0], hsvColors[1], hsvColors[2]))
                }
            }
            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
    }
View Code

此文件顏色不會變,所以不用重複操作,判斷首次轉換就行

private fun initHsvColor() {
        if (hsvTableList.isEmpty()) {
            runCatching {
                val json = SharedPreferencesUtils.getRGB256HsvColor(Utils.getApp())
                val listType = object : TypeToken<MutableList<HsvColor>>() {}.type
                Gson().fromJson<MutableList<HsvColor>>(json, listType)?.let {
                    hsvTableList.addAll(it)
                    log("initHsvColor xml list size=${hsvTableList.size}")
                }
            }.getOrElse {
                Log.e(TAG, "initHsvColor Exception ${it.message}")
            }
        }
        if (hsvTableList.isEmpty()) {
            saveHsvColor().let {
                if (it.isNotEmpty()) {
                    hsvTableList.addAll(it)
                }
            }
            log("initHsvColor json list size=${hsvTableList.size}")
        }
    }
View Code

耗時操作需要放在子線程

    @JvmStatic
    fun init() {
        log("$TAG init")
        scope.launch(Dispatchers.IO) {
            hsvTableList.clear()
            initHsvColor()
        }
    }
View Code

後面對圖片進行取色,見下面方案

取色後,跟256色進行就近查找,所以需要轉換成hsv,取 hue 進行對比

private fun findColor(bgHue: Float): ColorTipBean {
        if (hsvTableList.isEmpty()) {
            Log.w(TAG, "findColor hsvList is null")
            return ColorTipBean(Color.WHITE)
        }
        var result = hsvTableList[0]
        var minDiff = abs(result.hue - bgHue)
        for (i in 0 until hsvTableList.size) {
            val currentDiff = abs(hsvTableList[i].hue - bgHue)
            if (currentDiff < minDiff) {
                minDiff = currentDiff
                result = hsvTableList[i]
            }
        }
        log("findColor bgHue=$bgHue,result=$result")
        return ColorTipBean(
            Color.HSVToColor(floatArrayOf(result.hue, result.saturation, result.value))
        )
    }
View Code

拿到結果後,通過信號下設到氛圍燈顯示

準確度

想要達到聯動效果,需要確保取色結果的準確度,原生方案使用 getDominantColor 直接獲取主色,但是大部分結果差異較大,下面提供了幾種方案對比

方案一:

通過原生提供的方法直接獲取圖片主色

Palette.from(newMap).generate().apply {
            val dominantColor = getDominantColor(Color.WHITE)
            val hsvColorArray = FloatArray(3)
            val hsv = colorToHSV(dominantColor, hsvColorArray)
            Log.d(TAG, "dominantColor $dominantColor hsv $hsv")
            result.fill(hsv)
}
View Code

getDominantColor 方法直接取的 mDominantSwatch.getRgb

    /**
     * Returns the color of the dominant swatch from the palette, as an RGB packed int.
     *
     * @param defaultColor value to return if the swatch isn't available
     * @see #getDominantSwatch()
     */
    @ColorInt
    public int getDominantColor(@ColorInt int defaultColor) {
        return mDominantSwatch != null ? mDominantSwatch.getRgb() : defaultColor;
    }
View Code

而 mDominantSwatch 則根據色塊 population 排序的結果

    Palette(List<Swatch> swatches, List<Target> targets) {
        mSwatches = swatches;
        mTargets = targets;

        mUsedColors = new SparseBooleanArray();
        mSelectedSwatches = new ArrayMap<>();

        mDominantSwatch = findDominantSwatch();
    }

@Nullable
    private Swatch findDominantSwatch() {
        int maxPop = Integer.MIN_VALUE;
        Swatch maxSwatch = null;
        for (int i = 0, count = mSwatches.size(); i < count; i++) {
            Swatch swatch = mSwatches.get(i);
            if (swatch.getPopulation() > maxPop) {
                maxSwatch = swatch;
                maxPop = swatch.getPopulation();
            }
        }
        return maxSwatch;
    }
View Code

假設氛圍燈需要多個取色,可以直接從 mSwatches 顏色集合中按 population 排序獲取

image

Swatch 代表的顏色在圖片中的權重佔比(多個小紅點可能被聚類到同一個紅色 Swatch)

經自測驗證,改方案准確度不夠,偏差較大,特別是在氛圍燈所支持的256色中,查找出的相近結果出入較大,整體準確度不夠

因為實際環境中無法看到氛圍燈(車機上效果),所以在左上角顯示測試結果,方便查看

image

圖片中,左上角測試區域,中間上面是圖片主色,下面是通過主色映射的氛圍燈顏色,很顯然跟圖片差異較大

方案二:

在原生基礎上使用飽和度跟亮度參與計算,避免過暗或過亮的顏色

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)
    }
View Code

該方案將純黑白色過濾(實際圖片中純黑白色佔比很少,但是很印象色塊,容易出現誤差),同時避免了過亮的顏色,更突出我們肉眼看到的顏色

其它方案:

1、在方案二的基礎上,加入色相,改進計算公式

2、調整圖片,縮小區域,針對中心區域進行取色

3、自定義過濾器,針對業務情況單獨處理某些圖片

比如,可以針對純黑白佔比大於30%的進行過濾,否則不過濾

private fun isClear(bitmap: Bitmap): Boolean {
        val totalPixels = bitmap.width * bitmap.height
        var blackCount = 0.0
        var whiteCount = 0.0
        for (x in 0 until bitmap.width) {
            for (y in 0 until bitmap.height) {
                val pixel = bitmap[x, y]
                if (pixel == Color.BLACK) {
                    blackCount++
                }
                if (pixel == Color.WHITE) {
                    whiteCount++
                }
            }
        }
        val blackRatio = blackCount / totalPixels
        val whiteRatio = whiteCount / totalPixels
        val isClear = blackRatio > 0.3 || whiteRatio > 0.3
        Log.d(TAG, "isClear=$isClear totalPixels=$totalPixels,blackCount=$blackCount, blackRatio=${String.format("%.2f", blackRatio)},whiteRatio=${String.format("%.2f", whiteRatio)}")
        return isClear
    }
View Code

但需要慎重,會提高計算耗時

image

image

image

左上角,上面的方格代表直接從圖片中讀取的色值,下面的方格是映射後的色值,最左邊的是方案二,中間的是方案一,右邊的是替補方案

結論圖片不多展示,經過大量圖片驗證,準確度最高的是方案二

import android.graphics.Bitmap
import android.graphics.Color
import android.util.Log
import androidx.palette.graphics.Palette
import androidx.palette.graphics.Palette.Swatch
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.annotations.SerializedName
import com.google.gson.reflect.TypeToken

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import java.util.Collections
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.math.abs
import kotlin.math.sqrt
import androidx.core.graphics.get


object 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<HsvColor>()
    private val hueList = CopyOnWriteArrayList<FloatArray>()
    private val test1List = CopyOnWriteArrayList<FloatArray>()
    private val test2List = CopyOnWriteArrayList<FloatArray>()
    private val test3List = CopyOnWriteArrayList<FloatArray>()
    var test1Listener: ((Int, Int, Int) -> Unit)? = null
    var test2Listener: ((Int, Int, Int) -> Unit)? = null

    @JvmStatic
    fun init() {
        log("$TAG init")
        scope.launch(Dispatchers.IO) {
            hsvTableList.clear()
            initHsvColor()
        }
    }

    private fun initHsvColor() {
        if (hsvTableList.isEmpty()) {
            runCatching {
                val json = SharedPreferencesUtils.getRGB256HsvColor(Utils.getApp())
                val listType = object : TypeToken<MutableList<HsvColor>>() {}.type
                Gson().fromJson<MutableList<HsvColor>>(json, listType)?.let {
                    hsvTableList.addAll(it)
                    log("initHsvColor xml list size=${hsvTableList.size}")
                }
            }.getOrElse {
                Log.e(TAG, "initHsvColor Exception ${it.message}")
            }
        }
        if (hsvTableList.isEmpty()) {
            saveHsvColor().let {
                if (it.isNotEmpty()) {
                    hsvTableList.addAll(it)
                }
            }
            log("initHsvColor json list size=${hsvTableList.size}")
        }
    }

    /** 將本地rgb色值轉換成hsv保存到本地 */
    private fun saveHsvColor(): MutableList<HsvColor> {
        log("saveHsvColor")
        val hsvList = mutableListOf<HsvColor>()
        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)
            val hsvColors = FloatArray(3)
            for (i in 0 until bean.list.size) {
                bean.list[i].apply {
                    val myColor = Color.rgb(r, g, b)
                    Color.colorToHSV(myColor, hsvColors)
                    hsvList.add(HsvColor(hsvColors[0], hsvColors[1], hsvColors[2]))
                }
            }
            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 (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 {
        if (hsvTableList.isEmpty()) {
            Log.w(TAG, "findColor hsvList is null")
            return ColorTipBean(Color.WHITE)
        }
        var result = hsvTableList[0]
        var minDiff = abs(result.hue - bgHue)
        for (i in 0 until hsvTableList.size) {
            val currentDiff = abs(hsvTableList[i].hue - bgHue)
            if (currentDiff < minDiff) {
                minDiff = currentDiff
                result = hsvTableList[i]
            }
        }
        log("findColor bgHue=$bgHue,result=$result")
        return ColorTipBean(
            Color.HSVToColor(floatArrayOf(result.hue, result.saturation, result.value))
        )
    }


    /** 初始化資源 */
    @JvmStatic
    fun loadData(displayId: Int, pictures: List<String>) {
        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<ColorTipBean>()
        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 {
        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)
    }

    private fun isClear(bitmap: Bitmap): Boolean {
        val totalPixels = bitmap.width * bitmap.height
        var blackCount = 0.0
        var whiteCount = 0.0
        for (x in 0 until bitmap.width) {
            for (y in 0 until bitmap.height) {
                val pixel = bitmap[x, y]
                if (pixel == Color.BLACK) {
                    blackCount++
                }
                if (pixel == Color.WHITE) {
                    whiteCount++
                }
            }
        }
        val blackRatio = blackCount / totalPixels
        val whiteRatio = whiteCount / totalPixels
        val isClear = blackRatio > 0.3 || whiteRatio > 0.3
        Log.d(TAG, "isClear=$isClear totalPixels=$totalPixels,blackCount=$blackCount, blackRatio=${String.format("%.2f", blackRatio)},whiteRatio=${String.format("%.2f", whiteRatio)}")
        return isClear
    }

    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 <= 30f) -> 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<ColorTipBean>
    )

    data class ColorTipBean(
        @SerializedName("ColorTip")
        var colorTip: Int,
    )

}
View Code

 

user avatar MiddleByPass 頭像
點贊 1 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.