face alignment[Ordinary Procrustes Analysis]
人臉識別,大致可以分為以下四個步驟:
- 人臉檢測:從圖片中準確定位到人臉,並以矩形框將其裁剪出來;
- 人臉矯正(對齊): 檢測到的人臉,可能角度不是很正,需要使其對齊,比如旋轉,縮放;
- 特徵提取:對矯正後的人臉進行特徵提取,現在做法通常都是基於一個CNN模型;
- 人臉比對:對兩張人臉影象提取的特徵向量進行對比,計算相似度。
上述第一步,目前主流的做法都來自如Faster RCNN或者SSD等通用目標檢測的一些改進網路,大致可以直接將人臉檢測就看成特定目標的檢測;這裡主要介紹人臉校正部分,且介紹其中一種方法,也是MTCNN使用的方法,該方法簡單快速,不需要去先建立3D模型然後進行對映。當然該方法效果麼,從其原理上,主要解決的依然是正臉的對齊,無法很好解決側臉的對齊(這時候可以用GAN或者3D建模去恢復)。
0 引言
MTCNN中採用的人臉矯正方法,是假設拿到的人臉幾乎都是正臉,不過此時正臉有尺度不等,角度偏移等。而且需要預先設定一個平均臉,即目標臉的位置,標記出平均臉的所有關鍵點應該處於的位置,然後將所有人臉矯正到該平均臉上。
1 Procrustes Analysis普氏分析法
下述概念部分參考自《Master Opencv...讀書筆記》非剛性人臉跟蹤 II。
人臉由眼睛、鼻子、嘴巴、下巴等部位構成,正是因為這些部位形狀、大小和相對位置的各種變化,才使得人臉表情千差萬別,因此可以對這些部位的形狀和結構關係進行幾何描述,作為人臉表情識別的重要特徵。這裡,幾何關係就是指預定義點集的空間組態模式,而這些點與臉部器官在幾何空間存在對應關係(比如眼角、鼻尖、眉毛)。
Facial geometry,通過兩種元素的引數化配置組成:
- 全域性變形(剛性):指人臉在影象中的分佈,允許人臉出現在影象中任意位置,包括人臉的座標(x,y)、角度、大小;
- 區域性變形(非剛性):指不同人和不同表情之間臉部形狀的不同,與全域性形變不同,人臉的高度結構化特徵對區域性形變產生了極大的約束。
全域性變形可以由二維空間的函式表達,並且可以應用於任何型別的物件;然而區域性形變只針對特定目標,需要從訓練集中去學習得到。
簡單的仿射變換包括:平移,旋轉,縮放。普氏分析法是一種用來分析形狀分佈的方法。數學上來講,就是不斷迭代,尋找標準形狀(canonical shape),並利用最小二乘法尋找每個樣本形狀到這個標準形狀的仿射變化方式。(可參照維基百科的GPA演算法)
如上圖所示,我們 假設一張2D圖片上每個點都為座標\((x,y)\),我們對座標上表示的值不感興趣,我們只是對其座標位置感興趣。這裡表現的就是原始目標上每個畫素點的仿射對映。
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\)
所以現在問題變成了:如何旋轉,平移和縮放第一個向量,使其儘可能對齊第二個向量的點,即通過使用仿射變換將第一個影象進行變換,然後覆蓋第二個影象。並判斷變換後的第一個影象與第二個影象的距離,並使其距離最小。
假設兩個形狀矩陣分別為\(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}\]
那麼仿射變換矩陣為:
2 程式碼
如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)
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
3 例子
假設當前圖片包含4個人臉,如下圖,
通過 mxnet_mtcnn_face_detection進行檢測得到對應的4個人臉邊界框和對應的5個關鍵點
'''邊界框[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: