如何对图片主题色进行提取
一起网易云 🍉
网易云音乐想必是大家很熟悉的一款 app 了,毕竟大家在深夜都会网抑云

开玩笑了,最近在网易云听歌时,发现了一个很有意思的特效:

就是切换歌曲时,会根据当前封面替换背景色。作为资深切图仔,我那该死的好奇心兜不住了,不行,我要去一探究竟。
首先我构思了很多它可能的实现方式:
- 机器学习对图片进行色彩分析
- 前端提取图片主色调,做渐变处理
- 封面背景图做高斯模糊
对于第一种,他不在我的知识范围内,这里就不展开说明了 😂。
第二种的话,一般都是利用canvas来实现。
第三种相对来说,从技术层面来看,实现上是最为简单的。
做了猜测分析后,我默默打开了熟悉的 Chrome 控制台,打开了网易云音乐的源代码:

好家伙,果然是第三种实现方式。🤐
本来到这里,本文就该结束了。但之前也有朋友问过我如何对前端图片主题色进行提取的问题,正好之前也做过类似的需求,这里就展开做个说明吧。
我们这里以一个图片网站为例,来展示实际业务中应用较广的场景:

在弱网下,图片加载速度较慢,此时在图片完全加载之前,提取图片的主色调,然后填充为背景色。这样用户体验能有较大的提升。
那具体是怎么实现的呢?🤔
我们这里采用canvas来实现,具体分为三步:
- 获取图片数据
- 对图片数据进行处理
- 对颜色列表排序
这里我们使用的测试图片为:

相对来说,主色调较为明显,也便于测试~
获取图片数据 🦊
我们知道图片是由一个个像素点组成的。通过 canvas 的getImageData()方法恰好可以获取图片的像素数据:
let imgObj = document.getElementById('yourId');
// 创建画布
let canvas = document.createElement('canvas');
canvas.setAttribute('width', imgObj.width);
canvas.setAttribute('height', imgObj.height);
let context = canvas.getContext('2d');
// 将图片画在画布上
context.drawImage(imgObj, 0, 0);
// 获取像素数据
let imgData = context.getImageData(0, 0, imgObj.width, imgObj.height);
let pixelData = imgData.data;
但这时你去打印pixelData,你会发现结果为:

好家伙,全是 0,,,😳
我一时想不到是什么原因:难道是 canvas 的 api 使用不熟练?

在stackoverflow上找到了上面的回答:

但是我修改后还是不行。
这时,我想到图片加载是异步的。可能图片还没加载完毕就开始从画布读取图片数据了,显然这是不对的。于是我对原有代码做了一番调整:
getMainColor("./test.jpeg");
function getMainColor(image) {
return new Promise((resolve, reject) => {
try {
const canvas = document.createElement("canvas");
const img = new Image(); // 创建img元素
img.src = image; // 设置图片源地址
img.onload = () => {
let color = getImageColor(canvas, img);
resolve(color);
};
} catch (e) {
reject(e);
}
});
}
function getImageColor(canvas, img) {
const context = canvas.getContext("2d");
context.drawImage(img, 0, 0);
// 获取像素数据
let pixelData = context.getImageData(
0,
0,
canvas.width,
canvas.height
).data;
console.log("pixelData", pixelData);
return pixelData;
}
事实证明:it's true

获取了图片数据,下一步就要对其进行相应的处理。
对图片数据进行处理 🦁
展开上一步得到的数据:

这里的数据是什么意思呢?其实就是rgba,分布代表红色(Red),绿色(Green),蓝色(Blue)和透明度(Alpha)。rgba 的图片每个像素点是由上面四个数值表示的。也就是说每四个为一组。
知道了规律,那让我们来对数据做一下清洗:主要就是对颜色进行分组,并统计每种颜色分别出现的次数:
function getImageColor(canvas, img) { const context = canvas.getContext("2d"); context.drawImage(img, 0, 0); // 获取像素数据 let pixelData = context.getImageData( 0, 0, canvas.width, canvas.height ).data; console.log("pixelData", pixelData); return getCountsArr(pixelData); } function getCountsArr(pixelData) { let colorList = []; let rgba = []; let rgbaStr = ""; // 分组循环 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; } // console.log("rgba", rgba); rgbaStr = rgba.join(","); if (rgbaStr in colorList) { ++colorList[rgbaStr]; } else { colorList[rgbaStr] = 1; } } console.log("colorList", colorList); return colorList; }
打印colorList结果为:

到这里,我们就得到了每种数据分别出现的次数。
对颜色列表排序 🐳
最后一步,对上面得到的色值对象做一个排序:
for (let prop in colorList) { arr.push({ // 如果只获取rgb,则为`rgb(${prop})` color: `rgba(${prop})`, count: colorList[prop], }); } // 数组排序 arr.sort((a, b) => { return b.count - a.count; }); console.log("arr", arr);
排序后得到如下结果:

到这里我们就得到了图片色值出现次数从大到小的排序数组,我们来看排在第一位的rgba(206,205,201,255):

再把测试图片贴一下:

肉眼可见的主题色已经被提取出来了!🎉
反思 🚀
最后还是回到文章最开始提到的网易云音乐的播放器特效。不管它的实现方式是怎么样的,它的这种产品创意是值得我们学习的。
我们平时在浏览国内外的一些网站或者使用一些 app 时,总能遇到一些让你拍手称赞的效果。而这些特效往往又与我们前端分不开。
俗话说:前端是离产品最近的开发工程师,那最近你有没有遇到一些让你感觉很惊艳或者很有想法的效果呢,欢迎在评论区留言 🎯
转自https://mp.weixin.qq.com/s/6XgDWhIA-2jfacg49j6alA

浙公网安备 33010602011771号