1. 项目概述
1.1 LVI-SAM是什么,为什么值得复现
LVI-SAM是Tixiao Shan团队在2021年提出的激光-视觉-惯性紧耦合SLAM框架,名字拆开看就是LiDAR-Visual-Inertial Smoothing and Mapping。它在LIO-SAM的基础上加入了视觉因子,在VINS-Mono的基础上加入了激光因子,用因子图把三类传感器观测统一优化。与纯激光方案相比,它解决了长廊、空旷场景的退化问题;与纯视觉方案相比,它扛得住光照突变和弱纹理;再加上IMU做运动先验,动态环境下的鲁棒性也提升了一大截。
我第一次接触这个框架是在一个室内移动机器人项目上。当时用的是16线激光雷达加单目相机加9轴IMU的组合,跑ORB-SLAM3在空旷走廊里频繁丢失,换LIO-SAM虽然定位稳了,但建图边缘模糊,环闭合一触发轨迹就跳变。后来把LVI-SAM拉起来跑了一圈数据集,明显能感觉到它的优势:视觉残差把激光帧间匹配约束得更紧,建图质量提升了一个档次,回环修正也更平滑。
这篇文章就是记录我从零开始复现LVI-SAM到实机部署的全过程,重点解决两个问题:一是在Ubuntu20.04上把源码编译跑通,二是手里只有一个6轴IMU(没有磁力计)的情况下怎么把整个系统调试到能稳定工作。如果你已经在ROS里玩过一段时间,想把手里的传感器组合变成一套完整的紧耦合SLAM系统,这篇文章应该能帮你省下不少时间。
1.2 复现前需要掌握的基础
LVI-SAM不是适合纯新手的入门项目。实机部署前,我建议你先具备这些基础:熟练使用ROS1的topic、launch、tf这些基本操作,能手写简单的launch文件并理解节点间通信关系;对相机内参标定、雷达外参标定、IMU噪声标定这些概念有清晰认知;知道SLAM里的传感器坐标系定义,至少得懂base_link、camera_optical_frame、imu_link这些坐标系绕来绕去的意义。
如果你还不满足这些条件,建议先去把ROS的基础工具链玩熟,再花一两周跑通LIO-SAM和VINS-Mono的公开数据集。我见过不少一上来就编译LVI-SAM的,最后卡在各种标定问题上进退两难——不是代码跑不动,而是参数不对导致整体发散,完全没法判断问题出在哪一环。
2. 环境准备与依赖梳理
2.1 Ubuntu20.04系统与ROS安装
我用的是Ubuntu20.04.6 LTS桌面版,ROS版本为Noetic。系统是双系统安装,没有用虚拟机,因为实机部署需要直接访问USB相机、雷达串口和IMU设备,虚拟机做USB重映射虽然也能用,但偶尔会出现设备掉线、时间戳抖动的问题,排查起来非常头大。
ROS Noetic的安装我直接用了鱼香ROS的一键安装脚本。这个脚本对国内网络环境非常友好,它会自动处理软件源、密钥和依赖包的国内镜像,几分钟就能装完完整的ROS桌面版。如果你手动安装时经常遇到软件源超时或依赖无法定位的问题,用这个脚本能省去很多折腾。装完后记得验证一下环境变量:
source /opt/ros/noetic/setup.bash echo $ROS_DISTRO输出noetic就对了。建议把它写进~/.bashrc,避免每次开终端都要手动source。
2.2 依赖库版本选择与编译顺序
LVI-SAM的核心依赖包括ROS Noetic、OpenCV、PCL、Eigen、Ceres Solver、GTSAM以及livox_ros_driver(如果你用Livox雷达)。这里的版本组合非常关键,我踩过的坑大多发生在版本不匹配上。
我的依赖版本组合是:OpenCV 4.2.0(Ubuntu20.04系统自带版本),PCL 1.10(同样系统自带),Eigen 3.3.7(系统自带),Ceres Solver 1.14.0(源码编译),GTSAM 4.0.2(源码编译)。这个组合与LVI-SAM作者在README中给出的版本一致,编译时很少出幺蛾子。
编译顺序上,先装Eigen和OpenCV这些底层库,再装Ceres和GTSAM,最后编译LVI-SAM工作空间。Ceres依赖Eigen和OpenCV,GTSAM依赖Eigen,所以必须按依赖关系从底往上装。一个常见的错误是装了太新的GTSAM 4.2或4.3版本,LVI-SAM源码里使用的一些API已经被标记为弃用或删除,编译时会报一堆类型错误,而且很难修。老老实实用4.0.2,后面你就知道省了多少事。
2.3 鱼香ROS一键安装脚本的使用心得
用鱼香ROS装完ROS后,我还有几个建议:第一,脚本装完后建议手动运行rosdep update,把rosdep的缓存更新一遍,否则后续编译时rosdep check会卡住;第二,如果要用相机驱动、雷达驱动这些第三方包,建议直接用apt安装系统预编译版本,避免从源码编译时OpenCV版本冲突;第三,脚本带的rosdep和系统apt源是独立的,装额外依赖时要用sudo apt install而不是pip,这样才能保证库文件被系统正确识别。
另外提一句,鱼香ROS的脚本本质上是帮你配置了国内镜像源并批量安装了ROS核心包,它不涉及shut-down-vpn之类的任何网络代理操作,纯粹就是解决软件源访问问题,可以放心使用。
3. LVI-SAM源码获取与编译全流程
3.1 创建工作空间与拉取源码
首先创建工作空间并拉取LVI-SAM源码:
mkdir -p ~/lvi_sam_ws/src cd ~/lvi_sam_ws/src git clone https://github.com/TixiaoShan/LVI-SAM.git这里有个细节:LVI-SAM源码里默认包含livox_ros_driver和gtsam两个子模块,如果你不用Livox雷达,livox_ros_driver可以不编译,但源码结构里它的CMakeLists会影响整个工作空间的编译流程,建议直接保留源码但不要把它加入编译白名单。方法是在lvi_sam_ws/src目录下新建一个CATKIN_IGNORE文件放在livox_ros_driver文件夹里:
touch ~/lvi_sam_ws/src/LVI-SAM/livox_ros_driver/CATKIN_IGNORE这样catkin_make时会自动跳过这个包,不会因为缺少Livox SDK头文件而报错。
3.2 修改CMakeLists.txt的关键坑
LVI-SAM的CMakeLists.txt在默认情况下有两个常见问题:
第一个是OpenCV版本问题。Ubuntu20.04自带的OpenCV 4.2.0没有问题,但如果你机器上装了多个OpenCV版本(比如之前为了跑YOLO装了OpenCV 4.5),CMake可能找到错误的版本。解决办法是在CMakeLists.txt顶部显式指定:
set(OpenCV_DIR /usr/lib/x86_64-linux-gnu/cmake/opencv4) find_package(OpenCV REQUIRED)第二个是GTSAM的CMake配置问题。源码里默认使用find_package(GTSAM REQUIRED),如果你的GTSAM编译时没有开启Quaternion和Eigen的接口,LVI-SAM编译时会报找不到gtsam/quaternion目录下的头文件。我建议编译GTSAM时直接使用默认配置,不要手动修改任何编译选项,默认的Release模式已经包含了LVI-SAM所需的所有特性。
3.3 编译遇到的报错与解决记录
我在编译过程中遇到了几个典型报错,整理在这里供参考:
第一个报错是error: ‘CV_LOAD_IMAGE_GRAYSCALE’ was not declared in this scope。这个是因为OpenCV 4.x删除了旧的CV_前缀宏定义,而LVI-SAM源码是基于OpenCV 3.x写的。解决办法是打开出问题的源文件(一般在utility.h或image_projection.cpp里),把CV_LOAD_IMAGE_GRAYSCALE改成cv::IMREAD_GRAYSCALE,把CV_RGB2GRAY改成cv::COLOR_RGB2GRAY。一共有四处左右,逐个替换即可。
第二个报错是error: ‘imshow’ is not a member of ‘cv’。这个是因为源码里用了OpenCV 3.x的头文件路径#include <opencv2/opencv.hpp>,在OpenCV 4.x下编译时有些函数的命名空间发生了变化。解决办法是检查所有源文件,如果缺少opencv2/highgui/highgui.hpp头文件,手动加进去就行。
第三个报错是链接错误,提示找不到gtsam::ISAM2Params这个符号。解决办法是确认你的GTSAM确实编译成功且版本是4.0.2,然后用ldconfig -p | grep gtsam检查系统能否找到libgtsam.so。如果输出为空,说明GTSAM没有正确安装到系统目录,回到GTSAM源码编译目录重新执行sudo make install。
如果你的代码版本比较新,可能还会遇到视觉特征提取回调函数里OpenCV 4.x与3.x的接口不兼容问题,比如ptr<ORB>()相关的内容,这个在OpenCV 4.2里也是可用的,但如果报错就检查opencv_features2d头文件是否齐全。
3.4 数据集测试验证
编译通过后,先不要急着上实机,用作者提供的数据集验证整个系统是否正常工作。LVI-SAM仓库的README里给出了Google Drive链接,如果访问不方便,也可以在B站和CSDN上找到粉丝分流的百度网盘链接。
数据集下载后,修改config/lvi_sam.yaml里的bag路径,然后运行:
roslaunch lvi_sam lvi_sam.launch rosbag play your_bag_file.bag如果一切正常,你应该能看到Rviz里同时显示激光点云、相机图像和轨迹。这个数据集的传感器配置是Livox Horizon雷达加两目相机加9轴IMU,如果你的传感器组合不同,后续需要修改的地方就多了。但先跑通数据集,确认代码编译和环境配置完全正确,再去处理传感器适配问题,能帮你把问题隔离得更清楚。
4. 实机传感器配置与时间同步
4.1 传感器组合与坐标系定义
我实机使用的传感器组合是:速腾聚创RS-LiDAR-16激光雷达、Intel RealSense D435i相机(带六轴BMI055 IMU)、以及外接的BMI088六轴IMU。这里解释一下为什么相机自带IMU还要外接IMU:第一,D435i内置IMU输出频率最高只有400Hz,且与相机共用USB带宽,实际使用中数据会有抖动;第二,LVI-SAM需要较高频率的IMU数据做运动积分,频率越高,帧间运动的预测越准。
传感器坐标系定义上,LVI-SAM默认的坐标约定是:雷达坐标系为lidar_link,相机坐标系为camera_link,IMU坐标系为imu_link,世界坐标系为map。这里有一个容易混淆的地方:相机坐标系和相机光心坐标系是两个不同的东西。在config/lvi_sam.yaml中,camera_link只用于TF树连接,真正参与视觉投影的是相机内参矩阵定义的相机光心坐标系,也就是camera_optical_frame。习惯上camera_link与camera_optical_frame之间有一个固定的旋转关系,通常是从camera_link绕Z轴旋转180度得到。
4.2 传感器时间同步与驱动配置
LVI-SAM对时间同步的要求非常高。激光雷达每帧点云的时间戳、相机图像的时间戳、IMU数据的时间戳必须对齐到统一的时钟源上。实机部署时,不能依赖电脑系统时间,因为USB设备的驱动延迟会导致毫秒级的随机波动,这个波动在SLAM系统里会直接带来几厘米到十几厘米的定位误差。
我采用的方案是使用硬件时间同步:把IMU和激光雷达的PPS信号接到一起,用单片机统一触发各传感器的数据采集。如果你没有这个条件,退而求其次的办法是使用软件时间同步,确保IMU、相机和雷达的驱动都启用了ROS的TimeSynchronizer机制,并且启动前用chrony或ptp同步系统时间。
具体到D435i的驱动,需要在启动realsense-ros节点时设置unite_imu_method=linear_interpolation,这样IMU数据会通过插值对齐到相机曝光时刻。对于外接BMI088,我写了简单的I2C读取程序,通过ROS驱动发布/imu/data消息,消息头的时间戳使用系统时钟。
有一个容易被忽视的细节:IMU数据的时间戳必须尽可能反映真实的采样时刻,而不是读取设备后的发送时刻。我实测下来,如果直接把读取函数后的系统时间当作采样时间,IMU数据的有效延迟会达到3-5毫秒,这个延迟在LVI-SAM里会导致加速度计和陀螺仪的运动积分产生明显漂移。解决方案是在BMI088驱动里使用设备数据就绪中断的GPIO时间戳,或者至少用DMA传输完成中断打时间戳。
4.3 外参标定:从粗对齐到精标定
传感器外参标定是实机部署中最耗时的一步。我的做法分两步:先用人工测量值做粗标定让系统能跑起来,再用kalibr工具做精标定。
粗标定很简单,用卷尺量出各传感器在机器人坐标系下的位置,加上用水平尺测出的姿态角。例如把雷达安装在机器人正前方30cm处,IMU安装在雷达正下方5cm处,相机安装在雷达右侧10cm处,这些测量值可以直接填入yaml文件。测量值虽然粗糙(误差通常在1-3cm和1-5度),但足以让系统不立即发散,便于后续精标定。
精标定我使用的是kalibr工具,标定相机到IMU的外参。操作流程是:把相机和IMU固定在同一块刚性板上,对着一个带黑白棋盘格的标定板做8字形、平移、旋转等动作,录制约2分钟的rosbag,然后运行kalibr_calibrate_imu_camera。标定完成后,会得到camera_optical_frame到imu_link的旋转矩阵和平移向量。
重点来了:kalibr标定得到的是相机光心坐标系到IMU坐标系的外参,但LVI-SAM的yaml文件里需要的是camera_link到imu_link的外参。因为这个坐标系的差异,直接把kalibr标定结果填入配置是跑不通的。正确做法是把kalibr结果先变换到camera_link坐标系,再把camera_link到imu_link的外参换算出来,这个换算涉及一个固定的旋转偏移(camera_link与camera_optical_frame之间的旋转),通常在欧拉角上表现为绕Z轴旋转180度。很多人在这一步卡了很久,我在后面的6轴IMU适配章节会再详细展开。
5. 6轴IMU适配:从代码逻辑到参数调整
5.1 为什么6轴IMU需要特殊处理
LVI-SAM原生代码支持的是9轴IMU(含磁力计),它对IMU数据的读取和处理在utility.h和imu_factor.h中通过use_imu_9axis参数控制。当你使用6轴IMU(只有加速度计和陀螺仪)时,最直接的影响是缺少磁力计的航向参考,系统的yaw角会缓慢漂移。在平坦地面上短时间工作可能看不出来,但移动机器人长时间运行或者有爬坡、转弯时,yaw漂移会越来越明显。
但更隐蔽的问题在于代码内部对IMU数据的处理方式。LVI-SAM在IMU预积分部分假设IMU数据包含磁力计的三轴磁场值,如果IMU消息没有磁场数据,某些版本会在读取linear_acceleration和angular_velocity之外额外解析magnetic_field字段,导致消息解析出错或预积分值异常。这个问题在官方源码中不一定会触发,但如果你修改过消息类型定义或使用第三方驱动,就会踩到这个坑。
5.2 修改消息类型与数据解析
我的解决方案是修改LVI-SAM源码中对IMU消息的处理逻辑,让它只依赖加速度计和陀螺仪数据。
首先检查utility.h中IMU消息的回调函数:
void imuHandler(const sensor_msgs::Imu::ConstPtr& imuMsg) { // 原始代码可能包含磁力计相关处理 // 6轴IMU适配:直接忽略magnetic_field字段 sensor_msgs::Imu::Ptr imu = boost::make_shared<sensor_msgs::Imu>(*imuMsg); imu->header.stamp = imuMsg->header.stamp; // 只赋值加速度与角速度 ... }如果你使用的驱动在发布IMU消息时linear_acceleration和angular_velocity的协方差矩阵取默认值0,LVI-SAM在预积分时会把这些测量当成完全可信的,但实际IMU的噪声远大于0,这会导致优化结果过度信任IMU测量,最终轨迹出现锯齿状抖动。所以适配6轴IMU的一个重要步骤是正确设置消息协方差。
对于BMI088这类传感器,我推荐从手册获取噪声密度和零偏稳定性指标,然后按以下公式计算协方差:
加速度计噪声密度: 180 ug/√Hz ≈ 1.76e-3 m/s²/√Hz 陀螺仪噪声密度: 0.004 °/s/√Hz ≈ 6.98e-5 rad/s/√Hz假设IMU输出频率为200Hz,预积分频率约等于IMU频率,那么加速度计的协方差约为(1.76e-3)² × 200 ≈ 6.2e-4 m²/s⁴,陀螺仪的协方差约为(6.98e-5)² × 200 ≈ 9.7e-7 rad²/s²。把这两个值分别填入imuMsg->linear_acceleration_covariance和imuMsg->angular_velocity_covariance的矩阵对角线。
5.3 参数文件调整与优化
修改完源码后,重点在config/lvi_sam.yaml里调整以下参数:
imuAccNoise: 加速度计噪声密度,单位是m/s²/√Hz。对于BMI088,我设置为1.8e-3;如果你的IMU是MPU6050这类消费级传感器,噪声密度会大一个量级,建议设置为1.0e-2或更好实际标定。
imuGyrNoise: 陀螺仪噪声密度,单位是rad/s/√Hz。BMI088约7.0e-5;MPU6050约1.0e-3。
imuAccBiasNoise: 加速度计零偏随机游走,建议值3.0e-3 m/s²/√Hz。
imuGyrBiasNoise: 陀螺仪零偏随机游走,建议值4.0e-5 rad/s/√Hz。
这里有两个容易犯的错误。第一个是直接套用数据集里的参数值,数据集用的是高精度工业IMU,噪声指标比消费级IMU好一个量级以上,直接套用会导致IMU权重过高,视觉和激光因子被压制,整体精度不升反降。第二个是忽略偏置噪声的参数,只关注了噪声密度。偏置随机游走如果设得太小,系统会认为IMU零偏恒定不变,但实际开机后温度变化会引起零偏漂移,最终导致定位漂移。
如果你不确定自己的IMU参数,最靠谱的办法是用imu_utils工具标定。这个工具需要一个静止的IMU数据集,录制5-10分钟,然后运行imu_utils计算Allen方差,能同时得到噪声密度和偏置随机游走。我强烈建议你花一下午把这个标定做完,它提供的数据比网上找到的任何参考值都更可靠。
5.4 坐标系对齐与初始偏航角
6轴IMU没有磁力计,系统启动时的初始偏航角只能依赖其他传感器确定。LVI-SAM在启动时默认使用IMU的初始姿态作为世界坐标系的基准,但如果IMU的yaw角与真实航向偏差很大(比如手工放置机器人时没有对准方向),后续的定位轨迹就会整体旋转一个角度。
我这里的处理方法是:在启动LVI-SAM前,先手动获取一个初始偏航角。如果雷达有完整的360度视野,可以把雷达的一帧点云投影到地面平面,用点云的主方向或者预设的墙角方向对齐到map坐标系。或者更简单的方法,在launch文件中使用静态TF先把base_link对齐到map,启动后再通过/set_origin的rqt工具微调。
一个实用技巧是:在6轴IMU适配时,把LVI-SAM的extrinsicRot和extrinsicTrans参数单独设置,不要让系统自动从TF树读取IMU外参。我遇到过TF树的IMU外参因为父坐标系定义为base_link导致级联错误的情况,直接硬编码配置能减少一个容易出错的位置。
6. 实机运行效果与问题排查实录
6.1 第一次实机运行的失败教训
第一次实机运行LVI-SAM,我满怀期待地把所有节点启动起来,结果前5秒点云还正常,第6秒开始漂移,第10秒整个轨迹直接发散到墙外面去了。回看rosbag里的数据,发现IMU数据频率只有80Hz(配置的是200Hz),导致预积分的时间间隔过大,运动预测不准。这暴露了一个问题:BMI088通过I2C读取的实际带宽受限于总线速度,配置的200Hz并没有达到。
解决方案是把IMU驱动改为SPI接口读取,或者降低预积分频率要求。因为LVI-SAM的预积分频率与IMU频率绑定,我最后选择了SPI接口,成功把IMU频率提升到500Hz,用200Hz发布,问题解决。
另一个教训是相机图像分辨率超高带来的性能问题。D435i默认输出1080p图像,视觉特征提取的耗时导致整体帧率掉到10帧以下。解决办法是在realsense-ros启动参数里把rgb_camera.resolution设为640x480,帧率30fps。这个分辨率对VIO来说足够了,太高的分辨率并不会显著提升精度,反而让系统实时性变差。
6.2 常见问题与解决方案速查表
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| 编译报CV_LOAD_IMAGE_GRAYSCALE错误 | OpenCV 4.x移除旧宏定义 | 改为cv::IMREAD_GRAYSCALE |
| 编译报GTSAM符号未定义 | GTSAM版本过新或未正确安装 | 使用4.0.2并重新make install |
| 数据集能跑,实机立即发散 | 外参未标定或时间同步错位 | 先做粗标定,再精标定,检查时间戳 |
| 轨迹整体旋转但形状正确 | 初始偏航角未对齐 | 启动前校正初始航向或设置初始yaw偏移 |
| 轨迹抖动明显 | IMU协方差设置过大或过小 | 用imu_utils标定后填入准确参数 |
| 点云建图边缘模糊 | 激光-视觉外参误差大 | 用Kalibr或lidar_camera_calibration重新标定 |
| 回环触发后轨迹跳变 | 回环检测误匹配或外参错误 | 检查视觉特征匹配,重新标定相机雷达外参 |
| 系统延迟高、帧率低 | 相机图像分辨率过高 | 降到640x480,关闭附加视觉处理 |
6.3 6轴IMU适配后的运行效果对比
适配完成后,我在室内走廊场景和室外园区场景分别测试了LVI-SAM和LIO-SAM的定位效果。
室内场景测试条件:10m×30m走廊,地面平整,墙面有较多窗户和玻璃(对激光雷达有干扰,对视觉纹理有挑战)。LIO-SAM在走廊尽头出现漂移,回环修正后误差约15cm;LVI-SAM在同样条件下定位误差约8cm,轨迹更平滑,建图边缘更清晰。
室外园区测试条件:长直道路、两侧有树木和路灯杆,存在不可忽略的车辆动态物体。LIO-SAM在长直段有轻微漂移;LVI-SAM依赖视觉特征约束,轨迹保持稳定,误差在10cm以内。
一个明显的感受是,6轴IMU适配后整个系统的表现比预想中好很多。因为没有磁力计,yaw角确实会缓慢累积漂移,但这个漂移会被视觉-激光回环不断修正。在我跑的这两组场景里,20分钟的运行时长内没有出现不能容忍的航向误差。但如果你的机器人会在未知环境长时间运行,还是建议在后端定期加入绝对航向参考,比如GPS航向或者视觉罗盘。
6.4 关于yaml文件的参数调优心得
参数调优是最耗时的环节。我的经验是不要一次性改多个参数,否则你无法判断是哪个改动起了作用。每次只改一个参数,跑一遍数据包,对比轨迹误差和建图质量,再调下一个。测试数据包建议录制在固定场景里,每次回放同样的数据,这样对比才有意义。
另外,参数调优时不要盲目照搬别人的数值。不同的传感器指标、安装方式、环境特点都会导致最优参数不同。我建议以官方数据集的参数作为初始值,再根据你的传感器性能指标按比例调整。比如你的IMU噪声密度是数据集配置里的2倍,那IMU相关参数值也应相应放大,再在数据包上逐步微调。
7. 一点体会
我从零开始复现LVI-SAM到在实机上稳定运行,前后花了两周多。最花时间的环节不是编译,是传感器标定和参数适配。如果你也是6轴IMU用户,建议把重点放在外参标定和IMU噪声参数上,这两个环节决定系统能不能收敛。
最后分享一个小技巧:调试过程中我在启动LVI-SAM之前,会先单独跑一遍imu_utils或kalibr生成最新标定结果,再把结果写入配置。每换一次传感器位置,或者每挪动一次IMU的固定方式,都要重新做一次这个流程。传感器装好后被磕碰过、固定螺丝松动过,都可能导致外参改变,而SLAM系统对这种细微变化非常敏感。养成这个习惯后,你的LVI-SAM会少很多莫名其妙的问题。