如何对图片主题色进行提取

前言

本文转载于公众号『前端森林』,对作者前端森林表示感谢。

并非原文转载,里面部分代码及文字按照个人风格做了修改,但最终效果一致。

网易云特效

在网易云听歌时,发现了一个很有意思的特效:就是切换歌曲时,会根据当前封面替换背景色。

首先我构思了很多它可能的实现方式:

  • 机器学习对图片进行色彩分析
  • 前端提取图片主色调,做渐变处理
  • 封面背景图做高斯模糊

对于第一种,他不在我的知识范围内,这里就不展开说明了。

第二种的话,一般都是利用canvas来实现。

第三种相对来说,从技术层面来看,实现上是最为简单的。

做了猜测分析后,我默默打开了熟悉的 Chrome 控制台,打开了网易云音乐的源代码:

好家伙,果然是第三种实现方式。

本来到这里,本文就该结束了。但之前也有朋友问过我如何对前端图片主题色进行提取的问题,正好之前也做过类似的需求,这里就展开做个说明吧。

我们这里以一个图片网站为例,来展示实际业务中应用较广的场景:

在弱网下,图片加载速度较慢,此时在图片完全加载之前,提取图片的主色调,然后填充为背景色。这样用户体验能有较大的提升。

那具体是怎么实现的呢?

我们这里采用canvas来实现,具体分为三步:

  • 获取图片数据
  • 对图片数据进行处理
  • 对颜色列表排序

这里我们使用的测试图片为:

相对来说,主色调较为明显,也便于测试~

获取图片数据

我们知道图片是由一个个像素点组成的。通过 canvas 的getImageData()方法恰好可以获取图片的像素数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function getImage(url) {
return new Promise(resolve => {
let img = new Image();
img.onload = () => {
resolve(img);
}
img.src = url;
})
};

async function getColor(url) {
const img = await getImage(url);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
const PXdata = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
console.log(PXdata);
}

输出结果如下:

Uint8ClampedArray (canvas元素专有类型)之前文章有提及。

获取了图片数据,下一步就要对其进行相应的处理。

对图片数据进行处理

getImageData() 方法返回 ImageData 对象,该对象拷贝了画布指定矩形的像素数据。

对于 ImageData 对象中的每个像素,都存在着四方面的信息,即 RGBA 值:

  • R - 红色 (0-255)
  • G - 绿色 (0-255)
  • B - 蓝色 (0-255)
  • A - alpha 通道 (0-255; 0 是透明的,255 是完全可见的)

展开上一步得到的数据:

ImageData对象可参考ImageData

知道了规律,那让我们来对数据做一下处理:主要就是对颜色进行分组,并统计每种颜色分别出现的次数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function getCountColor(data) {
let rgba = [];
let rgbaStr = '';
let colorList = {};
for(let i = 0; i < data.length; i+=4) {
rgba[0] = data[i];
rgba[1] = data[i + 1];
rgba[2] = data[i + 2];
rgba[3] = data[i + 3];

rgbaStr = rgba.join();

if(colorList[rgbaStr]) {
colorList[rgbaStr] += 1;
} else {
colorList[rgbaStr] = 1;
}
}
console.log(colorList)
}

输出结果为:

到这里,我们就得到了每种颜色分别出现的次数。

对颜色进行排序

最后一步,对上面得到的色值对象做一个排序:

1
2
3
4
5
6
7
for(let prop in colorList) {
arr.push({
color: prop,
count: colorList[prop]
})
}
arr.sort((a, b) => b.count - a.count);

排序后得到如下结果:

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

最终代码:

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
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>取主题色</title>
<style>
* {
margin: 0;
padding: 0;
}
html, body {
height: 100%;
width: 100%;
}
img {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
</head>
<script src="./index.js"></script>
<script>
(async (url) => {
let data = await getColor(url);
document.body.style.background = 'rgba(' + data.color + ')';
})("./img/wukong.jpeg")
</script>
<body>
<img src="./img/wukong.jpeg" alt="">
</body>
</html>
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
// index.js
function getImage(url) {
return new Promise(resolve => {
let img = new Image();
img.onload = () => {
resolve(img);
}
img.src = url;
})
};

async function getColor(url) {
const img = await getImage(url);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
const PXdata = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
return getCountColor(PXdata);
}

function getCountColor(data) {
let rgba = [];
let rgbaStr = '';
let colorList = {};
let arr = [];
for(let i = 0; i < data.length; i+=4) {
rgba[0] = data[i];
rgba[1] = data[i + 1];
rgba[2] = data[i + 2];
rgba[3] = data[i + 3];

rgbaStr = rgba.join();

if(colorList[rgbaStr]) {
colorList[rgbaStr] += 1;
} else {
colorList[rgbaStr] = 1;
}
}
for(let prop in colorList) {
arr.push({
color: prop,
count: colorList[prop]
})
}
arr.sort((a, b) => b.count - a.count);
return arr[0];
}

最后

最后还是回到文章最开始提到的网易云音乐的播放器特效。不管它的实现方式是怎么样的,它的这种产品创意是值得我们学习的。

我们平时在浏览国内外的一些网站或者使用一些 app 时,总能遇到一些让你拍手称赞的效果。而这些特效往往又与前端分不开。