网传,产品经理要求App开发人员,让用户App的主题颜色能根据手机壳自动调整。

刚好笔者要做一个类似的事情,根据背景颜色自动改变文字的颜色,以便于用户识别。

image

alicl-bwq9y


正文

产品设计了一个人机校验组件,大致长这个样子。背景会每次随机取不同图片,开始的时候,箭头设置为蓝色。在背景为蓝色的时候,用户就分辨箭头就有些困难了。怎么解决这个问题呢?

image3

思路与实现

第一步

取到箭头底部背景的范围坐标。这个比较简单,基本运算就搞定,done

第二步

要识别图片,我们需要借助 Canvas,将图片绘制到 Canvas 上,来操作图像数据。

创建 Canvas 容器

1
2
3
4
const c4 = document.createElement('canvas')
c4.width = 190
c4.height = 190
const ctx4 = c4.getContext('2d')

放入图片

1
2
3
4
5
6
7
8
9
10
// 识别图片
const image = new Image()
image.onload = () => {
ctx4?.drawImage(image, 0, 0, 191, 190) // 绘制图片到 Canvas
const color = analysisColor(ctx4?.getImageData(0, 0, 191, 190)) // 分析颜色分布
setFontColor(color) // 设置字体颜色
resolve(true) // 完成Promise
}

image.src = images[`code-${index}`] // 取本次随机图片的地址设置到 image

跨域问题

可是进展并没有那么顺利,背景图片不在同域下面,Canvas 不允许跨域的图片,怎么办呢?

image

第三步

既然 Canvas 不允许跨域的图片,在无后端代理支持的情况下,怎么高效的解决这个问题呢?(本地是跨域,线上同域)

把图片下载的本地!借助 XMLHttpRequest 将图片先缓存到本地转成 Blob 对象,Canvas 是可以访问本地 Blob 的数据。

下载图片,解决图片跨域问题

1
2
3
4
5
6
7
8
9
10
11
// 下载图片,解决图片跨域问题
const xhr = new XMLHttpRequest()
xhr.open('get', images[`code-${index}`], true)
xhr.responseType = 'blob'
xhr.onload = function loaded() {
if (this.status === 200) {
const blob = this.response
image.src = window.URL.createObjectURL(blob)
}
}
xhr.send()

第四步

解决了跨域问题,接下来就是分析颜色了,getImageData 取到的就是图片 rgba 的数组。
这个时候就可以将计算好的坐标,代入到图片 rgba 里面计算其分布。

说到这里就要补一下图像算法的知识了。

许多从自然场景中拍摄的图像,其色彩分布上会给人一种和谐、一致的感觉;反过来,在许多界面设计应用中,我们也希望选择的颜色可以达到这样的效果,但对一般人来说却并不那么容易,这属于色彩心理学的范畴。从彩色图像中提取其中的主题颜色,不仅可以用于色彩设计,也可用于图像分类、搜索、识别等,本文分别总结并实现图像主题颜色提取的几种算法,包括颜色量化法(ColorQuantization)、聚类(Clustering)和颜色建模的方法

颜色量化算法

彩色图像一般采用RGB色彩模式,每个像素由RGB三个颜色分量组成。随着硬件的不断升级,彩色图像的存储由最初的8位、16位变成现在的24位、32真彩色。所谓全彩是指每个像素由8位($2^8$=0~255)表示,红绿蓝三原色组合共有1677万(256 x 256 x 256 )万种颜色,如果将RGB看作是三维空间中的三个坐标,可以得到下面这样一张色彩空间图:

image

RGB color cube

当然,一张图像不可能包含所有颜色,我们将一张彩色图像所包含的像素投射到色彩空间中,可以更直观地感受图像中颜色的分布:

image

因此颜色量化问题可以用所有矢量量化(vector quantization, VQ)算法解决。这里采用开源图像处理库 Leptonica 中用到的两种算法:中位切分法、八叉树算法。

这里核心使用中位切分法(Median cut) 参考项目 Github: color-thief

1
2
3
4
// 计算图片中间值
function analysisColor(rgbaArray: any) {
// Todo something,返回该区域颜色的主色
}

第五步

到这里,这个需求就算实现了基本核心的部分了,但是在运行过程中,发现性能消耗极大。大部分花在了 Canvas 绘制和图像遍历上

image

怎么来优化这个过程呢?能不能只提取图像的特征信息进行分析呢?

带着这两个问题,查阅了图像特征算法相关的文献后,找到了 方向梯度直方图(Histogram of Oriented Gradient, HOG) 这个算法。

基HOG特征

方向梯度直方图(Histogram of Oriented Gradient, HOG)特征是一种在计算机视觉和图像处理中用来进行物体检测的特征描述子。它通过计算和统计图像局部区域的梯度方向直方图来构成特征。Hog特征结合 SVM分类器已经被广泛应用于图像识别中,尤其在行人检测中获得了极大的成功。需要提醒的是,HOG+SVM进行行人检测的方法是法国研究人员Dalal 在2005的CVPR上提出的,而如今虽然有很多行人检测算法不断提出,但基本都是以HOG+SVM的思路为主。

主要思想

在一副图像中,局部目标的表象和形状(appearance and shape)能够被梯度或边缘的方向密度分布很好地描述。(本质:梯度的统计信息,而梯度主要存在于边缘的地方)。

具体的实现方法是

首先将图像分成小的连通区域,我们把它叫细胞单元。然后采集细胞单元中各像素点的梯度的或边缘的方向直方图。最后把这些直方图组合起来就可以构成特征描述器。

提高性能

把这些局部直方图在图像的更大的范围内(我们把它叫区间或block)进行对比度归一化(contrast-normalized),所采用的方 法是:先计算各直方图在这个区间(block)中的密度,然后根据这个密度对区间中的各个细胞单元做归一化。通过这个归一化后,能对光照变化和阴影获得更 好的效果。

优点

与其他的特征描述方法相比,HOG有很多优点。首先,由于HOG是在图像的局部方格单元上操作,所以它对图像几何的和光学的形变都能保持很好的不 变性,这两种形变只会出现在更大的空间领域上。其次,在粗的空域抽样、精细的方向抽样以及较强的局部光学归一化等条件下,只要行人大体上能够保持直立的姿 势,可以容许行人有一些细微的肢体动作,这些细微的动作可以被忽略而不影响检测效果。因此HOG特征是特别适合于做图像中的人体检测的。

HOG特征提取算法的实现过程

image

第六步

基于此,来做我们自己的算法实现。将原图在绘制时,按照等比平铺,一步步的绘制到 Canvas 格子上去。随着尺寸的缩小,图像的特征依然得以保留,大致效果如下。

image

在实验多个不同的压缩尺寸后,发现 16x16 这个尺寸能兼顾特征与识别性能,再小一些的格子比如 8x8 就会丢失特征值。

贴一下大致的实现过程

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
const checkBack = async (index: number) => {
return new Promise((resolve) => {
// 计算图片中间值
function analysisColor(rgbaArray: any) {
// Todo something,返回该区域颜色的主色
}

const c4 = document.createElement('canvas') // 压缩尺寸计算用
c4.width = 16
c4.height = 16
const ctx4 = c4.getContext('2d')

// 识别图片
const image = new Image()
image.onload = () => {
ctx4?.drawImage(image, 0, 0, 17, 16) // 绘制图片到 Canvas
const color = analysisColor(ctx4?.getImageData(0, 0, 17, 16)) // 分析颜色分布
setFontColor(color) // 设置字体颜色
resolve(true) // 完成Promise
}

// 下载图片,解决图片跨域问题
const xhr = new XMLHttpRequest()
xhr.open('get', images[`code-${index}`], true)
xhr.responseType = 'blob'
xhr.onload = function loaded() {
if (this.status === 200) {
const blob = this.response
image.src = window.URL.createObjectURL(blob)
console.log(image.src)
}
}
xhr.send()
})
}

最后

我们再来看看优化后,分析过程的耗时,差不多提升了 100 倍的速度!!!

image

最终的效果图:

image