Face3D学习笔记(5)3DMM示例源码解析【中下】从二维图片的特征点重建三维模型——黄金标准算法
寫在前面
- 為了保證整個示例項目更加直觀,方便理解,在展示一些函數(shù)的源碼時會使用numpy版本進(jìn)行展示,而在示例程序中并未使用numpy版本的庫,在Cython版本與numpy版本出現(xiàn)差異的原碼前會有標(biāo)注,希望讀者留意。
- 3DMM實例程序的jupyter版本后續(xù)會更新,完全免費(fèi),歡迎大家下載
源碼解析
在上一篇文章了解3DMM模型以及用隨機(jī)的形狀系數(shù)和表情系數(shù)生成面部網(wǎng)格進(jìn)行3DMM模型的前向重建過程后進(jìn)入例程的后半部分—— 由2D圖像點和對應(yīng)的3D頂點索引得到新的參數(shù)進(jìn)而從二維圖片進(jìn)行三維人臉的重建。
理論部分
理論這部分借鑒了大佬的文章和一些論文。
從上篇文章我們了解了3DMM模型的公式:
通過一張單張正臉照片,首先利用人臉對齊算法計算得到目標(biāo)二維人臉的68個特征點坐標(biāo)xix_ixi?,在BFM模型中有對應(yīng)的68個特征點XiX_iXi?,投影后忽略第三維,則特征點之間的對應(yīng)關(guān)系如下:
根據(jù)這些信息求出α,β\alpha, \betaα,β系數(shù),將平均臉模型與照片中的臉部進(jìn)行擬合,即:
因此,三維人臉重建問題再次轉(zhuǎn)化為求解系數(shù)(α,β\alpha,\betaα,β)以滿足下列能量方程的問題:
人臉模型的三維點以及對應(yīng)照片中的二維點存在映射關(guān)系,這個可以由一個3x4的仿射矩陣PPP進(jìn)行表示。即:X=PA?X3dX=P_A\cdot X_{3d}X=PA??X3d?。
黃金標(biāo)準(zhǔn)算法
要計算出仿射矩陣,代碼中使用了黃金標(biāo)準(zhǔn)算法(Gold Standard algorithm)
算法如下:
目標(biāo)為給定n>=4組3維(XiX_iXi?)到2維(xix_ixi??)的圖像點對應(yīng),確定仿射攝像機(jī)投影矩陣的最大似然估計。
- 歸一化,對于二維點(xix_ixi??),計算一個相似變換TTT,使得 xˉ=Txi\bar{x} =Tx_ixˉ=Txi??,同樣的對于三維點,計算Xˉ=UXi\bar{X}=UX_iXˉ=UXi??
- 對于每組對應(yīng)點 xix_ixi?~XiX_iXi??,都有形如 Ax=bA x = bAx=b 的對應(yīng)關(guān)系存在
- 求出A的偽逆
- 去掉歸一化,得到仿射矩陣
在Face3d中的求解過程
過程可以概述如下:
(1)初始化α,β\alpha,\betaα,β為0;
(2)利用黃金標(biāo)準(zhǔn)算法得到一個仿射矩陣PAP_APA?,分解得到s,R,t2ds,R,t_{2d}s,R,t2d?;
(3)將(2)中求出的s,R,t2ds,R,t_{2d}s,R,t2d?帶入能量方程,解得α\alphaα;
(4)將(2)和(3)中求出的α\alphaα代入能量方程,解得β\betaβ;
(5)更新α,β\alpha,\betaα,β的值,重復(fù)(2)-(4)進(jìn)行迭代更新。
代碼部分
下面將從Face3D的例程到源碼一步步進(jìn)行講解:
例程部分
x = projected_vertices[bfm.kpt_ind, :2] # 2d keypoint, which can be detected from image X_ind = bfm.kpt_ind # index of keypoints in 3DMM. fixed.# fit fitted_sp, fitted_ep, fitted_s, fitted_angles, fitted_t = bfm.fit(x, X_ind, max_iter = 3)# verify fitted parameters fitted_vertices = bfm.generate_vertices(fitted_sp, fitted_ep) transformed_vertices = bfm.transform(fitted_vertices, fitted_s, fitted_angles, fitted_t)image_vertices = mesh.transform.to_image(transformed_vertices, h, w) fitted_image = mesh.render.render_colors(image_vertices, bfm.triangles, colors, h, w)x就是公式中的二維特征點XXX,例程里面給的是上篇文章生成二維圖像時導(dǎo)出的二維數(shù)據(jù)。
X_ind是BFM模型三維特征點的索引,并非坐標(biāo)。
然后執(zhí)行了
fitted_sp, fitted_ep, fitted_s, fitted_angles, fitted_t = bfm.fit(x, X_ind, max_iter = 3
其中bfm.fit部分的源碼如下:
標(biāo)簽isShow給的是默認(rèn)的False所以執(zhí)行的else部分,里面執(zhí)行了模型擬合部分代碼:
fitted_sp, fitted_ep, s, R, t = fit.fit_points(x, X_ind, self.model, n_sp = self.n_shape_para, n_ep = self.n_exp_para, max_iter = max_iter)
以及生成旋轉(zhuǎn)矩陣代碼:
angles = mesh.transform.matrix2angle(R)
其中模型擬合部分的fit.fit_points部分的源碼如下:
fit.fit_points部分拆分講解
(1)初始化α,β\alpha,\betaα,β為0
x = x.copy().T#-- initsp = np.zeros((n_sp, 1), dtype = np.float32)ep = np.zeros((n_ep, 1), dtype = np.float32)x取轉(zhuǎn)置,格式變?yōu)?#xff08;2,68)
sp即α\alphaα,ep即β\betaβ。將它們賦值為格式(199,1)的零向量。
由于BFM模型中的頂點坐標(biāo)儲存格式為{x1x_1x1?,y1y_1y1?,z1z_1z1?,x2x_2x2?,y2y_2y2?,z2z_2z2?,x3x_3x3?,y3y_3y3?,…}
而在X_ind中只給出了三位特征點坐標(biāo)的位置,所以應(yīng)該根據(jù)X_ind獲取X3dX_{3d}X3d?的XYZ坐標(biāo)數(shù)據(jù)。
X_ind數(shù)據(jù)如下,是一個(68,1)的位置數(shù)據(jù)。
X_ind_all = np.tile(X_ind[np.newaxis, :], [3, 1])*3
X_ind_all拓展為(3,68)并乘3來定位到坐標(biāo)位置:
X_ind_all[1, :] += 1
X_ind_all[2, :] += 2
再將第二行加一、第三行加二來對于Y坐標(biāo)和Z坐標(biāo)。
然后將它們合并
valid_ind = X_ind_all.flatten('F')
flatten是numpy.ndarray.flatten的一個函數(shù),即返回一個折疊成一維的數(shù)組。但是該函數(shù)只能適用于numpy對象,即array或者mat,普通的list列表是不行的。
'F’表示以列優(yōu)先展開。
合并后的結(jié)果valid_ind如下圖:
通過合并后的valid_ind得到對應(yīng)特征點的人臉形狀、形狀主成分、表情主成分這三種數(shù)據(jù)。
shapeMU = model['shapeMU'][valid_ind, :]
shapePC = model['shapePC'][valid_ind, :n_sp]
expPC = model['expPC'][valid_ind, :n_ep]
人臉形狀shapeMU數(shù)據(jù)格式(68*3,1)
形狀主成分shapePC數(shù)據(jù)格式(68*3,199)
表情主成分expPC數(shù)據(jù)格式(68*3,29)
for i in range(max_iter):X = shapeMU + shapePC.dot(sp) + expPC.dot(ep)X = np.reshape(X, [int(len(X)/3), 3]).T#----- estimate poseP = mesh.transform.estimate_affine_matrix_3d22d(X.T, x.T)s, R, t = mesh.transform.P2sRt(P)rx, ry, rz = mesh.transform.matrix2angle(R)# print('Iter:{}; estimated pose: s {}, rx {}, ry {}, rz {}, t1 {}, t2 {}'.format(i, s, rx, ry, rz, t[0], t[1]))#----- estimate shape# expressionshape = shapePC.dot(sp)shape = np.reshape(shape, [int(len(shape)/3), 3]).Tep = estimate_expression(x, shapeMU, expPC, model['expEV'][:n_ep,:], shape, s, R, t[:2], lamb = 0.002)# shapeexpression = expPC.dot(ep)expression = np.reshape(expression, [int(len(expression)/3), 3]).Tsp = estimate_shape(x, shapeMU, shapePC, model['shapeEV'][:n_sp,:], expression, s, R, t[:2], lamb = 0.004)return sp, ep, s, R, t循環(huán)中的max_iter是自行定義的迭代次數(shù),這里的輸入為4。
X = shapeMU + shapePC.dot(sp) + expPC.dot(ep)
X = np.reshape(X, [int(len(X)/3), 3]).T
這里的XXX就是經(jīng)過如下的運(yùn)算的SnewmodelS_{newmodel}Snewmodel?,就是新的X3dX_{3d}X3d?。
真正重點的是mesh.transform.estimate_affine_matrix_3d22d(X.T, x.T),這是網(wǎng)格的擬合部分。
源碼如下:
下面對這部分進(jìn)行詳細(xì)解讀。
(2) 利用黃金標(biāo)準(zhǔn)算法得到一個仿射矩陣PAP_APA?,分解得到s,R,t2ds,R,t_{2d}s,R,t2d?;
estimate_affine_matrix_3d22d部分即黃金標(biāo)準(zhǔn)算法具體過程
a) 歸一化
對于二維點XXX,計算一個相似變換TTT,使得Xˉ=TX\bar{X}=TXXˉ=TX,同樣的對于三維點X3dX_{3d}X3d?,計算 Xˉ3d=UX3d\bar{X}_{3d}=UX_{3d}Xˉ3d?=UX3d?。
歸一化部分的概念在Multiple View Geometry in Computer Vision一書中描述如下:
所以歸一化可以概述為以下三步:
下面結(jié)合代碼進(jìn)行講解:
輸入檢測,確保輸入的二維和三維特征點的數(shù)目一致以及特征點數(shù)目大于4。
二維數(shù)據(jù)歸一化:
#--- 1. normalization# 2d pointsmean = np.mean(x, 1) # (2,)x = x - np.tile(mean[:, np.newaxis], [1, n])average_norm = np.mean(np.sqrt(np.sum(x**2, 0)))scale = np.sqrt(2) / average_normx = scale * xT = np.zeros((3,3), dtype = np.float32)T[0, 0] = T[1, 1] = scaleT[:2, 2] = -mean*scaleT[2, 2] = 1經(jīng)過x=x.T后x的格式變?yōu)?#xff08;2,68)
通過mean = np.mean(x, 1)獲取x的X坐標(biāo)和Y坐標(biāo)平均值mean,格式為(2,)
這一步x = x - np.tile(mean[:, np.newaxis], [1, n])
x的所有XY坐標(biāo)都減去剛剛算出的平均值,此時x中的坐標(biāo)點被平移到了質(zhì)心位于原點的位置。
average_norm = np.mean(np.sqrt(np.sum(x**2, 0)))
算出所有此時所有二維點到原點的平均距離average_norm,這是一個數(shù)值。
scale = np.sqrt(2) / average_norm
x = scale * x
算出scale再用scale去乘x坐標(biāo),相當(dāng)與x所有的坐標(biāo)除以當(dāng)前的平均距離之后乘以2\sqrt{2}2?。
這樣算出來的所有點到原點的平均距離就被縮放到了2\sqrt{2}2?。
T = np.zeros((3,3), dtype = np.float32)
T[0, 0] = T[1, 1] = scale
T[:2, 2] = -mean*scale
T[2, 2] = 1
三位歸一化的原理與二維相似,區(qū)別就是所有點到原點的平均距離要被縮放到3\sqrt{3}3?,以及生成的相似變換矩陣UUU格式為(4,4)。這里不贅述了。
b) 對于每組對應(yīng)點 xix_ixi?~XiX_iXi??,都有形如 Ax=bA x = bAx=b 的對應(yīng)關(guān)系存在
# --- 2. equationsA = np.zeros((n*2, 8), dtype = np.float32);X_homo = np.vstack((X, np.ones((1, n)))).TA[:n, :4] = X_homoA[n:, 4:] = X_homob = np.reshape(x, [-1, 1])這里結(jié)合下面的公式來看:
A對應(yīng)其中的[XˉiT0T0TXiT]\left [\begin{array}{l} \bar{X}_i^T & 0^T\\0^T & {X}_i^T\end{array}\right ][XˉiT?0T?0TXiT??]
b是展開為(68*2,1)格式的x。
c) 求出A的偽逆
# --- 3. solutionp_8 = np.linalg.pinv(A).dot(b)P = np.zeros((3, 4), dtype = np.float32)P[0, :] = p_8[:4, 0]P[1, :] = p_8[4:, 0]P[-1, -1] = 1關(guān)于A的偽逆的概念和求取方法可以參照Multiple View Geometry in Computer Vision書中的P590以后的內(nèi)容。這里A的偽逆是利用numpy里面的函數(shù)np.linalg.pinv直接計算出來的,非常方便。
d)去掉歸一化,得到仿射矩陣
# --- 4. denormalizationP_Affine = np.linalg.inv(T).dot(P.dot(U))return P_Affine這部分的代碼參照公式:
以上四步就是黃金標(biāo)準(zhǔn)算法的完整過程
得到的PAffineP_{Affine}PAffine?就是式中的PAP_APA?,到這里,我們通過黃金標(biāo)準(zhǔn)算法得到了X=PA?X3dX=P_A\cdot X_{3d}X=PA??X3d?中的PAP_APA??。
將仿射矩陣RAR_ARA?分解得到s,R,t2ds,R,t_{2d}s,R,t2d?
s, R, t = mesh.transform.P2sRt(P) rx, ry, rz = mesh.transform.matrix2angle(R)其中mesh.transform.P2sRt部分的源碼如下:
def P2sRt(P):''' decompositing camera matrix PArgs: P: (3, 4). Affine Camera Matrix.Returns:s: scale factor.R: (3, 3). rotation matrix.t: (3,). translation. '''t = P[:, 3]R1 = P[0:1, :3]R2 = P[1:2, :3]s = (np.linalg.norm(R1) + np.linalg.norm(R2))/2.0r1 = R1/np.linalg.norm(R1)r2 = R2/np.linalg.norm(R2)r3 = np.cross(r1, r2)R = np.concatenate((r1, r2, r3), 0)return s, R, t這部分就是將仿射矩陣RA{R_A}RA?分解為下圖的縮放比例s、旋轉(zhuǎn)矩陣R以及平移矩陣t。
這部分代碼比較簡單,讀者可以自行理解。
篇幅原因,這邊只給出(1)(2)的源碼解析部分,求解α,β\alpha,\betaα,β的過程將在下篇文章講解。
總結(jié)
以上是生活随笔為你收集整理的Face3D学习笔记(5)3DMM示例源码解析【中下】从二维图片的特征点重建三维模型——黄金标准算法的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: html中post和get区别
- 下一篇: windows phone 7 中文天气