基于opencv+Dlib的面部合成(Face Morph)
引自:http://blog.csdn.net/wangxing233/article/details/51549880
零、前言
前段时间看到文章【1】和【2】,大概了解了面部合成的基本原理。这两天空下来了,于是参考【3】自己实现了下。虽然【1】和【2】已经讲的很清楚了,但是有一些细节没有提到。所以我在这里记录一下实现的过程中以及一些小细节。
一、什么是面部合成?
这里的面部合成指的的是把一张脸逐渐的变化成另外一张脸。图1展示了从詹姆斯渐变到科比的过程。其实如果把这些图片合成视频的话效果会更好。但是我不知道在这里怎么添加视频,所以就没弄了。
图 1. 勒布朗詹姆斯到科比的渐变。第一排第一张为詹姆斯原图,第二排最后为科比原图。从第一排到第二排为渐变过程。
二 、主要步骤
面部合成的原理就是利用给定的两张图片 和 生成 张从渐变到的过度图片 。这过度图片生成的原理是一样的,通过一个参数来控制混合的程度。
(1)
当接近0时,看起来比较像 ,当接近1时,看起来比较像。当然啦,这个公式只是一个大概的意思。具体来说一共分为如下几部:1. 检测人脸关键点。2. 三角剖分。3. 图像变形。下面就从这3点展开来说。
1. 人脸关键点定位
给定两张图片,每张图片里面有一个人脸。我们要做的第一步就是分别从这两张图片中检测出人脸,并在定位出人脸关键点。人脸一共有68个关键点,分布如图2所示。不过我的研究方向不是搞人脸的,所以这个是做这个项目的时候才去了解的。如果有什么偏差,还望指正。
图 2. 人脸关键点分布说明图。
人脸检测和关键点定位可以使用Dlib[4]这个库来完成。Dlib是一个开源的使用现代C++技术编写的跨平台的通用库。它包含很多的模块,例如算法,线性代数,贝叶斯网络,机器学习,图像处理等等。其中图像处理模块就有人脸检测和关键点定位的函数。关于人脸检测和关键点定位的具体原理在这里我就不讨论了(不了解。。。),下面说下具体怎么调用。
首先我们需要检测出图像中人脸的位置,所以需要一个人脸检测器。这只要直接定义一个Dilb中frontal_face_detector类的对象就可以了。
frontal_face_detector detector = get_frontal_face_detector();
- 1
- 2
有了这个检测器之后我们就可以检测人脸了。由于一张图片中可能有多个人脸,所以这里检测的结果是保存在一个vector容器里面的。vector里面的对象类型是rectangle,这个数据类型描述了人脸在图片中的位置。具体来说,这一步人脸检测的结果只是一个人脸边界框(face bounding box),人脸在被包含在方框中(图3)。而rectangle里面保存了这个方框的左上和右下点的坐标。
array2d<rgb_pixel> img;
load_image(img, "yxy.png");
std::vector<rectangle> dets = detector(img);
- 1
- 2
- 3
图 3. 人脸检测示意图。
检测出人脸之后,我们接下来要在人脸中定位关键点。首先我们需要一个关键点检测器(shape_predictor)。首先定义一个Dlib中的shape_predictor类的对象,然后用shape_predictor_68_face_landmarks.dat这个模型来初始化这个检测器。shape_predictor_68_face_landmarks.dat模型可以从 Dlib官网 中下载下来,然后放入你的工程文件里面。
shape_predictor sp;
deserialize("shape_predictor_68_face_landmarks.dat") >> sp;
- 1
- 2
有了这个关键点检测器之后,我们就可以检测人脸关键点了。这个检测器的输入是一副图片和一个人脸边界框。输出是一个shape对象。这个shape对象里面保存了检测到的68个人脸关键点的坐标。可以通过下面的方式把这些关键点的坐标保存到一个txt文件中。
full_object_detection shape = sp(img, dets[j]);
ofstream out("yxy.txt");
for (int i = 0; i < shape.num_parts(); ++i)
{
auto a= shape.part(i);
out<<a.x()<<" "<<a.y()<<" ";
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
图 4. 检测出的人脸关键点
最后检测出的关键点图4所示。注意图4中每幅图片我都手工加了8个点。分别是图片四个顶点和四条边的中点。加这些点是为了下一步能有更好的效果。
2. 三角剖分
检测出了两幅图片中人脸的关键点之后,我们先求中间图片 关键点的坐标。这是通过公式1来计算的。具体来说就是我们要在中间图片 定位72个关键点的坐标。每一个关键点的坐标是通过给定的两幅图片 和 中对应的关键点坐标加权得到的。
std::vector<Point2f> points1 = readPoints("lbj.txt"); //詹姆斯关键点
std::vector<Point2f> points2 = readPoints("kb.txt"); //科比关键点
std::vector<Point2f> points; //中间图片关键点
for (int i = 0; i < points1.size(); i++)
{
float x, y;
x = (1 - alpha) * points1[i].x + alpha * points2[i].x;
y = (1 - alpha) * points1[i].y + alpha * points2[i].y;
points.push_back(Point2f(x, y));
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
求出了中间图片的关键点坐标之后,我们对这些点进行三角剖分。关于三角剖分的具体解释可以参考【5】。简单来说就是返回一堆三角形。每个三角形的顶点都是由那些关键点组成的。这样整个平面就被剖分成了很多小的三角形。我们可以针对每一个小三角形进行操作。在opencv中,三角剖分的类为Subdiv2D。在定义这个类的对象之前,我们需要先定义一个Rect类的对象。这里的Rect与前面提到的Dlib中的rectangle类似。都是表示图像中的一个方框区域。只不过这里的Rect里面保存的是方框的左上角坐标以及方框的长和宽。所以这里我们定义一个与输入图像同样大小的Rect对象,然后用这个对象去初始化一个Subdiv2D对象subdiv。然后我们把中间图像的关键点加入到subdiv中。最后我们会得到一些六元组,每个六元组包括一个三角形的三个顶点的坐标(x,y)。
Size size = img1.size();
Rect rect(0, 0, size.width, size.height);
Subdiv2D subdiv(rect);
for (vector<Point2f>::iterator it = points.begin(); it != points.end(); it++)
subdiv.insert(*it);
std::vector<Vec6f> triangleList;
subdiv.getTriangleList(triangleList);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
如图5(3)所示,我们通过三角剖分把中间图片分成了很多的小三角形。我们还需要把图片 和 也剖分成跟中间图片一样的三角形。也就是说中哪三个点构成一个三角形,那么 和 中对应的那三个点也构成一个三角形。这就需要构成一个三角形的三个顶点的索引。然而我们只有那三个顶点的坐标。所以我们需要把那些六元组里面的关键点坐标转换成关键点的索引。下面这段代码是【1】的作者提供的一种方法。这个方法的做法是把六元组中的三个点的坐标分别与所有的关键点坐标进行匹配。当两个点之间的距离小于1时认为是同一个点。当然这是一个比较笨的方法,其实opencv如果在Subdiv2D里面的数据结构里加一个点的索引项的话那就非常方便了。(不知道是不是本来就有的,只是我没找到。。。如果是这样的话求告知。。)。最后我们会得到类似图5(1)中的三元组。每一个三元组对应这一个三角形的顶点索引。比如说第一行[38 40 37]表示第一个三角形是由第38,40,37个关键点构成的。有了这些索引后,我们就可以把 和 也进行相应的三角剖分,如图5 (2)(4)所示。这样这三张图片中的三角形是一一对应的。
for (size_t i = 0; i < triangleList.size(); ++i)
{
Vec6f t = triangleList[i];
pt[0] = Point2f(t[0], t[1]);
pt[1] = Point2f(t[2], t[3]);
pt[2] = Point2f(t[4], t[5]);
if (rect.contains(pt[0]) && rect.contains(pt[1]) && rect.contains(pt[2]))
{
int count = 0;
for (int j = 0; j < 3; ++j)
for (size_t k = 0; k < points.size(); k++)
if (abs(pt[j].x - points[k].x) < 1.0 && abs(pt[j].y - points[k].y) < 1.0)
{
ind[j] = k;
count++;
}
if (count == 3)
delaunayTri.push_back(ind);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
图 5. 三角剖分结果
3. 图像变形
把输入的两幅图像以及要求的中间图像都三角剖分之后。我们接下来要做的是把中间图像上的小三角形一个一个的填满,然后得到最终的图像(图 6)。
接下来我们描述中间图像上的一个小三角形求得的过程。我们选定中间图像上的一个三角形,然后选定上对应的三角形。求出上的三角形中的像素到上的三角形中的像素的仿射变换。仿射变换满足下面的公式。其中左边为上三角形中的像素点的齐次坐标,右边为上三角形中的像素点的齐次坐标。中间为仿射变换矩阵。
(2)
求出仿射变换的参数后,我们把上三角形中的每一个像素点按照这个公式投影到上去,这样就得到了中选定的三角形区域的像素值。
以上这个求仿射变换和进行像素投影这两个步骤可以直接调用opencv中的函数applyAffineTransform来完成。但是applyAffineTransform的输入要求是一个方形区域而不是三角形区域。所以我们先boundingRect这个函数算出三角形的边界框(bounding box),对边界框内所有像素点进行仿射投影。同时用fillConvexPoly函数生成一个三角形的mask。也就是说生成一张三角形边界框大小的图片,这个图片中三角形区域像素值是1,其余区域像素值是0。投影完成后在用这个mask与投影结果进行逻辑与运算,从而获得三角形区域投影后的像素值。
以上只是求了中三角形到中选定三角形的投影。相应的,我们还要求图像中对应的三角形到中选定三角形的投影。方法和前面的一样。这样我们就得到了中选定三角形的两个变形图片。然后我们对这两个图片进行加权求的最终这个三角形的像素值。做法和公式(1)类似。具体代码如下:
void morphTriangle(Mat &img1, Mat &img2, Mat &img, std::vector<Point2f> &t1, std::vector<Point2f> &t2, std::vector<Point2f> &t, double alpha)
{
Rect r = boundingRect(t);
Rect r1 = boundingRect(t1);
Rect r2 = boundingRect(t2);
std::vector<Point2f> t1Rect, t2Rect, tRect;
std::vector<Point> tRectInt;
for (int i = 0; i < 3; ++i)
{
tRect.push_back(Point2f(t[i].x - r.x, t[i].y - r.y));
tRectInt.push_back(Point(t[i].x - r.x, t[i].y - r.y));
t1Rect.push_back(Point2f(t1[i].x - r1.x, t1[i].y - r1.y));
t2Rect.push_back(Point2f(t2[i].x - r2.x, t2[i].y - r2.y));
}
Mat mask = Mat::zeros(r.height, r.width, CV_32FC3);
fillConvexPoly(mask, tRectInt, Scalar(1.0, 1.0, 1.0), 16, 0);
Mat img1Rect, img2Rect;
img1(r1).copyTo(img1Rect);
img2(r2).copyTo(img2Rect);
Mat warpImage1 = Mat::zeros(r.height, r.width, img1Rect.type());
Mat warpImage2 = Mat::zeros(r.height, r.width, img2Rect.type());
applyAffineTransform(warpImage1, img1Rect, t1Rect, tRect);
applyAffineTransform(warpImage2, img2Rect, t2Rect, tRect);
Mat imgRect = (1.0 - alpha)*warpImage1 + alpha*warpImage2;
multiply(imgRect, mask, imgRect);
multiply(img(r), Scalar(1.0, 1.0, 1.0) - mask, img(r));
img(r) = img(r) + imgRect;
}
- 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
就这样一个三角形一个三角形的变换,我们就得到了一张完整的中间图像。然后通过变化的值(从0到1),从而得到一系列渐变的中间图像。最后将这些渐变图像写入到一个视频文件中就大功告成了!!
vector<Mat> pic;
pic.push_back(imread("lbj.png"));
string filename = "lbjkb";
for (double alpha = 0.1; alpha < 1; alpha = alpha + 0.1)
{
string framename = filename + to_string(alpha) + ".png";
pic.push_back(imread(framename));
}
pic.push_back(imread("kb.png"));
VideoWriter output_src("lbjkb.avi", CV_FOURCC('M', 'J', 'P', 'G'), 5, pic[0].size(), 1);
for (auto c : pic)
{
output_src<<c;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
图 6. 图像变形示意图
三、说明
- 以上所用到的部分图像来自网络,如有版权问题请联系我,谢谢。
- 这个项目的代码可以从【1】中下载。不过它里面默认关键点和索引对都是已知的。我自己的完整版代码见github:iamwx/FaceMorph
参考资料: 1. 【SATYA MALLICK】Face Morph Using OpenCV — C++ / Python 2. 【大数据文摘】手把手:使用OpenCV进行面部合成— C++ / Python 3. opencv github主页 4. Dlib 库主页 5. 【百度百科】Delaunay三角剖分算法
基于opencv+Dlib的面部合成(Face Morph)的更多相关文章
- 基于OpenCV的面部交换
需要装python库 OpenCV dlib docopt(根据打开方式选择是否装) # -*- coding: UTF-8 #本电脑试运行 命令 python F:\python_project\s ...
- 基于深度学习的人脸识别系统(Caffe+OpenCV+Dlib)【三】VGG网络进行特征提取
前言 基于深度学习的人脸识别系统,一共用到了5个开源库:OpenCV(计算机视觉库).Caffe(深度学习库).Dlib(机器学习库).libfacedetection(人脸检测库).cudnn(gp ...
- 基于深度学习的人脸识别系统系列(Caffe+OpenCV+Dlib)——【四】使用CUBLAS加速计算人脸向量的余弦距离
前言 基于深度学习的人脸识别系统,一共用到了5个开源库:OpenCV(计算机视觉库).Caffe(深度学习库).Dlib(机器学习库).libfacedetection(人脸检测库).cudnn(gp ...
- [转载]卡尔曼滤波器及其基于opencv的实现
卡尔曼滤波器及其基于opencv的实现 源地址:http://hi.baidu.com/superkiki1989/item/029f65013a128cd91ff0461b 这个是维基百科中的链接, ...
- 基于Opencv和Mfc的图像处理增强库GOCVHelper(索引)
GOCVHelper(GreenOpen Computer Version Helper )是我在这几年编写图像处理程序的过程中积累下来的函数库.主要是对Opencv的适当扩展和在实现Mfc程序时候的 ...
- 基于OpenCv的人脸检测、识别系统学习制作笔记之一
基于OpenCv从视频文件到摄像头的人脸检测 在OpenCv中读取视频文件和读取摄像头的的视频流然后在放在一个窗口中显示结果其实是类似的一个实现过程. 先创建一个指向CvCapture结构的指针 Cv ...
- 基于opencv网络摄像头在ubuntu下的视频获取
基于opencv网络摄像头在ubuntu下的视频获取 1 工具 原料 平台 :UBUNTU12.04 安装库 Opencv-2.3 2 安装编译运行步骤 安装编译opencv-2.3 参 ...
- 基于opencv的小波变换
基于opencv的小波变换 提供函数DWT()和IDWT(),前者完成任意层次的小波变换,后者完成任意层次的小波逆变换.输入图像要求必须是单通道浮点图像,对图像大小也有要求(1层变换:w,h必须是2的 ...
- 基于opencv在摄像头ubuntu根据视频获取
基于opencv在摄像头ubuntu根据视频获取 1 工具 原料 平台 :UBUNTU12.04 安装库 Opencv-2.3 2 安装编译执行步骤 安装编译opencv-2.3 參考h ...
随机推荐
- Exception的妙用
实际工作中遇到的一个例子: 一.看这样一个方法: /** 传入以微秒(us)为单位的时间字符串,转换成可读的(年-月-日 时:分:秒)日期格式*/ public String getDateStrin ...
- 【转载】Hibernate之hbm.xml集合映射的使用(Set集合映射,list集合映射,Map集合映射)
https://www.cnblogs.com/biehongli/p/6555994.html
- Python 爬虫 使用正则去掉不想要的网页元素
在做爬虫的时候,我们总是不想去看到网页的注释,或者是网页的一些其他元素,有没有好的办法去掉他们呢? 例如:下面的问题 第一种情况<ahref="http://artso.artron. ...
- 内心的平静就是财富本身-Cell组件-用友华表的由来-T君
时至今日,Cell组件仍是应用广泛的商业报表组件 作者:人生三毒 编者注:本文作者人生三毒为知名网站及网页游戏公司创始人,此前曾为IT类媒体资深编辑,见证了中国互联网早期的发展. 认识T君之前先认识的 ...
- [Golang] 从零开始写Socket Server(1): Socket-Client框架
版权声明:本文为博主原创文章,未经博主允许不得转载. 第一次跑到互联网公司实习 ..感觉自己进步飞快啊~第一周刚写了个HTTP服务器用于微信公共号的点餐系统~ 第二周就直接开始一边自学Go语言一边写用 ...
- Kinect v2 记录
最多可同时识别跟踪 6 人,每人可识别到 25 个关节数据.可以根据上身 10 个关节数据来判断坐姿状态. 物理极限识别范围:0.5m – 4.5m,最佳识别范围:0.8m – 3.5m. 深度数据可 ...
- session 详解
一.Session简单介绍 在WEB开发中,服务器可以为每个用户浏览器创建一个会话对象(session对象),注意:一个浏览器独占一个session对象(默认情况下).因此,在需要保存用户数据时,服务 ...
- tiny-cnn开源库的使用(MNIST)
tiny-cnn是一个基于CNN的开源库,它的License是BSD 3-Clause.作者也一直在维护更新,对进一步掌握CNN非常有帮助,因此以下介绍下tiny-cnn在windows7 64bit ...
- Android使用Custom debug keystore
有时候须要用到第三方API的时候,须要一个key store 的SH1值,比如使用百度地图API,假设是协同开发,就须要全部Eclipse使用同一keystore. 例如以下图所看到的: 这里须要注意 ...
- javascript基础拾遗(六)
1.Date内置对象 获取系统时间 var now = new Date() console.log(now) console.log(now.getDate()) console.log(now.g ...