
简介人体动作捕捉与三维重建的SMPL/SMPLify Python工程包面向计算机、数学、电子信息等专业学生、算法爱好者及毕设/课程设计选题者可作为从理论到代码的实践参考。压缩包共2000个文件含14个Python脚本、1份Markdown项目说明和1985张示例图片整体12.85MB其中py文件实现SMPL参数化生成与SMPLify姿态拟合两条主线jpg图片覆盖不同动作与视角的拟合结果便于逐组对照验证算法效果md说明则梳理了运行环境与调用方式。项目说明中还包括libosmesa6-dev、opendr等依赖安装及命令示例可快速启动hello_smpl.py与fit_3d.py复现从标准人体网格到关键点约束优化的完整流程。目前已有486人学习/下载适合具备基础Python知识、希望深入理解人体参数化建模和姿态优化原理的读者作为算法复现与二次开发素材。1. 人体动作捕捉与三维重建为什么绕不开 SMPL 和 SMPLify做人体动作捕捉和三维重建的从业者几乎都会在某个阶段遇到 SMPL 和 SMPLify 这两个名字。SMPL 是一个参数化人体模型用一个低维向量描述人体的形状和姿态输出带蒙皮的三角网格SMPLify 则是把单目图像里的 2D 关键点反推成 SMPL 姿态参数的经典优化方法。和直接预测顶点坐标的深色黑匣子不同这套 python 源码跑起来之后你能看到中间量能干预损失函数和约束项适合做算法研究和工程验证。这篇笔记我把拿到这份源码后怎么装、怎么跑、参数怎么调、哪里会翻车讲清楚照着做能省下不少试错时间。2. SMPL 参数化模型从源码结构到第一次前向计算2.1 SMPL 源码在做什么从顶点蒙皮到形状姿态分解SMPL 全称 Skinned Multi-Person Linear Model2015 年提出后基本成了人体参数化模型的工业基线。它把人体网格表示成一组顶点和面片顶点位置由形状参数 beta通常 10 维和姿态参数 theta通常 72 维即 24 个关节的轴角决定。模型内部做了混合蒙皮LBS并把蒙皮权重作为可学习参数还加了姿态相关的形变修正项专门处理人体弯曲时肌肉凸起这类线性蒙皮表达不了的现象。拿到这份 python 源码先看目录结构。常见做法是models目录放 SMPL 官方或重构的 pytorch/tensorflow 版本utils目录放加载 obj、可视化、相机投影之类的辅助函数根目录通常有一个demo.py或者smpl_demo.py用来跑通最小例子。源码里最核心的两个文件是smpl.py和lbs.pysmpl.py定义模型类负责把 beta 和 theta 转成关节位置和顶点偏移lbs.py实现线性混合蒙皮、顶点变换和法线计算。拿到别人的源码先别急着跑把每个函数的输入输出张量维度打印一遍就能搞清楚这个模型的数据流。SMPL 模型本身不包含相机和图像信息它只负责输出人体网格。如果要做三维重建里的单目姿态估计就需要额外一个相机模型把 3D 关节投影到 2D再和 2D 关键点比较。这也是 SMPLify 名字里那个ify的由来——把一个不可微的 2D 到 3D 推断问题变成一个有明确目标函数的优化问题。2.2 用 pytorch 版 SMPL 跑一次前向最小可复现代码大多数 python 源码现在都用 pytorch 实现。先给出最小调用假设已经把smpl.py或smplx包里的实现放在了项目models目录。import torch import numpy as np from models.smpl import SMPL # 模型初始化传入模型权重路径和女性模板 smpl SMPL(model_pathmodels/SMPL_NEUTRAL.pkl, genderneutral, batch_size1) # 构造一份默认参数beta 全 0theta 全 0 beta torch.zeros(1, 10) # 形状参数控制胖瘦高矮 theta torch.zeros(1, 72) # 姿态参数24个关节的轴角每个3维 # 前向计算 output smpl(betasbeta, body_posetheta[:, 3:], global_orienttheta[:, :3]) vertices output.vertices # (1, 6890, 3) joints output.joints # (1, 24, 3) 或 (1, 45, 3)取决于模型版本 faces smpl.faces # (13776, 3)三角面片索引这段代码的逻辑是把 10 维 beta 交给形状分支生成静态顶点偏移然后取 theta 前 3 维作为全局旋转后 69 维作为 23 个关节的局部旋转经过正向运动学算出每个关节的世界坐标最后用蒙皮权重把顶点从 T-pose 变形到当前姿态。输出里的vertices就是可直接渲染或保存为 obj 的网格顶点。参数说明body_pose和global_orient分开传是 SMPL 源码里常见的接口设计因为优化时全局姿态和局部姿态的约束策略不同。gender选 neutral 可以避免男女模板差异带来的形状偏差但如果你的训练数据明确是单性别换成 male/female 模板精度会更高。batch_size在优化场景里通常设 1因为 SMPLify 是对单帧逐个优化的多帧并行反而增加内存压力。3. SMPLify把 2D 关键点反推成 SMPL 姿态参数的优化流程3.1 SMPLify 的优化目标为什么长这样SMPLify 的核心思想不复杂给定一张图上的 2D 人体关键点我们希望找到一组 beta 和 theta使得 SMPL 模型投影到图像上的 3D 关节位置与观测的 2D 关键点尽量重合。但直接最小化重投影误差会有两个问题一是人体姿态的自由度太高单个视角本身存在深度歧义二是 SMPL 的 72 维姿态参数里大部分组合对应的是不可能的人体姿态。所以 SMPLify 的损失函数是一个多目标加权和。典型构成包括2D 关键点的重投影误差带置信度权重、形状参数的正则项、姿态参数偏离自然姿态的先验项、以及关节角度限制项。其中姿态先验是核心早期版本用 CMU 动作捕捉数据训练的高斯混合模型GMM后来 SMPLify-X 换成了 VPoser。如果你拿到的源码里prior目录下有几个.npz或.pt文件那就是先验模型的参数。另外还有两个细节第一SMPL 输出的关节位置和 OpenPose/MediaPipe 检测的关键点序号不一致必须做索引映射第二相机内参和初始平移量对优化结果影响极大源码里通常会有一个相机初始化步骤用成人身高先验估计尺度用 2D 关键点的中心估计平移。3.2 搭建一个最小 SMPLify 拟合循环下面这段代码是一个简化版的 SMPLify 拟合主循环重点展示三大项损失是怎么组合的。实际项目里建议在真实源码基础上改这里是为了把骨架逻辑讲清楚。import torch from torch.nn.functional import mse_loss def smplify_fit(smpl_model, keypoints_2d, conf, camera_intrinsics, init_beta, init_theta, n_iter100): keypoints_2d: [24, 2] 已按SMPL关节顺序对齐的2D关键点单位像素 conf: [24] 每个关键点的检测置信度 camera_intrinsics: 3x3 相机内参矩阵 # 需要优化的变量形状和姿态 beta torch.nn.Parameter(init_beta.clone().detach().requires_grad_(True)) theta torch.nn.Parameter(init_theta.clone().detach().requires_grad_(True)) # 平移量相对相机坐标系的偏移初始值设为0 transl torch.nn.Parameter(torch.zeros(3, requires_gradTrue)) optimizer torch.optim.Adam([beta, theta, transl], lr0.02) # 姿态先验参数从文件加载简化用L2正则代替 pose_prior_weight 0.5 shape_prior_weight 0.1 for it in range(n_iter): optimizer.zero_grad() out smpl_model(betasbeta, body_posetheta[:, 3:], global_orienttheta[:, :3]) joints3d out.joints # [24, 3]注意顺序已经对齐 # 相机投影简单针孔模型假设无畸变 proj_2d camera_project(joints3d transl, camera_intrinsics) # 1) 重投影损失加权均方误差 diff proj_2d - keypoints_2d reproj_loss (conf.unsqueeze(1) * diff).pow(2).sum() # 2) 形状正则防止beta过大产生畸形人体 shape_prior_loss (beta ** 2).sum() * shape_prior_weight # 3) 姿态先验用简单的轴角范数近似 pose_prior_loss (theta[:, 3:] ** 2).sum() * pose_prior_weight total_loss reproj_loss shape_prior_loss pose_prior_loss total_loss.backward() optimizer.step() if it % 20 0: print(fiter {it}: re-proj{reproj_loss.item():.1f}, fshape_prior{shape_prior_loss.item():.3f}, fpose_prior{pose_prior_loss.item():.3f}) return beta.detach(), theta.detach(), transl.detach()这段代码的逻辑分成三块先定义可学习参数包括形状、姿态和平移量然后构造一个 Adam 优化器在迭代里反复计算重投影误差和两个正则项最后打印中间损失用来判断优化是否收敛。注意这里用 L2 代替 GMM 先验只是为了演示真实源码里会加载gmm_prior并替换掉pose_prior_loss这一行。参数说明学习率设 0.02 是我在单目人体姿态拟合里常用的初始值SMPLify 官方源码早期版本用 0.04后来发现 0.02 更稳尤其在 2D 关键点存在噪声时。n_iter官方给的是 100 次左右但实际发现 50 次就能看到大致姿态100 次用来精细收敛。conf置信度这个坑很多人会踩——OpenVINO 或 MediaPipe 输出的置信度分布差异很大建议先做一次线性归一化把最大值压到 1.0。3.3 相机模型和关节匹配投影这一步最容易出错SMPLify 里相机模块独立于 SMPL 模型。常见做法是弱透视相机或全透视针孔模型取决于你的 2D 关键点来源和相机畸变程度。如果你拿到的源码里camera.py同时实现了这两种优先用针孔并传入内参矩阵如果只有弱透视要特别注意它假设焦距固定为 1这时所有坐标都要先除以焦距做归一化。关节匹配是另一个高频坑。OpenPose 的 COCO 25 关键点、MediaPipe 的 33 点、以及 SMPL 的 24 关节索引完全不是一回事。比如 COCO 里的左肩是索引 5SMPL 的对应关节索引可能是 16。源码里一般会在constants.py或joint_mapping.py里放一张映射表跑之前务必先打印一遍映射后的 2D 关键点可视化图不要直接拿索引号用。4. 把整个项目跑通的实用路径环境配置、权重文件与输入数据准备4.1 环境安装别让依赖库版本拖后腿这份源码的运行环境通常是 Ubuntu Python 3.7/3.8 PyTorch 1.x。新入门的人往往卡在环境上原因是 SMPL 源码是 2019 年前后的写法很多函数在新版 PyTorch 里已经改了导入路径。一个稳妥做法是先建独立虚拟环境再按 requirements 安装。# 创建虚拟环境Python 版本建议 3.8兼容 pytorch 1.13 和 opencv 4.x python -m venv smpl_env source smpl_env/bin/activate # 安装 pytorchCPU 版本用于调试GPU 版本用于训练 pip install torch1.13.1 torchvision0.14.1 --index-url # 项目依赖以 requirements.txt 为基准缺什么补什么 pip install numpy opencv-python scipy matplotlib pip install smplx0.1.28 # 如果源码用的是 smplx 接口参数说明torch1.13.1是兼容性较好的版本太新的 torch 可能因为torch.cat和torch.stack行为差异导致旧源码报错。smplx0.1.28是 SMPL-X 官方 python 包它兼容加载 SMPL 模型权重。如果你是纯 CPU 环境就把 torch 后缀换成cpu版本拟合速度会慢 5 倍左右但小分辨率图像还是能跑的。安装完后先跑一次 CPU 前向确认模型能加载再上 GPU不然排查问题时会分不清是代码问题还是驱动问题。4.2 模型权重文件从哪个文件加载加载后先检查什么SMPL 模型权重是 pickle 格式后缀一般为.pkl。网上能下载到官方发布的中性、男性和女性三套模板参数文件名常见的是SMPL_NEUTRAL.pkl、SMPL_MALE.pkl、SMPL_FEMALE.pkl。如果你的 python 源码里smpl.py的__init__写了model_path把对应 pkl 路径传进去即可。加载权重后第一件事是检查模型输出的网格顶点数是否为 6890、面片数是否为 13776。这两组数字是 SMPL 的标准结构标识如果代码输出 6890说明加载的是完整版本如果输出 6429 或 6892 这种说明权重文件不完整或经过了特殊裁剪。另外检查faces里是否有重复三角形索引少数重导出过程会把顶点排序打乱导致渲染时出现撕裂。import pickle import torch # 用 pickle 直接看权重文件里的关键键 with open(models/SMPL_NEUTRAL.pkl, rb) as f: data pickle.load(f, encodinglatin1) # 必须用 latin1否则 python3 报错 # 打印形状信息 print(v_template shape:, data[v_template].shape) # 期望 (6890, 3) print(f shape:, data[f].shape) # 期望 (13776, 3) print(weights shape:, data[weights].shape) # 期望 (6890, 24)逻辑说明第一行用pickle直接读取权重文件latin1编码是这里唯一的正确选择因为模型文件是用 Python 2 序列化的用默认utf-8解码会直接抛出 UnicodeDecodeError。打印出来的三个字段都是 SMPL 模型的必要数据v_template是 T-pose 下的标准顶点坐标f是面片索引weights是每个顶点对 24 个关节的蒙皮权重。如果在形状检查阶段发现前两个字段对不上直接换权重文件不要试图用代码补补来补去会引入不可见的网格畸形。属性说明v_template里的顶点坐标单位是米范围大约在 -1 到 1 之间这是后面做相机投影时确定先验尺度的关键。如果你拿到一批数据是厘米或毫米为单位的要先统一除以对应倍率否则拟合时平移参数会震荡。4.3 输入 2D 关键点从检测器输出到 SMPL 关节序的完整处理链获取 2D 关键点最常见的方法是用 OpenPose 或 MediaPipe 作为前置检测器输出 COCO 格式或自定义格式的坐标。这一阶段要做的处理顺序是先检测再对齐索引最后转成 tensor并过滤掉置信度低于阈值的点。import numpy as np import cv2 # 假设关键点来自 MediaPipe输出 33 个点格式 [x, y, visibility] def prep_2d_keypoints(mp_landmarks, image_w, image_h): # 关键点索引映射表mediapipe - smpl map_mp_to_smpl { 11: 16, # 左肩 12: 17, # 右肩 13: 18, # 左肘 14: 19, # 右肘 15: 20, # 左手腕 16: 21, # 右手腕 23: 1, # 左髋 24: 2, # 右髋 } keypoints np.zeros((24, 2), dtypenp.float32) conf np.zeros((24,), dtypenp.float32) for mp_idx, smpl_idx in map_mp_to_smpl.items(): x mp_landmarks[mp_idx].x * image_w y mp_landmarks[mp_idx].y * image_h v mp_landmarks[mp_idx].visibility keypoints[smpl_idx] [x, y] conf[smpl_idx] np.clip(v, 0.0, 1.0) # 低置信度点置零避免优化时被带偏 keypoints[conf 0.3] 0.0 return keypoints, conf逻辑说明这段代码把 MediaPipe 的坐标从归一化值转成像素坐标再按映射表填入 SMPL 关节索引。visibility字段是 MediaPipe 特有的置信度范围 0 到 1OpenPose 用的是score字段范围也是 0 到 1但两个检测器的分数分布差很多建议各自单独设阈值。第三行把低置信度关键点设为 0实际计算时再通过 conf 权重把这一项变成 0而不是真的让 2D 目标变成图像原点——后者会把骨架拉歪这是很多新手会犯的坑。5. 避坑清单SMPL 与 SMPLify 的 5 个高频踩坑点5.1 现象加载 pkl 权重文件报 UnicodeDecodeError原因极简单SMPL 官方权重是 Python 2 序列化的 pickle 文件Python 3 默认编码处理不了。很多人第一次跑直接报错然后去改系统编码或换 Python 版本浪费时间。解决办法打开 pkl 时强制指定encodinglatin1。如果你用的是smplx库它内部已经在加载时处理了这一点不需要额外改。如果你自己写加载逻辑统一用pickle.load(f, encodinglatin1)并确保读取后打印v_template.shape输出是(6890, 3)。如果看到(6890,)这种一维结果说明原始 pkl 里存的不是单纯数组而是带有元数据键的对象需要先用np.asarray()包一层。5.2 现象优化跑了 100 轮2D 重投影损失降不下去卡在 3000 左右原因通常是 2D 关键点索引没对齐或者相机内参给错了。比如 OpenPose 的鼻尖、眼睛这些头部关键点对应的是 SMPL 头部顶点但 SMPL 的 24 个关节里头部只有一个顶节点映射不准确会导致损失永远有一个固定残差。解决办法先把 2D 关键点和 SMPL 投影结果画在同一张图上每一轮打印投影坐标和检测坐标的差值。看差值最大的几个点是不是集中在手部或头部如果是单独调整这些关键点的权重或确认映射表。另外检查内参矩阵里的畸变系数是否有做手机照片和网络图片普遍存在径向畸变最好先做一次内参标定或者用cv2.undistort做预处理。5.3 现象拟合结果出现腿部交叉或手臂反向扭曲原因是 SMPL 的姿态参数是轴角表示轴角到旋转矩阵有周期性歧义优化器可能收敛到角度差了 180 度但旋转矩阵相同的解。姿态先验项能抑制一部分这种解但如果先验权重设太低就放开了。解决办法加大姿态先验项权重把pose_prior_weight从 0.5 调到 1.0 或 2.0。或者更直接初始化时把 theta 设成上一个动作捕捉帧的结果时序平滑不要每帧都从 0 开始。对单帧图像先做一次粗略姿态估计比如用 OpenPose 的 3D 输出做初始化再进入 SMPLify收敛速度快很多交叉翻车概率也低。5.4 现象渲染出来的网格整体在图像里偏大或偏小平移量反复横跳这是相机尺度未初始化的问题。SMPL 的顶点单位是米像素坐标是像素两者之间缺少一个尺度因子和主点偏移。如果平移量从 0 开始学优化器往往会把人体推远或拉近来补偿错误的相机参数。解决办法优化之前先把平移量初始化到使 SMPL 投影的关键点大致处于图像中心同时用一个基于人像高度的粗略度量尺度估计。常见做法是假设人的身高为 1.7 米图像里人的 bounding box 高度为 h_pixel那么尺度因子约等于1.7 / h_pixel把该值作为相机焦距的一部分嵌入投影。这一步对最终精度的影响比迭代次数都大务必先做。5.5 现象同一个人连续 10 帧的拟合结果抖动明显姿态不连贯原因是单帧优化完全独立没有利用时序信息。SMPLify 是逐帧优化的方法每一帧的 2D 检测噪声不一样优化到的姿态自然会在帧间跳动。如果你做的是动作捕捉而不是单图重建需要在帧与帧之间加平滑项。解决办法有二第一用上一帧的优化theta作为当前帧的初始化并加载一个小的时序先验比如限制每帧姿态变化量的 L1 范数第二把连续多帧一起做优化在损失函数里加入theta_t - theta_{t-1}的差分惩罚项。很多工业级动作捕捉方案比如市场里那些动捕手套方案都是在 SMPLify 框架上加了这个平滑项。6. 进阶用自由度换精度把 SMPL 输出接到三维重建和高保真动捕如果你的诉求不只在单张图而是把 SMPL 和 SMPLify 用于完整的三维重建流程有一个很实用的进阶方案先用 SMPLify 求出姿态参数再把姿态参数固定为常数重新优化形状参数——这样做通常比同时优化两项更容易收敛因为姿态的自由度远高于形状二者同优化时常出现姿态误差补偿了形状误差的耦合问题。我一般会做两阶段第一阶段只优化theta和transl固定beta为零向量第二阶段固定theta和transl只优化beta。第二阶段跑 30 轮就够看三维人体模型的表面是否出现明显鼓包或不合理凹陷如果不正常检查第二阶段的学习率是否从 0.02 降到了 0.005。进一步如果姿态和形状两个阶段跑通了可以考虑把 SMPL 模型替换成 SMPL-X——SMPL-X 在 SMPL 基础上加了手部和面部表情参数参数总量从 72 维扩展到了 119 维。这也是 SMPLify-X 被很多做虚拟人重建的项目采用的原因尤其当输入图像包含手部交互时SMPL-X 能减少手部关节错位问题。切换模型时唯一要改的是body_pose的维度切分SMPL 把 theta 分成 3 69SMPL-X 则要按你自己定义的关节数重新切分。换完之后用同一条 2D 关键点流水线重新拟合你会发现重投影误差会进一步下降因为手部多了自由度来表达细节。但代价是优化时间上涨92 维姿态参数的先验计算和梯度更新比原版慢明显一截这是值和不值之间最清晰的权衡。关于验证结果除了打印重投影损失我习惯把每帧输出的alpha值记录成 CSV统计一段动作序列里的变化幅度。如果alpha在 50 帧内波动超过 15 度说明该部位姿态解不稳定再去看是否该加时序平滑或者提高对应 2D 关键点的权重。这套习惯帮我调过了不少看似玄学但实际都是数值问题的案子。希望这些经验帮到你。本文还有配套的精品资源点击获取