拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SMPL三维人体模型核心解析:参数、原理与工程实践

SMPL三维人体模型核心解析:参数、原理与工程实践

1. 一个反直觉的起点:为什么一张2015年的网格,还在统治2025年的三维人体研究

我最初接触SMPL的时候,其实有点抵触:一个2015年提出的静态网格模型,凭什么在三维人体重建、NeRF类人体渲染、扩散模型姿态控制这些“很新”的方向里,几乎是无处不在?后来真正用起来才发现,SMPL的意义根本不是“一张好看的网格”,而是一套把人体抽象成低维参数的生成规则。你给模型一个体型系数β和一个姿态参数θ,它能从零生成一个有语义、有关节、可以驱动、可以求导的完整三角网格人体。这个性质让它在深度学习时代变得极其顺手。

不少初学者以为SMPL就是官网给的male、female、neutral三个OBJ/pkl文件,选一个当“人体模板”用。实际上,模板只是起点。SMPL真正厉害的地方在于:6890个顶点、13776个三角面、24个关节点全部保持固定语义和固定拓扑。第3000号顶点永远是左大腿外侧附近的某个点,第23号关节永远是左踝关节。这意味着你在一个数据集上训练出来的网络,可以把输出参数直接用到另一个SMPL实例上,也可以把不同人的网格放到同一套蒙皮和关节体系里做跨身份驱动。这种“拓扑一致”带来的便利,是点云、体素、隐式曲面甚至NeRF都很难替代的。

那SMPL到底解决了什么问题?简单说,它把“人体形状”和“人体姿态”解耦了。体型由β控制,与动作无关;姿态由θ控制,与高矮胖瘦无关。两者合并后,再通过混合形状和蒙皮变换得到最终网格。过去做人体动画的人,可能要靠美术手工调骨骼权重,或者靠扫描设备拍一堆模型再做非刚性配准。现在你只需给出几十个浮点数,就能得到连续、平滑、可微的人体模型,这让基于梯度下降的各类优化和神经网络回归变得异常方便。这也是为什么你在几乎每篇人体姿态估计、人体重建、数字人驱动论文里,都能看到SMPL的彩色网格。

当然,SMPL也不是万能的。后面我会详细聊它的坐标系统、参数表示、容易翻车的顶点顺序问题,以及从SMPL走向SMPL-X时的各种不兼容。如果你是一个正要入门三维视觉或数字人方向的研究者,这篇文章能帮你在一天内避开我当初花了一周才搞清楚的坑。

2. 数学骨架:β、θ、混合形状与蒙皮的完整解算顺序

2.1 从模板网格到最终姿态,需要五步变换

SMPL的核心公式在论文里写得很紧凑,但第一次看容易懵。我刚开始读的时候,最大的困惑是:它到底是先变形还是先摆姿势?关节位置是怎么来的?为什么同一个β还能改变关节坐标?

把公式拆开看,本质上是五个步骤:

  1. 读取基础模板网格 T,也就是中性T-pose下的6890个顶点坐标。
  2. 根据体型参数 β,叠加体型混合形状 B_s(β),得到带有个性化胖瘦高矮的T-pose网格 T_p。
  3. 根据姿态参数 θ,叠加姿态混合形状 B_p(θ),修正蒙皮带来的布料滑动和肌肉鼓起误差。
  4. 从T-pose网格的顶点位置回归出关节位置 J(β)。
  5. 以关节位置为骨骼,对顶点做线性蒙皮变换,也就是让每个顶点跟随它隶属的关节运动,最终得到带姿态的网格。

这五步里,第2步和第5步最容易混淆。很多人以为β是在最后时刻才叠加的,其实β必须先作用于模板网格,关节位置也基于变形后的网格重新回归,这样不同高矮的人,关节位置会自然不同。比如一个身高两米的人和一个一米五的人,即使姿态参数完全一样,膝盖位置也不在同一个世界坐标上,因为他们的骨骼长度分别由各自的β决定。

2.2 体型混合形状和姿态混合形状为什么要分开

体型混合形状的表达式是:

B_s(β) = Σ_{n=1}^{|β|} β_n * S_n

S_n是一组预先从数千个三维人体扫描中学习到的形状基,每个基可以理解为“把整个人体往某个方向拉一点”的位移场。β就是这些基的权重。默认SMPL通常取10个形状基,所以β是一个10维向量,但也可以学300维的扩展版本。

姿态混合形状的表达式是:

B_p(θ) = Σ_{j=1}^{K} (R_j(θ) - R_j(θ*)) * P_j

这里R_j(θ)是第j个关节在当前姿态下的旋转矩阵,R_j(θ*)是在T-pose下的旋转矩阵,正常就是单位矩阵。P_j是从数据中学到的校正项,专门用来弥补线性蒙皮在关节弯曲时产生的“皮肤塌陷”和“糖果纸扭曲”。

为什么不能把这两类混合形状合并成一个大矩阵?因为它们的来源和触发条件完全不同。体型混合形状是“纯几何形变”,和关节角度无关;姿态混合形状是“旋转产生的位移场”,只和姿态相关。如果合并,你无法独立控制一个人变胖的同时保持同一个动作,网络训练时的梯度解耦也会变得混乱。SMPL把两者拆开,本质上是因为人体外观变化可以分解成“生下来长什么样”和“现在摆了什么姿势”两个独立因素。

2.3 几个必须记住的维度和尺寸

在写代码或者debug时,我最常遇到的困惑就是各种矩阵到底多大。这里列一个常用对照表:

数据维度说明
顶点数6890所有SMPL模型统一
三角面数13776所有SMPL模型统一
关节数2423个肢体关节 + 1个全局根关节
β10默认形状系数维度
姿态θ72 = 24×3每个关节一个3维轴角
其中root朝向3维全局旋转变换
其中身体姿态69 = 23×3相对父节点的局部旋转
顶点混合形状6890×3叠加到顶点坐标的位移
形状基矩阵6890×3×10学习到的体型基底
姿态基矩阵6890×3×20723个关节的9维旋转矩阵展开,即23×9=207列
蒙皮权重6890×24每个顶点相对每个关节的权重,学习得到

第一次看到姿态基矩阵是6890×3×207的时候,有点被吓到。实际上它非常稀疏,因为一个顶点只受附近少数几个关节影响,其余列的系数接近零,所以官方存储时用了稀疏格式。实际操作中,你一般不需要手动跟这些矩阵打交道,直接用现成库就好,但理解维度能帮你在查文档时少走弯路。

2.4 关节位置是怎么“回归”出来的

SMPL里关节不是创建网格时手工标注的点,而是从网格顶点用学习到的回归矩阵算出来的。这个矩阵 J_regressor 的形状是24×6890,每一行对应一个关节,记录了这个关节位置等于哪些顶点坐标的加权和。为什么不用固定骨骼节点?因为人体骨骼长度和姿态有关——体型变了,同一个“膝盖”在世界坐标中的位置也会变;如果用固定节点,人变高以后关节就会陷到身体里。

这个设计在监督学习里特别有用。训练网络时,你既可以得到6890个顶点的密集监督,也可以只拿24个关节点去算2D重投影误差。HMR、VIBE这些经典方法,都是靠SMPL的关节回归矩阵把3D关节投影到2D,再与2D关键点检测结果做loss。不用这套回归,你还要额外维护一套关节标注,麻烦得多。

3. 本地实测:申请模型、搭Python环境、把6890个顶点跑出来

3.1 环境准备与模型文件

SMPL官方模型不能直接通过pip下载,需要到官方网站注册申请,同意许可协议后拿到一个下载链接。这个环节我有过一段不愉快的经历:当时图省事,直接用了某个GitHub仓库里转存好的pkl文件跑通了,但后来要商用才发现许可范围不明确,只能回头重新申请。建议大家在动手前先确认用途,学术研究和商用遵循的条款不同。

拿到模型文件后,通常是一堆pkl或npz格式的数据,里面有v_template、shapedirs、posedirs、J_regressor、weights等字段。你当然可以自己解析这些字段并按论文公式实现,但现在更推荐直接用官方提供的Python库smplx,它把SMPL、SMPL+H、SMPL-X、MANO都封装好了,用起来非常省事。

安装很简单:

pip install smplx

然后创建一个脚本加载neutral模型:

import torch import smplx import trimesh model_path = "./models" # 这里放你解压后的模型目录 smpl = smplx.create( model_path, model_type="smpl", gender="neutral", use_pca=False, batch_size=1 ) betas = torch.zeros(1, 10) body_pose = torch.zeros(1, 23 * 3) # 23个关节的轴角 global_orient = torch.zeros(1, 3) # 全局根关节旋转 output = smpl( betas=betas, body_pose=body_pose, global_orient=global_orient ) vertices = output.vertices[0].detach().cpu().numpy() # (6890, 3) faces = smpl.faces # (13776, 3) mesh = trimesh.Trimesh(vertices=vertices, faces=faces) mesh.export("smpl_template.obj")

跑完以后,你会得到一个OBJ文件,用MeshLab或者Windows自带的3D查看器打开,能看到一个标准的T-pose中性人体。这里千万注意:smpl.faces在部分版本里返回的是torch.Tensor,导出前要转成numpy数组。我最初直接传给了trimesh,结果报了一堆类型错误,还以为是模型文件坏了。

3.2 试试调整β和θ,看看参数怎么影响网格

零参数只是最基础的模板。接下来你可以试着改一下β,观察体型变化:

betas = torch.tensor([[1.0, -0.5, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]]) output = smpl(betas=betas, body_pose=body_pose, global_orient=global_orient) vertices_fat = output.vertices[0].detach().cpu().numpy()

第一个系数为正,整个人会明显变得更宽;第二个系数为负,会变矮。单一系数的影响是全局性的,你最好把多个β组合起来看,才能理解为什么说β是“体型基权重”而不是“局部部位开关”。

改姿态更直观。比如让左肘弯曲45度,对应关节的轴角可以这样设:

body_pose = torch.zeros(1, 23 * 3) body_pose[0, 18:21] = torch.tensor([0.0, 0.0, 0.785]) # 第7个身体关节,局部Z轴旋转45度

这里18:21对应的是第7个关节的三维轴角,为什么是这个索引不是另一个,官方文档里没有特别醒目的说明。我在实战中发现,最稳妥的办法是先把某个关节的轴角设成[1,0,0]、[0,1,0]、[0,0,1]分别试渲染,通过观察哪条腿或哪只胳膊在动,把关节索引和身体部位对应关系记录下来。不要盲目相信一些人写的“第几号关节是膝盖”,不同模型文件的关节顺序可能不一致。

3.3 用关节和顶点做什么

跑通渲染后,你还能从output里取joints:

joints = output.joints[0].detach().cpu().numpy() # (24, 3)

第一个关节通常是骨盆根节点,后面依次是脊柱、脖子、肩膀、手臂、腿等。计算3D关节和2D关键点的重投影误差,是很多姿态估计模型的训练流程。你可以先用matplotlib或者open3d把24个关节画成球体,看看它们是否正好贴合网格表面。如果发现关节偏到网格外部,不要急着调参数,多半是可视化代码里把坐标轴的x/y/z弄混了,或者缩放尺度不统一。

4. 姿态参数θ的坐标系陷阱:轴角、父节点旋转与万向锁

4.1 为什么SMPL用轴角而不是欧拉角或旋转矩阵

姿态参数θ的72维,本质上是由24个关节的3维轴角拼起来的。轴角表示法的含义是:旋转轴是一个三维单位向量,旋转角度是这个向量的长度,因此每个旋转只需要3个数。这比旋转矩阵的9个数少得多,也比四元数的4个数少,非常适合作为神经网络的回归目标——网络只需要输出72个浮点数,就能完整描述整个人体姿态。

相比之下,欧拉角虽然也是3个数,但存在万向锁问题,而且不同的旋转顺序(XYZ还是ZYX)含义完全不同。如果网络回归的欧拉角稍微偏离,中间表示可能突然跳到另一个姿态,导致训练不稳定。四元数需要归一化,而且双覆盖特性(q和-q表示同一个旋转)也会让监督学习变得麻烦。轴角在角度不太大的时候表现稳定,所以SMPL选择了它。

4.2 局部旋转和全局旋转是两码事

SMPL的姿态参数里,每一个关节的旋转都是相对父节点的局部旋转,而不是世界坐标系下的绝对旋转。举个我实际调试时遇到的例子:你想让左前臂做一个“从体侧抬起到水平”的动作,如果直接把世界坐标系下的旋转矩阵塞给肘关节,渲染出来往往是胳膊拧成麻花。因为正确的做法是,前臂的最终朝向 = 肩膀的旋转 × 肘关节的局部旋转,这个乘法顺序不能乱。

用链式规则来理解:骨盆是世界根节点,旋转会带动全身;腰椎在骨盆坐标系里旋转,带动上半身;颈椎在胸腔坐标系里旋转,带动头部。每个关节都“继承”了父节点的旋转,所以在代码里,SMPL不是简单地把每个轴角独立做旋转,而是把关节树从根节点遍历一次,累积所有祖先节点的旋转矩阵,最后对蒙皮顶点做线性混合蒙皮。

实际写代码时,如果你需要手动控制某个关节,建议这样做:

  1. 把目标关节的局部轴角通过Rodrigues公式转成旋转矩阵。
  2. 从根关节开始,沿关节树逐级传递,得到该关节的世界旋转矩阵。
  3. 把世界旋转矩阵作用到对应的蒙皮顶点上。

但大多数情况下你不需要自己实现这些,smplx库内部已经处理好了。你只需要输入每个关节的局部轴角。如果之前用过Blender或者Maya,这个概念和骨骼动画里的局部旋转完全一致,可以类比记忆。

4.3 万向锁和边界奇异为什么基本不构成问题

轴角表示在角度接近0时,方向向量本身含义变模糊,梯度可能会出现数值不稳定。但SMPL面对的绝大多数人体关节,角度范围有限,肘关节、膝关节几乎不可能旋转到180度以上,所以轴角的奇异性在实际训练里很少真正触发。

网上有些教程会让你把SMPL的轴角转成旋转矩阵或6D表示再去算loss,觉得这样更稳。我的经验是:如果你的任务只是回归SMPL参数,直接在72维的轴角空间算L1或L2损失也能收敛,但要注意不同关节点角度范围不一样,比如脊柱的活动范围就比手指小很多,最好按关节分组设置不同loss权重。如果任务涉及连续驱动和插值,比如做动作重定向,那么先转成四元数或旋转矩阵做插值,再转回轴角喂回SMPL,比直接在轴角空间插值要平滑得多。为什么?因为轴角在跨越2π边界时会有跳变,两个视觉上很接近的姿态,数值上可能差出3.14,直接用L2去插值会让动作突然扭一下。

4.4 坐标系方向是不可忽视的隐藏坑

SMPL模型内部默认的坐标朝向通常是Y轴向上、Z轴朝向人体正面,但在不同渲染器里,坐标约定可能完全不同。OpenGL相机看Z轴朝屏幕外,OpenCV相机看Z轴朝前,Unity又是左手坐标系。我吃过一次亏:把SMPL顶点直接扔进Open3D可视化,人看起来是倒着的,我当时以为模型文件坏了,来回折腾了半小时,最后发现只是需要把顶点绕X轴旋转180度。

更麻烦的是,SMPL的姿态参数和相机外参之间经常需要额外对齐。比如你用OpenPose检测2D关键点,它们通常在图像坐标系里;而SMPL的3D关节在世界坐标系里,训练时要先经过相机投影模型。如果坐标系没对齐,loss会一直震荡,但模型又不会完全发散,表现就是“看起来在学,但学不到点子上”。建议在一切实验开始前,写一个简单脚本:把SMPL的3D关节投影到图像上,和2D关键点画在一起,肉眼确认对齐关系。这个检查30秒就能做完,能省掉后面一整天的盲目调试。

5. 从SMPL到SMPL+X:手、脸和关键点兼容问题

5.1 三兄弟的定位差异

SMPL最明显的短板是:手只有一个球形手柄,面部基本没有细节。做全身动作估计够用,但做数字人、手势交互、面部表情驱动,就完全不够了。针对这个问题,同一批作者又推出了SMPL+H和SMPL-X。

SMPL+H在SMPL的基础上加入了手部模型,手指关节可以独立控制,总顶点数比SMPL多出不少。SMPL-X则更进一步,把面部表情、眼球、手指都纳入进去,总顶点数达到10475,光面部就有很多用于表达表情的顶点。它们共享相似的参数化思想,但不是同一个拓扑,顶点编号、关节索引、参数长度都不一样。

模型总关节数(约)顶点数是否含手指是否含表情典型应用
SMPL246890否否全身姿态估计、动作生成、NeRF人体先验
SMPL+H52左右约6890+手部顶点是否手物交互、手势动作
SMPL-X100+10475是是数字人、表情驱动、VR化身

这里要特别提醒:SMPL的模型参数无法直接喂给SMPL-X,即使你在代码里硬塞,也会因为维度不匹配直接报错。网络模型层面也是同理,一个输出SMPL参数的模型,不能直接改成SMPL-X输出就算完事,因为关节树结构完全变了。唯一能共享的,往往是训练好的backbone特征提取器,而不是最终的回归头。

5.2 什么时候选SMPL,什么时候选SMPL-X

我的经验是,先想清楚你的最终输出是什么,再决定用哪个模型。如果只是在HMR这类全身姿态估计任务里做3D人体先验,SMPL就够了,关节少、参数少、更容易收敛。如果任务里有明显的手部交互,比如手抓杯子、手拿手机,那就别在SMPL上硬扛。很多研究者会在SMPL之外额外跑一个手部模型(比如MANO)来细化手部姿态,但这样会导致手和手臂的接缝处出现裂缝或不自然穿插,后期需要额外处理。

SMPL-X在NeRF和隐式人体重建里越来越流行,因为它能提供更完整的语义先验。但代价是参数空间更大,优化难度更高,训练数据也更难获取。对于刚入门的研究者,我不建议一上来就追SMPL-X,先把SMPL的β、θ、蒙皮、关节回归这些概念吃透,再迁移过去会轻松很多。因为SMPL-X的公式与SMPL几乎一致,只是额外增加了头部表情基和手部姿态基,你已经理解的那些核心逻辑都能复用。

5.3 关键点名称和索引映射最容易被忽略

SMPL的24个关节在常见顶点的定义里,通常有固定的名称和顺序。到了SMPL-X,关节数量暴涨,还要区分左右手指、左右眼、下巴等。很多下游任务需要一定语义上的关键点,比如你从数据集里拿到“左肩膀”的2D位置,需要找到SMPL对应关节索引,但如果库版本不同,关节顺序可能不一样。我的建议是,建立一套自己的映射文件,把需要的关节点名称和索引固定下来,每次开新项目直接复制,不要每次都靠猜或者翻源码。

在写训练代码时,输出关节位置后还有一个常见误区:直接把SMPL关节拿去和某个2D关键点数据集算loss,却不做三个基本对齐。第一是坐标系对齐,保证3D关节和相机坐标一致;第二是尺度对齐,因为SMPL是以米为单位,而图像坐标是像素,两者不在一个量级;第三是关节顺序对齐,2D关键点检测器的序号和SMPL关节序号经常不一致。这个三层对齐写一个统一函数封装好,能省掉成堆的隐蔽bug。

6. 高频踩坑:顶点语义、蒙皮伪影与自动微分优化

6.1 顶点顺序和拓扑一致性:一个必须刻进DNA的原则

SMPL社区一个最常见的新手失误,是对网格做了各种“预处理”,比如在MeshLab里重新网格化、删掉重复面、或者用Quad Remesher减面,然后用处理后的网格当SMPL模板用。这会让6890个顶点的语义完全失效,后续任何基于顶点编号的监督、蒙皮权重、形状基都无法对齐。

记住一条铁律:SMPL的顶点编号就是一张身份证,任何增删顶点、改变面片顺序、重新排列顶点索引的操作,都是在破坏这张身份证。你可以做刚性变换、缩放、平移、旋转,但绝不能改变拓扑。如果某些场景确实需要低分辨率网格,正确的做法是先通过SMPL得到密集网格,再对网格做简化,并且维护好密集网格和简化网格之间的顶点对应关系,而不是把简化后的网格直接当新的SMPL用。

另外,导出OBJ时也要注意面片朝向。有些库导出OBJ时默认的逆时针/顺时针规则不一样,导致法线翻转,模型看起来黑一块亮一块。用trimesh可以快速修复:

mesh.fix_normals()

但修复之前,最好先检查你是不是在某个步骤里对网格做了镜像变换。左右手模型切换时最容易出现这种问题。我遇到过整个人体变成一个“负体积”网格的情况,渲染出来的模型从里到外都是黑的,最后发现是导入导出过程中某一步把顶点坐标的X轴乘了-1。

6.2 LBS的糖果纸扭曲在SMPL里依然存在

线性蒙皮变换的经典缺点,是关节旋转幅度过大时,皮肤会出现“糖果纸”一样的扭曲。SMPL加入了姿态混合形状B_p来修正,但修正不是万能的。你把大腿关节绕自身长轴旋转超过90度,依然可能看到明显的皮肤褶皱和体积收缩。

所以做动作数据时,如果发现渲染出来的四肢有异常扭曲,先别急着调形状基和蒙皮权重。大概率是你的输入轴角本身不合理,比如给膝关节加了一个绕Y轴的旋转。人的膝关节基本只能绕一个轴转动,如果你的姿态估计网络在某个困难样本上输出了奇怪角度,渲染出来的腿就会跟果冻一样拧起来。建议在渲染前加一个轴角范围校验,把超出人体合理范围的角度截断或平滑掉,视觉效果会稳定很多。

6.3 优化β/θ时的自动微分陷阱

SMPL是纯参数化模型,天然支持对β和θ求梯度,所以很多人喜欢直接用PyTorch反向传播去拟合一个目标,比如把SMPL的3D关键点投影到2D,最小化和图像检测结果的差异。这个思路本身没问题,但有两个细节很影响收敛。

第一个是轴角朝向歧义。轴角在角度接近0时,方向向量没有稳定含义,小扰动就可能让梯度跳变。如果优化初始值离目标很远,建议前几十步先固定β,只用姿态损失优化θ,等姿态大致对上以后再放开β一起优化。第二个是不同参数的量纲差异太大。β的数量级通常在几以内,而轴角的数量级可能从0.01到3.14不等,如果用同一个学习率,往往会出现姿态没学好、体型已经被改得面目全非的情况。实操中我会把β的梯度缩放设成0.1或者0.01,再观察损失曲线。

还有一种更隐蔽的问题:SMPL的模板T-pose和网络预测的初始姿态相差很大时,某些关节由于局部坐标系的非线性,优化会陷入局部极小。最常见的表现是肘关节弯成了反方向,在2D投影上看起来还挺像。解决方法是加入姿态先验loss,比如对每个关节轴角的范数加一个小的L2惩罚,或者用数据集里学习出的姿态先验分布约束,防止网络输出生理上不可能的转角。

6.4 从诊断到复现:一个调试SMPL代码的checklist

最后把我平时排查SMPL问题时的固定流程分享出来,当作一个查错清单:

  1. 先确认模型加载没有报错,检查顶点数是不是6890、面数是不是13776。
  2. 用零参数生成模板网格,肉眼确认T-pose方向正确,没有镜像、没有倒立。
  3. 单独改一个β分量,确认人体形状变化方向符合预期;单独改一个关节轴角,确认对应肢体在动。
  4. 输出joints,和网格叠加渲染,确认关节在网格内部或表面附近。
  5. 如果做2D投影,先画一次投影结果,确认3D到2D的坐标变换没有x/y/z轴错位。
  6. 如果做优化,先把β锁死,看姿态能不能收敛;再加β,观察是否有量纲或梯度冲突。
  7. 任何涉及网格导出的环节,检查法线方向和顶点顺序是否被意外改动。

这套流程在大多数情况下能定位到90%的问题。剩下10%,基本都和某个特定版本库的API变动有关,处理方式就是看报错信息去翻源码,别在社区里盲目搜“为什么我的SMPL坏了”,绝大多数人遇到的情况和你不一定一样。

我现在每次带新人入门SMPL,都会要求他们先手工改参数渲染几十张图,把β的每个系数和θ的主要关节都摸一遍。这个习惯看起来笨,但对建立直觉特别有帮助。等你能从渲染结果反推出大概哪几个关节的轴角被改动了,再去看论文公式和下游模型,整个SMPL的框架就会变得特别清晰。如果你也正在折腾SMPL,希望这份笔记能帮你少走几段我走过的弯路。

返回列表