平均脸
公司年会,大部门一起照了大合影。忽然有兴趣看看大家的平均脸是什么样子的,于是用 OpenCV 从大合影中提取出一千多名程序员的脸,构造了所有人的平均脸。
拿给同事看,大家又要求看分性别平均的平均脸。于是又下载了 Caffe 的 gender classification model,将样本做了一下性别分类,之后分别构造了双方的平均脸。得出结果:大平均的颜值原来是被男生拉低的[哈哈]
本文就讲述根据照片计算平均脸的原理,具体代码,配发实现代码和分类模型。
本场 Chat 只有文章,没有交流。
0. 有趣的平均脸
1878年,英国的弗朗西斯·高尔顿爵士(Sir Francis Galton)发明了一种将许多人的照片合成为一张照片,从而创造出一个“平均”面容的技术。
弗朗西斯·高尔顿爵士,英国维多利亚时代的博学家、人类学家、优生学家、热带探险家、地理学家、发明家、气象学家、统计学家、心理学家和遗传学家;也是《物种起源》作者查尔斯·达尔文的表弟。*
当时具体的合成方法是照片叠加——给多个人,比如20个人,照相,将每个人照片所需的曝光时间缩短为1/20,通过20次曝光得到一张“平均”照片。
弗朗西斯·高尔顿最初合成平均脸的目的是将不同“种类”的人(例如:囚犯、精神病患者等等)视觉化,以期得到这类人的“原型”(共同特征),但结果却意外的发现,这样合成的人脸却比用于合成大部分(甚至是全部)都要好看!
虽然高尔顿爵士的初衷没有达到,合成平均脸的方法却保留了下来。
随着技术的发展,照片不再需要物理底片,合成也不再需要复杂的曝光冲印技术,通过一些简单的操作就能做到,人人都可以上手。
大家想必看到过一些按国家、民族合成的平均脸吧:
想不想自己动手制作一张周围人的平均脸?
一点都不复杂,只要知道了原理,会写几行简单的代码,就能顺利完成。
1. 用Image Morphing技术叠加照片
Image Morphing技术的原理相当简单:给定两张图片I和J,我们通过叠加(或者叫做混合)I和J来获得一张中间状态的图片M。
I和J的叠加由一个参数[0,1]区间内的参数alpha来控制。当alpha=0时,M就等同于I,而aphla=1时,M就为J。
换言之, M中的每一个像素M(x,y),都可以通过这样一个公式来得到它的值:
M(x,y) = (1 – alpha)·I(x,y) + alpha·J(x,y)
当alpha=0.5的时候,I和J就五五开,平均贡献了M。如果I和J是两张人脸照片的话,M自然也就成了它们的“平均脸”。
看起来好容易哦,那我们赶紧找两张照片来试试吧!就用这两张:
这两张照片alpha=0.5后直接叠加的结果是这样的:
这也不是人脸呀!先别急,看看为什么会这样?
从这张“重影图”上不难看出来,之所以这样,是因为最基本的五官都没有对齐。如果我们事先把两个人的眼睛和嘴对齐,效果就不会是这样的了。
2. 叠加两张“对齐的”人脸
叠加图片I和图片J的时候,首先应该建立两张照片中像素的对应关系。
对I中的某一个像素点(xi,yi),我们不是直接在J中取同样位置的点就可以了,而是要找到它在J中内容上的对应点 (xj,yj),然后再进一步找到M中这两个点叠加之后应当处在的位置(xm,ym),最后再用下列式子得出M中对应点的像素值:
xm = (1-alpha) · xi + alpha · xj
ym = (1-alpha) · yi + alpha · yj
算式-1
对一个像素点我们这样做,对整幅图片,则是将上面的过程运用到它的每一个像素点上:
M(xm,ym) = (1 – alpha)·I(xi,yi) + alpha·J(xj,yj)
算式-2
很好,我们已经知道从原理上该怎么叠加两张图了。
其中关键的一步就是:找到对应点。
其实对应点叠加的方法可以用来叠加任何图片,不仅限于人脸。不同物体的叠加,真正的区别就在于找到像素点之间的对应关系!一旦对应关系找到,直接运用算式-2就好了,
既然我们现在要做的是叠加人脸,那么首先当然要找到人脸上的对应点。
人脸是生活中最常见的事物,我们每一个人都非常熟悉。一个人的脸如果用简笔画画出来,可以简化为:脸型+五官(眉毛、眼睛、鼻子、嘴)。
那么如果我们要叠加两个人的脸的话,自然就是要针对他们的脸型和五官形制求平均。
人的五官如果用图形来描绘,都是不规则图形。如果要完全不走样的获取一个人的眼睛、眉毛、鼻子或者嘴,需要绘制非常复杂的形状。
实际上,我们没有必要这样做,而是可以通过一种非常简单的近似方法,把一张人脸分割成若干三角形的区域。然后再来叠加两张脸上对应的三角区域。
具体方法如下:
Step 1. 获取人脸特征
在图片中获取人脸和人脸特征(脸型+五官)。
我们先在每张面孔上获取68个面部基准点(如下图)。
Step2. Delaunay 三角剖分
在获得了68个面部基准点之后,我们结合人脸所在的矩形的四个顶点和每条边的中心点,将人脸所在的矩形分割成如下图所示的三角形的组合。
这一方法又称为Delaunay三角剖分。更多细节请看这里。
Delaunay三角剖分将图像分解成若干三角形。
Step 3. 基于Delaunay剖分三角形的仿射变换
得到这些Delaunay剖分三角形后,再分别对齐各个区域,对其中像素值进行平均。
【step-3.1】 使用前述的算式-1,根据图像I和图像J中已经获得的76个点,在叠加的结果图像M中找到76个点(xm, ym)
【step-3.2】现在我们在图像I,J和M中分别得到了76个点,以及由这76个点剖分而成的三组三角形。
从图像I中选取一个三角形ti,在M中找到对应区域tm,通过ti三个顶点到tm三个顶点的映射关系来计算ti到tm的仿射变换。
同理计算出tj到tm的仿射变换。
【step-3.3】 扭曲三角形
对于图像I中的一个三角形,使用step-3.2中计算出的放射变换,将其中每一个像素通过仿射变换对应到M中对应的位置去。
重复这个过程,处理图像I中的每一个三角形,得到一个扭曲的(warped)图像I'。用同样的方法处理图像J,获得扭曲的图像J'。
【step-3.4】叠加两张脸
在step-3.3中我们已经得到了扭曲的图像I'和图像J'。这两个图像就可以直接使用算式-2进行叠加了。最后得到叠加结果。
4. 叠加多张人脸
算式-2用于叠加2张人脸,在alpha=0.5时求取的是两张脸的平均。
那么我们把算式推广一下,从图像I和图像J推广为图像I1, I2, I3, ..., In;令alpha=1/n;则算式-2变形为如下:
M(xm,ym) = 1/n · [I1(xi1, yi1) + I2(xi2, yi2) + ... ... + In(xin, yi_n)]
由此,我们也就得到了n张脸的平均。
用这个方法,我们可以得到6位美国总统的平均脸:
他们平均之后的样子是这样的:
我们用同样的方法来看看gitchat 达人课讲师的平均脸!
这是平均后的结果:
5. opencv + dlib 轻松搞定平均脸
之前是从描述角度来讲解平均脸原理。现在,我们来看看code。
[Code -1 ] 使用dlib来进行人脸识别和人脸特征点的提取
detector = dlib.get_frontal_face_detector()# predictor_path is the local path of facial shape predictor# we don't need to train facial shape predictor by ourselves,# the predictor could be downloaded from # http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2predictor = dlib.shape_predictor(predictor_path) img = io.imread(image_file_path)dets = detector(img, 1)for k, d in enumerate(dets): shape = predictor(img, d) # shape_np stores the 68 face feature points shape_np = np.zeros((68, 2), dtype = int) for i in range(0, 68): shape_np[i] = (int(shape.part(i).x), int(shape.part(i).y)) # we saved shape_np to a text file np.savetxt(image_file_path + '.txt', shape_np, fmt = '%i') index = index + 1
[Code-2] 根据特征点获得Delaunay剖分三角
def calculateDelaunayTriangles(rect, points): # Create subp subp = cv2.Subp2D(rect); # Insert points into subp for p in points: subp.insert((p[0], p[1])); # List of triangles. Each triangle is a list of 3 points ( 6 numbers ) triangleList = subp.getTriangleList(); # Find the indices of triangles in the points array delaunayTri = [] for t in triangleList: pt = [] pt.APPend((t[0], t[1])) pt.append((t[2], t[3])) pt.append((t[4], t[5])) pt1 = (t[0], t[1]) pt2 = (t[2], t[3]) pt3 = (t[4], t[5]) if rectContains(rect, pt1) and rectContains(rect, pt2) and rectContains(rect, pt3): ind = [] for j in xrange(0, 3): for k in xrange(0, len(points)): if(abs(pt[j][0] - points[k][0]) < 1.0 and abs(pt[j][1] - points[k][1]) < 1.0): ind.append(k) if len(ind) == 3: delaunayTri.append((ind[0], ind[1], ind[2])) return delaunayTri
[Code-3] 计算仿射变换
def applyAffineTransform(src, srcTri, dstTri, size) : # Given a pair of triangles, find the affine transform. warpMat = cv2.getAffineTransform( np.float32(srcTri), np.float32(dstTri) ) # Apply the Affine Transform just found to the src image dst = cv2.warpAffine( src, warpMat, (size[0], size[1]), None, flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101 ) return dst
[Code-4] 通过仿射变换扭曲Delaunay剖分三角形
def warpTriangle(img1, img2, t1, t2) : # Find bounding rectangle for each triangle r1 = cv2.boundingRect(np.float32([t1])) r2 = cv2.boundingRect(np.float32([t2])) # offset points by left top corner of the respective rectangles t1Rect = [] t2Rect = [] t2RectInt = [] for i in xrange(0, 3): t1Rect.append(((t1[i][0] - r1[0]),(t1[i][1] - r1[1]))) t2Rect.append(((t2[i][0] - r2[0]),(t2[i][1] - r2[1]))) t2RectInt.append(((t2[i][0] - r2[0]),(t2[i][1] - r2[1]))) # Get mask by filling triangle mask = np.zeros((r2[3], r2[2], 3), dtype = np.float32) cv2.fillConvexPoly(mask, np.int32(t2RectInt), (1.0, 1.0, 1.0), 16, 0); # Apply warpImage to small rectangular patches img1Rect = img1[r1[1]:r1[1] + r1[3], r1[0]:r1[0] + r1[2]] size = (r2[2], r2[3]) img2Rect = applyAffineTransform(img1Rect, t1Rect, t2Rect, size) img2Rect = img2Rect * mask # Copy triangular region of the rectangular patch to the output image img2[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]] = img2[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]] * ( (1.0, 1.0, 1.0) - mask ) img2[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]] = img2[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]] + img2Rect
6. 用大合影构造“平均脸”
原理和代码都非常简单,不过在实际运行当中,我们需要注意:
【NOTE-1】我们用来做平均脸的单个人脸图像的尺寸很可能不一样,为了方便起见,我们将它们全部转为600*600大小。而所用原始图片,最好比这个尺寸大。
【NOTE-2】既然是要做平均脸,最好都是选用正面、端正姿态的人脸,面部表情最好也不要过于夸张。
根据这两点,我们发现:证件照非常合适用来做平均脸。
不过,一般我们很难找到那么多证件照,却比较容易获得另一类照片——合影。
特别是那种相对正规场合的合影,比如毕业照,公司年会、研讨会集体合影之类的。这类照片,大家都朝一个方向看,全部面带克制、正式的微笑,简直就是构造平均脸的理想样本啊!
我们只需要将一张大合影中每个人的头像“切”下来,生成一张单独的人脸照片,然后在按照4中的描述来叠加多张人脸不就好了吗?
可是,如果一张大合影上有几十几百,甚至上千人,难道我们手动去切图吗?
当然不用,别忘了,我们本来就可以检测人脸啊!我们只需要检测到每一张人脸所在的区域,然后再将该区域sub-image独立存储成一张照片就好了!所有过程,完全可以自动化完成!
当然所用原图最好清晰度好一点,不然切出来的照片模糊,得出结果就更模糊了。
正好笔者所在的大部门前不久年会,照了一张高清合影。笔者从中切割出1100+张面孔,构造了如下这张基于大合影的平均脸。
很年轻吧 [哈哈]
7. 用caffe区分人脸的性别
当笔者把自己部门的平均脸给同事看之后,马上有同事问:为什么只平均了男的?
回答:不是只平均了男的,是不分男女一起平均的,不过得出的结果看着像个男的而已。
又问:为什么不把男女分开平均?
是啊,一般人脸能够直接提供的信息包括:性别、年龄、种族。从大合影中提取的脸,一般年龄差距不会太大(考虑大多数合影场合),种族也相对单一,性别却大多是混合的,如果不能区分男女,合成的平均脸意义不大。
如果能自动获得一张脸的性别信息,然后将男女的照片分开,再构造平均脸显然合理的多。
于是,又在网上找了一个性别分类模型,用来给人脸照片划分性别。因为是用现成的模型,所以代码非常简单,不过需要预先安装caffe和cv2:
mean_filename='models\mean.binaryproto'gender_net_model_file = 'models\deploy_gender.prototxt'gender_net_pretrained = 'models\gender_net.caffemodel'gender_net = caffe.Classifier(gender_net_model_file, gender_net_pretrained, mean=mean, channel_swap=(2, 1, 0), raw_scale=255, image_dims=(256, 256))gender_list = ['Male', 'Female']img = io.imread(image_file_path)dets = detector(img, 1)for k, d in enumerate(dets): cropped_face = img[d.top():d.bottom(), d.left():d.right(), :] h = d.bottom() - d.top() w = d.right() - d.left() hF = int(h * 0.1) wF = int(w*0.1) cropped_face_big = img[d.top() - hF:d.bottom() + hF, d.left() - wF:d.right() + wF, :] prediction = gender_net.predict([cropped_face_big]) gender = gender_list[prediction[0].argmax()].lower() print 'predicted gender:', gender dirname = dirname + gender + "\\" copyfile(image_file_path, dirname + filename)
用这个模型先predict一遍每张人脸的性别,将不同性别的照片分别copy到male或者female目录下,然后再分别对这两个目录下的照片求平均,就可以得到男女不同的平均脸了!
NOTE:这一步的代码、运行都很简单,比较坑的是caffe的安装。
因为笔者用的是windows机器,只能下载caffe源代码自己编译安装,全过程遵照https://github.com/BVLC/caffe/tree/windows,相当繁琐。
而且由于系统设置的问题,编译后,libraries目录不是生成在caffe源码根目录下,而是位于C:\Users\build.caffe\dependencies\librariesv140x64py271.1.0 —— 这一点未必会发生在你的机器上,但是要注意编译过程中每一步的结果。
8. 训练自己的性别识别模型
想法是很好,但是,这个直接download的gender classification模型性能不太好。有很多照片的性别被分错了!
这种分错看不出什么规律,有些明明很女性化的女生头像被分成了male,很多特征鲜明的男生头像却成了female。
能够看出来的是,gender_net.caffemodel 是一个而分类模型,而且male是它的positive类,所有不被认为是male的,都被分入了female(包括一些根本就不是人脸的照片)。
笔者用自己从大合影中截取的1100+张头像做了一次测试,发现此模型的precision相对高一些——83.7%,recall低得多——54%,F1Score只有0.66。
考虑到这是一个西方人训练的模型,很可能它并不适合亚洲人的脸。笔者决定用自己同事的一千多张照片训练自己的性别分类模型!
我们用caffe训练模型,不需要写代码,只需要准备好训练数据(人脸图片),编写配置文件,并运行命令即可。命令和配置文件均在笔者github的FaceGenderClassification项目中。
为了验证新模型效果,笔者创建了几个数据集,最大的一个(下面称为testds-1)包含110+张照片,取自一张从网上搜索到的某大学毕业照中切分出的人脸;另外还有3个size在10-20不等的小数据集。
原始性别分类模型在testds-1上的Precision = 94%, Recall = 12.8% ——完全不可用啊!新训练的性别分类模型在testds-1上的Precision = 95%, Recall = 56% ——明显高于原始模型。
笔者在一台内存为7G,cpu为Intel Xeon E5-2660 0 @ 2.20GHz 2.19 GHz的机器上训练(无GPU);训练数据为1100+张平均8-9K大小的图片;每1000次迭代需要大概3个小时。
设置为每1000次迭代输出一个模型。最后一共训练了14000轮,输出了14个模型。通过在几个不同的test data set上对比,发现整体性能最好的是第10次输出,也就是10000次迭代的结果。
虽然第7次输出后的各模型差距并不大,但唯独在一个数据集上,第10次输出的模型明显由于其他输出,这个数据集就是:gitchat的达人课讲师头像——interesting ...
9. 区分性别的平均脸
虽然我们有模型来区分性别,但是如果想要“纯粹”的结果,恐怕还是得在模型分类后在人工检验并手动纠错一遍。毕竟,再好的模型,F1Score也不是1。
经过模型分类再手工分拣后,笔者把自己同事的照片分成了两个set:300+女性和800+男性。然后分别构造了平均脸。
是这个样子的:
对比一下上面那张不分性别的大平均,女生简直就被融化了——女生对大平均的贡献只是让最终的头像皮肤好了点,眼睛大了点,整个性别特征都损失掉了!
10. 外一题:平均脸=/=平均颜值
平均脸不等于平均颜值,恰恰相反,平均脸是高颜值的代表——这一点,通过构造平均脸,能够获得直观的感受。
大多数平均脸构造的结果比其中每一个个体的颜值都要高。如果有一个个体的颜值能够超过TA所参与构造的平均脸,那TA肯定属于非常好看的那种。
弗朗西斯·高尔顿爵士曾经在100多年前合成过犯人的平均脸,他当时企图通过这一方法找到“犯罪的典型面孔”。合成结果却是:原本一个个面目狰狞、恐怖甚至畸形的重罪刑事犯的脸被叠加在一起求了“平均”之后,变得好看了。
后来,这一现象被一次又一次证明。如今,长着一张“平均脸”已经可以算是高颜值的标准了。
参考资料
【1】 AverageFace 代码、模型及样例图片
【2】 FaceGenderClassification 配置文件及命令
【3】 原始的性别分类模型
【4】Delaunay三角剖分原理
【5】caffe安装指南
本文首发于GitChat,未经授权不得转载,转载需与GitChat联系。
阅读全文: http://gitbook.cn/gitchat/activity/5a1d034d81daaa5e4a3c3997
一场场看太麻烦?成为 GitChat 会员,畅享 1000+ 场 Chat !点击查看
相关阅读
import numpy as np import cv2 #打开本地摄像头,括号内表示设备编号,第一个设备为0,如果电脑有两个摄像头,第二个摄像头就是1 cap=cv
opencv 检测直线 #!coding=utf-8 import cv2 as cv import numpy as np #直线检测 img = cv.imread('./1.jpeg')
opencv 大图(当前页面)找小图(需要点击的地方),主要方法(cv.matchTemplate) import aircv as ac from PIL import ImageGrab import wi
#include<iostream> #include<opencv2/opencv.hpp> using namespace std; using namespace cv; int main() { Mat img = imrea
既然是提示缺少,那么,补上即可!要么就是OpenCV2.3编译的时候是with tbb,而tbb的文件莫名的丢了,要么就是你的系统里没有安装tbb,管他