点击视频:一分钟告诉你如何进行面部合成
这篇教程将教大家如何用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和合成图片间重复这个过程即可。
三角形变形:对于图片1中的每个三角形,用之前步骤计算出的仿射变换来将三角形内的所有像素点变形到合成图片中。对图片1中的所有三角形重复使用这个变形过程,可得到图片1的变形版。同样的,可以得到图片2的变形版。在OpenCV中,可以用warpAffine函数来实现这个变形。然而,warpAffine的输入要求为一个图像,而不是一个三角形。有个窍门是算出三角形的边界框,在边界框内用warpAffine变形所有像素点,然后遮盖掉三角形外的像素点。可以用fillConvexPoly创建三角形状的遮片。一定要确保用warpAffine的同时使用blendMode BORDER_REFLECT_101。这样会将接合处隐藏得很好——比国务卿克林顿藏她的邮箱藏得好多了!(译者注:请搜索克林顿的Email Scandal,这位美国政界的女性翘楚似乎不大满意白宫配发给她的邮箱啊~)
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.x - r.x, t.y - r.y) );
tRectInt.push_back( Point(t.x - r.x, t.y - r.y) ); // for fillConvexPoly
t1Rect.push_back( Point2f( t1.x - r1.x, t1.y - r1.y) );
t2Rect.push_back( Point2f( t2.x - r2.x, t2.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.x + alpha * points2.x;
y = ( 1 - alpha ) * points1.y + alpha * points2.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制作动态图
志愿者团队介绍
大数据文摘后台回复“志愿者”了解如何加入我们
|