[数字人] 从0开始的人脸重建 (一)

发布时间 2023-08-01 11:39:00作者: aoru45

从0开始的人脸重建

本篇非教程。

数学基础

Scratchapixel
GAMES101

  1. 世界系方向:+Y up, +X left and +Z in
  2. LookatTransform(属于视图变换):

给定一个相机在世界系中的位置和要看向的点在世界系中的位置,求得将世界系原点转向相机坐标系的变换。人脸系统中,往往人脸中心处于(0,0,0)位置,因此看向的点是(0,0,0),相机就摆在(0,0,e)处,这里相当于规定了相机系的z轴指向,但另两个轴无法确定,因此还需要一个up向量描述相机的y轴,之后z轴向量和y轴向量叉积得到最后一个轴的向量表示,因此最终问题就变成了把三个原来世界系的基向量变成新的坐标系的基向量的坐标变换矩阵,由于原坐标系是单位矩阵,因此左边变换矩阵就是变换后的基向量组成的矩阵:

void lookat(const float3& from, const float3& to, const float3& up, mat4& m) 
{ 
    // forward方向是z轴负向,因此是-(to - from)
    float3 forward = from - to; 
    normalize(forward); 
    float3 right = cross(up, forward); 
    normalize(right); 
    float3 newup = cross(forward, right); 
    // 右手系
    m[0][0] = right.x,   m[0][1] = right.y,   m[0][2] = right.z; 
    m[1][0] = newup.x,   m[1][1] = newup.y,   m[1][2] = newup.z; 
    m[2][0] = forward.x, m[2][1] = forward.y, m[2][2] = forward.z; 
    m[3][0] = from.x,    m[3][1] = from.y,    m[3][2] = from.z; 
}

数据表示

原始人脸三维数据的表示

mesh:由顶点、平面组成。顶点是扫描到的所有顶点。平面由多组idx表示,表明哪些顶点构成一个平面。

纹理:每个平面对应的颜色,一般用UV图表示。

3DMM

建模:

对于原始人脸数据,可以表示为\(s=(n,m*3)^T,t=(n,m*3)^T\),其中n表示人脸数,m表示顶点数,3是坐标或者是rgb颜色,因此可以对s和t分别做PCA,找到若干个方向作为基,基矩阵为\(M\),然后把数据投影上去,变为\((u,m*3)^T\),需要注意的是,这里实际上是对人脸样本那个维度进行了降维,可以理解为所有样本点都具有某些特征来描述某个特定的人,而事实上200个不同的人就可能包含所有能够描述其他所有人的特征,因此对样本维度进行降维,若干个基的线性组合就是一个特定的人。也就是说,BFM的PCA过程是对人的分布的建模,将人与人之间那些无法直接描述的特征变成了可以由统计分布来描述的特征,所以是对人这一维度降维。其基本思想是人与人之间是线性相关的,因此可以通过某些人来线性组合生成其他人,正因为线性相关,所以也许并不需要世界上所有的人来做线性组合来描述一个特定的人,而是只需要一部分的人的线性组合来描述,继而在这里进行降维。

\[\mathbf{S}_{\text {mod }}=\sum_{i=1}^M \alpha_i \mathbf{S}_i ; \mathbf{T}_{\text {mod }}=\sum_{i=1}^M \beta_i \mathbf{T}_i \]

建模的时候,形状得到M个基,纹理得到M个基,分别确定这些基的系数就能描述一个特定的人。

此外,一些方法除了对形状和纹理进行建模,还对表情、光照进行建模。

对于表情而言,其实是对形状的细分,那么前面BFM的shape可以认为是identity shape,训练数据是不同的人,如果这些人每个人都搜集到了不同的表情,则可以加一些表情维度进去,即最终的人脸可以由identity和表情代表的维度一起决定:

认为表情是identity shape的bias,可以通过在identity shape上加上系数得到。

\[\mathbf{S}_{\text {mod }}=\sum_{i=1}^M \alpha_i \mathbf{S}_i + \sum_{i=1}^M \alpha_i \mathbf{E}_i \]

拟合:

初始化一个基本参数,由粗到细,非deep方式迭代直接拟合参数。

3DDFA(Face Alignment Across Large Poses: A 3D Solution):

3d点到图像上2d点对应:\(V(\mathbf{p})=f * \operatorname{Pr} * \mathbf{R} *\left(\overline{\mathbf{S}}+\mathbf{A}_{i d} \boldsymbol{\alpha}_{i d}+\mathbf{A}_{e x p} \boldsymbol{\alpha}_{e x p}\right)+\mathbf{t}_{2 d}\)

模型需要预测的参数为\(\left[f, \text { pitch, yaw, roll }, \mathbf{t}_{2 d}, \boldsymbol{\alpha}_{i d}, \boldsymbol{\alpha}_{e x p}\right]^T\)

f、Pr如何提前获取?一般数据集里都应该有的,因为采集3d数据的时候肯定记录了3d和其对应的2d rgb图像的对应关系,f和Pr属于内参,只和相机有关。

对于任意人脸rgb图,希望从rgb图中预测3DMM模型的参数,其思路是通过比较求解的参数和重建的点来估计其损失。

损失这一块其实可以设计的比较多,因为有3d信息,所以可以给3d点的对应损失(VDC)、最佳参数和预测参数的损失(PDC)。

之所以没有重建渲染后的损失,是因为当时似乎还不太支持可微分渲染,不然理论上应该利用可微分渲染来提升效果。

对于PDC,由于不同参数对结果的影响程度不一样(大姿态往往比小细节更重要),所以一般对重要的参数权重给多一点,改良之后为WPDC,权重这里作者采用了结合VDC的方式,先用网络预测的参数计算VDC,然后VDC取均值normalize作为姿态参数的weight,实现动态调整权重,其结果导致先着重优化姿态,再细优化细节。级连式训练直至收敛。

后续做了关键点检测,由于3DDFA已经粗略的给出了3d关键点,投影到2d平面上时可以确定哪些关键点是被遮挡的,因此只去refine那些没有遮挡的关键点。

数据:用3DMM对2D图像构建了大量的3D模型,用于训练。因此可以认为其上限是3DMM,但是速度上比3DMM直接优化要快。

3DDFA-V1.5(Face Alignment in Full Pose Range: A 3D Total Solution

Improvements:

  1. Pose Adaptive Feature

输入加了PAF,在3D模型上定义64X64个anchor,投影到2D平面得到位置,sample这些位置的像素得到一组64d X64 d的feature作为输入,d为sample的时候窗口大小。之后第一个卷积层的kernel设置为d X d以防止卷到边缘位置可能对结果的影响。

  1. 不用欧拉角用四元数

\(\mathbf{p}=\left[q_0, q_1, q_2, q_3, \mathbf{t}_{2 d}, \boldsymbol{\alpha}_{i d}, \boldsymbol{\alpha}_{e x p}\right]^{\mathrm{T}}\)

欧拉角会导致万向锁。

万向节死锁应该可以这么理解:原本是有3个轴决定向量的方向,现在第2条轴固定为90度,那么理论上还可以沿着第1和第3这2条轴做旋转,有2个自由度。但经过第2次旋转以后,第3轴和第1条轴重合了,也就是变成只剩绕1条轴旋转的效果了,不就是失去了1个自由度。

  1. Optimized Weighted Parameter Distance Cost

OWPDC相对WPDC的改进在于权重,WPDC的权重是VDC,而OWPDC则是通过一个二次优化得到的:

\[\begin{gathered}\mathbf{w}^*=\arg \min _{\mathbf{w}}\left\|V\left(\mathbf{p}^c+\operatorname{diag}(\mathbf{w}) *\left(\mathbf{p}^g-\mathbf{p}^c\right)\right)-V\left(\mathbf{p}^g\right)\right\|^2 \\+\lambda\left\|\operatorname{diag}(\mathbf{w}) *\left(\mathbf{p}^g-\mathbf{p}^c\right)\right\|^2, \\\text { s.t. } \quad \mathbf{0} \preceq \mathbf{w} \preceq \mathbf{1},\end{gathered} \]

即让要优化的loss最小的那个w,其实是想要同时考虑点对点损失和参数损失来共同决定权重,而不是只看点对点损失来决定权重,但是这个权重又不能直接说加权加个\(\lambda\)来引导,而通过这个二次优化,点对点损失如果很小,那最终参数损失的权重就会比较大,否则点对点损失的权重比较大。这一点其实有点否定了之前的想法,之前认为要先优化姿态,所以只去考虑点对点损失来生成权重,但是可能有些情况下先优化参数可能优化效果要更好,这个二次优化就提供了这种可能性。

从效果上看OWPDC 几乎没啥提升。估计是为了多找个创新点加的。。

3DDFA v2(Towards Fast, Accurate and Stable 3D Dense Face Alignment)

主要是速度上的优化和损失函数的优化,研究意义不大。

**Accurate 3D Face Reconstruction with Weakly-Supervised Learning:

From Single Image to Image Set**

奠定了可微分渲染用于人脸重建的基石,前面3DDFA还是用PNCC来利用纹理,这里开始用渲染闭环。

无需3D信息,主要是利用可微分渲染来实现自监督。大体想法是回归BFM参数后重建人脸并渲染,利用渲染后的图像和原始rgb图做损失。

损失:

  1. 损失这块由于人脸往往有遮挡,所以需要mask掉,用一个预训练的模型做粗分割实现。
  2. 2d图像一般有landmark标注,也可做投影后的损失。
  3. 利用人脸识别网络的deep feature,将重建后的图片的feature和重建前的图的feature做cos loss。

C-Net预测\(\alpha\)的score,乘上去再优化。

可微渲染:https://github.com/sicxu/Deep3DFaceRecon_pytorch/blob/master/util/nvdiffrast.py

FLAME(Learning a model of facial shape and expression from 4D scans)

3DDFA之后,研究界提出了更大体量的人脸模型FLAME,之后的工作往往基于FLAME。

相比于BFM,提供了更大的数据集构建的模型,主要区别如下:

  1. FLAME对一些可旋转部位进行的建模,称之为pose,而BFM未对此进行建模,而是对整个人脸进行旋转表示。
  2. FLAME是对整个人头进行的建模,BFM主要针对人脸。
  3. FLAME使用的数据量更大,因此模型的精度要更高,可以理解为上限更高。
  4. FLAME缺乏纹理,BFM有纹理数据。

表情和shape前面已经探讨过,pose这里是采用了SMPL: A Skinned Multi-Person Linear Model这篇

SMPL: A Skinned Multi-Person Linear Model

文章的方法进行建模和训练,在SMPL中是这样表示的:

由于SMPL是人体模型,因此可旋转的点定义了23个+1个全局点,其清楚地给出了旋转的公式:

\[\begin{aligned}\overline{\mathbf{t}}_i^{\prime} & =\sum_{k=1}^K w_{k, i} G_k^{\prime}(\vec{\theta}, \mathbf{J}) \overline{\mathbf{t}}_i \\G_k^{\prime}(\vec{\theta}, \mathbf{J}) & =G_k(\vec{\theta}, \mathbf{J}) G_k\left(\vec{\theta}^*, \mathbf{J}\right)^{-1} \\G_k(\vec{\theta}, \mathbf{J}) & =\prod_{j \in A(k)}\left[\begin{array}{c|c}\exp \left(\vec{\omega}_j\right) & \mathbf{j}_j-\mathbf{j}_{A(j)} \\\hline \overrightarrow{0} & 1\end{array}\right]\end{aligned} \]

其中\(t_i\)就是旋转前后的一个顶点,在世界坐标系中进行表示,\(w\)则是影响程度,下面的累乘\((G_k(\vec{\theta}, \mathbf{J}))\)是前向运动学公式,即给定每个子节点相对父节点的旋转和在父节点下的坐标,得到任意节点相对根节点坐标系的位置(变换矩阵),第二个公式表达的意思是先将这一点的世界坐标转换到第k个joint的坐标系下,即原始角度表示下的逆矩阵,然后利用前向运动学公式求出参数\(\theta\)时的位置。

这样的pose建模虽然简单但是和shape、expression的建模并没有一致性(基的线性和),于是对pose的旋转进行一致性建模:

\[\overline{\mathbf{t}}_i^{\prime}=\sum_{k=1}^K w_{k, i} G_k^{\prime}(\vec{\theta}, J(\vec{\beta}))\left(\overline{\mathbf{t}}_i+\mathbf{b}_{S, i}(\vec{\beta})+\mathbf{b}_{P, i}(\vec{\theta})\right) \]

这样就建模成了线性和的形式。可以看到\(\theta\)在两个地方出现,后者更多的是描述不同人的“体态”的pose,所以出现在blend pose里,这里是认为pose本身也应该作为组成人的一个基,就好像有些人本身就驼背一样,这应该属于这类人的特性;而前者才描述运动pose,此外,由于shape的不同会导致joint的位置不同,而在旋转的时候是需要joint的坐标的,因此\(J\)的作用是得到joint的坐标。

值得注意的是,shape基是在pose normalize之后通过PCA得到的,而pose的基则是利用multi-pose的dataset训练得到,为啥可以训练呢,因为基的坐标咱可以人为定义,相当于数据集中有标注,具体定义为和rest pose的残差:

\[B_P(\vec{\theta} ; \mathcal{P})=\sum_{n=1}^{9 K}\left(R_n(\vec{\theta})-R_n\left(\vec{\theta}^*\right)\right) \mathbf{P}_n \]

其中系数为\(\theta\)下轴角表示的变换阵与rest pose的差,\(P_n\)就是其对应的基,其中\(w\)\(P_n\)都是训练来的,训练是拿有标注的3D数据集进行registration学习这些参数,\(P_n\)加了正则项惩罚,这块其实等价于autoencoder,以得到基。

关于code实现中

G = G - self.pack(
      np.matmul(
        G,
        np.hstack([self.J, np.zeros([24, 1])]).reshape([24, 4, 1])
        )
      )

看到有些人问,想了下,应该是一个坐标系的转换,转到joint系下再做变换,由于根节点是相对于世界系的变换,所以最终运动链的变换会直接变为世界系下的坐标,所以并不需要转回去。

\[\begin{bmatrix}R_{11}&R_{12}&R_{13}&d_1\\R_{21}&R_{22}&R_{23}&d_2\\R_{31}&R_{31}&R_{31}&d_3\\0&0&0&1 \end{bmatrix}\begin{bmatrix}1&0&0&-j_1\\0&1&0&-j_2\\0&0&1&-j_3\\0&0&0&1 \end{bmatrix}=\begin{bmatrix}R_{11}&R_{12}&R_{13}&d_1-R_{11}j_1-R_{12}j_2-R_{13}j_3\\R_{21}&R_{22}&R_{23}&d_2-R_{21}j_1-R_{22}j_2-R_{23}j_3\\R_{31}&R_{31}&R_{31}&d_3-R_{31}j_1-R_{32}j_2-R_{33}j_3\\0&0&0&1 \end{bmatrix} \]

\(w\)控制的是joint对某个顶点的影响程度,也是通过训练得到的,那如何保证哪些顶点受哪些joint影响呢?

先通过segmentation分区域,然后让\(w\)去拟合这个结果。

至此SMPL中的pose搞清楚了,再看看人脸中的pose。

同样的,定义了4个joint

基于对SMPL的理解,FLAME的pose也是一样的建模方式加上个expression,只是关节链没有在论文中给出,下载数据集之后看parents是:

[-1          0          1          1          1]

其中0为根节点。