做 3D 视觉和视频生成方向时,我一直很关注一个问题:能不能让一个普通视频里出现的物体“自己学会动”,并且把这种运动迁移到可交互的 3D 资产上?最近在看相关工作时,标题为BLARM: Animating 3D Objects from Video via Blending Latent Rigid Motion Primitives的工作引起了我的注意。
单看标题,它涉及的信息量很大:视频输入、3D 对象动画、潜在空间、刚性运动、运动基元、混合机制。这些词拆开都能理解,但合在一起,背后对应的是一整套“从视频中学习可动画化 3D 表示”的方法论。
这篇文章不打算照搬论文摘要,而是以标题为切入点,把 BLARM 涉及的核心概念、技术动机、可能的技术路线、工程落点以及复现时的常见坑点梳理一遍。适合有一定 3D 视觉基础、正在入门动态场景重建或神经渲染的开发者阅读;如果你完全没有任何基础,可以先看前两章,再决定是否需要深入。
1. 论文标题拆解:BLARM 到底想解决什么问题
BLARM 这四个字母,从标题可以理解成BlendingLatentRigidMotion 相关内容的缩写。但只看名字没有意义,真正有价值的是它背后定义的问题:如何从一段视频中,让一个静态的 3D 对象按照视频中的方式动起来。
这和你平时接触的 3D 动画流程非常不一样。传统流程需要建模师建模、绑定师做骨骼、动画师 K 帧或做动捕数据重定向。而现在这类方法尝试把整个流程压缩成一个可学习的模型:输入视频,输出一个具有动画能力的 3D 表示。
1.1 从标题的关键词理解任务链路
我们可以把标题拆成三个动作阶段:
- Input from Video:输入是一段或多段视频。视频里包含物体运动信息,但通常是单目视角,没有深度,没有 3D 标注。
- Animating 3D Objects:目标是让某个物体的 3D 表示能够被驱动、旋转、变形,甚至生成从未出现过的新姿态。
- Blending Latent Rigid Motion Primitives:这是实现动画的核心机制。系统不是直接预测每个顶点的位移,而是在一个潜在空间中对多个刚性运动基元做混合,然后作用到物体表面。
从这个角度看,BLARM 属于动态 3D 重建与可动画化数字资产生成的交叉方向。它强调的目标不是简单重建几何,而是重建“运动的规律”。
1.2 动态 3D 重建和传统 3D 动画的区别
在传统的 3D 内容生产链路里,动画是需要大量人工介入的。你要手动定义骨架 Joint、蒙皮权重、控制器等。这类资产很难直接从 RGB 视频生成,因为视频只有像素变化,没有拓扑、没有骨架构型。
而像 BLARM 这类方法,本质上是希望神经网络能从像素级监督中“隐式学会”物体的运动结构。它不一定要显式输出一个骨架,但它要能表达:物体哪个区域在动、动的幅度多大、局部区域之间的关系是什么。
一个比较形象的理解方式是这样:一段视频里的人物走路时,胳膊、腿、头、躯干都有各自的运动。从全局看,整个人体是非刚性运动;但从局部看,大腿可以近似成刚体,小腿可以近似成另一个刚体,两者通过关节连接。因此,复杂运动可以用若干“局部刚性运动”的组合来表示。
1.3 核心问题:运动结构与 3D 几何如何联合学习
这里最大的难点在于,模型并不知道一段视频里物体的“关节”在哪里,也不知道哪些区域应该绑定在一起运动。它同时要做两件事:
- 估计出物体每一帧的 3D 几何和外观;
- 估计出控制这些几何变化的运动参数。
这两件事互相影响。如果你不知道物体长了什么样,就很难判断某个像素变化是几何变化还是视角变化;如果你不知道运动模型,也很难从多个视角把同一块表面拼起来。
BLARM 这种“潜在刚性运动基元混合”的思路,是希望通过一种结构化但足够灵活的表征来统一解决这两个问题。用刚性运动基元约束局部一致性,用混合权重表达非刚性形变,这样既不会因为刚体约束太强导致模型无法表达复杂动作,也不会因为完全自由变形导致重建结果不稳定。
2. 先补背景:刚性运动、非刚性运动与运动基元
想要理解 BLARM,不能绕开三块基础概念:刚体运动、非刚体形变、运动基元。这三块概念同时出现在标题里,说明作者试图组合它们各自的优势。
2.1 刚体运动和非刚体运动
先给一个通俗解释。把一支笔从桌面拿到空中,无论怎么转,笔上任意两点之间的距离始终不变,这种运动就是刚体运动。在数学上,刚体运动可以用旋转矩阵和平移向量表示,也就是常见的 SE(3) 变换。
但如果运动对象是一块布、一张脸、一双手,情况就复杂了。布料会皱褶,脸部肌肉会牵动皮肤,手指弯曲时,指尖和掌根的距离在变化。简单用一个全局刚体变换不可能描述这种形变,因此需要非刚体模型。
非刚体模型也有不同层次。有些方法直接预测稠密位移场,比如对每个采样点输出一个三维位移向量;有些方法则使用骨骼蒙皮,比如先定义骨架,再让每个表面点根据蒙皮权重跟随骨骼运动。BLARM 标题中的“Rigid Motion Primitives”暗示的是一种介于两者之间的思路:多个局部刚体运动组合成全局非刚体运动。
2.2 什么是运动基元
“Motion Primitive”这个概念在机器人学里出现得比较多,指的是复杂运动可以分解为一系列基本的、可复用的运动片段或控制模态。比如机器人行走可以分解为抬腿、跨步、落地、身体重心转移等基本动作。
在 BLARM 这类 3D 视觉工作里,运动基元更像是“一组候选的刚体运动”。模型从视频中学习到这组基元之后,每个局部区域选择若干个基元进行加权混合。你可以把基元理解为运动空间中的“词典”,而混合权重就是使用词典表达某种动作的编码。
使用运动基元带来的好处是,它限制了表达空间。直接预测每个点的自由度很高,容易出现局部不合理的形变,比如表面扭曲、穿插、撕裂。而只允许每个点在少数几个刚体运动之间做混合,会让结果在局部保持空间连续性和几何一致性。
2.3 为什么局部刚体假设对视频动画很重要
生物运动、日常物体运动,很多都能拆成局部近似刚体运动。试想一个带关节的手臂:同一块体表上的皮肤点,在运动中彼此距离变化很小,只是整块体表绕关节旋转。这种情况下,强制让同一个小区域的点共享同一个刚体变换,可以显著提高估计稳定性。
而且,单目视频本身存在很强的深度模糊性。如果模型可以自由地给每个点安排一个运动向量,很容易出现“用运动解释噪声”的情况。这个时候,刚体先验相当于一种正则化,能减少病态程度,让优化过程更快收敛到符合真实物理规律的解。
3. 核心思路:潜在刚性运动基元的混合
BLARM 标题里最关键的动作词是 “Blending”,也就是混合。为什么不是直接选择某一个最合适的运动基元,而是要做混合?因为真实物体表面很少能被切分成完全独立的刚性部件。相邻部件之间存在过渡区域,比如肩膀到大臂、腰部到臀部。在过渡区域,运动应该柔和地从一个基元过渡到另一个基元。这个柔和的过渡,就是通过 Blending 完成的。
3.1 Latent 空间在这里承担什么角色
Latent Space,也就是潜在空间或隐空间,在深度学习里是一种低维的中间特征表达。视频里同一类物体有很多种运动方式。比如一个纸盒可以向左倒、向右倒、向前滑动,这些运动如果直接用高维输入去建模,很难泛化;但如果把每一时刻的运动状态压缩成一个 latent code,模型就能在低维空间里学会这些运动模式之间的关系。
BLARM 标题里的 “Latent Rigid Motion Primitives”,读起来更像是在潜在空间中维护一组刚性运动基元。这组基元不一定等同于物理上的骨骼,而是一组有结构性的潜变量。模型的职责是,给定一个当前时间步或姿态状态,先编码成 latent code,再通过某种解码方式得到每个基元的参数和空间中每个位置对这些基元的混合权重。
这种解耦有一个明显好处。几何重建可以集中在 canonical 空间处理,也就是物体在某个标准姿态下的形状;运动模块只需要负责预测从 canonical 姿态到当前视频帧姿态的变换。这样视频中不同帧共享同一套几何,模型不容易出现每帧重建一个割裂模型的问题。
3.2 Blending 的物理含义与数学形式
混合权重在传统蒙皮算法里有一个成熟对应物:Skinning Weight。每个顶点对每个骨骼都会有一个权重值,权重越大,这个顶点受该骨骼运动影响越大。
如果类比到 BLARM,每个运动基元就相当于一根“隐式骨骼”,混合权重就是让表面点在这组基元之间取加权平均。最终某个点的运动可以写成一个公式:
[ \text{Point}' = \sum_{k=1}^{K} w_k(\mathbf{x}) \cdot T_k \cdot \text{Point} ]
其中:
- K 表示运动基元数量;
- (w_k(\mathbf{x})) 是三维空间中位置 (\mathbf{x}) 对第 k 个基元的权重;
- (T_k) 是第 k 个基元的刚体变换;
- Point 是 canonical 姿态下的点。
这里需要注意的是旋转矩阵。刚体变换中的旋转分量属于特殊正交群 SO(3),不能简单对多个旋转矩阵做线性平均。如果你想学习潜在空间里两个刚体运动的中间状态,需要对旋转做更有原则的插值,比如四元数插值、李代数上的线性插值等。否则两个相反旋转平均后可能得到一个接近零矩阵,动画会产生明显的不自然扭曲。
3.3 为什么这种 “局部刚体 + 全局混合” 比纯端到端位移更合理
如果直接让模型回归每个点的三维位移,其实是一个完全自由的运动场建模。缺点在于,没有局部结构约束的稠密运动场非常容易产生过拟合。尤其在单目视频中,模型只需要把当前帧像素拟合好,但不一定能学到合理物体运动规律。
而局部刚体假设可以让同一片表面区域的点倾向于拥有相近的旋转平移,减少运动场的局部突变。混合权重在网络中通常通过一个空间平滑的函数预测,这样相邻点的权重分布也会平滑,最终动画效果自然很多。加上潜在刚性运动基元数量有限,整体模型的参数复杂度远低于逐点运动预测,对于数据量很少的单目视频来说更容易训练。
4. 从论文标题出发的完整技术路线
需要提前说明:这一节梳理的是从标题出发、结合当前动态三维重建常见技术栈得出的理解路线,并不代表 BLARM 官方实现的一定是这个结构。如果之后你拿到论文原版,应该以论文正文和官方开源代码为准。这个路线的主要作用是帮助你读图、读代码时心里有一个框架。
4.1 一条通用的 Pipeline 框架
整体思路大致可以分成以下几步:
- 从输入视频中重建一个 Canonical 3D 表示。
- 对每一帧或一段时间片段提取运动上下文特征。
- 在潜在空间中生成或选择一组刚性运动基元。
- 对 3D 空间的每个位置预测对这些运动基元的混合权重。
- 将 canonical 点按混合结果变换到目标帧。
- 通过渲染得到像素,并与真实视频帧做损失计算。
从工程实现来看,它很像 “NeRF / 3DGS + 动态形变场” 的变体,区别在于形变场不是直接预测位移,而是通过多个刚性变换加权混合得到。
4.2 Canonical 表示与运动估计解耦
一个值得关注的设计选择是:静态几何和动态运动是不是分成两个独立模块。
在 NeRF 类方法中,常见的做法是用一个静态场表示密度和颜色,再用一个形变场把采样点从观察帧映射到 canonical 帧。BLARM 这种题目暗示的方法,运动部分通常也设计成独立模块。Canonical 空间只负责回答“这个物体的标准姿态长什么样”,运动模块负责回答“某个时间点它的各个局部区域分别应该怎么变换”。
这种解耦逻辑适合做扩展。当你已经有了一个标准姿态的 3D 资产后,可以换用不同的视频来学习新的动作;理论上,模型可以只更新运动模块,而不需要重新优化几何。但这样做的前提是,几何模块本身足够稳定,并且坐标空间对齐方式一致。
4.3 模型输出的结构
BLARM 要输出的不是一张图片,也不只是一组顶点位置,它需要输出一套可供驱动和渲染的运动表示。通常由三部分组成:
- 一个 canonical 3D 表征,可能是显式网格,也可能是神经辐射场或 3D 高斯点云;
- 一组潜在刚性运动基元的参数,每个基元包含旋转和平移;
- 一个混合权重的预测器,它接收三维坐标和运动条件,输出每个位置的权重分布。
如果还需要支持新视角渲染或直接编辑,通常还会加入相机参数模块和材质光照模块。个人建议,读这类论文时最好把“几何重建、运动变形、渲染合成”三个模块分开理解,这样不容易被复杂的整体框架绕晕。
5. 环境准备与复现建议
因为 BLARM 属于较新的研究型方向,读者可能会有两种需求:一种是只希望读懂论文、理解实验图表;另一种是希望复现跑通甚至基于它做二次开发。两种需求对环境的要求完全不同。
5.1 先确定目标是阅读还是复现
如果只是想读懂方法,只需要准备论文 PDF、官方项目主页和代码仓库即可。阅读时重点对比 Model Overview 图和公式里的符号定义。不要一上来就陷入代码细节。
如果要复现,就需要至少准备一块支持 CUDA 的 GPU,显存建议 24GB 以上。因为绝大多数动态神经渲染或 3D 高斯相关方法在训练阶段非常占用显存,官方实验也很少在小显存显卡上跑。
5.2 推荐的通用复现环境
由于不同论文依赖的具体版本差异很大,最稳妥的方法是先看官方代码仓库中的 requirements.txt、environment.yml 或 Dockerfile。以下是一个常见的 Python 和 PyTorch 环境模板,实际使用时要按项目要求调整版本。
# 创建虚拟环境 conda create -n blarm python=3.10 conda activate blarm # 安装 PyTorch,注意根据 CUDA 版本选择对应的安装命令 pip install torch torchvision # 常用 3D 视觉库,以项目要求为准 pip install pytorch3d pip install nerfacc pip install opencv-python pip install imageio imageio-ffmpeg pip install tqdm tensorboard这里特别提醒一点:PyTorch3D 对 PyTorch 版本和 CUDA 版本比较敏感。不要直接执行pip install pytorch3d期望它自动装到合适版本,推荐到 PyTorch3D 官方仓库的安装页面,选择与当前 PyTorch 对应的预编译轮子。
5.3 示例项目目录结构
一个典型的动态 3D 重建项目,代码目录大致长这样:
blarm_project/ ├── config/ │ ├── train.yaml │ └── eval.yaml ├── datasets/ │ └── custom_video_dataset.py ├── models/ │ ├── canonical_field.py │ ├── motion_primitives.py │ ├── renderer.py │ └── losses.py ├── scripts/ │ ├── train.py │ └── eval.py ├── checkpoints/ └── outputs/如果一个开源仓库在结构和命名上高度接近这个惯例,那么上手成本通常不会太高。你需要重点关心的文件是models/motion_primitives.py或类似命名的文件,它是 BLARM 这类方法的核心。
6. 最小复现思路与 PyTorch 示意代码
这一节我会提供一个概念性示例,用于帮助理解潜在刚性运动基元混合的思路。它并非 BLARM 的官方实现,也不会真的跑出论文效果,但它能给你一个代码骨架,让你知道哪些地方需要网络预测、哪些地方是固定计算。
6.1 定义一组潜在运动基元
最朴素的思路是:把每个基元定义成一组可学习的 embedding,再根据当前运动编码和三维坐标,动态解码出基元变换。
""" motion_primitives_demo.py 说明:以下代码只用于演示“刚性运动基元混合”的代码结构。 """ import torch import torch.nn as nn class RigidMotionPrimitiveBank(nn.Module): def __init__(self, num_primitives=32, hidden_dim=128): super().__init__() self.num_primitives = num_primitives self.hidden_dim = hidden_dim # 每个运动基元对应一个可学习 embedding self.primitive_embeds = nn.Parameter( torch.randn(num_primitives, hidden_dim) * 0.02 ) # 根据运动编码预测当前时刻每个基元是否活跃 self.primitive_activator = nn.Sequential( nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, num_primitives), ) def forward(self, pose_code): """ pose_code: [batch_size, hidden_dim] 返回每个运动基元的激活分数。 """ scores = self.primitive_activator(pose_code) # 这里没有使用 softmax,方便后续利用温度控制稀疏性 return scores class SimpleMotionRouter(nn.Module): """ 根据空间位置和运动状态,预测一个点对每个运动基元的混合权重。 """ def __init__(self, num_primitives=32, pos_dim=3, hidden_dim=128): super().__init__() self.num_primitives = num_primitives self.net = nn.Sequential( nn.Linear(pos_dim + hidden_dim, hidden_dim), nn.SiLU(), nn.Linear(hidden_dim, hidden_dim), nn.SiLU(), nn.Linear(hidden_dim, num_primitives), ) def forward(self, spatial_pos, pose_code): """ spatial_pos: [num_points, 3] pose_code: [batch_size, hidden_dim] 或 [hidden_dim] """ point_feat = spatial_pos # 如果输入有 batch,就通过广播把 pose_code 拼到每个点上 merged = torch.cat([point_feat, pose_code], dim=-1) logits = self.net(merged) weights = torch.softmax(logits, dim=-1) return weights在这段代码中,可以看到两个核心模块:
- PrimitiveBank 维护一组运动基元的潜表达;
- MotionRouter 负责输出空间中的混合权重。
为什么 MotionRouter 要把空间位置也作为输入?因为不同位置的局部运动不一样。比如人的手指尖和手腕,虽然处于同一运动状态,但它们的刚体运动模式不同。只有注入空间位置信息,路由网络才能学会按空间区域分配权重。
6.2 把基元变换应用到一个 3D 点上
接下来演示一个最简化的变换逻辑。为了便于理解,下面代码里逐点循环处理,实际项目会使用并行矩阵运算优化。
def apply_blend_to_point(weights, rotations, translations, point): """ 对单个 3D 点应用“刚性运动基元混合”。 weights: [num_primitives] rotations: [num_primitives, 3, 3] translations: [num_primitives, 3] point: [3] 返回混合后的 3D 点 [3] """ transformed_points = [] for k in range(weights.shape[0]): v = rotations[k] @ point + translations[k] transformed_points.append(v) stacked = torch.stack(transformed_points, dim=0) # [K, 3] weight = weights.unsqueeze(-1) # [K, 1] out = (weight * stacked).sum(dim=0) return out这里的“混合”是线性加权。严格来说,旋转矩阵加权平均并不属于 SO(3) 群上的合法操作,例如两个相差 180 度的旋转平均后可能变成零矩阵。在正式实现时作者通常会把旋转参数表达成四元数、轴角或李代数,并在李代数空间做插值。但这段代码用来理解整体流程还比较直观。
6.3 训练框架伪代码
训练阶段通常不会直接从单个点出发,而是从 canonical 空间采样大量三维点,经过运动变换后渲染成 2D 图像,与视频真帧计算损失。训练循环的伪代码如下:
# 训练伪代码:用于展示整体数据流 for step, batch in enumerate(train_loader): # batch 包含: # rgbs: 当前帧图像 # masks: 前景掩码 # camera_poses: 相机外参 # time_codes: 时间或运动编码 optimizer.zero_grad() # 1. 从 canonical 空间采点 coords = sample_canonical_points(batch["num_points"]) # 2. 对每帧估计 pose_code pose_code = motion_encoder(batch["time_codes"]) # 3. 预测刚体基元和混合权重 primitive_scores = primitive_bank(pose_code) rotations, translations = primitive_bank.decode_primitive(primitive_scores, coords) blend_weights = motion_router(coords, pose_code) # 4. 将 canonical 点变换到目标姿态 deformed_coords = apply_blend_to_points( blend_weights, rotations, translations, coords ) # 5. 渲染颜色 rgbs_pred, density = renderer(deformed_coords, view_dirs) # 6. 计算损失 loss_rgb = l1_loss(rgbs_pred, batch["rgbs"]) loss_mask = binary_cross_entropy(density, batch["masks"]) loss_reg = motion_regularizer(blend_weights) loss = loss_rgb + loss_mask + loss_reg loss.backward() optimizer.step()这个伪代码把很多细节省略了,比如体积渲染怎么进行、视角方向如何处理、背景如何建模等。但如果把握住这个主干,你在阅读 BLARM 或类似方法官方代码时,就知道先看哪个函数、每一步 Tensor 的维度应该是什么。
6.4 运行与验证时的预期结果
在不改动结构的前提下,直接跑随机初始化的模型,输出会非常模糊。正常的实验流程应该是:
- 先冻结运动模块,仅训练 canonical 几何,让模型能够重建出一个标准姿态;
- 再逐步放开运动模块,让几何开始随时间变化;
- 最后联合微调,让动画效果更加精细。
训练过程中,你可以通过 TensorBoard 观察渲染的 RGB 序列是否逐步逼近输入视频帧。如果出现颜色清晰但轮廓乱动的情况,说明运动模块学习不稳定;如果轮廓稳定但纹理模糊,则多观察几何模块是否过拟合到了某个视角。
7. 如何把这类思路迁移到实际动画工程
读完论文,很多开发者会思考一个问题:这种学术方法能不能用到游戏、短视频、电商展示等场景里?完全照搬研究方案肯定不行,但它背后的几个关键思想是可以迁移的。
7.1 用视频生成可控动画资产
传统 3D 资产从建模到绑定需要大量人工,而视频驱动的动画方法,理论上能缩短这个流程。拍摄一段物体旋转或运动的视频,算法自动重建 3D 几何,并学习物体的运动规律,最终交付一个可以被驱动的新资产。
在企业落地时,要考虑的第一件事是数据规范化。视频拍摄的机位、背景、光照是否可控,直接影响重建质量。如果背景杂乱且物体运动幅度太复杂,直接端到端训练会很容易失败。更稳妥的做法是先做目标分割,把视频裁剪成相对干净的物体运动序列,再交给模型。
7.2 将潜在混合权重理解为“自动化蒙皮”
传统蒙皮需要动画师为网格顶点手绘权重。而 BLARM 这类方法在学到的混合权重,本质就是“自动蒙皮权重”。这个权重输出后,可以转换成标准蒙皮格式,进入传统游戏引擎管线。
这意味着你不需要把整个 NeRF 或 3DGS 模型部署到游戏里,也能使用它学到的运动结构。你可以先从 latent motion primitive 中导出主要刚体部件,然后再导出每个点的 soft blending weight,最后改用线性混合蒙皮 LBS 算法驱动一个手动重建的高模。这种方式会丢失一部分细节,但可以兼容标准渲染管线。
实际项目里关键点在于运动基元数量 K 的选择。K 太小,无法表达复杂动作;K 太大,权重不稳定,容易出现某些基元没有实际意义或互相竞争。一般建议从 K=16 或 K=24 开始实验,并根据物体类别和动作复杂度做调整。
7.3 可拓展的几个方向
这类方法的可拓展方向很多:
- 从单视频变成多视频输入,期望模型学到类别级别的运动先验;
- 加入多视角约束,缓解单目视频深度歧义;
- 引入语义分割或关键点检测作为辅助监督,让混和权重更有物理意义;
- 与扩散模型结合,从单张图片生成可动画的 3D 资产,再用视频控制运动。
这些方向不一定都来自 BLARM 原文,而是值得你在读过标题方法后做的后续思考。我认为,理解一个研究课题最好的方式不是背结论,而是尝试沿着几个约束方向延伸,看看哪里可能产出自己的 idea。
8. 常见疑问与踩坑记录
把阅读和实验过程中容易被卡住的问题梳理一下。这些问题一部分来自原理理解层面,一部分来自代码实现,整理成表格方便查阅。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 三维重建结果整体正确但动画很抖 | 运动权重缺乏时序平滑 | 对运动特征加入时序编码,或对权重增加时间平滑正则 |
| 模型倾向于把非刚性动作退化全局刚体 | 基元数量太少或激活过于稀疏 | 增加基元数量,调整激活分数温度,加入局部区域约束 |
| 混合权重出现明显空间断裂 | 路由网络对坐标输入过于敏感 | 空间特征使用更高频位置编码,并让权重在 3D 空间上保持平滑 |
| 旋转插值导致模型扭曲 | 在旋转矩阵空间直接加权 | 改用轴角、四元数或李代数插值 |
| 训练初期几何和运动同时发散 | 两个模块一起学习难度太大 | 先只训练 canonical 几何,再分阶段训练运动模块 |
| 单视角视频看起来运动方向含糊 | 自身存在深度歧义 | 借助光流、深度估计模型或物体姿态先验作为辅助监督 |
| 运行时显存不足 | 采样点太多或渲染分辨率太高 | 减小 batch size、降低采样点数,或做分块渲染 |
下面挑几个容易忽略的细节展开说明。
第一个是旋转插值问题。很多人在实现时误以为对旋转矩阵取加权平均即可,但实际上旋转矩阵并不适合线性平均。使用四元数时要小心双覆盖问题:四元数 q 和 -q 表示同一个旋转,如果直接对两个四元数线性插值,可能绕了远路。正确做法是先检查点积符号,如果为负就翻转向量再插值。
第二个是运动模块和几何模块的训练节奏。我见过不少刚开始做动态 NeRF 的开发者,总想一次性把所有模块端到端跑通。实际上,动态物体重建非常依赖先有稳定的 canonical 几何,再有形变。就像你给一个人拍视频,如果连人长什么样都没想清楚,去预测他下一帧怎么动,基本不可能收敛。
第三个是前景 mask 的重要性。视频里往往包含静态背景。如果模型把背景像素也纳入监督,运动模块会把全局相机运动和背景运动都嵌进去,导致前景运动被稀释。大部分方法都会先用分割模型提取前景 mask,并在训练时让背景和前景分开展示。
9. 给初学者的学习建议
如果你对 BLARM 这类“从视频动画化 3D 对象”的方法感兴趣,但此前只了解基础深度学习,下面这条路线会帮助你把知识空缺补齐。
9.1 建立 3D 视觉的基础
你需要先知道坐标变换、相机成像、旋转表示、四元数、透视投影这些最基础的概念。它们是理解一切 3D 视觉论文的前提。不要第一篇文章就读 BLARM,因为里面每一个缩写背后都依赖较多前置知识。
推荐先理解 NeRF 和 3D Gaussian Splatting。它们解决了“如何把离散的 3D 点变成一张可微渲染图像”的问题。BLARM 再复杂,最终大概率也要落在某种可微渲染框架中,因此读懂 NeRF/3DGS 的渲染公式非常关键。
9.2 从经典工作找共同点
相比直接啃最新论文,先阅读几个有代表性的“从视频重建动态 3D”项目会更容易建立整体认知。例如动态 NeRF、可动画 3DGS、人体重新建模的相关方法,它们都会涉及 canonical 空间、形变场、蒙皮权重等概念。
读论文时要重点关注三张图:系统框架图、网络结构图、实验结果可视化。框架图告诉你模块之间的数据流;网络结构图告诉你可学习的模块有哪些;结果可视化告诉你方法在什么场景会崩溃。把这三张图吃透,论文就已经读懂了一半。
9.3 做一个小规模实验
不要等到学会所有理论再动手。你可以用 PyTorch 写一个简化版:把一个简单的立方体分成左右两个块,给每块定义不同旋转,然后让一个 MLP 预测点的混合权重。实验目标是让模型自己学会在左右块交界处平滑过渡。
这个小实验可能在半小时内写完,却可以让你真正理解“刚性运动基元混合”的意义。它会直观地告诉你:
- 为什么需要多组刚体变换;
- 为什么混合权重要依赖空间坐标;
- 为什么要使用 softmax 或稀疏归一化来约束权重。
一旦在这个 toy example 上跑通了,再回头看 BLARM 论文中的复杂公式和模块,你会发现自己不再害怕。
10. 结语:值得继续关注的方向
BLARM 这个工作最吸引我的地方,不是它某个具体模块多复杂,而是它给出了一种关于“运动如何被结构化表达”的思路。复杂对象不该被简单当成一个完全自由的形变场,也不该被强行设定成刚性物体。通过潜在刚性运动基元的混合,模型在柔性与刚性之间找到了一个相对平衡的中间位置。
如果你之后要复现或借鉴这方向,我的建议很简单:先搭建一个小而稳的版本,把数据、模型、渲染、损失这四个模块拆开调通。不要一开始就追求和论文一模一样的效果。先让代码可以稳定训练,再逐步加上更复杂的运动基元表达和损失约束,这样排错成本会低很多。
如果这篇文章对你理解 BLARM 或动态 3D 重建有帮助,可以先收藏备用。后面拿官方代码做复现时,如果遇到具体环境配置或训练不收敛的问题,欢迎在评论区把你的报错或训练曲线发出来一起讨论。