我刚拿到fast-livo2代码的那段时间,大部分精力其实都没花在「跑通」上,而是花在「拆开、改掉、再缝合」上面。这套系统的定位是LiDAR-Inertial-Visual紧耦合里程计,很多朋友把它当作一个开箱即用的定位工具,但真正做传感器融合项目的人,最后一定会走到「改源码」这一步——要么是换雷达、换相机,要么是想往点云里塞进自己的语义信息,要么是嫌回环模块不够灵活。这篇修改笔记就是我折腾完一轮之后的记录,从目录结构到改动点,从编译坑到验证方法,尽量把「怎么想的」和「怎么改的」都写清楚。
开写之前先说结论:fast-livo2的核心价值不在某一种传感器上,而在于它把异步的视觉、激光、惯导数据收进同一个状态估计器里,且计算代价控制得相当低。正因为它把模块边界画得比较清晰,修改空间其实比想象中大。这篇笔记适合准备深度定制这套系统的开发者,或者已经在改、但被某些隐蔽问题卡住的人。
1. 我为什么盯上fast-livo2,以及改了哪些地方
1.1 从「能用」到「想改」之间到底隔着什么
用fast-livo2跑公开数据集很快,但真实项目里几乎不可能直接用默认配置。最常见的几个痛点:第一,外参标定方式和自己的传感器组合对不上,雷达坐标系、IMU坐标系、相机坐标系之间的变换关系在配置里改来改去总感觉不透明;第二,动态场景下点云里人来人往,默认的配准会把移动物体当作静态环境去匹配,地图慢慢就花了;第三,想往里加一个自己的语义分割模块,却不知道特征点应该在哪里接进去。
这套系统把激光-惯性做了紧耦合,又把视觉以点线特征的方式并入同一个优化框架。相比上一代方案,它在视觉部分用了更紧凑的残差形式,计算量小了很多,这也是我最终选择在它上面动手的原因。改代码之前我最关心的其实是「数据流在哪一步可以被截住、替换、注入新信息」,搞清楚了这个,剩下的修改就是按图索骥。
1.2 我实际的修改清单
我给自己定的修改目标不大,但覆盖了从工程适配到算法层的几个层次:
| 修改方向 | 具体内容 | 改动级别 |
|---|---|---|
| 外参管理系统 | 把雷达-IMU-相机的相对位姿从写死的yaml提取成可运行时加载的结构体,并加入简单的校验逻辑 | 工程级 |
| 点云预处理 | 加入按频率抽帧的逻辑,控制scan-to-map匹配的输入节奏,避免CPU长时间跑满 | 工程级 |
| 语义信息注入 | 把分割网络输出的语义标签挂到特征点上,动态物体在配准时做降权处理 | 算法级 |
| 编译与依赖 | 处理OpenCV版本、PCL版本、ROS版本之间的兼容问题,固化一套可复现的构建流程 | 环境级 |
这个清单不算激进,但足以让一套「自动驾驶Demo级」的定位系统在真实机器人平台上稳定跑起来。改完之后回头看,真正的分水岭不在算法理解,而在「从哪里切一刀最安全」。
2. 源码目录结构与核心数据流,改码前必须摸清的骨架
2.1 先别急着改,把目录当「地图」读一遍
fast-livo2的代码结构是典型的模块化ROS工程,src下面按功能拆成多个包。我改码之前花了大概一个下午,把每个包的作用和依赖关系捋了一遍。核心的几个包分别负责:前端点云处理与状态递推、视觉特征跟踪与残差构建、地图管理与体素哈希、以及把三者对接起来的融合估计器。
我看源码有个习惯——先找每个模块里的头文件,把头文件里的类成员变量当作「这个模块到底握着哪些状态」的索引。fast-livo2里最值得读的头文件是那几个定义状态向量和特征结构的,因为紧耦合系统的核心就是把不同传感器的观测量表达成同一个状态向量的残差。理解了成员变量,基本就理解了整个系统的一半。
2.2 数据流的主干道:一帧点云和一张图是怎么走完全程的
系统运行时大致有三条并行链路:
- 雷达链路:原始点云经过运动补偿后,提取局部特征,送到scan-to-map匹配模块,产出配准结果并更新状态;
- 视觉链路:图像帧提取点、线特征,通过与地图中的特征关联,构建视觉残差,参与状态估计;
- 惯性链路:IMU数据以较高频率做状态递推,为雷达和视觉提供预测位姿,同时负责帧间的运动先验。
这三条链路不是简单的「分别处理再加权平均」,而是在一个迭代优化框架里交替更新。雷达与视觉各自产生的残差,会基于不确定性加权后进入同一份状态量。所以改任何一路的观测模型,都要想清楚它对另外两路的影响。
2.3 这些代码为什么「忌讳乱改」
因为紧耦合系统里没有绝对独立的模块。如果你只是想让某个输出更平滑,直接改输出层的滤波参数是安全的;但如果你想改配准的权重,就得顺着残差项一路查到状态更新。很多刚上手的人把一处看起来「孤立」的代码改完,发现地图开始漂,就是这个原因——改的点其实在半路上被别的模块引用了。
我修改前做的第一件事是把每个核心文件的引用关系画出来,标注出「谁调用了谁」。虽然不是正式文档,但后续所有改动都能快速定位到影响面。
3. 最容易下手的修改点:外参、帧率、坐标系
3.1 外参修改的「正确姿势」:从写死到可配置
fast-livo2默认把外参写在配置yaml中,但代码里读取方式比较分散。以雷达-IMU外参为例,它本质是一个4x4齐次变换矩阵T_imu_lidar,表示「IMU坐标系下的点,在雷达坐标系里的坐标」。这个矩阵在代码里出现在点云预处理和状态递推两处,如果两处读取的不是同一个数据源,就会出现「旋转对了但平移不对」的奇怪现象。
我改的方式很朴素:在配置读取阶段创建一个外参管理类,一次性读入所有外参,做一次齐次矩阵的合法性校验(旋转部分的行列式是否为1、平移部分是否有明显异常值),然后通过常引用传给所有需要的模块。这样后续换传感器只需要改一处配置,不需要再翻代码。给大家一个参考格式:
lidar_to_imu: rotation: w: 0.0034 x: 0.0001 y: 0.0001 z: 0.0012 translation: x: 0.032 y: 0.001 z: 0.018注意四元数一定要归一化,很多诡异漂移问题究其根源就是四元数没归一化。我见过有人把旋转写成欧拉角,转换完忘了转回四元数,结果系统十分钟后彻底发散。
3.2 帧率控制:别让CPU在空转里烧掉
默认情况下,雷达帧率多高,前端处理就跑多快。但很多雷达实际输出的是10Hz点云,而IMU是200Hz,视觉是30Hz。如果点云帧率不稳定,scan-to-map匹配就可能出现积压。
我在预处理模块加了一个简单的帧率闸门:记录上一帧被送入配准模块的时间戳,如果距当前帧小于设定间隔就直接丢弃。这个方法很土,但有效。对于室内低速机器人,我把点云匹配频率限制在5Hz,里程计精度几乎没有下降,CPU占用却降了将近25%。
另一个容易忽略的参数是点云降采样分辨率。这个参数设置得过小,特征点数量爆炸;设置得过大,地图变得模糊。我调试时一般先设成0.3米,再根据地图密度微调。
3.3 坐标系变换的逻辑,搞错一个符号全盘皆输
fast-livo2的坐标系约定是常见的ROS标准:雷达坐标系为前左上(或前右下),具体以配置为准。但视觉特征的深度方向、IMU加速度的方向,三者如果不在同一个约定下,融合出来的姿态就是歪的。
我最常踩的坑是「视觉特征初始深度方向搞反」。视觉特征只有一个方向向量,深度是未知的,系统通过多帧三角化来估计。如果方向向量变换时少乘了一个旋转矩阵,那三角化出来的深度可能是负的,特征会跑到相机后面去。这类问题在RViz里看特别明显——特征点云和雷达点云明明是同一个场景,却一个正向一个反向。
要验证坐标系没问题,最直接的办法是把雷达点云和视觉稀疏特征同时显示在RViz里,叠加到地图上,看两个来源是否对齐。如果整体旋转偏差很明显,优先检查外参矩阵的手性是否一致。
4. 修改观测模型:语义感知与动态物体过滤这类深度改动怎么入手
4.1 给特征点贴上语义标签,从哪一步介入最合适
如果想要让定位系统对动态物体更鲁棒,直接删点是最简单但危险的做法。因为你删除的可能是本该参与配准的静态特征,比如行人手里的购物袋,它确实是动态的,但旁边墙上的纹理特征是静态的。
我的做法是在特征提取之后、送入配准之前,插入一个语义标签挂载环节。具体来说,把分割网络输出的逐像素标签转换到点云坐标系,通过时间戳对齐找到每个特征点在图像上的投影位置,然后把标签写到特征点的附加字段里。这样配准模块在构建残差时,遇到「人」「车」这类动态标签,可以降低权重而不是完全丢弃。
4.2 动态物体「降权」和「剔除」的真实取舍
我一开始用的是硬剔除,结果在某些场景下地图出现空洞。后来改成降权:动态标签的特征点依然参与匹配,但残差权重下调到原来的0.2左右。效果很微妙——动态物体不再是主导,但静态部分的信息仍然被利用。
这里的关键是对「语义置信度」的处理。分割网络的输出不可能100%准确,如果一个静态物体被误标成「人」,硬剔除就等于把好特征扔了。降权则给了系统一个容错空间,误标只影响权重,不至于造成灾难性的信息丢失。实现上只需要在残差函数里给坐标点添加一个权重因子,改动量大概几十行。
4.3 一个极具迷惑性的「隐形错误」:时间戳不同步
加入语义信息后,我遇到一个很隐蔽的问题:视觉特征和语义标签在时间上不对齐。图像帧率30Hz,点云帧率10Hz,两张图像之间点云已经扫描完了一圈。如果直接用最近时间戳的图像标签去贴点云特征,某些快速运动的物体会出现「标签拖影」——本来应该是「人」的点,贴上了一秒前「墙」的标签。
解决办法是按积分时间对齐:把点云的采集起止时间与图像的时间戳求交集,在交集内的图像帧才参与标签投影。虽然增加了很小的时间开销,但结果的可信度提升是质的。
5. 编译环境折腾记录:版本不匹配才是修改路上最大的坑
5.1 依赖版本选择题:OpenCV 3还是4,PCL要哪个小版本
fast-livo2对依赖版本的敏感度比一般项目高很多,尤其是OpenCV和PCL。项目大部分核心逻辑在PCL的点云类型和OpenCV的矩阵运算上,这两个库的大版本差异会直接导致编译失败,而且报错信息往往看不懂。
我自己的经验是:能跟ROS发行版配套的版本优先。比如系统自带的是OpenCV 4.x,就别为了旧惯性强行降级到3.x,除非你能忍受一堆deprecated警告。PCL方面,1.10和1.12在一些头文件的路径上有差异,如果编译时报找不到某个头文件,检查一下是不是版本路径变了。
5.2 catkin build的坑与我的「一条龙」构建脚本
fast-livo2用catkin tools构建。我第一次编译时遇到的问题是某些依赖包没有按顺序构建,导致链接时找不到符号。后来我写了一个构建脚本,先强制构建核心依赖包,再构建主工程:
catkin build --force-cmake catkin build fast_livo2_core如果出现undefined reference之类的链接报错,先检查target_link_libraries里是不是漏了库。在我遇到的报错里,十个有八个是漏链PCL或OpenCV模块,而不是算法问题。
5.3 内存不够、死机、编译超时的应急方案
fast-livo2整体编译属于中等偏重,我在一台16G内存的老机器上编译时被OOM杀掉过好几次。后来我限制了并行编译的线程数:
catkin build -j4这个方法虽然慢一点,但稳定。还有一个技巧是单独编译某个包时,先用--no-deps跳过依赖包的重复编译,能省不少时间。编译环境这块我没用什么高级方案,就是「先稳定,再提速」。
6. 修改之后的调试方法论:怎么确认改对了,而不是改坏了
6.1 从「结果反推」的验证顺序
改完代码的第一反应不该是「跑一遍看看」,而应该是「看日志、看可视化、看数值」。我调试时有一个固定流程:先打开INFO级日志,确认状态估计器里的协方差数值没有爆炸;然后在RViz里叠加地图和当前帧,看配准是否收敛;最后再用保存的轨迹做精度评估。
这三个步骤分别对应「数值层面」、「几何层面」、「精度层面」。三个层面都通过,改动才算基本可靠。只跑一遍数据集、看到轨迹差不多,很容易被偶然的顺滑轨迹骗过去。
6.2 RViz可视化里怎么「看出」协方差发散前的征兆
协方差发散在RViz里往往表现为地图点出现「拖尾」或「重影」。雷达点云和地图叠加时,如果历史地图边缘出现重叠的双层结构,说明最近几帧的位姿估计出现了偏差。
视觉特征和雷达点云在RViz里分别用不同颜色显示,方便对比。当视觉特征的投影位置和雷达点云的表面明显错位时,优先怀疑时间同步或外参矩阵,而不是算法里的优化器。
6.3 精度评估要怎么看,绝对轨迹误差和相对误差的区别
保存轨迹后,我一般用常见的SLAM评估工具做精度分析。绝对轨迹误差反映的是全局一致性,适合看回环之后的表现;相对误差反映的是局部一小段轨迹的漂移程度,适合评价里程计的短期精度。
我改完外参管理后,绝对轨迹误差从0.3米降到0.1米以下;改完动态物体降权后,相对误差在中速场景下改善了约18%。这些数字都来自实测对比,不是感觉。修改之前一定先保存一份baseline轨迹,否则改完没对比,很难判断是变好还是变坏。
7. 关于机制级修改和性能瓶颈的后续思路
7.1 往系统里加回环检测,从哪里接入成本最低
fast-livo2本身没有特别强的全局回环检测,长期运行还是会有累计漂移。如果想加一个回环检测模块,最简单的方式是在现有地图管理模块旁边增加一个「关键帧池」,每隔若干帧存一个关键帧的特征描述子,当新的关键帧与之匹配成功后,把回环残差加入优化框架。
这个改动的难点不在回环检测本身,而在如何把回环残差和现有的雷达/视觉残差统一进同一份状态量。如果直接把回环当作一个「额外位姿约束」,实现会简单很多;如果想让回环参与全局地图修正,牵扯就大了。
7.2 性能瓶颈的直觉判断:先查降采样,再查特征数量,最后查优化迭代次数
很多人一遇到CPU占用高就急着换硬件,其实最有效的调优往往在参数层。第一步检查的是降采样分辨率是否过小;第二步统计特征点的总量,如果一帧里有上万特征点,占用的计算量肯定不小;第三步减少优化器的迭代次数,从默认的10次减到5次,精度损失通常不大,但速度能提升明显。
我在实测中发现,点云降采样从0.2改成0.3,配准时间几乎减少了一半,而轨迹精度只下降了不到5%。这种「性价比很高」的参数改动,远比换一台高配工控机实在。
7.3 最后分享一个我自己总结的调试习惯
改fast-livo2这类紧耦合系统,最重要的是「每次只改一个变量」。我见过很多朋友一次性改外参、改帧率、加语义模块,结果出问题后完全无法定位。我自己现在坚持「单变量修改+基线对比」的实验方法:每次改动前记录当时的轨迹评估结果,改完后跑同一个数据集,对比误差变化。宁可多花几次跑数据的时间,也不要陷入「改了三处却不知道哪处有效」的泥潭。
还有一点,代码注释一定要同步更新。fast-livo2的代码本身不算特别长,但逻辑密度很高,半年后再看自己改的模块,如果没有注释,基本要重新读一遍。我有一次改完外参管理类,自信满满地过了三个月,再回去找某段逻辑时,发现连自己都忘了当时为什么那么写。从那以后,凡是改动过的文件,我都会在头部写清楚「改了什么、为什么改、影响范围」。这点建议对所有改开源项目的人都有效。