一句话总结

从单张图像重建杂乱厨房场景中的多个物体,同时保证物体之间接触合理、无穿透——这是机器人抓取和物理仿真的基础能力。


为什么这个问题重要?

想象一个机器人要从一堆叠放的碗里取出某一个——它不仅需要知道每个碗的形状,还需要知道这些碗是如何接触的,哪些边缘是悬空的,哪些表面有支撑。现有方法在这里会失败,因为它们把”重建”和”物理合理性”当作两个分离的问题。

现有方法的三个核心痛点:

  1. 孤立重建:NeRF 和 3DGS 擅长整体场景渲染,但无法分解出可以独立操作的物体实例
  2. 物体穿透:现有的 object-level 方法各自估计位姿,拼在一起后物体互相穿透,在物理引擎里直接爆炸
  3. 缺乏接触真值:现有数据集(ShapeNet、Objectron)没有精确的接触标注,导致模型无法学习”物体放在桌上是什么样的约束”

MessyKitchens 同时解决了这三个问题:新数据集提供精确接触真值,新方法(Multi-Object Decoder,MOD)实现联合重建与物理约束。


背景知识

3D 物体表示方式对比

表示方式 优点 缺点 MessyKitchens 适用性
点云 轻量、易获取 无表面连通性,难以做接触判断 低
体素 规则、易碰撞检测 内存爆炸,分辨率受限 低
隐式 SDF 连续、支持穿透检测 需要网络推理 高
网格 Mesh 渲染快、物理引擎兼容 拓扑固定,难优化 高(最终输出)

MessyKitchens 的方法以 SDF(Signed Distance Field)作为中间表示,最终输出可用于物理引擎的网格。

接触建模的数学基础

设物体 $i$ 的 SDF 为 $\phi_i(\mathbf{x})$(正值在物体外,负值在物体内,零值在表面)。

关键约束:

  • 非穿透约束:对任意空间点 $\mathbf{x}$,至多一个物体的 SDF 为负值,即 $\min(\phi_i(\mathbf{x}), \phi_j(\mathbf{x})) \geq 0$
  • 接触约束:接触点 $\mathbf{x}^$ 满足 $\phi_i(\mathbf{x}^) = 0$ 且 $\phi_j(\mathbf{x}^*) = 0$(两个物体表面同时经过该点)
  • 法线一致性:接触点处两物体法线方向满足 $\nabla\phi_i(\mathbf{x}^) \cdot \nabla\phi_j(\mathbf{x}^) \approx -1$(面对面接触)

核心方法

直觉解释

SAM 3D 的思路是:给定单张图,先分割出物体掩码,再用形状先验重建单个物体。问题在于,多个物体独立重建后拼在一起会互相穿透。

MOD(Multi-Object Decoder)的核心创新在于联合解码:不是一个物体一个物体地重建,而是把所有可见物体的特征同时喂给 decoder,让网络在输出阶段就考虑物体间的空间关系。

单张 RGB 图像
    ↓
实例分割 (SAM)
    ↓ 每个物体的掩码 + 裁剪图
Per-Object 特征提取 (ViT backbone)
    ↓ 特征序列 [f₁, f₂, ..., fₙ]
Multi-Object Decoder (MOD)    ← 关键!跨物体注意力
    ↓ 联合位姿 + 形状输出
物理约束优化
    ↓ 非穿透 + 接触精化
最终场景网格

关键公式

联合重建的目标函数:

\[\mathcal{L} = \mathcal{L}_\text{shape} + \lambda_1 \mathcal{L}_\text{pose} + \lambda_2 \mathcal{L}_\text{contact} + \lambda_3 \mathcal{L}_\text{penetration}\]

其中穿透惩罚项定义为:

\[\mathcal{L}_\text{penetration} = \sum_{i \neq j} \sum_{\mathbf{x} \in \mathcal{S}_i} \max(0, -\phi_j(\mathbf{x}))^2\]

对物体 $i$ 表面上的采样点 $\mathbf{x}$,惩罚其落入物体 $j$ 内部($\phi_j < 0$)的程度。

接触项鼓励语义上应该接触的物体真正接触:

\[\mathcal{L}_\text{contact} = \sum_{(i,j) \in \mathcal{C}} \left( \min_{\mathbf{x}} \left[ \phi_i(\mathbf{x})^2 + \phi_j(\mathbf{x})^2 \right] \right)\]

其中 $\mathcal{C}$ 是语义上应该接触的物体对集合(从图像语义推断)。


实现

核心:穿透检测与量化

import torch
import numpy as np

def compute_penetration_depth(sdf_i, sdf_j, surface_points_i, surface_points_j):
    """
    计算两个物体之间的穿透深度
    sdf_i, sdf_j: 各自的 SDF 函数 (callable)
    surface_points_*: 表面采样点 [N, 3]
    返回: 穿透深度标量,0 表示无穿透
    """
    # 物体 i 的表面点在物体 j 内部的深度
    phi_j_at_i = sdf_j(surface_points_i)   # [N]
    phi_i_at_j = sdf_i(surface_points_j)   # [M]

    # 穿透:SDF 为负值(在物体内部)
    penetration_i = torch.clamp(-phi_j_at_i, min=0)  # [N]
    penetration_j = torch.clamp(-phi_i_at_j, min=0)  # [M]

    # 最大穿透深度
    max_pen = max(penetration_i.max().item(), penetration_j.max().item())
    # 平均穿透体积(用于 loss)
    mean_pen = (penetration_i.mean() + penetration_j.mean()) / 2

    return max_pen, mean_pen


def find_contact_points(sdf_i, sdf_j, bbox, resolution=64, threshold=0.005):
    """
    在给定包围盒内搜索两物体的接触点
    接触定义:两物体 SDF 均接近 0
    """
    # 在 bbox 内均匀采样
    x = torch.linspace(bbox[0], bbox[3], resolution)
    y = torch.linspace(bbox[1], bbox[4], resolution)
    z = torch.linspace(bbox[2], bbox[5], resolution)
    grid = torch.stack(torch.meshgrid(x, y, z, indexing='ij'), dim=-1)
    pts = grid.reshape(-1, 3)  # [N, 3]

    phi_i = sdf_i(pts)  # [N]
    phi_j = sdf_j(pts)  # [N]

    # 两个物体表面附近的点
    near_surface_i = phi_i.abs() < threshold
    near_surface_j = phi_j.abs() < threshold
    contact_mask = near_surface_i & near_surface_j

    contact_points = pts[contact_mask]
    return contact_points  # [K, 3]

Multi-Object Decoder 骨架

import torch
import torch.nn as nn

class MultiObjectDecoder(nn.Module):
    """
    MOD: 联合解码多个物体的位姿和形状
    核心是跨物体的 cross-attention,让每个物体感知其邻居
    """
    def __init__(self, feat_dim=512, n_heads=8, n_layers=3, latent_dim=256):
        super().__init__()
        # 跨物体注意力:每个物体 query 其他物体的特征
        self.cross_attn_layers = nn.ModuleList([
            nn.MultiheadAttention(feat_dim, n_heads, batch_first=True)
            for _ in range(n_layers)
        ])
        self.ffn_layers = nn.ModuleList([
            nn.Sequential(
                nn.Linear(feat_dim, feat_dim * 2),
                nn.GELU(),
                nn.Linear(feat_dim * 2, feat_dim)
            ) for _ in range(n_layers)
        ])
        # 输出头:位姿 (9-DoF) + 形状 latent
        self.pose_head = nn.Linear(feat_dim, 9)   # 3 trans + 6 rot
        self.shape_head = nn.Linear(feat_dim, latent_dim)

    def forward(self, object_feats):
        """
        object_feats: [B, N_obj, feat_dim]  N_obj 个物体的特征
        返回: poses [B, N_obj, 9], shape_codes [B, N_obj, latent_dim]
        """
        x = object_feats
        for attn, ffn in zip(self.cross_attn_layers, self.ffn_layers):
            # 每个物体 attend 到所有其他物体(包括自身)
            attended, _ = attn(x, x, x)
            x = x + attended          # 残差
            x = x + ffn(x)            # FFN 残差

        poses = self.pose_head(x)          # [B, N_obj, 9]
        shape_codes = self.shape_head(x)   # [B, N_obj, latent_dim]
        return poses, shape_codes

物理约束优化

def physics_refinement(initial_poses, sdf_models, n_iters=100, lr=1e-3):
    """
    在初始位姿基础上,用物理约束做后处理优化
    这是 inference-time 优化,不需要重新训练
    """
    # 位姿作为可优化参数
    poses = [p.clone().requires_grad_(True) for p in initial_poses]
    optimizer = torch.optim.Adam(poses, lr=lr)

    for it in range(n_iters):
        optimizer.zero_grad()
        total_loss = 0.0

        # 穿透惩罚
        for i in range(len(poses)):
            for j in range(i + 1, len(poses)):
                # 在物体 i 位姿下的表面点
                pts_i = transform_surface_samples(sdf_models[i], poses[i])
                # 计算这些点在物体 j SDF 中的值
                phi_j = sdf_models[j].query(pts_i, poses[j])
                pen = torch.clamp(-phi_j, min=0).mean()
                total_loss += pen

        total_loss.backward()
        optimizer.step()

    return [p.detach() for p in poses]

可视化:接触点与穿透区域

import open3d as o3d

def visualize_scene_with_contacts(meshes, contact_points, penetration_regions=None):
    """
    可视化场景:物体网格 + 接触点 + 穿透区域(红色高亮)
    """
    geometries = []

    # 为每个物体分配颜色
    colors = [[0.2, 0.6, 0.9], [0.9, 0.6, 0.2], [0.2, 0.9, 0.4],
              [0.8, 0.2, 0.8], [0.9, 0.9, 0.2]]
    for i, mesh in enumerate(meshes):
        mesh.paint_uniform_color(colors[i % len(colors)])
        geometries.append(mesh)

    # 接触点:黄色大球
    if contact_points is not None and len(contact_points) > 0:
        pcd = o3d.geometry.PointCloud()
        pcd.points = o3d.utility.Vector3dVector(contact_points)
        pcd.paint_uniform_color([1.0, 1.0, 0.0])
        geometries.append(pcd)

    # 穿透区域:红色高亮(仅 debug 用)
    if penetration_regions is not None:
        pen_pcd = o3d.geometry.PointCloud()
        pen_pcd.points = o3d.utility.Vector3dVector(penetration_regions)
        pen_pcd.paint_uniform_color([1.0, 0.0, 0.0])
        geometries.append(pen_pcd)

    o3d.visualization.draw_geometries(
        geometries,
        window_name="MessyKitchens Scene Reconstruction",
        width=1280, height=720
    )

实验

数据集说明

MessyKitchens 与现有数据集的关键区别在于接触精度:

数据集 场景类型 接触真值 物体密度 遮挡程度
ShapeNet 合成单物体 无 单物体 无
YCB-Video 桌面场景 近似 中等 中等
ScanNet 室内场景 无 高 高
MessyKitchens 厨房杂乱场景 高精度 极高 极高

数据集核心指标对比(论文报告):

数据集 Registration RMSE (mm) ↓ Inter-object Penetration (cm³) ↓
前代数据集 ~8.2 ~12.4
MessyKitchens ~2.1 ~0.8

定量评估

方法 CD ↓ (cm) Pose Error ↓ (°) Penetration ↓ (cm³) Contact F1 ↑
SAM 3D (单物体) 2.8 12.3 18.6 0.34
Instant3D 3.1 14.7 21.2 0.28
MOD (本文) 1.9 8.4 3.2 0.61

穿透体积从 18.6 降到 3.2 cm³,Contact F1 从 0.34 提升到 0.61——这两个数字最能说明方法的价值。


工程实践

实际部署考虑

计算量:单张图推理时间约 2-4 秒(V100),物体数量线性影响 Decoder 部分(注意力计算 $O(N^2)$)。超过 20 个物体的密集场景需要分块处理。

内存:每个物体的 SDF 网络约 20MB,10 个物体的场景 ~200MB GPU 内存,在 RTX 3090 上完全可行。

输出格式:最终输出标准 .obj 或 .ply 格式,可直接导入 PyBullet、MuJoCo、Isaac Sim。

接触检测的常见坑

坑 1:接触阈值不统一

# 错误:阈值是物理单位,要随场景尺度变化
CONTACT_THRESHOLD = 0.005  # 固定值,小场景 OK,大场景完全漏掉接触

# 正确:相对于物体尺寸归一化
def adaptive_threshold(mesh_i, mesh_j, ratio=0.01):
    scale_i = mesh_i.get_axis_aligned_bounding_box().get_extent().max()
    scale_j = mesh_j.get_axis_aligned_bounding_box().get_extent().max()
    return min(scale_i, scale_j) * ratio

坑 2:SDF 查询坐标系混乱

# 错误:忘记把世界坐标系点变换到物体局部坐标系
phi = sdf_model.query(world_points)  # 结果完全错误

# 正确:先逆变换到物体局部坐标系
def query_sdf_world(sdf_model, pose, world_points):
    T_inv = torch.linalg.inv(pose)  # 4x4 变换矩阵的逆
    local_points = (T_inv[:3, :3] @ world_points.T).T + T_inv[:3, 3]
    return sdf_model.query(local_points)

坑 3:物理优化陷入局部最优

推理时的物理精化优化对初始位姿很敏感。当物体严重遮挡时,MOD 输出的初始位姿误差可能超过优化盆地半径,直接用 Adam 跑 100 步无法收敛。解决方案是先做粗粒度的随机重启(n_restarts=5),再做精细优化。


什么时候用 / 不用?

适用场景 不适用场景
机器人抓取规划(需要精确接触) 实时场景重建(2-4秒/帧不够)
物理仿真数据生成 动态场景(物体运动中)
稀疏视图场景理解 非刚体物体(布料、液体)
数字孪生构建 极端光照(镜面厨具反光严重)
机器人学习的演示数据 物体超过 30 个的大型场景

与其他方法对比

方法 优点 缺点 核心适用场景
NeRF / 3DGS 渲染质量高,几何细节好 无物体分解,无物理约束 场景外观重建、新视角合成
BundleSDF 视频序列重建单物体,鲁棒 单物体,无接触处理 机器人操作前的单物体扫描
SAM 3D 单张图多物体,速度快 各自独立,穿透严重 快速场景理解(精度要求不高)
MOD (本文) 联合推理,接触准确,物理合理 慢(2-4s),强依赖形状先验 需要物理正确的机器人学习场景

我的观点

这个方向的价值被低估了。 大多数 3D 重建工作只关注渲染质量(PSNR/SSIM),但对机器人来说,”物体放在哪里、怎么接触”比”渲染出来好不好看”重要得多。MessyKitchens 把评估标准从视觉相似度转移到物理正确性,这是正确的方向。

离实际应用的距离:推理时间是主要瓶颈,目前 2-4 秒/帧适合离线规划,不适合在线感知。随着 ViT 加速(flash attention、量化)和形状先验的泛化能力提升,2 年内实现实时化是可期的。

值得关注的开放问题:

  • 非刚体接触(堆叠的软袋、变形的包装)
  • 透明物体(厨房里的玻璃杯是硬伤)
  • 接触动态变化时的增量更新(不需要每帧全量重建)
  • 与 LLM 的结合:用语言描述”这个杯子在那个盘子旁边”来初始化接触先验

官方代码和数据集:messykitchens.github.io,论文链接:arxiv.org/abs/2603.16868