React获取图片色值

本片文章主要涉及两个场景:

  1. 根据图片的平均色值获取文字的最佳显示颜色
  2. 提取图片的主题色

一、根据图片的平均色值获取文字的最佳显示颜色

登录页面由登录框,铺满背景的图片,位于背景图片上方的文字构成,背景图片可配置,如下图。如果这时候文字的颜色固定为白色,图片配置为白雪背景的图片,那么就会出现版权信息与白色背景太过相似,而显示不清晰的情况。思路是通过计算图片的平均色值,基于此判断文本应该为黑色或白色能够拥有更高的对比度。具体实现过程如下:

image-20210925165445682

1. 创建图片标签
1
2
3
4
5
6
7
8
9
10
export const createImage = (url: string): Promise<HTMLImageElement> => {
return new Promise((resolve, reject) => {
const image = new Image();
image.addEventListener("load", () => resolve(image));
image.addEventListener("error", (error) => reject(error));
// 图片添加跨域
image.setAttribute("crossOrigin", "anonymous");
image.src = url;
});
};
2. 将图片绘制到 canvas 容器中,获取其像素数组

这里因为我获取的是局部图片而非整张图片的像素数组,所以针对截取哪部分,做了一下处理。即可以通过传入相应的比例系数截取指定部分,其实就是相当于对 canvas 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
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
export const getImgData = async (
params: GetImgDataParams
): Promise<GetImgDataRes> => {
const {
imgSrc,
xMultiple = 0,
yMultiple = 0,
wMultiple = 1,
hMultiple = 1,
} = params;

const multipleArr = [xMultiple, yMultiple, wMultiple, hMultiple];
const isVerify = multipleArr.every((item) => item >= 0 && item <= 1);
if (!isVerify) {
throw new Error("请输入合法的比例系数,即大于0小于1的数字");
}

const myCanvas = document.createElement("canvas");
const bgImg = await createImage(imgSrc);

const iHeight = bgImg.height;
const iWidth = bgImg.width;
const canvasWidth = iWidth * wMultiple;
const canvasHeight = iHeight * hMultiple;
const canvasSize = canvasWidth * canvasHeight;

myCanvas.width = iWidth * wMultiple;
myCanvas.height = iHeight * hMultiple;

const ctx = myCanvas.getContext("2d");
if (!ctx) {
throw new Error("Canvas创建失败");
}
ctx.drawImage(
bgImg,
iWidth * xMultiple,
iHeight * yMultiple,
canvasWidth,
canvasHeight,
0,
0,
canvasWidth,
canvasHeight
);

// 获取canvas中图像的像素数据
const data = ctx.getImageData(0, 0, canvasWidth, canvasHeight).data;
return { data: data, canvasSize: canvasSize };
};
3. 根据像素数组计算出平均像素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export const getAverageColor = (params: GetImgDataRes): number[] => {
const { data, canvasSize } = params;
let r = 0;
let g = 0;
let b = 0;

for (let i = 0, offset; i < canvasSize; i++) {
offset = i * 4;
r = data[offset + 0];
g = data[offset + 1];
b = data[offset + 2];
}
// 求取平均值
r = Math.round(r / canvasSize);
g = Math.round(g / canvasSize);
b = Math.round(b / canvasSize);

return [r, g, b];
};

这里解释一下这个为什么要这样写,看下面这张图你就明白了。我们通过 getImageData 得到的像素数组是一个一维数组,包含以 RGBA 顺序的数据,数据使用 0 至 255(包含)的整数表示。就像这样: preview

4. 使用平均像素计算 YIQ 值,通过该值判断文本显示颜色。

YIQ值具体指什么感兴趣的可以查一下,我这里直接解释为色系,就是通过这个值判断色彩是偏黑色系还是白色系。从而判断文本应该是黑色还是白色,基于哪个会具有更高的对比度,以此来提供最佳的可读性。

1
2
3
4
export const getContrastYIQ = (r: number, g: number, b: number) => {
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
return yiq >= 128 ? "black" : "white";
};

像这样,总能找到合适的显示颜色:

localhost_3001_theme-color


二、提取图片的主题色

上传一张主题图片,提取该图片中的主题色。一开始我是打算直接使用 color-thief 的,参照官方文档中的使用 ES6 方式引入行不通,我就去看了一下源代码,一通研究之下发现其实挺简单,最核心的代码是引入 quantize 包处理颜色数组的一段,下面我会讲到,于是我就基于他的源码做了一下处理。以下是实现过程:

1. 绘制图片到 canvas 中,提取颜色数组

提取主题色的过程前半段与我们上面的场景一致,我们都是需要先获取到图片的像素数组,代码可以参考上面,这里不再赘述。

2. 整理有效像素数组

imgData 就是我们要提取图片的像素数组;pixelCount 是像素点的数量,也就是图片的尺寸;quality 是精度,因为很多时候其实我们没必要挨着去将每个像素点取出来,从下面的方法中我们能看出,该值越大我们就会跳过更多的像素点,即获取到的色值就会越不准确,但是同时处理速度也会有所上升,所以需要做权衡

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
export const createPixelArray = ({
imgData,
pixelCount,
quality,
}: CreatePixelArrayParams) => {
const pixels = imgData;
const pixelArray = [];

// 以适合量化函数的数组格式存储 RGB 值
for (let i = 0, offset, r, g, b, a; i < pixelCount; i = i + quality) {
offset = i * 4;
r = pixels[offset + 0];
g = pixels[offset + 1];
b = pixels[offset + 2];
a = pixels[offset + 3];

// 像素要是不透明的和半透明以上的
if (typeof a === "undefined" || a >= 125) {
// 像素不能是太贴近白色的
if (!(r > 250 && g > 250 && b > 250)) {
pixelArray.push([r, g, b]);
}
}
}

return pixelArray;
};

我们需要把获取到像素数组做一些处理,因为我们只是提取主题色,所以我们不需要过于透明的颜色。即 rgbaa 值不存在或小于 125 的。rgb 三个值同时大于 250 的我们也不需要,因为我们认为他过于贴近白色,去除掉这部分不会影响提取效果,还可以提高处理效率。

3.量化颜色数组,并返回调色板

有关颜色提取的算法主要有:最小差值法中位切分法八叉树算法 等。这里使用了 quantize 包来处理颜色,这个包使用的是中位切分法

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
export const getPalette = async ({
imgSrc,
colorCount = 10,
quality = 10,
}: GetPaletteParams) => {
if (
typeof colorCount === "undefined" ||
!Number.isInteger(colorCount) ||
colorCount < 2 ||
colorCount > 20
) {
colorCount = 10;
}

if (
typeof quality === "undefined" ||
!Number.isInteger(quality) ||
quality < 1
) {
quality = 10;
}

const { data: imgData, canvasSize: pixelCount } = await getImgData({
imgSrc,
});
const pixelArray = createPixelArray({
imgData,
pixelCount,
quality: quality,
});

// quantize将像素数组进行量化,聚类,最终返回面板数组
// 使用中位切分法
const cmap = quantize(pixelArray, colorCount);
const palette = cmap ? cmap.palette() : null;

return palette;
};

效果如下:localhost_3000_theme-color (3)

结尾

两个场景就介绍完了,一开始本来只是讲显示文字颜色这个功能的,但是后面发现这两个场景有很大的共同点,所以就把之前的方法整理了一下,重新进行了一次封装,上述讲到的所有功能方法我已经封装完毕并发布到 npm 并上传至 github,感兴趣的朋友可以看代码,仓库地址:https://github.com/chanceyliu/react-img-contrast/tree/master