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

一、简单工厂模式

# public class Operation //基类{private double _numberA 0;private double _numberB 0;public double NumberA{get{ return _numberA; }set{_numberA value;}}public double NumberB{get{ return _numberB; }set{_numberB value;}}public virtual double GetResult(){d…

软件生命周期模型及其类型

A life cycle model is also known as a process model. As the name suggests, the software life cycle model (or the software process model) gives us a pictorial representation of the entire software development process. 生命周期模型也称为过程模型 。 顾名思义&…

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;后来发…

ruby 集合 分组_在Ruby中找到两个集合之间的区别

ruby 集合 分组Finding differences simply means that finding elements that are uncommon between two sets as well as are only present in the first set. We can find this, with the help of a – operator. You can also consider the objective as to find the uniqu…

怎样在linux卸载java,卸载linux自带java,linux自带java

卸载linux自带java&#xff0c;linux自带java第一步&#xff1a;rpm查询java安装包名称[rootlocalhost java]# rpm -qa | grep javajava-1.7.0-openjdk-headless-1.7.0.51-2.4.5.5.el7.x86_64tzdata-java-2014b-1.el7.noarchpython-javapackages-3.4.1-5.el7.noarchjava-1.7.0-…

Swift iOS : 内存管理

Swift是自动管理内存的。这意味着&#xff0c;你不需要主动释放内存。 比如Foo内包含的Bar&#xff0c;可以随同Foo一起被释放&#xff1a; import UIKit UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate {var window : UIWindow?func application(…

python实现接口_Python | 使用类实现接口

python实现接口In this program, we are implementing the concept of Interface using class. Here, Class Shape worked as Interface. In Interface all methods must be non-implemented it must be implemented in child class unlike abstract class, where we can have …

linux lp 打印中文,Linux基础命令---lp打印文件

lplp指令用来打印文件&#xff0c;也可以修改存在的打印任务。使用该指令可以指定打印的页码、副本等。此命令的适用范围&#xff1a;RedHat、RHEL、Ubuntu、CentOS、Fedora、openSUSE、SUSE。1、语法lp [ -E ] [ -U username ] [ -c ] [ -d destination[/instance] ] [ -h…

【转载】浏览器缓存详解: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: …

ctype函数_PHP ctype_xdigit()函数与示例

ctype函数PHP ctype_xdigit()函数 (PHP ctype_xdigit() function) ctype_xdigit() function is a character type (CType) function in PHP, it is used to check whether a given string contains hexadecimal digits or not. ctype_xdigit()函数是PHP中的字符类型(CType)函数…

linux ldd运行不成功,Linux_Linux:Ldd命令介绍及使用方法,1、首先ldd不是一个可执行程序 - phpStudy...

Linux&#xff1a;Ldd命令介绍及使用方法1、首先ldd不是一个可执行程序&#xff0c;而只是一个shell脚本2、ldd能够显示可执行模块的dependency&#xff0c;其原理是通过设置一系列的环境变量&#xff0c;如下&#xff1a;LD_TRACE_LOADED_OBJECTS、LD_WARN、LD_BIND_NOW、LD_L…

开发原生的 Google 眼镜应用 【已翻译100%】(2/2)

使用传感器 Glass没有键盘或触摸屏&#xff0c;但仍然具有移动设备所有的标准的传感器。你可以使用标准的传感器组件来访问这些传感器。 定位和GPS Glass内置有GPS。TLocationSensor具有一个OnLocationChanged事件&#xff0c;这一事件在GPS组件被激活时产生&#xff0c;在有除…

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…