引自:http://blog.csdn.net/wangxing233/article/details/51549880
零、前言
前段時間看到文章【1】和【2】,大概了解了面部合成的基本原理。這兩天空下來了,於是參考【3】自己實現了下。雖然【1】和【2】已經講的很清楚了,但是有一些細節沒有提到。所以我在這里記錄一下實現的過程中以及一些小細節。
一、什么是面部合成?
這里的面部合成指的的是把一張臉逐漸的變化成另外一張臉。圖1展示了從詹姆斯漸變到科比的過程。其實如果把這些圖片合成視頻的話效果會更好。但是我不知道在這里怎么添加視頻,所以就沒弄了。
圖 1. 勒布朗詹姆斯到科比的漸變。第一排第一張為詹姆斯原圖,第二排最后為科比原圖。從第一排到第二排為漸變過程。
二 、主要步驟
面部合成的原理就是利用給定的兩張圖片 和
生成
張從
漸變到
的過度圖片
。這
過度圖片生成的原理是一樣的,通過一個參數
來控制混合的程度。

當接近0時,
看起來比較像
,當
接近1時,
看起來比較像
。當然啦,這個公式只是一個大概的意思。具體來說一共分為如下幾部:1. 檢測人臉關鍵點。2. 三角剖分。3. 圖像變形。下面就從這3點展開來說。
1. 人臉關鍵點定位
給定兩張圖片,每張圖片里面有一個人臉。我們要做的第一步就是分別從這兩張圖片中檢測出人臉,並在定位出人臉關鍵點。人臉一共有68個關鍵點,分布如圖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

檢測出人臉之后,我們接下來要在人臉中定位關鍵點。首先我們需要一個關鍵點檢測器(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中每幅圖片我都手工加了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




3. 圖像變形
把輸入的兩幅圖像以及要求的中間圖像都三角剖分之后。我們接下來要做的是把中間圖像上的小三角形一個一個的填滿,然后得到最終的圖像(圖 6)。
接下來我們描述中間圖像上的一個小三角形求得的過程。我們選定中間圖像上的一個三角形,然后選定
上對應的三角形。求出
上的三角形中的像素到
上的三角形中的像素的仿射變換。仿射變換滿足下面的公式。其中左邊為
上三角形中的像素點的齊次坐標,右邊為
上三角形中的像素點的齊次坐標。中間為仿射變換矩陣。

求出仿射變換的參數后,我們把上三角形中的每一個像素點按照這個公式投影到
上去,這樣就得到了
中選定的三角形區域的像素值。
以上這個求仿射變換和進行像素投影這兩個步驟可以直接調用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
三、說明
- 以上所用到的部分圖像來自網絡,如有版權問題請聯系我,謝謝。
- 這個項目的代碼可以從【1】中下載。不過它里面默認關鍵點和索引對都是已知的。我自己的完整版代碼見github:iamwx/FaceMorph
參考資料: 1. 【SATYA MALLICK】Face Morph Using OpenCV — C++ / Python 2. 【大數據文摘】手把手:使用OpenCV進行面部合成— C++ / Python 3. opencv github主頁 4. Dlib 庫主頁 5. 【百度百科】Delaunay三角剖分算法