工作时遇到一个需求:提取图片主题色,通过某种映射关系,选取ui给出的对应颜色。脑海中浮现如果只是纯前端如何实现呢?
一、思路与准备
利用canvas获取图像像素信息,然后用某种算法将主题颜色提取出来。
1.1 了解Canvas画布真实像素原理
MDN: 事实上,你可以直接通过
getImageData
,返回一个imageData
对象,获取场景像素数据。
imageData
对象包含下列几个只读属性:
width
:图片宽度,单位是像素height
:图片高度,单位是像素data
:Uint8ClampedArray
类型的一维数组,包含着RGBA格式的整型数据,范围在0至255之间(包括255)。
data
属性返回一个 Uint8ClampedArray
,它可以被使用作为查看初始像素数据。每个像素用4个 1 bytes值(按照红,绿,蓝和透明值的顺序,"RGBA"格式) 来代表。每个颜色值部份用0至255来代表。每个部分被分配到一个在数组内连续的索引,左上角像素的红色部分在数组的索引0位置。像素从左到右被处理,然后往下,遍历整个数组。 Uint8ClampedArray
包含高度 × 宽度 × 4 bytes数据,索引值从0到(高度×宽度×4)-1
1.2 了解中位切分法 (Median cut)
中位切分法 / MMCQ算法 是指的是 将色彩空间里的颜色按照密集程度分组,先将整个 ColorSpace 视为一个整体A,然后通过计算颜色差值:
找到 median point,然后切割成两个分组A1 和 A2,然后再找到最大的一个组(比如A1),计算差值找到 median point, 继续切割成A3,A4,现在我们有了三个分组(A2,A3,A4),继续找到最大分组切割,直到达到所需的颜色数量 N。然后得到 N 个分组,这 N 个分组的 median point 的颜色值即为要求的颜色值。颜色比例即为 N 个分组的大小比值。
其中color-thief库就是基于中位切分法实现的。
Leptonica 作者在报告 Median-Cut Color Quantization 中总结了这一算法存在的一些问题,其中主要问题是有可能存在某些条件下 VBox 体积很大但只包含少量像素。解决的方法是,每次进行切分时,并不是对上一次切分得到的所有VBox进行切分,而是通过一个优先级队列进行排序,刚开始时这一队列以VBox仅以VBox所包含的像素数作为优先级考量,当切分次数变多之后,将体积*包含像素数作为优先级。
除此之外,算法中最重要的部分是统计色彩分布直方图。我们需要将三维空间中的任意一点对应到一维坐标中的整数,这样才能以最快地速度定位这一颜色。如果采用全部的24位信息,那么我们用于保存直方图的数组长度至少要是224=16777216,既然是要提取颜色主题(或是颜色量化),我们可以将颜色由RGB各8位压缩至5位,这样数组长度只有215=32768
// color-thief部分源码:
var sigbits = 5,
rshift = 8 - sigbits,
// get reduced-space color index for a pixel
function getColorIndex(r, g, b) {
return (r << (2 * sigbits)) + (g << sigbits) + b;
}
function getHisto(pixels) {
var histosize = 1 << (3 * sigbits),
histo = new Array(histosize),
index, rval, gval, bval;
pixels.forEach(function(pixel) {
rval = pixel[0] >> rshift;
gval = pixel[1] >> rshift;
bval = pixel[2] >> rshift;
index = getColorIndex(rval, gval, bval);
histo[index] = (histo[index] || 0) + 1;
});
return histo;
}
复制代码
参考:
Pixels and Palettes: Extracting Color Palettes From Images