3D人臉問題
人臉關鍵點算法已經從2D人臉漸漸發展變化爲3D人臉,2D人臉是給定一副圖片,找到圖片中人臉關鍵點,這些關鍵點都是有着明確語義信息的,或者說都是可見的。而對於3D人臉,本身就是有一個立體結構的,也就是所謂的深度信息。在3D人臉中所要預測出來的關鍵點數量會遠遠地多於2D人臉。通過3D人臉關鍵點定位,能更好的對人臉來進行重構。目前2D人臉對關鍵點的檢測已經相當準確了,從2D過度到3D人臉是一個主要的問題。
- 3D人臉關鍵點定位
- 2D人臉主要爲可見點信息,對於側臉和遮擋很難訓練
- 正臉到側臉姿態變化較大,且標註困難
- 人臉本身就具有深度信息
在進行人臉到美妝、人臉編輯、換臉,3D人臉是具有非常大的作用的。
- 3D人臉算法
- Dense Face Alignment
- DensseReg
- FAN
- 3DDFA
- PRNet
對於3D人臉,主要用到的是人臉的深度信息,目前常用的是3D mesh,UV圖。對於UV圖,輸入的是2D圖,輸出的是UV圖,中間的網絡是一個編解碼的過程。UV圖就描述了3D人臉的座標信息。
人臉對齊算法常用數據集
數據集 | 關鍵點個數 | 下載地址 |
---|---|---|
BioID | 20 | https://www.bioid.com/About/BioID-Face-Database |
LFPW | 29 | http://neerajkumar.org/databases/lfpw |
AFLW | 21 | https://lrs.icg.tugraz.at/research/aflw |
COFW | 29 | http://www.vision.caltech.edu/xpburgos |
300-W | 68 | http://ibug.doc.ic.ac.uk/resources/300-W_IMAVIS |
HELEN | 29 | http://www.f-zhou.com/fa_code.html |
CelebA | 5 | http://mmlab.ie.cuhk.edu.hk/projects/CelebA.html |
我們這裏使用300W-LP的數據集,它包含了68個人臉關鍵點,並且包含了非常多的人臉圖向,這些人臉圖向分別來自AFW、LFPW、HELEN、IBUG and XM2VTS。300W-LP將這些數據集進行了彙總,並進行了68個人臉關鍵點的標註。下載地址鏈接:https://pan.baidu.com/s/1XGHrHdZzvLw1TNcNNmuKkw 提取碼:bwus
除了已經公開的數據集以外,當我們面對一個新的任務,這個任務同我們實際的數據集中的人臉都會存在明顯的差異,比如說要檢測一些明星的人臉關鍵點,這個時候我們就需要去下載一個明星的數據庫,這個數據庫可能需要用爬蟲去爬取圖片。爬取到這些圖片之後需要做標註。如果人工的去標註可能需要消耗非常多的時間成本,這裏推薦使用Dlib庫,利用這個庫能夠去檢測出當前下載的圖片中的人臉的位置並且完成人臉關鍵點定位。Dlib庫完成一個68點的人臉關鍵點的定位。
利用Dlib庫能夠快速的進行人臉檢測和關鍵點定位,生成深度學習模型輸入的訓練樣本。此時我們能夠快速的驗證模型是否能夠收斂,是否合理。驗證模型的收斂性,模型的合理性之後再去人工的對人臉關鍵點座標進行調整。在上圖中就睡Dlib庫檢測出的正臉以及正臉關鍵點的位置。如果我們只是驗證模型的算法和優劣,收斂性合理性,這個時候我們可以人爲的去生成一些樣本。整個任務解決的效率也會高很多。當然最終要去做一款好的產品,此時這些關鍵點的信息必須要去經過人工進行標註的,這個工作是省不了的。
人臉對齊算法常見問題及解決思路
- 環境的變化,會導致拍攝出來的圖像會存在暗光、強光等一系列的問題。解決思路就是數據增強,添加一些光照的變化,圖像扭曲的變化,圖像的旋轉等等。
- 姿態的變化,人臉關鍵點定位中面臨的一個重要的問題,尤其是一些大尺度的姿態的變化,會出現很多人臉關鍵點被遮擋,側臉會導致很多關鍵點消失。對側臉關鍵點檢測會面臨很大的挑戰。解決思路就是姿態分類,對於不同的姿態採用不同的迴歸模型。人臉對齊(矯正),在我們的網絡中加入一個分支,分支網絡就是爲了完成人臉對齊、矯正。
- 表情的變化,如果當前人臉表情非常的浮誇,一些表情會導致很多點位消失。比如說睜眼和閉眼會導致一些關鍵點重合。解決思路是數據增強,這裏的數據增強跟環境變化的數據增強不同,而是採用GAN網絡來去生成一些不同的表情來增加數據集的數據量。
- 遮擋問題,如果人爲的將人臉遮擋住一部分,就會存在一些關鍵點消失。如果想要去預測出這些關鍵點,就需要去利用點和點之間的關聯性來找到遮擋住的關鍵點。對於姿態變化和遮擋導致關鍵點消失,我們可以將其定義爲真假關鍵點,真即爲當前關鍵點是存在的,假則是消失掉的關鍵點。此時我們需要對關鍵點做一個標識——0、1的分類,表示點位是否可見。在Fashion AI中有一個服飾關鍵點定位的問題,會存在一個label來標識當前的關鍵點位是否可見,對於這類問題可以參考相關的任務來進行解決。
- 稠密點,3D人臉會有8000個高維特徵點。預測難度也會變得非常大。
對於遮擋和稠密點可以採用3D人臉關鍵點定位的相關策略來解決這些遮擋和側臉,對人臉加一個深度信息來解決相關的問題。比如說PRNet或編解碼網絡或者熱力圖等。除了這些策略以外我們還可以去優化主幹網絡,比如去關注ImageNet圖像挑戰賽中更好的網絡,能夠提取出更加魯棒的特徵,對主幹網絡進行優化同樣也能提高模型的性能。
- 人臉關鍵點定位問題擴展
- 姿態估計,定位出人體中的非常重要的骨骼點,就是關節位置上的點位,將這些點位進行連接就可以拿到當前人的姿態。對於姿態問題本質上也是一個點回歸的問題,同人臉是非常相似的。
- 服裝關鍵點,主要用於Fashion AI,我們可以利用服飾關鍵點去了解當前服裝設計的一些設計思路。去預測這些服飾的點本質上也是一個點回歸的問題,與人臉關鍵點也有着非常大的相似性。
SENet模型介紹
這裏的數據就是訓練樣本,網絡結構採用SENet來作爲核心網絡結構,經過FC層最後輸出68點關鍵點136維的向量。因爲預測的是一個值,所以這個網絡是一個迴歸網絡。
- 網絡結構
在上圖中的c1、c2表示了一個卷積層,對於卷積的結構,SENet的核心單元實際上是對於卷積的通道,通過一個單獨的分支去計算這個通道(特徵圖)c2所對應的權重。對於一個特徵圖,它的尺寸爲n*H*W*c,其中n爲樣本數量,H爲特徵圖的高,W爲特徵圖的寬,c爲通道的數量,也就是特徵圖的個數。表示一個平均池化層,將特徵圖轉化爲一個1*1*c2的一個點,每一個特徵圖對應到一個c2。經過平均池化層之後,再經過FC層迴歸計算出每一個通道的權重,計算出權重後去對c2特徵圖的不同的通道進行加權。這樣就可以拿到對通道進行加權之後的特徵圖(就是最後那個彩色圖)。在上圖中我們也可以看到該特徵圖不同的顏色對應到不同的權值,這個流程是對通道的加權,這個加權可以理解成Attention。SENet主要是體現在了對通道的加權上,具體SENet的設計需要去結合一些具體的網絡結構。
這裏就給出了兩個網絡結構,一個是SENet Inception,一個是SENet ResNet。對於Inception輸出的特徵圖再經過SENet進行通道上的加權,所以SENet Inception的結構就是在Inception結構的後面再加一個SENet的單元。而SENet ResNet則是將SENet融入到了ResNet殘差的結構中,SENet添加的部分是在殘差之後的單元,在這個結構單元,拿到了特徵圖之後,同樣經過一個SENet來對特徵圖進行通道上的加權,最後再同原先的特徵圖進行相加,可以看到這是集成到了ResNet殘差結構當中,同Inception結構有一點區別。這裏我們可以看到在SENet的結構中有兩個FC層,第一個FC層會對通道進行一個下采樣,此時整個網絡的參數量會有一定的減少,通過FC層計算出權重後,再通過一個Sigmoid將權值歸一化,做個激活,映射到0~1之間。最後再將這個權重乘回到特徵圖上完成對通道的加權。最終將加權之後的特徵圖同輸入的原始的特徵圖進行相加,得到最終ResNet結構網絡的輸出。對於SENet ResNet的網絡結構可以用下面的代碼來表示
這裏X1 = conv(X0)卷積就表達了Residual結構,X2 = weight(X0)就是計算權重的過程,就表達了SENet結構的部分,X3 = X0 + X1 ** X2表示最後相加的運算。
數據準備和環境參數
在下載好的300W-LP中,有一個landmarks文件夾,該文件夾中有4個數據集的標註信息。
進入任意一個文件夾,這裏我們進入AFW文件夾,可以看到裏面包含了人臉關鍵點標註的文件
這裏.mat是matlab解析的文件。給出了圖片所對應的landmark點位的座標,也就是有136個值,對應到68個關鍵點。原始的圖片則是對應到AFW下面的圖片
這裏的圖片很多都睡合成的圖片,這裏也有.mat文件,也是包含了關鍵點信息,用這裏的.mat文件同樣也能拿到landmark座標。現在我們將landmarks文件夾下的.mat文件給讀取出來。
from scipy.io import loadmat if __name__ == "__main__": m = loadmat("/Users/admin/Documents/300W_LP/landmarks/AFW/AFW_134212_1_0_pts.mat") print(m)
運行結果
{'__header__': b'MATLAB 5.0 MAT-file, Platform: PCWIN64, Created on: Wed Nov 25 21:42:19 2015', '__version__': '1.0', '__globals__': [], 'pts_3d': array([[161.33554, 236.34529],
[166.22395, 258.93695],
[173.30295, 279.23016],
[178.36287, 296.87012],
[182.11465, 316.6834 ],
[184.54196, 335.62225],
[186.31201, 350.7883 ],
[193.3487 , 365.7949 ],
[212.22702, 373.85773],
[238.0674 , 365.59265],
[262.0358 , 350.78265],
[281.9492 , 335.1942 ],
[297.3871 , 316.2213 ],
[305.51746, 296.2011 ],
[310.01917, 278.38666],
[313.57056, 257.59332],
[314.6806 , 234.8151 ],
[141.07718, 233.8748 ],
[142.43147, 231.9335 ],
[149.534 , 232.06624],
[158.13736, 234.31857],
[167.2975 , 237.67184],
[205.5932 , 234.99286],
[216.04825, 229.73251],
[229.51547, 225.43398],
[245.78271, 224.81818],
[261.82166, 228.8091 ],
[186.61603, 257.0625 ],
[183.976 , 273.16412],
[180.8042 , 288.92126],
[181.61343, 300.68912],
[180.7037 , 302.45096],
[184.21692, 305.5235 ],
[190.96376, 307.95453],
[199.12416, 305.38718],
[206.87915, 302.37335],
[154.32986, 249.83685],
[156.83566, 248.11707],
[166.40779, 247.80139],
[177.58078, 251.61589],
[168.43463, 254.51057],
[159.2126 , 254.42873],
[216.7242 , 250.81557],
[224.1378 , 246.13824],
[234.09474, 245.99228],
[245.98262, 248.45946],
[236.002 , 253.01753],
[224.37584, 253.48346],
[175.8696 , 319.5031 ],
[179.09326, 319.5954 ],
[187.91132, 319.06714],
[193.99792, 320.62445],
[200.40106, 319.3349 ],
[217.08116, 320.4863 ],
[233.78671, 322.4403 ],
[219.32835, 334.79114],
[208.45264, 340.6076 ],
[198.55411, 341.61356],
[189.62987, 339.7993 ],
[183.00745, 332.94818],
[178.17087, 319.56323],
[188.85269, 324.193 ],
[196.60971, 325.01086],
[205.92148, 324.9917 ],
[231.70996, 322.35394],
[206.32933, 331.23206],
[197.35251, 331.98334],
[189.53477, 330.0774 ]], dtype=float32), 'pts_2d': array([[144.78085, 253.39178],
[143.70929, 275.59055],
[147.26495, 292.5534 ],
[154.74083, 307.56976],
[164.76851, 325.37213],
[175.56622, 341.61438],
[183.95714, 350.29553],
[191.24448, 364.3733 ],
[212.22702, 373.85773],
[238.0674 , 365.59265],
[262.0358 , 350.78265],
[281.9492 , 335.1942 ],
[297.3871 , 316.2213 ],
[305.51746, 296.2011 ],
[310.01917, 278.38666],
[313.57056, 257.59332],
[314.6806 , 234.8151 ],
[141.07718, 233.8748 ],
[142.43147, 231.9335 ],
[149.534 , 232.06624],
[158.13736, 234.31857],
[167.2975 , 237.67184],
[205.5932 , 234.99286],
[216.04825, 229.73251],
[229.51547, 225.43398],
[245.78271, 224.81818],
[261.82166, 228.8091 ],
[186.61603, 257.0625 ],
[183.976 , 273.16412],
[180.8042 , 288.92126],
[181.61343, 300.68912],
[180.7037 , 302.45096],
[184.21692, 305.5235 ],
[190.96376, 307.95453],
[199.12416, 305.38718],
[206.87915, 302.37335],
[154.32986, 249.83685],
[156.83566, 248.11707],
[166.40779, 247.80139],
[177.58078, 251.61589],
[168.43463, 254.51057],
[159.2126 , 254.42873],
[216.7242 , 250.81557],
[224.1378 , 246.13824],
[234.09474, 245.99228],
[245.98262, 248.45946],
[236.002 , 253.01753],
[224.37584, 253.48346],
[175.8696 , 319.5031 ],
[179.09326, 319.5954 ],
[187.91132, 319.06714],
[193.99792, 320.62445],
[200.40106, 319.3349 ],
[217.08116, 320.4863 ],
[233.78671, 322.4403 ],
[219.32835, 334.79114],
[208.45264, 340.6076 ],
[198.55411, 341.61356],
[189.62987, 339.7993 ],
[183.00745, 332.94818],
[178.17087, 319.56323],
[188.85269, 324.193 ],
[196.60971, 325.01086],
[205.92148, 324.9917 ],
[231.70996, 322.35394],
[206.32933, 331.23206],
[197.35251, 331.98334],
[189.53477, 330.0774 ]], dtype=float32)}
這裏我們可以看到,m的數據格式就是一個字典,而我們所需要的就是pts_2d這個key的value,該value包含了68個landmark。現在我們將這68個點給繪製到原圖中去。這裏我們選擇的原始圖像爲
from scipy.io import loadmat import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) m = loadmat("/Users/admin/Documents/300W_LP/landmarks/AFW/AFW_134212_1_0_pts.mat") landmark = m['pts_2d'] img = cv2.imread("/Users/admin/Documents/300W_LP/AFW/AFW_134212_1_0.jpg") for i in range(68): cv2.circle(img, (int(landmark[i][0]), int(landmark[i][1])), 2, (0, 255, 0), 2) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
繪製後圖像
這裏有68個點,也就是要回歸的68個landmark,也就是136個值。
人臉關鍵點數據打包
import tensorflow as tf import cv2 import numpy as np import glob from scipy.io import loadmat import os if __name__ == "__main__": landmark_path_data = "/Users/admin/Documents/300W_LP/landmarks/" landmark_path_folders = glob.glob(landmark_path_data + "/*") landmark_anno_list = [] output_dir = "/Users/admin/Documents/300W_LP/tfrecord_basic/" if not os.path.exists(output_dir): os.mkdir(output_dir) writer_train = tf.io.TFRecordWriter(output_dir + "train.tfrecords") writer_test = tf.io.TFRecordWriter(output_dir + "test.tfrecords") for f in landmark_path_folders: landmark_anno_list += glob.glob(f + "/*.mat") for idx in range(landmark_anno_list.__len__()): landmark_info = landmark_anno_list[idx] im_path = landmark_info.replace("300W_LP/landmarks", "300W_LP").replace("_pts.mat", ".jpg") img = cv2.imread(im_path) landmark = loadmat(landmark_info)['pts_2d'] # for i in range(68): # cv2.circle(img, (int(landmark[i][0]), int(landmark[i][1])), 2, (0, 255, 0), 2) # cv2.imshow('img', img) # key = cv2.waitKey(0) x_max = int(np.max(landmark[0:68, 0])) x_min = int(np.min(landmark[0:68, 0])) y_max = int(np.max(landmark[0:68, 1])) y_min = int(np.min(landmark[0:68, 1])) x_min = int(x_min - (x_max - x_min) * 0.05) x_max = int(x_max + (x_max - x_min) * 0.05) y_min = int(y_min - (y_max - y_min) * 0.3) y_max = int(y_max + (y_max - y_min) * 0.1) # cv2.rectangle(img, (x_min, y_min), (x_max, y_max), (0, 255, 255), 2) # cv2.imshow('img', img) # key = cv2.waitKey(0) face_data = img[y_min:y_max, x_min:x_max] sp = face_data.shape im_points = [] # 歸一化處理 for p in range(68): im_points.append((landmark[p][0] - x_min) / sp[1]) im_points.append((landmark[p][1] - y_min) / sp[0]) # cv2.circle(face_data, (int(im_points[p * 2] * sp[1]), int(im_points[p * 2 + 1] * sp[0])), # 2, (0, 255, 0), 2) # cv2.imshow('img', face_data) # key = cv2.waitKey(0) face_data = cv2.resize(face_data, (128, 128)) ex = tf.train.Example( features=tf.train.Features( feature={ "image": tf.train.Feature(bytes_list=tf.train.BytesList(value=[face_data.tobytes()])), "label": tf.train.Feature(float_list=tf.train.FloatList(value=im_points)) } ) ) if idx > landmark_anno_list.__len__() * 0.9: writer_test.write(ex.SerializeToString()) else: writer_train.write(ex.SerializeToString()) writer_train.close() writer_test.close()
運行後可以看到
模型訓練代碼實現