手把手:使用OpenCV进行面部合成— C++ / Python
Posted 大数据文摘
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手把手:使用OpenCV进行面部合成— C++ / Python相关的知识,希望对你有一定的参考价值。
点击视频:一分钟告诉你如何进行面部合成
这篇教程将教大家如何用OpenCV做面部合成,把一张脸演变为另外一张脸。
◆ ◆ ◆
图片合成
图片合成首次在电影《Willow》(《风云际会》)中得到大量运用,这是由工业光魔(译者注:Industrial Light and Magic/ILM,电影特效制作公司)开发的一项技术。下面是电影的一个场景片段。
点击视频查看电影片段
这个图片合成背后的想法相当简单。给定两张图片I和J,通过混合而成一张中间图M。图片I和J的混合程度由参数α控制,α的值在0和1之间(0≤α≤1)。当α= 0时,图片M看似I;当α= 1时,图片M看似J。简单来讲,你可以用以下方程在每个像素点(x,y)混合图片
但是,用以上方程(假设α= 0.5)得到的国务卿希拉里•克林顿与参议员特德•克鲁兹的混合图片,是下面这个有点糟糕的结果。
这个混合图片看起来很闹心,但它似乎在向你喊着要解决方案,恳求你无论如何在混合前把眼睛和嘴巴对准。当你想把两位不同政治思想家的观点糅合,如果没有事先统一他们的思想,你会得到同样闹心的结果——这有点离题了。
所以,为了将图片I过渡合成到图片J,我们需要先在两张图片之间建立对应像素点。换句话说,对于图片I中的每一个像素点 ,我们需要在图片J中找到对应像素点 。假设我们已经神奇般地找到了所有对应点,我们可以用两个步骤将图片混合。第一步,我们需要计算合成图片中像素点 的位置。可以由以下方程算出
(1)
第二步,我们需要利用以下方程找到像素点 的像素强度
(2)
这就是合成过程,我们已经完成了。现在,让我们去给特朗普投票吧!开玩笑的!就像特朗普一样,我省略掉了一些重要的细节。想要在图片I中找到图片J中的每一个对应像素点,就如同在美国和墨西哥之间建10英尺高的墙一样难。当然这不是不可能,只是有点费力不讨好。
但是,找到部分对应点还是比较容易的。想要合成两个相异的物体,比如一张猫的脸和一张人的脸,我们可以点击两张图片中的部分像素点来建立对应关系,对于其余的像素点则采用插值法来得到最终结果。我们接下来会看到面部合成的具体步骤,这种方法可以应用于任意两个物体。
◆ ◆ ◆
面部合成:一步一步来
以下步骤可以合成两张脸。为了简化,我们假定这两张图片大小相同,但实际上这并不必要。
1.用“面部特征检测”找到对应点
让我们从获取对应点开始。首先,我们可以通过检测面部特征点自动(或手工)获得大量像素点。我用dlib库检测到68个对应点。接下来,我手工增加了4个点(1个在右侧耳朵,1个在脖子处,2个在肩膀处)。最后,我增加了图片的角点和边的中点作为对应点。毋庸置疑,在头部和颈部增加越多的点,得到的图片效果就更好;反之,去掉手工点(只剩下自动点),得到的图片效果就要差一些。
2.德洛内三角剖分算法
我们从之前的步骤得到了两个80个点的集合——每个图片有一个集合。我们可以计算出两个集合中对应点的平均值,由此获得一个新的集合。我们在这个均值点集上使用德洛内三角剖分算法。这个算法将返回一个三角形列表,该列表由80个点的数组中点的索引表示(译者注:点的“索引”在此为保存80个像素点坐标文件的行数,即每个数字代表一个点的坐标)。在这个例子中,三角剖分法根据80个点产生了149个三角形。该结果以三列数组的形式保存。以下显示的是数组的前几行。
该列表表示点38,40和37组成了一个三角形,以此类推。三角剖分法的结果显示在下面的图中。
请注意,两张图中的三角形抓取了近乎相似的区域。我们从对应点开始,由于使用了三角剖分,可以得到对应三角(或区域)。
3.图片变形和alpha混合处理
现在,我们具备智能混合两张图片的条件了。正如前文所言,混合的程度是由参数控制的。可以按以下步骤创建一张合成图片。
找到合成图片中的特征点坐标:在合成图片M中,我们可以用方程(1)找到全部80个点的坐标 。
计算仿射变换:我们有了图片1中80个点的集合,图片二中80个点的集合,以及合成图片的80个点的集合。我们也知道这些像素点所确定的三角形。选取图片1中的一个三角形和合成图片中的对应三角形,计算仿射变换,将图片1的三角形的三个顶点映射到合成图片的对应三角形的三个顶点。在OpenCV中,可以使用getAffineTransform来计算149对三角形各自的仿射变换。最后,在图片2和合成图片间重复这个过程即可。
Alpha混合变形图像:在之前的步骤里我们得到了图片1和图片2的变形版。这些图片可以用方程(2)做alpha混合,会得到最终的合成图片。在我提供的代码里,把三角形变形和alpha混合组合成简单的一步。
◆ ◆ ◆
面部合成结果
应用以上技巧合成的结果如下方所示。中间的图片是将左、右两图按50%的比例进行混合。本文的第一个视频展示了使用不同alpha数值的动画。动画可以很好地掩盖合成过程的一些瑕疵;参议员特德•克鲁兹也喜欢这样的小把戏。
大多数面部特征是对准的。但脸部外侧的部分重合得不好,因为在那些区域我们选取的对应点较少。你可以手工增加额外的像素点来修正未对准的部分,以此获得更棒的效果。
图片来源
国务卿希拉里·克林顿和参议员特德•克鲁兹的图片为公共图片。
唐纳德·特朗普的图片获得知识共享署名授权许可。
原文作者的程序链接在此:
https://github.com/spmallick/learnopencv/blob/master/FaceMorph/faceMorph.cpp
我们把主要的程序段放在这里,供大家参考
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
#include <fstream>
#include <iomanip>
#include <stdlib.h>
using namespace cv;
using namespace std;
//读取存储在文本文件内的像素点信息
vector<Point2f> readPoints(string pointsFileName)
{
vector<Point2f> points;
ifstream ifs(pointsFileName);
float x, y;
while(ifs >> x >> y)
{
points.push_back(Point2f(x,y));
}
return points;
}
//利用srcTri进行仿射变换计算,把srcTri转换成src
void applyAffineTransform(Mat &warpImage, Mat &src, vector<Point2f> &srcTri, vector<Point2f> &dstTri)
{
//对于给定的一对三角形,找出仿射变换
Mat warpMat = getAffineTransform( srcTri, dstTri );
//应用仿射变换来找到src图像
warpAffine( src, warpImage, warpMat, warpImage.size(), INTER_LINEAR, BORDER_REFLECT_101);
}
//变形和alpha混合三角区域,把img1和img2变成img
void morphTriangle(Mat &img1, Mat &img2, Mat &img, vector<Point2f> &t1, vector<Point2f> &t2, vector<Point2f> &t, double alpha)
{
//为每个三角区域找出边界
Rect r = boundingRect(t);
Rect r1 = boundingRect(t1);
Rect r2 = boundingRect(t2);
//像素点按各自边界的左上角进行偏移
vector<Point2f> t1Rect, t2Rect, tRect;
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) ); // for fillConvexPoly
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);
//Alpha混合矩形补丁
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;
}
int main( int argc, char** argv)
{
string filename1("hillary_clinton.jpg");
string filename2("ted_cruz.jpg");
//Alpha值决定合成的深度
double alpha = 0.5;
//读取输入图像
Mat img1 = imread(filename1);
Mat img2 = imread(filename2);
//把Mat转化为浮点数据类型
img1.convertTo(img1, CV_32F);
img2.convertTo(img2, CV_32F);
//清空平均图像矩阵
Mat imgMorph = Mat::zeros(img1.size(), CV_32FC3);
//读取像素点
vector<Point2f> points1 = readPoints( filename1 + ".txt");
vector<Point2f> points2 = readPoints( filename2 + ".txt");
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));
}
//读取三角形索引
ifstream ifs("tri.txt");
int x,y,z;
while(ifs >> x >> y >> z)
{
//三角形
vector<Point2f> t1, t2, t;
//image1的三个角
t1.push_back( points1[x] );
t1.push_back( points1[y] );
t1.push_back( points1[z] );
//image2的三个角
t2.push_back( points2[x] );
t2.push_back( points2[y] );
t2.push_back( points2[z] );
//合成图像的三个角
t.push_back( points[x] );
t.push_back( points[y] );
t.push_back( points[z] );
morphTriangle(img1, img2, imgMorph, t1, t2, t, alpha);
}
//显示结果
imshow("Morphed Face", imgMorph / 255.0);
waitKey(0);
return 0;
}
原文作者 SATYA MALLICK (企业家,博主,加州圣迭戈分校PH.D,TAAZ联合创始人,酷爱计算机视觉和机器学习)
大数据文摘作品,转载需授权
选文:姚佳灵
编译:田晋阳
校对:姚佳灵
往期精彩文章推荐,点击图片可阅读
福利|听课免费,还送礼品:亚马逊云计算AWS在线技术干货52小时
[手把手]教你绘制全球热门航线和客流分布图
手把手:如何用R制作动态图
志愿者团队介绍
以上是关于手把手:使用OpenCV进行面部合成— C++ / Python的主要内容,如果未能解决你的问题,请参考以下文章