最近在对接公司一些新闻接口的时候,发现接口茫茫多:CMS接口、无线CMS接口、正文接口、列表接口……更令人捉急的是,由于新闻推送场景不同,每条新闻的配图尺寸也就不同,比如PC要求高清大图,而移动端就会根据屏幕尺寸要求各种尺寸的小图,一个接口也就要吐出好几个尺寸的图片供客户端使用。比如无线CMS的接口里就需要640330、150120、280*210……那么问题来了,难道每多一种尺寸就需要编辑裁一次图上传到CMS?
要实现图片自动化剪裁,最重要的当然是最大化的保留图片的信息。当要求的裁剪尺寸比例和原图片不同时,就需要考虑怎样裁剪才能更少的影响原图。 最简单的方法就是对齐裁剪:先计算出裁剪的比例,保留原图满足比例最长的一条边,从中间裁剪后缩放成目标尺寸。举个栗子,有一张640330的横图,我们要把它裁成120150竖图,步骤如下:
1.计算目标比例 = 120 / 150 = 0.8
2.保留原图的高,计算裁剪后宽度 = 330 * 0.8 = 264
3.定位到图片中央,裁掉两边的部分
搞定,这么一看,要全自动地裁剪一张图片很简单嘛!全网新闻图片裁剪全自动化指日可待了……等等,处理了一批图片之后,发现好像有哪儿不对……
有这样的:
还有这样的:
这些支离破碎的迷之图片可不是我想要的结果……
效果不满足需求,改!总结一番后,我发现上述这些裁剪之所以坑爹是因为图片的主体——人物的脸部被咔擦了。而新闻的图片多以人物为主,也就是说,保留原图的信息量也就是保持人脸的完整,结果呼之欲出——先识别出图片中的人脸,以此为中心裁剪图片即可。那么问题来了,人脸识别怎么做呢?听上去是个很高大上的技术啊,是不是还牵扯到各种机器学习的方法?提出这个方案后,我也是茫然的,不能说为了做个图片裁剪就各种捣鼓神经网络什么的,搞出来duang,别人会说都是特效,是有化学成分的……直到万能的Python解决了一切,它告诉我说几十行代码就能做出个人脸识别,就问你信不信。反正当时我是不信的,但现在我信了。
在做人脸识别之前,首先要理解在计算机里,一张图片意味着什么。简单地说,一张图片就是一个由数字组成的矩阵,矩阵的每一个单元上的数字则表示了光在图片的这个像素上留下的数字信号,也就是说,这一整个矩阵就是一张图片的数字表达形式。对于彩色图片,这些数字是经过几个通道叠加而成的。比如我们常见的RGB通道的图片,每一个像素都包含红(R)、绿(G)、蓝(B)三个通道,每个通道都由一个八位的二进制数来表示灰度值,可以算出这个灰度值的范围位0~255(2^8 - 1)。最后,一幅图在数字化之后是这样的:
(0, 0, 0) (2, 0, 0) …… (0, 0, 0) …… …… …… …… (160, 255, 255) (170, 255, 255) …… (255, 255, 255)
显然,在做人脸识别的时候,我们并不需要所有通道的全部信息,因此在计算时,一般都是先将图片转换为单通道的灰度图,然后去掉一些冗余数据,提高计算效率——可以联想一下PS的抠图方法:转为单通道灰度图,再调整色阶使亮部更亮、暗部更暗,达到更简单地取出轮廓边缘的目的。
有了简化之后的图片数据,我们需要在这个基础上对人脸进行搜索。人脸是个和复杂的东西,他包含无数的特征,因此相对应地我们也需要将“识别是否人脸”这个庞大的任务拆解,分成成千上万个目的单一的小任务,这些任务会遍历目标图片,将待测的图片窗口特征作为输入,所有任务的结果得出之后我们便可据此判断目标是不是人脸,这些任务即被称为“分类器”。而在本文中,我们要使用的是Haar分类器。这个分类器的名称来由是它使用了Haar特征来计算图片像素,并作为分类器的输入。那么Haar特征又是个什么东西?我们来看下张图:
图:Haar特征[1]
所以这一堆黑白色的条条框框是什么鬼?我们可以把这些矩形当作提取图片特征的过滤器,当你把某个矩形套在图片的某个区域上,你会发现这块区域中有一部分像素落在了矩形的白色区域里,另一部分落在黑色区域。然后我们用白色区域的像素和减去黑色区域的像素和,得到的结果便是该图片区块的“特征”。可以想象,把这些矩形放在人脸上提取出的特征,与在其他区域上提出的一定会大相径庭,因此通过Haar量化这些特征,再通过分类器(如Boost),我们便可以判断某个区域是否人脸。
上文中我们知道了Haar特征提取分类识别人脸的过程,然而问题来了——如果我们要扫描整张图片,判断出哪些区域是人脸,就必须得从左上到右下的每个区块都重复提取特征——分类的过程,在大数据量的情况下,这样的计算十分缓慢,也很消耗资源。
为了解决这个问题,神器OpenCV采用了级联表技术。它将检测的过程拆分成若干以瀑布流的形式组成的小任务,只有上层的检测任务通过之后,才会继续执行下层的检测,只有所有测试都通过后才会最终识别为人脸。这样做的好处是在最初的检测中便可过滤掉大部分不属于人脸的对象,从而无需对所有特征区块都做一遍计算,大大降低了计算时间。
好了,终于可以进入正题了。秉着知其然还要知其所以然的精神,我们扯了上面之那么长的一段话,现在终于可以来到见证奇迹的时刻了。
几十行代码完成人脸识别,首先需要OpenCV和Python这两个神器。OpenCV实现了Haar级联,而Python对OpenCV又做了一层封装。话不多少,上代码:
首先,用PIL库获取图片对象
from PIL import Image
img = Image.open('test.jpg')
示例图片:
打开haarcascadefrontalfacedefault.xml。这个xml文件是OpenCV训练好的人脸Haar特征分类器,我们要做的就是直接用这个数据来匹配图像。下面几行完成了读取级联表和图像灰度化。
face_cascade = cv2.CascadeClassifier('conf/haarcascade_frontalface_default.xml')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
下面的代码运用Haar分类器扫描图像,识别人脸。
faces = face_cascade.detectMultiScale(
gray,
scaleFactor=1.1,
minNeighbors=10,
minSize=(30, 30),
flags=cv2.cv.CV_HAAR_SCALE_IMAGE
)
detectMultiScale方法中的各个参数需要根据情况作调整,其中: - gray表示灰度图; - scaleFactor用以补偿多人脸透视现象中存在的大小差距; - minNeighbors表示构成扫描图像滑动窗口的矩形的最小个数; - minSize表示滑动窗口的大小 - flags指定边缘检测的策略,一般为默认值
方法的返回值是一系列指定人脸边缘的矩形元组,每个元组中包括矩形的横、纵坐标、宽、高。为了方便查看效果,我们遍历返回值,根据坐标绘制一个矩形,展示出来。
for (x, y, w, h) in faces:
cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 2)
cv2.imshow("Faces found", img)
cv2.waitKey(0)
结果如图:
实验证明,在照片较清晰且人脸为正脸的话,效果十分不错。有了人脸的位置,我们再想优化剪裁就很容易了。举个栗子:在以上594 X 444的原图基础上,我们想裁出一张90 X 160的竖图。首先,我们遍历之前识别的人脸中心坐标,取它们的中心点:
center = [0, 0]
point_num = len(faces)
if point_num == 0:
return center
for (x, y, w, h) in faces:
center[0] += (x + w/2)
center[1] += (y + h/2)
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
center[0] /= point_num
center[1] /= point_num
然后再以此坐标为中心,按照之前的对齐裁剪方案,最终结果如下:
对比之前的方案处理结果:
是不是高下立判呢:)
当然,还会存在诸如多人脸如何取舍、误识别等问题,可以针对具体问题具体优化。
图像处理是新闻数据里很重要的一环,人脸识别只是其中一个部分,还有很多手段去提高新闻图片质量,钻研其中也是一件很有乐趣的事情。这一块的公用部分的源代码我传到了Github,比较简单,也希望能起到抛砖引玉的效果XD。
[1] Wilson P I, Fernandez J. Facial feature detection using Haar classifiers[J]. Journal of Computing Sciences in Colleges, 2006, 21(4): 127-133.