最近做了个很有意思的实践
这个项目到底做了什么?
一句话:给一段手机拍的视频,先用 SAM2 把人抠出来,再用 GVHMR 恢复出标准的 SMPLX 人体模型,最终得到可以在三维空间里任意旋转观看的 3D 白模动画。
整个流程用到的核心技术栈:
| 技术 | 用途 | 运行环境 |
|---|---|---|
| SAM2(Segment Anything Model 2) | 视频人物分割 | Meta |
| Depth Anything V2 | 单目深度估计 | CPU |
| 3D Gaussian Splatting | 逐帧 3D 重建 | CPU (简化版) |
| GVHMR | 人体运动恢复 | CPU |
| SMPLX | 参数化人体模型 | PyTorch |
不需要深度相机,不需要动作捕捉服,不需要绿幕背景。只需要一段普通的手机视频和一台有 CPU 的电脑。
为什么要做这件事?
视频是二维的。你的眼睛(和相机)只能看到投影到平面上的光影信息,深度信息在成像的那一刻就丢失了。一个人侧身站着和正面站着,在照片上可能看起来差不多大。更麻烦的是,视频里的人可能在走动、转身、手臂挥舞——每一帧的姿态都不一样,而你需要从这些二维画面里"猜"出三维的形状和运动。
想象一个具体场景:你站在商场门口,看到一个街舞选手在表演 Breaking。你掏出手机拍了一段 10 秒的视频。你想把这段舞蹈"保存"下来,不是作为一段平面视频,而是作为一个可以在三维空间里 360 度观看、可以暂停在任何一帧仔细看动作细节的 3D 数字人。
这就是我们要做的事。
实战:用自己的数据集走完全流程
光说不练假把式。下面我们用data/custom/long_video这个数据集,完整走一遍处理流程。
这个数据集包含 300 帧图像(1280x720 分辨率,30 FPS),以及对应的 300 张分割掩码。掩码中白色区域是人物,黑色区域是背景——这是用 SAM2 做视频目标追踪后得到的。
整个处理流程可以用一张图概括:
原始视频 (300帧) -> SAM2分割 -> 深度估计 -> 3D点云 -> 4D组装 -> 高斯训练 -> GVHMR人体建模 -> 3D白模第一步:SAM2 视频分割
虽然我们的数据集已经有了掩码,但值得了解一下这些掩码是怎么来的。
SAM2(Segment Anything Model 2)是 Meta 推出的通用分割模型,支持视频目标追踪。你给它一张图片和一个提示(点、框或掩码),它就能在后续帧中持续追踪同一个目标。
在我们的场景中,SAM2 的propagate_in_videoAPI 被用来追踪视频中的每一帧人物,输出二值掩码。掩码文件名格式为frame_000000_target_1.png,其中target_1表示追踪到的第 1 个目标实例。
SAM2 相比逐帧分割的优势在于:它利用了帧间的时间一致性,追踪结果更连贯,不会出现"第 10 帧追踪到人、第 11 帧突然追踪到树"这种跳帧问题。
下面是我们在 kun 数据集上的 SAM2 分割效果展示(左边是原图,右边是分割掩码叠加效果,绿色区域为检测到的人物):
kun 数据集的 SAM2 分割效果。绿色区域为 SAM2 追踪到的人物掩码,可以看到即使在人物转身、走动等复杂动作下,分割结果依然连贯稳定。
分割的质量直接决定了后续重建的效果。如果掩码把背景也包含进来了,重建出来的 3D 模型就会"胖"一圈;如果掩码把人的手臂切掉了一部分,那部分就永远重建不回来了。
第二步:深度估计
有了掩码,我们知道"哪些像素属于人"。但光知道哪些像素属于人还不够——我们还需要知道这些像素距离相机有多远。
这就是深度估计的任务。我们使用Depth Anything V2模型,它是一个轻量级的单目深度估计模型,可以在 CPU 上运行。
深度估计的输出是一张深度图:每个像素的值表示该点距离相机的远近。
这里有一个微妙的问题:单目深度估计只能给出相对深度,而不是绝对深度。也就是说,它知道"A 点比 B 点远",但不知道"A 点距离相机 exactly 3.5 米"。我们需要一个缩放因子来把相对深度转换为绝对深度。在我们的实验中,这个缩放因子设为 5.0 米。
关键公式是针孔相机模型的反投影:
X = (u - cx) * depth / fx Y = (v - cy) * depth / fy Z = depth * scale其中(u, v)是像素坐标,(cx, cy)是主点,(fx, fy)是焦距,depth是深度图的值。通过这个公式,我们可以把每个像素从二维图像空间"反投影"到三维世界空间。
在我们的实验中,相机参数设置为fx=fy=600, cx=640, cy=360,深度缩放因子为5.0米。
第三步:逐帧 3D 点云生成
有了深度图和掩码,生成 3D 点云就是水到渠成的事:
- 遍历掩码中所有白色像素(属于人物的像素)
- 对每个像素,用上面的反投影公式计算其 3D 坐标
- 从原始图像中读取该像素的 RGB 颜色
- 将
(X, Y, Z, R, G, B)保存为一个 3D 点
每帧生成 5,000 到 17,000 个点(取决于人物在画面中的大小和掩码覆盖面积)。所有点保存为 PLY 格式的点云文件。
这个过程对每一帧独立执行。300 帧视频,每帧生成一个点云文件,最终得到 300 个 PLY 文件。
每个点云文件包含了该帧中人物的完整 3D 几何信息:形状、姿态、颜色。但它们是"各自为政"的——第 10 帧的点云和第 11 帧的点云之间没有显式的关联。
第四步:4D 时序组装
把 300 个独立的 3D 点云按时间顺序排列,就得到了一个 4D 序列。
"4D"这个概念其实很简单:3D 空间 + 1D 时间。每一帧是一个 3D 快照,按时间排列就形成了 4D 序列。
在我们的实现中,这些帧点云被组织在一个统一的数据结构中,支持逐帧播放、时间插值和变形追踪。
第五步:3D Gaussian Splatting 训练
点云虽然直观,但渲染质量有限。每个点就是一个小圆球,放大看会有明显的颗粒感。而且点云不支持新视角合成——你只能从原来相机的角度看,不能"绕到背后"去看。
为了得到更高质量的渲染和新视角合成能力,我们对每一帧训练3D Gaussian Splatting模型。
3D Gaussian Splatting 的核心思想是:用一组三维高斯椭球来表示场景。每个高斯有 5 组参数:
| 参数 | 维度 | 含义 |
|---|---|---|
| Position (xyz) | 3 | 高斯中心的 3D 位置 |
| Scale (s) | 3 | 三个轴的缩放比例 |
| Rotation (q) | 4 | 四元数表示的旋转 |
| Opacity (a) | 1 | 不透明度 |
| SH Coeffs | 48+ | 球谐函数系数,表示从不同角度看过去的颜色 |
训练过程是:初始化高斯参数(从点云初始化),然后通过可微分渲染器渲染出图像,与原始图像比较,计算损失,反向传播更新参数。经过反复迭代,高斯的位置、大小、颜色都会逐渐优化,最终渲染出和原始图像几乎一样的效果。
在我们的 CPU 模式下,每帧训练 50 次迭代,最终损失约 0.73。虽然不如 GPU 训练几千次的效果好,但已经能看出人物的基本形状和颜色分布了。
第六步:GVHMR 人体运动恢复
前面的步骤给了我们每一帧的 3D 形状,但这些形状是"散装"的——没有统一的人体模型约束。点云可能看起来像个人,但它没有"24 个关节、10 个体型参数"这样的结构化表示。
GVHMR(Gravity-View Human Motion Recovery)的加入,让我们能从单目视频中恢复出标准的 SMPLX 人体模型参数。
GVHMR 的核心创新是重力视角坐标系(Gravity-View Coordinates)。传统方法直接在相机坐标系或世界坐标系下预测人体运动,但这两种坐标系都有问题:相机坐标系下,人体的运动会随着相机转动而"虚假"变化;世界坐标系下,"上"方向的定义是任意的。GVHMR 利用重力方向定义了一个中间坐标系,让预测变得稳定且物理合理。
GVHMR 的处理流程分三步:
- YOLO 追踪:在每一帧中检测并追踪人物边界框
- ViTPose 姿态估计:提取人体关键点特征
- Transformer 推理:将特征序列输入 GVHMR 模型,输出 SMPLX 参数
GVHMR 的输出是每帧的 SMPLX 参数:
body_pose(63 维):21 个关节的旋转角度,编码了人体的姿态betas(10 维):体型参数,决定了人是高是矮、是胖是瘦global_orient(3 维):全局朝向,决定了人面朝哪个方向transl(3 维):全局位移,决定了人在空间中的位置
有了这些参数,我们就可以用 SMPLX 模型渲染出标准的 3D 人体网格(白模)。这个白模有 10,475 个顶点和 20,908 个三角面片,是一个完整的人体表面模型。
下面是我们用 GVHMR 对 自己数据集生成的 SMPLX 白模动画:
GVHMR 恢复的 SMPLX 人体模型。可以看到人物的行走、转身等动作被准确还原,体型保持一致,运动流畅自然。坐标系已修正为 Z 轴朝上,人物自然站立。
这个白模动画展示了 GVHMR 的核心能力:从单目视频中恢复出完整的人体运动参数。注意,这里的坐标系已经修正为 Z 轴朝上(重力反方向),所以人物是自然站立的,而不是趴着或躺着。
实战二:用 kun 数据集验证流程
为了验证处理流程对不同数据集的适应性,我们用data/custom/kun数据集又跑了一遍完整流程。
kun 数据集特点
kun 数据集包含908 帧图像(比 long_video 多了 3 倍),同样有对应的 SAM2 分割掩码。这个数据集的人物动作更加复杂,包含了更多的转身和姿态变化。
批处理策略
908 帧视频如果一次性送入 GVHMR 推理,预处理(YOLO + ViTPose + ViT 特征提取)会超过 10 分钟,很容易超时。我们的解决方案是分批处理:
- 将 908 帧按每批 150 帧切分,共 6 批
- 每批独立创建 masked 视频、运行 GVHMR 推理、渲染白模
- 最后将所有批次的白模帧合并,重新切分为 5 个均匀大小的 GIF
| 批次 | 帧范围 | 帧数 | 预处理耗时 | 推理耗时 |
|---|---|---|---|---|
| 0 | 0-149 | 150 | ~8 min | 0.40s |
| 1 | 150-299 | 150 | ~8 min | 0.38s |
| 2 | 300-449 | 150 | ~8 min | 0.38s |
| 3 | 450-599 | 150 | ~6 min | 0.27s |
| 4 | 600-749 | 150 | ~6 min | 0.28s |
| 5 | 750-899 | 150 | ~6 min | 0.27s |
总计处理了 900 帧(最后 8 帧因太短被跳过),生成了 450 帧白模动画。
坐标调整:让人物踩到地面
在渲染白模时,我们发现了一个问题:人物的脚没有踩在地面上,而是悬浮在空中或者陷入地下。
这是因为 GVHMR 输出的transl参数是相对于某个参考点的,不一定保证脚底刚好在 Z=0 的平面上。
我们的解决方案是:逐帧调整 Z 坐标,让每帧的最低点(通常是脚底)对齐到 Z=0。
# 对每一帧min_z=vertices[:,2].min()# 找到最低点vertices[:,2]-=min_z# 整体上移,让最低点到达 Z=0这个简单的调整让白模动画看起来更加自然——人物像是站在地面上,而不是漂浮在空中。
结果展示
450 帧白模动画(600x600 分辨率,15 FPS)被均匀切分为 5 个 GIF,每个约 5 MB:
第 1 段(帧 0-89,0-6 秒):
第 2 段(帧 90-179,6-12 秒):
第 3 段(帧 180-269,12-18 秒):
第 4 段(帧 270-359,18-24 秒):
第 5 段(帧 360-449,24-30 秒):
kun 数据集的 SMPLX 白模动画,共 450 帧(30 秒)。5 个 GIF 均匀覆盖整个视频序列。坐标已调整,脚底踩在地面上。灰色半透明平面表示地面(Z=0)。
对比 long_video 和 kun
| 指标 | long_video | kun |
|---|---|---|
| 总帧数 | 300 | 908 |
| 处理帧数 | 150 | 900 (6批 x 150) |
| 视频时长 | 5 秒 | 30 秒 |
| 总预处理耗时 | ~6.6 分钟 | ~42 分钟 |
| 总推理耗时 | 0.26 秒 | ~2.3 秒 |
| 白模帧数 | 75 | 450 |
| GIF 数量 | 1 个 | 5 个 (每个 ~5 MB) |
两个数据集的处理结果都非常成功,证明了我们的流程具有良好的泛化能力。
技术细节深挖
为什么选择 Gaussian Splatting 而不是 NeRF?
NeRF(Neural Radiance Field)用神经网络隐式表示场景,渲染质量高但训练慢。3D Gaussian Splatting 用显式的几何基元(高斯椭球)表示场景,渲染速度快(可以实时),训练也快(分钟级)。
对于我们的任务,Gaussian Splatting 的优势更明显:
- 可编辑性:每个高斯是一个独立的几何基元,可以单独移动、缩放、删除
- 可组合性:不同帧的高斯可以组合在一起,形成 4D 模型
- 渲染速度:可微分光栅化器让训练和渲染都很快
- 显式表示:不像 NeRF 那样是一个"黑盒"神经网络,高斯的参数是直观的、可解释的
CPU 模式下的挑战
我们的实验环境是纯 CPU(没有 GPU)。这带来了一些挑战:
- 深度估计慢:Depth Anything V2 在 CPU 上每帧需要几秒
- 高斯训练慢:可微分渲染在 CPU 上比 GPU 慢 10-100 倍
- GVHMR 推理:Transformer 模型在 CPU 上推理需要几分钟(预处理约 6-8 分钟,推理不到 1 秒)
但 CPU 模式的好处是:任何人都能跑,不需要昂贵的 GPU。对于演示和验证目的,CPU 模式足够了。
掩码质量对重建的影响
分割掩码的质量是整个流水线的瓶颈。如果掩码不准确:
- 过分割(掩码包含了背景):重建出来的 3D 模型会有"鬼影",人物周围漂浮着背景的碎片
- 欠分割(掩码切掉了人物的一部分):人物的手臂、腿等部位可能缺失,重建不完整
在我们的数据集中,SAM2 的掩码质量总体不错,但有些帧的掩码边缘不够精确(比如手指、头发丝),这会导致重建的细节丢失。
中间结果数据一览
让我们用数字说话。以下是处理过程中产生的关键数据:
点云统计
| 指标 | 数值 |
|---|---|
| 总帧数 | 300 帧 (采样处理 42 帧) |
| 每帧点数 | 5,000 - 17,000 |
| 总点数 | 约 400,000 个 3D 点 |
| 点格式 | XYZ + RGB (6 个浮点数) |
| 文件格式 | PLY |
| 相机参数 | fx=fy=600, cx=640, cy=360 |
高斯训练统计
| 指标 | 数值 |
|---|---|
| 训练帧 | 第 0、20、40 帧 |
| 每帧迭代 | 50 次 |
| 每帧高斯数 | 约 870-1,000 个 |
| 最终损失 | ~0.73 |
| 图像尺寸 | 180x320 (缩小以加速) |
| 损失函数 | L1 + SSIM |
GVHMR 推理统计
| 指标 | 数值 |
|---|---|
| 处理帧数 | 150 帧/批 (5 秒/批) |
| 预处理耗时 | ~6-8 分钟/批 (CPU) |
| 推理耗时 | 0.27-0.40 秒/批 |
| SMPLX 顶点数 | 10,475 |
| SMPLX 面数 | 20,908 |
| 输出参数 | body_pose(63) + betas(10) + orient(3) + transl(3) |
掩码覆盖率变化
有趣的是,掩码覆盖率(掩码中白色像素占整帧的比例)在整个视频序列中是变化的:
- 第 0 帧:1.5%(人物在画面中很小)
- 第 75 帧:6.5%(人物走近了)
- 第 135 帧:26.7%(人物占据了画面的四分之一)
- 第 140 帧:31.0%(人物最大的一帧)
- 第 190 帧:8.8%(人物走远了)
这种覆盖率的变化直接影响点云的密度:覆盖率大的帧,点云更密集,重建质量更高;覆盖率小的帧,点云更稀疏,细节可能丢失。
踩过的坑和经验教训
在跑这个流水线的过程中,我们遇到了不少坑。这里分享几个最有价值的经验:
坑一:坐标系方向搞反了
SMPLX 模型的默认坐标系是 Y 轴朝上,但 Matplotlib 的 3D 绘图是 Z 轴朝上。如果你不做坐标转换,渲染出来的白模就是趴着或者躺着的——看起来像是人在做俯卧撑。
解决方法很简单:交换 Y 和 Z 轴,并翻转前后方向:
vertices_fixed[:,0]=vertices[:,0]# X 不变vertices_fixed[:,1]=-vertices[:,2]# Y = -Zvertices_fixed[:,2]=vertices[:,1]# Z = Y坑二:CPU 模式下 chumpy 不兼容
SMPL 模型的原始格式依赖 chumpy 库,而 chumpy 在 Python 3.10+ 上已经不能用了(inspect.getargspec被移除了)。解决方法是使用项目中提供的SmplLite和SmplxLite类,它们绕过了 chumpy 的依赖,直接用 PyTorch 张量加载模型参数。
坑三:深度缩放因子的选择
深度缩放因子(depth_scale)决定了重建出来的 3D 模型的绝对大小。设得太小,人物就像一个手办;设得太大,人物像一个巨人。我们最终选择了 5.0 米,这是一个比较合理的值——它让重建出来的人物高度大约在 1.7-1.8 米之间,接近真实人体身高。
但这个值不是万能的。如果你的视频里人物离相机很近,可能需要调小;如果人物离得很远,可能需要调大。最好的方法是:先跑一帧,看看重建出来的人物大小是否合理,然后调整缩放因子。
总结
整个项目做的事情可以用一句话概括:用 SAM2 把人从视频里抠出来,用 GVHMR 恢复出标准的 SMPLX 人体模型,最终得到可以在三维空间里任意旋转观看的 3D 白模动画。
核心技术栈:SAM2(分割)+ Depth Anything V2(深度估计)+ 3D Gaussian Splatting(3D 重建)+ GVHMR(人体运动恢复)+ SMPLX(参数化人体模型)。
我们的实验展示了完整的处理流程:从视频出发,经过分割、深度估计、点云生成、4D 组装、高斯训练、GVHMR 人体建模,最终得到一个多层次的 3D 人体资产。
这个资产可以用于:
- 动作回放:在 3D 查看器中任意角度观看人物动作
- 动作编辑:修改 SMPL 参数来调整姿态
- 虚拟现实:将数字人导入 VR/AR 场景
- 动作分析:提取运动学参数用于运动科学研究
- 游戏开发:将真实人物的动作导入游戏引擎
从一段普通视频到一个完整的 3D 数字人——而我们在纯 CPU 环境下走通了整个流程,证明了这项技术的可及性。
GVHMR 项目:https://github.com/Kehan-L/GVHMR
SAM2 项目:https://github.com/facebookresearch/sam2
Depth Anything V2:https://github.com/DepthAnything/Depth-Anything-V2