利用 OpenCV 和 Caffe,根據大合影構造“平均臉”

GitChat的部落格發表於2018-04-12

公司年會,大部門一起照了大合影。忽然有興趣看看大家的平均臉是什麼樣子的,於是用 OpenCV 從大合影中提取出一千多名程式設計師的臉,構造了所有人的平均臉。

拿給同事看,大家又要求看分性別平均的平均臉。於是又下載了 Caffe 的 gender classification model,將樣本做了一下性別分類,之後分別構造了雙方的平均臉。得出結果:大平均的顏值原來是被男生拉低的[哈哈]

本文就講述根據照片計算平均臉的原理,具體程式碼,配發實現程式碼和分類模型。

本場 Chat 只有文章,沒有交流。

0. 有趣的平均臉

1878年,英國的弗朗西斯·高爾頓爵士(Sir Francis Galton)發明了一種將許多人的照片合成為一張照片,從而創造出一個“平均”面容的技術。

enter image description here

弗朗西斯·高爾頓爵士,英國維多利亞時代的博學家、人類學家、優生學家、熱帶探險家、地理學家、發明家、氣象學家、統計學家、心理學家和遺傳學家;也是《物種起源》作者查爾斯·達爾文的表弟。*

當時具體的合成方法是照片疊加——給多個人,比如20個人,照相,將每個人照片所需的曝光時間縮短為1/20,通過20次曝光得到一張“平均”照片。

enter image description here

弗朗西斯·高爾頓最初合成平均臉的目的是將不同“種類”的人(例如:囚犯、精神病患者等等)視覺化,以期得到這類人的“原型”(共同特徵),但結果卻意外的發現,這樣合成的人臉卻比用於合成大部分(甚至是全部)都要好看!

雖然高爾頓爵士的初衷沒有達到,合成平均臉的方法卻保留了下來。

隨著技術的發展,照片不再需要物理底片,合成也不再需要複雜的曝光沖印技術,通過一些簡單的操作就能做到,人人都可以上手。

大家想必看到過一些按國家、民族合成的平均臉吧:

enter image description here

想不想自己動手製作一張周圍人的平均臉?

一點都不復雜,只要知道了原理,會寫幾行簡單的程式碼,就能順利完成。

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自然也就成了它們的“平均臉”。

看起來好容易哦,那我們趕緊找兩張照片來試試吧!就用這兩張:

enter image description here

這兩張照片alpha=0.5後直接疊加的結果是這樣的:

enter image description here

這也不是人臉呀!先別急,看看為什麼會這樣?

從這張“重影圖”上不難看出來,之所以這樣,是因為最基本的五官都沒有對齊。如果我們事先把兩個人的眼睛和嘴對齊,效果就不會是這樣的了。

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個面部基準點(如下圖)。

enter image description here

Step2. Delaunay 三角剖分

在獲得了68個面部基準點之後,我們結合人臉所在的矩形的四個頂點和每條邊的中心點,將人臉所在的矩形分割成如下圖所示的三角形的組合。

enter image description here

這一方法又稱為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進行疊加了。最後得到疊加結果。

enter image description here

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位美國總統的平均臉:

enter image description here

他們平均之後的樣子是這樣的:

enter image description here

我們用同樣的方法來看看gitchat 達人課講師的平均臉!

enter image description here

這是平均後的結果:

enter image description here

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+張面孔,構造瞭如下這張基於大合影的平均臉。

enter image description here

很年輕吧 [哈哈]

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+男性。然後分別構造了平均臉。

是這個樣子的:

enter image description here

對比一下上面那張不分性別的大平均,女生簡直就被融化了——女生對大平均的貢獻只是讓最終的頭像皮膚好了點,眼睛大了點,整個性別特徵都損失掉了!

10. 外一題:平均臉=/=平均顏值

平均臉不等於平均顏值,恰恰相反,平均臉是高顏值的代表——這一點,通過構造平均臉,能夠獲得直觀的感受。

大多數平均臉構造的結果比其中每一個個體的顏值都要高。如果有一個個體的顏值能夠超過TA所參與構造的平均臉,那TA肯定屬於非常好看的那種。

弗朗西斯·高爾頓爵士曾經在100多年前合成過犯人的平均臉,他當時企圖通過這一方法找到“犯罪的典型面孔”。合成結果卻是:原本一個個面目猙獰、恐怖甚至畸形的重罪刑事犯的臉被疊加在一起求了“平均”之後,變得好看了。

後來,這一現象被一次又一次證明。如今,長著一張“平均臉”已經可以算是高顏值的標準了。

參考資料

【1】 AverageFace 程式碼、模型及樣例圖片

【2】 FaceGenderClassification 配置檔案及命令

【3】 原始的性別分類模型

【4】Delaunay三角剖分原理

【5】caffe安裝指南


本文首發於GitChat,未經授權不得轉載,轉載需與GitChat聯絡。

閱讀全文: http://gitbook.cn/gitchat/activity/5a1d034d81daaa5e4a3c3997

一場場看太麻煩?成為 GitChat 會員,暢享 1000+ 場 Chat !點選檢視

相關文章