返回博客列表

识别图片颜色

2026-01-29
5 min read
css

useThemeColor 首先创建一个canvas容器 将图片绘制到容器中 使用getImageData方法获取rgba, 查看getImageData 4通过中位数切分算法切割并提取颜色 筛选掉相似的颜色 使用方法: Demo

useThemeColor

首先创建一个canvas容器 将图片绘制到容器中 使用getImageData方法获取rgba, 查看getImageData 4通过中位数切分算法切割并提取颜色 筛选掉相似的颜色

使用方法:

const {colors, extract} = useThemeColor()

ts
// src/hooks/useThemeColor.ts
import { ref } from 'vue'
class ColorBox {
    constructor(
        public colorRange: number[][],
        public total: number,
        public data: Uint8ClampedArray
    ) {
        this.volume =
            (colorRange[0][1] - colorRange[0][0]) *
            (colorRange[1][1] - colorRange[1][0]) *
            (colorRange[2][1] - colorRange[2][0])
        this.rank = total * this.volume
    }
    volume: number
    rank: number
    getColor(): number[] {
        let red = 0, green = 0, blue = 0
        for (let i = 0; i < this.total; i++) {
            red += this.data[i * 4]
            green += this.data[i * 4 + 1]
            blue += this.data[i * 4 + 2]
        }
        return [Math.round(red / this.total), Math.round(green / this.total), Math.round(blue / this.total)]
    }
}

function getCutSide(colorRange: number[][]) {
    const diff = colorRange.map(([min, max]) => max - min)
    return diff.indexOf(Math.max(...diff))
}

function cutRange(colorRange: number[][], side: number, cut: number): number[][][] {
    const left = colorRange.map(r => [...r])
    const right = colorRange.map(r => [...r])
    left[side][1] = cut
    right[side][0] = cut
    return [left, right]
}

function quickSort(arr: { color: number; count: number }[]): typeof arr {
    if (arr.length <= 1) return arr
    const pivot = arr.splice(Math.floor(arr.length / 2), 1)[0]
    const left = arr.filter(i => i.count <= pivot.count)
    const right = arr.filter(i => i.count > pivot.count)
    return [...quickSort(left), pivot, ...quickSort(right)]
}

function getMedianColor(map: Record<string, number>) {
    const arr = Object.entries(map).map(([color, count]) => ({
        color: Number(color),
        count
    }))
    const sorted = quickSort(arr)
    const index = Math.floor(sorted.length / 2)
    let total = 0
    for (let i = 0; i <= index; i++) total += sorted[i].count
    return { color: sorted[index].color, count: total }
}

function cutBox(box: ColorBox): ColorBox[] {
    const side = getCutSide(box.colorRange)
    const map: Record<string, number> = {}

    for (let i = 0; i < box.total; i++) {
        const color = box.data[i * 4 + side]
        map[color] = (map[color] || 0) + 1
    }

    const median = getMedianColor(map)
    const cut = median.color
    const count = median.count
    const ranges = cutRange(box.colorRange, side, cut)

    const leftData = box.data.slice(0, count * 4)
    const rightData = box.data.slice(count * 4)

    return [
        new ColorBox(ranges[0], count, leftData),
        new ColorBox(ranges[1], box.total - count, rightData)
    ]
}

function queueCut(boxes: ColorBox[], target: number): ColorBox[] {
    while (boxes.length < target) {
        boxes.sort((a, b) => b.rank - a.rank)
        const box = boxes.shift()
        if (!box) break
        boxes.push(...cutBox(box))
    }
    return boxes
}

function filterSimilarColors(arr: number[][], diff: number): number[][] {
    const result: number[][] = []
    for (let i = 0; i < arr.length; i++) {
        const [r1, g1, b1] = arr[i]
        if (
            !result.some(
                ([r2, g2, b2]) =>
                    Math.abs(r1 - r2) < diff &&
                    Math.abs(g1 - g2) < diff &&
                    Math.abs(b1 - b2) < diff
            )
        ) {
            result.push(arr[i])
        }
    }
    return result
}

/**
 * useThemeColor - Vue 3 hook to extract dominant colors from image
 */
export function useThemeColor() {
    const colors = ref<number[][]>([])

    const extract = async (
        source: string | HTMLImageElement,
        options?: {
            colorNumber?: number
            difference?: number
        }
    ): Promise<number[][]> => {
        const { colorNumber = 8, difference = 30 } = options || {}

        return new Promise((resolve, reject) => {
            const img = typeof source === 'string' ? new Image() : source

            if (typeof source === 'string') {
                img.crossOrigin = 'anonymous'
                img.src = source
            }

            img.onload = () => {
                const canvas = document.createElement('canvas')
                const ctx = canvas.getContext('2d')!

                canvas.width = img.width as number
                canvas.height = img.height as number
                ctx.drawImage(img, 0, 0)

                const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data
                const total = imageData.length / 4

                let rMin = 255, rMax = 0,
                    gMin = 255, gMax = 0,
                    bMin = 255, bMax = 0

                for (let i = 0; i < total; i++) {
                    const r = imageData[i * 4]
                    const g = imageData[i * 4 + 1]
                    const b = imageData[i * 4 + 2]
                    if (r < rMin) rMin = r
                    if (r > rMax) rMax = r
                    if (g < gMin) gMin = g
                    if (g > gMax) gMax = g
                    if (b < bMin) bMin = b
                    if (b > bMax) bMax = b
                }

                const range = [[rMin, rMax], [gMin, gMax], [bMin, bMax]]
                const rootBox = new ColorBox(range, total, imageData)
                const boxes = queueCut([rootBox], colorNumber)
                let colorArr = boxes.map(box => box.getColor())
                colorArr = filterSimilarColors(colorArr, difference)
                colors.value = colorArr
                resolve(colorArr)
            }

            img.onerror = reject
        })
    }

    return {
        colors,
        extract
    }
}

Demo

<template>
  <div :style="backgroundStyle">
    <div
        id="extract-color-id"
        class="extract-color"
        style="display: flex;padding: 0 20px; justify-content:end;">
    </div>
  </div>
</template>
<script setup lang="ts">

import {useThemeColor} from '@/hooks/useThemeColor'
import {computed, onMounted} from "vue";

const {colors, extract} = useThemeColor()

const backgroundStyle = computed(() => {
  if (!colors.value || colors.value.length === 0) return {}
  const stops = colors.value
      .map(c => `rgb(${c.map(v => Math.round(v)).join(',')})`)
      .join(', ')
  return {
    background: `linear-gradient(135deg, ${stops})`,
    animation: 'gradientMove 15s ease infinite',
    backgroundSize: '300% 300%',
    filter: 'blur(0px)',
  }
})

const SetColor = (colorArr: number[][]) => {
  // 初始化删除多余子节点
  const extractColor = document.querySelector('#extract-color-id') as HTMLElement;
  while (extractColor.firstChild) {
    extractColor.removeChild(extractColor.firstChild);
  }
  // 创建子节点
  for (let index = 0; index < colorArr.length; index++) {
    const bgc = '(' + colorArr[index][0] + ',' + colorArr[index][1] + ',' + colorArr[index][2] + ')';
    const colorBlock = document.createElement('div') as HTMLElement;
    colorBlock.id = `color-block-id${index}`;
    colorBlock.style.cssText = 'height: 50px;width: 50px;margin-right: 10px;border-radius: 50%;';
    colorBlock.style.backgroundColor = `rgb${bgc}`;
    extractColor.appendChild(colorBlock);
  }
};

onMounted(async () => {
  const url = 'https://fastly.picsum.photos/id/1039/800/400.jpg?hmac=SKX6rgTRDAY5YFLTjO2eIGoSKhJl1KDQsT13NLejas4';
  const img = new Image();
  img.src = url;
  img.crossOrigin = 'anonymous';
  img.onload = () => {
    extract(url).then(colors => {
      SetColor(colors)
    })
  };
  console.log(colors.value)
})
</script>

<style>
html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  /* 禁止垂直方向回弹 */
  overscroll-behavior-y: none;
}

#app {
  width: 100%;
  height: 100%;
}

/* 动态背景动画 */
@keyframes gradientMove {
  0% {
    background-position: 0% 50%;
  }
  50% {
    background-position: 100% 50%;
  }
  100% {
    background-position: 0% 50%;
  }
}
</style>

返回博客列表
最后更新于 2026-01-29
想法或问题?在 GitHub Issue 下方参与讨论
去评论