🗒️用工具系列(一):3D人体姿态模型和相关数据集使用
2021-7-2
| 2023-10-30
0  |  0 分钟
type
Post
status
Published
date
Jul 2, 2021
slug
using-tools:1
summary
第一期用工具系列博客的主题是学会使用常用的三维人体姿态模型以及相关的数据集。在研究人体运动和人体姿态估计时,我们往往会在一些大型的数据集上进行模型的训练,此外还有各种用来表现人体姿态的模型,这些模型采用不同的参数来刻画人的身体姿态。因此掌握这些人体姿态模型和数据集的用法非常有必要。
tags
Python
工具
Ego-pose Estimation
category
技术分享
icon
password
在研究人体模型和人体姿态等问题时,我们往往会使用一些通用的人体姿态表示模型来可视化最终的结果;同时,我们经常在一些高质量的人体运动数据集上进行训练来学习人体运动先验。在这篇文章中,我将会介绍一些我个人研究中会用的一些工具以及使用方法。

🤔 SMPL+H模型

SMPL模型是一种对人类身体姿态进行参数化表征的统一框架。通过使用这个框架,我们可以规范地表示一个人的三维身体姿态,并且能够得到身体的网格信息,有利于人体姿态的可视化。
SMPL+H模型则是SMPL模型的升级版,额外考虑了更精细的手部姿态。对人类身体位置的计算部分,SMPL+H模型和SMPL模型是一致的。

SMPL关节树

SMPL模型将人类的身体简化为24个关节节点,同时将这些节点组织成树的形式,pelvis被设置为关节树的根节点,除了根节点和叶子节电,其余每一个关节都有一个父节点和子节点。
SMPL的关节树如下图所示:
notion image

用SMPL表示的人体关节坐标探究

由于根节点本身就有平移量,因此我们可以通过前向运动学计算出每一个关节点的全局坐标。在全局坐标系中,z轴正方向竖直向上。
 

 

📝 AMASS数据集

介绍

AMASS是人体运动有关的一个大型数据集,它在一个通用的框架下使用参数化方法统一地表示不同的MoCap数据集。AMASS数据集的github地址是:
AMASS数据集使用SMPL+H模型来表示3D人体姿态,但也可兼容其他版本的SMPL。

使用


下载AMASS数据集需要先在网站上进行注册:
可以看到AMASS数据集中包含了很多个MoCap数据集的以SMPL表示的模型:
notion image
我在研究中下载了所有种类的SMPL+H G模型数据,你可以根据你的需求进行下载。下载完成并解压之后你会发现,每一个子数据集表示的其实是男性和女性在做不同种类的动作时的三维身体姿态数据,而这些数据都是用.npz数组的格式来存储的。
我们以子数据集ACCAD为例,来看一下这个数据中都包含了哪些信息。
import numpy as np ### Read AMASS-ACCAD npz_data_path = 'B21 s3 - put down box to walk_poses.npz' #我没打错,它真的叫这个名字 bdata = np.load(npz_data_path) ### bdata中总共包含了6个npy数组 ### 分别是:trans.npy gender.npy mocap_framerate.npy betas.npy dmpls.npy poses.npy print(bdata['mocap_framerate']) !output: array(120.) ### 说明AMASS数据的帧率是120 num_frames = bdata['trans'].shape[0] ! output: 1289 ### 显示该动作序列一共持续了1289帧 trans = bdata['trans'][:] ### trans.npy包含了人体在全局坐标系中的位移 ### trans的shape: (1289,3) root_orient = bdata['poses'][:, :3] ## shape:(1289,3) ### root_orient表示根关节的旋转弧度 pose_body = bdata['poses'][:, 3:66] ### pose_body: 身体其他关节的旋转弧度 pose_hand = bdata['poses'][:, 66:] ## shape:(1289,90) ### 手指关节旋转
在我们实际研究中,会只取每一段动作序列中间80%的部分来避免动作静止的情况:
trim_data = [trans, root_orient, pose_body, pose_hand] for i, data_seq in enumerate(trim_data): trim_data[i] = data_seq[int(0.1*num_frames):int(0.9*num_frames)] trans, root_orient, pose_body, pose_hand = trim_data num_frames = trans.shape[0] print(num_frames) ! output: 1032

动力学前向计算得到关节位置

在AMASS数据集中,我们直接得到的是每个关节的旋转角度,因此为了得到每个关节的全局坐标位置,我们必须要通过SMPL模型的前向计算过程来得到每个关节的3d位置。这里我们终于要用到前面讲过的SMPLH模型了🤩
SMPLH模型还考虑了人类的性别,所以我们要根据amass数据集动作序列所属的性别来选择要加载的模型。我们还是以ACCAD中的Female1Walking_c3d下的动作序列'B21 s3 - put down box to walk_poses.npz'为例。
### forward pass to get joints using smplh from body_model.body_model import BodyModel ### SMPLH需要body_model这个模块 gender = 'female' bm_path = os.path.join(smplh_path, gender, 'model.npz'). ## smplh_path是下载的SMPLH模型的路径 device = torch.device("cuda" if available else "cpu") bm = BodyModel(bm_path=bm_path, num_betas=16, batch_size=num_frames).to(device) ## num_betas代表SMPL使用的体型参数的个数 npz_data = np.load(bm_path) ### model.npz一共包含了12个子数组,它们分别是: # J_regressor_prior.npy # f.npy # J_regressor.npy # kintree_table.npy # J.npy # weights_prior.npy # weights.npy # posedirs.npy # bs_style.npy # v_template.npy # shapedirs.npy # bs_type.npy ori_kintree_table = npz_data['kintree_table'] # 2*52 kintree_table = ori_kintree_table[0, :22] # 22 kintree_table[0] = -1 ### kintree_table记录的是人体骨骼关节树中,每一个关节的父节点编号;这里根节点的父节点被设置成-1 pose_body = torch.Tensor(pose_body).to(device) pose_hand = torch.Tensor(pose_hand).to(device) betas = torch.Tensor(np.repeat(betas[:NUM_BETAS][np.newaxis], num_frames, axis=0)).to(device) root_orient = torch.Tensor(root_orient).to(device) trans = torch.Tensor(trans).to(device) ### 这里的pose_body, betas, trans等变量是上面的代码里从AMASS数据集中读取到的 body = bm(pose_body=pose_body, pose_hand=pose_hand, betas=betas, root_orient=root_orient, trans=trans) ### 得到body之后,事实上我们已经根据body_model和SMPLH得到了关节位置信息;这些信息都在body这个对象中 cur_joint_seq = body.Jtr.data.cpu() ## shape: (1032,52,3) from body_model.utils import SMPL_JOINTS, KEYPT_VERTS cur_body_joint_seq = cur_joint_seq[: , :len(SMPL_JOINTS), :] ## shape: (1032,52,3) ### 读取三维人体模型的网格信息 cur_vtx_seq = body.v.data.cpu() ## shape: (1032,6890,3)

进阶

我们观察转换后的人体关节的全局坐标时常常会发现,有的关节的z轴坐标竟然会小于0,这其实是MoCap数据本身在采集时的误差以及前向计算时的误差等等所导致的。因此,为了增加严谨性和准确性,我们往往需要对关节位置进行进一步的矫正,矫正其实就是要重新确定地板(即z=0)的位置;另外,有的时候我们比较关心人物运动时脚和地面在哪些时刻会有接触(甚至是手关节或者是根关节),这是就需要一些简单的算法对“是否和地面接触”这件事进行判断;还有,之前我们曾经说过AMASS数据集的帧率是120,但是在其他测试环境中的帧率往往只有30,这时候就需要对动作序列进行降采样。

姿态矫正

接触判断

降采样

这里提供一个降采样代码的example
### fps是实际的帧率;OUTPUT_FPS是降采样之后的输出帧率 fps_ratio = float(OUT_FPS) / fps print('Downsamp ratio: %f' % (fps_ratio)) new_num_frames = int(fps_ratio*num_frames) print('Downsamp num frames: %d' % (new_num_frames)) # print(cur_num_frames) # print(new_num_frames) downsamp_inds = np.linspace(0, num_frames-1, num=new_num_frames, dtype=int) # print(downsamp_inds) # update data to save fps = OUT_FPS num_frames = new_num_frames contacts = contacts[downsamp_inds] trans = trans[downsamp_inds] root_orient = root_orient[downsamp_inds] pose_body = pose_body[downsamp_inds] pose_hand = pose_hand[downsamp_inds] joint_seq = joint_seq[downsamp_inds]

计算某个关节的全局位姿

关节的全局位姿包括全局的旋转量+全局的平移量。在关节树中,每一个关节都有一个三维旋转角,但是那是该关节相对于父关节的旋转角,属于局部旋转角。因此如果要求一个关节的全局旋转量,还必须递归地乘上所有父关节的旋转矩阵,而全局平移量则很好处理,因为之前我们已经得到了人体关节的全局位置坐标,只需要用当前时刻关节所对应的向量减去起始时刻的向量即可。
三维空间中平移量的表示很简单,只需要用一个包含三个元素的向量即可;但是对于旋转量却有很多的表示方式,最常见的当然是用一个3*3的矩阵来表示。但是使用矩阵来表示其实包含了很多冗余信息,旋转这件事其实只需要知道一个旋转轴(3元素向量)+一个角度一共4个值就可以表示了,这种用4个数来表示旋转的表示方式被称为四元数。除此之外,还有用6个数来表示旋转的方法,这种表示方式的好处是可以连续地表示旋转过程。将来我可以另外写一篇博客来讨论这两种表示形式。
求关节的全局旋转矩阵的代码如下所示,这里我们以头部为例:
def local2global_pose(local_pose, kintree): # local_pose: T X J X 3 X 3 bs = local_pose.shape[0] local_pose = local_pose.view(bs, -1, 3, 3) global_pose = local_pose.clone() for jId in range(len(kintree)): parent_id = kintree[jId] if parent_id >= 0: global_pose[:, jId] = torch.matmul(global_pose[:, parent_id], global_pose[:, jId]) return global_pose local_joint_aa = torch.cat((torch.from_numpy(root_orient).float(), torch.from_numpy(pose_body).float()), dim=1) # T X (3+21*3) local_joint_aa = local_joint_aa.reshape(-1, 22, 3) # T X 22 X 3 local_joint_rot_mat = transforms.axis_angle_to_matrix(local_joint_aa) # T X 22 X 3 X 3 global_joint_rot_mat = local2global_pose(local_joint_rot_mat, kintree_table) # T X 22 X 3 X 3
在一些情况下,我们还想进一步求出全局位姿的变化量。对于平移向量,这是很容易做到的,只需要用两向量相减即可:
global_head_trans_diff = global_head_trans[1:, :] - global_head_trans[:-1, :] # (T-1) X 3
对于旋转矩阵来说就有点复杂。想象一下:现在我们有初始时刻的旋转矩阵和当前时刻的旋转矩阵,我们想要求出这两个旋转矩阵之间的旋转变化,我们应该怎么做?首先排除直接用两个旋转矩阵相减 🤣。其实可以从矩阵乘法的角度去理解,即对一个矩阵,我们应该对它做怎样的变换才能得到,答案当然是右乘上矩阵,所以写成代码就是:
global_head_rot_mat_diff = torch.matmul(torch.inverse(global_head_rot_mat[:-1]), global_head_rot_mat[1:]) # (T-1) X 3 X 3

👤强化学习环境Mujoco中的人体姿态表示

Mujoco是一种强化学习环境,之前的一些工作使用该环境搭建一个仿真环境来模拟人类和物体之间的交互动作。关于该环境怎么使用,博主目前也不太会,等我学会了再写一篇博客专门介绍一下它吧。这一部分的内容会介绍,怎么把从AMASS数据集读取的人体姿态参数转换到Mujoco中使用。
这里我们以这个仓库中的代码为例进行介绍:
该项目的kinpoly/sample_data下有h36m_test.pkl和standing_neutral.pkl这两个文件,读取第一个文件之后可以发现,该文件保存了一个字典,最外层的key是‘S1-Directions’,value包含了8个数组,它们分别是pose_aa, pose_6d, qpos, trans, beta, expert:专家动作姿态序列, obj_pose:物体位姿, action_one_hot:相关动作的one-hot编码;value中除了这8个数组,还有一些元素。这是给Mujoco环境输入的数据形式,也就是说:如果我们想要让仿真环境中的Humanoid的做出和AMASS数据一样的动作,那么我们就需要将AMASS数据读取后转换成这8个数组。

SMPL位姿转换成qpos位姿

从AMASS数据中读取得到的smpl格式的人体姿势主要包含了5个部分,分别是:root_orient-根部位姿(shape: T*3),pose_body-身体位姿(shape: T*63),trans-人体位移向量(shape: T*3),betas-人体体型参数(shape: 16),gender-性别参数(shape: 1)

pose_aa

pose_aa包含了72个元素,我们需要将SMPL中的根部位姿和身体位姿拼接起来再拼接上一个6元素的零向量:
seq_length = amass_pose.shape[0] pose_aa = torch.cat((torch.tensor(amass_root_pose), torch.tensor(amass_pose), torch.zeros(seq_length, 6)), dim=-1)

trans

trans数组可以直接用SMPL中的位移向量:
curr_trans = amass_trans

qpos

相比之下,将SMPL位姿转换成qpos有些复杂,首先需要创建一个用于Mujoco环境的humanoid,其次需要同时用到pose_aa和trans向量:
from mujoco_py import load_model_from_path # 首先导入mujoco的python接口 from scipy.spatial.transform import Rotation as sRot ### 在使用Mujoco之前需要先编写xml格式的配置文件,然后通过该文件创建humanoid model_file = '***.xml' humanoid_model = load_model_from_path(model_file) ### 先把SMPL的骨骼名称定义好 SMPL_BONE_NAMES = ['Pelvis', 'L_Hip', 'R_Hip', 'Torso', 'L_Knee', 'R_Knee', 'Spine', 'L_Ankle', 'R_Ankle', 'Chest', 'L_Toe', 'R_Toe', 'Neck', 'L_Thorax', 'R_Thorax', 'Head', 'L_Shoulder', 'R_Shoulder', 'L_Elbow', 'R_Elbow', 'L_Wrist', 'R_Wrist', 'L_Hand', 'R_Hand'] euler_order = 'ZYX' ### 编写一个函数来输出humanoid各个骨关节在数组中的索引 def get_body_qposaddr(model): body_qposaddr = dict() for i, body_name in enumerate(model.body_names): start_joint = model.body_jntadr[i] if start_joint < 0: continue end_joint = start_joint + model.body_jntnum[i] start_qposaddr = model.jnt_qposadr[start_joint] if end_joint < len(model.jnt_qposadr): end_qposaddr = model.jnt_qposadr[end_joint] else: end_qposaddr = model.nq body_qposaddr[body_name] = (start_qposaddr, end_qposaddr) return body_qposaddr smpl_2_mujoco = [ SMPL_BONE_NAMES.index(q) for q in list(get_body_qposaddr(model).keys()) if q in SMPL_BONE_NAMES] pose = pose_aa.reshape(-1, 24, 3).reshape(-1, 72) ### 计算每一个关节的旋转矩阵: (T*24,3)->(T,24,4,4) curr_pose_mat = angle_axis_to_rotation_matrix(pose.reshape(-1, 3)).reshape(pose.shape[0], -1, 4, 4) curr_spose = sRot.from_matrix(curr_pose_mat[:,:,:3,:3].reshape(-1, 3, 3).numpy()) ### 将旋转矩阵转换成欧拉角,注意欧拉角的顺序是Z轴->Y轴->X轴 ### (T*24,3,3)->(T,72) curr_spose_euler = curr_spose.as_euler(euler_order, degrees=False).reshape(curr_pose_mat.shape[0], -1) ### 将欧拉角按照Humanoid中骨骼树的先后顺序进行重新排序 curr_spose_euler = curr_spose_euler.reshape(-1, 24, 3)[:, smpl_2_mujoco, :].reshape(-1, 72) ### 将根关节的旋转矩阵转化成四元数 root_quat = rotation_matrix_to_quaternion(curr_pose_mat[:,0,:3,:]) ### 将根关节平移向量trans, 根关节的旋转四元数和各个关节的旋转欧拉角进行拼接 ### 得到适用于mujoco的qpos格式的人体位姿 curr_qpos = np.concatenate((trans, root_quat, curr_spose_euler[:,3:]), axis = 1)
 

beta

只取SMPL中16个beta参数的前10个作为qpos参数
beta = betas_amass[:10]

expert

在Kinpoly中,作者使用了模仿学习的策略,因此需要一种专家策略即expert,专家策略包含了每一个关节应该施加的力矩信息。因此这里就需要一个将专家的身体姿态(即AMASS数据集中的人物身体姿态)转化为关节力矩的函数,这个函数依赖于Mujoco环境,并且需要Humanoid在仿真环境中一步一步模拟才能获得,由于比较复杂,在这里不再详述,我会在另一篇博客里分析这件事。

将其余参数以及数组打包

smpl_seq_data[dest_seq_name] = { "pose_aa": pose_aa.numpy(), # "pose_6d":pose_seq_6d.numpy(), "qpos": qpos, 'trans': curr_trans, 'beta': betas[:10], "seq_name": dest_seq_name, "gender": gender, "expert": expert_res }
顺便提一下,这个项目首先对所有的AMASS数据集的动作序列进行了预处理,预处理后的结果全都保存在data/processed_amass_shape_seq/这个文件夹下;然后作者用了上面这些代码将预处理的结果全都转换成qpos格式,事实上,作者使用了一个for循环,并且创建了一个极大的字典,将amass数据集下的每一个动作序列转换后的结果全都写入了这个字典里,然后再使用joblib将字典保存到data/amass_processed_for_kinpoly/amass_kinpoly_motion.p这个文件中。可以预想到这个文件应该是比较大的,事实上确实如此,在我的服务器上一共占据了4.6G的内存。

qpos位姿转换为SMPL位姿

这一部分我们介绍一下如何将KinPoly中提供的qpos数据集转换成SMPL姿态。在后续实验中我们会在KinPoly数据集上进行训练和测试,因此熟练掌握将qpos姿态转换成SMPL姿态的过程是非常重要的。KinPoly作者放出的姿态数据被保存在一个.p格式的文件中,我们读取后发现这是一个字典,字典中储存着人体每一时刻的姿态数组,每一时刻的身体姿态参数一共有76个,前三个参数代表人体根节点的位移,第4-7个参数代表根节点的旋转四元数,然后剩余的69个参数代表了23个关节的旋转欧拉角。
这里需要说明一下,SMPL人体模型所包含的人体关节和qpos包含的人体关节不太一致,我们需要先进行关节上的转换。

参考文章

  1. EgoBody
  1. Kinpoly
 
致谢:
💡
有关Notion安装或者使用上的问题,欢迎您在底部评论区留言,一起交流~
 
 
技术分享
  • Python
  • 工具
  • Ego-pose Estimation
  • 读论文系列(一):Ego-pose Estimation读论文系列(一):Ego-pose Estimation
    目录