第39章:多传感器SLAM系统架构设计(第40周)¶
文档类型:算法工程教学 定位:这不是算法课,而是**架构课**。理解"为什么这个项目选择这种架构"比"这个算法的数学推导"更重要——同样的卡尔曼滤波数学,在 FAST-LIO2 里是 1500 行单文件,在 LVI-SAM 里是分布在四个 ROS 节点的因子图。架构决定了代码长什么样、能跑多快、坏掉时怎么坏。 前置章节:本章建立在「SLAM理论基础」(第10章,前端/后端/回环的概念)、「卡尔曼滤波与ESKF」、「图优化与因子图」、「李群李代数」(SO3/SE3/S²流形)、「C++ 并发与 Eigen」之上。 代码精读对象:LIO-SAM、ORB-SLAM3、FAST-LIVO2、R3LIVE、Kimera-VIO、VINS-Mono、FAST-LIO2、Point-LIO。
前置自测¶
在进入本章之前,请先尝试回答以下问题。如果有超过两道题完全没有思路,建议先回顾对应的前置章节——本章会大量复用这些概念,但不会从头讲解它们。
-
(卡尔曼滤波) ESKF(误差状态卡尔曼滤波)的更新步骤里,卡尔曼增益 \(K = P H^T (H P H^T + R)^{-1}\) 中的 \(P\)、\(H\)、\(R\) 各代表什么?如果某个观测方向上 \(R\) 很大(噪声很大),增益 \(K\) 在那个方向上会变大还是变小?这对融合意味着什么? → 答不出来,回顾「卡尔曼滤波与ESKF」章节。
-
(因子图) 因子图里的"变量节点"和"因子节点"分别对应优化问题中的什么?为什么说一个 IMU 预积分因子连接的是"两个相邻关键帧位姿 + 速度 + 零偏"这五类变量,而不是只连接两个位姿? → 答不出来,回顾「图优化与因子图」章节。
-
(李群) 重力向量为什么可以用 \(S^2\)(二维单位球面流形)建模而不是 \(\mathbb{R}^3\)?提示:重力的什么量是已知的、什么量是未知的?用 \(\mathbb{R}^3\) 建模会引入什么多余的自由度? → 答不出来,回顾「李群李代数」章节。
-
(传感器) 单目相机、双目相机、机械式激光雷达、固态激光雷达、IMU 这五种传感器,哪些能直接测量绝对尺度(米)?哪些在静止时仍然输出有效信息?哪些在黑暗中失效? → 答不出来,回顾「SLAM理论基础」第10章的传感器对比小节。
-
(C++ 并发) 多个生产者线程(传感器回调)向同一个队列写数据、一个消费者线程读数据,为什么需要
std::mutex?如果不加锁,最坏会发生什么?std::deque的push_back和pop_front是线程安全的吗? → 答不出来,回顾「C++ 并发与内存模型」章节。
本章目标¶
学完本章后,你应该能够:
- 对比五类主流传感器(相机/LiDAR/IMU/轮速/GNSS)的可观测量、失效模式、互补关系,并据此判断一个给定场景应该选择哪种传感器组合。
- 区分四大架构范式——松耦合 vs 紧耦合(融合层次维度)、滤波器 vs 图优化(求解器维度)——并能从一段陌生的 SLAM 代码反推它属于哪个范式。
- 设计多传感器的时间同步与外参标定方案:判断何时该用硬件触发、何时用软件时间戳对齐、何时该在线估计时间偏移和外参,并理解每种选择的代价。
- 看懂因子图融合架构的 C++ 实现:从 GTSAM 的
NonlinearFactorGraph、ISAM2、自定义因子,到 Ceres 的滑窗优化与边缘化,理解异构约束如何在同一张图里统一优化。 - 实现退化检测与降级响应:用 Hessian 特征值分解检测不可观方向,并在里程计级(增大协方差)和后端级(约束降权)做出响应。
- 精读三个典型系统的架构:LVI-SAM(双子系统+因子图+故障切换)、R3LIVE(松耦合双子系统+共享状态)、FAST-LIVO2(紧耦合 ESIKF 顺序更新+统一体素地图),并能说清它们架构选择背后的工程权衡。
本章知识导航¶
本章的知识结构是一棵**自顶向下的决策树**。最顶层是"我有哪些传感器、它们各能干什么"(§39.1 传感器特性),这是一切架构决策的输入。第二层是两个正交的架构维度——"融合得多紧"(§39.2 松/紧耦合)和"用什么求解器"(§39.4 因子图 vs §39.3 中穿插的滤波器),这两个维度组合出四个象限。第三层是把传感器接进系统必须解决的两个工程前提:时间对齐和空间标定(§39.3)。第四层是系统鲁棒性的关键——当某个传感器退化时如何检测和降级(§39.5)。最后用三个真实系统把前面所有维度串起来(§39.6 系统精读)。
传感器特性对比 (§39.1) ← 一切的输入:每种传感器测什么、什么时候坏
│
├── 融合层次维度:松耦合 vs 紧耦合 (§39.2)
│ 松 = 各传感器先独立出位姿再融合
│ 紧 = 原始测量直接进同一个估计器
│
├── 工程前提:时间同步 + 外参标定 (§39.3)
│ 时间不齐 → 高速运动下融合发散
│ 外参不准 → 紧耦合直接崩
│
├── 求解器维度:因子图融合架构 (§39.4)
│ 滤波器(ESKF/MSCKF):边缘化历史,O(状态维)
│ 图优化(GTSAM/Ceres):保留历史,可重线性化
│
├── 鲁棒性关键:退化检测与切换 (§39.5)
│ Hessian 特征值 → 哪个方向不可观 → 降级响应
│
└── 综合:典型系统架构精读 (§39.6)
LVI-SAM = 紧耦合 + 因子图 + 双子系统故障切换
R3LIVE = 松耦合 + 滤波器 + 共享状态
FAST-LIVO2 = 紧耦合 + 滤波器 + 统一体素地图
推荐阅读路径: - 精读(8-10 小时):按顺序读完全章,每个代码片段都对照真实项目源码看一遍,完成所有练习。 - 速读(3-4 小时):读 §39.1 传感器对比表、§39.2 四象限图、§39.6 三个系统的架构对比表,跳过推导和代码细节。 - 速查(30 分钟):直接看每节开头的"一句话总结"和章末的知识点总表、故障排查手册。
前置知识桥接¶
本章会反复用到以下前置概念,这里用三句话各重述一遍核心要点,你不必翻回去也能跟上:
- 回顾「SLAM理论基础」:SLAM 分前端(视觉/激光里程计,估计相邻帧间运动)、后端(从带噪数据估计全局一致状态,本质是最大后验估计)、回环检测(识别重访位置,消除累积漂移)三大模块。本章讨论的"架构"就是这三个模块外加多个传感器输入如何在代码里组织。
- 回顾「卡尔曼滤波与ESKF」:ESKF 把状态分成"标称状态"(在流形上积分)和"误差状态"(在切空间里用卡尔曼滤波估计的小量)。预测步用 IMU 前向传播,更新步用观测(如 LiDAR 配准残差)修正误差状态,再把误差注入标称状态。本章里"滤波器方案"指的就是这套机制。
- 回顾「图优化与因子图」:把状态估计写成一张图——变量节点是待优化的量(位姿、速度、零偏、路标点),因子节点是约束(每个因子是一个带权重的残差块,权重是信息矩阵 = 协方差的逆)。优化就是找一组变量取值使所有因子的加权残差平方和最小。iSAM2 是增量求解器,新增因子时只重新计算受影响的部分而非整张图。
如果跳过本章会怎样¶
场景一:你接手了一个走廊里疯狂漂移的 LiDAR SLAM 系统。 你调遍了 ICP 的所有参数、换了更密的点云、加大了迭代次数,漂移依旧。如果没读过本章的"退化检测"(§39.5),你永远不会想到:长直走廊在前进方向上**几何上就是不可观的**——没有任何点云特征能约束"我到底往前走了多远",再多迭代也无济于事。读过本章你会知道,正确做法是检测 Hessian 在前进方向的特征值塌缩,然后引入另一个传感器(如轮速或视觉)补上这个方向的约束。
场景二:你想给一个开源 LIO 系统加个相机做视觉融合。 你直接把相机数据塞进了滤波器,结果系统在快速转弯时输出乱跳。如果没读过本章的"时间同步"(§39.3),你不会意识到:相机和 IMU 之间哪怕只有 10 毫秒的时间偏移,在 200°/s 的转速下就对应 2° 的姿态误差,这个误差会被紧耦合放大到整个状态。读过本章你会知道,要么用硬件触发把偏移压到零,要么把时间偏移 \(t_d\) 加进状态向量在线估计。
预计阅读时间¶
- 精读:8-10 小时(含代码对照与练习)
- 速读:3-4 小时
- 速查:30 分钟
§39.1 传感器特性对比——一切架构决策的起点 ⭐⭐¶
一句话总结:多传感器融合的全部意义在于"用一种传感器的长处补另一种传感器的短处",所以在谈任何架构之前,必须先精确地知道每种传感器各能测什么、各在什么时候失效——这张"能力与失效表"才是架构决策真正的输入。
动机:为什么单一传感器永远不够¶
回顾我们在「SLAM理论基础」里讲过的单目相机:它能拍到丰富的纹理,但**丢掉了深度这一维**,单目 SLAM 的轨迹和地图与真实世界相差一个未知的尺度因子。这是一个根本性的、无法靠"更好的算法"绕过的限制——信息在投影那一刻就丢了,再聪明的优化也变不出本来不存在的尺度。
那换成激光雷达呢?激光雷达直接测距,尺度是确定的、绝对的。但激光雷达在另一个维度上同样有根本性短板:它只看几何,不看外观。把一台激光雷达放进一条笔直、光滑、没有任何凸起的隧道,它扫到的每一帧点云几乎完全一样——前后两帧之间,点云配准算法找不到任何能告诉它"我往前走了多远"的几何特征。这个方向上的运动在几何上**不可观**(unobservable),这同样是算法绕不过去的。
再看 IMU(惯性测量单元):它测量角速度和线加速度,频率极高(通常 200Hz 以上),在黑暗中、在隧道里、在快速运动时都照常工作——它不依赖任何外部环境。但 IMU 的致命弱点是**积分漂移**:要从加速度得到位置,必须积分两次;要从角速度得到姿态,必须积分一次。任何微小的零偏(bias)和噪声经过积分都会随时间累积成巨大的误差。一个消费级 IMU 单独工作几秒钟,位置估计就会漂到离谱。
本质洞察:每种传感器都有一个"它在物理上根本测不到"的维度——相机测不到绝对尺度,激光雷达测不到无几何特征方向上的运动,IMU 测不到不积分就得到的绝对位姿。多传感器融合不是"多几个传感器精度更高"这么简单,而是**用一种传感器去观测另一种传感器观测不到的维度**。两个都能测尺度的传感器叠在一起,价值远小于一个测尺度、一个测纹理的组合。这就是"互补"二字的真正含义。
这就引出了多传感器 SLAM 的核心命题:把可观测维度互补的传感器组合起来,让整个系统在任何单一传感器失效的维度上仍然可观。激光给相机补尺度,相机给激光补纹理(在几何退化场景下用视觉特征约束运动),IMU 给两者补高频运动和短时预测,GNSS 给所有传感器补全局无漂移的绝对定位。
反面:如果我们假装传感器没有失效模式会怎样¶
设想一个天真的工程师,他认为"传感器越多越好,全塞进卡尔曼滤波器,让数学自动搞定一切"。他把单目相机、激光雷达、IMU、轮速计、GNSS 全部接进一个 EKF,每个传感器来一帧就更新一次。在理想环境(特征丰富、光照良好、有卫星信号、轮子不打滑)下,这套系统跑得很好,他很满意。
然后系统进了地下停车场。GNSS 信号瞬间消失,但他的代码还在用最后一帧 GNSS 观测更新——而那帧观测此刻已经完全失效,相当于在用一个错误的"绝对位置"硬拽整个状态,估计立刻被带偏。车开上了刚下过雨的光滑坡道,轮子打滑,轮速计报告"我没动",但车实际在滑行——又一个失效的观测被当成真值喂进了滤波器。车进了一条长直坡道隧道,激光雷达点云在前进方向不可观,但他的代码照样信任 LiDAR 配准的全部六个自由度——配准算法在前进方向上随便给了个数(因为那个方向的残差对任何取值都一样大),这个垃圾值被当成可信观测。
结果:在最需要鲁棒性的恶劣场景下,这套"传感器最多"的系统反而比单一可靠传感器**崩得更彻底**。因为它不知道哪个传感器在什么时候失效,把失效传感器的输出和正常传感器一视同仁。
对比性思维("不是 X 而是 Y"):多传感器融合的难点**不是**"如何把更多观测加进估计器"——那只是往滤波器或因子图里多塞几个观测方程,是体力活。真正的难点**是**"如何知道每个传感器此刻是否可信,并在它失效时把它的影响降到零"。前者是加法,后者是乘以一个动态的可信度权重。本章后面的"退化检测与切换"(§39.5)整节都在解决后者。
这正是为什么我们必须先把每种传感器的失效模式刻进脑子里——架构的鲁棒性,本质上是对"失效模式表"的工程化响应。
历史:从单传感器到多传感器融合的演进¶
SLAM 的传感器使用经历了清晰的三个阶段。第一阶段(2000s-2014 左右)是单一传感器的纯粹时代:要么纯视觉(如 MonoSLAM、PTAM、早期 ORB-SLAM),要么纯激光(如 GMapping、Hector SLAM、早期 LOAM)。研究者专注于把单一传感器用到极致,融合还不是主流。
第二阶段(2014-2018 左右)是视觉-惯性融合(VIO)的崛起:人们意识到 IMU 和相机是绝配——IMU 提供高频运动先验和尺度信息(加速度积分能恢复尺度),相机提供低频但精确的几何约束修正 IMU 漂移。OKVIS、ROVIO、VINS-Mono、MSCKF 等系统把 VIO 推向成熟。同期激光这边,LOAM 系列把激光-惯性里程计(LIO)做到了高精度。
第三阶段(2019 至今)是激光-视觉-惯性的三模态融合(LVI):研究者发现激光和视觉本身就是互补的(激光擅长几何/弱纹理,视觉擅长纹理/弱几何),把两者加上 IMU 融合,能在几乎所有场景下保持鲁棒。LVI-SAM(2021)、R2LIVE/R3LIVE(2021)、FAST-LIVO(2022)/FAST-LIVO2(2024)是这个阶段的代表作——它们正是本章 §39.6 要精读的对象。
本质洞察:传感器融合的演进史,本质上是一部"不断为系统增加可观测维度、堵住失效漏洞"的历史。VIO 用 IMU 给单目补了尺度和高频预测;LVI 又用激光给视觉补了直接深度、用视觉给激光补了退化场景下的约束。每加一种传感器,都是在填补前一代组合的某个失效维度。理解这条主线,你就能预测下一步——比如为什么近年又开始融合事件相机(补极端运动和高动态范围)、毫米波雷达(补恶劣天气)。
理论:五类主流传感器的可观测量与失效模式¶
下面这张表是本章最重要的"输入数据"。请务必读透每一行——后面所有架构决策都是对这张表的响应。
| 传感器 | 直接可观测量 | 频率 | 绝对尺度 | 静止时 | 致命失效模式 | 互补对象 |
|---|---|---|---|---|---|---|
| 单目相机 | 2D 投影(像素),角度信息 | 20-60Hz | ❌ 尺度未知 | 无新信息(无视差) | 黑暗、弱纹理、过曝、快速运动模糊 | IMU 补尺度+运动,LiDAR 补深度 |
| 双目相机 | 2D 投影 + 视差→深度 | 20-60Hz | ✅(基线已知) | 仍有深度 | 远处深度精度差、弱纹理、光照 | IMU 补高频,LiDAR 补远处 |
| RGB-D 相机 | 2D 投影 + 直接深度 | 30Hz | ✅ | 仍有深度 | 室外阳光、透明/反光物体、量程窄(<5m) | IMU 补运动,仅限室内 |
| 机械式 LiDAR | 3D 点云(精确测距) | 10-20Hz | ✅ 绝对 | 仍有几何 | 弱几何(隧道/平面/开阔地)、雨雾、运动畸变 | 视觉补纹理/退化,IMU 补畸变补偿 |
| 固态 LiDAR | 3D 点云(非重复扫描) | 10Hz | ✅ 绝对 | 累积视场 | 小 FOV、弱几何、运动畸变 | 同上,更依赖 IMU 去畸变 |
| IMU | 角速度 ω、线加速度 a | 100-1000Hz | ✅(加速度含尺度) | 测重力+零偏 | 积分漂移(秒级发散)、零偏随温度漂移 | 一切——提供高频运动先验 |
| 轮速计/里程计 | 轮子转速→平面位移 | 10-100Hz | ✅ | 测到"零速" | 打滑、侧滑、仅平面(2.5D) | 提供零速观测+平面约束 |
| GNSS/GPS | 全局经纬高 | 1-10Hz | ✅ 全局 | 仍有定位 | 室内/隧道/城市峡谷无信号、多路径 | 提供全局无漂移锚点 |
逐行解读这张表里几个最关键的工程含义:
关于"静止时"这一列——这是初学者最容易忽略、却在工程中反复坑人的一点。单目相机在静止时**完全无法提供新信息**:没有运动就没有视差,三角化失败,深度无法恢复。这意味着纯视觉 SLAM 在机器人静止或纯旋转(无平移)时会退化。而 IMU 即使静止也在持续测量重力方向(这恰恰是估计 roll/pitch 和零偏的黄金机会,很多系统的初始化就靠静止时的 IMU 数据),LiDAR 静止时也持续提供完整几何。所以"静止鲁棒性"是选传感器组合时一个容易被忽视但很实际的维度——一个会频繁走走停停的巡检机器人,绝不能只靠单目视觉。
关于"绝对尺度"这一列——单目相机是唯一一个尺度未知的传感器。这就是为什么纯单目 SLAM 几乎从不单独部署,必须搭配 IMU(通过加速度积分恢复尺度)或激光/双目/深度相机(直接提供米制深度)。一旦你看到一个系统用单目相机,第一个该问的问题就是"它的尺度从哪来"。
关于运动畸变——机械式和固态激光雷达扫一帧需要时间(机械式转一圈约 100ms),在这段时间里如果载体在运动,先扫到的点和后扫到的点处在不同的位姿下,导致点云"扭曲"。这就是为什么几乎所有现代 LIO 系统都**强制需要 IMU**:用高频 IMU 积分出扫描期间的连续运动,把每个点反投影回扫描起始时刻,消除畸变。没有 IMU 的纯激光里程计在快速运动下精度会显著下降。
多视角理解(跨领域类比):可以把多传感器系统类比成一个**侦探团队**。IMU 像一个反应极快但记性极差的助手——它能瞬间告诉你"刚才向右转了一下",但你要是问它"我们一小时前在哪",它的答案早就漂到天边了。相机像一个观察力敏锐但怕黑的目击者——光线好时能精确指认细节,一旦进了暗室就两眼一抹黑,而且它说不准距离("那栋楼很大但很远,还是很小但很近?")。激光雷达像一个拿着精确卷尺、但脸盲的测量员——它能告诉你每面墙的精确距离,但在一条所有墙都长得一样的走廊里,它分不清自己走到了哪一段。GNSS 像一个偶尔能联系上的卫星导航员——空旷处它直接报出经纬度,但一进隧道就彻底失联。好的架构师做的事,和一个好的侦探队长一样:知道每个队员的长处和盲区,在不同情境下采信不同队员的证词,并且永远不让一个明显在胡说的队员主导结论。 这个类比像在"信息互补、各有盲区"上,不像在"侦探有主观意识、传感器只是被动测量"上——传感器不会自己判断该不该相信自己,这个判断必须由架构(退化检测模块)来做。
传感器组合的典型选型决策¶
理解了单个传感器,现在看实际工程中怎么组合。下面给出一个选型决策流程,覆盖绝大多数地面/手持/无人机场景:
你的应用场景是?
│
┌───────────────┼────────────────┐
室内为主 室内外混合 室外为主
│ │ │
光照稳定? 是否有快速运动? 是否需要全局定位?
│ │ │ │ │ │
是 否 是 否 是 否
│ │ │ │ │ │
双目+IMU RGB-D LVI三模态 LIO LVI+GNSS LIO/LVI
或VIO +IMU (相机+激光 (激光 (激光+视觉 (取决于
(室内 +IMU) +IMU) +IMU+GPS) 退化风险)
近距离)
│
弱纹理风险高?
──→ 加激光 → LVI
几个决策要点的解释: - 室内 + 光照稳定 + 近距离:RGB-D + IMU 是性价比最高的选择(RGB-D 直接给深度且便宜),代表如很多扫地机器人和 AR 设备。但 RGB-D 一出门见阳光就废,量程也窄。 - 可能遇到弱纹理(白墙、玻璃幕墙)或弱几何(长走廊、开阔广场):必须上 LVI 三模态。因为弱纹理坑视觉、弱几何坑激光,只有两者都有才能在任一场景下保底。这就是 LVI-SAM/FAST-LIVO2 的设计目标场景。 - 室外大范围 + 需要全局一致:在 LVI 基础上加 GNSS,用 GNSS 因子把累积漂移钉死在全局坐标系(这正是 LIO-SAM 集成 GPS 的原因,见 §39.6)。 - 追求极致简洁、场景几何特征丰富:LIO(激光+IMU)就够了,FAST-LIO2 的成功证明了这一点——它甚至没有相机和回环,靠高精度 LIO 前端在很多场景下就足够好。
理论-工程桥接:上面这张选型表不是凭空来的,每个分支都对应前面"失效模式表"里的某一行。"弱纹理风险高 → 加激光"对应的是"单目/双目相机在弱纹理失效";"需要全局定位 → 加 GNSS"对应的是"所有本体传感器都会累积漂移,只有 GNSS 提供全局锚点"。架构选型的本质,就是用一种传感器的可观测维度去覆盖另一种传感器的失效维度,直到你的目标场景里不存在任何"全部传感器同时失效"的维度。 当你下次面对一个陌生场景要做选型时,就这样推:列出场景里所有可能的失效因素(暗?空旷?走廊?室外?打滑?),然后挑一组传感器使得每个失效因素都至少被一个不受它影响的传感器覆盖。
⚠️ 常见陷阱¶
陷阱 1(概念误区):认为"传感器越多,精度一定越高" - 错误描述:在系统里堆砌尽可能多的传感器,期望精度单调上升。 - 现象/后果:加了某个传感器后精度不升反降,甚至系统变得不稳定。 - 根本原因:每个传感器都引入自己的噪声、外参误差、时间同步误差。如果一个传感器的有效信息增量小于它带来的标定/同步误差,净效果是负的。更糟的是,如果这个传感器有未被处理的失效模式(如 GNSS 多路径),它会在失效时主动污染估计。融合的收益来自**互补**(覆盖新维度),而非**冗余**(重复观测已可观维度)。 - 正确做法:先用失效模式表分析目标场景,只加入能覆盖新失效维度的传感器。每加一个传感器都要单独验证它的净收益(消融实验:有/无该传感器的精度对比)。对每个传感器都必须实现失效检测,确保它失效时影响为零。
陷阱 2(思维陷阱):把"频率高"等同于"信息多" - 错误描述:认为 1000Hz 的 IMU 比 10Hz 的 LiDAR 提供多 100 倍的信息,因此应该让 IMU 主导估计。 - 现象/后果:过度信任 IMU 的高频输出,系统快速漂移。 - 根本原因:IMU 的高频测量是**相对的、会积分漂移的**——它告诉你"相对上一刻怎么动",但不告诉你"绝对在哪"。LiDAR 虽然只有 10Hz,但每一帧都提供**绝对的、不漂移的几何约束**。频率和信息价值是两回事:高频传感器适合做预测(短时插值),低频传感器适合做修正(消除漂移)。这正是 ESKF 里"IMU 做预测步、LiDAR/相机做更新步"分工的根本原因。 - 正确做法:在融合架构里给每个传感器分配恰当的角色——高频相对传感器(IMU)做运动预测和插值,低频绝对传感器(LiDAR/相机/GNSS)做漂移修正。不要让任何一类单独主导。
陷阱 3(概念误区):忽略"静止/纯旋转退化" - 错误描述:用纯视觉或纯单目方案部署到会频繁停车或原地转向的机器人上。 - 现象/后果:机器人停下或原地旋转时,位姿估计跳变、尺度漂移、甚至跟踪丢失。 - 根本原因:单目视觉的尺度恢复依赖平移产生的视差。静止(无平移)或纯旋转(旋转中心在相机光心)时视差为零,三角化失败,尺度不可观。这是几何本质,不是 bug。 - 正确做法:会走走停停的场景必须引入静止时仍提供信息的传感器(IMU 测重力、LiDAR 测几何、轮速计测零速)。很多 VIO 系统专门加了"零速检测"(ZUPT, Zero-Velocity Update)——检测到静止时用"速度为零"作为观测来抑制漂移。
练习¶
-
(分析题) 一个仓储巡检机器人需要在以下环境工作:明亮的货架区(纹理丰富)、昏暗的冷库(接近全黑)、室外装卸平台(有阳光、偶尔下雨)、地面偶尔有水渍(轮子可能打滑)。请用本节的"失效模式表"逐一分析每种环境会让哪些传感器失效,然后设计一套传感器组合,使得每种环境下都至少有两种互补传感器仍然可观。说明你的理由。
-
(对比题) 双目相机和"单目相机 + IMU"都能恢复绝对尺度,但恢复机制完全不同。请分别说明两者恢复尺度的物理原理,并对比:(a) 哪种在静止时仍能维持尺度?(b) 哪种对标定误差更敏感?(c) 哪种在快速运动时更可靠?给出你的分析。
-
(开放设计题) 假设你要给一个穿越长隧道的自动驾驶卡车设计传感器方案。隧道内:无 GNSS、墙面光滑无几何特征(激光在前进方向退化)、照明昏暗且周期性闪烁(视觉时好时坏)。请问:仅靠"激光+视觉+IMU"能否在隧道里保持前进方向可观?如果不能,你会引入什么额外传感器或约束来补上前进方向?(提示:回顾失效模式表里"轮速计"那一行,以及思考"匀速假设"能否作为一种软约束。)
§39.2 松耦合 vs 紧耦合——融合层次的根本抉择 ⭐⭐⭐¶
一句话总结:松耦合让每个传感器先独立算出自己的位姿估计、再把这些"半成品结果"融合;紧耦合则把所有传感器的**原始测量**直接塞进同一个估计器联合求解。前者像把几个专家各自写好的报告拼在一起,后者像让所有专家围着同一张桌子从原始证据开始一起推理——后者用得更充分,但任何一个专家递错证据都会污染整桌讨论。
动机:上一节我们决定了用哪些传感器,现在要决定"它们在哪一层握手"¶
承接 §39.1:假设我们已经决定用"激光 + 相机 + IMU"三模态。现在面临一个绕不开的工程问题——这三股数据流到底在系统的哪一层汇合? 是让激光先独立跑出一条轨迹、相机先独立跑出一条轨迹,然后在最后把两条轨迹"平均"一下?还是把激光的每一个点、相机的每一个像素、IMU 的每一次测量,全部当作约束扔进同一个优化问题里一起解?
这个"在哪一层汇合"的选择,就是**融合层次**(fusion level),它有两个极端:松耦合(loose coupling)和紧耦合(tight coupling)。这是多传感器架构里最根本、最影响代码结构的一个抉择——它决定了你的数据流图长什么样、哪些模块能独立测试、单传感器坏掉时系统怎么反应。
反面:先看一个"假装融合"的松耦合反例¶
设想最朴素的"融合":你有一个现成的激光里程计(输出 LiDAR 位姿 \(T_L\))和一个现成的视觉里程计(输出相机位姿 \(T_C\)),你想把它们融合。最偷懒的做法是——把两个位姿做加权平均:
// ❌ 反例:朴素位姿平均——这几乎总是错的
Eigen::Vector3d t_fused = 0.5 * T_L.translation() + 0.5 * T_C.translation();
Eigen::Quaterniond q_fused = T_L.unit_quaternion().slerp(0.5, T_C.unit_quaternion());
这个做法错在哪?
第一,它丢掉了每个估计的不确定性。激光里程计在弱几何方向上的估计可能完全是垃圾(不确定性极大),视觉里程计在弱纹理区的估计同样不可信。简单平均把一个可信值和一个垃圾值各取 50%,结果比单独用可信的那个还差。正确的融合必须按**逆协方差(信息矩阵)加权**——谁更确定就更信谁。
第二,它没有处理两个估计之间的相关性。两个里程计可能共享同一个 IMU、同一段历史,它们的误差不独立。把相关的估计当独立的融合,会过度自信(协方差被低估)。
第三,它在测量层面浪费了信息。激光里程计内部把成千上万个点压缩成了一个 6 自由度位姿,这个压缩过程丢掉了大量信息——比如"这个方向我测得特别准、那个方向几乎没约束"。等它输出位姿时,这些细粒度的方向性信息已经没了。
这个反例引出了松耦合和紧耦合的正式区分。
历史:从松耦合的工程务实到紧耦合的精度追求¶
早期多传感器系统几乎都是松耦合的,原因很现实:模块复用。激光里程计和视觉里程计往往是不同团队、不同时期独立开发的成熟模块,把它们的输出在后端用一个 EKF 或位姿图融合,是最快能出系统的做法。LOAM + 视觉、GPS + INS(惯性导航)的传统组合导航,都是松耦合的典型。松耦合的工程价值在于:每个子系统可以独立开发、独立测试、独立替换。
随着对精度和鲁棒性要求的提高,紧耦合逐渐成为研究前沿的主流。MSCKF(2007)是早期紧耦合 VIO 的代表——它把视觉特征的原始观测直接放进卡尔曼滤波,而不是先算视觉位姿。VINS-Mono(2018)用滑窗优化把 IMU 预积分和视觉重投影残差放在同一个优化问题里。到了 LVI 时代,FAST-LIVO2 把激光的点到面残差和相机的光度残差放进同一个 ESIKF 顺序更新——这是紧耦合的极致。
但有意思的是,松耦合并没有消失。R3LIVE(2021,本章 §39.6 精读对象)就是一个精心设计的松耦合系统——它的 LIO 子系统和 VIO 子系统并行运行、通过共享状态通信。R3LIVE 证明了:精心设计的松耦合,配合共享状态和合理的信息传递,可以接近紧耦合的精度,同时保留模块独立性的工程优势。所以这不是"紧耦合一定更好"的单调进步史,而是一个持续的工程权衡。
理论:松耦合与紧耦合的精确定义¶
我们用一个统一的框架来定义。设系统状态为 \(\mathbf{x}\)(位姿、速度、零偏等),传感器 \(i\) 的原始测量为 \(\mathbf{z}_i\)。
松耦合:每个传感器 \(i\) 先独立地从自己的测量解出一个**局部状态估计** \(\hat{\mathbf{x}}_i\)(通常是位姿),然后融合层只用这些局部估计:
关键特征:融合层**看不到原始测量** \(\mathbf{z}_i\),只看到各子系统压缩后的估计 \(\hat{\mathbf{x}}_i\)。
紧耦合:所有传感器的原始测量直接进入同一个估计器,联合求解:
其中 \(\mathbf{r}_i\) 是传感器 \(i\) 的原始测量残差(如 LiDAR 点到面距离、视觉重投影误差),\(\Sigma_i^{-1}\) 是对应的信息矩阵。关键特征:没有中间的局部估计,原始测量直接约束全局状态。
两者的本质区别,可以用一句话概括:
本质洞察:松耦合在"测量 → 局部位姿"这一步就把信息**压缩并丢失**了——一个 6 自由度位姿无法承载"这个方向测得准、那个方向没约束"这种细粒度的方向性不确定性。紧耦合保留了全部原始测量,让估计器在融合时能看到每个测量在每个方向上的真实约束强度。松耦合丢的不是数据量,而是数据的"方向结构"——而恰恰是这个方向结构,决定了退化场景下哪个传感器该在哪个方向上被信任。 这就是为什么紧耦合在退化场景(走廊、弱纹理)下往往明显更鲁棒:它知道激光在前进方向没约束,于是自动让视觉在那个方向接管;松耦合的激光子系统已经"自作主张"地在前进方向填了个垃圾位姿值,融合层无从分辨。
理论:两者的工程权衡对比¶
| 维度 | 松耦合 | 紧耦合 |
|---|---|---|
| 融合对象 | 各传感器的位姿估计(半成品) | 各传感器的原始测量 |
| 信息利用 | 不充分(压缩丢失方向结构) | 最充分 |
| 退化鲁棒性 | 弱(子系统已自行填垃圾值) | 强(估计器感知每个方向的约束强度) |
| 实现复杂度 | 低(子系统可独立开发/测试/替换) | 高(所有测量耦合在一个估计器里) |
| 模块独立性 | 高(一个子系统坏了可单独调试) | 低(强耦合,单点测量错误污染全局) |
| 时间同步要求 | 较宽松(融合的是低频位姿) | 严格(原始测量需精确对齐) |
| 外参标定要求 | 较宽松 | 严格(外参错误直接进残差) |
| 计算分布 | 分散(各子系统并行) | 集中(一个大优化/滤波) |
| 单传感器故障影响 | 局部(可隔离失效子系统) | 全局(需专门的鲁棒核/降权机制) |
| 典型系统 | R3LIVE、传统 GPS/INS 组合导航 | FAST-LIVO2、VINS-Mono、LVI-SAM |
请特别注意"单传感器故障影响"这一行——它揭示了一个反直觉的权衡:紧耦合用得更充分,但也更脆弱。因为所有测量耦合在一起,一个被污染的测量(如视觉的错误匹配)会通过耦合污染整个状态。所以紧耦合系统必须配备强力的鲁棒机制(鲁棒核函数、外点剔除、退化降权),否则它在异常情况下可能比松耦合崩得更厉害。松耦合反而因为子系统隔离,更容易做故障隔离——这正是 R3LIVE 选择松耦合的一个重要理由。
对比性思维("不是 X 而是 Y"):很多初学者以为"紧耦合 = 高级 = 总是更好的选择",这是错的。正确的理解是:紧耦合**不是**"更先进的松耦合",而是一个**用工程复杂度和脆弱性换取信息利用率和退化鲁棒性**的权衡。当你的场景里退化风险低、传感器都可靠、且你需要快速集成现成模块时,松耦合是更明智的工程选择。R3LIVE 的作者完全有能力做紧耦合,但他们选择了松耦合——这是深思熟虑的工程判断,不是技术不够。
实现:松耦合的典型 C++ 结构(以 R3LIVE 式共享状态为例)¶
松耦合的精髓在于"子系统并行 + 共享状态通信"。下面展示这种模式的骨架。
Step 1:为什么用"共享状态"而不是"消息传递"?
松耦合的两个子系统(LIO 和 VIO)需要交换信息——LIO 算出的位姿要给 VIO 当
初始值,VIO 优化后的结果要回写。有两种通信方式:
方式 A(消息传递):LIO 把位姿打包成 ROS 消息发给 VIO。
问题:消息有序列化/反序列化开销,有传输延迟,且只能传"快照"。
方式 B(共享状态):LIO 和 VIO 共享同一个全局状态对象 g_lio_state,
LIO 更新后直接写入,VIO 读取后优化再写回。
优势:零拷贝、零延迟、两个子系统看到的永远是最新状态。
R3LIVE 选方式 B——用一个全局状态结构体 + 互斥锁保护。代价是:必须
小心管理并发写入(两个子系统可能同时想改状态),这正是松耦合"模块独立
但需要协调"的体现。
Step 2:正确写法——共享状态 + 互斥锁
// 全局共享状态(R3LIVE 的 g_lio_state 风格)
struct GlobalState {
Eigen::Vector3d pos; // 位置
Eigen::Matrix3d rot; // 姿态(旋转矩阵)
Eigen::Vector3d vel; // 速度
Eigen::Vector3d bias_g; // 陀螺零偏
Eigen::Vector3d bias_a; // 加速度计零偏
Eigen::Matrix<double, 29, 29> cov; // 协方差(含外参、内参、时间偏移)
double last_update_time;
};
GlobalState g_lio_state; // 全局唯一状态
std::mutex g_state_mutex; // 保护并发访问
// LIO 子系统线程:更新后写入共享状态
void lio_thread() {
while (ros::ok()) {
LidarFrame frame = lidar_queue.pop(); // 取一帧点云
// ... ESKF 用点到面残差更新本地状态副本 local ...
{
std::lock_guard<std::mutex> lk(g_state_mutex); // ✅ 加锁
g_lio_state.pos = local.pos; // 原子地写回
g_lio_state.rot = local.rot;
g_lio_state.cov = local.cov;
g_lio_state.last_update_time = frame.time;
} // 锁在此自动释放(RAII)
}
}
// VIO 子系统线程:读取共享状态作为初值,优化后写回
void vio_thread() {
while (ros::ok()) {
ImageFrame img = image_queue.pop();
GlobalState init;
{
std::lock_guard<std::mutex> lk(g_state_mutex); // ✅ 读也要加锁
init = g_lio_state; // 拷贝一份当初值
}
// ... 用 init 当初值做视觉优化,得到 refined ...
{
std::lock_guard<std::mutex> lk(g_state_mutex);
// 注意:这里不能无脑覆盖!LIO 可能在此期间又更新了。
// R3LIVE 的做法是只在 VIO 时间戳更新时才写回对应字段。
if (refined.time >= g_lio_state.last_update_time) {
g_lio_state.pos = refined.pos;
g_lio_state.rot = refined.rot;
}
}
}
}
Step 3:错误写法及其后果
// ❌ 错误 1:读写共享状态不加锁
void vio_thread_BAD() {
GlobalState init = g_lio_state; // 数据竞争!LIO 可能正在写
// 后果:读到"撕裂"的状态——pos 是新的、rot 是旧的,
// 这是未定义行为(UB),可能读到一半被改的内存。
// 在多核上几乎必然出问题,且难以复现、难以调试。
}
// ❌ 错误 2:VIO 优化完无脑覆盖整个状态
{
std::lock_guard<std::mutex> lk(g_state_mutex);
g_lio_state = refined; // 把 LIO 在这期间的更新全冲掉了!
// 后果:VIO 优化耗时较长(比如 30ms),这期间 LIO 可能更新了 6 次
// (200Hz)。无脑覆盖会丢掉这 6 次 LIO 更新,导致状态回退。
}
// ❌ 错误 3:持锁期间做耗时计算
{
std::lock_guard<std::mutex> lk(g_state_mutex);
init = g_lio_state;
refined = expensive_visual_optimization(init); // 持锁 30ms!
g_lio_state = refined;
// 后果:LIO 线程在这 30ms 里全被阻塞在锁上,IMU/LiDAR 数据堆积,
// 高频里程计的实时性被毁。锁应该只保护"拷贝数据"这个瞬间操作。
}
Step 4:松耦合的数据流全景
LiDAR ──→ [LIO 子系统线程] ──写──┐
IMU ──→ (ESKF/iEKF) ├──→ g_lio_state ←──读── [VIO 子系统线程]
│ (共享状态) (视觉优化)
Camera ─────────────────────────────────────────────────────┘
└──写回──┘
特点:两个子系统在独立线程并行,各自处理自己的原始测量,
只通过低维的共享状态(位姿+协方差)交换信息。
LiDAR 的点云、Camera 的像素永远不会出现在对方的估计器里。
实现:紧耦合的典型 C++ 结构(以 ESIKF 顺序更新为例)¶
紧耦合的精髓在于"所有原始测量进同一个估计器"。FAST-LIVO2 用 ESIKF 顺序更新,下面展示这种模式。
Step 1:为什么需要"顺序更新"而不是"一次性更新"?
紧耦合要把 LiDAR 残差和相机残差放进同一个卡尔曼滤波更新。最直接的想法是
把它们堆成一个大观测向量一次更新:
z = [z_lidar; z_camera] (维度 = LiDAR点数 + 相机patch像素数)
但这有个维度灾难问题:LiDAR 一帧可能有几千个点,相机一个 patch 有几百个
像素,两者维度差异巨大且时变。把它们堆在一起,观测雅可比 H 是一个超大的
矩阵,卡尔曼增益里的求逆 (H P H^T + R)^{-1} 维度爆炸。
FAST-LIVO2 的解法是顺序更新(sequential update):先用 LiDAR 残差做一次
ESIKF 更新得到中间状态,再用相机残差在中间状态基础上做第二次更新。数学上,
当两类观测噪声独立时,顺序更新和联合更新等价(这是卡尔曼滤波的可分解性),
但顺序更新避免了大矩阵求逆,每次只处理一种传感器的观测。
Step 2:正确写法——ESIKF 顺序更新主循环
// FAST-LIVO2 风格的紧耦合主循环(单线程顺序执行)
void process_frame(const MeasureGroup& meas) {
// ① IMU 前向传播:用本帧内的 IMU 序列把状态推进到帧末时刻
// 同时传播协方差 P,并用 IMU 积分做点云去畸变
state_propagate_with_imu(meas.imu, state, P);
// ② LiDAR 更新:点到面残差,第一次 ESIKF 更新
if (!meas.lidar->empty()) {
for (int iter = 0; iter < MAX_ITER; ++iter) { // 迭代卡尔曼(IEKF)
compute_lidar_residual(state, meas.lidar, H_lidar, r_lidar);
// 卡尔曼增益(点到面,H 行数 = 有效点数)
K = P * H_lidar.transpose() *
(H_lidar * P * H_lidar.transpose() + R_lidar).inverse();
dx = K * r_lidar;
state.boxplus(dx); // 流形上的状态更新(⊞ 运算)
if (dx.norm() < EPS) break; // 收敛即停
}
P = (I - K * H_lidar) * P; // 更新协方差
}
// ③ 相机更新:光度残差,在 ② 的结果基础上做第二次 ESIKF 更新
if (meas.img) {
for (int iter = 0; iter < MAX_ITER; ++iter) {
compute_photometric_residual(state, meas.img, H_cam, r_cam);
K = P * H_cam.transpose() *
(H_cam * P * H_cam.transpose() + R_cam).inverse();
dx = K * r_cam;
state.boxplus(dx);
if (dx.norm() < EPS) break;
}
P = (I - K * H_cam) * P;
}
// ④ 此时 state 已融合了 IMU + LiDAR + 相机三种原始测量
}
Step 3:错误写法及其后果
// ❌ 错误 1:在切空间外直接加 dx(忽略流形结构)
state.rot = state.rot + dx.block<3,1>(3,0); // 旋转矩阵直接加向量?
// 后果:旋转矩阵加一个向量不再是合法旋转矩阵(不满足正交+行列式为1)。
// 必须用 boxplus:state.rot = state.rot * Exp(dx_rot)。
// 这是李群更新的铁律,见「李群李代数」章节。
// ❌ 错误 2:LiDAR 和相机更新顺序颠倒且不重新计算残差
compute_lidar_residual(state, ...);
compute_photometric_residual(state, ...); // 用的还是 LiDAR 更新前的 state!
// 后果:相机残差应该在 LiDAR 更新后的状态上计算,否则两次更新不是
// "顺序精化"而是"基于同一旧状态的两次独立更新",破坏了顺序更新
// 与联合更新的等价性。
// ❌ 错误 3:把 LiDAR 和相机观测噪声设成相关
// R_joint = [[R_lidar, R_cross], [R_cross^T, R_cam]] // 引入了交叉项
// 后果:顺序更新的等价性前提是两类观测噪声独立(R 块对角)。
// 如果它们真的相关(极少见),必须联合更新,不能顺序更新。
// 好在 LiDAR 测距噪声和相机光度噪声物理上确实独立,前提成立。
Step 4:紧耦合的数据流全景
IMU ──→ 前向传播 ──→ ┌─────────────────────────────┐
│ 单一 ESIKF 状态 + 协方差 │
LiDAR ──原始点云──────→ │ ① IMU 传播 │
│ ② LiDAR 点到面残差更新 │
Camera ──原始像素─────→ │ ③ 相机光度残差更新 │
└─────────────────────────────┘
特点:三种传感器的原始测量都直接进入同一个 ESIKF。
没有"子系统",没有中间位姿估计。所有信息在测量层面融合。
代价:单线程顺序执行,任一传感器的坏测量直接污染状态。
把松耦合和紧耦合的两张数据流图对比着看,你能立刻看出架构的根本差异:松耦合有**两个并行的估计器**和它们之间的共享状态通道;紧耦合只有**一个估计器**,所有原始测量汇入其中。这个差异会一路传导到代码组织、线程模型、测试策略、故障处理——后面 §39.6 精读三个系统时,你会反复看到这个二分。
⚠️ 常见陷阱¶
陷阱 1(概念误区):把"松耦合"等同于"低精度"、把"紧耦合"等同于"高精度" - 错误描述:见到松耦合就认为这是个低端方案,见到紧耦合就认为精度一定更高。 - 现象/后果:盲目把一个工作良好的松耦合系统改成紧耦合,结果引入了同步/标定/鲁棒性的一堆新问题,精度反而下降。 - 根本原因:精度取决于"信息利用是否充分"和"坏测量是否被正确处理"两个因素的综合。紧耦合信息利用更充分(+),但对同步/标定误差更敏感、对坏测量更脆弱(−)。如果你的同步/标定不够精确,紧耦合的负面因素会压过正面因素。R3LIVE 的松耦合精度很高,正是因为它的子系统各自做得很扎实、信息传递设计得当。 - 正确做法:根据场景和工程约束选择,而非迷信"紧耦合更高级"。同步标定难保证、需要模块独立性、退化风险低时,松耦合是合理选择。
陷阱 2(编程陷阱):松耦合共享状态的数据竞争与持锁过久
- 错误描述:读写共享状态不加锁,或持锁期间做耗时计算。
- 现象/后果:不加锁→读到撕裂状态、偶发崩溃、结果随机错误且难复现;持锁过久→高频子系统被阻塞、实时性崩溃。
- 根本原因:多个线程并发读写同一内存是数据竞争(C++ 中是未定义行为)。而锁的持有时间直接等于其他线程的最长阻塞时间——在锁里做优化计算会把并行变成串行。
- 正确做法:用 std::lock_guard 保证 RAII 加锁/解锁;锁的临界区只包含"拷贝数据"这种瞬间操作,把耗时计算放在锁外。需要读一份快照就先在锁内拷贝出来,到锁外慢慢算。
陷阱 3(思维陷阱):紧耦合时忽略单测量污染,不加鲁棒核 - 错误描述:把所有原始测量平等地塞进紧耦合估计器,不做外点剔除、不加鲁棒核。 - 现象/后果:一次视觉误匹配或一个 LiDAR 动态点(行人、车辆),就能让整个状态跳变。 - 根本原因:紧耦合的强耦合是双刃剑——它让好测量充分发挥作用,也让坏测量充分破坏。一个量级异常的残差会主导最小二乘(因为平方放大),把状态拉偏。 - 正确做法:紧耦合**必须**配鲁棒机制。优化框架里用鲁棒核(Huber/Cauchy)压制大残差;滤波框架里用卡方检验(chi-square test)剔除外点观测;对整类传感器用退化检测(§39.5)做方向性降权。鲁棒性不是紧耦合的可选项,而是必需品。
练习¶
-
(分析题) 给你一段陌生的 SLAM 代码,你如何快速判断它是松耦合还是紧耦合?请列出至少三个"代码层面的判别特征"(提示:看估计器的数量、看原始测量出现在哪里、看线程结构)。然后说明:如果看到代码里有一个全局共享的状态结构体被多个线程读写,这更可能是松耦合还是紧耦合?为什么?
-
(设计扩展题) 上面的松耦合
vio_thread在写回时用了if (refined.time >= g_lio_state.last_update_time)来避免覆盖更新的 LIO 状态。但这个判断仍不完善——如果 VIO 优化耗时很长,等它写回时 LIO 早已更新多次,简单的时间戳比较会导致 VIO 的优化结果被丢弃(白算了)。请设计一个更好的方案,让 VIO 的优化结果能正确地"叠加"到最新的 LIO 状态上,而不是被丢弃。(提示:VIO 优化得到的是一个"相对修正量"还是一个"绝对位姿"?如果把它表示成相对修正量会怎样?) -
(实现题) 仿照 ESIKF 顺序更新的结构,写出一个"IMU + LiDAR + 相机 + 轮速计"四传感器紧耦合的顺序更新伪代码框架。轮速计提供平面速度观测。要求:(a) 标明每个传感器更新的顺序和理由;(b) 轮速计观测在打滑时不可信,写出你如何在更新前判断是否采信它(提示:比较轮速观测和当前状态预测的速度之差,超过阈值则跳过该观测)。
§39.3 时间同步与外参标定——把传感器接进系统的两个工程前提 ⭐⭐⭐¶
一句话总结:上一节我们决定了"传感器在哪一层握手",但无论松耦合还是紧耦合,握手之前都必须先回答两个物理问题——两个测量是不是同一时刻发生的(时间同步)、两个传感器在空间里差多少(外参标定)。这两件事做不好,再精巧的融合数学都建在沙地上:时间差几毫秒会在高速运动下放大成发散,外参差几度会让紧耦合直接崩。
动机:融合的隐含前提是"对齐"¶
回顾 §39.2,无论松耦合的共享状态、还是紧耦合的联合估计,我们写下的每一个残差都隐含着一个假设:这两个测量描述的是同一个时空点。比如紧耦合里相机的光度残差,本质是"把 LiDAR 重建的地图点投影到当前相机像素,比较预测亮度和实测亮度"。这个残差只有在"相机这一帧和 IMU 推算出的这个位姿是同一时刻"且"相机相对 IMU 的位姿(外参)已知"时才成立。
这两个前提,前者叫**时间同步**(temporal synchronization),后者叫**外参标定**(extrinsic calibration)。它们不是"锦上添花的优化",而是融合能否成立的**地基**。地基歪一寸,楼塌一丈——而且塌的方式很隐蔽:系统在低速、静止时看起来一切正常,一到高速运动或剧烈转动就发散,让人误以为是滤波器参数没调好,实际是对齐没做对。
本质洞察:融合的数学(卡尔曼增益、信息矩阵、最小二乘)默认输入数据已经"时空对齐"。对齐是数据预处理层的责任,不是估计器的责任。把对齐误差留给估计器去"自动消化",等于让一个解微分方程的求解器去猜你的初值——它能跑,但跑出来的不是你要的答案。
反面:一个被时间偏移悄悄毁掉的系统¶
设想你给一个跑得很好的纯 LIO 系统加相机。你把相机驱动跑起来,时间戳用相机驱动报的 ROS 接收时间戳(ros::Time::now(),即数据到达 CPU 的时刻,而非曝光时刻)。低速测试一切正常,你很满意,提交了代码。
然后机器人快速转弯,角速度到了 200°/s。相机的"接收时间戳"比"真实曝光时刻"晚了大约 30ms(USB 传输 + 驱动缓冲 + ROS 序列化)。在 200°/s 下,30ms 对应:
也就是说,紧耦合估计器以为相机"在某个位姿"拍的照,实际相机在 6° 之外的姿态拍的。这个 6° 的姿态错位被投影进光度残差,估计器为了"消掉"这个莫名其妙的残差,会把整个状态往错误方向拉。在快速转弯时残差最大、拉得最狠,于是系统在转弯时输出乱跳——而你因为低速时正常,会一直怀疑是滤波器参数,永远查不到根因。
时间偏移 t_d 与姿态误差的放大关系(直观感受为什么"几毫秒"是大事):
角速度 ω 时间偏移 t_d 引入的姿态误差 Δθ = ω·t_d
─────────────────────────────────────────────────────────
10°/s(缓慢) 30ms 0.3° ← 几乎无感
100°/s(正常) 30ms 3.0° ← 已经明显
200°/s(快速) 30ms 6.0° ← 紧耦合发散
500°/s(剧烈) 30ms 15.0° ← 彻底崩溃
结论:同样的 t_d,运动越剧烈,误差越大。这就是"低速正常、高速发散"的根因。
历史:从硬件触发到在线时间标定¶
时间同步的方案演进,本质是"把对齐责任从硬件逐步搬到软件、最后搬进估计器"的过程:
- 第一代:硬件触发(hardware trigger)。 用一根物理线,让 IMU 或一个外部信号发生器去触发相机曝光、触发 LiDAR 转一圈的起点。所有传感器共享同一个时钟源,时间偏移天然为零。代价是硬件复杂、并非所有传感器都支持外触发(很多消费级相机没有触发引脚)。
- 第二代:软件时间戳对齐 + 时钟同步协议。 用 PTP(Precision Time Protocol,IEEE 1588)或 PPS(Pulse Per Second,常配合 GNSS)让多台设备的系统时钟对齐到亚微秒级,每个传感器在自己的驱动里打"采集时刻"的时间戳,融合时按时间戳插值对齐。代价是要求每个驱动都正确地打"采集时刻"而非"接收时刻"。
- 第三代:在线时间标定(online temporal calibration)。 干脆把传感器间的时间偏移 \(t_d\) 当作一个待估变量,放进状态向量或因子图里一起优化。VINS-Mono 的
estimate_td选项、Kalibr 工具箱都属于这一代。代价是增加了状态维度和可观测性要求(\(t_d\) 在匀速运动下不可观,必须有加减速激励才能估出来)。
理论:时间同步的三种层次与各自代价¶
把上面三代方案整理成一张工程决策表——你在实际项目里要根据"硬件支不支持""精度要求多高""愿不愿意改驱动"来选:
| 同步层次 | 机制 | 可达精度 | 前提条件 | 典型代价 |
|---|---|---|---|---|
| 硬件触发 | 共享时钟源 / 外触发线 | 微秒级(近乎完美) | 传感器有触发引脚、能接线 | 硬件复杂、布线、不通用 |
| PTP/PPS 时钟同步 | 协议对齐系统时钟 | 亚微秒~微秒 | 驱动打"采集时刻"戳 | 需交换机支持 PTP / GNSS 接 PPS |
| 软件时间戳插值 | 按时间戳线性插值对齐 | 毫秒级(受驱动质量限制) | 时间戳来源一致、单调 | 驱动若打"接收时刻"则精度差 |
| 在线 \(t_d\) 估计 | 把 \(t_d\) 放进估计器 | 取决于激励充分性 | 运动有加减速激励 | 增维、可观测性要求 |
这四种不是互斥的——实践中常组合:先用 PTP 把系统时钟对齐到微秒,再用软件插值对齐到具体测量时刻,最后用在线 \(t_d\) 估计吸收残余的固定延迟(如曝光中点偏移)。
在线时间标定的数学形式。以视觉-惯性为例,设相机驱动报的时间戳为 \(t\),真实曝光时刻为 \(t + t_d\)(\(t_d\) 是待估的固定偏移)。一个被跟踪的特征点在图像上的速度为 \(\mathbf{v}_{\text{feat}}\)(像素/秒,由光流估计),那么把特征观测从"驱动时刻"修正到"真实时刻"只需平移:
这个修正后的像素坐标进入重投影残差,对 \(t_d\) 求导即可把 \(t_d\) 纳入优化。关键洞察:\(t_d\) 的可观测性来自 \(\mathbf{v}_{\text{feat}} \neq 0\) 且运动状态在变化——如果相机匀速直线运动、特征点像素速度恒定,\(t_d\) 的平移和位姿的平移就耦合在一起分不开了(这是个常见的可观测性陷阱,见本节陷阱 3)。
实现:软件时间戳对齐——线性插值与最近邻的取舍¶
最常见的工程场景是:你有一串高频 IMU(200Hz)和一帧相机(30Hz),要为相机这一帧找到"对应时刻的 IMU 状态"。两个 IMU 采样之间没有相机时刻的数据,必须插值。
Step 1:为什么不能用"最近邻"凑合
IMU 时间轴: ●────────●────────●────────● (每 5ms 一个, 200Hz)
↑
相机时刻: t_cam (落在两个 IMU 采样之间)
最近邻:直接拿离 t_cam 最近的那个 IMU 采样。
问题:最坏情况下时间差达半个 IMU 周期 = 2.5ms。
在 200°/s 转速下,2.5ms = 0.5° 误差,对紧耦合已不可忽略。
线性插值:用 t_cam 前后两个 IMU 采样按时间比例插值。
优势:时间误差→0(只要 IMU 在这 5ms 内近似线性)。
Step 2:正确写法——带流形插值的对齐
// 为给定时刻 t_query 插值出 IMU 状态(关键:旋转必须在流形上插值)
struct ImuSample { double t; Eigen::Vector3d gyr, acc; };
ImuSample interpolate_imu(const ImuSample& a, const ImuSample& b, double t_query) {
// 防御:t_query 必须落在 [a.t, b.t] 内,否则是外推(危险)
assert(t_query >= a.t && t_query <= b.t && b.t > a.t);
double alpha = (t_query - a.t) / (b.t - a.t); // 插值比例 ∈ [0,1]
ImuSample out;
out.t = t_query;
out.gyr = (1 - alpha) * a.gyr + alpha * b.gyr; // 角速度是 R^3 向量,线性插值 OK
out.acc = (1 - alpha) * a.acc + alpha * b.acc; // 加速度同理
return out;
}
// 若插值的是"姿态 R"而非"角速度",必须用 SLERP(球面线性插值),不能线性插!
Eigen::Quaterniond slerp_rotation(const Eigen::Quaterniond& qa,
const Eigen::Quaterniond& qb, double alpha) {
return qa.slerp(alpha, qb); // Eigen 内置;等价于 qa * Exp(alpha * Log(qa^{-1} qb))
}
Step 3:错误写法及其后果
// ❌ 错误 1:对旋转矩阵/四元数做线性插值
Eigen::Quaterniond q_interp;
q_interp.coeffs() = (1 - alpha) * qa.coeffs() + alpha * qb.coeffs(); // 逐元素线性插值!
// 后果:线性插值的四元数不再是单位四元数(模长≠1),代表的"旋转"被缩放扭曲。
// 即便事后归一化,插值路径也不是测地线(最短旋转路径),中间姿态是错的。
// 两个相差 90° 的姿态,线性插值的中点不是 45°。必须用 slerp。
// ❌ 错误 2:t_query 落在区间外还硬插(变成外推)
double alpha = (t_query - a.t) / (b.t - a.t); // 若 t_query > b.t, alpha > 1
// 后果:alpha 超出 [0,1] 变成外推,IMU 在区间外的线性假设不成立,
// 高动态下外推几个毫秒就能引入大误差。必须先确保 query 时刻被两个采样夹住,
// 做法是:缓冲足够的 IMU,等到相机时刻被 IMU 数据"包住"再处理(引入一点延迟换正确性)。
// ❌ 错误 3:用 double 直接比较时间戳是否"相等"
if (imu.t == cam.t) { /* ... */ } // 浮点相等判断几乎永远为 false
// 后果:时间戳是 double,因表示误差几乎不可能精确相等,这个分支永远进不去。
// 应该用区间判断(a.t <= t && t <= b.t)或容差(fabs(x-y) < eps)。
Step 4:时间同步的数据流位置
原始传感器 时间对齐层(本节) 估计器(§39.2/39.4)
───────── ────────────────── ──────────────────
IMU @200Hz ──┐
├──→ [按时间戳排序入缓冲]
Cam @30Hz ──┤ [为每帧 Cam 插值对齐 IMU] ──→ 对齐好的测量组
LiDAR@10Hz ──┘ [LiDAR 去畸变需逐点对齐] (此时才进卡尔曼/因子图)
↑
时间偏移 t_d 若在线估计,则由估计器反馈修正这里
注意 LiDAR 的特殊性:机械式 LiDAR 转一圈需要 100ms,一帧点云里**不同点是在不同时刻、不同位姿下采集的**。所以 LiDAR 的"时间对齐"不是给整帧打一个时间戳,而是**逐点**用 IMU 积分做运动补偿(去畸变 / motion compensation),把所有点统一到同一参考时刻。这也是为什么 LIO 系统离不开 IMU——没有 IMU 提供帧内高频运动,点云去畸变无从谈起。
理论:外参标定——刚体变换的"另一半"¶
时间对齐解决了"何时",外参解决了"何地"。外参(extrinsic parameter)是两个传感器坐标系之间的刚体变换 \(\mathbf{T}_{B}^{A} \in SE(3)\)——把 B 坐标系下的点变到 A 坐标系下。比如相机相对 IMU 的外参 \(\mathbf{T}_{C}^{I}\),由旋转 \(\mathbf{R}_{C}^{I}\)(3 自由度)和平移 \(\mathbf{t}_{C}^{I}\)(3 自由度)共 6 个自由度组成。
为什么外参对紧耦合是生死问题?回顾 §39.2 的紧耦合光度残差:把地图点 \(\mathbf{p}^W\) 变到相机系再投影。这个变换链是:
其中 \(\mathbf{T}_{W}^{I}\) 是估计器要求的 IMU 位姿,\(\mathbf{T}_{C}^{I}\) 是外参。如果外参的旋转错了 2°,那么每一个投影点都偏 2°——在 10 米远处偏 \(10 \times \tan(2°) \approx 0.35\) 米,光度残差全错,估计器收到一致的"系统性偏差",要么发散,要么把这个偏差错误地补偿进位姿(产生固定的位姿偏置)。
| 标定参数 | 自由度 | 误差的后果 | 对架构的敏感度 |
|---|---|---|---|
| 旋转外参 \(\mathbf{R}\) | 3 | 投影方向偏,随距离放大 | 紧耦合极敏感、松耦合较宽容 |
| 平移外参 \(\mathbf{t}\) | 3 | 近距离偏明显、远距离可忽略 | 近景作业敏感 |
| 时间偏移 \(t_d\) | 1 | 高速运动下放大成姿态误差 | 高动态极敏感 |
| 相机内参 \(K\) | 4+ | 投影模型本身错 | 所有方案都敏感 |
理论-工程桥接:外参的 6 个自由度里,旋转 3 维比平移 3 维敏感得多。原因是旋转误差的影响随观测距离线性放大(远处一点点角度差就是很大的位置差),而平移误差是绝对的、不随距离放大。这就是为什么实践中"旋转外参标定不准"是紧耦合系统最常见的发散原因,而平移外参差几厘米往往还能跑。
理论:把时间偏移和外参误差合起来算一笔账¶
时间偏移和外参误差单独看都"只有几度/几毫秒",听起来微不足道。但它们在紧耦合里会叠加,且都被运动放大。我们算一笔具体的账,让你对"为什么对齐是地基"有量化的体感。
设一个无人机以角速度 \(\omega = 150°/\text{s}\) 转弯,观测一个 8 米远的特征点。三种误差源各自贡献多少投影偏差:
误差源 数值 在 8m 处的投影偏差(折算到位置)
──────────────────────────────────────────────────────────────
① 时间偏移 t_d=20ms Δθ = ω·t_d = 3° 8·tan(3°) ≈ 0.42 m
② 旋转外参误差 1.5° Δθ = 1.5° 8·tan(1.5°) ≈ 0.21 m
③ 平移外参误差 5cm Δt = 0.05 m 0.05 m(不随距离放大)
──────────────────────────────────────────────────────────────
叠加(最坏同向) — ≈ 0.68 m ← 8m 处偏了将近 0.7 米!
注意三件事。第一,时间偏移的贡献最大(0.42m),因为它被角速度放大,且 20ms 在"用接收时刻当时间戳"时很常见。第二,旋转外参误差(0.21m)随距离放大,8 米处已经 0.2 米,若特征在 30 米处会放大到 0.78 米。第三,平移外参误差(5cm)是唯一不随距离放大的,所以远景作业时它最不重要、近景抓取时才需在意。
对比性思维:把这三种误差和「卡尔曼滤波」里的"观测噪声"对比——观测噪声是**随机的、零均值的**,多帧平均会抵消;而对齐误差是**系统性的、有偏的**,它在每一帧都朝同一个方向偏,多帧平均不但不抵消,反而被估计器当成"一致的证据"采信,把状态拉向错误方向。这就是为什么对齐误差比随机噪声危险得多:随机噪声让估计"抖",系统性偏差让估计"偏"。滤波器的协方差能描述前者,却对后者无能为力——它会自信地收敛到一个错误答案。这也解释了 §39.5 为什么强调"残差小不代表估计准":系统性偏差下残差可以很小,但估计是偏的。
这笔账的工程结论很直接:紧耦合系统对对齐的要求是"误差累加后仍远小于你能容忍的定位精度"。如果你要厘米级定位,0.68 米的对齐偏差是灾难性的——必须把时间偏移压到 5ms 以内(硬件触发或在线 \(t_d\) 估计)、旋转外参标到 0.3° 以内(充分激励的离线标定)。松耦合对此宽容一些(子系统各自的位姿已经平滑过),但也只是程度问题,不是可以无视。
实现:在线外参估计 vs 离线标定¶
// 方案 A:离线标定(Kalibr 风格)——标定一次,运行时当常量
// 适用:外参在运行中不变(传感器刚性固连)。
// 做法:标定板 + 充分激励运动,离线优化出 T_C_I,写进配置文件。
struct ExtrinsicConfig {
Eigen::Matrix3d R_C_I; // 从标定文件读入,运行时只读
Eigen::Vector3d t_C_I;
};
// 方案 B:在线估计(VINS-Mono estimate_extrinsic 风格)——放进状态一起优化
// 适用:外参可能有微小漂移(温度形变、轻微松动),或懒得离线标。
struct StateWithExtrinsic {
// ... 位姿、速度、零偏 ...
Eigen::Quaterniond q_C_I; // 外参旋转,作为待估变量
Eigen::Vector3d t_C_I; // 外参平移,作为待估变量
// 代价:状态维度 +6;且外参的可观测性同样需要充分激励运动
};
关键工程经验:在线外参估计不是"免费午餐"。它要求运动充分激励所有自由度——纯平移运动估不出旋转外参,纯匀速运动估不出时间偏移。VINS-Mono 的做法很务实:提供一个"外参完全未知→在线标定→收敛后锁定为常量"的三档模式(estimate_extrinsic = 2/1/0),先粗标、再精修、最后固定,兼顾了鲁棒性和精度。
⚠️ 常见陷阱¶
陷阱 1(编程陷阱):用"数据到达时刻"当传感器时间戳
- 错误描述:用 ros::Time::now() 或回调被触发的时刻作为传感器测量的时间戳。
- 现象/后果:低速正常、高速发散;同一份数据在不同机器(CPU 负载不同)上表现不一致;偏移量随系统负载波动,无法用固定 \(t_d\) 补偿。
- 根本原因:数据从"传感器采集"到"CPU 回调"之间有传输+缓冲+调度延迟(USB/网络/驱动队列),这个延迟不仅大(几十毫秒)而且抖动。"接收时刻"≠"采集时刻",两者相差一个时变的延迟。
- 正确做法:用传感器硬件打的"采集时刻"时间戳(很多工业相机/LiDAR 在数据包里带硬件时间戳)。若驱动只给接收时刻,则必须先做时钟同步(PTP/PPS)或在线估计固定偏移 \(t_d\),且要意识到抖动部分无法靠固定 \(t_d\) 消除。
陷阱 2(编程陷阱):对旋转做线性插值而非 SLERP
- 错误描述:插值两个时刻的姿态时,对四元数或旋转矩阵逐元素线性插值。
- 现象/后果:插值出的"旋转"模长不为 1、不正交;中间姿态偏离测地线;大角度差时中点严重错位(90° 的中点不是 45°)。
- 根本原因:旋转构成的是流形 \(SO(3)\) 而非线性空间,两个旋转的线性组合一般不再是合法旋转。线性插值走的是"弦",正确的是走"弧"(测地线)。
- 正确做法:用 SLERP(Eigen::Quaterniond::slerp)或等价的 \(\mathbf{R}_a \mathrm{Exp}(\alpha \, \mathrm{Log}(\mathbf{R}_a^{-1}\mathbf{R}_b))\)。这与「李群李代数」章节的流形插值是同一回事——一切流形上的"中间量"都要走指数/对数映射,不能直接加权。
陷阱 3(思维陷阱):以为在线外参/时间标定能"自动搞定一切",忽略可观测性 - 错误描述:打开在线标定开关就不管了,假设系统会自动估出正确的外参和时间偏移。 - 现象/后果:外参/时间偏移估计值缓慢漂移或卡在错误值上不收敛;系统在标定参数错误的情况下勉强运行,精度莫名其妙地差。 - 根本原因:外参旋转、外参平移、时间偏移的可观测性都依赖**充分的运动激励**。纯平移估不出旋转外参;匀速直线运动下时间偏移和位姿平移耦合不可分;某些自由度长期不被激励,对应的标定参数就在零空间里随机游走。 - 正确做法:理解每个待估参数需要什么激励(旋转外参需要旋转运动、时间偏移需要加减速),在标定阶段主动做激励运动(绕各轴转、加减速)。生产中常用"先在线标定到收敛、再锁定为常量"的策略,避免长期在线估计带来的漂移。可用协方差或 Hessian 特征值监控标定参数是否真的收敛(与 §39.5 的可观测性分析同源)。
练习¶
-
(分析题) 你的相机-IMU 系统在快速转弯时输出乱跳,怀疑是时间偏移。设计一个**不依赖真值轨迹**的实验来估计时间偏移 \(t_d\) 的大小(提示:让系统做纯绕 Z 轴的来回旋转,比较 IMU 陀螺仪的角速度曲线和图像光流推算出的角速度曲线,两条曲线的时间错位就是 \(t_d\))。这种方法对平移外参有要求吗?为什么?
-
(实现题) 实现一个 IMU 缓冲管理器:传感器回调持续往里塞 IMU 数据,外部按任意
t_query来取插值后的 IMU 状态。要求:(a) 用std::deque维护按时间排序的缓冲;(b)t_query早于缓冲最早时刻或晚于最晚时刻时如何处理(早于→报错,晚于→等待更多数据,而非外推);(c) 及时丢弃太老的数据防止内存无限增长。写出关键的几个成员函数。 -
(设计扩展题) 机械式 LiDAR 一帧点云里不同点采集时刻不同(跨度约 100ms)。如果你的 IMU 突然丢了 50ms 的数据(驱动卡顿),这一帧的点云去畸变会发生什么?设计一个降级策略:当 IMU 数据有缺口时,如何尽量减少这一帧的去畸变误差,或者判断这一帧是否应该直接丢弃?(提示:考虑用匀速运动假设临时填补,并评估缺口时长对应的位姿不确定度。)
§39.4 因子图融合架构——异构约束如何在同一张图里统一优化 ⭐⭐⭐¶
一句话总结:因子图把"多传感器融合"统一成一句话——每种约束都是一个因子,所有因子连到共享的变量节点上,求一组变量使加权残差平方和最小。它和滤波器是求解器维度的两极:滤波器边缘化历史、只保留当前状态(快、省内存),图优化保留历史、可重线性化(准、能回环修正)。理解了"因子=带权残差块、变量=待优化量、信息矩阵=协方差的逆"这组对应,IMU 预积分、LiDAR 配准、视觉重投影、GPS、回环就能全部塞进同一张图。
动机:上一节解决了"对齐",现在要把对齐好的异构测量"统一表达"¶
经过 §39.3,我们手里有了时空对齐好的测量:IMU 序列、LiDAR 帧、相机帧、可能还有 GPS。但这些测量长得完全不一样——IMU 给的是帧间相对运动约束,LiDAR 配准给的是帧到地图的位姿约束,相机给的是路标点的重投影约束,GPS 给的是绝对位置约束。它们的维度不同、坐标系不同、噪声特性不同。
回顾 §39.2 的紧耦合:我们用 ESIKF 顺序更新把它们塞进同一个滤波器。但滤波器有个根本局限——它**边缘化掉了历史**:每更新一步,旧状态就被压缩进协方差矩阵,再也回不去了。这意味着两件事做不了:一是发现回环时无法修正历史轨迹(历史已经被边缘化),二是发现某个早期线性化点选错了无法重新线性化。
因子图正是为了突破这个局限。它的核心思想是:不要边缘化历史,把每一个测量都保留成一个因子,整张图一起优化。这样回环就是"在两个历史关键帧之间加一条因子边",重线性化就是"在新的线性化点上重新求解整张图"。代价当然是计算量和内存随历史增长——所以才有了滑窗(只保留近期)和 iSAM2(增量更新)这些技术来折中。
本质洞察:滤波器和图优化求解的是**同一个最大后验(MAP)问题**,区别只在"如何处理历史"。滤波器用边缘化(marginalization)把历史压成一个先验,得到 \(O(1)\) 的更新但丢失了重线性化能力;图优化把历史保留成因子,得到可重线性化、可回环的能力但付出 \(O(n)\) 的代价。iSAM2 是两者之间的精妙折中——它用贝叶斯树只重算受新因子影响的那部分变量,逼近滤波器的速度同时保留图优化的灵活性。
反面:用一个大滤波器硬扛回环会怎样¶
设想你用纯 ESKF 跑一个大场景 SLAM,绕了一大圈回到起点。由于积分漂移,你估计的"回到起点"的位姿和真正的起点差了 2 米。现在你的回环检测模块识别出"我回到起点了",给出一个约束:当前位姿 ≈ 起点位姿。
在滤波器里,起点的状态早在几千帧前就被边缘化了——它的信息被压进了当前协方差,但**那个状态变量本身已经不在状态向量里了**。你无法说"把当前位姿和起点位姿拉到一起",因为起点位姿这个变量已经不存在。你最多只能用回环约束更新当前位姿,但这只修正了当前一帧,中间几千帧的轨迹依然是漂的——整条轨迹没有被"拉直",回环的全局一致性收益拿不到。
而在因子图里,起点位姿变量一直在图里(或至少在可恢复的历史中)。回环就是在当前位姿节点和起点位姿节点之间加一条因子边,然后重新优化——优化器会把这 2 米的误差**均摊到整条回环路径的所有位姿上**,整条轨迹被一致地"拉直"。这就是因子图相对滤波器在大场景下的核心优势:全局一致的历史修正能力。
滤波器遇到回环: 因子图遇到回环:
起点(已边缘化,变量没了) 起点 ●━━━━━━━━━━━━━━━━┓
✗ 无法连接 ┃ ┃ 回环因子
┃ ┃ (新增一条边)
当前 ● ← 只能修正这一帧 ┃ ┃
误差留在中间轨迹 当前 ●━━━━━━━━━━━━━┛
↓ 重新优化
误差均摊到整条路径, 轨迹被拉直
历史:从 EKF-SLAM 到因子图的范式转移¶
- EKF-SLAM 时代(1990s-2000s):状态向量同时包含机器人位姿和所有路标点,协方差矩阵随路标数平方增长,几百个路标就跑不动了。回环修正困难。
- 图优化兴起(2006 起,g2o/Ceres):把 SLAM 显式建成图,用稀疏非线性最小二乘批量求解。Sparse Bundle Adjustment、g2o、Ceres 让大规模优化成为可能,但每次都重优化整张图,增量场景下浪费。
- 因子图 + 增量平滑(2008 起,iSAM/iSAM2/GTSAM):Dellaert 等人提出用因子图统一表达,并用 iSAM2 的贝叶斯树做增量更新——新因子只触发局部重计算。GTSAM 成为多传感器融合的事实标准库,LIO-SAM、LVI-SAM 都建立其上。
- 滑窗优化(VINS-Mono、OKVIS、DSO):在视觉-惯性里,为了控制计算量,只保留固定数量的近期关键帧(滑窗),把滑出窗口的帧边缘化成先验因子(Schur 补)。这是"图优化"和"滤波"思想的融合——窗口内是图优化,窗口边界用边缘化。
理论:因子图的三要素与最大后验¶
因子图把状态估计写成一个二部图(bipartite graph):一类节点是**变量**(variable),一类节点是**因子**(factor),因子和它约束的变量之间连边。优化目标是最大后验估计:
其中 \(\mathbf{r}_i\) 是第 \(i\) 个因子的残差,\(\boldsymbol{\Sigma}_i\) 是该测量的协方差,\(\boldsymbol{\Sigma}_i^{-1}\) 是**信息矩阵**(information matrix,权重)。三要素对应如下:
| 因子图要素 | SLAM 中的对象 | 数学含义 |
|---|---|---|
| 变量节点 | 关键帧位姿、速度、IMU 零偏、路标点、外参、\(t_d\) | 待优化的未知量 \(\mathbf{X}\) |
| 因子节点 | IMU 预积分、LiDAR 配准、视觉重投影、GPS、回环、先验 | 一个带权残差块 \(\mathbf{r}_i^\top \boldsymbol{\Sigma}_i^{-1} \mathbf{r}_i\) |
| 边 | 因子约束了哪些变量 | 残差对哪些变量求导非零(雅可比稀疏结构) |
多视角理解:可以从三个角度看同一张因子图。概率角度:因子图是联合后验概率 \(p(\mathbf{X}|\mathbf{Z})\) 的因子分解,每个因子是一个似然项,求 MAP 就是求联合概率最大。优化角度:它是一个加权非线性最小二乘问题,每个因子贡献一个残差平方项。线性代数角度:每次迭代要解一个稀疏线性系统 \(\mathbf{H}\,\delta\mathbf{X} = -\mathbf{b}\),其中 \(\mathbf{H} = \sum_i \mathbf{J}_i^\top \boldsymbol{\Sigma}_i^{-1} \mathbf{J}_i\) 是信息矩阵(海森),它的稀疏结构正是因子图的连接结构。三个角度互相印证:图越稀疏(因子连的变量越少),\(\mathbf{H}\) 越稀疏,求解越快。
IMU 预积分因子为什么连五个变量? 回顾前置自测第 2 题。IMU 在两个关键帧 \(i, j\) 之间的预积分量约束的不是单纯的相对位姿,而是相对位姿 + 速度变化 + 零偏。因为 IMU 测的是加速度(积分两次到位置、一次到速度)和角速度,且测量里含零偏。所以这个因子的残差同时依赖 \(\{\mathbf{p}_i, \mathbf{v}_i, \mathbf{R}_i\) 的零偏\(, \mathbf{p}_j, \mathbf{v}_j\}\)——它把两个关键帧的位姿、速度、零偏五类变量绑在一起。这正是因子"连多个变量"的典型例子。
理论:滤波器 vs 图优化的系统性对比¶
这是本章求解器维度的核心二分,务必吃透。把两者放在同一张表里逐维对比:
| 对比维度 | 滤波器(ESKF/MSCKF) | 图优化(GTSAM/Ceres) |
|---|---|---|
| 历史处理 | 边缘化,只留当前状态 | 保留历史因子 |
| 重线性化 | 不能(线性化点一旦定下就固定) | 能(每次迭代在新点线性化) |
| 回环修正 | 困难(历史变量已消失) | 自然(加一条因子边即可) |
| 计算复杂度 | \(O(状态维^3)\),与时间无关 | 随因子数增长(iSAM2 增量缓解) |
| 内存 | \(O(1)\)(只存状态+协方差) | \(O(n)\)(存所有因子/关键帧) |
| 延迟 | 低(实时性好) | 较高(批量优化耗时) |
| 典型代表 | FAST-LIO2、R3LIVE、Point-LIO | LIO-SAM、ORB-SLAM3、VINS(滑窗) |
| 适用场景 | 高频里程计、算力受限、小场景 | 大场景、需回环、离线高精建图 |
对比性思维:不要把"滤波 vs 图优化"理解成"差 vs 好"。它们是**速度-精度-灵活性的不同折中点**。无人机上算力紧、要 200Hz 里程计,滤波器是对的;建一张要回环的大型园区地图,图优化是对的。更现代的系统往往两者并用:用滤波器跑高频前端里程计,用因子图跑低频后端优化和回环(LVI-SAM 就是这个思路——见 §39.6)。这也呼应了 §39.2 的松/紧耦合二分:松/紧是"融合多紧",滤波/图优化是"用什么解",两个维度正交,组合出四个象限。
实现:用 GTSAM 搭一个最小多传感器因子图¶
GTSAM 是多传感器因子图的事实标准库。下面用它的 API 展示"异构约束进同一张图"的骨架。
Step 1:为什么 GTSAM 用符号键(Symbol)而不是整数下标管理变量
// GTSAM 用带类型前缀的符号键区分不同类型的变量,避免下标混淆
#include <gtsam/inference/Symbol.h>
using gtsam::symbol_shorthand::X; // X(i): 第 i 帧的位姿 Pose3
using gtsam::symbol_shorthand::V; // V(i): 第 i 帧的速度 Vector3
using gtsam::symbol_shorthand::B; // B(i): 第 i 帧的 IMU 零偏 imuBias
// 为什么需要前缀?因为位姿、速度、零偏都用整数 i 索引,
// 若只用裸整数会撞键(X(0) 和 V(0) 都是 0)。前缀让 X(0)、V(0)、B(0) 互不冲突。
Step 2:正确写法——构建图、加异构因子、用 iSAM2 增量优化
#include <gtsam/nonlinear/NonlinearFactorGraph.h>
#include <gtsam/nonlinear/ISAM2.h>
#include <gtsam/navigation/ImuFactor.h>
#include <gtsam/slam/PriorFactor.h>
#include <gtsam/geometry/Pose3.h>
gtsam::ISAM2 isam(gtsam::ISAM2Params()); // 增量求解器
gtsam::NonlinearFactorGraph graph; // 本次要新增的因子
gtsam::Values initial; // 新变量的初值
// ① 第 0 帧:加先验因子锚定第一个位姿(否则整张图可任意平移旋转,零空间)
auto prior_noise = gtsam::noiseModel::Diagonal::Sigmas(
(gtsam::Vector(6) << 1e-2,1e-2,1e-2, 1e-1,1e-1,1e-1).finished()); // [rot;trans] sigma
graph.add(gtsam::PriorFactor<gtsam::Pose3>(X(0), first_pose, prior_noise));
initial.insert(X(0), first_pose);
// ② IMU 预积分因子:连接第 i 和 i+1 帧的位姿+速度+零偏(五个变量)
// preint 是在两帧间预积分好的 IMU 量(GTSAM 的 PreintegratedImuMeasurements)
graph.add(gtsam::ImuFactor(X(i), V(i), X(i+1), V(i+1), B(i), preint));
// ③ LiDAR 帧到地图的位姿约束:表达成一个 BetweenFactor 或自定义一元因子
// 这里用相对位姿因子,把 LiDAR 配准结果作为帧间约束
auto lidar_noise = gtsam::noiseModel::Diagonal::Sigmas(
(gtsam::Vector(6) << 5e-3,5e-3,5e-3, 1e-2,1e-2,1e-2).finished());
graph.add(gtsam::BetweenFactor<gtsam::Pose3>(X(i), X(i+1), lidar_relative_pose, lidar_noise));
// ④ GPS 绝对位置因子(异构!只约束平移,3 维)——一元因子直接钉住某帧的全局位置
auto gps_noise = gtsam::noiseModel::Diagonal::Sigmas(gtsam::Vector3(1.0,1.0,2.0));
graph.add(gtsam::GPSFactor(X(i+1), gps_xyz, gps_noise));
// ⑤ 增量优化:iSAM2 只重算受新因子影响的变量,不重优化整张图
isam.update(graph, initial);
gtsam::Values result = isam.calculateEstimate(); // 取当前最优估计
graph.resize(0); initial.clear(); // 清空,准备下一轮增量
Step 3:错误写法及其后果
// ❌ 错误 1:忘记给第一个位姿加先验(或锚定)
// graph 里只有相对约束(BetweenFactor、ImuFactor),没有任何绝对约束
// 后果:整张图有 6 维零空间(gauge freedom)——可以整体平移旋转而残差不变。
// 信息矩阵 H 奇异(不满秩),Cholesky 分解失败,优化报 IndeterminantLinearSystemException。
// 必须有至少一个先验因子或固定一个变量来消除规范自由度。
// ❌ 错误 2:把 GPS 当成 6 维位姿因子(GPS 只测位置不测姿态)
graph.add(gtsam::BetweenFactor<gtsam::Pose3>(...gps当成完整位姿...));
// 后果:GPS 根本不提供姿态信息,硬给它一个姿态等于注入了凭空捏造的约束,
// 会把姿态估计拉向 GPS 那个假姿态。GPS 应该用只约束平移的因子(GPSFactor / 3维一元因子)。
// ❌ 错误 3:每次都重新构建整张图并全量优化,不用增量
LevenbergMarquardtOptimizer opt(full_graph_from_scratch, all_values); // 每帧都全量重优化
// 后果:复杂度随轨迹长度线性甚至超线性增长,几分钟后单次优化就超过一帧的时间预算,
// 实时性崩溃。应该用 ISAM2::update 做增量更新,只重算受影响的子图。
Step 4:因子图融合的数据流全景
对齐好的测量(来自 §39.3) 因子图(本节) 输出
─────────────────────── ────────────── ──────
IMU 序列 ──预积分──→ ImuFactor ──┐
LiDAR 帧 ──配准───→ BetweenFactor ├──→ NonlinearFactorGraph ──→ ISAM2.update
相机帧 ──重投影──→ ProjectionFactor│ ↑ 共享变量节点 ↓
GPS ──────────→ GPSFactor ──┤ X(i),V(i),B(i),路标点 最优 Values
回环 ──────────→ BetweenFactor ──┘ (位姿+地图)
特点:所有异构约束都"翻译"成因子,连到共享变量上,统一优化。
加一种新传感器 = 加一类新因子,不改变量结构——这就是因子图的可扩展性。
实现:Ceres 滑窗优化与边缘化(VINS-Mono 思路)¶
GTSAM 的 iSAM2 是"保留全部历史 + 增量",另一条主流路线是"只保留滑窗 + 边缘化老帧",VINS-Mono、OKVIS 走这条。核心是当一帧滑出窗口时,不能直接删——它携带的信息要通过**边缘化(Schur 补)**变成一个先验因子保留下来。
// 滑窗优化的核心循环(Ceres 风格伪代码)
void slide_window_optimize() {
ceres::Problem problem;
// ① 窗口内所有关键帧的位姿/速度/零偏作为参数块
for (int i = 0; i < WINDOW_SIZE; ++i)
problem.AddParameterBlock(para_pose[i], 7, new PoseLocalParameterization());
// 注意:位姿用 7 维(四元数 4 + 平移 3)存储,但流形上只有 6 自由度,
// 必须用 LocalParameterization 告诉 Ceres 在 SE(3) 流形上更新(⊞),
// 否则四元数会被当成无约束的 4 个数优化,失去单位模长。
// ② 加各类残差因子(IMU 预积分、视觉重投影),都配 Huber 鲁棒核压外点
for (auto& imu : imu_factors)
problem.AddResidualBlock(imu_cost, nullptr, para_pose[i], para_speedbias[i],
para_pose[i+1], para_speedbias[i+1]);
for (auto& feat : visual_factors)
problem.AddResidualBlock(reproj_cost, new ceres::HuberLoss(1.0),
para_pose[i], para_pose[j], para_feature[k]);
// ③ 上一轮边缘化留下的先验因子(这是滑窗与纯局部优化的关键区别)
if (last_marginalization_info)
problem.AddResidualBlock(marginalization_factor, nullptr, last_marg_param_blocks);
ceres::Solve(options, &problem, &summary);
// ④ 边缘化最老帧:用 Schur 补把它的信息压进先验,而不是直接丢弃
marginalize_oldest_frame(); // 维护 last_marginalization_info 供下一轮使用
}
为什么边缘化不能简单地"删掉老帧"? 老帧虽然滑出了窗口,但它和窗口内帧之间存在约束(IMU、共视路标)。直接删除等于扔掉这些约束携带的信息,系统会丢失尺度/姿态的长期一致性,产生漂移。边缘化通过 Schur 补把"被删变量"对"保留变量"的约束转化成一个等价的先验因子(一个高斯先验),信息得以保留。代价是这个先验**固定了被边缘化时的线性化点**——这正是滑窗法相对全图优化损失的那部分重线性化能力,也是它介于纯滤波和全图优化之间的本质。
理论-工程桥接:边缘化(marginalization)和滤波器的"预测+更新压缩历史"在数学上是同一个操作——都是对联合高斯分布做条件化/边缘化,把不再显式保留的变量的信息折叠进一个先验或协方差。所以滑窗优化常被称为"图优化形式的滤波器":它在窗口内享受图优化的重线性化,在窗口边界做滤波器式的边缘化。理解了这一点,滤波器和图优化就不是两个割裂的世界,而是同一个 MAP 问题在"保留多少历史"这个轴上的不同取值。
⚠️ 常见陷阱¶
陷阱 1(编程陷阱):因子图缺少先验/锚点导致信息矩阵奇异
- 错误描述:图里只有相对约束(IMU、帧间配准、回环),没有任何绝对约束或固定变量。
- 现象/后果:优化抛出 IndeterminantLinearSystemException(GTSAM)或 Ceres 求解发散/结果整体漂移;线性系统 \(\mathbf{H}\delta\mathbf{X}=-\mathbf{b}\) 中 \(\mathbf{H}\) 奇异,Cholesky 失败。
- 根本原因:纯相对约束保留了 6 维规范自由度(gauge freedom)——整张图可以任意平移旋转而所有相对残差不变。信息矩阵在这 6 个方向上是零特征值,不可逆。
- 正确做法:至少加一个先验因子锚定第一帧(PriorFactor),或显式固定一个变量(Ceres 的 SetParameterBlockConstant)。在视觉惯性里,重力方向 + 第一帧位姿提供了消除规范自由度的锚定。
陷阱 2(编程陷阱):位姿参数化没用 LocalParameterization / 流形更新
- 错误描述:把位姿的四元数当成 4 个无约束的数直接交给优化器更新。
- 现象/后果:四元数模长偏离 1,旋转被扭曲;优化收敛慢或不收敛;雅可比维度和流形自由度不匹配(4 维更新 vs 3 维旋转自由度)。
- 根本原因:\(SO(3)/SE(3)\) 是流形,过参数化的存储(四元数 4 维存 3 维旋转)需要告诉优化器"在流形上怎么加增量"(⊞ 运算)。不指定 LocalParameterization,优化器会在过参数化的欧氏空间里乱走。
- 正确做法:GTSAM 的 Pose3 类型内置流形更新,自动正确;Ceres 必须显式给位姿参数块配 LocalParameterization(新版叫 Manifold),实现 SE(3) 上的 ⊞。这与 §39.2 的 boxplus、§39.3 的 SLERP 是同一族流形操作。
陷阱 3(概念误区):以为因子图一定比滤波器精度高 - 错误描述:见到因子图就认为精度更高,把高频 LIO 的滤波器无脑换成因子图。 - 现象/后果:换成因子图后实时性崩溃(单次优化超过一帧预算),或在小场景无回环时精度并无提升却付出了大得多的计算/内存开销。 - 根本原因:因子图的优势是"重线性化 + 回环修正 + 全局一致",这些优势只在**大场景、有回环、需要全局一致**时才兑现。小场景、高频里程计、无回环时,滤波器的精度足够且快得多。精度高低取决于问题特性,不取决于求解器名字。 - 正确做法:按场景选求解器。高频前端里程计用滤波器,大场景后端 + 回环用因子图,两者可分层并用(LVI-SAM 即如此)。这与 §39.2 陷阱 1"松/紧耦合不等于精度高低"是同一类思维错误——不要给工具贴"高级/低级"标签。
陷阱 4(思维陷阱):边缘化时不固定线性化点,反复重线性化被边缘化的变量 - 错误描述:在滑窗里边缘化老帧后,又在后续优化中改变了边缘化先验所依赖的线性化点。 - 现象/后果:信息矩阵出现不一致(FEJ, First-Estimate Jacobian 问题),系统人为获得了不该有的"虚假信息",可观测性被破坏,估计出现 inconsistency(协方差过度自信)。 - 根本原因:边缘化先验是在某个线性化点处计算的 Schur 补。如果之后又改变这些变量的线性化点,先验的雅可比和当前雅可比就不一致,在零空间方向注入了虚假信息,破坏了系统的可观测性(典型表现为偏航角 yaw 被错误地变成可观)。 - 正确做法:用 First-Estimate Jacobian(FEJ)——对被边缘化相关的变量,始终用它们第一次被估计时的线性化点计算雅可比,保证边缘化前后信息一致。VINS-Mono、OKVIS 都实现了 FEJ。理解这一点需要回到可观测性分析(§39.5)。
练习¶
-
(分析题) 给你一个 LIO-SAM 的因子图截图,上面有位姿节点、IMU 预积分因子、回环因子、GPS 因子。请说明:(a) 如果去掉所有 GPS 因子,整张图还能不能确定全局位置?为什么?(b) 如果某一段轨迹只有 IMU 因子没有 LiDAR 配准因子(比如 LiDAR 临时失效),这段轨迹的不确定度会怎样变化?(c) 回环因子加入后,误差是如何"传播"到中间帧的?
-
(实现题) 用 GTSAM 写一个最小可运行的例子:3 个位姿节点,节点间用
BetweenFactor连相对位姿约束,第 0 帧加先验,然后用ISAM2增量优化。故意让相对约束之间有矛盾(比如 0→1→2 的累积和 0→2 的直接约束不一致),观察优化器如何在矛盾中折中。(提示:矛盾会被按信息矩阵加权分摊。) -
(设计扩展题) 你要给一个滑窗 VIO 加 LiDAR。LiDAR 帧率 10Hz、相机 30Hz、IMU 200Hz,三者帧率不同。请设计:(a) 滑窗里的关键帧应该按什么频率选取?(b) LiDAR 的配准约束如何加进滑窗(作为帧间因子还是帧到地图因子)?(c) 边缘化时 LiDAR 地图点和视觉路标点的处理是否应该不同?为什么 LiDAR 地图点通常不作为待优化变量而视觉路标点是?
§39.5 退化检测与切换——让系统知道自己"什么时候瞎了" ⭐⭐⭐⭐¶
一句话总结:多传感器融合的鲁棒性不在于"传感器多",而在于系统能**实时知道哪个传感器在当前时刻、哪个方向上失效**,并据此调整对它的信任度。退化检测的核心工具是对优化问题的 Hessian(信息矩阵)做特征值分解——某个方向特征值塌缩,就意味着这个方向"几何上不可观";检测到之后在里程计级(增大协方差)和后端级(约束降权)做出降级响应。
动机:回到 §39.1 的反面教材,给它一个解药¶
回顾 §39.1 那个"传感器最多反而崩得最彻底"的反例:进隧道时 LiDAR 在前进方向不可观,但代码照样信任配准的全部六个自由度。我们当时说这是病根,现在来开药方。
问题的本质是:估计器默认所有观测在所有方向上都同样可信,但现实中观测的可信度是有方向性的、随时间变化的。长直隧道里,LiDAR 配准在垂直于隧道的方向(左右、上下)依然可观(墙壁约束了),但在沿隧道方向(前进)不可观——这个方向上点云怎么平移残差都一样。如果估计器不知道这件事,它会在前进方向上接受配准给出的任意值(通常是上一帧的惯性,或者数值噪声),把垃圾当真值。
退化检测就是要让系统**自己算出"我现在哪个方向不可观"**,然后对那个方向的观测降权或干脆不信,转而依赖能观测那个方向的其他传感器(IMU 的惯性、轮速计、视觉特征)。
本质洞察:退化(degeneracy)不是"传感器坏了",而是"环境让某个观测在某些方向上失去了约束能力"。激光雷达硬件完好,但隧道环境让它的几何约束在前进方向上退化。所以退化是**环境 × 传感器**的联合属性,必须在线检测,无法离线预判——同一个 LiDAR 在特征丰富的房间里全方向可观,进了隧道就在一个方向上退化。
反面:用"残差大小"判断观测好坏的误区¶
一个常见的错误直觉是:用残差大小判断观测是否可信——残差大就不信。但退化场景恰恰相反:退化方向上的残差往往很小!
设想长直隧道,LiDAR 配准在前进方向退化。你把当前帧沿隧道方向平移 1 米,再做点到面残差——因为隧道处处一样,平移后每个点依然能找到很近的对应面,残差几乎不变、依然很小。也就是说,"残差小"在退化方向上不代表"估计准",而代表"这个方向上怎么取值残差都小(即没有约束)"。
非退化方向(垂直隧道): 退化方向(沿隧道):
残差 残差
│ ╱ │
│ ╱ ← 偏离真值残差迅速变大 │ ───────────── ← 怎么平移残差都平坦
│ ╱ │ (没有约束力)
└──┴──→ 平移 └────────────────→ 平移
真值 任意值残差都一样小
结论:用"残差小"判断"估计准"在退化方向上完全失效。
必须看"残差对状态变化的敏感度"——也就是 Hessian/曲率。
这就引出了正确的判据:不看残差的**大小**,看残差对状态变化的**敏感度**(曲率)。敏感度高(残差随状态变化剧烈)= 可观;敏感度低(残差对状态变化平坦)= 退化。而这个"敏感度",数学上就是优化问题的 Hessian(信息矩阵)。
历史:从经验阈值到 Hessian 特征值分析¶
- 早期:经验启发式。 用环境特征数量、点云分布范围等启发式指标判断退化(比如"提取到的平面/线特征太少就认为退化")。简单但不精确——没有告诉你"哪个方向"退化。
- 里程碑:Zhang 等人的退化因子(2016, "On Degeneracy of Optimization-based State Estimation Problems")。 提出对优化问题的信息矩阵做特征值分解,用最小特征值或特征值比值作为退化因子,并提出"解重映射(solution remapping)"——只在可观的特征向量方向上更新状态,退化方向保持先验。这是退化检测的理论基石。
- 现代:融入主流 LIO/LVIO。 LeGO-LOAM、LIO-SAM 等用特征值阈值做退化判断;更精细的系统把特征值分析用于方向性的协方差调整,而非简单的"退化/不退化"二值判断。
理论:可观测性与 Hessian 特征值的对应¶
把退化检测建立在严格的数学上。考虑一个最小二乘问题(无论来自滤波器更新还是因子图优化),在当前估计点线性化后,要解:
\(\mathbf{H}\) 是 Hessian(也是信息矩阵),\(\mathbf{J}\) 是残差对状态的雅可比。对 \(\mathbf{H}\) 做特征值分解:
每个特征向量 \(\mathbf{v}_k\) 是状态空间里的一个方向,对应特征值 \(\lambda_k\) 是"残差在这个方向上的曲率"——也就是这个方向被观测约束的强度。物理意义如下:
| 特征值 \(\lambda_k\) | 含义 | 物理对应 |
|---|---|---|
| \(\lambda_k\) 大 | 该方向曲率大、强约束 | 可观方向(如隧道左右) |
| \(\lambda_k \to 0\) | 该方向曲率塌缩、无约束 | 不可观/退化方向(如隧道前进) |
| \(\lambda_k = 0\) | 完全不可观 | 零空间(如缺锚点的全局位姿) |
多视角理解:同一个最小特征值塌缩,可从三个角度看。几何角度:退化方向上残差曲面是平的,沿这个方向走残差不增,所以约束不了。信息角度:\(\mathbf{H}\) 是信息矩阵,特征值是该方向上获得的信息量,\(\lambda_k \to 0\) 意味着这个方向上几乎没拿到信息。协方差角度:协方差 \(\boldsymbol{\Sigma} = \mathbf{H}^{-1}\),所以 \(\lambda_k \to 0\) 对应协方差在该方向的特征值 \(\to \infty\)——不确定度爆炸。三个角度说的是同一件事:退化方向上"约束弱、信息少、不确定度大"。
退化因子(degeneracy factor)的定义。Zhang 提出用归一化的最小特征值或特征值比作为退化指标。一种常见形式:
比值接近 1 表示各方向约束均衡(健康),比值趋于 0 或 \(\lambda_{\min}\) 低于阈值表示存在退化方向。注意阈值 \(\tau\) 与传感器、特征提取方式强相关,需要按系统标定。
理论:解重映射——只在可观方向更新¶
检测到退化后,最经典的响应是 Zhang 的**解重映射(solution remapping)**:把更新量 \(\delta\mathbf{x}\) 投影到可观子空间,退化方向上不更新(保持预测/先验值)。具体做法是构造一个投影矩阵:
其中 \(\mathbf{V}_{\text{obs}}\) 是由可观特征向量(\(\lambda_k > \tau\) 的那些 \(\mathbf{v}_k\))张成的子空间。这样退化方向上的更新被置零——不让不可观方向的垃圾值污染状态,而是依赖预测(IMU 惯性)在那个方向上"惯性滑行",等其他传感器或后续帧补上约束。
解重映射的几何直觉:
原始更新 δx(含退化方向的垃圾分量)
│
├──→ 投影到可观子空间 V_obs
│
▼
δx_remap:只保留可观方向的更新,退化方向清零
退化方向交给 IMU 预测/先验"接管"
类比:像在浓雾里开车,你只相信能看清的方向(可观)的修正,
看不清的方向(退化)暂时按惯性直行,不乱打方向盘。
——边界:类比成立在"短时间惯性可靠"上;若退化持续过久,
IMU 漂移会累积,惯性也不再可靠,必须靠换传感器解决。
理论:手把手算一遍——长直走廊里点到面 Hessian 长什么样¶
光说"特征值塌缩"太抽象。我们用一个最小化的例子,亲手推一遍走廊里的 Hessian,让你看清退化是怎么从几何"长"进矩阵里的。
考虑点到面 ICP 只估平移 \(\mathbf{t}=(t_x,t_y,t_z)\)(旋转固定,简化讨论)。第 \(i\) 个点的点到面残差是:
其中 \(\mathbf{n}_i\) 是对应平面的单位法向量。残差对平移的雅可比是 \(\frac{\partial r_i}{\partial \mathbf{t}} = \mathbf{n}_i^\top\)。于是平移部分的 Hessian(信息矩阵,假设各点权重为 1)是所有点法向量外积之和:
这个公式是理解几何退化的钥匙——Hessian 完全由观测到的平面法向量分布决定。现在分三种环境看它的特征值:
环境 A:特征丰富的房间(地面+左右墙+前后墙,法向量朝各个方向)
n 分布:覆盖 x,y,z 三个轴
H_t ≈ diag(大, 大, 大) → 三个特征值都大 → 三个平移方向全可观 ✓
环境 B:长直走廊(只有左右墙 + 地面,没有垂直于前进方向的面)
设走廊沿 x 轴,则左右墙法向沿 y,地面法向沿 z,没有法向沿 x 的面!
H_t = Σ n n^T ≈ [[0, 0, 0], → x 方向特征值 = 0(前进方向退化!)
[0, 大, 0], → y 方向可观(左右墙约束)
[0, 0, 大]] → z 方向可观(地面约束)
环境 C:开阔平地(只有地面,法向全沿 z)
H_t ≈ diag(0, 0, 大) → x,y 两个方向都退化,只有 z 可观
看环境 B:因为没有任何平面的法向量在前进方向(\(x\))上有分量,\(\mathbf{H}_t\) 在 \(x\) 方向的对角元就是 0,对应特征值为 0——这就是"前进方向不可观"的数学本体。它不是因为残差大,而是因为**没有任何观测对 \(t_x\) 的变化产生响应**(\(t_x\) 怎么变,所有 \(r_i\) 都不变,因为 \(\mathbf{n}_i^\top\) 在 \(x\) 分量为 0)。
本质洞察:退化的根源是"观测的雅可比在某方向上恒为零"。Hessian \(\mathbf{H}=\sum \mathbf{J}_i^\top \mathbf{J}_i\) 是雅可比的"能量累加",某方向上所有雅可比都没分量,这个方向的能量就是零,特征值塌缩。这把"几何上看不出运动"翻译成了"代数上信息矩阵奇异"——几何直觉和线性代数在这里完美对应。这也解释了 §39.4 的规范自由度(gauge freedom):缺锚点时整张图能整体平移,等价于所有相对约束的雅可比对"整体平移"这个方向恒为零,同样导致 \(\mathbf{H}\) 奇异。退化和规范自由度本质是同一回事——信息矩阵的零空间。
这个例子如何指导工程:检测时,对完整的 6×6 Hessian(含旋转)做特征值分解,看最小特征值对应的特征向量——在环境 B 里它会指向前进方向 \(x\)。响应时,解重映射把 \(\delta\mathbf{x}\) 在 \(x\) 方向的分量清零,让 IMU 预测接管前进方向。这正是下面代码要实现的。
实现:Hessian 特征值退化检测的 C++ 骨架¶
Step 1:为什么在每次配准/更新时都要算特征值
// 退化是环境×传感器的实时属性,必须每帧检测,不能离线预判
// 在 ICP/点到面配准收敛后,我们手里正好有 H = J^T J(高斯牛顿的近似 Hessian)
// 复用它做特征值分解,几乎不增加额外计算(6×6 矩阵分解极快)
Step 2:正确写法——特征值分解 + 解重映射
#include <Eigen/Eigenvalues>
// 输入:配准/更新得到的 6x6 Hessian H 和更新量 dx;输出:重映射后的 dx
Eigen::Matrix<double,6,1> remap_solution(const Eigen::Matrix<double,6,6>& H,
const Eigen::Matrix<double,6,1>& dx,
double lambda_threshold) {
// 对称矩阵用 SelfAdjointEigenSolver(数值更稳、特征值实数有序)
Eigen::SelfAdjointEigenSolver<Eigen::Matrix<double,6,6>> es(H);
Eigen::Matrix<double,6,1> eigvals = es.eigenvalues(); // 升序
Eigen::Matrix<double,6,6> eigvecs = es.eigenvectors(); // 列是特征向量
// 构造可观子空间投影:特征值低于阈值的方向判为退化,清零其更新分量
Eigen::Matrix<double,6,6> P = Eigen::Matrix<double,6,6>::Zero();
for (int k = 0; k < 6; ++k) {
if (eigvals(k) > lambda_threshold) { // 可观方向
P += eigvecs.col(k) * eigvecs.col(k).transpose(); // 累加投影算子 v v^T
}
// 否则退化方向:不加入投影,等价于该方向更新被置零
}
return P * dx; // 只在可观方向更新;退化方向交给 IMU 预测接管
}
Step 3:错误写法及其后果
// ❌ 错误 1:用普通 EigenSolver 而非 SelfAdjointEigenSolver 分解对称矩阵
Eigen::EigenSolver<Eigen::Matrix<double,6,6>> es(H); // 通用求解器
auto vals = es.eigenvalues(); // 返回复数!即使 H 对称
// 后果:通用 EigenSolver 返回复数特征值(虚部理论为 0 但有数值噪声),
// 且不保证排序,处理起来麻烦还可能因虚部判断出错。
// 对称矩阵必须用 SelfAdjointEigenSolver:实特征值、升序、数值稳定。
// ❌ 错误 2:用一个固定的绝对阈值,不随传感器/尺度调整
if (eigvals(0) < 100.0) { /* 判退化 */ } // 魔法数字 100,哪来的?
// 后果:特征值的量级取决于残差权重(信息矩阵 Σ^{-1})和雅可比尺度,
// 不同传感器、不同点数下量级天差地别。固定绝对阈值在 A 系统合适、
// 在 B 系统要么永远报退化要么永远不报。应该用相对阈值(λ_min/λ_max)
// 或按系统标定的、与残差权重匹配的阈值。
// ❌ 错误 3:检测到退化后什么都不做,只打印一条警告
if (degenerate) ROS_WARN("degenerate!"); // 然后照常用原始 dx 更新
// 后果:检测了等于没测——退化方向的垃圾值依然污染了状态。检测必须连着响应:
// 要么解重映射(清零退化方向更新),要么增大该方向协方差让后端降权。
Step 4:退化检测在系统里的位置——双层响应
配准/更新得到 H, dx
│
┌──────┴──────┐
│ 特征值分解 │ ← 本节核心
└──────┬──────┘
│ 哪些方向退化?
┌───────┴────────┐
▼ ▼
里程计级响应 后端级响应
─────────── ───────────
解重映射,或 把退化方向的约束
增大该方向协方差 在因子图里降权
(前端立刻不信) (ISAM2 优化时少信)
│ │
└───→ 退化方向依赖其他传感器(IMU/轮速/视觉)补约束
理论:双层降级响应的分工¶
检测到退化后,响应发生在两个层次,分工明确:
| 响应层次 | 在哪做 | 怎么做 | 效果 |
|---|---|---|---|
| 里程计级(前端) | ESKF 更新 / ICP 配准处 | 解重映射,或在退化方向增大观测协方差 \(R\) | 立刻阻止退化方向的垃圾值进入状态 |
| 后端级(因子图) | 因子的信息矩阵 | 把退化方向对应的因子权重调低(信息矩阵在该方向变小) | 全局优化时少信这个约束,让其他因子主导 |
理论-工程桥接:里程计级响应是"硬切"——直接不让退化方向更新;后端级响应是"软调"——降低权重让优化器自己折中。两者配合:前端用解重映射保证高频里程计不被瞬间污染,后端用权重调整保证全局优化时退化约束不主导。这正好对应 §39.4 的滤波/图优化双层架构——前端滤波器快速响应,后端因子图全局协调。LVI-SAM 的"一个子系统失效时另一个独立运行"(§39.6)是这个思想的极端形式:直接整体切换。
⚠️ 常见陷阱¶
陷阱 1(编程陷阱):对称 Hessian 用通用特征值求解器,得到复数特征值
- 错误描述:用 Eigen::EigenSolver 而非 Eigen::SelfAdjointEigenSolver 分解对称的信息矩阵。
- 现象/后果:返回复数特征值(虚部本应为 0 但带数值噪声),不保证排序,后续判断退化方向时因虚部/排序出错。
- 根本原因:通用 EigenSolver 为非对称矩阵设计,不利用对称性,返回复数;信息矩阵 \(\mathbf{H}=\mathbf{J}^\top\boldsymbol{\Sigma}^{-1}\mathbf{J}\) 是对称半正定的。
- 正确做法:对称矩阵一律用 SelfAdjointEigenSolver——保证实特征值、升序排列、数值稳定,且更快。这是处理任何协方差/信息矩阵特征分解的默认选择。
陷阱 2(概念误区):用残差大小而非曲率判断退化 - 错误描述:以为残差大就是观测不好、残差小就是估计准,据此判断退化。 - 现象/后果:在退化方向上残差恰恰很小(怎么取值都小),被误判为"估计很准",垃圾值被当真值采纳。 - 根本原因:退化的本质是"残差对状态变化不敏感"(曲率塌缩),而非"残差大"。残差大小反映的是当前点的拟合好坏,曲率反映的是约束强度,两者是不同的东西。 - 正确做法:用 Hessian 特征值(曲率/信息量)判断退化,不用残差大小。残差大小另有用途(判断外点、判断是否收敛),但不能用来判断方向性退化。
陷阱 3(思维陷阱):检测到退化却不做方向性响应,只做全局降权或全局丢弃 - 错误描述:检测到退化后,把整个传感器的所有观测都降权或丢弃。 - 现象/后果:丢掉了退化方向之外那些**仍然可观、仍然有用**的信息。隧道里 LiDAR 在前进方向退化,但左右、上下方向依然可观——整体丢弃 LiDAR 等于把这些好约束也扔了,反而让系统在可观方向上失去约束。 - 根本原因:退化是**方向性**的,不是整个传感器失效。一个传感器可以在某些方向退化、同时在另一些方向健康。一刀切的全局响应忽略了这种方向性。 - 正确做法:做方向性响应——只对退化方向(退化特征向量张成的子空间)降权或清零更新,保留可观方向的约束。这正是解重映射 \(\mathbf{V}_{\text{obs}}\mathbf{V}_{\text{obs}}^\top\) 的意义:精确地只动退化方向。
练习¶
-
(推导题) 证明:对于点到面 ICP,当所有点的法向量都平行(比如只扫到一面墙)时,Hessian 在垂直于法向量的两个方向上特征值为 0。提示:点到面残差 \(r = \mathbf{n}^\top(\mathbf{R}\mathbf{p}+\mathbf{t}-\mathbf{q})\) 对平移 \(\mathbf{t}\) 的雅可比是 \(\mathbf{n}^\top\),所以平移部分的 Hessian 是 \(\sum \mathbf{n}\mathbf{n}^\top\)。当所有 \(\mathbf{n}\) 平行时这个矩阵的秩是多少?
-
(实现题) 实现一个退化监控器:输入每帧配准的 6×6 Hessian,输出 (a) 6 个特征值;(b) 退化因子 \(\lambda_{\min}/\lambda_{\max}\);(c) 退化方向对应的特征向量(在哪个物理方向退化——是平移退化还是旋转退化?如何从 6 维特征向量看出来)。在长直走廊数据上跑,观察前进方向对应的特征值是否随着走廊变长而塌缩。
-
(设计扩展题) 解重映射在退化方向上"不更新、靠 IMU 惯性接管"。但如果退化持续很久(一条 500 米的隧道),IMU 在前进方向上的积分漂移会累积成很大的误差。请设计一个补救方案:(a) 如何引入轮速计或视觉特征来约束前进方向?(b) 如果这些传感器也都不可用(纯几何退化 + 黑暗 + 打滑),系统应该如何"诚实地"报告自己在前进方向上的巨大不确定度,而不是假装知道走了多远?(提示:让协方差如实增长,下游模块据此决策。)
§39.6 典型系统架构精读——把所有维度串成三张活地图 ⭐⭐⭐⭐¶
一句话总结:前面五节给了我们四个分析维度——传感器互补(§39.1)、松/紧耦合(§39.2)、时空对齐(§39.3)、滤波/图优化(§39.4)、退化响应(§39.5)。本节用三个真实系统把这些维度全部串起来:LVI-SAM(紧耦合 + 因子图 + 双子系统故障切换)、R3LIVE(松耦合 + 滤波器 + 共享辐射地图)、FAST-LIVO2(紧耦合 + 滤波器 + 统一体素地图)。读懂它们的架构选择,就读懂了"为什么同样的传感器组合能长成完全不同的样子"。
为什么精读这三个系统:它们恰好占据三个不同象限¶
回顾 §39.2 和 §39.4 的两个正交维度(融合层次 × 求解器),它们组合出四个象限。这三个系统刻意选取,覆盖了其中三个、且各有独特的工程亮点:
滤波器 图优化
┌──────────────────────┬──────────────────────┐
松耦合 │ R3LIVE │ (较少见,松耦合多用 │
│ 双子系统+共享辐射地图 │ 滤波器以求快) │
├──────────────────────┼──────────────────────┤
紧耦合 │ FAST-LIVO2 │ LVI-SAM │
│ ESIKF顺序更新+体素图 │ 因子图+双子系统切换 │
└──────────────────────┴──────────────────────┘
三个系统都是 LiDAR + 相机 + IMU 的组合,但架构截然不同——
这正是本章的中心论点:架构是选择,不是必然。
精读的方法论:对每个系统,我们都回答同样五个问题——(1) 传感器怎么组合互补(§39.1);(2) 松耦合还是紧耦合(§39.2);(3) 怎么做时空对齐(§39.3);(4) 滤波还是图优化(§39.4);(5) 怎么处理退化/失效(§39.5)。用统一的五问框架对照着读,你会看到同样的问题被三种不同哲学回答。
系统一:LVI-SAM——紧耦合 + 因子图 + 双子系统故障切换¶
架构总览。LVI-SAM(Shan et al., ICRA 2021)由港科沈劭劼组的 LIO-SAM 作者 Tixiao Shan 主导,是一个**紧耦合的激光-视觉-惯性系统**。它的精妙之处在于把系统拆成两个相对独立又紧密协作的子系统:
LVI-SAM 顶层架构:
┌─────────────────────────────────────────────────────────┐
│ 视觉-惯性子系统 (VIS, 改自 VINS-Mono) │
│ 相机 + IMU → 视觉里程计;LiDAR 给视觉特征提供深度 │
└──────────────┬──────────────────────────────────────────┘
│ ① VIS 给 LIS 提供初始位姿 + 回环候选
│ ② LIS 给 VIS 提供特征深度 + 失效时接管
▼
┌─────────────────────────────────────────────────────────┐
│ 激光-惯性子系统 (LIS, 改自 LIO-SAM) │
│ LiDAR + IMU → 激光里程计;GTSAM 因子图后端 │
└─────────────────────────────────────────────────────────┘
两个子系统都建在 IMU 预积分之上,通过 GTSAM 因子图耦合,
且任一子系统失效时另一个可独立运行——这是它的"故障切换"。
五问框架逐条拆解:
| 五问 | LVI-SAM 的回答 |
|---|---|
| ① 传感器互补 | LiDAR 给视觉补深度/尺度;视觉给 LiDAR 补回环和退化时的约束;IMU 给两者补高频运动 |
| ② 松/紧耦合 | 紧耦合——视觉特征用 LiDAR 深度、IMU 预积分约束进同一因子图 |
| ③ 时空对齐 | 离线标定相机-LiDAR-IMU 外参;IMU 预积分做帧内运动补偿 |
| ④ 滤波/图优化 | 图优化——GTSAM 因子图 + iSAM2 增量优化做后端 |
| ⑤ 退化/失效 | 双子系统独立运行 + 故障切换——LiDAR 退化时靠 VIS,视觉失效(黑暗/无纹理)时靠 LIS |
架构亮点 1:LiDAR 给视觉特征补深度。VINS-Mono 是纯视觉惯性,视觉特征的深度要靠三角化估计(有尺度模糊和初始化难题)。LVI-SAM 让 LiDAR 点云投影到图像上,直接给视觉特征赋予**度量深度**——这一步消除了单目的尺度模糊,也加速了初始化。
// LVI-SAM 中"用 LiDAR 深度增强视觉特征"的思路(简化示意)
// 把 LiDAR 点云投影到图像平面,为落在特征点附近的视觉特征赋深度
float get_feature_depth(const cv::Point2f& feat_px,
const pcl::PointCloud<PointType>& lidar_in_cam) {
// ① 把 LiDAR 点投到归一化平面,建一个深度查找结构(如以方位角索引的KD树)
// ② 找落在 feat_px 邻域内的 LiDAR 点
// ③ 检查这些点深度是否一致(落在同一平面上才可信,跨越深度断层则丢弃)
// —— 关键:若邻域内 LiDAR 点深度差异大(特征点在物体边缘),深度不可信,
// 宁可不赋深度(退回三角化),也不要赋一个错误的深度。
return consistent_depth_or_invalid;
}
架构亮点 2:双向初始化与故障切换。VIS 和 LIS 互为备份:
- VIS → LIS:视觉里程计为激光里程计提供帧间初始位姿猜测(让 LiDAR 配准从一个好初值开始,加速收敛、避免局部最优)。
- LIS → VIS:当视觉子系统因快速运动/光照剧变/无纹理而失效时,激光子系统接管,并在视觉恢复后帮它重新初始化。
- 回环:VIS 做视觉回环检测(DBoW2 词袋),检测到的回环作为因子加进 GTSAM 因子图,全局修正。
本质洞察:LVI-SAM 的"双子系统"设计是 §39.5 退化响应的**架构级实现**。§39.5 讲的是"在一个估计器内部对退化方向降权",LVI-SAM 更进一步——直接准备两个能独立工作的子系统,当一个整体退化(LiDAR 在隧道全退化、相机在黑暗全失效)时切换到另一个。这是"模块独立性"(§39.2 松耦合的优点)和"紧耦合精度"的巧妙结合:子系统内部紧耦合求精度,子系统之间松散解耦求鲁棒。
架构亮点 3:为什么用因子图而非滤波器。LVI-SAM 要做大场景建图 + 回环修正(§39.4 讲过这是图优化的主场)。GTSAM 因子图让它能把激光里程计因子、视觉里程计因子、IMU 预积分因子、回环因子、GPS 因子全部统一进一张图,用 iSAM2 增量优化。回环检测到时,误差被均摊到整条轨迹——这是滤波器做不到的。
// LVI-SAM 后端因子图的异构因子(概念示意,对应 §39.4 的 GTSAM 骨架)
graph.add(ImuFactor(X(i),V(i),X(i+1),V(i+1),B(i), imu_preint)); // IMU 预积分
graph.add(BetweenFactor<Pose3>(X(i),X(i+1), lidar_odom, lidar_noise));// 激光里程计
graph.add(BetweenFactor<Pose3>(X(i),X(i+1), visual_odom, vis_noise)); // 视觉里程计
graph.add(BetweenFactor<Pose3>(X(loop_i),X(loop_j), loop_pose, loop_noise)); // 回环
// 五种约束、同一张图、iSAM2 增量优化——这就是因子图融合的威力
系统二:R3LIVE——松耦合 + 滤波器 + 共享辐射地图¶
架构总览。R3LIVE(Lin & Zhang, 港大 MARS Lab, 2021)是一个**松耦合的 RGB 辐射地图 LIVO 系统**,名字里 R3 = Robust, Real-time, RGB-colored。它的独特定位是:不只是定位,而是实时重建一张**带颜色(辐射率)的稠密点云地图**。架构上它由两个 ESIKF 子系统组成:
R3LIVE 顶层架构(松耦合双子系统 + 共享状态/地图):
┌──────────────────────────┐ ┌──────────────────────────┐
│ LIO 子系统 (ESIKF) │ │ VIO 子系统 (ESIKF) │
│ LiDAR + IMU │ │ 相机 + IMU │
│ → 估计几何位姿 │ │ → 估计位姿 + 给地图上色 │
│ → 构建几何地图结构 │ │ │
└─────────────┬────────────┘ └────────────┬──────────────┘
│ │
▼ 共享同一个状态 + 同一张地图 ▼
┌────────────────────────────────────────────────┐
│ 全局辐射地图(带 RGB 的稠密点云) │
│ LIO 负责几何(点的位置),VIO 负责辐射(点的颜色)│
└────────────────────────────────────────────────┘
两个子系统各自是完整的 ESIKF,共享状态和地图——这是松耦合。
五问框架逐条拆解:
| 五问 | R3LIVE 的回答 |
|---|---|
| ① 传感器互补 | LiDAR 建几何骨架;相机给点云上色 + 在几何退化时补约束;IMU 高频传播 |
| ② 松/紧耦合 | 松耦合——LIO 和 VIO 是两个独立的 ESIKF 估计器,通过共享状态/地图协作 |
| ③ 时空对齐 | 离线标定外参;各子系统内部用 IMU 传播对齐 |
| ④ 滤波/图优化 | 滤波器——两个子系统都是 ESIKF(误差状态迭代卡尔曼) |
| ⑤ 退化/失效 | LiDAR 退化时 VIO 的光度约束补位;VIO 用 frame-to-map 光度对齐避免漂移 |
架构亮点 1:VIO 是"光度 frame-to-map"而非"特征 frame-to-frame"。这是 R3LIVE 区别于 VINS 类系统的核心。传统视觉惯性提取特征点、做特征匹配和重投影。R3LIVE 的 VIO 直接用**光度误差**——把全局辐射地图里的带色点投影到当前帧,比较投影点的预测颜色和图像实测颜色,最小化这个光度残差来估计位姿。
// R3LIVE VIO 光度残差的思路(frame-to-map photometric,概念示意)
// 对地图中每个带 RGB 的点 p_world(颜色记为 c_map):
double photometric_residual(const RadiancePoint& p, const StateGroup& state,
const cv::Mat& cur_img) {
Eigen::Vector3d p_cam = state.R_cam_world * p.xyz + state.t_cam_world; // 变到相机系
Eigen::Vector2d uv = project(p_cam, camera_intrinsics); // 投影到像素
if (!in_image(uv)) return 0; // 视野外跳过
double c_observed = bilinear_sample(cur_img, uv); // 当前图像在该像素的实测亮度/色
return c_observed - p.radiance; // 光度残差 = 实测 - 地图记录
// ESIKF 用这个残差更新状态。注意:这要求地图点的颜色(辐射率)已被准确估计,
// 所以 VIO 同时还在更新地图点的颜色——估位姿和估辐射是耦合进行的。
}
为什么用 frame-to-map 而不是 frame-to-frame?frame-to-frame 会累积漂移(每帧相对上一帧,误差逐帧累加),而 frame-to-map 直接对齐到全局地图,不累积帧间漂移。代价是地图必须足够准、且要维护地图点的辐射率。R3LIVE 实际用了两步:先 frame-to-frame 光流给个初值,再 frame-to-map 光度精化。
架构亮点 2:几何与辐射分工——为什么是松耦合。R3LIVE 刻意让 LIO 和 VIO 各管一摊:LIO 用 LiDAR 的精确测距构建**几何**(点在哪),VIO 用相机的颜色信息估计**辐射**(点什么色)并辅助定位。两个 ESIKF 共享状态,但各自处理自己的原始测量——LiDAR 点云不进 VIO 的滤波器,图像像素不进 LIO 的滤波器。这正是 §39.2 松耦合的定义:各传感器在各自的估计器里处理,通过共享状态交换信息。
对比性思维:R3LIVE 和 FAST-LIVO2 用的传感器完全一样(LiDAR+相机+IMU),都用 ESIKF(滤波器),但一个松耦合(R3LIVE:两个 ESIKF)、一个紧耦合(FAST-LIVO2:一个 ESIKF 顺序更新)。差异的根源在目标:R3LIVE 追求**实时 RGB 稠密建图**,把几何和辐射分开管理更自然(LIO 管几何、VIO 管颜色,职责清晰);FAST-LIVO2 追求**极致的里程计精度和效率**,把所有原始测量塞进一个 ESIKF 信息利用最充分。同样的料,不同的菜——因为食客要的不一样。
架构亮点 3:共享状态的工程实现。R3LIVE 的两个子系统跑在不同线程,共享状态结构。这就回到了 §39.2 的松耦合 C++ 陷阱——共享状态的数据竞争和持锁过久。R3LIVE 用互斥保护状态访问,临界区只做状态拷贝,把耗时的光度优化放在锁外(正是 §39.2 我们强调的"锁里只拷贝、锁外慢慢算")。
// R3LIVE 风格的共享状态访问(呼应 §39.2 松耦合陷阱的正确做法)
StateGroup snapshot;
{
std::lock_guard<std::mutex> lk(state_mutex); // 锁
snapshot = g_shared_state; // 临界区只做拷贝(瞬间)
} // 立刻解锁
// 锁外:用 snapshot 做耗时的 frame-to-map 光度优化,不阻塞 LIO 线程
StateGroup refined = photometric_optimize(snapshot, cur_img, radiance_map);
{
std::lock_guard<std::mutex> lk(state_mutex);
if (refined.timestamp >= g_shared_state.last_lio_update) // 防止覆盖更新的 LIO 结果
g_shared_state = refined;
}
LVI-SAM 与 R3LIVE 的对照小结¶
把两个系统并排,差异立刻清晰——这正是"架构是选择"的最好注脚:
| 维度 | LVI-SAM | R3LIVE |
|---|---|---|
| 融合层次 | 紧耦合(统一因子图) | 松耦合(两个 ESIKF + 共享状态) |
| 求解器 | 图优化(GTSAM/iSAM2) | 滤波器(ESIKF ×2) |
| 视觉前端 | 特征点 + LiDAR 深度增强 | 光度 frame-to-map(直接法) |
| 核心目标 | 大场景定位 + 回环建图 | 实时 RGB 稠密辐射地图 |
| 退化/失效响应 | 双子系统故障切换 | 视觉光度约束补几何退化 |
| 回环能力 | 强(因子图天然支持) | 弱(滤波器不擅长,需外挂) |
| 计算/内存 | 较高(保留历史因子) | 较低(滤波器 O(1) 状态) |
理论-工程桥接:这张表完美演示了本章的两个正交维度如何组合出不同系统。LVI-SAM 选"紧耦合 + 图优化"是因为它要大场景回环(图优化主场)+ 高精度(紧耦合);R3LIVE 选"松耦合 + 滤波器"是因为它要实时稠密建图(滤波器够快)+ 几何辐射分工(松耦合自然)。没有哪个更好,只有哪个更适合目标——这就是架构课的核心素养。
系统三:FAST-LIVO2——紧耦合 + ESIKF 顺序更新 + 统一体素地图¶
架构总览。FAST-LIVO2(Zheng et al., 港大 MARS Lab, IEEE T-RO 2024)把紧耦合推到了极致:一个 ESIKF、所有原始测量、一张统一的体素地图。它的设计哲学是"信息利用最充分 + 计算最省",§39.2 的紧耦合 ESIKF 顺序更新骨架就是以它为原型写的。
FAST-LIVO2 顶层架构(紧耦合单 ESIKF + 统一体素地图):
IMU ──前向传播──→ ┌──────────────────────────────────┐
│ 单一 ESIKF 状态 + 协方差 │
LiDAR ─原始点──→ │ ① IMU 传播(含点云去畸变) │
│ ② LiDAR 点到面更新(第一次 ESIKF) │
相机 ─原始图块─→ │ ③ 视觉光度更新(第二次 ESIKF) │
└──────────────┬───────────────────┘
│ 读/写
▼
┌─────────────────────────────────────┐
│ 统一体素地图(LiDAR 几何 + 视觉块) │
│ 同一张地图既存平面/点(供 LiDAR 配准) │
│ 又存参考图块(供视觉光度对齐) │
└─────────────────────────────────────┘
所有原始测量进同一个 ESIKF,LiDAR 和相机共用一张体素地图——
这是紧耦合到底 + 地图统一到底。
五问框架逐条拆解:
| 五问 | FAST-LIVO2 的回答 |
|---|---|
| ① 传感器互补 | LiDAR 给视觉地图点的精确深度;视觉给 LiDAR 退化方向补光度约束;IMU 高频传播 + 去畸变 |
| ② 松/紧耦合 | 紧耦合到底——LiDAR 点到面残差 + 视觉光度残差进同一个 ESIKF |
| ③ 时空对齐 | 离线标定外参;IMU 传播做帧内对齐;在线估计光度参数(曝光) |
| ④ 滤波/图优化 | 滤波器——ESIKF 顺序更新(LiDAR 先、相机后) |
| ⑤ 退化/失效 | 紧耦合 + 鲁棒机制;视觉退化(少纹理)时 LiDAR 主导,LiDAR 退化时视觉主导,单 ESIKF 内自然加权 |
架构亮点 1:ESIKF 顺序更新(呼应 §39.2 的核心代码)。FAST-LIVO2 不把 LiDAR 和相机观测堆成一个大向量联合更新,而是顺序更新——先用 LiDAR 点到面残差做一次 ESIKF 更新,再在结果上用视觉光度残差做第二次更新。这正是 §39.2 我们详细讲过的"顺序更新避免维度灾难、且当噪声独立时等价于联合更新"。
// FAST-LIVO2 的顺序更新主循环(与 §39.2 的骨架一脉相承,这里点明它的两个地图角色)
void fast_livo2_process(const MeasureGroup& meas) {
state_propagate_with_imu(meas.imu, state, P); // ① IMU 传播 + 点云去畸变
if (!meas.lidar->empty()) { // ② LiDAR 更新(点到面)
// 从统一体素地图里查每个点所属体素的平面,构造点到面残差
iekf_update_lidar(state, P, meas.lidar, voxel_map);
// 更新后:把新点并入体素地图,更新/新建体素的平面估计
voxel_map.insert_lidar_points(meas.lidar, state);
}
if (meas.img) { // ③ 视觉更新(光度,在 ② 之后)
// 从统一体素地图里取可见体素的"参考图块",投影到当前帧算光度残差
iekf_update_visual(state, P, meas.img, voxel_map);
// 更新后:为体素挂载/更新视觉参考图块(供后续帧光度对齐)
voxel_map.attach_visual_patches(meas.img, state);
}
// 此刻 state 融合了 IMU + LiDAR + 相机;voxel_map 同时持有几何和视觉信息
}
架构亮点 2:统一体素地图——一张地图两种用途。这是 FAST-LIVO2 区别于 R3LIVE 的关键设计。R3LIVE 的几何和辐射虽然在同一张点云上,但 LIO 和 VIO 是两个估计器。FAST-LIVO2 更彻底:同一张体素地图,既给 LiDAR 提供平面(点到面配准),又给相机提供参考图块(光度对齐)。一个体素里同时存几何信息(平面参数)和视觉信息(参考图块)。
// FAST-LIVO2 体素的"双重身份"(概念示意)
struct Voxel {
// 几何部分:供 LiDAR 点到面配准
Eigen::Vector3d plane_center; // 平面中心
Eigen::Vector3d plane_normal; // 平面法向
Eigen::Matrix3d point_cov; // 点分布协方差(判断平面质量)
// 视觉部分:供相机光度对齐
struct RefPatch { // 参考图块
Eigen::Vector3d xyz; // 该图块对应的 3D 点(来自 LiDAR,深度精确!)
std::vector<float> intensity; // 参考帧上这个图块的像素灰度
Eigen::Matrix<double,2,6> jac;// 预计算的雅可比(加速 ESIKF)
double exposure; // 拍摄该参考帧时的曝光(用于光度补偿)
};
std::vector<RefPatch> patches; // 一个体素可挂多个不同视角的参考图块
};
// 关键:视觉图块的 3D 位置来自 LiDAR,深度天然精确——
// 这避免了纯视觉的三角化误差,是 LiDAR 给视觉补深度的极致体现。
架构亮点 3:直接法光度 + 无特征提取 + 在线光度补偿。FAST-LIVO2 的视觉部分是**直接法**——不提取 ORB/SIFT 特征、不做描述子匹配,直接用原始图像块的光度(灰度)误差。这省掉了特征提取的开销(快),也避免了无纹理区域提不出特征的问题(鲁棒)。但直接法对光照敏感,所以它**在线估计光度仿射参数**(曝光时间变化),把"两帧之间相机曝光不同导致的整体亮度变化"补偿掉。
// 光度仿射补偿:补偿曝光变化导致的整体亮度差(直接法的关键鲁棒性措施)
// 残差不是直接比较灰度,而是补偿曝光后再比较:
double photometric_residual_with_affine(const RefPatch& ref, const cv::Mat& cur_img,
const StateGroup& state, double cur_exposure) {
Eigen::Vector2d uv = project(state.R_cw * ref.xyz + state.t_cw, K);
double I_cur = bilinear_sample(cur_img, uv);
// 用曝光比缩放参考亮度,再比较(a = 曝光比,b = 偏置,可在线估计)
double a = cur_exposure / ref.exposure; // 仿射增益(曝光比)
return I_cur - (a * ref.intensity_center + b); // 补偿后的光度残差
// 不补偿的后果:相机自动曝光一变,所有光度残差整体偏移,
// ESIKF 误以为位姿错了去"修",实际只是曝光变了——位姿被带偏。
}
本质洞察:FAST-LIVO2 把"LiDAR 给视觉补深度"做到了极致——视觉图块的 3D 位置直接来自 LiDAR 测距,深度精确无三角化误差;同时"视觉给 LiDAR 补退化"也内生于单一 ESIKF——LiDAR 在某方向退化(点到面 Hessian 该方向特征值塌缩,§39.5),同一个 ESIKF 里视觉光度残差自然在那个方向提供约束,无需显式切换。紧耦合 + 统一地图让两种互补在测量层面、地图层面双重融合,这是它精度和效率俱佳的根源。
架构演进的家谱——这三个系统不是凭空出现的 ⭐⭐⭐¶
为什么要讲家谱:前面我们把三个系统当成"三个独立的设计样本"横向对比。但工程现实里,没有哪个 SOTA 系统是凭空蹦出来的——它们都是在前作的痛点上长出来的。理解"它从哪个系统、因为什么痛点演化而来",比单看它"长什么样"更能教会你架构决策的逻辑。这一节我们沿时间轴纵向追溯,看每一次架构改动背后被解决的具体问题。这本身就是 §39 全章方法论的活教材:架构演进的每一步,都是对"五问"中某一问的重新回答。
谱系一:LOAM → LIO-SAM → LVI-SAM(激光派的"加法"路线)
LOAM (2014) LIO-SAM (2020) LVI-SAM (2021)
纯 LiDAR 里程计 → LiDAR+IMU 紧耦合因子图 → 再加视觉子系统
(scan-to-scan/map) (GTSAM, IMU预积分) (双子系统故障切换)
│ │ │
痛点: 痛点: 痛点:
无 IMU,快速运动 纯激光在几何退化 激光退化(隧道)无
下畸变/丢帧 环境(隧道/旷野)崩 备份;缺视觉回环
- LOAM 的贡献与痛点:LOAM(Zhang & Singh, RSS 2014)提出了"边缘点/平面点分离 + scan-to-scan 高频里程计 + scan-to-map 低频精化"的双层结构,是激光里程计的奠基。但它没有 IMU,快速运动下点云畸变严重、视场被遮挡时容易丢帧。这对应五问的第①问(传感器互补)尚未展开——只有一种传感器。
- LIO-SAM 的改动:Shan et al.(IROS 2020)把 IMU 紧耦合进来,用 IMU 预积分因子(§39.4)做帧内去畸变和高频传播,后端换成 GTSAM 因子图 + iSAM2,并引入回环因子。这一步回答了第②③④问——紧耦合(②)、IMU 帧内对齐(③)、图优化后端(④)。但它仍是纯激光-惯性,在长隧道、空旷场地等几何退化环境下,没有第二种几何无关的传感器兜底,照样会在退化方向漂移。
- LVI-SAM 的改动:在 LIO-SAM 之上嫁接一个改自 VINS-Mono 的视觉-惯性子系统(VIS),让相机在激光退化时接管、并提供视觉回环;反过来 LiDAR 给视觉特征补度量深度消除尺度模糊。这一步才真正回答第①问(互补)和第⑤问(退化/失效响应)——激光退化靠视觉、视觉失效靠激光,双子系统互为备份。看清这条线,你就明白 LVI-SAM 的"双子系统故障切换"不是炫技,而是 LIO-SAM 在退化环境吃了亏后长出来的解药。
谱系二:FAST-LIO → FAST-LIO2 → FAST-LIVO → FAST-LIVO2(滤波派的"做减法 + 提效率"路线)
FAST-LIO (2021) FAST-LIO2 (2022) FAST-LIVO (2022) FAST-LIVO2 (2024)
LiDAR+IMU 紧耦合 → 增量kd树(ikd-Tree) → 加视觉,双地图 → 统一体素地图
ESIKF 直接配准原始点 (LiDAR图+视觉图分开) +在线光度补偿
│ │ │ │
痛点: 痛点: 痛点: 痛点:
ESIKF增益计算 特征提取耗时; 两套地图冗余; 曝光变化带偏
随观测维度爆炸 地图增删慢 几何/视觉信息没共享 位姿;效率
- FAST-LIO 的关键数学贡献:Xu & Zhang(RA-L 2021)的核心创新是一个**等价的卡尔曼增益公式**——传统 ESIKF 的增益矩阵维度随 LiDAR 观测数(成百上千个点)膨胀,求逆代价巨大;FAST-LIO 推导出一个把求逆维度从"观测数"降到"状态维数"的等价形式,让稠密 LiDAR 点也能实时进 ESIKF。这是第④问(滤波器)的工程突破:让滤波器路线在稠密激光下重新变得可行。
- FAST-LIO2 的改动:抛弃 LOAM 式的边缘/平面特征提取,直接拿原始点做 scan-to-map 配准(少了特征提取这一步,更快也更不挑环境),并用增量式 kd 树(ikd-Tree)支持地图点的高效增删。这一步是"做减法"——去掉特征工程,回归原始测量。
- FAST-LIVO 的改动:加入相机,走直接法光度路线(呼应 R3LIVE 的去特征思路),但此时几何地图和视觉地图还是**两套**,信息没有充分共享。
- FAST-LIVO2 的改动:把两套地图合并成**统一体素地图**(一个体素同时存平面和参考图块,§39.6 系统三详述),并加入在线光度仿射补偿解决曝光问题。这一步把第①问(互补)做到极致——视觉图块的 3D 位置直接复用 LiDAR 测距,地图层面也实现融合。
对比性思维(认知工具 B):两条谱系揭示了两种截然不同的演进哲学。激光派(LOAM 系)是**"加法"——不断往因子图里加新的传感器和约束,靠图优化的统一表达力把异构信息糅在一起,代价是计算和内存随历史增长。滤波派(FAST 系)是"减法 + 提效"**——先用数学技巧(等价增益公式)让滤波器扛得住稠密观测,再砍掉特征提取、合并地图,把每一分算力榨干,代价是放弃了回环这种需要回溯历史的能力。这不是谁对谁错,而是两群人面对"既要精度又要实时"这个永恒矛盾时,从不同方向逼近的结果。
本质洞察:架构演进的驱动力永远是**"上一代在某个具体场景下吃的亏"。LIO-SAM 在隧道退化吃亏 → LVI-SAM 加视觉;FAST-LIO 的增益矩阵在稠密点下爆炸 → 等价增益公式;FAST-LIVO 两套地图冗余 → 统一体素地图。这给你一个极其实用的读论文技巧:**拿到一篇新系统的论文,先别看它的架构图,先去 Related Work 和 Introduction 里找"它说前作有什么不足"——那句话就是整篇论文所有架构改动的总开关。顺着"痛点 → 改动"这条因果链读,任何复杂系统都会变得可预测。这也正是本章一直在训练的能力:不是记住某个系统长什么样,而是理解它为什么长成这样。
这条家谱对你做架构设计的启示。当你为自己的项目选型时,不要直接照搬最新的 FAST-LIVO2 或 LVI-SAM,而要**先定位自己处在哪条谱系的哪个节点**:
| 你的处境 | 谱系定位 | 建议起点 |
|---|---|---|
| 只有 LiDAR+IMU、小场景、要快 | FAST 系早期节点 | FAST-LIO2(去特征、滤波、快) |
| LiDAR+IMU、大场景、需回环 | LOAM 系中段 | LIO-SAM(因子图、可回环) |
| 加了相机、几何退化是主敌 | LVI-SAM 节点 | LVI-SAM(双子系统切换) |
| 加了相机、要极致里程计精度/效率 | FAST 系末端 | FAST-LIVO2(统一地图、紧耦合到底) |
| 加了相机、要 RGB 稠密重建 | 旁支 | R3LIVE(几何/辐射分工) |
读这张表的方法:先用 §39.1 的传感器配置和 §39.5 的主要退化场景定位自己在哪一行,再用对应起点做"五问"微调。这比盲目追新更可靠——很多项目用 FAST-LIO2 就够了,强上 FAST-LIVO2 反而背上了用不到的视觉复杂度。
三系统总对照表——本章所有维度的收敛点¶
把三个系统在本章五个维度上并排,这张表是整章的浓缩:
| 维度 | LVI-SAM | R3LIVE | FAST-LIVO2 |
|---|---|---|---|
| 融合层次(§39.2) | 紧耦合 | 松耦合 | 紧耦合 |
| 求解器(§39.4) | 图优化(GTSAM) | 滤波器(ESIKF×2) | 滤波器(ESIKF 顺序更新) |
| 视觉前端 | 特征点+LiDAR深度 | 光度 frame-to-map | 直接法光度图块 |
| 地图组织 | 因子图+点云 | 共享辐射点云 | 统一体素地图 |
| 估计器数量 | 双子系统 | 双子系统 | 单 ESIKF |
| 退化/失效(§39.5) | 双子系统故障切换 | 光度补几何退化 | 单 ESIKF 内自然加权 |
| 回环能力 | 强 | 弱 | 弱 |
| 核心追求 | 大场景回环建图 | 实时 RGB 稠密图 | 极致精度+效率 |
| 主导机构/年份 | T. Shan, 2021 | 港大 MARS, 2021 | 港大 MARS, 2024 |
系统性分类(认知工具 E):这三个系统沿"估计器数量"可分两类——双子系统(LVI-SAM、R3LIVE,模块独立、可故障切换)vs 单估计器(FAST-LIVO2,信息利用最充分、无切换开销);沿"求解器"分两类——图优化(LVI-SAM,能回环)vs 滤波器(R3LIVE、FAST-LIVO2,快);沿"视觉范式"分两类——间接法/特征(LVI-SAM)vs 直接法/光度(R3LIVE、FAST-LIVO2,省特征提取、抗弱纹理)。每一种分类轴都对应一个工程权衡,没有免费的午餐。
走一遍完整的架构决策——以室内巡检无人机为例 ⭐⭐⭐¶
前面三个系统都是"成品",对照表也列了它们的取舍。但你真正要学会的是**从零做决策**的过程。这里我们不给答案、而是演示"思考的轨迹"——拿一个具体场景,把五问框架当成一份决策清单逐条走,看每个选择是怎么被场景逼出来的。这就是本章所有内容的最终用法。
场景设定:一架室内仓库巡检无人机。约束很苛刻——机载算力弱(嵌入式 NUC 级)、载重小(传感器不能堆太多)、要钻货架间的窄巷道(长直、弱纹理、几何退化典型场景)、飞行速度中等但有急转弯、需要实时避障定位、巡检完不要求 RGB 稠密重建(只要轨迹和稀疏地图)、巡检路线是闭环(有回环机会但不强求全局一致)。
这个场景是刻意挑的——它把本章每一节的核心矛盾都浓缩进了一组具体约束:窄巷道直接命中 §39.5 的退化、急转弯命中 §39.3 的时间同步放大、弱算力命中 §39.2/§39.4 的求解器选择、闭环命中 §39.4 的回环取舍。换句话说,走完这个场景的五问,等于把全章五节又串了一遍,只不过这次是"用"而非"学"。下面每一问,我都会先摆出场景给出的约束、再推出决策、最后标明它落在本章哪一节——你可以把它当成自己做项目时的决策模板。
第①问——传感器怎么互补(§39.1)。先看每个维度谁能观测:
| 候选传感器 | 能补什么 | 在本场景的顾虑 |
|---|---|---|
| 固态/小型 LiDAR | 精确测距、几何骨架、避障 | 窄巷道前进方向退化(§39.5) |
| IMU | 高频运动、帧内去畸变、急转弯下的姿态 | 单独用会漂 |
| 相机(单目) | 弱纹理外的纹理约束、补 LiDAR 退化方向 | 弱纹理巷道里特征少;重量/算力 |
决策:LiDAR + IMU 是地基(避障 + 几何 + 高频),必选。相机要不要加?这取决于第⑤问的退化分析——窄巷道 LiDAR 前进方向退化,正需要一个几何无关的传感器在那个方向补约束。所以**加一个轻量相机**。这就是 §39.1"用一种补另一种观测不到的维度"的直接应用——不是为了多而多,而是精准地补 LiDAR 的退化方向。
第②问——松还是紧耦合(§39.2)。场景算力弱、且急转弯下时间/外参误差会被放大。紧耦合信息利用充分但对同步标定敏感、实现复杂;松耦合实现简单、模块可独立调试但精度略损。
决策:算力弱 + 急转弯下要稳,倾向**紧耦合的滤波器路线**——把 LiDAR 点到面和视觉光度残差顺序更新进同一个 ESIKF(§39.2 的顺序更新骨架),信息用满又不至于像联合大向量那样维度爆炸。但前提是把第③问的同步标定做扎实,否则紧耦合反而放大对齐误差(§39.2 陷阱)。
第③问——时空对齐怎么做(§39.3)。急转弯意味着角速度大,时间偏移会被放大成姿态误差(\(\Delta\theta = \omega t_d\),§39.3)——这是本场景的高危项。
决策:尽量上**硬件触发或 PTP 同步**让 LiDAR/相机共享时钟;做不到就在线估计 \(t_d\)(§39.3),但要保证标定阶段有足够的旋转激励(绕各轴转一圈)让 \(t_d\) 和旋转外参可观。外参离线用 Kalibr 标到收敛、锁定为常量。这一步是紧耦合方案能否成立的命门——前面选了紧耦合,这里就必须把对齐做到位。
第④问——滤波还是图优化、要不要回环(§39.4)。场景算力弱、要实时、闭环但"不强求全局一致"。图优化能回环但吃算力和内存;滤波器快但不擅长回环。
决策:主里程计用**滤波器(ESIKF)保证实时和低算力。回环不强求,但既然路线闭环、又想要轨迹尽量一致,可以**外挂一个轻量位姿图——平时不动,检测到回环时才在后台做一次小规模位姿图优化修正漂移(这正是 §39.4 滤波器"外挂回环"和 LVI-SAM"因子图原生回环"之间的折中)。这体现了两个正交维度可以混搭:前端滤波求快,后端按需图优化求一致。
第⑤问——哪里会退化、怎么响应(§39.5)。本场景的头号退化是**窄巷道前进方向**——LiDAR 点到面 Hessian 在前进方向特征值塌缩(§39.5 手推过)。
决策:在 ESIKF 的 LiDAR 更新里做**Hessian 特征值分解 + 解重映射**——前进方向特征值塌缩时,把该方向的更新量清零、交给 IMU 预测和视觉光度接管(视觉正是为此而加,第①问的伏笔在这里兑现)。这是"检测必须连着响应"(§39.5 陷阱)的落实。
综合架构与谱系定位。把五个回答拼起来:LiDAR+相机+IMU、紧耦合、ESIKF 顺序更新、外挂轻量回环、Hessian 退化检测 + 解重映射。对照"家谱定位表"——这最接近 FAST-LIVO2 节点(紧耦合滤波 + 视觉补退化),但做了两处场景化裁剪:(a) 砍掉了 RGB 稠密辐射建图(场景不需要,省算力);(b) 外挂了一个 FAST-LIVO2 没有的轻量回环(场景路线闭环、想要轨迹一致)。注意这两处裁剪不是"改进 FAST-LIVO2",而是"按本场景的目标重新取舍"——增的(回环)和减的(辐射建图)都直接对应场景约束,没有一处是为了用而用。这正是架构设计的纪律:每一个加减都要能追溯到一条具体的场景需求。
理论-工程桥接:注意整个决策过程的因果链——第⑤问的退化分析(窄巷道)反过来决定了第①问要不要加相机,第②问的紧耦合选择又给第③问的同步标定提出了硬要求。五问不是五个独立的填空,而是一张相互牵制的约束网:动一个选择,相关的几个都要重新审视。这正是架构设计的真实质感——它是一个需要反复迭代、前后呼应的整体,而不是按清单逐项打勾。你能独立走完这样一条链,就真正具备了本章要传授的架构素养。
贯穿五问的隐形约束:算力预算。上面的决策里"算力弱"反复出现——它不是第六个问题,而是一条**贯穿全部五问的横切约束**,值得单独点明。算力预算会同时挤压多个选择:它让第④问倾向滤波器(图优化吃算力)、让第②问的紧耦合实现要选顺序更新(避免大向量联合更新的求逆代价,呼应 §39.2 和 FAST-LIO 的等价增益公式)、让第①问克制加传感器(每个传感器都带来处理开销)、甚至影响第⑤问退化检测的频率(每帧做特征值分解也有成本,需权衡精度与开销)。
本质洞察:好的架构师做决策时,脑子里始终挂着一张"算力账本"——每加一个传感器、每选一种更重的求解器、每开一个检测模块,都要问"这点算力花得值不值"。SOTA 系统的精度数字是在某个算力平台上跑出来的,换到你的弱算力平台未必复现。所以读论文不仅要看精度,还要看它报告的运行平台和帧率——一个在桌面 GPU 上 30Hz 的系统,搬到嵌入式可能只剩 5Hz,实时性假设直接崩塌。架构决策的本质,是在"精度、鲁棒、实时、算力"这个四角张力里找一个对你的场景最优的平衡点,而不是追求任何单一指标的极值。这也是为什么本章从头到尾强调"没有最好的架构,只有最适合的架构"。
⚠️ 常见陷阱¶
陷阱 1(概念误区):以为"更新(如 FAST-LIVO2 之于 R3LIVE)"就是全面更好 - 错误描述:看到 FAST-LIVO2 比 R3LIVE 新、精度数字更好,就认为它在所有场景全面替代后者。 - 现象/后果:在需要 RGB 稠密辐射地图的应用(如三维重建、可视化)里强行用 FAST-LIVO2,发现它的输出更偏向里程计+几何地图,辐射建图不是它的核心强项;或在需要回环的大场景里用纯滤波系统,吃了无回环的漂移亏。 - 根本原因:系统是为特定目标优化的。FAST-LIVO2 追求里程计精度和效率,R3LIVE 追求实时 RGB 稠密重建,LVI-SAM 追求大场景回环建图。"更新"只代表在原作者的目标上做得更好,不代表覆盖所有目标。 - 正确做法:按你的应用目标选系统——要回环建图选 LVI-SAM 类,要 RGB 稠密图选 R3LIVE 类,要高频高精里程计选 FAST-LIVO2 类。读论文时先看它的目标场景和评测指标,别只看精度数字排名。
陷阱 2(思维陷阱):把三个系统的架构当成"唯一正确答案"去套用 - 错误描述:学了 FAST-LIVO2 就认为"LiDAR+相机+IMU 就该用单 ESIKF 顺序更新 + 统一体素地图",把它当模板硬套到自己的项目。 - 现象/后果:自己的场景(比如算力极弱的嵌入式、或没有 LiDAR 只有双目+IMU、或需要多机协同)和原系统假设不符,硬套导致水土不服。 - 根本原因:每个系统的架构都是在特定硬件、特定场景、特定目标下的最优解。脱离了这些前提,架构选择就要重做。架构是"权衡的结果",不是"可以无脑复制的配方"。 - 正确做法:学系统时学的是**它如何权衡**(为什么紧耦合、为什么用滤波器、为什么统一地图),而非照搬结论。回到本章的五问框架,对你自己的项目重新回答这五个问题,得出适合你的架构。
陷阱 3(概念误区):混淆"松耦合的双子系统"和"紧耦合的双子系统" - 错误描述:看到 LVI-SAM 和 R3LIVE 都有"两个子系统",就以为它们融合层次一样。 - 现象/后果:错误地认为 LVI-SAM 也是松耦合,或 R3LIVE 也是紧耦合,在分析/选型时判断失误。 - 根本原因:"几个子系统"是**模块划分**维度,"松/紧耦合"是**信息融合层次**维度,两者正交。LVI-SAM 有两个子系统但它们通过统一因子图紧耦合(视觉特征用 LiDAR 深度、约束进同一张图);R3LIVE 也有两个子系统但它们是两个独立 ESIKF、松耦合(各自处理原始测量、只共享状态)。 - 正确做法:判断松/紧耦合看"原始测量是否进同一个估计器"(§39.2 的判据),而非看"有几个模块"。LVI-SAM 的视觉特征带着 LiDAR 深度进了统一因子图(紧),R3LIVE 的 LiDAR 点和图像像素分别进两个 ESIKF(松)。
练习¶
-
(分析题) 给你三段陌生的 LIVO 代码片段(分别来自类 LVI-SAM、类 R3LIVE、类 FAST-LIVO2 的系统),但没有告诉你出处。请用本章的五问框架,列出你会在代码里寻找哪些"判别特征"来确定每段属于哪类架构。至少给出:(a) 如何看出滤波器还是因子图(找什么类/函数);(b) 如何看出松耦合还是紧耦合(找什么数据流);(c) 如何看出视觉用特征法还是直接法(找什么处理步骤)。
-
(设计题) 你要为一个**地下管廊巡检机器人**设计 LIVO 系统。环境特点:长直管廊(LiDAR 前进方向退化)、光照差且不均(视觉直接法的曝光挑战)、需要事后生成带颜色的三维模型(要辐射建图)、机器人算力中等、巡检路线是闭环(有回环机会)。请从三个系统里取长补短,设计你的架构,并对每个设计决策说明它解决了哪个环境挑战、对应本章哪一节。
-
(跨章综合题,连接 §39.1~§39.6) 综合全章:从传感器选型(§39.1)开始,一路走到系统架构(§39.6),为一个**室外园区配送机器人**做完整的架构设计文档。要求覆盖:(a) 选哪些传感器、为什么(§39.1 互补性论证);(b) 松还是紧耦合、为什么(§39.2);(c) 时间同步和外参标定方案(§39.3);(d) 滤波还是图优化、是否要回环(§39.4);(e) 哪些场景会退化、怎么检测和响应(§39.5);(f) 整体架构图,并说明它最接近三个精读系统中的哪一个、做了哪些改动。这道题没有标准答案,重点是论证链条是否自洽。
-
(论文研读题,运用家谱方法) 找一篇本章没讲过的 LIVO/LIO 系统论文(如 Point-LIO、SuperOdometry、或任意一篇新作),用 §39.6"家谱"一节教的方法读它:(a) 先只读 Introduction/Related Work,把"它说前作有什么不足"的句子摘出来;(b) 据此预测它会做哪些架构改动来解决这些不足;(c) 再读方法部分,验证你的预测对了几条;(d) 最后给它做一次"算力账本"分析——它为精度/鲁棒付出了哪些算力代价,报告的运行平台和帧率是多少。完成后你会发现:先读痛点再读方法,比直接啃方法部分高效得多。
本章常见误解汇总¶
下表收集了初学者在多传感器架构上最容易产生的误解,左列是常见的错误认知,右列是正确理解。建议在读完全章后回头逐条自检。
| 常见误解 | 正确理解 |
|---|---|
| 传感器越多,系统越鲁棒 | 传感器多只在"知道每个何时失效并据此降权"时才鲁棒;否则失效传感器的输出会污染整个状态,反而崩得更彻底(§39.1) |
| 紧耦合一定比松耦合精度高 | 紧耦合信息利用更充分,但对同步/标定误差更敏感、对坏测量更脆弱。同步标定不够好时,紧耦合反而更差(§39.2) |
| 时间同步差几毫秒无所谓 | 在 200°/s 转速下,30ms 偏移就是 6° 姿态误差,被紧耦合放大到整个状态。这正是"低速正常、高速发散"的根因(§39.3) |
| 外参旋转和平移同样重要 | 旋转外参误差随观测距离线性放大,比平移外参敏感得多。旋转标定不准是紧耦合最常见的发散原因(§39.3) |
| 因子图一定比滤波器精度高 | 因子图的优势(重线性化、回环、全局一致)只在大场景、有回环时兑现。小场景高频里程计用滤波器又快又够准(§39.4) |
| 边缘化老帧就是直接删掉它 | 直接删会丢失信息。必须用 Schur 补把它的信息折叠成先验因子保留,且要 FEJ 固定线性化点保证一致性(§39.4) |
| 残差大就是观测不好、估计不准 | 退化方向上残差恰恰很小(怎么取值都小)。判断退化要看曲率(Hessian 特征值),不看残差大小(§39.5) |
| 退化了就把整个传感器丢弃 | 退化是方向性的。隧道里 LiDAR 只在前进方向退化,左右上下仍可观。整体丢弃会扔掉有用约束,应只对退化方向降权(§39.5) |
| 检测到退化打个警告就行 | 检测必须连着响应(解重映射或降权),否则退化方向的垃圾值照样污染状态,检测等于没做(§39.5) |
| 学会一个先进系统就能套用到所有项目 | 每个系统是特定硬件/场景/目标下的最优解。学的是"如何权衡",不是照搬结论。换了前提要用五问框架重新设计(§39.6) |
| "有两个子系统"就是松耦合 | "几个模块"和"松/紧耦合"是正交维度。LVI-SAM 有两个子系统但紧耦合(统一因子图),R3LIVE 两个子系统但松耦合(两个独立 ESIKF)(§39.6) |
| 旋转可以像向量一样线性插值/相加 | 旋转在流形 \(SO(3)\) 上,线性插值/相加会破坏正交性。必须用 SLERP / boxplus / 指数映射(§39.3,呼应李群章节) |
| 选系统就该选最新的那个 | 最新只代表在原作者目标上更先进。很多项目用 FAST-LIO2/LIO-SAM 就够,强上 FAST-LIVO2 反而背上用不到的视觉复杂度。先按谱系定位自己处在哪个节点(§39.6 家谱) |
| 论文的架构图是理解它的入口 | 真正的入口是 Introduction/Related Work 里"前作有什么不足"那句话——它是所有架构改动的总开关。顺着"痛点→改动"读才看得懂为什么这么设计(§39.6 家谱) |
| 五问是五个独立的选择题 | 五问是相互牵制的约束网:退化分析(⑤)决定要不要加相机(①),紧耦合(②)又给同步标定(③)提硬要求。动一个,相关的都要重审(§39.6 决策演示) |
本章小结¶
本章是一堂**架构课**而非算法课。核心论点贯穿始终:同样的传感器、同样的数学,能长成完全不同的系统,架构是工程权衡的选择而非必然。我们建立了一套自顶向下的分析框架:
-
传感器特性(§39.1)是一切的输入。每种传感器都有物理上测不到的维度——相机测不到尺度、LiDAR 测不到无几何特征方向的运动、IMU 测不到不积分的绝对位姿。融合的本质是"用一种传感器观测另一种观测不到的维度",互补性比数量更重要。
-
两个正交的架构维度。融合层次(松耦合 vs 紧耦合,§39.2)决定"传感器在哪一层握手"——松耦合各传感器先独立出位姿再融合,紧耦合原始测量直接进同一估计器。求解器(滤波器 vs 图优化,§39.4)决定"用什么解"——滤波器边缘化历史求快,图优化保留历史能回环。两个维度组合出四个象限。
-
两个工程前提(§39.3)。时间同步(何时)和外参标定(何地)是融合的地基。时间差几毫秒在高速下放大成发散,外参旋转差几度让紧耦合崩。地基歪的特征是"低速正常、高速发散"。
-
鲁棒性的关键(§39.5)是退化检测。退化是环境×传感器的方向性属性,用 Hessian 特征值分解检测(特征值塌缩=该方向不可观),用解重映射或方向性降权响应。判据是曲率而非残差大小。
-
三个真实系统(§39.6)把所有维度串起来。LVI-SAM(紧耦合+图优化+双子系统切换)、R3LIVE(松耦合+滤波器+共享辐射地图)、FAST-LIVO2(紧耦合+滤波器+统一体素地图),用同一套传感器演示了三种架构哲学。
全章最重要的一句话:遇到任何多传感器 SLAM 系统,用"五问框架"拆解它——传感器怎么互补、松还是紧、怎么对齐、滤波还是图优化、怎么处理退化。这五个问题答完,你就读懂了这个系统的灵魂,也学会了为自己的项目做架构决策。
学完本章你应该能做到¶
下面是本章的能力出口清单,对应章首的前置自测——读完后回头逐条自检。如果某条做不到,括号里标了回炉的小节。这不是知识点罗列,而是"能否独立完成某个动作"的检验。
- 拿到一个陌生 LIVO 系统的论文或代码,能用**五问框架**把它的架构完整拆解出来(§39.6)。
- 看一段后端代码,能从"估计器数量 + 原始测量去向"判断它是**松耦合还是紧耦合**,不被"有几个子系统"迷惑(§39.2、§39.6 陷阱 3)。
- 能解释为什么一个系统"低速正常、高速发散",并说出至少三个排查方向(§39.3、故障排查场景一)。
- 会算时间偏移在给定角速度下造成多大姿态误差(\(\Delta\theta=\omega t_d\)),并据此判断同步精度是否够用(§39.3)。
- 能解释"为什么退化方向上残差反而小",并说出正确的退化判据是 **Hessian 特征值**而非残差大小(§39.5)。
- 给定一个环境(走廊/旷野/房间),能预判 LiDAR 点到面 Hessian 哪个方向会退化,并写出 \(\mathbf{H}_t=\sum \mathbf{n}_i\mathbf{n}_i^\top\) 的大致结构(§39.5)。
- 能说清因子图相对滤波器的优势(重线性化、回环、全局一致)只在**什么条件下**才兑现(§39.4)。
- 拿到一篇新系统论文,会先去 Introduction/Related Work 找"前作的不足"作为理解所有架构改动的总开关(§39.6 家谱)。
- 能为一个给定约束的新场景(如本章的无人机/管廊/园区例子)走完一条**前后呼应、相互牵制**的五问决策链,而不是逐项独立打勾(§39.6 决策演示)。
如何用这份清单:最后三条是最高阶的能力——它们要求你把分散的知识点织成一张能互相牵制的决策网。如果前六条都能做到、后三条还吃力,说明你掌握了"零件"但还没学会"组装",建议重做 §39.6 的决策演示和三道练习。架构素养的标志不是记住多少系统,而是能否独立走完一条自洽的决策链。
术语速查表¶
本章新引入或重点强调的术语,按首次出现顺序:
| 术语(中/英) | 一句话定义 |
|---|---|
| 松耦合(loose coupling) | 各传感器先独立估出位姿,再融合这些"半成品结果" |
| 紧耦合(tight coupling) | 所有传感器的原始测量直接进同一个估计器联合求解 |
| 共享状态(shared state) | 松耦合中多个子系统通过读写的低维状态(位姿+协方差)通道 |
| ESIKF(误差状态迭代卡尔曼滤波) | 在误差状态上做迭代卡尔曼更新,标称状态在流形上积分 |
| 顺序更新(sequential update) | 先用一种传感器更新、再在结果上用另一种更新;噪声独立时等价于联合更新 |
| 时间同步(temporal synchronization) | 确保不同传感器的测量对齐到同一时刻 |
| 时间偏移 \(t_d\)(time offset) | 传感器报的时间戳与真实采集时刻之间的固定差 |
| 硬件触发(hardware trigger) | 用物理信号让多传感器共享时钟、同步采集 |
| 运动补偿/去畸变(motion compensation) | 用 IMU 把一帧 LiDAR 内不同时刻的点统一到同一参考时刻 |
| 外参(extrinsic) | 两个传感器坐标系之间的刚体变换 \(\mathbf{T}\in SE(3)\) |
| SLERP(球面线性插值) | 在 \(SO(3)\) 流形上插值旋转,走测地线而非弦 |
| 因子图(factor graph) | 变量节点(待估量)+ 因子节点(带权残差)的二部图表示 |
| 信息矩阵(information matrix) | 协方差的逆 \(\boldsymbol{\Sigma}^{-1}\),作为残差的权重 |
| 最大后验(MAP) | 求一组状态使后验概率最大,等价于加权最小二乘 |
| iSAM2 | 用贝叶斯树做增量更新的因子图求解器,只重算受影响的变量 |
| 边缘化(marginalization) | 用 Schur 补把变量的信息折叠成先验,从显式状态中移除 |
| 规范自由度(gauge freedom) | 纯相对约束下整张图可整体平移旋转的不可观零空间 |
| First-Estimate Jacobian(FEJ) | 对被边缘化相关变量固定线性化点,保证边缘化前后信息一致 |
| 退化(degeneracy) | 环境让某观测在某些方向上失去约束能力(不可观) |
| 退化因子(degeneracy factor) | Hessian 最小特征值或特征值比,量化退化程度 |
| 解重映射(solution remapping) | 把更新量投影到可观子空间,退化方向不更新 |
| 可观测性(observability) | 状态某方向能否被当前观测约束 |
| 辐射地图(radiance map) | 带 RGB/灰度(辐射率)的稠密点云地图 |
| frame-to-map 光度 | 把全局地图点投影到当前帧、最小化光度误差(不累积帧间漂移) |
| 直接法(direct method) | 不提取特征、直接用图像块光度误差估计运动 |
| 统一体素地图(unified voxel map) | 同一张体素地图既存几何(平面)又存视觉(参考图块) |
| 光度仿射补偿(photometric affine compensation) | 用曝光比缩放参考亮度,补偿自动曝光导致的整体亮度变化 |
| 增量 kd 树(ikd-Tree) | 支持点的高效增量插入/删除的 kd 树,FAST-LIO2 用它维护地图 |
| 等价卡尔曼增益(equivalent Kalman gain) | FAST-LIO 把增益求逆维度从观测数降到状态维数的等价公式 |
| scan-to-scan / scan-to-map | 当前帧配准到上一帧(累积漂移)/ 配准到全局地图(不累积) |
| 边缘点/平面点(edge/planar feature) | LOAM 按局部曲率把 LiDAR 点分为边缘和平面两类特征 |
知识点总表¶
| 知识点 | 难度 | 一句话掌握标准 | 所在小节 |
|---|---|---|---|
| 五类传感器可观测量与失效模式 | ⭐⭐ | 能说出每种传感器测不到什么维度、何时失效 | §39.1 |
| 互补性原则 | ⭐⭐ | 理解"用一种补另一种观测不到的维度">"数量多" | §39.1 |
| 松耦合 vs 紧耦合定义与判据 | ⭐⭐⭐ | 能从代码(估计器数量、原始测量去向)反推架构 | §39.2 |
| 松/紧耦合的工程权衡 | ⭐⭐⭐ | 能论证何时该选松、何时该选紧 | §39.2 |
| ESIKF 顺序更新 | ⭐⭐⭐ | 理解顺序更新为何等价联合更新、为何避免维度灾难 | §39.2 |
| 时间偏移的放大效应 | ⭐⭐⭐ | 会算 \(\Delta\theta=\omega t_d\),理解"低速正常高速崩" | §39.3 |
| 时间同步四层次 | ⭐⭐⭐ | 能按硬件/精度/改驱动意愿选同步方案 | §39.3 |
| 流形插值(SLERP) | ⭐⭐⭐ | 知道旋转不能线性插值,会用 SLERP | §39.3 |
| 外参敏感性(旋转>平移) | ⭐⭐⭐ | 理解旋转误差随距离放大,平移不放大 | §39.3 |
| 在线时间/外参标定与可观测性 | ⭐⭐⭐⭐ | 知道每个参数需要什么激励、为何匀速估不出 | §39.3 |
| 因子图三要素 | ⭐⭐⭐ | 能把异构约束翻译成因子,连到共享变量 | §39.4 |
| MAP/优化/线性代数三视角 | ⭐⭐⭐ | 理解因子图稀疏性 = H 稀疏性 = 求解快 | §39.4 |
| 滤波器 vs 图优化对比 | ⭐⭐⭐ | 能按场景(回环/算力/场景大小)选求解器 | §39.4 |
| 规范自由度与锚点 | ⭐⭐⭐ | 知道纯相对约束需先验锚定,否则 H 奇异 | §39.4 |
| 边缘化与 FEJ | ⭐⭐⭐⭐ | 理解边缘化=Schur 补,FEJ 保证一致性 | §39.4 |
| Hessian 特征值与可观测性 | ⭐⭐⭐⭐ | 理解特征值塌缩=该方向不可观,三视角统一 | §39.5 |
| 解重映射 | ⭐⭐⭐⭐ | 会构造可观子空间投影,退化方向不更新 | §39.5 |
| 退化的方向性 | ⭐⭐⭐⭐ | 理解退化是方向性的,要方向性响应而非整体丢弃 | §39.5 |
| 双层降级响应 | ⭐⭐⭐ | 区分里程计级硬切和后端级软调 | §39.5 |
| LVI-SAM 架构 | ⭐⭐⭐⭐ | 能说清紧耦合+因子图+双子系统切换的权衡 | §39.6 |
| R3LIVE 架构 | ⭐⭐⭐⭐ | 能说清松耦合+滤波器+辐射地图的权衡 | §39.6 |
| FAST-LIVO2 架构 | ⭐⭐⭐⭐ | 能说清紧耦合+顺序更新+统一体素地图的权衡 | §39.6 |
| 五问框架 | ⭐⭐⭐ | 能用五问拆解任意 LIVO 系统 | §39.6 |
API 速查表¶
本章涉及的核心 API 签名与用途,供查阅:
| API / 类型 | 库 | 用途 |
|---|---|---|
std::lock_guard<std::mutex> |
C++ STL | RAII 加锁,保护共享状态临界区(§39.2) |
Eigen::Quaterniond::slerp(t, q2) |
Eigen | 球面线性插值旋转(§39.3) |
Eigen::SelfAdjointEigenSolver<M> |
Eigen | 对称矩阵特征值分解(实数、升序)(§39.5) |
gtsam::NonlinearFactorGraph |
GTSAM | 存放所有因子的图容器(§39.4) |
gtsam::ISAM2 / ISAM2::update |
GTSAM | 增量平滑求解器(§39.4) |
gtsam::ImuFactor |
GTSAM | IMU 预积分因子(连位姿+速度+零偏)(§39.4) |
gtsam::BetweenFactor<Pose3> |
GTSAM | 相对位姿约束因子(里程计/回环)(§39.4) |
gtsam::PriorFactor<T> |
GTSAM | 先验因子,锚定变量消除规范自由度(§39.4) |
gtsam::GPSFactor |
GTSAM | 只约束平移的 GPS 绝对位置因子(§39.4) |
gtsam::Symbol / X(i),V(i),B(i) |
GTSAM | 带类型前缀的变量键,避免下标冲突(§39.4) |
ceres::Problem::AddResidualBlock |
Ceres | 添加残差块(可配鲁棒核)(§39.4) |
ceres::HuberLoss |
Ceres | Huber 鲁棒核,压制外点(§39.4) |
ceres::LocalParameterization / Manifold |
Ceres | 位姿在 SE(3) 流形上更新(§39.4) |
gtsam::noiseModel::Diagonal::Sigmas |
GTSAM | 用标准差构造对角噪声模型(信息矩阵)(§39.4) |
累积项目:MiniLIVO——一个可扩展的多传感器融合框架¶
本章的累积项目模块名为 MiniLIVO,目标是从零搭一个**架构清晰、可逐章扩展**的多传感器融合框架。它不追求 SOTA 精度,而追求让你亲手实现本章每个架构概念,把"读懂别人的系统"变成"自己能搭一个"。代码保存在独立目录 cumulative_project/minilivo/。
本章新增模块。在前面章节(卡尔曼滤波、因子图、李群)累积的基础上,本章给 MiniLIVO 加四个模块,对应本章四个核心知识点:
| 模块 | 对应小节 | 实现内容 | 验收标准 |
|---|---|---|---|
sensor_sync/ |
§39.3 | IMU 缓冲 + 时间戳插值(SLERP)+ LiDAR 去畸变 | 给定乱序时间戳能正确对齐;旋转插值用 SLERP 不用线性 |
coupling/ |
§39.2 | 一个松耦合(双估计器+共享状态)和一个紧耦合(ESIKF 顺序更新)的可切换后端 | 同一份数据两种后端都能跑,输出可对比 |
factor_graph/ |
§39.4 | 基于 GTSAM 的因子图后端,支持 IMU/里程计/回环/先验因子 | 加回环后整条轨迹被拉直 |
degeneracy/ |
§39.5 | Hessian 特征值退化检测 + 解重映射 | 长走廊数据上前进方向特征值塌缩被检出并响应 |
项目架构设计(呼应本章五问框架)。MiniLIVO 的目录结构本身就是本章架构维度的体现:
cumulative_project/minilivo/
├── sensors/ # §39.1 传感器抽象:统一的测量接口
│ ├── imu.hpp # IMU 测量 + 预积分
│ ├── lidar.hpp # LiDAR 帧 + 去畸变接口
│ └── camera.hpp # 相机帧 + 光度采样
├── sensor_sync/ # §39.3 时空对齐层(本章新增)
│ ├── imu_buffer.hpp # 时间戳排序缓冲 + 插值
│ └── motion_compensate.hpp # LiDAR 逐点去畸变
├── coupling/ # §39.2 融合层次(本章新增)
│ ├── loose_backend.hpp # 双估计器 + 共享状态(带锁)
│ └── tight_backend.hpp # 单 ESIKF 顺序更新
├── factor_graph/ # §39.4 图优化后端(本章新增)
│ └── graph_backend.hpp # GTSAM 因子图 + iSAM2
├── degeneracy/ # §39.5 退化检测(本章新增)
│ └── eigen_monitor.hpp # Hessian 特征值分解 + 解重映射
└── app/
└── run_minilivo.cpp # 主程序:可配置松/紧、滤波/图优化
本章的累积任务(建议动手完成):
-
实现
sensor_sync/imu_buffer.hpp(对应 §39.3 练习 2):一个按时间戳排序的 IMU 缓冲,支持任意t_query的插值(角速度线性、若插姿态用 SLERP),并正确处理"查询时刻早于缓冲/晚于缓冲"的边界。这是整个系统的数据对齐地基。 -
实现
coupling/的两个后端(对应 §39.2):松耦合后端用两个估计器 + 一个带std::mutex保护的共享状态(临界区只拷贝);紧耦合后端用单 ESIKF 顺序更新。让主程序能通过配置切换,在同一份数据上对比两者的精度和耗时——亲手验证 §39.2 的工程权衡。 -
接 GTSAM 实现
factor_graph/graph_backend.hpp(对应 §39.4):把里程计结果作为BetweenFactor、IMU 作为ImuFactor进图,第一帧加PriorFactor锚定,用ISAM2增量优化。制造一个回环,观察误差被均摊、轨迹被拉直——亲手验证因子图相对滤波器的回环优势。 -
实现
degeneracy/eigen_monitor.hpp(对应 §39.5):对配准 Hessian 做SelfAdjointEigenSolver分解,计算退化因子,对退化方向做解重映射。在合成的"长走廊"数据上跑,打印前进方向特征值随走廊变长的塌缩曲线——亲手验证退化是方向性的、可检测的。 -
(贯通题,对应 §39.6 的决策演示) 把 §39.6"室内巡检无人机"决策演示的结论落到 MiniLIVO 的配置上:在
app/run_minilivo.cpp里写一份配置,让它跑成"紧耦合 ESIKF 顺序更新 + 退化方向解重映射"的形态(对应那个决策链的第②④⑤问)。然后改一行配置切到松耦合后端,在同一份合成走廊数据上对比两者在前进方向的漂移。这道题的意义:MiniLIVO 的可切换设计(coupling/两个后端、degeneracy/可开关)本身就是为了让你能把本章任何一条五问决策链"配置出来"并亲手验证——架构决策不再是纸上谈兵,而是改几行配置就能跑出对比的实验。
MiniLIVO 与本章决策框架的对应:MiniLIVO 的四个模块恰好是五问框架中可由代码控制的四问的旋钮——
coupling/对应第②问(松/紧)、sensor_sync/对应第③问(对齐)、factor_graph/对应第④问(滤波/图优化)、degeneracy/对应第⑤问(退化响应)。第①问(传感器选型)是硬件层面的输入、不在代码里调。这意味着:你在 §39.6 为某个场景走完的五问决策链,几乎都能一一映射成 MiniLIVO 的一组配置开关——这正是"读懂系统"到"搭出系统"的最后一公里。累积项目的连贯性:MiniLIVO 不是孤立的——它的 ESIKF 复用「卡尔曼滤波与 ESKF」章节的代码,因子图复用「图优化与因子图」章节的 GTSAM 基础,流形操作(SLERP/boxplus)复用「李群李代数」章节的 SO3/SE3 工具。下一章「回环检测与全局优化」会给它加回环检测模块,再下一章会加多机协同。这条主线一路把零散的算法拼成一个完整系统。
延伸阅读¶
按主题分类,标注难度和推荐理由。带 ⭐ 表示强烈推荐先读。
架构与综述 - ⭐ Cadena et al., "Past, Present, and Future of Simultaneous Localization and Mapping: Towards the Robust-Perception Age", IEEE T-RO 2016——SLAM 领域的权威综述,对前后端、鲁棒性、可观测性有系统论述,本章的"架构观"很大程度源于此。 - Huang, "Visual-Inertial Navigation: A Concise Review", ICRA 2019——视觉惯性融合的简明综述,紧耦合/松耦合、滤波/优化的对比讲得清楚。
松/紧耦合与滤波/图优化(§39.2、§39.4) - ⭐ Qin et al., "VINS-Mono: A Robust and Versatile Monocular Visual-Inertial State Estimator", IEEE T-RO 2018——紧耦合滑窗优化的经典,在线时间标定、外参标定、边缘化的工程实现都在这里。 - Mourikis & Roumeliotis, "A Multi-State Constraint Kalman Filter for Vision-aided Inertial Navigation", ICRA 2007——MSCKF,滤波器派视觉惯性的奠基作。 - Dellaert & Kaess, "Factor Graphs for Robot Perception", Foundations and Trends in Robotics 2017——因子图的系统教材,GTSAM 的理论基础。 - ⭐ Shan et al., "LIO-SAM: Tightly-coupled Lidar Inertial Odometry via Smoothing and Mapping", IROS 2020——本章 §39.6 LVI-SAM 的前身,GTSAM 因子图 LIO 的范本。
时间同步与标定(§39.3) - Furgale et al., "Unified Temporal and Spatial Calibration for Multi-Sensor Systems", IROS 2013——Kalibr 工具箱背后的理论,时空联合标定的标准方法。 - Qin & Shen, "Online Temporal Calibration for Monocular Visual-Inertial Systems", IROS 2018——在线时间标定 \(t_d\) 的具体做法,对应 §39.3 的在线标定。
退化检测(§39.5) - ⭐ Zhang, Kaess & Singh, "On Degeneracy of Optimization-based State Estimation Problems", ICRA 2016——本章 §39.5 的理论来源,退化因子和解重映射的原始论文,必读。 - Hinduja et al., "Degeneracy-Aware Factors with Applications to Underwater SLAM", IROS 2019——退化感知因子在退化环境的应用,把 §39.5 的思想用到因子图。
三个精读系统(§39.6) - ⭐ Shan et al., "LVI-SAM: Tightly-coupled Lidar-Visual-Inertial Odometry via Smoothing and Mapping", ICRA 2021——§39.6 系统一原文。 - ⭐ Lin & Zhang, "R3LIVE: A Robust, Real-time, RGB-colored, LiDAR-Inertial-Visual tightly-coupled state Estimation and mapping package", ICRA 2022——§39.6 系统二原文(注意它的标题自称 tightly-coupled,但其 LIO/VIO 是两个 ESIKF 通过共享状态协作的结构,本章按"两个独立估计器"将其归为松耦合范式;阅读时重点体会几何/辐射分工的设计)。 - ⭐ Zheng et al., "FAST-LIVO2: Fast, Direct LiDAR-Inertial-Visual Odometry", IEEE T-RO 2024——§39.6 系统三原文,统一体素地图 + 顺序更新的集大成。 - Zheng et al., "FAST-LIVO: Fast and Tightly-coupled Sparse-Direct LiDAR-Inertial-Visual Odometry", IROS 2022——FAST-LIVO2 的前身,对比着读能看到架构演进。
关于 R3LIVE 的归类说明:R3LIVE 论文标题用了 "tightly-coupled",指的是 LIO 与 VIO 都紧耦合各自的传感器与 IMU。但从"原始测量是否进同一个估计器"这一本章判据看,它的 LIO 和 VIO 是两个独立 ESIKF(LiDAR 点不进 VIO、像素不进 LIO),通过共享状态协作,因此本章把它作为"松耦合双子系统"的范例来讲。这个看似矛盾之处恰恰是个好的教学点:论文里的"紧/松"措辞和严格的架构判据可能不完全一致,读论文时要回到"原始测量去向"这个本质判据自己判断,不要被标题措辞带跑——这也是 §39.6 陷阱 3 的延伸。
后续章节关系¶
本章在 SLAM 系统精读模块中处于"架构总览"的位置,向前承接基础理论,向后展开各子模块的深入:
本章(第39章)多传感器架构设计
↑ 建立在
┌────────────────────┼────────────────────┐
│ │ │
卡尔曼滤波与ESKF 图优化与因子图 李群李代数
(滤波器后端) (图优化后端) (流形操作)
│ 向后展开
┌────────────────────┼────────────────────┐
▼ ▼ ▼
回环检测与全局优化 多机协同SLAM SLAM工程部署
(给MiniLIVO加回环) (给MiniLIVO加多机) (把MiniLIVO上车)
- 向前依赖:本章大量复用「卡尔曼滤波与 ESKF」(滤波器后端的数学)、「图优化与因子图」(GTSAM 因子图)、「李群李代数」(SLERP、boxplus、\(S^2\) 流形)。如果这些前置概念不牢,建议先回炉。
- 向后展开:
- 「回环检测与全局优化」:本章 §39.4 提到回环作为一条因子边加进图,但"如何检测回环"(视觉词袋、Scan Context)留到下一章。MiniLIVO 的
factor_graph/模块会在那一章接上回环检测。 - 「多机协同 SLAM」:本章是单机架构,多机如何共享地图、对齐坐标系、分布式优化是下一层主题。
- 「SLAM 工程部署」:本章讲架构选择,部署章讲如何把系统跑在真实硬件上——驱动、时间源(PTP/PPS 的实际配置,呼应 §39.3)、算力优化、故障恢复。
- 横向关联:本章的退化检测(§39.5)与「最优估计与可观测性」章节的可观测性分析同源;时间同步(§39.3)与「机器人中间件 ROS2」章节的时间戳与 QoS 设置直接相关。
故障排查手册¶
本节给出多传感器架构调试中最高频的故障场景,按"症状→可能原因→排查步骤→相关章节"的结构组织。遇到问题时先按症状定位,再顺着排查步骤逐条排除。
症状速查索引(先用一句话症状定位到对应场景,再跳过去看详细排查表):
| 你观察到的现象 | 去看 | 主要怀疑方向 |
|---|---|---|
| 低速好、高速/转弯发散 | 场景一 | 时间同步 / 外参旋转 / 去畸变 |
| 长走廊前进方向缩水或乱滑 | 场景二 | 几何退化未检测/未响应 |
| 优化抛异常或结果整体漂移 | 场景三 | 规范自由度 / 流形参数化 / 孤立变量 |
| 偶发崩溃、结果不可复现 | 场景四 | 多线程数据竞争 |
| 加相机后光照变化时被带偏 | 场景五 | 光度仿射补偿缺失 |
| 在线标定值漂移/不收敛 | 场景六 | 运动激励不足 / 协方差未收敛 |
| 地图出现重影/鬼墙 | 场景七 | 漂移累积 / 写图位姿过期 |
场景一:系统低速正常,一到快速运动/转弯就发散¶
| 项目 | 内容 |
|---|---|
| 症状 | 静止和慢速时轨迹平滑正确;快速移动或转弯时位姿剧烈跳动、最终发散 |
| 可能原因 | ① 时间同步差(用了接收时刻而非采集时刻);② 外参旋转标定不准;③ IMU 在帧内的去畸变没做或做错;④ 紧耦合放大了对齐误差 |
| 排查步骤 | 1. 让系统做纯绕单轴的来回旋转,比较 IMU 陀螺曲线和图像光流/LiDAR 推算的角速度曲线,看是否有时间错位(错位量即 \(t_d\));2. 检查传感器时间戳是硬件采集时刻还是 ROS 接收时刻;3. 临时关掉相机只跑纯 LIO,若纯 LIO 高速也正常,问题在视觉对齐/外参;4. 检查 LiDAR 去畸变是否启用、IMU 是否覆盖整帧 |
| 相关章节 | §39.3(时间同步、外参敏感性)、§39.2(紧耦合放大对齐误差) |
场景二:长直走廊/隧道里轨迹在前进方向"缩水"或乱滑¶
| 项目 | 内容 |
|---|---|
| 症状 | 进入长直走廊后,轨迹在前进方向上的长度明显短于真实距离,或前进方向位姿乱跳;垂直方向(左右上下)正常 |
| 可能原因 | ① LiDAR 在前进方向几何退化但系统照样信任配准全部 6 自由度;② 没有退化检测;③ 检测了但没做方向性响应;④ 缺少能观测前进方向的传感器(轮速/视觉特征) |
| 排查步骤 | 1. 在配准后打印 Hessian 的 6 个特征值,看前进方向对应的特征值是否塌缩(远小于其他);2. 确认有无退化检测和解重映射逻辑;3. 检查是否误用残差大小判断退化(退化方向残差恰恰小);4. 考虑引入轮速计/视觉直接法约束前进方向 |
| 相关章节 | §39.5(退化检测、解重映射、方向性响应)、§39.1(LiDAR 失效模式) |
场景三:因子图优化抛 IndeterminantLinearSystemException 或结果整体漂移¶
| 项目 | 内容 |
|---|---|
| 症状 | GTSAM 优化抛 IndeterminantLinearSystemException,或 Ceres 求解不收敛/结果整体平移旋转漂移 |
| 可能原因 | ① 缺少先验因子或固定变量,存在规范自由度(gauge freedom);② 位姿没用流形参数化(LocalParameterization/Manifold);③ 某些变量没有任何因子连接(孤立变量);④ 噪声模型设置不当导致信息矩阵病态 |
| 排查步骤 | 1. 确认第一帧是否加了 PriorFactor 或固定了某变量;2. 检查位姿参数块是否配了 LocalParameterization(Ceres)或用了 Pose3(GTSAM);3. 检查每个变量是否至少被一个因子约束(无孤立节点);4. 打印信息矩阵的条件数,看是否病态;5. 检查噪声 sigma 是否有零或负值 |
| 相关章节 | §39.4(规范自由度、流形参数化、因子图构建) |
场景四:松耦合系统偶发崩溃或结果随机错误且难复现¶
| 项目 | 内容 |
|---|---|
| 症状 | 多线程的松耦合系统偶尔崩溃(段错误),或同一份数据多次运行结果不一致,且无法稳定复现 |
| 可能原因 | ① 共享状态读写没加锁(数据竞争,未定义行为);② 加锁了但读到撕裂状态;③ 持锁期间做耗时计算导致死锁或实时性问题被误判为崩溃;④ 写回时覆盖了更新的状态 |
| 排查步骤 | 1. 用 ThreadSanitizer(-fsanitize=thread)跑,定位数据竞争;2. 检查所有共享状态访问是否都用 std::lock_guard 保护;3. 检查临界区是否只做拷贝、把耗时计算放在锁外;4. 检查写回是否有时间戳/版本判断防止覆盖更新的结果;5. 用 AddressSanitizer 排查越界 |
| 相关章节 | §39.2(松耦合共享状态的数据竞争与持锁过久陷阱) |
场景五:加了相机做视觉融合后,自动曝光场景下位姿被带偏¶
| 项目 | 内容 |
|---|---|
| 症状 | 直接法光度融合系统,在光照变化(进出阴影、对着窗户)触发相机自动曝光时,位姿出现莫名偏移,但环境几何没变 |
| 可能原因 | ① 没做光度仿射补偿,曝光变化导致整体亮度偏移被误认为位姿误差;② 光度残差权重过高,曝光噪声主导了优化;③ 参考图块的曝光信息没记录 |
| 排查步骤 | 1. 关掉相机自动曝光(固定曝光)测试,若问题消失则确认是曝光变化引起;2. 检查光度残差是否做了仿射(增益+偏置)补偿;3. 检查是否记录并使用了参考帧的曝光参数;4. 适当降低光度残差权重或加鲁棒核;5. 对比有/无补偿的轨迹 |
| 相关章节 | §39.6(FAST-LIVO2 光度仿射补偿)、§39.1(相机失效模式) |
场景六:在线外参/时间标定值缓慢漂移或不收敛¶
| 项目 | 内容 |
|---|---|
| 症状 | 打开在线标定后,外参或时间偏移 \(t_d\) 的估计值持续缓慢漂移、卡在错误值,或在不同运行间差异很大 |
| 可能原因 | ① 运动激励不足(匀速直线、纯平移),对应自由度不可观;② 标定参数的协方差没有收敛就被使用;③ 把不该在线估计的量也放进了状态 |
| 排查步骤 | 1. 检查标定阶段的运动是否充分激励所有自由度(旋转外参需要绕各轴转、\(t_d\) 需要加减速);2. 监控标定参数的协方差/对应 Hessian 特征值,确认是否真的收敛;3. 用"先在线标定到收敛、再锁定为常量"的策略;4. 对照 Kalibr 离线标定结果验证在线估计是否合理 |
| 相关章节 | §39.3(在线标定的可观测性、激励要求) |
场景七:地图出现"重影/鬼墙",同一面墙在地图里裂成两层¶
| 项目 | 内容 |
|---|---|
| 症状 | 实时建出的点云/体素地图里,同一个结构(墙、地面)出现明显的双层或错位重影;轨迹本身看起来还算平滑,但地图越建越糊 |
| 可能原因 | ① 里程计有缓慢累积漂移、又没有回环或 frame-to-map 约束去拉回(漂移把新点叠错位置);② 紧耦合里某传感器被坏测量缓慢带偏;③ 地图更新用的是漂移后的位姿;④ 多线程下用了过期的位姿往地图里插点(时间戳没对齐) |
| 排查步骤 | 1. 区分是"轨迹漂移导致"还是"地图写入用错位姿"——对比同一时刻的轨迹位姿和插点用的位姿是否一致;2. 检查是否用 frame-to-map(不累积漂移)还是 frame-to-frame(累积),后者长时间必糊(§39.6 R3LIVE 亮点一);3. 跑一段有回环的数据,看回环修正后重影是否合并,确认是纯漂移问题;4. 检查多线程插点是否用了带正确时间戳的位姿、有无版本判断(呼应场景四);5. 看退化方向是否在缓慢漂移(§39.5)导致沿该方向叠错 |
| 相关章节 | §39.4(回环修正全局一致)、§39.6(frame-to-map vs frame-to-frame)、§39.5(退化方向漂移) |
本章结语:你现在手里有了一套完整的多传感器架构分析工具——五问框架、两个正交维度、退化检测、三个范例系统。下次再遇到任何 SLAM 系统,不要被它的名字和精度数字唬住,用五问框架冷静地拆解它:传感器怎么互补、松还是紧、怎么对齐、滤波还是图优化、怎么处理退化。拆完,你就站在了和系统设计者同样的高度。架构不是玄学,是一连串有迹可循的工程权衡——而你已经掌握了追溯这些权衡的方法。