前言

去年就产生一个想法,通过提取图片主题色,配合图片起到沉迷式的视觉效果,产生一种谐和、统一的感觉。磨磨蹭蹭到往年才着手开发~

花了一周的工夫别离以客户端和服务端两种形式实现了这个性能,两种形式各有利弊,通常主题色的提取都是在服务端实现的,客户端将须要解决的图片以链接或id的模式提供给服务端,服务端通过运行相应的算法来提取出主题色后,再返回相应的后果。这样能够满足大多数展现类的场景,但对于须要依据用户“定制”、“生成”的图片,这样的形式就有了一个上传图片---->服务端计算---->返回后果的工夫,等待时间兴许会比拟长。而在客户端实现,挪动端浏览器的兼容问题是个很痛疼的事。

下面讲的有些累赘了,简略的说,在大多数状况下都倡议抉择服务端实现,在用户实时定制、生成图片状况下的提取色彩需要并且不思考挪动端的状况下能够抉择客户端进行实现。

本篇和大家分享客户端的实现计划:

目前提取图片色彩比拟罕用的主题色提取算法有:最小差值法、中位切分法、八叉树算法、聚类、色调建模法等,在这里我抉择了中位切分法进行实现。

思路

中位切分法通常是在图像处理中升高图像位元深度的算法,可用来将高位的图转换位低位的图,如将24bit的图转换为8bit的图。咱们也能够用来提取图片的主题色,其原理是是将图像每个像素色彩看作是以R、G、B为坐标轴的一个三维空间中的点,因为三个色彩的取值范畴为0~255,所以图像中的色彩都散布在这个色彩立方体内,如下图所示:

从初始整个图像作为一个长方体开始,之后将RGB中最长的一边从色彩统计的中位数所有为二,使失去的两个长方体所蕴含的像素数量雷同,如下图所示:

反复上述步骤,直到最终切分失去长方体的数量等于主题色彩数量为止,最初取每个长方体的中点即可。

在理论应用中如果只是依照中点进行切割,会呈现有些长方体的体积很大然而像素数量很少的状况。解决的方法是在切割前对长方体进行优先级排序,排序的系数为体积 * 像素数。这样就能够根本解决此类问题了。

成果

代码

  1. 首先创立一个canvas容器
  2. 将图片绘制到容器中
  3. 应用getImageData办法获取rgba, 查看getImageData
  4. 通过中位数切分算法切割并提取色彩
  5. 筛选掉类似的色彩

color.vue (下列代码为VUE3.0语法)

<template>    <div>      <canvas style="display: none" id="canvas"></canvas>      <div         id="extract-color-id"         class="extract-color"         style="display: flex;padding: 0 20px; justify-content:end;">      </div>    </div></template><script lang="ts">import themeColor from '../../components/colorExtraction';export default defineComponent({ setup(props) {    /**     * 设置色彩办法     */    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(()=> {        const img = new Image();        img.src = `图片的地址`;        img.crossOrigin = 'anonymous';        img.onload = () => {          themeColor(50, img, 20, SetColor);        };    })

colorExtraction.ts(下列代码为TypeScript语法,转换JavaScript删除掉所有的类型定义即可)

/** * 色彩盒子类 * * @param {Array} colorRange    [[rMin, rMax],[gMin, gMax], [bMin, bMax]] 色彩范畴 * @param {any} total   像素总数, imageData / 4 * @param {any} data    像素数据汇合 */class ColorBox {    colorRange: unknown[];    total: number;    data: Uint8ClampedArray;    volume: number;    rank: number;    constructor(colorRange: any[], total: number, data: Uint8ClampedArray) {        this.colorRange = colorRange;        this.total = total;        this.data = data;        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;    }    getColor() {        const total = this.total;        const data = this.data;        let redCount = 0,            greenCount = 0,            blueCount = 0;        for (let i = 0; i < total; i++) {            redCount += data[i * 4];            greenCount += data[i * 4 + 1];            blueCount += data[i * 4 + 2];        }        return [redCount / total, greenCount / total, blueCount / total];    }}// 获取切割边const getCutSide = (colorRange: number[][]) => {   // r:0,g:1,b:2    const arr = [];    for (let i = 0; i < 3; i++) {        arr.push(colorRange[i][1] - colorRange[i][0]);    }    return arr.indexOf(Math.max(arr[0], arr[1], arr[2]));}// 切割色彩范畴const cutRange = (colorRange: number[][], colorSide: number, cutValue: any) => {    const arr1: number[][] = [];    const arr2: number[][] = [];    colorRange.forEach(function (item) {        arr1.push(item.slice());        arr2.push(item.slice());    })    arr1[colorSide][1] = cutValue;    arr2[colorSide][0] = cutValue;    return [arr1, arr2];}// 找到呈现次数为中位数的色彩const __quickSort = (arr: any[]): any => {    if (arr.length <= 1) {        return arr;    }    const pivotIndex = Math.floor(arr.length / 2);    const pivot = arr.splice(pivotIndex, 1)[0];    const left = [];    const right = [];    for (let i = 0; i < arr.length; i++) {        if (arr[i].count <= pivot.count) {            left.push(arr[i]);        }        else {            right.push(arr[i]);        }    }    return __quickSort(left).concat([pivot], __quickSort(right));}const getMedianColor = (colorCountMap: Record<string, number>, total: number) => {    const arr = [];    for (const key in colorCountMap) {        arr.push({            color: parseInt(key),            count: colorCountMap[key]        })    }    const sortArr = __quickSort(arr);    let medianCount = 0;    const medianIndex = Math.floor(sortArr.length / 2)    for (let i = 0; i <= medianIndex; i++) {        medianCount += sortArr[i].count;    }    return {        color: parseInt(sortArr[medianIndex].color),        count: medianCount    }}// 切割色彩盒子const cutBox = (colorBox: { colorRange: number[][]; total: number; data: Uint8ClampedArray }) => {    const colorRange = colorBox.colorRange;    const cutSide = getCutSide(colorRange);    const colorCountMap: Record<string, number> = {};    const total = colorBox.total;    const data = colorBox.data;    // 统计出各个值的数量    for (let i = 0; i < total; i++) {        const color = data[i * 4 + cutSide];        if (colorCountMap[color]) {            colorCountMap[color] += 1;        }        else {            colorCountMap[color] = 1;        }    }    const medianColor = getMedianColor(colorCountMap, total);    const cutValue = medianColor.color;    const cutCount = medianColor.count;    const newRange = cutRange(colorRange, cutSide, cutValue);    const box1 = new ColorBox(newRange[0], cutCount, data.slice(0, cutCount * 4));    const box2 = new ColorBox(newRange[1], total - cutCount, data.slice(cutCount * 4));    return [box1, box2];}// 队列切割const queueCut = (queue: any[], num: number) => {    while (queue.length < num) {        queue.sort((a: { rank: number }, b: { rank: number }) => {            return a.rank - b.rank        });        const colorBox = queue.pop();        const result = cutBox(colorBox);        queue = queue.concat(result);    }    return queue.slice(0, num)}// 色彩去重const colorFilter = (colorArr: number[][], difference: number) => {    for (let i = 0; i < colorArr.length; i++) {        for (let j = i + 1; j < colorArr.length; j++) {            if (Math.abs(colorArr[i][0] - colorArr[j][0]) < difference && Math.abs(colorArr[i][1] - colorArr[j][1]) < difference && Math.abs(colorArr[i][2] - colorArr[j][2]) < difference) {                colorArr.splice(j, 1)                j--            }        }    }    return colorArr}/** * 提取色彩 * @param colorNumber 提取最大色彩数量 * @param img 须要提取的图片 * @param difference 图片色彩筛选精准度 * @param callback 回调函数 */const themeColor = (colorNumber: number, img: CanvasImageSource, difference: number, callback: (arg0: number[][]) => void) => {    const canvas = document.createElement('canvas') as HTMLCanvasElement;    const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;    let width = 0    let height = 0    let imageData = null    canvas.width = img.width as number;    width = canvas.width as number    canvas.height = img.height as number    height = canvas.height    ctx.drawImage(img, 0, 0, width, height);    imageData = ctx.getImageData(0, 0, width, 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 red = imageData[i * 4];        const green = imageData[i * 4 + 1];        const blue = imageData[i * 4 + 2];        if (red < rMin) {            rMin = red;        }        if (red > rMax) {            rMax = red;        }        if (green < gMin) {            gMin = green;        }        if (green > gMax) {            gMax = green;        }        if (blue < bMin) {            bMin = blue;        }        if (blue > bMax) {            bMax = blue;        }    }    const colorRange = [[rMin, rMax], [gMin, gMax], [bMin, bMax]];    const colorBox = new ColorBox(colorRange, total, imageData);    const colorBoxArr = queueCut([colorBox], colorNumber);    let colorArr = [];    for (let j = 0; j < colorBoxArr.length; j++) {        colorBoxArr[j].total && colorArr.push(colorBoxArr[j].getColor())    }    colorArr = colorFilter(colorArr, difference)    callback(colorArr);}export default themeColor

参考:
https://github.com/lokesh/col...
http://blog.rainy.im/2015/11/...
https://www.yuque.com/along-n...
https://cloud.tencent.com/dev...
https://xcoder.in/2014/09/17/...