天天看點

基于opencv和Dlib的人臉交換(face swap)

作者:hahahaha233

一 什麼是人臉交換。

如下圖所示,将右邊湯唯的臉換成左邊鹿晗的臉,就變成啦中間的照片。這就是人臉交換。這個效果通過PS也可以實作,不過這裡是完全自動的實作,還是很贊的。這篇文章主要參考[1],作者給出在文章中給出啦代碼,不過和上篇人臉合成的一樣,給出的代碼不是完整的。我這裡給出完整的代碼:https://github.com/iamwx/faceSwap

基于opencv和Dlib的人臉交換(face swap)
基于opencv和Dlib的人臉交換(face swap)
基于opencv和Dlib的人臉交換(face swap)

注:鹿晗和湯唯的圖檔來自網絡

二 具體步驟

主要分為兩個步驟:人臉對齊(face aligment)和無縫融合(Seamless Cloning,可能翻譯的不準确)。其中人臉對齊又分為人臉關鍵點檢測(face landmark detection),計算凸包(convex hull),Delaunay三角剖分(delaunay trangulation), 仿射變換(affine warp)。下面來一點點說。

1 人臉關鍵點檢測

首先要檢測出兩張圖檔上的人臉關鍵點。直接使用dlib的檢測函數,先檢測人臉,再檢測人臉關鍵點。

void faceLandmarkDetection(dlib::array2d<unsigned char>& img, shape_predictor sp, std::vector<Point2f>& landmark)
{
        dlib::frontal_face_detector detector = get_frontal_face_detector();    //dlib::pyramid_up(img);

    std::vector<dlib::rectangle> dets = detector(img);    //cout << "Number of faces detected: " << dets.size() << endl;


    full_object_detection shape = sp(img, dets[0]);    //image_window win;
    //win.clear_overlay();
    //win.set_image(img);
    //win.add_overlay(render_face_detections(shape));
    for (int i = 0; i < shape.num_parts(); ++i)
    {        float x=shape.part(i).x();        float y=shape.part(i).y();  
        landmark.push_back(Point2f(x,y));       
    }


}      
基于opencv和Dlib的人臉交換(face swap)

注:該圖來自原作

2 . 計算凸包

如上圖所示一共檢測出68個關鍵點。然而我們隻需要人臉邊緣的哪些關鍵點。是以我們先計算這68個關鍵點的凸包。關于凸包的概念可以參考[2]。

std::vector<Point2f> hull1;    std::vector<Point2f> hull2;    std::vector<int> hullIndex;//儲存組成凸包的關鍵點的下标索引。

    cv::convexHull(points2, hullIndex, false, false);//計算凸包

    //儲存組成凸包的關鍵點。
    for(int i = 0; i < hullIndex.size(); i++)
        {
        hull1.push_back(points1[hullIndex[i]]);
        hull2.push_back(points2[hullIndex[i]]);
        }      

包是有那些處在邊緣的點組成的。如上圖最左邊顯示。

3. Delaunay 三角剖份 和 仿射變換

有啦那些臉部邊緣的關鍵點之後,我們對這些關鍵點進行三角剖份,如上圖中間所示。具體的可以參考我的另外一篇部落格基于opencv+Dlib的面部合成(Face Morph)。 

然後我們在将這左邊臉上的這一個個小三角形通過仿射變換投影到右邊的臉上,就得到啦右邊那副圖檔。

for(size_t i=0;i<delaunayTri.size();++i)
    {        std::vector<Point2f> t1,t2;//存放三角形的頂點
        correspondens corpd=delaunayTri[i];        for(size_t j=0;j<3;++j)
        {
            t1.push_back(hull1[corpd.index[j]]);
            t2.push_back(hull2[corpd.index[j]]);
        }

        warpTriangle(imgCV1,imgCV1Warped,t1,t2); //進行仿射變換           
    }      
void warpTriangle(Mat &img1, Mat &img2, std::vector<Point2f> &t1, std::vector<Point2f> &t2)
{

    Rect r1 = boundingRect(t1);
    Rect r2 = boundingRect(t2);    // Offset points by left top corner of the respective rectangles
    std::vector<Point2f> t1Rect, t2Rect;    std::vector<Point> t2RectInt;    for(int i = 0; i < 3; i++)
    {

        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) );
        t2RectInt.push_back( Point(t2[i].x - r2.x, t2[i].y - r2.y) ); // for fillConvexPoly

    }    // Get mask by filling triangle
    Mat mask = Mat::zeros(r2.height, r2.width, CV_32FC3);
    fillConvexPoly(mask, t2RectInt, Scalar(1.0, 1.0, 1.0), 16, 0);    // Apply warpImage to small rectangular patches
    Mat img1Rect;
    img1(r1).copyTo(img1Rect);

    Mat img2Rect = Mat::zeros(r2.height, r2.width, img1Rect.type());

    applyAffineTransform(img2Rect, img1Rect, t1Rect, t2Rect);

    multiply(img2Rect,mask, img2Rect);
    multiply(img2(r2), Scalar(1.0,1.0,1.0) - mask, img2(r2));
    img2(r2) = img2(r2) + img2Rect;    

}void applyAffineTransform(Mat &warpImage, Mat &src, std::vector<Point2f> &srcTri, std::vector<Point2f> &dstTri)
{    // Given a pair of triangles, find the affine transform.
    Mat warpMat = getAffineTransform( srcTri, dstTri );    // Apply the Affine Transform just found to the src image
    warpAffine( src, warpImage, warpMat, warpImage.size(), cv::INTER_LINEAR, BORDER_REFLECT_101);
}      
基于opencv和Dlib的人臉交換(face swap)

注:該圖來自原作

4. 無縫融合

可以從上圖最左邊看出來,這樣直接投影過去的話非常生硬。是以還需要把投影過去的圖檔src和原來的圖檔dst進行融合。 

這裡需要先計算一個臉部的mask,如上圖3所示。

//calculate mask
    std::vector<Point> hull8U;    for(int i=0; i< hull2.size();++i)
    {
        Point pt(hull2[i].x,hull2[i].y);
        hull8U.push_back(pt);
    }


    Mat mask = Mat::zeros(imgCV2.rows,imgCV2.cols,imgCV2.depth());
    fillConvexPoly(mask, &hull8U[0], hull8U.size(), Scalar(255,255,255));      

之後再直接調用opencv的seamlessClone函數進行融合就可以啦。使用seamlessClone這個函數需要包含頭檔案

#include<opencv2/photo.hpp>      

seamlessClone的具體定義可以參考opencv的官方文檔[3]。在本文中的使用如下:

Rect r = boundingRect(hull2);
    Point center = (r.tl() +r.br()) / 2;

    Mat output;
    imgCV1Warped.convertTo(imgCV1Warped, CV_8UC3);
    seamlessClone(imgCV1Warped,imgCV2,mask,center,output,NORMAL_CLONE);      

經過這些操作之後就得到了最終的圖檔。

關注【OpenCV學習交流】