Color Extract Tool 颜色提取小工具

心血来潮,做了个自己可以用的提取颜色的工具,目前Demo非常土鳖,但是基本功能算是有了。

Demo 地址:https://rvtea.github.io/ColorExtractTool/

项目地址:https://github.com/Rvtea/ColorExtractTool

引子

受一条微博的启发,找到了原始的出处Design Seeds,然后自己思考了下基本原理觉得可以自己做,于是趁着还在学习js的初学热情高涨阶段,搞了个Demo玩儿。

基本原理

我们从图片最终效果来入手分析。

example

如上图,我们看到最终的效果图里包含一部分原图和抽取出来的主要的颜色,英文称之为Dominant Colors,那么我们的工作首先就变成了

  • 选取图片并上传,这里涉及到对图片格式的判断
  • 图片处理,包括生成preview、提取rgb信息、合并相近颜色等
  • 图片展示,需要把原图(或一部分)连同提取出来的主要颜色用canvas的形式展示出来

实现过程

图片上传

这一部分网上的demo非常之多,于是我就偷懒没怎么认真写,因为学习JS用的是廖雪峰的JavaScript教程,于是主要就参考这里面的代码做了基础的判断,自己把重心放在了onload事件上。这部分的代码很好地展示了文件上传中的边界考察并进行简单地处理,我觉得作为初学者有必要好好看看这段代码。

图片处理

这是这个Demo的核心部分,主要是需要考虑三部分的工作,我们按顺序来说明。

图片预览

1
2
3
4
5
6
7
8
if (img_preview) {
img_preview.setAttribute('src', e.target.result);
} else {
img_preview = document.createElement('img');
img_preview.setAttribute('id', 'image-preview');
img_preview.setAttribute('src', e.target.result);
document.getElementById('image-preview-before').appendChild(img_preview);
}

这里代码很好理解,主要是生成img元素来预览图片,考虑到第二次使用的时候页面上已经有了img元素,就直接修改对应文件即可。

RGB信息提取

这部分的背景知识其实很简单,就是每张图的每个像素会需要四个值来描述,亦即由(red, green, blue, alpha)所描述的色彩空间,其中alpha一般用作不透明度参数,在这个Demo里面我暂时没有用到,所以代码里也只是取了rgb的数值来进行计算。

1
2
3
4
5
6
7
8
9
10
11
12
// obtain image data
let imageData = context.getImageData(0, 0, img_width, img_height);
let imagePixelsRgb = [];
// process the stats
var len = imageData.data.length;
for (let i = 0; i < len; i += 4) {
let r = imageData.data[i],
g = imageData.data[i + 1],
b = imageData.data[i + 2];
imagePixelsRgb.push([r, g, b]);
};

显然这里有个疑问就是这里的context是何物。在这里就涉及到canvas的原理了。我们获取图像的像素的数值是通过将图像绘制在一个canvas上,然后再通过canvas的接口获取对应像素点的rgb信息,于是在context.getImageData()执行之前,我们还需要

1
2
3
4
5
6
7
8
9
10
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
var img_width = this.width;
var img_height = this.height;
// set the size of canvas
canvas.width = (img_width < 600) ? img_width : 600;
canvas.height = img_height * canvas.width / img_width + 50;
var eachWidth = canvas.width / maxcolors; // define each color bar width
// draw image
context.drawImage(this, 0, 0, canvas.width, canvas.height - 50);

关于宽高的设置是为了满足CSS文件里最大宽度600px的设定所做的妥协,而高度值那里额外增加的50则是为了最终demo里绘制出抽取的颜色所预留的空间。这都是后话了,因为最初的时候我直接设定的就是图片大小的宽高值,毕竟为了能糙快猛地整出来一个Demo,这些都暂时不做细调。

提取颜色

这是最核心的算法部分,目前的demo里采用了quantize.js所提供的实现,但在最初的算法里,我的想法非常朴素——既然是找最多的颜色值,那么做个统计然后排序就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var len = imageData.data.length;
for (let i = 0; i < len; i += 4) {
let r = imageData.data[i],
g = imageData.data[i + 1],
b = imageData.data[i + 2];
let hex_info = rgbToHex(r, g, b);
if (!colorInfo[hex_info]) {
colorInfo[hex_info] = 1;
} else {
colorInfo[hex_info]++;
}
};
var colorInfoArray = [];
var colorInfoKeys = getSortedKeys(colorInfo);
for (var k in colorInfoKeys) {
colorInfoArray.push([colorInfoKeys[k], colorInfo[colorInfoKeys[k]]]);
}

然而无情的事实是,一旦图像的大小超出400KB,基本上就得10+秒以上出结果,而且还存在一个严重的UE问题,因为没有合并相近的颜色,导致统计出来的颜色几乎差别都很小!所以这个朴素的算法被我最终抛弃了,我开始研究如何用更快的速度来获取一张图片中的Dominant Colors,我发现了两个办法:

  • 一方面,通过搜索引擎,了解到给图片划分palette来计算的算法,如果有兴趣,你可以去Color Quantization进行进一步的了解,简而言之,这个算法是一种分而治之的思想,大而化小,小而…哈哈哈,那就直接算呗!
  • 另一方面,我没有将全部的图片都画到canvas上面,换言之,我做了个抽样处理,所以如果你在demo上上传的图片足够清楚的话,你会发现预览图和最终结果图之间还是有一定区别的,但这个trade-off带来了处理速度的提升,基本上1s的数量级就能拿到结果

所以最终的代码就变得异常简洁,除去引入了quantize.js:

1
2
3
4
var maxcolors = 5; // currently only support top 5
var cmap = MMCQ.quantize(imagePixelsRgb, maxcolors)
var topResults = cmap.palette();
var topFive = topResults.map((x) => rgbToHex(x[0], x[1], x[2]));

图片展示

一切数据都有了之后,这部分的功能就很好实现,重点在于弄清楚context.drawImage()里面的几个参数的含义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// loop to display all
for (let i = 0; i < maxcolors; i++) {
// display the top 5 colors using canvas
var canvas1 = document.createElement('canvas');
var ctx = canvas1.getContext('2d');
canvas1.width = eachWidth;
canvas1.height = 50;
// set the size of canvas
let rectWidth = eachWidth,
rectHeight = 50;
// draw image
ctx.fillStyle = topFive[i];
ctx.fillRect(0, 0, eachWidth, rectHeight); // 75 for test
// display another kind preview with color display under image
// leave 1 line between the image and the color bar
context.drawImage(canvas1, 0, 0, eachWidth, 49, 0 + i * eachWidth, canvas.height - 49, eachWidth, 49);
}
var finalDisplay = document.getElementById('finalDisplay');
while (finalDisplay.firstChild) {
finalDisplay.removeChild(finalDisplay.firstChild);
}
finalDisplay.appendChild(canvas);

Demo效果

直接上图。

example
example

结语

目前的Demo还很粗糙,值得改进的地方很多,但已经实现了最核心的功能,接下来会考虑重构代码,并支持配置参数。

作为JS初学者,受StackOverflow和Github上的源码帮助甚多,很粗糙的一个小工具,作为自己JS学习路上的一个小的标记,代码写的比较挫,欢迎各种拍砖。