一句话总结

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


为什么这个问题重要?

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

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

  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