洪哥的博客里面,当你点进去一篇的时候,会根据文章封面的图片的主色来生成文章顶部背景的颜色,那我们来通过 js 来简单实现一下这个功能
概念
何为图片的主色?图片的主色是指在一幅图像中占据较大比例并且最具代表性的颜色。
我们知道图片是由一个个带有颜色的像素点组成的,简单理解的话,在图片中出现次数最多的那个颜色就是图片的主色
但是实际上提取主色没这么简单,我们这里先讨论简单实现,后面会进一步说明如何提高准确度
思路
从概念可以得知,我们只需要获取图片中出现次数最多的那个像素的颜色就行了
对于这种图片处理,我们可以使用 canvas API 来进行操作
首先将图片绘制到 canvas 上,然后就获取 canvas 上每一个像素点的颜色信息并保存到一个 map 中,最后获取 map 中记录次数最多的那个颜色
准备
首先先准备一张图片(最好颜色丰富一点),然后准备一个 html 文件,最后还需要部署一个本地服务器,因为要获取 canvas 绘制的图片的数据的话,需要这个一个有域名的图片,要不然会报跨域这个错,可以使用 http-server 或者使用 vscode 的 Live Server 插件来解决这个问题
实现
编写一个函数,接受一个图片的 src 地址,获取到图片后画到 canvas 上然后获取像素数据,再进行简单的提取和排序形成一个对象,提取颜色需要一定的时间,所以最好是异步,这里直接返回一个 Promise 对象,Promise 的结果是一个对象,包含颜色出现次数最多的那个颜色的 rgba、rgb 和 count,代码整体比较简单,没有什么难懂的地方,稍微难一点的都加了注释这里不再说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| const getImageMainColor = (src) => { const getRGBAArr = (pixelData) => { const colorList = []; const rgba = []; let rgbaStr = ""; let rgbStr = ""; const colorMap = new Map(); for (let i = 0; i < pixelData.length; i += 4) { rgba[0] = pixelData[i]; rgba[1] = pixelData[i + 1]; rgba[2] = pixelData[i + 2]; rgba[3] = pixelData[i + 3]; if (rgba.indexOf(undefined) !== -1 || pixelData[i + 3] === 0) { continue; } rgbaStr = rgba.join(","); rgbStr = rgba.slice(0, 3).join(","); if (colorMap.has(rgbaStr)) { colorList[colorMap.get(rgbaStr)].count++; } else { colorMap.set(rgbaStr, colorList.length); colorList.push({ rgba: `rgba(${rgbaStr})`, rgb: `rgb(${rgbStr})`, count: 1, }); } } colorList.sort((a, b) => { return b.count - a.count; }); return colorList; }; const getImageColor = (canvas, img) => { const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); const pixelData = ctx.getImageData(0, 0, canvas.width, canvas.height).data; return getRGBAArr(pixelData); }; return new Promise((resolve, reject) => { try { const canvas = document.createElement("canvas"); const image = new Image(); image.crossOrigin = "anonymous"; image.src = src; image.onload = () => { canvas.height = image.height; canvas.width = image.width; const color = getImageColor(canvas, image); resolve(color[0]); }; } catch (e) { reject(e); } }); };
|
结果预览
提取的颜色预览
优化
现在我们最基本的要求算是实现了,但是还有很多要优化的地方
首先对性能进行优化
我们先思考一下,一张图片如果稍微大一点几 MB 的话,那么是不是有几百万的像素甚至千万,那么我们要循环上千万次进行计算吗,显然是没必要的,如果对图片整体缩放 10 倍的话,图片整体的颜色不会发生肉眼可见的变化,除非你的图片很小很小。
基于此,我们可以对图片进行缩放,比如先缩小 10 倍再画到 canvas 上,这样就可以减少 10 倍的计算量
接着我们对结果进行优化
我们先思考,我们大部分常用的图片的主色都不会是黑色或者白色,那我们是不是可以过滤掉这些颜色,这样不仅可以减少计算量,还可以提高准确度
所以我们可以优化代码如下,在获取到图片之后先缩小 10 倍再画到 canvas 上,然后再对图片像素循环的时候,过滤掉我们不需要的颜色
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| const getImageMainColor = (src) => { const getRGBAArr = (pixelData) => { const colorList = []; const rgba = []; let rgbaStr = ""; let rgbStr = ""; const colorMap = new Map(); for (let i = 0; i < pixelData.length; i += 4) { rgba[0] = pixelData[i]; rgba[1] = pixelData[i + 1]; rgba[2] = pixelData[i + 2]; rgba[3] = pixelData[i + 3]; if (rgba.indexOf(undefined) !== -1 || pixelData[i + 3] === 0) { continue; } if ( (rgba[0] > 220 && rgba[1] > 220 && rgba[2] > 220) || (rgba[0] < 30 && rgba[1] < 30 && rgba[2] < 30) ) { continue; } rgbaStr = rgba.join(","); rgbStr = rgba.slice(0, 3).join(","); if (colorMap.has(rgbaStr)) { colorList[colorMap.get(rgbaStr)].count++; } else { colorMap.set(rgbaStr, colorList.length); colorList.push({ rgba: `rgba(${rgbaStr})`, rgb: `rgb(${rgbStr})`, count: 1, }); } } colorList.sort((a, b) => { return b.count - a.count; }); return colorList; }; const getImageColor = (canvas, img) => { const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); const pixelData = ctx.getImageData(0, 0, canvas.width, canvas.height).data; return getRGBAArr(pixelData); }; return new Promise((resolve, reject) => { try { const canvas = document.createElement("canvas"); const image = new Image(); image.crossOrigin = "anonymous"; const shrinkFactor = 10; image.src = src; image.onload = () => { canvas.height = image.height / shrinkFactor; canvas.width = image.width / shrinkFactor; const color = getImageColor(canvas, image); resolve(color[0]); }; } catch (e) { reject(e); } }); };
|
进阶与尾声
到这里我们还可以继续思考,其实大部分情况下我们提取的主色应该不是颜色出现最多的那个,而是一个综合的颜色,比如一张图片如果颜色很丰富的话,那么他的主色就不应该是某一个占比比较多的色块的颜色,而应该是一种颜色的组合,比如红色、黄色、蓝色、绿色等等,这样才能更准确的描述这张图片的主题。
我们可以对提取到的所有颜色进行排序,然后选取其中出现次数前五的那个颜色进行综合,比如颜色加起来再除以 5 获得一个平均颜色,或者自己创建一个合适的算法进行计算都可以
这里以前五个颜色的平均值作为主色,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| const getImageMainColor = (src) => { const getRGBAArr = (pixelData) => { const colorList = []; const rgba = []; let rgbaStr = ""; let rgbStr = ""; const colorMap = new Map(); for (let i = 0; i < pixelData.length; i += 4) { rgba[0] = pixelData[i]; rgba[1] = pixelData[i + 1]; rgba[2] = pixelData[i + 2]; rgba[3] = pixelData[i + 3]; if (rgba.indexOf(undefined) !== -1 || pixelData[i + 3] === 0) { continue; } rgbaStr = rgba.join(","); rgbStr = rgba.slice(0, 3).join(","); if (colorMap.has(rgbaStr)) { colorList[colorMap.get(rgbaStr)].count++; } else { colorMap.set(rgbaStr, colorList.length); colorList.push({ rgba: `rgba(${rgbaStr})`, rgb: `rgb(${rgbStr})`, count: 1, r: rgba[0], g: rgba[1], b: rgba[2], }); } } colorList.sort((a, b) => { return b.count - a.count; }); return colorList; }; const getImageColor = (canvas, img) => { const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); const pixelData = ctx.getImageData(0, 0, canvas.width, canvas.height).data; return getRGBAArr(pixelData); }; return new Promise((resolve, reject) => { try { const canvas = document.createElement("canvas"); const image = new Image(); const shrinkFactor = 10; image.crossOrigin = "anonymous"; image.src = src; image.onload = () => { canvas.height = image.height / shrinkFactor; canvas.width = image.width / shrinkFactor; const colors = getImageColor(canvas, image); let r = 0, g = 0, b = 0; colors.slice(0, 5).forEach((rgba) => { r += rgba.r; g += rgba.g; b += rgba.b; }); r = Math.round(r / 5); g = Math.round(g / 5); b = Math.round(b / 5); resolve(`rgb(${r}, ${g}, ${b})`); }; } catch (e) { reject(e); } }); };
|
结果预览
颜色预览
我们拿到了图片所有的颜色数据,所以我们可以任意修改,比如对颜色取反,就是用 255 减去现在的颜色也可以得到一个很特殊的效果,或者我们可以对颜色进行加权平均等等。