手把手:使用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)得到的国务卿希拉里•克林顿与参议员特德•克鲁兹的混合图片,是下面这个有点糟糕的结果。


手把手:使用OpenCV进行面部合成— C++ / Python


这个混合图片看起来很闹心,但它似乎在向你喊着要解决方案,恳求你无论如何在混合前把眼睛和嘴巴对准。当你想把两位不同政治思想家的观点糅合,如果没有事先统一他们的思想,你会得到同样闹心的结果——这有点离题了。

 

所以,为了将图片I过渡合成到图片J,我们需要先在两张图片之间建立对应像素点。换句话说,对于图片I中的每一个像素点 ,我们需要在图片J中找到对应像素点 。假设我们已经神奇般地找到了所有对应点,我们可以用两个步骤将图片混合。第一步,我们需要计算合成图片中像素点 的位置。可以由以下方程算出


手把手:使用OpenCV进行面部合成— C++ / Python                                                                  (1) 


第二步,我们需要利用以下方程找到像素点 的像素强度

手把手:使用OpenCV进行面部合成— C++ / Python                            (2)


这就是合成过程,我们已经完成了。现在,让我们去给特朗普投票吧!开玩笑的!就像特朗普一样,我省略掉了一些重要的细节。想要在图片I中找到图片J中的每一个对应像素点,就如同在美国和墨西哥之间建10英尺高的墙一样难。当然这不是不可能,只是有点费力不讨好。


但是,找到部分对应点还是比较容易的。想要合成两个相异的物体,比如一张猫的脸和一张人的脸,我们可以点击两张图片中的部分像素点来建立对应关系,对于其余的像素点则采用插值法来得到最终结果。我们接下来会看到面部合成的具体步骤,这种方法可以应用于任意两个物体。


◆ ◆ 

面部合成:一步一步来


以下步骤可以合成两张脸。为了简化,我们假定这两张图片大小相同,但实际上这并不必要。

 

1.用“面部特征检测”找到对应点

手把手:使用OpenCV进行面部合成— C++ / Python


让我们从获取对应点开始。首先,我们可以通过检测面部特征点自动(或手工)获得大量像素点。我用dlib库检测到68个对应点。接下来,我手工增加了4个点(1个在右侧耳朵,1个在脖子处,2个在肩膀处)。最后,我增加了图片的角点和边的中点作为对应点。毋庸置疑,在头部和颈部增加越多的点,得到的图片效果就更好;反之,去掉手工点(只剩下自动点),得到的图片效果就要差一些。


2.德洛内三角剖分算法

我们从之前的步骤得到了两个80个点的集合——每个图片有一个集合。我们可以计算出两个集合中对应点的平均值,由此获得一个新的集合。我们在这个均值点集上使用德洛内三角剖分算法。这个算法将返回一个三角形列表,该列表由80个点的数组中点的索引表示(译者注:点的“索引”在此为保存80个像素点坐标文件的行数,即每个数字代表一个点的坐标)。在这个例子中,三角剖分法根据80个点产生了149个三角形。该结果以三列数组的形式保存。以下显示的是数组的前几行。

手把手:使用OpenCV进行面部合成— C++ / Python


该列表表示点38,40和37组成了一个三角形,以此类推。三角剖分法的结果显示在下面的图中。

手把手:使用OpenCV进行面部合成— C++ / Python

请注意,两张图中的三角形抓取了近乎相似的区域。我们从对应点开始,由于使用了三角剖分,可以得到对应三角(或区域)。


3.图片变形和alpha混合处理

现在,我们具备智能混合两张图片的条件了。正如前文所言,混合的程度是由参数控制的。可以按以下步骤创建一张合成图片。


  • 找到合成图片中的特征点坐标:在合成图片M中,我们可以用方程(1)找到全部80个点的坐标

  • 计算仿射变换:我们有了图片1中80个点的集合,图片二中80个点的集合,以及合成图片的80个点的集合。我们也知道这些像素点所确定的三角形。选取图片1中的一个三角形和合成图片中的对应三角形,计算仿射变换,将图片1的三角形的三个顶点映射到合成图片的对应三角形的三个顶点。在OpenCV中,可以使用getAffineTransform来计算149对三角形各自的仿射变换。最后,在图片2和合成图片间重复这个过程即可。

  • Alpha混合变形图像:在之前的步骤里我们得到了图片1和图片2的变形版。这些图片可以用方程(2)做alpha混合,会得到最终的合成图片。在我提供的代码里,把三角形变形和alpha混合组合成简单的一步。


◆ ◆ 

面部合成结果


应用以上技巧合成的结果如下方所示。中间的图片是将左、右两图按50%的比例进行混合。本文的第一个视频展示了使用不同alpha数值的动画。动画可以很好地掩盖合成过程的一些瑕疵;参议员特德•克鲁兹也喜欢这样的小把戏。

 

大多数面部特征是对准的。但脸部外侧的部分重合得不好,因为在那些区域我们选取的对应点较少。你可以手工增加额外的像素点来修正未对准的部分,以此获得更棒的效果。


手把手:使用OpenCV进行面部合成— C++ / Python

手把手:使用OpenCV进行面部合成— C++ / Python

手把手:使用OpenCV进行面部合成— C++ / Python


图片来源

国务卿希拉里·克林顿和参议员特德•克鲁兹的图片为公共图片。

唐纳德·特朗普的图片获得知识共享署名授权许可。

原文作者的程序链接在此:

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
手把手:使用OpenCV进行面部合成— C++ / Python

手把手:使用OpenCV进行面部合成— C++ / Python

手把手:使用OpenCV进行面部合成— C++ / Python

手把手:使用OpenCV进行面部合成— C++ / Python

手把手:使用OpenCV进行面部合成— C++ / Python



以上是关于手把手:使用OpenCV进行面部合成— C++ / Python的主要内容,如果未能解决你的问题,请参考以下文章

使用 OpenCV Android 进行面部特征检测

使用OpenCV,Haar级联检测器进行面部眼睛嘴部检测

如何使用具有面部特征的 openCV 训练支持向量机(svm)分类器?

带有面部检测和形状预测的 Dlib 网络摄像头捕获速度很慢

如何使用python(Opencv)识别头部(面部)运动

OpenCV-Python实战(15)——面部特征点检测详解(仅需5行代码学会3种面部特征点检测方法)