必威体育Betway必威体育官网
当前位置:首页 > IT技术

利用 OpenCV 和 Caffe,根据大合影构造“平均脸”

时间:2019-08-01 04:11:07来源:IT技术作者:seo实验室小编阅读:72次「手机版」
 

平均脸

公司年会,大部门一起照了大合影。忽然有兴趣看看大家的平均脸什么样子的,于是用 OpenCV 从大合影中提取出一千多名程序员的脸,构造了所有人的平均脸。

拿给同事看,大家又要求看分性别平均的平均脸。于是又下载了 Caffe 的 gender classification model,将样本做了一下性别分类,之后分别构造了双方的平均脸。得出结果:大平均的颜值原来是被男生拉低的[哈哈]

本文就讲述根据照片计算平均脸的原理,具体代码,配发实现代码和分类模型

本场 Chat 只有文章,没有交流。

0. 有趣的平均脸

1878年,英国的弗朗西斯·高尔顿爵士(Sir Francis Galton)发明了一种将许多人的照片合成为一张照片,从而创造出一个“平均”面容的技术

enter image description here

弗朗西斯·高尔顿爵士,英国维多利亚时代的博学家、人类学家、优生学家、热带探险家、地理学家、发明家、气象学家、统计学家、心理学家和遗传学家;也是《物种起源》作者查尔斯·达尔文的表弟。*

当时具体的合成方法是照片叠加——给多个人,比如20个人,照相,将每个人照片所需的曝光时间缩短为1/20,通过20次曝光得到一张“平均”照片。

enter image description here

弗朗西斯·高尔顿最初合成平均脸的目的是将不同“种类”的人(例如:囚犯、精神病患者等等)视觉化,以期得到这类人的“原型”(共同特征),但结果却意外的发现,这样合成的人脸却比用于合成大部分(甚至是全部)都要好看!

虽然高尔顿爵士的初衷没有达到,合成平均脸的方法却保留了下来。

随着技术的发展,照片不再需要物理底片,合成也不再需要复杂的曝光冲印技术,通过一些简单的操作就能做到,人人都可以上手。

大家想必看到过一些按国家、民族合成的平均脸吧:

enter image description here

想不想自己动手制作一张周围人的平均脸?

一点都不复杂,只要知道了原理,会写几行简单的代码,就能顺利完成。

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自然也就成了它们的“平均脸”。

看起来好容易哦,那我们赶紧找两张照片来试试吧!就用这两张:

enter image description here

这两张照片alpha=0.5后直接叠加的结果是这样的:

enter image description here

这也不是人脸呀!先别急,看看为什么会这样?

从这张“重影图”上不难看出来,之所以这样,是因为最基本的五官都没有对齐。如果我们事先把两个人的眼睛和嘴对齐,效果就不会是这样的了。

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个面部基准点(如下图)。

enter image description here

Step2. Delaunay 三角剖分

在获得了68个面部基准点之后,我们结合人脸所在的矩形的四个顶点和每条边的中心点,将人脸所在的矩形分割成如下图所示的三角形的组合。

enter image description here

这一方法又称为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进行叠加了。最后得到叠加结果。

enter image description here

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位美国总统的平均脸:

enter image description here

他们平均之后的样子是这样的:

enter image description here

我们用同样的方法来看看gitchat 达人课讲师的平均脸!

enter image description here

这是平均后的结果:

enter image description here

5. opencv + dlib 轻松搞定平均脸

NOTE:所有代码都可以从笔者github的averageFace项目中获取。

之前是从描述角度来讲解平均脸原理。现在,我们来看看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+张面孔,构造了如下这张基于大合影的平均脸。

enter image description here

很年轻吧 [哈哈]

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+男性。然后分别构造了平均脸。

是这个样子的:

enter image description here

对比一下上面那张不分性别的大平均,女生简直就被融化了——女生对大平均的贡献只是让最终的头像皮肤好了点,眼睛大了点,整个性别特征都损失掉了!

10. 外一题:平均脸=/=平均颜值

平均脸不等于平均颜值,恰恰相反,平均脸是高颜值的代表——这一点,通过构造平均脸,能够获得直观的感受。

大多数平均脸构造的结果比其中每一个个体的颜值都要高。如果有一个个体的颜值能够超过TA所参与构造的平均脸,那TA肯定属于非常好看的那种。

弗朗西斯·高尔顿爵士曾经在100多年前合成过犯人的平均脸,他当时企图通过这一方法找到“犯罪的典型面孔”。合成结果却是:原本一个个面目狰狞、恐怖甚至畸形的重罪刑事犯的脸被叠加在一起求了“平均”之后,变得好看了。

后来,这一现象被一次又一次证明。如今,长着一张“平均脸”已经可以算是高颜值的标准了。

参考资料

【1】 AverageFace 代码、模型及样例图片

【2】 FaceGenderClassification 配置文件及命令

【3】 原始的性别分类模型

【4】Delaunay三角剖分原理

【5】caffe安装指南


本文首发于GitChat,未经授权不得转载,转载需与GitChat联系。

阅读全文: http://gitbook.cn/gitchat/activity/5a1d034d81daaa5e4a3c3997

一场场看太麻烦?成为 GitChat 会员,畅享 1000+ 场 Chat !点击查看

相关阅读

用opencv打开电脑摄像头

import numpy as np import cv2 #打开本地摄像头,括号内表示设备编号,第一个设备为0,如果电脑有两个摄像头,第二个摄像头就是1 cap=cv

opencv之简单霍夫变换

opencv 检测直线 #!coding=utf-8 import cv2 as cv import numpy as np #直线检测 img = cv.imread('./1.jpeg')

opencv 大图中找小图,并点击小图

opencv 大图(当前页面)找小图(需要点击的地方),主要方法(cv.matchTemplate) import aircv as ac from PIL import ImageGrab import wi

Opencv的moveWindow函数

#include<iostream> #include<opencv2/opencv.hpp> using namespace std; using namespace cv; int main() { Mat img = imrea

OpenCV缺少tbb_debug.dll的解决方法

既然是提示缺少,那么,补上即可!要么就是OpenCV2.3编译的时候是with tbb,而tbb的文件莫名的丢了,要么就是你的系统里没有安装tbb,管他

分享到:

栏目导航

推荐阅读

热门阅读