单目铰接体三维重建:MonoArt 的渐进式结构推理
一句话总结
MonoArt 从单张 RGB 图像重建铰接体(有关节的物体)的完整 3D 结构——包括几何形状、部件划分和关节运动参数——不依赖多视角输入或外部模板库。
为什么这个问题重要?
铰接体无处不在
机器人要开门、拉抽屉、操作机械臂,这些都是铰接体(articulated objects)。铰接体的特点是:整体由刚性部件组成,部件之间通过关节连接,关节允许特定自由度的运动。
重建铰接体的 3D 结构是很多下游任务的基础:
- 机器人操作:知道抽屉关节轴的位置,才能规划如何拉开它
- AR/VR 场景:把真实物体的可动结构数字化
- 场景理解:理解环境中物体的功能属性
为什么单目重建很难?
从多视角或深度图重建相对容易——信息充分。单目图像面临的核心困难:
- 几何歧义:单张 2D 图像无法唯一确定 3D 形状
- 运动歧义:同一视觉观测可能对应不同关节状态(门半开还是全开?)
- 耦合问题:部件形状和关节状态高度纠缠,难以分离
现有方法的缺陷:多视角方法数据采集成本高;检索拼装方法依赖模板库泛化差;视频辅助方法需要时间序列效率低。
MonoArt 的核心思路:不直接从图像特征回归所有参数,而是分阶段从粗到细推理——先理解几何,再理解部件,最后理解运动。
背景知识
铰接体的数学表示
铰接体建模为一棵运动学树(kinematic tree):
\[\mathcal{A} = \{P_i, J_j\}\]- $P_i$:第 $i$ 个部件的几何形状 + 世界坐标系中的刚体变换 $T_i \in SE(3)$
- $J_j$:第 $j$ 个关节,包含类型(旋转/平移)、轴方向 $\mathbf{a}$、轴位置 $\mathbf{p}$、状态 $\theta$
关节类型:
- 旋转关节(Revolute):绕轴旋转,如门铰链、机械臂关节
- 棱柱关节(Prismatic):沿轴平移,如抽屉、伸缩杆
正向运动学
给定关节状态 $\theta$,子部件相对父部件的变换:
旋转关节(Rodrigues 公式): \(T_{\text{child}} = T_{\text{parent}} \cdot \exp(\theta \cdot [\mathbf{a}]_\times)\)
平移关节: \(T_{\text{child}} = T_{\text{parent}} \cdot \begin{pmatrix} I & \theta \cdot \mathbf{a} \\ 0 & 1 \end{pmatrix}\)
其中 $[\mathbf{a}]_\times$ 是向量 $\mathbf{a}$ 的反对称矩阵。
Canonical Space(标准空间)
MonoArt 的关键设计:在标准姿态($\theta = 0$,所有关节中性位置)下预测几何,再通过运动参数变换到观测状态。这样几何预测和运动预测可以解耦:
观测图像 → 标准几何(θ=0)
→ 运动参数(θ)
最终结果 = 标准几何 + 正向运动学(θ)
核心方法
直觉解释
直接从图像回归所有参数太难。MonoArt 把问题拆成三个渐进子问题:
单张图像
↓ 视觉特征提取(CNN/ViT backbone)
特征图
↓ 阶段1:标准几何解码
标准空间中的部件几何(θ=0)
↓ 阶段2:部件结构推理(Transformer 建模部件间关系)
结构化部件特征
↓ 阶段3:运动感知嵌入
关节类型 + 轴方向 + 轴位置 + 关节状态
↓
完整铰接体 3D 模型
关键洞见:先有形状,再有结构,最后有运动。这个顺序符合人类认知——我们先看到柜子的形状,再意识到面板是独立部件,最后理解它可以被拉开。
损失函数
整体损失:
\[\mathcal{L} = \lambda_{\text{geo}} \mathcal{L}_{\text{geo}} + \lambda_{\text{part}} \mathcal{L}_{\text{part}} + \lambda_{\text{motion}} \mathcal{L}_{\text{motion}}\]几何重建损失(标准空间中的 Chamfer Distance):
\[\mathcal{L}_{\text{geo}} = \frac{1}{|S_1|} \sum_{x \in S_1} \min_{y \in S_2} \|x - y\|^2 + \frac{1}{|S_2|} \sum_{y \in S_2} \min_{x \in S_1} \|x - y\|^2\]运动参数损失分三项:
- 轴方向:$\mathcal{L}_{\text{axis}} = 1 - \lvert\hat{\mathbf{a}} \cdot \mathbf{a}^*\rvert$(余弦相似度绝对值,消除方向二义性)
- 轴位置:$\mathcal{L}_{\text{pos}} = |\hat{\mathbf{p}} - \mathbf{p}^*|^2$
- 关节状态:$\mathcal{L}_{\text{state}} = \lvert\hat{\theta} - \theta^*\rvert$
实现
铰接体核心数据结构
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from dataclasses import dataclass, field
from typing import List
@dataclass
class Joint:
joint_type: str # 'revolute' 或 'prismatic'
axis: np.ndarray # 关节轴方向,单位向量 (3,)
position: np.ndarray # 关节轴上一点 (3,)
parent_id: int
child_id: int
state: float = 0.0 # 当前状态(弧度 or 米)
@dataclass
class ArticulatedObject:
part_points: List[np.ndarray] # 每个部件的标准空间点云
joints: List[Joint]
def apply_kinematics(self) -> List[np.ndarray]:
"""正向运动学:标准姿态 → 当前状态(关节需按根到叶顺序排列)"""
transforms = [np.eye(4)] * len(self.part_points)
for joint in self.joints:
T_local = self._joint_transform(joint)
transforms[joint.child_id] = transforms[joint.parent_id] @ T_local
result = []
for i, pts in enumerate(self.part_points):
pts_h = np.hstack([pts, np.ones((len(pts), 1))])
result.append((pts_h @ transforms[i].T)[:, :3])
return result
def _joint_transform(self, j: Joint) -> np.ndarray:
T = np.eye(4)
if j.joint_type == 'revolute':
a, theta = j.axis, j.state
K = np.array([[0,-a[2],a[1]],[a[2],0,-a[0]],[-a[1],a[0],0]])
R = np.eye(3) + np.sin(theta)*K + (1-np.cos(theta))*(K@K)
T[:3,:3] = R
T[:3, 3] = j.position - R @ j.position # 绕关节点旋转
elif j.joint_type == 'prismatic':
T[:3, 3] = j.axis * j.state
return T
渐进式推理模型
class ProgressiveArticulationHead(nn.Module):
"""MonoArt 渐进推理头简化实现"""
def __init__(self, feat_dim=512, num_parts=4, num_points=256):
super().__init__()
self.num_parts, self.num_points = num_parts, num_points
# 阶段1:标准几何解码器
self.geo_decoder = nn.Sequential(
nn.Linear(feat_dim, 256), nn.ReLU(),
nn.Linear(256, num_parts * num_points * 3)
)
# 阶段2:部件特征投影 + Transformer 建模部件间关系
self.part_proj = nn.Linear(3, 64)
self.part_encoder = nn.TransformerEncoder(
nn.TransformerEncoderLayer(d_model=64, nhead=4, batch_first=True),
num_layers=2
)
# 阶段3:运动参数预测(每关节 8 维:类型1+轴3+位置3+状态1)
self.motion_head = nn.Sequential(
nn.Linear(64 * num_parts, 256), nn.ReLU(),
nn.Linear(256, (num_parts - 1) * 8)
)
def forward(self, visual_feat):
B = visual_feat.shape[0]
# 阶段1:标准几何
geo = self.geo_decoder(visual_feat).view(B, self.num_parts, self.num_points, 3)
# 阶段2:部件特征(对点云做全局平均池化,再用 Transformer 聚合部件关系)
# 实际实现中此处用 PointNet 提取更丰富的部件特征
part_feat = self.part_proj(geo.mean(dim=2)) # (B, P, 64)
part_feat = self.part_encoder(part_feat) # Transformer 建模部件间关系
# 阶段3:运动参数
motion = self.motion_head(part_feat.reshape(B, -1))
motion = motion.view(B, self.num_parts - 1, 8) # (B, num_joints, 8)
return geo, motion
损失函数实现
def articulation_loss(pred_geo, pred_motion, gt_geo, gt_motion,
lam_geo=1.0, lam_axis=1.0, lam_pos=0.5, lam_state=0.5):
# 几何损失:简化版 Chamfer(实际用 pytorch3d 的实现)
B, P, N, _ = pred_geo.shape
pred_flat = pred_geo.reshape(B*P, N, 3)
gt_flat = gt_geo.reshape(B*P, N, 3)
dist = torch.cdist(pred_flat, gt_flat)
loss_geo = dist.min(dim=2).values.mean() + dist.min(dim=1).values.mean()
# 轴方向损失:绝对值余弦,消除 a 和 -a 的二义性
pred_axis = F.normalize(pred_motion[..., 1:4], dim=-1)
gt_axis = F.normalize(gt_motion[..., 1:4], dim=-1)
loss_axis = 1.0 - torch.abs(F.cosine_similarity(pred_axis, gt_axis, dim=-1)).mean()
# 轴位置损失 + 关节状态损失
loss_pos = F.mse_loss(pred_motion[..., 4:7], gt_motion[..., 4:7])
loss_state = F.l1_loss(pred_motion[..., 7], gt_motion[..., 7])
return lam_geo*loss_geo + lam_axis*loss_axis + lam_pos*loss_pos + lam_state*loss_state
可视化:标准姿态 vs 当前状态
import matplotlib.pyplot as plt
def visualize_articulated_object(obj: ArticulatedObject):
colors = ['#2196F3', '#FF5722', '#4CAF50', '#9C27B0']
fig = plt.figure(figsize=(14, 6))
ax1 = fig.add_subplot(121, projection='3d')
ax1.set_title('Canonical Pose (θ=0)')
for i, pts in enumerate(obj.part_points):
ax1.scatter(*pts.T, c=colors[i%4], s=2, alpha=0.6, label=f'Part {i}')
ax1.legend(markerscale=4)
ax2 = fig.add_subplot(122, projection='3d')
ax2.set_title('Current Articulation State')
for i, pts in enumerate(obj.apply_kinematics()):
ax2.scatter(*pts.T, c=colors[i%4], s=2, alpha=0.6, label=f'Part {i}')
for j in obj.joints: # 画出关节轴
ax2.quiver(*j.position, *j.axis, length=0.4, color='red', linewidth=2)
ax2.legend(markerscale=4)
plt.tight_layout()
plt.savefig('articulated_result.png', dpi=150)
plt.show()
# 示例:抽屉(棱柱关节,沿 Z 轴拉出 0.4 单位)
body = np.random.uniform(-0.5, 0.5, (300, 3)) * np.array([1,1,0.3])
drawer = np.random.uniform(-0.3, 0.3, (200, 3)) + np.array([0, 0, 0.35])
joint = Joint('prismatic', axis=np.array([0,0,1]),
position=np.array([0,0,0]), parent_id=0, child_id=1, state=0.4)
visualize_articulated_object(ArticulatedObject([body, drawer], [joint]))
预期输出:左图为标准姿态(抽屉未拉出),右图为拉出 0.4 单位的状态,红色箭头标注关节轴方向。
实验
数据集说明
PartNet-Mobility 是这个方向最重要的基准:
| 属性 | 说明 |
|---|---|
| 物体类别 | 46 类(柜子、门、冰箱、机械臂等) |
| 模型数量 | ~2500 个带关节标注的 CAD 模型 |
| 关节类型 | 旋转关节、棱柱关节 |
| 标注内容 | 部件分割 + 关节轴/位置/状态范围 |
| 数据格式 | URDF + 部件 mesh |
类别分布严重不均——柜子、门类样本多,特种机械少。训练时需要关注小类别的表现。
定量评估
| 方法 | 关节轴误差↓ | 关节位置误差↓ | 状态误差↓ | 推理延迟 |
|---|---|---|---|---|
| MonoArt | ~8° | ~0.05m | ~0.08 | ~50ms |
| 多视角基线 | 12° | 0.08m | 0.12 | 需多帧 |
| 检索拼装方法 | 15° | 0.12m | 0.15 | ~200ms |
关节轴误差用角度偏差衡量,状态误差为归一化绝对误差。
工程实践
常见坑
坑1:标准空间对齐不一致
# 错误:直接比较预测点云和 GT(关节状态差异会污染几何损失)
loss = chamfer_distance(pred_pts, gt_pts)
# 正确:先把 GT 也变换到标准空间再比较
gt_canonical = inverse_kinematics(gt_pts, gt_joints)
loss = chamfer_distance(pred_pts_canonical, gt_canonical)
坑2:关节轴方向二义性导致梯度反转
# 错误:L2 loss,a 和 -a 物理等价但梯度方向相反
loss_axis = F.mse_loss(pred_axis, gt_axis)
# 正确:用余弦相似度绝对值
loss_axis = 1.0 - torch.abs(F.cosine_similarity(pred_axis, gt_axis, dim=-1)).mean()
坑3:部件数量不固定
真实场景中不同物体部件数量不同,用固定 num_parts 训练会出现部件配对错误。解决方案:推理时用 Hungarian matching 做预测-GT 的最优配对,而不是按索引对齐。
实际部署考虑
- 训练硬件:完整 PartNet-Mobility 训练需要至少 24GB 显存(A100/3090)
- 推理速度:~50ms/张(RTX 3090),约 20fps——对实时机器人控制偏慢
- 精度上限:关节轴 8° 误差在精密操作(如拧螺丝)中不可接受,大范围操作(开门)基本够用
数据采集建议
- 在自有数据上微调时,用深度相机 + ARKit/ARCore 采集多视角 RGBD 辅助生成伪标签
- 关节状态训练样本要覆盖全范围(0~100% 开合度),不要只在极端状态上采集
- 对小类别做数据增强(随机旋转、缩放、点云噪声)
什么时候用 / 不用?
| 适用场景 | 不适用场景 |
|---|---|
| 单张 RGB 输入(移动端、边缘计算) | 需要毫米级精度的工业场景 |
| PartNet-Mobility 覆盖的物体类别 | 柔性物体(布料、软体机器人) |
| 机器人大致操作规划 | 部件间有复杂接触约束的场景 |
| 快速场景数字化 | 超过 8 个部件的复杂机械 |
与其他方法对比
| 方法 | 核心思路 | 优点 | 缺点 |
|---|---|---|---|
| MonoArt | 单目渐进推理 | 高效,无需模板 | 跨类别泛化差 |
| ANCSH | 多视角 NOCS | 几何精度高 | 需要多帧输入 |
| DITTO | 视频交互观察 | 无监督关节发现 | 需要物体被操作的视频 |
| GAPartNet | 检索拼装 | 可解释性强 | 依赖模板库 |
| RPM-Net | 点云配准 | 对遮挡鲁棒 | 需要深度图 |
我的观点
单目铰接体重建是具身智能(embodied AI)的一个关键缺口。机器人要从单张图像快速理解可操作物体的结构,这正是 MonoArt 类方法的定位。
离实际应用还有几个关键障碍:
-
类别泛化:在未见过的物体类别上重建质量大幅下降。零样本泛化是核心挑战,可能需要结合大规模视频预训练来建立更好的运动先验。
-
精度天花板:8° 的关节轴误差对粗操作够用,对精密任务不够。单目的信息量限制了精度上限,除非融合语言先验(”这是冰箱门,铰链通常在左侧”)。
-
遮挡鲁棒性:被遮挡的关节需要靠先验补全,容易出现系统性错误。
值得关注的开放问题:能否用无标注视频数据(观察物体被操作的过程)替代昂贵的 3D 标注?如何把端到端的机器人操控成功率作为评估指标,而不仅是 PartNet 上的几何误差?
Comments