目前, 主流的手部姿态估计的技术方案是使用马普所于2017年提出的MANO参数化模型, 在此基础上回归3D坐标, 这是因为MANO有很合理的结构以及定义好的前向动力学树。本文的目的在于为大家介绍,基于MANO的手部姿态估计的流程:包括并不限于: 数据处理MANO的推理流程(与论文对齐)手的解剖学和生物学特点

1. 什么是手部姿态估计?

我理解的 手部姿态估计(hand pose estimation) 是:
同人体姿态估计一样, 是给定一张手部特写图片,估计其姿态(2D/3D keypoint)的位置(通常是21个).

下图是一个最经典的实现(无参数化模型):

将一个手部特写图片(hand image)输入Encoder-Decoder结构中,从Decoder输出的特征图中,选取响应最大的位置,与ground truth生成2D landmark的热力图计算loss,目的是让Decoder生成的特征图在手部的不同位置有对应的响应,即从point of interest (PoI)变为构造一种regional of interest (RoI)的表示, 即让网络从关注某个确定的点,到关注一个范围, 以此扩大模型的泛化能力以及对复杂情况下的估计鲁棒性.

这种方法在参数化模型MANO/SMPL出现之前,是姿态估计领域的主流叙事.
在这里插入图片描述

此图来自2018年,Olha CHERNYTSKA(毕业于乌克兰天主教大学)的硕士毕业论文[3].

其中3D keypoint的坐标一般用相对坐标系表示, 对手部来说,一般会选择手腕(下图的0)/食指和手掌连接关节(下图5)作为局部坐标系的原点(0, 0, 0). 这种做法也叫做root-relative.

在这里插入图片描述

下图是以上图的节点5为原点的相对3D坐标表示,以下图为例, 我们想让节点5的坐标为(0, 0, 0),在实践中,大家的做法一般很简单(伪代码):

img = cv2.imread("xxx.jpg")  # 1) 读取手部特写图片
img = normalize(img)         # 2) 对图片进行处理(规则化等)
pred_3d = Net(img)           # 3) 送入网络进行预测
pred_3d -= pred_3d[5]        # 4) 将预测结果变为root-relative的方式.

在这里插入图片描述

红框就是节点5,可见其坐标为(0, 0, 0).

2. MANO是什么?

近年来, CVPR, ECCV, ICCV的手部姿态估计论文,基本或多或少的都是model-based, 即基于参数化模型的方案。而其中,最主流的参数化模型就是Javier Romero, Dimitrios Tzionas, Michael J. Black于2017年发表于Siggraph Asia的《Embodied Hands: Modeling and Capturing Hands and Bodies Together[1].

这篇文章也是在马普所和工业光魔联合提出的《SMPL: A Skinned Multi-Person Linear Model(2015)[2]的基础上,提出了针对手部的参数化模型,其主要目的是:

To cope with low-resolution, occlusion, and noise, we develop a
new model called MANO (hand Model with Articulated and Non-rigid defOrmations).
在这里插入图片描述 图来自[1].

我的个人感受是,由于手势姿态估计的问题是:

  • 自遮挡: 以下图为例,中指到小指都被手背挡住了.
    在这里插入图片描述

  • 手抓握物体导致的遮挡:
    在这里插入图片描述

  • 分辨率低: 手部在整个构图中的所占像素比例非常小,对正确估计其手势增加了难度.

确实,在全身/半身的时候, 手势相比整体来说所占的像素非常小 ,所以 难
以分辨其动作 MANO这个模型,相当于在图片 ->3D pose中间加了一个过渡表示(或者说加上了强经验prior,从而能够预测遮挡和低分辨率的图像).
在这里插入图片描述

图来自Frankmocap仓库[4]

用不带先验信息的方法,即model-free的方法,对于这些情况的姿态估计效果通常就会失败。而自从有了参数化模型MANO,由于MANO是由 “1000 high-resolution 3D scans of hands of 31 subjects in a wide variety of hand poses.” 得到,其中包括抓握,不良光照等场景。

所以,根据18年以来的众多手部姿态分析工作表明:使用MANO参数化模型,对于估计出一个合理且准确的手部姿态,有至关重要的作用

需要注意的是,由于手部是分段刚性的(articulated objects), 因此手部的建模还是有一定难度的,马普所用了来自美国亚特兰大的3dMD公司的扫描设备,估计花费不菲…, 至于MANO是怎么制作的,这里我不详细展开,有兴趣的朋友可以直接在文章下面问。

在这里插入图片描述

图来自中科大刘利刚老师的games102课程[5].

那么,MANO作为一个 3D参数化模型,其参数都有哪些呢?

  • 778个vertices 1538个faces,并根据 16个 关键点 +从顶点中获取 5个
    手指指尖的点, 构成完整的手部链条,或者叫做前向动力学树 (forward kinematic tree).
    下图来自牛津大学的CVPR2019论文[7]《3D Hand Shape and Pose from Images in the Wild》.
    在这里插入图片描述

(即图中的 ,16,17,18,19,20都是由变形后的 vertex中按照规定规则取到的 , 代码. 以节点9为例,其3D坐标可以在变形后的Mesh上,根据其所在顶点(vertex)去提取。在这里插入图片描述

3. 手势3D pose estimation的MANO部分处理逻辑分析重点

如下图所示, 手部的crop图片送入神经网络,预测得到 MANO所需的61个参数的值,其中包含 MANO所需的相机参数 (前 3个 ), θ(3-51), β(51:61),其中θ是MANO中用于控制pose的参数,β则是 MANO中用于控制shape的参数。
在这里插入图片描述

其中,神经网络的设计就比较简单,只要按照输入和输出的结构设计就行, 最经典的实现方案是UCB和马普所合作的CVPR2018论文: HMR[6], 其使用了auto-regressive的方式去优化预测的MANO所需的61个系数.

  • 输入(bs,3,224,224)
    为图像处理后得到的Tensor(NCHW,图像的分辨率可以按照自己的需求调整), bs是batch size.

  • 输出为(bs, 61)
    得到MANO所需的参数, 这些参数输入MANO, 我们就可以得到3D的pose estimation结果(相对坐标系下的21个关键点的xyz位置).

3.1 MANO的计算逻辑

下图来自CVPR2020的《Minimal-Hand》[8], 其把MANO的流程简单用两个公式概况了:
在这里插入图片描述

  • T ( β , θ ) = T ‾ + B s ( β ) + B p ( θ ) T(\beta, \theta) = \overline{T} + B_s(\beta) + B_p(\theta) T(β,θ)=T+Bs(β)+Bp(θ)
    此公式是将shape的参数 β ∈ R 10 \beta \in R^{10} βR10 和pose的参数 θ ∈ R 48 \theta \in R^{48} θR48进行变形,变形的实现通过 B s / p B_{s/p} Bs/p实现。 T ‾ \overline{T} T表示一个标准的3D 手部mesh, 下图就是MANO的hand_mean T ‾ \overline{T} T:

可以看到, MANO的 T ‾ \overline{T} T是一个手掌摊平的姿势, 在动画领域,一般称为T型姿势(T-pose).
在这里插入图片描述
变形示意图:
在这里插入图片描述
在这里插入图片描述

  • M ( β , θ ) = W ( T ( θ , β ) , θ , β , W , J ( θ ) ) M(\beta, \theta) = \mathbf{W}(T(\theta, \beta), \theta, \beta, W, J(\theta)) M(β,θ)=W(T(θ,β),θ,β,W,J(θ))
    通过第①步,我们得到变形后的mesh: T ( θ , β ) T(\theta, \beta) T(θ,β), 第②步的目的就是进行蒙皮操作(linear blend skinning), W W W蒙皮权重 J ( θ ) J(\theta) J(θ)节点的位置.
    在这里插入图片描述
3.2 MANO的实际计算流程

好了,在了解了MANO的基本流程后,让我们回归正题,本篇文章的目的 是分析 MANO究竟是如何处理cam, shape, pose的参数,来得到 (21, 3)的 joint rotation(axis-angle表示)的?

这里以下面的的rot_pose_beta_to_mesh函数为核心进行分析, 其中, rot_pose_beta_to_mesh接收3个入参, 拼起来正好是网络预测出来的61个参数:

  • rots ∈ R 3 \in R^{3} R3
    root节点(手腕CMC)的旋转axis-angle.
  • poses ∈ R 45 \in R^{45} R45
    除每个手指指尖外(除TIP这一排)和手腕外,所有的关键点的axis-angle (15*3=45), 下图来自ECCV2020 BMC[9]
    在这里插入图片描述

手部的骨骼分为灰色的手腕(CMC), 黄色的MCP(手掌和各个手指的交界处), 绿色的PIP, 蓝色的DIP以及表示指尖位置的骨骼TIP。 可见,骨骼是“横着”来命名的…

  • betas ∈ R 10 \in R^{10} R10
    mano所需要的shape参数
3.2.1 MANO的一些参与计算的重要参数
  • kintree_table, parent 等动力学与继承关系参数
    如下图所示,parent和 kintree_table组建了手势的链条
    在这里插入图片描述
    注意,kintree_table是 不包含指尖 的 !!(只有 16个 joints 15个手指的 joints+1个 wrist joint),也符合 MANO定义的结构形式,即没有上图的(4,8,12,16,20)这5个点.

  • hands_mean: 和 mesh_mu类似,这里的 hands_mean应该
    是rest的手的axis-angle, 其用法是用来加上网络预测的 pose (axis-angle),再对其进行处理。
    需要注意的是,这里的pose除了不包含TIP骨骼外,还不包括手腕.
    在这里插入图片描述

  • mesh_mu: 下图 (MANO2017[1])的公式 2的 T ‾ \overline{T} T. 即平均 shape。
    在这里插入图片描述
    公式2的计算得到的 T p T_p Tp对应代码中的 v p o s e d v_{posed} vposed:
    在这里插入图片描述

  • mesh_pca: 对应最大的 shape特征值的特征向量 , 因为 shape是根据pca取最大的 10维参数作为 shape的,这里的 mesh_pca根据其用法,是公式 4(MANO2017)里面的Sn。
    在这里插入图片描述
    对应代码:
    在这里插入图片描述

  • J_regressor: 对应 (SMPL2015)的公式10, 其目的是: 将mesh上的顶点 (vertices)变为节点 (joints).
    在这里插入图片描述

  • root_rot: 根节点, 是指手腕的那个点 即下图的 0点 ,我理解这里应该是用这种方式来计算相对的距离。
    在这里插入图片描述

  • posedirs: 根据之前的分析, posedirs就是MANO(2017)中公式3中的Pn, 表示pose的blend shape参数.
    在这里插入图片描述

  • weights: weights就是3.1中公式2 M ( β , θ ) = W ( T ( θ , β ) , θ , β , W , J ( θ ) ) M(\beta, \theta) = \mathbf{W}(T(\theta, \beta), \theta, \beta, W, J(\theta)) M(β,θ)=W(T(θ,β),θ,β,W,J(θ)) W W W.

3.2.2 旋转矩阵计算(Rodrigues)

这里主要分析的是rodrigues函数,此函数的目的是把轴向角(axis-angle)变为旋转矩阵(rotation matrix)[10].
在这里插入图片描述
下面代码里的n等于下图(SMPL2015)的unit norm axis of rotation w ‾ \overline{w} w, R

R = I3 + torch.sin(theta).view(-1, 1, 1) * Sn \
    + (1. - torch.cos(theta).view(-1, 1, 1)) * torch.matmul(Sn, Sn)

等于SMPL2015的公式 (1), S(n_)函数对应的就是上面的Skew(V).
在这里插入图片描述

def rodrigues(r):
    theta = torch.sqrt(torch.sum(torch.pow(r, 2), 1))

    def S(n_):
        ns = torch.split(n_, 1, 1)
        Sn_ = torch.cat([torch.zeros_like(ns[0]), -ns[2], ns[1], ns[2], torch.zeros_like(ns[0]), -ns[0], -ns[1], ns[0],
                         torch.zeros_like(ns[0])], 1)
        Sn_ = Sn_.view(-1, 3, 3)
        return Sn_

    n = r / (theta.view(-1, 1))
    Sn = S(n)

    # R = torch.eye(3).unsqueeze(0) + torch.sin(theta).view(-1, 1, 1)*Sn\
    #        +(1.-torch.cos(theta).view(-1, 1, 1)) * torch.matmul(Sn,Sn)

    I3 = Variable(torch.eye(3).unsqueeze(0).cuda())
	
	# R等于 公式 (1)---SMPL
    R = I3 + torch.sin(theta).view(-1, 1, 1) * Sn \
        + (1. - torch.cos(theta).view(-1, 1, 1)) * torch.matmul(Sn, Sn)

    Sr = S(r)
    theta2 = theta ** 2
    R2 = I3 + (1. - theta2.view(-1, 1, 1) / 6.) * Sr \
         + (.5 - theta2.view(-1, 1, 1) / 24.) * torch.matmul(Sr, Sr)

    idx = np.argwhere((theta < 1e-30).data.cpu().numpy())

    if (idx.size):
        R[idx, :, :] = R2[idx, :, :]

    return R, Sn

3.2.3 get_poseweights (poses, bszie)

此函数用于计算 pose_weights属性, 其 shape为 [bs, 135], 而 135 = 45 ∗ 3 135 = 45 * 3 135=453.
在这里插入图片描述
其分为 2步:

  • ① 先将 poses([bs, 16, 3])送 入3.2.2所介绍的rodrigues函数中, 按照SMPL2015的说法, 其目的是将每个joint的 axis-angle转为rotation matrix.
    pose_matrix为 [15xbs, 3, 3]
    在这里插入图片描述

  • ② pose_matrix 减去单位矩阵, 理解为 (MANO2017)公式 3的 R n ( θ ) − R n ( θ ∗ ) R_n(θ)- R_n(θ^*) Rn(θ)Rn(θ):
    在这里插入图片描述

在这里插入图片描述

def get_poseweights(poses, bsize):
    # pose: batch x 24 x 3
    pose_matrix, _ = rodrigues(poses[:, 1:, :].contiguous().view(-1, 3))
    # pose_matrix, _ = rodrigues(poses.view(-1,3))
    pose_matrix = pose_matrix - Variable(torch.from_numpy(
        np.repeat(np.expand_dims(np.eye(3, dtype=np.float32), 0), bsize * (keypoints_num - 1), axis=0)).cuda())
    pose_matrix = pose_matrix.view(bsize, -1)
    return pose_matrix
3.3 rot_pose_beta_to_mesh梳理

① 第一步 是 计算 posesv_shaped.

⚫ poses: poses是网络预测的axis angle + hand_mean (rest pose), 再加上 root_rot(wrist的位置, 代码里面写为 (0,0,0), 是相对位置, 构造root-relative的结果). [bs, 16, 3]

⚫ v_shaped: 对应 SMPL2015的公式10里面的 T ‾ \overline{T} T+ Bs部分. [bs, 778, 3]
在这里插入图片描述

对应代码:
在这里插入图片描述

② 第 2步 是 计算 pose_weights, v_posedJ_posed.

⚫ pose_weights 是 通过

pose_weights = get_poseweights(poses, batch_size)

计算得到的。具体分析看 3.2.3的get_poseweights函数的介绍.

⚫ v_posed 是 <3D Hand Shape and Pose from Images in the wild, CVPR2019>的公式 2的 T ( β , θ ) T(β, θ) T(β,θ), 也是 MANO2017的公式 2:
在这里插入图片描述

⚫ J_posed 是 SMPL2015的 公式 10 其 作用 是 将 vertices变为 joints。([bs, 16, 3])
在这里插入图片描述

在这里插入图片描述
③ 第3步是 计算 results_global, 和 T, v.
在这里插入图片描述

⚫ 从J_posed (bs, 16, 3)变为J_posed_split(16, bs, 3). 即这里的处理逻辑是按手指的节点去处理, 而非batch-wise维度的。

⚫ 同理, 有从poses得到的poses_split, 再由poses_split得到angle_matrix.

results_global是根据 kinematic tree来计算的. 每个当前节点的joint location是根据其父节点以及当前节点的旋转矩阵进行矩阵相乘得到的.

results_global也是16个手指节点的 3D xyz. [(bs ,4, 4), (bs, 4, 4), … , (bs ,4, 4)].
在这里插入图片描述
注意, 父节点只是当前节点的上面一个节点, 而非根节点哦~。之前有图解释过这个问题.
在这里插入图片描述

T是根据前面的results和weights相乘得到的,这里的 weights是蒙皮blend weights W W W.
在这里插入图片描述

V是 经过蒙皮处理后的最终变形节点, Jtr是最终的手部节点 3D位置的集合, 需要注意的是,**指尖(TIP骨骼)**的5个joint的位置是根据 V得到 的.
而且,
VJtr是要再旋转 (根据root节点的旋转角)一下输出的才是最终正确结果.
在这里插入图片描述

④ 最后一步,要从标准的MANO模型里,取TIP骨骼的joint的位置,
这个取法需要手动的去MANO模型里看顶点,以大拇指为例,这里取得是745号顶点
在这里插入图片描述
然后,就把顶点v和节点Jtr都根据root节点(手腕)的旋转矩阵进行旋转。

最后的最后,为了得到局部坐标系的3D坐标(root-relative),我们进行的处理是以节点5为(0, 0, 0),所以,把VJtr都要减去5号节点的位置。

在这里插入图片描述

在这里插入图片描述

4. 总结

根据上面的分析,我们最终得到了每个手部图像对应的21个关键点的3Dxyz(root-relative)和扭转后的所有778个顶点(MANO的手部模型有778个顶点)。

有任何问题欢迎提问,交流~

参考文献

[1] MANO2017
[2] SMPL2015
[3] OlgaChernytska/3D-Hand-Pose-Estimation
[4] Frankmocap Facebook
[5] 《几何建模与处理基础》Geometry Modeling and Processing (GMP)
[6] HMR CVPR2018
[8] Minimal Hand CVPR2020
[9] hand-biomechanical-constraints (ECCV2020)
[10] 旋转矩阵(Rotation matrix):旋转轴与旋转角 ( axis and angle )

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐