基于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

 

sanyuanzulbjdelaunylbjkbdelaunykbdelauny

 

 

图 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. 以上所用到的部分图像来自网络,如有版权问题请联系我,谢谢。
  2. 这个项目的代码可以从【1】中下载。不过它里面默认关键点和索引对都是已知的。我自己的完整版代码见github:iamwx/FaceMorph

参考资料: 1.  【SATYA MALLICK】Face Morph Using OpenCV — C++ / Python 2. 【大数据文摘】手把手:使用OpenCV进行面部合成— C++ / Python 3. opencv github主页 4. Dlib 库主页 5. 【百度百科】Delaunay三角剖分算法

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/541312.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大脑应对危机的模式_危机的完整形式是什么?

大脑应对危机的模式危机&#xff1a;印度信用评级信息服务有限公司 (CRISIL: Credit Rating Information Services of India Limited) CRISIL is an abbreviation of Credit Rating Information Services of India Limited. It is an international analytic company which off…

linux网络延迟命令,2. Linux使用ping命令查看网络延迟

ping命令持续发送少量互联网流量到远程地址并报告收到回应的总时间。如果流量因为网络故障或者错误配置而被丢弃&#xff0c;它也会报告。ping命令是最基本和初级的诊断网络问题的工具之一。ping常被用来测试网络延迟&#xff0c;但是有时ping的延迟并不是网络引起的&#xff0…

linux查看磁盘io带宽,[Linux] 磁盘IO性能查看和优化以及iostat命令

iostat命令:%user&#xff1a;CPU处在用户模式下的时间百分比。%nice&#xff1a;CPU处在带NICE值的用户模式下的时间百分比。%system&#xff1a;CPU处在系统模式下的时间百分比。%iowait&#xff1a;CPU等待输入输出完成时间的百分比。%steal&#xff1a;管理程序维护另一个虚…

Jsoup 数据修改

2019独角兽企业重金招聘Python工程师标准>>> 1 设置属性的值 在解析一个Document之后可能想修改其中的某些属性值&#xff0c;然后再保存到磁盘或都输出到前台页面。 可以使用属性设置方法 Element.attr(String key, String value), 和 Elements.attr(String key, S…

软件静态架构 软件组件图_组件图| 软件工程

软件静态架构 软件组件图什么是组件图&#xff1f; (What is Component Diagram?) A Component Diagram breaks down the real system under development into different heights of working. Every component is reactive for the main aim in the entire system and only re…

如何卸载非linux系统分区,如何卸载Linux系统分区?卸载Linux系统分区的方法-站长资讯中心...

系统为windows xp sp2和redhat as 5双系统&#xff0c;其中linux系统后安装的在D盘&#xff0c;华彩软件站www.huacolor.com小编今天发现硬盘不够用了&#xff0c;想干掉linux分区&#xff0c;在虚拟机中用linux。就在windows的磁盘管理(命令为:diskmgmt)下删除linux分区&#…

顺序结构复习

复习一些易错知识点还有习题 目录 可能不熟悉的知识点 逻辑表达式的求解 if,else的配队 条件运算符 运算符优先级的问题 switch的使用 goto和if构成的循环 例题讲解 1 2 3 4 ​编辑 5 ​编辑 6赋值 ​编辑 7 可能不熟悉的知识点 逻辑表达式的求解 如果…

模板模式(部分方法延迟到子类实现)

项目中&#xff0c;用到了抽象类作为父类&#xff0c;有部分实现。 提供了了模板方法作为子类公共方法&#xff0c;模板方法中调用了抽象类的抽象方法和部分非抽象方法。 执行代码时&#xff0c;发现模板方法调用了抽象类的抽象方法&#xff0c;当时比较好奇&#xff0c;后来发…

【转载】浏览器缓存详解:expires cache-control last-modified

下面的内容展示了一个常见的 Response Headers&#xff0c;这些 Headers 要求客户端最多缓存 3600 秒&#xff0c;也给出了一个 pub1259380237;gz 的校验值。 HTTP/1.x 200 OK Transfer-Encoding: chunked Date: Sat, 28 Nov 2009 04:36:25 GMT Server: LiteSpeed Connection: …

linux下php的安装,Linux下PHP安装

1 下载php源码安装包 个人是php-5.6.30.tar.gzphp2 解压文件mysqltar -zxvf php-5.6.30.tar.gznginx3 编译安装sqlcd php-5.6.30api建立www用户和www用户组curlgroupadd wwwsocketuseradd -g www wwwphp-fpm在编译以前先把依赖包都装上urlyum install curl curl-develyum inst…

微软宣布以 262 亿美元现金收购 LinkedIn

北京时间6月13日20:45&#xff08;美国当地时间6月13日上午8:45&#xff09;&#xff0c;微软(Nasdaq: MSFT)和LinkedIn(领英&#xff0c;NYSE: LNKD)对外宣布&#xff0c;双方达成协议&#xff0c;微软宣布将以每股196美元、总价262亿美元的价格收购LinkedIn&#xff0c;由现金…

本月初 本月末 java_本月内容作家(2018年8月)

本月初 本月末 javaWe are feeling glad to announce that Prerana Jain is the Content Writer Of The Month (August 2018) on the basis of her excellent contribution and her support to us. 我们很高兴地宣布&#xff0c; Prera​​na Jain凭借其出色的贡献和对我们的支…

c语言案例朗读工具源码,C语言编写简单朗读小工具(有源码)

原标题&#xff1a;C语言编写简单朗读小工具(有源码)最近不少人在后台留言说学C都是面对枯燥的控制台程序&#xff0c;能不能体现一下C语言的实际用途&#xff0c;今天我们就理论结合实践一把&#xff1a;C语言结合VBS脚本编写一个简单的朗读小工具&#xff0c;做一个能够发音的…

WAS集群系列(5):集群搭建:步骤3:安装IHS软件

选择“安装IBM HTTPServer”选项&#xff0c;点击“安装向导”。例如以下图提示&#xff1a; 安装提示&#xff0c;逐步点击“下一步”&#xff0c;当中偶有几处细节注意就可以。列举例如以下&#xff1a; &#xff08;1&#xff09;、产品安装路径与先前WAS软件所安装路径统一…

WordPress数据表wp-options数据字段存JSON数据

2019独角兽企业重金招聘Python工程师标准>>> wp_options表是WordPress中最重要的表&#xff0c;一切程序设置、主题设置和绝大多数插件的设置大都保存在此表。 WordPress里面数据表wp-options数据字段存JSON数据 &#xff1a; a:90: {s:11:"^wp-json/?$"…

css scroll属性_CSS中的scroll-behavior属性

css scroll属性CSS | 滚动行为属性 (CSS | scroll-behavior property) Who does not want their links to function smoothly and attractively? This type of functionality is very easy to implement. All you need is a bit of awareness about the property that would h…

基于HTML5 Canvas 实现弹出框

用户鼠标移入时&#xff0c;有弹出框出现&#xff0c;这样的需求很常见。这在处理HTML元素实现时简单&#xff0c;但是如果是对 HTML5 Canvas 构成的图形进行处理&#xff0c;这种方法不再适用&#xff0c;因为 Canvas 使用的是另外一套机制&#xff0c;无论在 Canvas 上绘制多…

JavaScript | 嵌套if的示例

Example: 例&#xff1a; In this example, we are reading salary of an employee and finding the discount and net pay based on given salary and discount rate. 在此示例中&#xff0c;我们正在读取员工的薪水&#xff0c;并根据给定的薪水和折扣率找到折扣和净工资。 …

node.js 中间件_Node.js中的Passport中间件(模块)

node.js 中间件Hi! Welcome to Node.js Authentication Series, where well study and program the passport module or middleware. 嗨&#xff01; 欢迎使用Node.js身份验证系列 &#xff0c;我们将在其中研究和编程通行证模块或中间件 。 Nowadays, an important tool in m…

android开发自动提示框,Android 多种简单的弹出框样式设置代码

简介这是一个基于AlertDialog和Dialog这两个类封装的多种弹出框样式&#xff0c;其中提供各种简单样式的弹出框使用说明。同时也可自定义弹出框。项目地址&#xff1a;http://www.easck.com/jjdxmashl/jjdxm_dialogui特性1.使用链式开发代码简洁明了2.所有的弹出框样式都在Dial…