利用 OpenCV 和 Caffe,根據大合影構造“平均臉”
公司年會,大部門一起照了大合影。忽然有興趣看看大家的平均臉是什麼樣子的,於是用 OpenCV 從大合影中提取出一千多名程式設計師的臉,構造了所有人的平均臉。
拿給同事看,大家又要求看分性別平均的平均臉。於是又下載了 Caffe 的 gender classification model,將樣本做了一下性別分類,之後分別構造了雙方的平均臉。得出結果:大平均的顏值原來是被男生拉低的[哈哈]
本文就講述根據照片計算平均臉的原理,具體程式碼,配發實現程式碼和分類模型。
本場 Chat 只有文章,沒有交流。
0. 有趣的平均臉
1878年,英國的弗朗西斯·高爾頓爵士(Sir Francis Galton)發明了一種將許多人的照片合成為一張照片,從而創造出一個“平均”面容的技術。
弗朗西斯·高爾頓爵士,英國維多利亞時代的博學家、人類學家、優生學家、熱帶探險家、地理學家、發明家、氣象學家、統計學家、心理學家和遺傳學家;也是《物種起源》作者查爾斯·達爾文的表弟。*
當時具體的合成方法是照片疊加——給多個人,比如20個人,照相,將每個人照片所需的曝光時間縮短為1/20,通過20次曝光得到一張“平均”照片。
弗朗西斯·高爾頓最初合成平均臉的目的是將不同“種類”的人(例如:囚犯、精神病患者等等)視覺化,以期得到這類人的“原型”(共同特徵),但結果卻意外的發現,這樣合成的人臉卻比用於合成大部分(甚至是全部)都要好看!
雖然高爾頓爵士的初衷沒有達到,合成平均臉的方法卻保留了下來。
隨著技術的發展,照片不再需要物理底片,合成也不再需要複雜的曝光沖印技術,通過一些簡單的操作就能做到,人人都可以上手。
大家想必看到過一些按國家、民族合成的平均臉吧:
想不想自己動手製作一張周圍人的平均臉?
一點都不復雜,只要知道了原理,會寫幾行簡單的程式碼,就能順利完成。
1. 用Image Morphing技術疊加照片
Image Morphing技術的原理相當簡單:給定兩張圖片I和J,我們通過疊加(或者叫做混合)I和J來獲得一張中間狀態的圖片M。
I和J的疊加由一個引數[0,1]區間內的引數alpha來控制。當alpha=0時,M就等同於I,而aphla=1時,M就為J。
換言之, M中的每一個畫素M(x,y),都可以通過這樣一個公式來得到它的值:
M(x,y) = (1 – alpha)·I(x,y) + alpha·J(x,y)
當alpha=0.5的時候,I和J就五五開,平均貢獻了M。如果I和J是兩張人臉照片的話,M自然也就成了它們的“平均臉”。
看起來好容易哦,那我們趕緊找兩張照片來試試吧!就用這兩張:
這兩張照片alpha=0.5後直接疊加的結果是這樣的:
這也不是人臉呀!先別急,看看為什麼會這樣?
從這張“重影圖”上不難看出來,之所以這樣,是因為最基本的五官都沒有對齊。如果我們事先把兩個人的眼睛和嘴對齊,效果就不會是這樣的了。
2. 疊加兩張“對齊的”人臉
疊加圖片I和圖片J的時候,首先應該建立兩張照片中畫素的對應關係。
對I中的某一個畫素點(xi,yi),我們不是直接在J中取同樣位置的點就可以了,而是要找到它在J中內容上的對應點 (xj,yj),然後再進一步找到M中這兩個點疊加之後應當處在的位置(xm,ym),最後再用下列式子得出M中對應點的畫素值:
xm = (1-alpha) · xi + alpha · xj
ym = (1-alpha) · yi + alpha · yj
算式-1
對一個畫素點我們這樣做,對整幅圖片,則是將上面的過程運用到它的每一個畫素點上:
M(xm,ym) = (1 – alpha)·I(xi,yi) + alpha·J(xj,yj)
算式-2
很好,我們已經知道從原理上該怎麼疊加兩張圖了。
其中關鍵的一步就是:找到對應點。
其實對應點疊加的方法可以用來疊加任何圖片,不僅限於人臉。不同物體的疊加,真正的區別就在於找到畫素點之間的對應關係!一旦對應關係找到,直接運用算式-2就好了,
既然我們現在要做的是疊加人臉,那麼首先當然要找到人臉上的對應點。
人臉是生活中最常見的事物,我們每一個人都非常熟悉。一個人的臉如果用簡筆畫畫出來,可以簡化為:臉型+五官(眉毛、眼睛、鼻子、嘴)。
那麼如果我們要疊加兩個人的臉的話,自然就是要針對他們的臉型和五官形制求平均。
人的五官如果用圖形來描繪,都是不規則圖形。如果要完全不走樣的獲取一個人的眼睛、眉毛、鼻子或者嘴,需要繪製非常複雜的形狀。
實際上,我們沒有必要這樣做,而是可以通過一種非常簡單的近似方法,把一張人臉分割成若干三角形的區域。然後再來疊加兩張臉上對應的三角區域。
具體方法如下:
Step 1. 獲取人臉特徵
在圖片中獲取人臉和人臉特徵(臉型+五官)。
我們先在每張面孔上獲取68個面部基準點(如下圖)。
Step2. Delaunay 三角剖分
在獲得了68個面部基準點之後,我們結合人臉所在的矩形的四個頂點和每條邊的中心點,將人臉所在的矩形分割成如下圖所示的三角形的組合。
這一方法又稱為Delaunay三角剖分。更多細節請看這裡。
Delaunay三角剖分將影像分解成若干三角形。
Step 3. 基於Delaunay剖分三角形的仿射變換
得到這些Delaunay剖分三角形後,再分別對齊各個區域,對其中畫素值進行平均。
【step-3.1】 使用前述的算式-1,根據影像I和影像J中已經獲得的76個點,在疊加的結果影像M中找到76個點(xm, ym)
【step-3.2】現在我們在影像I,J和M中分別得到了76個點,以及由這76個點剖分而成的三組三角形。
從影像I中選取一個三角形ti,在M中找到對應區域tm,通過ti三個頂點到tm三個頂點的對映關係來計算ti到tm的仿射變換。
同理計算出tj到tm的仿射變換。
【step-3.3】 扭曲三角形
對於影像I中的一個三角形,使用step-3.2中計算出的放射變換,將其中每一個畫素通過仿射變換對應到M中對應的位置去。
重複這個過程,處理影像I中的每一個三角形,得到一個扭曲的(warped)影像I'。用同樣的方法處理影像J,獲得扭曲的影像J'。
【step-3.4】疊加兩張臉
在step-3.3中我們已經得到了扭曲的影像I'和影像J'。這兩個影像就可以直接使用算式-2進行疊加了。最後得到疊加結果。
4. 疊加多張人臉
算式-2用於疊加2張人臉,在alpha=0.5時求取的是兩張臉的平均。
那麼我們把算式推廣一下,從影像I和影像J推廣為影像I1, I2, I3, ..., In;令alpha=1/n;則算式-2變形為如下:
M(xm,ym) = 1/n · [I1(xi1, yi1) + I2(xi2, yi2) + ... ... + In(xin, yi_n)]
由此,我們也就得到了n張臉的平均。
用這個方法,我們可以得到6位美國總統的平均臉:
他們平均之後的樣子是這樣的:
我們用同樣的方法來看看gitchat 達人課講師的平均臉!
這是平均後的結果:
5. opencv + dlib 輕鬆搞定平均臉
NOTE:所有程式碼都可以從筆者github的AverageFace專案中獲取。
之前是從描述角度來講解平均臉原理。現在,我們來看看code。
[Code -1 ] 使用dlib來進行人臉識別和人臉特徵點的提取
detector = dlib.get_frontal_face_detector()# predictor_path is the local path of facial shape predictor# we don't need to train facial shape predictor by ourselves,# the predictor could be downloaded from # http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2predictor = dlib.shape_predictor(predictor_path) img = io.imread(image_file_path)dets = detector(img, 1)for k, d in enumerate(dets): shape = predictor(img, d) # shape_np stores the 68 face feature points shape_np = np.zeros((68, 2), dtype = int) for i in range(0, 68): shape_np[i] = (int(shape.part(i).x), int(shape.part(i).y)) # we saved shape_np to a text file np.savetxt(image_file_path + '.txt', shape_np, fmt = '%i') index = index + 1
[Code-2] 根據特徵點獲得Delaunay剖分三角
def calculateDelaunayTriangles(rect, points): # Create subdiv subdiv = cv2.Subdiv2D(rect); # Insert points into subdiv for p in points: subdiv.insert((p[0], p[1])); # List of triangles. Each triangle is a list of 3 points ( 6 numbers ) triangleList = subdiv.getTriangleList(); # Find the indices of triangles in the points array delaunayTri = [] for t in triangleList: pt = [] pt.append((t[0], t[1])) pt.append((t[2], t[3])) pt.append((t[4], t[5])) pt1 = (t[0], t[1]) pt2 = (t[2], t[3]) pt3 = (t[4], t[5]) if rectContains(rect, pt1) and rectContains(rect, pt2) and rectContains(rect, pt3): ind = [] for j in xrange(0, 3): for k in xrange(0, len(points)): if(abs(pt[j][0] - points[k][0]) < 1.0 and abs(pt[j][1] - points[k][1]) < 1.0): ind.append(k) if len(ind) == 3: delaunayTri.append((ind[0], ind[1], ind[2])) return delaunayTri
[Code-3] 計算仿射變換
def applyAffineTransform(src, srcTri, dstTri, size) : # Given a pair of triangles, find the affine transform. warpMat = cv2.getAffineTransform( np.float32(srcTri), np.float32(dstTri) ) # Apply the Affine Transform just found to the src image dst = cv2.warpAffine( src, warpMat, (size[0], size[1]), None, flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101 ) return dst
[Code-4] 通過仿射變換扭曲Delaunay剖分三角形
def warpTriangle(img1, img2, t1, t2) : # Find bounding rectangle for each triangle r1 = cv2.boundingRect(np.float32([t1])) r2 = cv2.boundingRect(np.float32([t2])) # Offset points by left top corner of the respective rectangles t1Rect = [] t2Rect = [] t2RectInt = [] for i in xrange(0, 3): t1Rect.append(((t1[i][0] - r1[0]),(t1[i][1] - r1[1]))) t2Rect.append(((t2[i][0] - r2[0]),(t2[i][1] - r2[1]))) t2RectInt.append(((t2[i][0] - r2[0]),(t2[i][1] - r2[1]))) # Get mask by filling triangle mask = np.zeros((r2[3], r2[2], 3), dtype = np.float32) cv2.fillConvexPoly(mask, np.int32(t2RectInt), (1.0, 1.0, 1.0), 16, 0); # Apply warpImage to small rectangular patches img1Rect = img1[r1[1]:r1[1] + r1[3], r1[0]:r1[0] + r1[2]] size = (r2[2], r2[3]) img2Rect = applyAffineTransform(img1Rect, t1Rect, t2Rect, size) img2Rect = img2Rect * mask # Copy triangular region of the rectangular patch to the output image img2[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]] = img2[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]] * ( (1.0, 1.0, 1.0) - mask ) img2[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]] = img2[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]] + img2Rect
6. 用大合影構造“平均臉”
原理和程式碼都非常簡單,不過在實際執行當中,我們需要注意:
【NOTE-1】我們用來做平均臉的單個人臉影像的尺寸很可能不一樣,為了方便起見,我們將它們全部轉為600*600大小。而所用原始圖片,最好比這個尺寸大。
【NOTE-2】既然是要做平均臉,最好都是選用正面、端正姿態的人臉,面部表情最好也不要過於誇張。
根據這兩點,我們發現:證件照非常合適用來做平均臉。
不過,一般我們很難找到那麼多證件照,卻比較容易獲得另一類照片——合影。
特別是那種相對正規場合的合影,比如畢業照,公司年會、研討會集體合影之類的。這類照片,大家都朝一個方向看,全部面帶剋制、正式的微笑,簡直就是構造平均臉的理想樣本啊!
我們只需要將一張大合影中每個人的頭像“切”下來,生成一張單獨的人臉照片,然後在按照4中的描述來疊加多張人臉不就好了嗎?
可是,如果一張大合影上有幾十幾百,甚至上千人,難道我們手動去切圖嗎?
當然不用,別忘了,我們本來就可以檢測人臉啊!我們只需要檢測到每一張人臉所在的區域,然後再將該區域sub-image獨立儲存成一張照片就好了!所有過程,完全可以自動化完成!
當然所用原圖最好清晰度好一點,不然切出來的照片模糊,得出結果就更模糊了。
正好筆者所在的大部門前不久年會,照了一張高清合影。筆者從中切割出1100+張面孔,構造瞭如下這張基於大合影的平均臉。
很年輕吧 [哈哈]
7. 用caffe區分人臉的性別
當筆者把自己部門的平均臉給同事看之後,馬上有同事問:為什麼只平均了男的?
回答:不是隻平均了男的,是不分男女一起平均的,不過得出的結果看著像個男的而已。
又問:為什麼不把男女分開平均?
是啊,一般人臉能夠直接提供的資訊包括:性別、年齡、種族。從大合影中提取的臉,一般年齡差距不會太大(考慮大多數合影場合),種族也相對單一,性別卻大多是混合的,如果不能區分男女,合成的平均臉意義不大。
如果能自動獲得一張臉的性別資訊,然後將男女的照片分開,再構造平均臉顯然合理的多。
於是,又在網上找了一個性別分類模型,用來給人臉照片劃分性別。因為是用現成的模型,所以程式碼非常簡單,不過需要預先安裝caffe和cv2:
mean_filename='models\mean.binaryproto'gender_net_model_file = 'models\deploy_gender.prototxt'gender_net_pretrained = 'models\gender_net.caffemodel'gender_net = caffe.Classifier(gender_net_model_file, gender_net_pretrained, mean=mean, channel_swap=(2, 1, 0), raw_scale=255, image_dims=(256, 256))gender_list = ['Male', 'Female']img = io.imread(image_file_path)dets = detector(img, 1)for k, d in enumerate(dets): cropped_face = img[d.top():d.bottom(), d.left():d.right(), :] h = d.bottom() - d.top() w = d.right() - d.left() hF = int(h * 0.1) wF = int(w*0.1) cropped_face_big = img[d.top() - hF:d.bottom() + hF, d.left() - wF:d.right() + wF, :] prediction = gender_net.predict([cropped_face_big]) gender = gender_list[prediction[0].argmax()].lower() print 'predicted gender:', gender dirname = dirname + gender + "\\" copyfile(image_file_path, dirname + filename)
用這個模型先predict一遍每張人臉的性別,將不同性別的照片分別copy到male或者female目錄下,然後再分別對這兩個目錄下的照片求平均,就可以得到男女不同的平均臉了!
NOTE:這一步的程式碼、執行都很簡單,比較坑的是caffe的安裝。
因為筆者用的是Windows機器,只能下載caffe原始碼自己編譯安裝,全過程遵照https://github.com/BVLC/caffe/tree/windows,相當繁瑣。
而且由於系統設定的問題,編譯後,libraries目錄不是生成在caffe原始碼根目錄下,而是位於C:\Users\build.caffe\dependencies\librariesv140x64py271.1.0 —— 這一點未必會發生在你的機器上,但是要注意編譯過程中每一步的結果。
8. 訓練自己的性別識別模型
想法是很好,但是,這個直接download的gender classification模型效能不太好。有很多照片的性別被分錯了!
這種分錯看不出什麼規律,有些明明很女性化的女生頭像被分成了male,很多特徵鮮明的男生頭像卻成了female。
能夠看出來的是,gender_net.caffemodel 是一個而分類模型,而且male是它的positive類,所有不被認為是male的,都被分入了female(包括一些根本就不是人臉的照片)。
筆者用自己從大合影中擷取的1100+張頭像做了一次測試,發現此模型的precision相對高一些——83.7%,recall低得多——54%,F1Score只有0.66。
考慮到這是一個西方人訓練的模型,很可能它並不適合亞洲人的臉。筆者決定用自己同事的一千多張照片訓練自己的性別分類模型!
我們用caffe訓練模型,不需要寫程式碼,只需要準備好訓練資料(人臉圖片),編寫配置檔案,並執行命令即可。命令和配置檔案均在筆者github的FaceGenderClassification專案中。
為了驗證新模型效果,筆者建立了幾個資料集,最大的一個(下面稱為testds-1)包含110+張照片,取自一張從網上搜尋到的某大學畢業照中切分出的人臉;另外還有3個size在10-20不等的小資料集。
原始性別分類模型在testds-1上的Precision = 94%, Recall = 12.8% ——完全不可用啊!新訓練的性別分類模型在testds-1上的Precision = 95%, Recall = 56% ——明顯高於原始模型。
筆者在一臺記憶體為7G,CPU為Intel Xeon E5-2660 0 @ 2.20GHz 2.19 GHz的機器上訓練(無GPU);訓練資料為1100+張平均8-9K大小的圖片;每1000次迭代需要大概3個小時。
設定為每1000次迭代輸出一個模型。最後一共訓練了14000輪,輸出了14個模型。通過在幾個不同的test data set上對比,發現整體效能最好的是第10次輸出,也就是10000次迭代的結果。
雖然第7次輸出後的各模型差距並不大,但唯獨在一個資料集上,第10次輸出的模型明顯由於其他輸出,這個資料集就是:gitchat的達人課講師頭像——interesting ...
9. 區分性別的平均臉
雖然我們有模型來區分性別,但是如果想要“純粹”的結果,恐怕還是得在模型分類後在人工檢驗並手動糾錯一遍。畢竟,再好的模型,F1Score也不是1。
經過模型分類再手工分揀後,筆者把自己同事的照片分成了兩個set:300+女性和800+男性。然後分別構造了平均臉。
是這個樣子的:
對比一下上面那張不分性別的大平均,女生簡直就被融化了——女生對大平均的貢獻只是讓最終的頭像皮膚好了點,眼睛大了點,整個性別特徵都損失掉了!
10. 外一題:平均臉=/=平均顏值
平均臉不等於平均顏值,恰恰相反,平均臉是高顏值的代表——這一點,通過構造平均臉,能夠獲得直觀的感受。
大多數平均臉構造的結果比其中每一個個體的顏值都要高。如果有一個個體的顏值能夠超過TA所參與構造的平均臉,那TA肯定屬於非常好看的那種。
弗朗西斯·高爾頓爵士曾經在100多年前合成過犯人的平均臉,他當時企圖通過這一方法找到“犯罪的典型面孔”。合成結果卻是:原本一個個面目猙獰、恐怖甚至畸形的重罪刑事犯的臉被疊加在一起求了“平均”之後,變得好看了。
後來,這一現象被一次又一次證明。如今,長著一張“平均臉”已經可以算是高顏值的標準了。
參考資料
【2】 FaceGenderClassification 配置檔案及命令
【3】 原始的性別分類模型
【5】caffe安裝指南
本文首發於GitChat,未經授權不得轉載,轉載需與GitChat聯絡。
閱讀全文: http://gitbook.cn/gitchat/activity/5a1d034d81daaa5e4a3c3997
一場場看太麻煩?成為 GitChat 會員,暢享 1000+ 場 Chat !點選檢視
相關文章
- 889. 根據前序和後序遍歷構造二叉樹二叉樹
- opencv呼叫caffe模型OpenCV模型
- Leetcode 889. 根據前序和後序遍歷構造二叉樹LeetCode二叉樹
- 【根據前序和中序遍歷構造二叉樹】棧+迭代 || 遞迴二叉樹遞迴
- 利用opencv 做一個簡單的人臉識別OpenCV
- 【proto】python根據proto檔案構造message,並換為二進位制Python
- 根據一個輸入資料構造二叉樹和連結串列資料結構的方法(c++)二叉樹資料結構C++
- 根據臉型選項鍊,打造自己的獨特氣質
- Mysql利用Like支援根據匹配度進行查詢MySql
- Python程式碼閱讀(第38篇):根據謂詞函式和屬性字串構造判斷函式Python函式字串
- [資料結構] 根據前中後序遍歷中的兩種構造二叉樹資料結構二叉樹
- 樹莓派利用OpenCV的影像跟蹤、人臉識別等樹莓派OpenCV
- opencv 人臉識別OpenCV
- 根據意圖而不是架構構建程式 - Janos Pasztor架構
- PHP利用反射根據類名反向尋找類所在檔案PHP反射
- 根據提示操作
- Java--構造器和構造方法Java構造方法
- jQuery根據表格欄位升序和降序詳解jQuery
- opencv視訊人臉檢測OpenCV
- 根據奧卡姆剃刀原理選擇架構 - Eduards Sizovs架構
- 根據JSON自動構建的vue篩選框元件JSONVue元件
- 大根堆和小根堆的介紹
- List根據時間排序排序
- js根據時間排序JS排序
- 根據欄位查表名
- 根據年月份分表
- SAP RETAIL 如何根據分配表查到根據它建立的採購訂單?AI
- openCV實戰專案--人臉考勤OpenCV
- JavaScript字串物件 之 根據字元返回位置、根據位置返回字元、字串操作方法JavaScript字串物件字元
- 利用FreeSql.Generator自動根據資料庫表動態生成實體類SQL資料庫
- python - 根據均值和標準差生成隨機整數Python隨機
- JavaScript 根據type篩選inputJavaScript
- 根據 Promises/A+ 手寫 PromsiePromise
- 根據IP定位地理位置
- linux 下根據埠kill 程式Linux
- Ubuntu + CUDA9 + CUDNN7 + OpenCV3.4 + contrib +CAFFE-masterUbuntuDNNOpenCVAST
- 利用一維陣列構造二叉樹陣列二叉樹
- OpenCV-Python 人臉眼睛嘴識別OpenCVPython