一句话总结

LingBot-Map 把 SLAM 的三大核心理念(坐标锚定、局部窗口、长程记忆)嵌入 Transformer 注意力机制,在 ~20 FPS 下处理超过 10,000 帧的流式视频,同时输出相机轨迹和稠密点云。


为什么这个问题重要?

机器人要在陌生房间里导航,AR 头显要把虚拟家具准确放在地板上,自动驾驶要知道自己在哪儿——这些都需要从视频流实时恢复 3D 结构。

现有方法面临一个根本矛盾:

  • 传统 SLAM(ORB-SLAM3、COLMAP):手工特征脆弱,弱纹理/动态光照直接崩溃
  • DUSt3R / MASt3R:全局优化精度高,但需要迭代求解,无法流式处理
  • NeRF / 3DGS:每个场景要训练几分钟到几小时,实时无从谈起

LingBot-Map 的核心创新:把 SLAM 的设计哲学直接编进 Transformer 的注意力权重,得到一个不需要任何迭代优化、单次前向传播就能输出结果的 feed-forward 模型。


背景知识

3D 表示方式选择

表示 优点 缺点 典型用途
点云 直接、轻量 稀疏,无拓扑 SLAM、LiDAR
体素 规则,易操作 内存随分辨率立方增长 碰撞检测
NeRF (隐式) 高质量渲染 训练慢,无显式几何 影视、重建
3D Gaussian 实时渲染 存储大,动态场景难 VR、游戏
稠密点图 像素对齐,适合 CNN/ViT 需要解决坐标系问题 本文

LingBot-Map 选择稠密点图(point map):每个像素 $(u, v)$ 直接预测对应的 3D 世界坐标 $\mathbf{p}_{uv} \in \mathbb{R}^3$。这种表示和图像 patch 天然对齐,非常适合 ViT 类骨干网络处理。

流式处理 vs 离线优化

离线优化 (DUSt3R):  [帧1, 帧2, ..., 帧N] → 全局优化 → 结果
流式处理 (LingBot-Map): 帧1 → 结果1 → 帧2 → 结果2 → ... (实时)

流式处理的核心挑战是漂移:误差随帧数累积,1000 帧之后轨迹可能已经偏了几十厘米。


核心方法

直觉解释

想象你在黑暗中闭眼走路:

  • 锚点(anchor)= 起点的记忆,你知道自己从哪里出发
  • 参考窗口(pose-reference window)= 最近几步的步伐记忆,维持局部精度
  • 轨迹记忆(trajectory memory)= 偶尔睁眼确认是否回到熟悉地点,修正漂移

GCT 把这三件事变成注意力机制的三个组件,让 Transformer 在处理每一帧时都能同时访问这三种几何线索。

数学细节

稠密点图预测

对于当前帧 $t$,模型预测每个像素的 3D 坐标(在世界坐标系下):

\[\hat{\mathbf{P}}_t \in \mathbb{R}^{H \times W \times 3}\]

相机位姿 $\mathbf{T}_t \in SE(3)$ 通过点图之间的几何关系隐式求解,无需单独的姿态回归头。

GCT 注意力(三路融合)

设当前帧 token 为 $\mathbf{f}_t$,GCT 注意力依次融合三种上下文:

\[\mathbf{f}_t^{(1)} = \mathbf{f}_t + \text{MLP}(\mathbf{f}_{\text{anchor}})\] \[\mathbf{f}_t^{(2)} = \mathbf{f}_t^{(1)} + \text{CrossAttn}\!\left(\mathbf{f}_t^{(1)},\ \{\mathbf{f}_{t-W},\ldots,\mathbf{f}_{t-1}\}\right)\] \[\mathbf{f}_t^{(3)} = \mathbf{f}_t^{(2)} + \text{CrossAttn}\!\left(\mathbf{f}_t^{(2)},\ \mathbf{M}\right)\]

其中 $\mathbf{M} \in \mathbb{R}^{K \times D}$ 是固定大小为 $K$ 的轨迹记忆库(循环缓冲区),解决了长序列下内存线性增长的问题。

损失函数

\[\mathcal{L} = \frac{1}{HW} \sum_{u,v} \left\| \hat{\mathbf{p}}_{uv} - \mathbf{p}_{uv}^* \right\|_2 + \lambda \cdot \mathcal{L}_{\text{conf}}\]

置信度损失 $\mathcal{L}_{\text{conf}}$ 让模型学会在遮挡区域输出低置信度,而非强行猜测。

Pipeline 概览

视频帧流
  ↓
ViT 特征提取(共享权重)
  ↓
GCT 注意力(三路融合)
  ├── 锚点上下文:坐标系基准
  ├── 姿态参考窗口(最近 W 帧):局部几何
  └── 轨迹记忆(固定大小 K):长程漂移修正
  ↓
稠密点图预测头
  ↓
隐式姿态求解(从点图几何关系)
  ↓
实时输出:相机位姿 + 稠密点云

实现

GCT 核心注意力机制

import torch
import torch.nn as nn
import torch.nn.functional as F

class GeometricContextAttention(nn.Module):
    """GCT 三路注意力:锚点 + 窗口 + 记忆"""

    def __init__(self, dim=256, num_heads=8, window_size=4, memory_size=64):
        super().__init__()
        self.window_size = window_size
        self.memory_size = memory_size

        # 三个组件
        self.anchor_proj = nn.Sequential(nn.Linear(dim, dim), nn.GELU())
        self.window_attn = nn.MultiheadAttention(dim, num_heads, batch_first=True)
        self.memory_attn = nn.MultiheadAttention(dim, num_heads, batch_first=True)

        self.norm1 = nn.LayerNorm(dim)
        self.norm2 = nn.LayerNorm(dim)

        # 循环轨迹记忆缓冲区(固定大小,不随帧数增长)
        self.register_buffer('memory_bank', torch.zeros(memory_size, dim))
        self.memory_ptr = 0

    def forward(self, x, anchor_feat, window_feats):
        """
        x:            [B, N, D]  当前帧 patch tokens
        anchor_feat:  [B, N, D]  锚点帧 tokens(坐标系基准)
        window_feats: [B, W, N, D] 近期 W 帧 tokens
        """
        B, N, D = x.shape

        # 1. 锚点上下文:坐标系归一化
        x = x + self.anchor_proj(anchor_feat)

        # 2. 姿态参考窗口:局部稠密几何线索
        W = window_feats.shape[1]
        kv_window = window_feats.reshape(B, W * N, D)  # 展平窗口
        x_win, _ = self.window_attn(x, kv_window, kv_window)
        x = self.norm1(x + x_win)

        # 3. 轨迹记忆:修正长程漂移
        mem = self.memory_bank.unsqueeze(0).expand(B, -1, -1)
        x_mem, _ = self.memory_attn(x, mem, mem)
        x = self.norm2(x + x_mem)

        # 更新记忆(用当前帧摘要替换最旧的记忆槽)
        slot = self.memory_ptr % self.memory_size
        self.memory_bank[slot] = x.detach().mean(dim=(0, 1))  # 帧级摘要
        self.memory_ptr += 1

        return x

流式状态管理器

class StreamingState:
    """管理流式重建的滑动窗口和锚点状态"""

    def __init__(self, window_size=4, anchor_interval=200):
        self.window_size = window_size
        self.anchor_interval = anchor_interval
        self.frame_buffer = []   # 滑动窗口缓冲
        self.anchor_feat = None  # 锚点帧特征
        self.poses = []          # 累积位姿序列
        self.frame_count = 0

    def get_window(self, current_feat):
        """返回当前帧 + 历史窗口,用于 GCT 注意力输入"""
        window = self.frame_buffer[-self.window_size:]
        if len(window) < self.window_size:
            # 序列开头:用当前帧填充(广播)
            pad = [current_feat] * (self.window_size - len(window))
            window = pad + window
        return torch.stack(window, dim=1)  # [B, W, N, D]

    def update(self, feat, pose, points):
        """处理完一帧后更新状态"""
        self.frame_buffer.append(feat)
        if len(self.frame_buffer) > self.window_size:
            self.frame_buffer.pop(0)

        # 首帧设为锚点;之后每隔 anchor_interval 帧重设锚点
        if self.anchor_feat is None or self.frame_count % self.anchor_interval == 0:
            self.anchor_feat = feat

        self.poses.append(pose)
        self.frame_count += 1
        return {'frame': self.frame_count, 'pose': pose, 'points': points}

    def is_ready(self):
        return self.anchor_feat is not None

最小可运行的流式推理循环

import numpy as np

def streaming_reconstruct(model, video_frames, device='cuda'):
    """
    model:        预训练的 GCT 模型(含 ViT backbone + 点图预测头)
    video_frames: list of [H, W, 3] numpy arrays
    """
    state = StreamingState(window_size=4, anchor_interval=200)
    all_points, all_poses = [], []

    model.eval()
    with torch.no_grad():
        for frame_np in video_frames:
            # 预处理:归一化到 [-1, 1]
            frame = torch.from_numpy(frame_np).float().permute(2, 0, 1)
            frame = (frame / 127.5 - 1.0).unsqueeze(0).to(device)  # [1,3,H,W]

            # ViT 特征提取(省略数据加载细节)
            feat = model.backbone(frame)  # [1, N, D]

            # GCT 注意力需要锚点和窗口
            anchor = state.anchor_feat if state.is_ready() else feat
            window = state.get_window(feat)

            # 前向推理:预测点图 + 位姿
            pointmap, pose, conf = model.gct_head(feat, anchor, window)
            # pointmap: [1, H, W, 3]  pose: [1, 4, 4]  conf: [1, H, W]

            # 过滤低置信度点(遮挡区域)
            mask = (conf > 0.5).squeeze(0)
            valid_points = pointmap.squeeze(0)[mask].cpu().numpy()

            result = state.update(feat, pose.cpu().numpy(), valid_points)
            all_points.append(valid_points)
            all_poses.append(result['pose'])

    return np.concatenate(all_points, axis=0), all_poses

3D 可视化

import open3d as o3d

def visualize_reconstruction(all_points, all_poses, subsample=10):
    """可视化重建结果:点云 + 相机轨迹"""
    geometries = []

    # 合并点云(随机下采样避免卡顿)
    pcd = o3d.geometry.PointCloud()
    idx = np.random.choice(len(all_points), min(len(all_points), 500_000))
    pcd.points = o3d.utility.Vector3dVector(all_points[idx])
    pcd.estimate_normals()
    geometries.append(pcd)

    # 相机中心轨迹
    cam_centers = [T[0, :3, 3] for T in all_poses]
    lines = [[i, i+1] for i in range(len(cam_centers)-1)]
    traj = o3d.geometry.LineSet(
        points=o3d.utility.Vector3dVector(cam_centers),
        lines=o3d.utility.Vector2iVector(lines)
    )
    traj.paint_uniform_color([1, 0, 0])
    geometries.append(traj)

    # 每 subsample 帧显示一个坐标轴
    for T in all_poses[::subsample]:
        frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=0.05)
        frame.transform(T[0])
        geometries.append(frame)

    o3d.visualization.draw_geometries(geometries,
        window_name="LingBot-Map Reconstruction",
        zoom=0.5, front=[0, 0, -1], up=[0, -1, 0])

实验

评测指标

方法 ATE (cm) ↓ 速度 (FPS) ↑ 序列上限 硬件
ORB-SLAM3 ~0.8 30+ 无限制 CPU
DROID-SLAM ~0.4 ~2 ~1000帧 RTX 3090
MonST3R ~1.2 ~5 ~500帧 RTX 3090
DUSt3R (离线) ~0.6 <1 受内存限制 RTX 3090
LingBot-Map ~0.5 ~20 >10,000帧 RTX 3090

注:数值来自同类方法量级估计,精确结果以论文为准

关键突破在于 ATE 精度接近离线优化方法,但速度达到实时(20 FPS),且支持超长序列。

典型失败场景

  • 纯旋转运动:无平移时深度无法三角化,单目固有缺陷
  • 高速运动模糊:ViT backbone 对运动模糊鲁棒性一般
  • 大规模室外场景(>1km):轨迹记忆容量有限,超长序列漂移可能累积

工程实践

内存管理:长序列的真正挑战

朴素实现会让内存随帧数线性增长,1000 帧即可 OOM:

# 错误:把所有历史帧都存下来
self.all_features.append(current_feat)  # 内存炸了

# 正确:循环缓冲区,固定大小
slot = self.ptr % self.memory_size
self.memory_bank[slot] = current_feat.mean(0).detach()
self.ptr += 1

锚点切换问题

长序列中,锚点帧和当前帧视角差异过大会导致坐标偏移:

# 检测锚点切换时机(视角变化超过阈值)
def need_anchor_update(current_pose, anchor_pose, threshold=2.0):
    trans_diff = np.linalg.norm(current_pose[:3, 3] - anchor_pose[:3, 3])
    return trans_diff > threshold  # 平移超过 2 米时更新锚点

实时性优化

# 半精度推理:显存减半,速度提升约 1.5x
model = model.half()
frame = frame.half()

# 编译加速(PyTorch 2.0+)
model = torch.compile(model, mode='reduce-overhead')

常见坑

  1. 坐标系混乱 → 确保所有点图都在同一锚点坐标系下;锚点切换时需显式变换
  2. 序列开头窗口不足 → 用当前帧重复填充窗口,而非用零填充(零会引入错误几何线索)
  3. 置信度阈值不当 → 太高丢失大量点,太低引入噪声;建议根据场景动态调整(室外 0.3,室内 0.5)
  4. GPU 同步开销 → 每帧 .cpu().numpy() 会阻塞 GPU,用异步拷贝或积累后批量处理

什么时候用 / 不用?

适用场景 不适用场景
室内场景扫描(机器人建图) 纯旋转的全景拍摄
单目相机,无 IMU 需要精确绝对尺度(需要深度传感器辅助)
长走廊、大房间(>10,000帧) 高速无人机(运动模糊严重)
实时 AR 锚定(20 FPS 够用) 需要毫米级精度的工业测量
光照变化适中的场景 全黑/强逆光环境

与其他方法对比

方法 核心思路 优点 缺点 适合场景
ORB-SLAM3 手工特征 + 图优化 轻量、实时、可嵌入 弱纹理崩溃 嵌入式机器人
DROID-SLAM 学习特征 + 迭代优化 精度高 慢(2 FPS)、内存大 离线精密重建
DUSt3R 图像对全局优化 精度极高 非流式,无法实时 三维重建后处理
3DGS 高斯渲染 渲染质量顶级 需要已知位姿,训练慢 高质量 NVS
LingBot-Map SLAM 原则 + Transformer 实时、长序列、feed-forward 单目尺度模糊,需要大规模训练数据 实时建图、AR

我的观点

LingBot-Map 代表了一类重要的研究方向:把经典算法的设计直觉直接编码进神经网络结构,而不是用神经网络替代整个系统。锚点 + 窗口 + 记忆的三段式设计,本质上是在用注意力机制重新实现 SLAM 的关键字数据结构(关键帧、局部地图、全局地图)。

值得关注的开放问题:

  1. 尺度恢复:单目系统的宿命——输出的点云是相对尺度,真实部署中要想拿到绝对尺度,还得依赖已知物体大小或稀疏 IMU
  2. 动态物体:人走来走去的场景,置信度机制能处理多少?目前论文未见这方面系统评估
  3. 泛化能力:在训练分布外的场景(水下、工业管道)效果如何?feed-forward 模型的泛化是否比迭代优化更脆?

离实际应用还有多远?对于室内机器人场景,今天就可以用——20 FPS 够用,精度可接受,长序列稳定。对于要求更严苛的工业测量或自动驾驶,还需要与深度传感器或 IMU 融合来解决尺度和极速运动问题。

从趋势来看,feed-forward 3D 重建正在快速逼近传统 SLAM 的精度,同时保持学习方法的鲁棒性优势。下一个里程碑可能是:在手机上实时运行、不需要任何外部传感器的完整世界重建。