在本系列的上一篇中,我們講解了用Image morphing方法合成人臉圖片的基本原理。
所有代碼都在:https://github.com/juliali/AverageFace 和 https://github.com/juliali/FaceGenderClassification
用OpenCV + dlib 製作「平均臉」
既然知道了原理,我們現在就要開始動手製作了。
再來回顧一下步驟,當我們要將N張人臉照片合稱為一張平均臉的時候,我們首先要處理每一張照片:
【1】獲取其中的68個臉部特徵點,並以這些點為定點,剖分Delaunay 三角形,就如下圖這樣:
[Code-1] 首先要獲得68個臉部特徵點,這68個點定義了臉型、眉毛、眼睛、鼻子和嘴的輪廓。幸運的是,這麼複雜的操作,我們用OpenCV,幾行代碼就搞定了!
detector = dlib.get_frontal_face_detector()
predictor = 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)
points = np.zeros((68, 2), dtype = int)
for i in range(0, 68):
points[i] = (int(shape.part(i).x), int(shape.part(i).y))
[Code-2] 接著剖分Delaunay 三角形,其中points就是68個面部特徵點,rect是臉部所在的矩形:
def calculateDelaunayTriangles(rect, points):
subdiv = cv2.Subdiv2D(rect);
for p in points:
subdiv.insert((p[0], p[1]));
triangleList = subdiv.getTriangleList();
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
【2】然後計算每張臉上各個Delaunay剖分三角的仿射變換,再通過仿射變換扭曲Delaunay三角形:
[Code-3] 計算仿射變換
def applyAffineTransform(src, srcTri, dstTri, size) :
warpMat = cv2.getAffineTransform( np.float32(srcTri), np.float32(dstTri) )
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) :
r1 = cv2.boundingRect(np.float32([t1]))
r2 = cv2.boundingRect(np.float32([t2]))
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])))
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);
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
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
以上就是製作平均臉幾個關鍵步驟的代碼。
完整代碼在筆者的GitHub的AverageFace項目:https://github.com/juliali/AverageFace
用大合影構造「平均臉」
原理和代碼都非常簡單,不過在實際運行當中,我們需要注意:
【NOTE-1】我們用來做平均臉的單個人臉圖像的尺寸很可能不一樣,為了方便起見,我們將它們全部轉為600*600大小。而所用原始圖片,最好比這個尺寸大。
【NOTE-2】既然是要做平均臉,最好都是選用正面、端正姿態的人臉,面部表情最好也不要過於誇張。
根據這兩點,我們發現:證件照非常合適用來做平均臉。
不過,一般我們很難找到那麼多證件照,卻比較容易獲得另一類照片——合影。
特別是那種相對正規場合的合影,比如畢業照,公司年會、研討會集體合影之類的。這類照片,大家都朝一個方向看,全部面帶克制、正式的微笑,簡直就是構造平均臉的理想樣本啊!
我們只需要將一張大合影中每個人的頭像「切」下來,生成一張單獨的人臉照片,然後在按照4中的描述來疊加多張人臉不就好了嗎?
可是,如果一張大合影上有幾十幾百,甚至上千人,難道我們手動去切圖嗎?
當然不用,別忘了,我們本來就可以檢測人臉啊!我們只需要檢測到每一張人臉所在的區域,然後再將該區域sub-image獨立存儲成一張照片就好了!所有過程,完全可以自動化完成!
當然所用原圖最好清晰度好一點,不然切出來的照片模糊,得出結果就更模糊了。
正好筆者所在的大部門前不久年會,照了一張高清合影。筆者從中切割出1100+張面孔,構造了如下這張基於大合影的平均臉。
用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目錄下,然後再分別對這兩個目錄下的照片求平均,就可以得到男女不同的平均臉了!
這一步的代碼、運行都很簡單,比較坑的是caffe的安裝!
因為筆者用的是Windows機器,只能下載caffe原始碼自己編譯安裝,全過程遵照https://github.com/BVLC/caffe/tree/windows,相當繁瑣。
而且由於系統設置的問題,編譯後,libraries目錄不是生成在caffe源碼根目錄下,而是位於C:\Users\build.caffe\dependencies\librariesv140x64py271.1.0 —— 這一點未必會發生在你的機器上,但是要注意編譯過程中每一步的結果。
訓練自己的性別識別模型
想法是很好,但是,這個直接download的gender classification模型性能不太好。有很多照片的性別被分錯了!
這種分錯看不出什麼規律,有些明明很女性化的女生頭像被分成了male,很多特徵鮮明的男生頭像卻成了female。
能夠看出來的是,gender_net.caffemodel 是一個而分類模型,而且male是它的positive類,所有不被認為是male的,都被分入了female(包括一些根本就不是人臉的照片)。
筆者用自己從大合影中截取的1100+張頭像做了一次測試,發現此模型的precision相對高一些——83.7%,recall低得多——54%,F1Score只有0.66。
考慮到這是一個西方人訓練的模型,很可能它並不適合亞洲人的臉。筆者決定用自己同事的一千多張照片訓練自己的性別分類模型!
我們用caffe訓練模型,不需要寫代碼,只需要準備好訓練數據(人臉圖片),編寫配置文件,並運行命令即可。
命令和配置文件均在筆者github的FaceGenderClassification項目中:https://github.com/juliali/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次迭代的結果。這個迭代的結果也放在github中。
區分性別的平均臉
雖然我們有模型來區分性別,但是如果想要「純粹」的結果,恐怕還是得在模型分類後在人工檢驗並手動糾錯一遍。畢竟,再好的模型,F1Score也不是1。
經過模型分類再手工分揀後,筆者把自己同事的照片分成了兩個set:300+女性和800+男性。然後分別構造了平均臉。
是這個樣子的:
對比一下上面那張不分性別的大平均,女生簡直就被融化了——女生對大平均的貢獻只是讓最終的頭像皮膚好了點,眼睛大了點,整個性別特徵都損失掉了!
歡迎掃面下列二維碼關注「悅思悅讀」公眾微信號