人臉識別,大體能夠分爲如下四個步驟:html
- 人臉檢測:從圖片中準肯定位到人臉,並以矩形框將其裁剪出來;
- 人臉矯正(對齊): 檢測到的人臉,可能角度不是很正,須要使其對齊,好比旋轉,縮放;
- 特徵提取:對矯正後的人臉進行特徵提取,如今作法一般都是基於一個CNN模型;
- 人臉比對:對兩張人臉圖像提取的特徵向量進行對比,計算類似度。
上述第一步,目前主流的作法都來自如Faster RCNN或者SSD等通用目標檢測的一些改進網絡,大體能夠直接將人臉檢測就當作特定目標的檢測;這裏主要介紹人臉校訂部分,且介紹其中一種方法,也是MTCNN使用的方法,該方法簡單快速,不須要去先創建3D模型而後進行映射。固然該方法效果麼,從其原理上,主要解決的依然是正臉的對齊,沒法很好解決側臉的對齊(這時候能夠用GAN或者3D建模去恢復)。python
MTCNN中採用的人臉矯正方法,是假設拿到的人臉幾乎都是正臉,不過此時正臉有尺度不等,角度偏移等。並且須要預先設定一個平均臉,即目標臉的位置,標記出平均臉的全部關鍵點應該處於的位置,而後將全部人臉矯正到該平均臉上。git
下述概念部分參考自《Master Opencv...讀書筆記》非剛性人臉跟蹤 II。
人臉由眼睛、鼻子、嘴巴、下巴等部位構成,正是由於這些部位形狀、大小和相對位置的各類變化,才使得人臉表情千差萬別,所以能夠對這些部位的形狀和結構關係進行幾何描述,做爲人臉表情識別的重要特徵。這裏,幾何關係就是指預約義點集的空間組態模式,而這些點與臉部器官在幾何空間存在對應關係(好比眼角、鼻尖、眉毛)。github
Facial geometry,經過兩種元素的參數化配置組成:算法
- 全局變形(剛性):指人臉在圖像中的分佈,容許人臉出如今圖像中任意位置,包括人臉的座標(x,y)、角度、大小;
- 局部變形(非剛性):指不一樣人和不一樣表情之間臉部形狀的不一樣,與全局形變不一樣,人臉的高度結構化特徵對局部形變產生了極大的約束。
全局變形能夠由二維空間的函數表達,而且能夠應用於任何類型的對象;然而局部形變只針對特定目標,須要從訓練集中去學習獲得。網絡
簡單的仿射變換包括:平移,旋轉,縮放。普氏分析法是一種用來分析形狀分佈的方法。數學上來說,就是不斷迭代,尋找標準形狀(canonical shape),並利用最小二乘法尋找每一個樣本形狀到這個標準形狀的仿射變化方式。(可參照維基百科的GPA算法)
app
Procrustes analysis的做用能夠看做是一種對原始數據的預處理,目的是爲了獲取更好的局部變化模型做爲後續模型學習的基礎。以下圖所示:函數
- 每個人臉特徵點能夠用一種單獨的顏色表示;
- 通過歸一化變化,人臉的結構愈來愈明顯,即臉部特徵簇的位置愈來愈接近他們的平均位置;
- 通過一系列迭代,尺度和旋轉的歸一化操做,這些特徵簇變得更加緊湊,它們的分佈愈來愈能表達人臉表情的變化。【剔除剛性部分、保留柔性部分】
下圖爲不一樣大小、不一樣長寬比的矩形,通過歸一化過程後,各個樣本點分佈服從必定機率分佈趨勢
學習![]()
在圖1.1中,將這三種方法合併到一個式子中:
\[ \begin{align} \begin{bmatrix} u \\ v \\ \end{bmatrix} & = \begin{bmatrix} a_2 & a_1 & a_0 \\ b_2 & b_1 & b_0 \\ \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \\ \end{bmatrix} \\ & = s\begin{bmatrix} cos\theta & -sin\theta \\ sin\theta & cos\theta \\ \end{bmatrix} \begin{bmatrix} x \\ y \\ \end{bmatrix}+\begin{bmatrix} t_1 \\ t_2 \\ \end{bmatrix} \\ & = sR\begin{bmatrix} x \\ y \\ \end{bmatrix}+T \end{align} \]
上述式子中\(s\)就是縮放因子,\(\theta\)就是旋轉角度,\(t\)表示平移的量,\(R\)是一個正交矩陣\(R^TR=I\)spa
因此如今問題變成了:如何旋轉,平移和縮放第一個向量,使其儘量對齊第二個向量的點,即經過使用仿射變換將第一個圖像進行變換,而後覆蓋第二個圖像。並判斷變換後的第一個圖像與第二個圖像的距離,並使其距離最小。
假設兩個形狀矩陣分別爲\(p\),\(q\),矩陣每一行表示一個特徵點(人臉中即爲像素點)的(x,y)座標,假設有N個特徵點座標,則\(p\in R^{N \times 2}\),\(q\in R^{N \times 2}\)。對應的數學形式爲:
\[Loss = \sum_i^N||sRp_i^T+T-q_i^T||^2\]
求其最小,也就是
\[\begin{aligned} & \underset{s,R,T}{\arg min}||sRp^T+T-q^T||_F \\ & s.t. \, R^TR=I \end{aligned}\]
其中\(||\cdot||_F\)是Frobenius範數,這裏就是L2範數。
在人臉對齊:Procrustes analysis中所述,對兩邊的點集進行消除平移,消除縮放以後,旋轉的角度部分能夠變成求解下面式子:
\[\begin{aligned} & \underset{R}{\arg min}||RA-B||_F \\ & s.t. \, R^TR=I \end{aligned}\]
此時根據Ordinary_Procrustes_analysis中的Orthogonal_Procrustes_problem所述,式子解爲:
\[\begin{aligned} & M = BA^T \\ & svd(M) = U\Sigma V^T \\ & R = UV^T \\ \end{aligned}\]
那麼仿射變換矩陣爲:
如switching-eds-with-python第一部分所述:
- 將矩陣數值類型轉換成float;
- 每一個特徵點減去當前形狀的中心點,消除平移的影響(一旦找到了處理後的特徵點集和的最優縮放因子和角度,這裏的中心點c1和c2能夠用來找回完整解);
- 每一個特徵點除以當前形狀的尺度因子,消除尺度的縮放影響;
- 使用SVD計算旋轉角度;
- 返回一個仿射變換矩陣的完整矩陣。
'''a.py https://matthewearl.github.io/2015/07/28/switching-eds-with-python/ 中代碼是有問題的; 由於numpy的矩陣相乘須要numpy.dot,直接的相乘是逐元素相乘 ''' def transformation_from_points(points1, points2): '''0 - 先肯定是float數據類型 ''' points1 = points1.astype(numpy.float64) points2 = points2.astype(numpy.float64) '''1 - 消除平移的影響 ''' c1 = numpy.mean(points1, axis=0) c2 = numpy.mean(points2, axis=0) points1 -= c1 points2 -= c2 '''2 - 消除縮放的影響 ''' s1 = numpy.std(points1) s2 = numpy.std(points2) points1 /= s1 points2 /= s2 '''3 - 計算矩陣M=BA^T;對矩陣M進行SVD分解;計算獲得R ''' # ||RA-B||; M=BA^T A = points1.T # 2xN B = points2.T # 2xN M = np.dot(B, A.T) U, S, Vt = numpy.linalg.svd(M) R = np.dot(U, Vt) '''4 - 構建仿射變換矩陣 ''' s = s2/s1 sR = s*R c1 = c1.reshape(2,1) c2 = c2.reshape(2,1) T = c2 - np.dot(sR,c1) # 模板人臉的中心位置減去 須要對齊的中心位置(通過旋轉和縮放以後) trans_mat = numpy.hstack([sR,T]) # 2x3 return trans_mat
在找到對應的仿射映射矩陣後,能夠經過opencv的warpAffine進行映射。
'''上述函數實現的時候,注意模板臉和須要對其的人臉的順序 landmarks1: 檢測出來須要對齊的人臉關鍵點; landmarks2:對齊的模板人臉,即平均臉關鍵點''' trans_mat = transformation_from_points(landmarks1, landmarks2) def warp_im(in_image, trans_mat, dst_size): output_image = cv2.warpAffine(in_image, trans_mat, dst_size, # (cols, rows) borderMode=cv2.BORDER_TRANSPARENT) return output_image
假設當前圖片包含4我的臉,以下圖,
'''邊界框[x1, y1, x2, y2, score] ''' [222.22601686, 43.14613463, 280.12375677, 123.65308259, 1. ], [ 53.22718975, 30.1167623 , 105.30491075, 98.62653339, 0.99999237], [374.89622349, 44.23550975, 432.30359537, 125.07026242, 0.99998629], [497.07639685, 32.2071521 , 548.87478065, 105.17269786, 0.99970442] '''points 關鍵點[x1, x2 ... x5, y1, y2 ..y5] ''' [255.76176 , 278.4415 , 274.27048 , 255.08255 , 273.5981 , 73.90924 , 75.331924, 92.13313 , 103.375435, 105.174866], [ 82.20165 , 102.67773 , 99.4637 , 82.03625 , 100.24012 , 55.405067, 55.737637, 69.84144 , 82.090454, 81.33897 ], [389.51086 , 416.09406 , 397.90732 , 388.24335 , 408.37756 , 68.65562 , 72.8951 , 87.913956, 102.8963 , 105.9071 ], [513.3108 , 537.25714 , 525.7555 , 514.9328 , 535.16156 , 61.96994 , 61.56072 , 77.64981 , 89.027435, 88.96181 ]
這裏修改MTCNN中的對齊代碼
def extract_image_chips1( img, points, desired_size=256, padding=0): """ crop and align face Parameters: ---------- img: numpy array, input image points: numpy array, n x 10 (x1, x2 ... x5, y1, y2 ..y5) desired_size: default 256 padding: default 0 Retures: ------- crop_imgs: list, n cropped and aligned faces """ crop_imgs = [] for ind,p in enumerate(points): # 當前圖片中一共有len(points)我的臉 shape =[] for k in range(len(p)//2): shape.append(p[k]) shape.append(p[k+5]) if padding > 0: padding = padding else: padding = 0 # 平均臉(模板臉)的5個關鍵點座標 mean_face_shape_x = [0.224152, 0.75610125, 0.490127, 0.254149, 0.726104] mean_face_shape_y = [0.2119465, 0.2119465, 0.628106, 0.780233, 0.780233] from_points = [] to_points = [] for i in range(len(shape)//2): x = (padding + mean_face_shape_x[i]) / (2 * padding + 1) * desired_size y = (padding + mean_face_shape_y[i]) / (2 * padding + 1) * desired_size to_points.append([x, y]) from_points.append([shape[2*i], shape[2*i+1]]) # 構建人臉關鍵點矩陣 from_mat = np.asarray(from_points) to_mat = np.asarray(to_points) # 計算from_mat映射到to_mat的仿射變換矩陣,是一個2x3的矩陣 trans_mat = transformation_from_points(from_mat,to_mat) # 進行仿射變換,並取當前中心向外(desired_size,desired_size)大小的區域 dst_size = (desired_size,desired_size) chips = warp_im(img, trans_mat, dst_size) crop_imgs.append(chips) return crop_imgs chips = extract_image_chips1(img, points, 144, 0.37)
對應的四個旋轉矩陣爲 array([[ 1.64844171e+00, 8.79546584e-02, -3.74864686e+02], [-8.79546584e-02, 1.64844171e+00, -4.19148490e+01]]) array([[ 1.84494424e+00, -1.19245336e-02, -9.78834105e+01], [ 1.19245336e-02, 1.84494424e+00, -4.60283424e+01]]) array([[ 1.48460564e+00, 2.23927075e-01, -5.41851323e+02], [-2.23927075e-01, 1.48460564e+00, 4.27248780e+01]]) array([[ 1.79634282e+00, -3.48299747e-03, -8.71374899e+02], [ 3.48299747e-03, 1.79634282e+00, -5.51812653e+01]])
結果以下圖所示。
conference: