基于opencv+Dlib的面部合成(Face Morph)

引自:http://blog.csdn.net/wangxing233/article/details/51549880

零、前言

前段時間看到文章【1】和【2】,大概了解了面部合成的基本原理。這兩天空下來了,于是參考【3】自己實現了下。雖然【1】和【2】已經講的很清楚了,但是有一些細節沒有提到。所以我在這里記錄一下實現的過程中以及一些小細節。

一、什么是面部合成?

這里的面部合成指的的是把一張臉逐漸的變化成另外一張臉。圖1展示了從詹姆斯漸變到科比的過程。其實如果把這些圖片合成視頻的話效果會更好。但是我不知道在這里怎么添加視頻,所以就沒弄了。

勒布朗詹姆斯這里寫圖片描述這里寫圖片描述這里寫圖片描述這里寫圖片描述這里寫圖片描述這里寫圖片描述這里寫圖片描述這里寫圖片描述這里寫圖片描述

圖 1. 勒布朗詹姆斯到科比的漸變。第一排第一張為詹姆斯原圖,第二排最后為科比原圖。從第一排到第二排為漸變過程。

二 、主要步驟

面部合成的原理就是利用給定的兩張圖片 和? 生成 張從漸變到的過度圖片 。這過度圖片生成的原理是一樣的,通過一個參數來控制混合的程度。

?

(1)

?

當接近0時,看起來比較像 ,當接近1時,看起來比較像。當然啦,這個公式只是一個大概的意思。具體來說一共分為如下幾部:1. 檢測人臉關鍵點。2. 三角剖分。3. 圖像變形。下面就從這3點展開來說。

1. 人臉關鍵點定位

給定兩張圖片,每張圖片里面有一個人臉。我們要做的第一步就是分別從這兩張圖片中檢測出人臉,并在定位出人臉關鍵點。人臉一共有68個關鍵點,分布如圖2所示。不過我的研究方向不是搞人臉的,所以這個是做這個項目的時候才去了解的。如果有什么偏差,還望指正。

?

這里寫圖片描述
圖 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

?

這里寫圖片描述 圖 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所示。注意圖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

?

sanyuanzu?lbjdelauny?lbjkbdelauny?kbdelauny

?

?

圖 5. 三角剖分結果

?

3. 圖像變形

把輸入的兩幅圖像以及要求的中間圖像都三角剖分之后。我們接下來要做的是把中間圖像上的小三角形一個一個的填滿,然后得到最終的圖像(圖 6)。

接下來我們描述中間圖像上的一個小三角形求得的過程。我們選定中間圖像上的一個三角形,然后選定上對應的三角形。求出上的三角形中的像素到上的三角形中的像素的仿射變換。仿射變換滿足下面的公式。其中左邊為上三角形中的像素點的齊次坐標,右邊為上三角形中的像素點的齊次坐標。中間為仿射變換矩陣。

?

(2)

?

求出仿射變換的參數后,我們把上三角形中的每一個像素點按照這個公式投影到上去,這樣就得到了中選定的三角形區域的像素值。

以上這個求仿射變換和進行像素投影這兩個步驟可以直接調用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

這里寫圖片描述這里寫圖片描述這里寫圖片描述這里寫圖片描述這里寫圖片描述這里寫圖片描述這里寫圖片描述這里寫圖片描述這里寫圖片描述這里寫圖片描述

圖 6. 圖像變形示意圖

?

三、說明

  1. 以上所用到的部分圖像來自網絡,如有版權問題請聯系我,謝謝。
  2. 這個項目的代碼可以從【1】中下載。不過它里面默認關鍵點和索引對都是已知的。我自己的完整版代碼見github:iamwx/FaceMorph

參考資料: 1.? 【SATYA MALLICK】Face Morph Using OpenCV — C++ / Python 2. 【大數據文摘】手把手:使用OpenCV進行面部合成— C++ / Python 3. opencv github主頁 4. Dlib 庫主頁 5. 【百度百科】Delaunay三角剖分算法

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/541312.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/541312.shtml
英文地址,請注明出處:http://en.pswp.cn/news/541312.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

大腦應對危機的模式_危機的完整形式是什么?

大腦應對危機的模式危機&#xff1a;印度信用評級信息服務有限公司 (CRISIL: Credit Rating Information Services of India Limited) CRISIL is an abbreviation of Credit Rating Information Services of India Limited. It is an international analytic company which off…

linux網絡延遲命令,2. Linux使用ping命令查看網絡延遲

ping命令持續發送少量互聯網流量到遠程地址并報告收到回應的總時間。如果流量因為網絡故障或者錯誤配置而被丟棄&#xff0c;它也會報告。ping命令是最基本和初級的診斷網絡問題的工具之一。ping常被用來測試網絡延遲&#xff0c;但是有時ping的延遲并不是網絡引起的&#xff0…

一、簡單工廠模式

# public class Operation //基類{private double _numberA 0;private double _numberB 0;public double NumberA{get{ return _numberA; }set{_numberA value;}}public double NumberB{get{ return _numberB; }set{_numberB value;}}public virtual double GetResult(){d…

軟件生命周期模型及其類型

A life cycle model is also known as a process model. As the name suggests, the software life cycle model (or the software process model) gives us a pictorial representation of the entire software development process. 生命周期模型也稱為過程模型 。 顧名思義&…

linux查看磁盤io帶寬,[Linux] 磁盤IO性能查看和優化以及iostat命令

iostat命令:%user&#xff1a;CPU處在用戶模式下的時間百分比。%nice&#xff1a;CPU處在帶NICE值的用戶模式下的時間百分比。%system&#xff1a;CPU處在系統模式下的時間百分比。%iowait&#xff1a;CPU等待輸入輸出完成時間的百分比。%steal&#xff1a;管理程序維護另一個虛…

Jsoup 數據修改

2019獨角獸企業重金招聘Python工程師標準>>> 1 設置屬性的值 在解析一個Document之后可能想修改其中的某些屬性值&#xff0c;然后再保存到磁盤或都輸出到前臺頁面。 可以使用屬性設置方法 Element.attr(String key, String value), 和 Elements.attr(String key, S…

軟件靜態架構 軟件組件圖_組件圖| 軟件工程

軟件靜態架構 軟件組件圖什么是組件圖&#xff1f; (What is Component Diagram?) A Component Diagram breaks down the real system under development into different heights of working. Every component is reactive for the main aim in the entire system and only re…

如何卸載非linux系統分區,如何卸載Linux系統分區?卸載Linux系統分區的方法-站長資訊中心...

系統為windows xp sp2和redhat as 5雙系統&#xff0c;其中linux系統后安裝的在D盤&#xff0c;華彩軟件站www.huacolor.com小編今天發現硬盤不夠用了&#xff0c;想干掉linux分區&#xff0c;在虛擬機中用linux。就在windows的磁盤管理(命令為:diskmgmt)下刪除linux分區&#…

順序結構復習

復習一些易錯知識點還有習題 目錄 可能不熟悉的知識點 邏輯表達式的求解 if,else的配隊 條件運算符 運算符優先級的問題 switch的使用 goto和if構成的循環 例題講解 1 2 3 4 ?編輯 5 ?編輯 6賦值 ?編輯 7 可能不熟悉的知識點 邏輯表達式的求解 如果…

模板模式(部分方法延遲到子類實現)

項目中&#xff0c;用到了抽象類作為父類&#xff0c;有部分實現。 提供了了模板方法作為子類公共方法&#xff0c;模板方法中調用了抽象類的抽象方法和部分非抽象方法。 執行代碼時&#xff0c;發現模板方法調用了抽象類的抽象方法&#xff0c;當時比較好奇&#xff0c;后來發…

ruby 集合 分組_在Ruby中找到兩個集合之間的區別

ruby 集合 分組Finding differences simply means that finding elements that are uncommon between two sets as well as are only present in the first set. We can find this, with the help of a – operator. You can also consider the objective as to find the uniqu…

怎樣在linux卸載java,卸載linux自帶java,linux自帶java

卸載linux自帶java&#xff0c;linux自帶java第一步&#xff1a;rpm查詢java安裝包名稱[rootlocalhost java]# rpm -qa | grep javajava-1.7.0-openjdk-headless-1.7.0.51-2.4.5.5.el7.x86_64tzdata-java-2014b-1.el7.noarchpython-javapackages-3.4.1-5.el7.noarchjava-1.7.0-…

Swift iOS : 內存管理

Swift是自動管理內存的。這意味著&#xff0c;你不需要主動釋放內存。 比如Foo內包含的Bar&#xff0c;可以隨同Foo一起被釋放&#xff1a; import UIKit UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate {var window : UIWindow?func application(…

python實現接口_Python | 使用類實現接口

python實現接口In this program, we are implementing the concept of Interface using class. Here, Class Shape worked as Interface. In Interface all methods must be non-implemented it must be implemented in child class unlike abstract class, where we can have …

linux lp 打印中文,Linux基礎命令---lp打印文件

lplp指令用來打印文件&#xff0c;也可以修改存在的打印任務。使用該指令可以指定打印的頁碼、副本等。此命令的適用范圍&#xff1a;RedHat、RHEL、Ubuntu、CentOS、Fedora、openSUSE、SUSE。1、語法lp [ -E ] [ -U username ] [ -c ] [ -d destination[/instance] ] [ -h…

【轉載】瀏覽器緩存詳解:expires cache-control last-modified

下面的內容展示了一個常見的 Response Headers&#xff0c;這些 Headers 要求客戶端最多緩存 3600 秒&#xff0c;也給出了一個 pub1259380237;gz 的校驗值。 HTTP/1.x 200 OK Transfer-Encoding: chunked Date: Sat, 28 Nov 2009 04:36:25 GMT Server: LiteSpeed Connection: …

ctype函數_PHP ctype_xdigit()函數與示例

ctype函數PHP ctype_xdigit()函數 (PHP ctype_xdigit() function) ctype_xdigit() function is a character type (CType) function in PHP, it is used to check whether a given string contains hexadecimal digits or not. ctype_xdigit()函數是PHP中的字符類型(CType)函數…

linux ldd運行不成功,Linux_Linux:Ldd命令介紹及使用方法,1、首先ldd不是一個可執行程序 - phpStudy...

Linux&#xff1a;Ldd命令介紹及使用方法1、首先ldd不是一個可執行程序&#xff0c;而只是一個shell腳本2、ldd能夠顯示可執行模塊的dependency&#xff0c;其原理是通過設置一系列的環境變量&#xff0c;如下&#xff1a;LD_TRACE_LOADED_OBJECTS、LD_WARN、LD_BIND_NOW、LD_L…

開發原生的 Google 眼鏡應用 【已翻譯100%】(2/2)

使用傳感器 Glass沒有鍵盤或觸摸屏&#xff0c;但仍然具有移動設備所有的標準的傳感器。你可以使用標準的傳感器組件來訪問這些傳感器。 定位和GPS Glass內置有GPS。TLocationSensor具有一個OnLocationChanged事件&#xff0c;這一事件在GPS組件被激活時產生&#xff0c;在有除…

linux下php的安裝,Linux下PHP安裝

1 下載php源碼安裝包 個人是php-5.6.30.tar.gzphp2 解壓文件mysqltar -zxvf php-5.6.30.tar.gznginx3 編譯安裝sqlcd php-5.6.30api建立www用戶和www用戶組curlgroupadd wwwsocketuseradd -g www wwwphp-fpm在編譯以前先把依賴包都裝上urlyum install curl curl-develyum inst…