基于参数化模型(MANO)的手势姿态估计---全面剖析
目前, 主流的手部姿态估计的技术方案是使用马普所于2017年提出的MANO参数化模型, 在此基础上回归3D坐标, 这是因为MANO有很合理的结构以及定义好的前向动力学树。本文的目的在于为大家介绍,基于MANO的手部姿态估计的流程:包括并不限于: 数据处理,MANO的推理流程(与论文对齐),手的解剖学和生物学特点。1. 什么是手部姿态估计?我理解的 手部姿态估计(hand pose estimati
目前, 主流的手部姿态估计的技术方案是使用马普所于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=45∗3.
其分为 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
梳理
① 第一步 是 计算 poses和 v_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_posed和 J_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得到 的.
而且,
V和 Jtr是要再旋转 (根据root节点的旋转角)一下输出的才是最终正确结果.
④ 最后一步,要从标准的MANO模型里,取TIP骨骼的joint的位置,
这个取法需要手动的去MANO模型里看顶点,以大拇指为例,这里取得是745号顶点:
然后,就把顶点v和节点Jtr都根据root节点(手腕)的旋转矩阵进行旋转。
最后的最后,为了得到局部坐标系的3D坐标(root-relative),我们进行的处理是以节点5为(0, 0, 0),所以,把V和 Jtr都要减去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 )
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)