一句话总结

RMGS-SLAM 将 LiDAR-惯性-视觉(LIV)三模态传感器与 3D Gaussian Splatting 紧耦合,首次在大规模室外场景中同时实现低延迟位姿估计与照片级真实感地图重建。


为什么这个问题重要?

大规模室外 SLAM 是自动驾驶、机器人导航和 AR 测绘的核心挑战。现有方法各有硬伤:

  • 纯视觉 SLAM(ORB-SLAM3):对光照敏感,室外深度估计误差大
  • LiDAR SLAM(LIO-SAM):轨迹精准但仅稀疏点云,无外观信息
  • NeRF-SLAM:渲染细腻但无法实时,大场景内存爆炸

3D Gaussian Splatting(3DGS)带来了新可能:显式表示 + GPU 光栅化,渲染速度从 NeRF 的秒级降到毫秒级。但将 3DGS 搬进真实室外 SLAM,面临三个核心挑战:

  1. 初始化质量:随机初始化的高斯体在大场景收敛极慢
  2. 全局一致性:长距离运动累积漂移导致地图”撕裂”
  3. 深度来源:单相机深度不可靠,需要 LiDAR 的几何精度

RMGS-SLAM 的核心答案:用 Voxel-PCA 从 LiDAR 点云直接初始化高斯几何,用 Gaussian-GICP 在高斯地图上做回环检测。


背景知识

3D Gaussian Splatting 快速回顾

3DGS 用一组三维高斯椭球体表示场景。每个高斯的关键属性:

  • 位置 $\mu \in \mathbb{R}^3$:高斯中心
  • 协方差 $\Sigma = RSS^TR^T$:椭球形状(分解为缩放 $S$ 和旋转 $R$)
  • 不透明度 $\alpha \in [0,1]$
  • 颜色 $c$:球谐函数(SH)表示视角相关外观

渲染时,将高斯投影到图像平面,按深度排序做 $\alpha$-blending:

\[C = \sum_{i=1}^{N} c_i \alpha_i \prod_{j<i}(1-\alpha_j)\]

核心优势:GPU 并行光栅化,可达 100+ FPS(RTX 4090)。

LiDAR-惯性-视觉状态估计

紧耦合 LIV 估计器同时维护:

  • IMU 预积分:100Hz 高频姿态传播
  • LiDAR 点云配准:绝对几何约束(误差 <5cm)
  • 视觉特征跟踪:弱纹理区域(天空、道路)的补充

状态向量 $\mathbf{x} = [\mathbf{p}, \mathbf{q}, \mathbf{v}, \mathbf{b}_a, \mathbf{b}_g]$ 包含位置、姿态、速度和 IMU 偏置。


核心方法

直觉解释

系统是一个双线程并行架构,前后端不互相阻塞:

LiDAR帧 ──┐
IMU数据  ──┼─→ [前端: LIV状态估计] ──→ 位姿 + 稀疏点云
相机帧   ──┘           │
                       ↓
              [Gaussian初始化: Voxel-PCA] ──→ 新Gaussian体
                       ↓
         [后端: 全局Gaussian优化 (异步)] ──→ 密集3DGS地图
                       ↓
         [回环检测: Gaussian-GICP (低频)] ──→ 位姿图优化 → 全局一致地图

前端跑 100Hz(IMU 驱动),后端优化可以稍慢。两者通过无锁队列通信,互不阻塞是实时性的关键。

Voxel-PCA 几何先验初始化

传统 3DGS 从 SfM 稀疏点云初始化各向同性小高斯,大场景需要几十次迭代才能”长大”到合适尺寸。

RMGS-SLAM 用 Voxel-PCA 从 LiDAR 点云直接获取几何先验:

将 LiDAR 点云体素化,对每个体素内的点集 ${p_i}$ 做主成分分析:

\[\Sigma_{\text{voxel}} = \frac{1}{n}\sum_{i=1}^{n}(p_i - \mu)(p_i - \mu)^T = U \Lambda U^T\]
  • 特征向量矩阵 $U$ → 高斯旋转 $R = U$
  • 特征值 $\lambda_i$ → 高斯缩放 $s_i = \sqrt{\lambda_i}$

几何直觉:LiDAR 打到一面墙,体素内的点沿墙面铺展(两个大特征值)而垂直方向很薄(一个小特征值)。Voxel-PCA 自动发现”薄饼形”几何,避免从球形高斯爬山,收敛速度提升 3-5×。

Gaussian-GICP 回环检测

广义 ICP(GICP)将点对点和点对面 ICP 统一为分布对分布(D2D)配准:

\[\mathcal{L}_{\text{GICP}} = \sum_{(i,j)} \mathbf{d}_{ij}^T \left(\Sigma_i^A + R\Sigma_j^B R^T\right)^{-1} \mathbf{d}_{ij}\]

其中 $\mathbf{d}_{ij} = p_i^A - (Rp_j^B + t)$ 是对应点残差,$\Sigma_i^A, \Sigma_j^B$ 是局部协方差。

关键洞察:3DGS 高斯体的协方差 $\Sigma$ 天然就是 GICP 所需的局部几何描述子——不需要额外估计,直接用!这让 Gaussian-GICP 比传统点云配准计算量减少约 40%。

检测到回环后,通过位姿图优化修正累积漂移,再将修正传播到全局高斯地图:每个高斯按其所在关键帧的位姿变化做刚体变换。


实现

Voxel-PCA 高斯初始化

import numpy as np

def voxel_pca_init(lidar_points: np.ndarray, voxel_size: float = 0.1):
    """
    从LiDAR点云用Voxel-PCA初始化3DGS高斯参数
    Returns: means (M,3), scales (M,3) log空间, rotations (M,4) 四元数
    """
    # 体素化:将每个点分配到体素格子
    voxel_indices = np.floor(lidar_points / voxel_size).astype(int)
    voxel_dict = {}
    for i, idx in enumerate(map(tuple, voxel_indices)):
        voxel_dict.setdefault(idx, []).append(i)

    means, scales, rotations = [], [], []

    for _, point_ids in voxel_dict.items():
        pts = lidar_points[point_ids]   # (k, 3) 当前体素内的点

        if len(pts) < 3:                # 点太少,退回各向同性初始化
            means.append(pts.mean(0))
            scales.append(np.log(np.full(3, voxel_size / 4)))
            rotations.append(np.array([1., 0., 0., 0.]))
            continue

        # PCA:计算局部协方差矩阵
        centroid = pts.mean(0)
        centered = pts - centroid
        cov = (centered.T @ centered) / len(pts)
        eigenvalues, eigenvectors = np.linalg.eigh(cov)   # 升序排列

        # 特征值 → 高斯缩放(log空间,训练更稳定)
        eigenvalues = np.clip(eigenvalues, 1e-6, None)
        scale = np.sqrt(eigenvalues)   # 从小到大排列

        # 特征向量 → 旋转矩阵,确保右手系
        R = eigenvectors.copy()
        if np.linalg.det(R) < 0:
            R[:, 0] *= -1

        # 旋转矩阵 → 四元数 [w, x, y, z]
        trace = R[0,0] + R[1,1] + R[2,2]
        s = 0.5 / np.sqrt(max(trace + 1.0, 1e-8))
        quat = np.array([0.25/s,
                         (R[2,1]-R[1,2])*s,
                         (R[0,2]-R[2,0])*s,
                         (R[1,0]-R[0,1])*s])

        means.append(centroid)
        scales.append(np.log(scale))
        rotations.append(quat / np.linalg.norm(quat))

    return (np.array(means, dtype=np.float32),
            np.array(scales, dtype=np.float32),
            np.array(rotations, dtype=np.float32))

Gaussian-GICP 配准核心

import torch

class GaussianGICP:
    """基于3DGS协方差的广义ICP,用于回环检测"""

    def gicp_loss(self, src_means, src_covs, tgt_means, tgt_covs, R, t):
        """
        计算GICP目标函数(马氏距离加权点对距离)
        src/tgt_covs: (N, 3, 3) — 直接使用3DGS高斯体的协方差
        """
        # 变换源点云,找最近邻对应
        src_transformed = src_means @ R.T + t          # (N, 3)
        dist = torch.cdist(src_transformed, tgt_means) # (N, M)
        nn_idx = dist.argmin(-1)                        # (N,)

        d = src_transformed - tgt_means[nn_idx]         # (N, 3) 残差

        # 联合协方差:C = C_src + R @ C_tgt @ R^T
        C_tgt = tgt_covs[nn_idx]                        # (N, 3, 3)
        R_exp = R.unsqueeze(0).expand_as(C_tgt)
        C_joint = src_covs + R_exp @ C_tgt @ R_exp.transpose(-1, -2)

        # 马氏距离:d^T * C^{-1} * d
        C_inv = torch.linalg.inv(C_joint + 1e-6 * torch.eye(3))
        d_col = d.unsqueeze(-1)                         # (N, 3, 1)
        loss = (d_col.transpose(-1, -2) @ C_inv @ d_col).squeeze()
        return loss.mean()

    def register(self, src_means, src_covs, tgt_means, tgt_covs, max_iter=30):
        """ICP主循环,返回最优 (R, t)"""
        R = torch.eye(3)
        t = torch.zeros(3)
        for _ in range(max_iter):
            # 用SVD求解点对对应的最优刚体变换(省略完整推导)
            # 实际工程中可用 open3d.pipelines.registration.registration_generalized_icp
            pass
        return R, t

3D 可视化:LiDAR 点云与 Gaussian 初始化对比

import open3d as o3d
import numpy as np

def visualize_gaussian_init(lidar_points, gaussian_means, gaussian_scales):
    """对比LiDAR原始点云与Voxel-PCA初始化的高斯中心"""
    # 原始LiDAR点云(灰色)
    pcd_lidar = o3d.geometry.PointCloud()
    pcd_lidar.points = o3d.utility.Vector3dVector(lidar_points)
    pcd_lidar.paint_uniform_color([0.6, 0.6, 0.6])

    # 高斯中心点(按尺度着色:红=大高斯, 蓝=小高斯)
    pcd_gauss = o3d.geometry.PointCloud()
    pcd_gauss.points = o3d.utility.Vector3dVector(gaussian_means)
    scale_norm = (gaussian_scales.sum(-1) - gaussian_scales.sum(-1).min())
    scale_norm = scale_norm / scale_norm.max()    # 归一化到[0,1]
    colors = np.stack([scale_norm, np.zeros_like(scale_norm),
                       1 - scale_norm], axis=1)   # 红→蓝渐变
    pcd_gauss.colors = o3d.utility.Vector3dVector(colors)

    o3d.visualization.draw_geometries([pcd_lidar, pcd_gauss],
                                       window_name="Gaussian Init vs LiDAR")

实验

数据集说明

数据集 类型 场景规模 传感器 真值来源
KITTI 公开基准 城市驾车 LiDAR + Stereo RTK-GPS
KITTI-360 公开基准 长距离环绕 LiDAR + 360°Cam RTK-GPS
作者自采集 私有 大规模室外含回环 硬件同步 LIV 高精度 RTK-IMU

作者自采集数据集的核心价值:硬件时间同步(解决传感器时差),以及设计了含回环的轨迹,专门测试全局一致性。

定量评估

方法 传感器 ATE (m) ↓ PSNR ↑ 实时性
LIO-SAM LiDAR+IMU 0.15
MonoGS 单目 >1.0 22.3
SplaTAM RGBD 28.1 ✗ 慢5×
RMGS-SLAM LIV 0.08 31.2

数据来自论文,不同序列有差异,以原文为准。

典型失败情况

  • 高速行驶(>80km/h):LiDAR 点云稀疏,Voxel-PCA 退化为各向同性,PSNR 下降约 2-3dB
  • 强反射表面(玻璃幕墙):LiDAR 产生虚假点,Gaussian 初始化位置偏移
  • 高动态场景:行人/车辆被烘焙进地图,出现”鬼影”伪影

工程实践

实际部署要求

模块 典型延迟 线程优先级
IMU预积分 ~1ms/帧 最高(实时性关键)
LiDAR配准 ~20ms/帧 高(前端线程)
Gaussian初始化 ~5ms/帧 中(后端线程)
全局Gaussian优化 ~100ms/iter 低(异步后端)
回环检测 ~500ms/次 最低(后台低频)

最低硬件要求:RTX 3090(24GB 显存),12核 CPU,32GB RAM。室外大场景运行 10 分钟,高斯数量可超 200 万,显存是瓶颈。

数据采集建议

  1. 硬件时间同步:软件同步误差 >1ms 在高速运动时会引入明显点云-图像错位,建议使用硬件触发信号
  2. LiDAR 线数:16线以上机械旋转式效果最好,MEMS 点密度不均匀会导致 Voxel-PCA 退化
  3. 场景设计:刻意规划含回环的路径,以触发并验证全局一致性优化

常见坑及解决方案

坑1:Voxel 尺寸不当导致 PCA 退化

体素太小则每个格子点数不足;体素太大则细节压缩。用自适应体素尺寸

def adaptive_voxel_size(pts_in_voxel, base_size=0.1):
    """根据局部点密度自动调整体素尺寸"""
    density = len(pts_in_voxel) / (base_size ** 3)
    return float(np.clip(base_size * (10 / max(density, 1e-3)) ** 0.33,
                         0.05, 0.5))   # 限制在 5cm ~ 50cm

坑2:Gaussian-GICP 误匹配回环(长走廊场景)

视觉相似但几何不同的区域会触发虚假回环。加入双阶段验证

def verify_loop_candidate(desc_cur, desc_cand, gaussians_cur, gaussians_cand):
    # 阶段1:外观描述子粗筛(快速,毫秒级)
    if cosine_similarity(desc_cur, desc_cand) < 0.75:
        return False, None
    # 阶段2:Gaussian-GICP精验证(较慢,~500ms)
    R, t = gicp.register(gaussians_cur.means, gaussians_cur.covs,
                          gaussians_cand.means, gaussians_cand.covs)
    inlier_ratio = compute_inlier_ratio(R, t, gaussians_cur, gaussians_cand)
    return inlier_ratio > 0.6, (R, t)

坑3:长时运行显存溢出

每 1000 帧执行高斯剪枝,删除退化高斯,同时做视野外换出:

def prune_gaussians(means, opacities_logit, opacity_thresh=0.005):
    """删除低不透明度高斯,释放显存"""
    opacities = torch.sigmoid(opacities_logit)
    alive = opacities > opacity_thresh
    print(f"剪枝: {(~alive).sum().item()} / {len(alive)} 高斯被删除")
    return means[alive], opacities_logit[alive]

什么时候用 / 不用?

适用场景 不适用场景
大规模室外静态场景重建 高速行驶(>80km/h)导致点云稀疏
需要照片级真实感地图 动态物体密集(闹市行人)
已有 LiDAR+Camera 硬件平台 只有单目/RGBD 相机
自动驾驶/测绘车辆 嵌入式设备(无高端 GPU)
需要高精度轨迹 + 高质量渲染 需要实时语义分割或实例理解

与其他方法对比

方法 优点 缺点 最适场景
LIO-SAM 轻量实时,轨迹精准 仅稀疏点云,无颜色 纯定位导航
MonoGS 只需单目相机 大场景漂移,深度不准 小规模室内
SplaTAM 3DGS 渲染质量好 需 RGBD,无回环,慢 桌面级重建
NeRF-SLAM 渲染极细腻 无法实时,内存爆炸 离线精细建图
RMGS-SLAM LIV 融合,实时,有回环 依赖 LiDAR,硬件成本高 大规模室外 SLAM

我的观点

RMGS-SLAM 代表了 3DGS-SLAM 走向实用的一个重要节点——不再是桌面物体或小型室内走廊的 demo,而是面对真实室外大场景的脏数据和累积漂移。

值得关注的开放问题

  1. 动态物体处理:行人和车辆会被烘焙进高斯地图形成鬼影。当前主流做法是用 3D 检测器标注动态区域并跳过初始化,但计算开销大,如何高效过滤是未解问题。

  2. 地图更新与老化:现有方法假设静态世界,季节变化(落叶、施工)导致地图过时。支持增量更新的 3DGS-SLAM 是下一步。

  3. 从 GPU 到边缘:24GB 显存的需求限制了真实部署。高斯量化(INT8 协方差矩阵)和稀疏激活机制是可能的压缩路径,但对渲染质量的影响还不明朗。

  4. 语义高斯:每个高斯附带语义标签,使同一张地图同时服务于定位、重建和场景理解,这是通向具身智能的关键一步。

当前距离产品化主要差两个突破:边缘硬件适配和动态场景鲁棒性。以现在的推进速度,在自动驾驶测试车上看到类 RMGS-SLAM 的系统,两三年内并不意外。

论文链接:https://arxiv.org/abs/2604.12942v1 注:截至本文写作时,官方代码尚未开源