简介:针对Kitti数据集优化的LIO-SAM项目压缩包,面向自动驾驶、SLAM算法研究与工程落地的开发者及高校学生。LIO-SAM融合激光雷达与IMU实现同时定位与建图,此版本针对Kitti数据格式在数据预处理、点云特征提取、扫描匹配、滤波器参数及回环检测等环节做了适配调整,尤其适合在城区道路场景下验证定位精度与鲁棒性,也能帮助初学者理解多传感器融合SLAM的完整流程。压缩包共44个文件,约76MB,包含C++核心源码、launch启动脚本、参数配置文件、Dockerfile、RViz可视化配置,以及8张gif示意图和8张png截图,可直观呈现运行效果与建图过程,另有许可证、说明文档等辅助材料,目录组织清晰便于定位修改点。目前已有1073人学习下载。读者可获得可直接对接Kitti数据集的工程框架、参数调优思路及可视化演示,配合代码注释与文档可深入理解改进逻辑,为后续迁移到其他激光雷达平台或扩展多传感器方案提供有力参考。
1. KITTI bag跑不动原版LIO-SAM?先弄懂这个zip在改什么
KITTI是SLAM领域最常见的benchmark,但很多从别的项目里拿来的KITTI bag直接喂给原版LIO-SAM,十有八九跑不起来。报错倒是其次,更常见的是话题名对不上、IMU方向反了、GPS时间戳不连续,最终轨迹一开局就飞掉。所谓“为适配kitti数据集修改的lio-sam.zip”,本质上不是某个神秘魔改版本,而是围绕着KITTI数据格式做的一整套翻译工作:把64线Velodyne点云、OXTS惯导/GPS数据,转成LIO-SAM紧耦合后端真正认识的消息流。这篇不讨论下载哪个包,而是把适配KITTI最常见的几条路径讲清楚,你看完能自己改出一版,也能判断网上那些zip值不值得用。
2. KITTI数据结构与原版LIO-SAM之间的三堵墙
2.1 第一堵墙:话题名和消息格式对不上
原版LIO-SAM靠三个核心输入跑起来:激光点云(/points_raw,类型sensor_msgs/PointCloud2)、IMU(/imu_correct,类型sensor_msgs/Imu)、GPS(/gps,类型sensor_msgs/NavSatFix)。而社区常见的KITTI bag里,点云话题往往是/kitti/velo/pointcloud,IMU是/kitti/oxts/imu,GPS是/kitti/oxts/gps/fix;消息类型虽然基本一致,话题名却完全错位。
第一种解法是直接改LIO-SAM源码里的订阅topic名,但我不推荐——你改了原版代码,以后想换别的数据集又得改回来,改来改去容易引入隐性bug。更干净的做法是加一个转发节点,或者直接用bag play的remap参数:
rosbag play kitti_2011_09_26_drive_0002.bag \ /kitti/velo/pointcloud:=/points_raw \ /kitti/oxts/imu:=/imu_correct \ /kitti/oxts/gps/fix:=/gps先跑一把这个命令,能快速验证LIO-SAM能不能转起来。需要注意,remap只改话题名,不校验消息内容和坐标定义。KITTI的OXTS IMU消息虽然是sensor_msgs/Imu格式,但它的坐标系定义和LIO-SAM默认期望并不完全一致,这一步只能解决“能不能收到数据”,不能保证轨迹不飞。
如果bag里的话题名不是上面这种规律,建议先用以下命令把实际话题摸清楚:
rostopic list rostopic info /kitti/velo/pointcloud rostopic hz /kitti/velo/pointcloudrostopic info能显示话题类型,rostopic hz看频率。KITTI Velodyne一般10Hz,OXTS的IMU一般是100Hz。如果hz显示不出来或者频率忽高忽低,说明bag录制有问题,后面时间同步大概率也要翻车。
2.2 第二堵墙:时间戳对齐是去畸变的前提
KITTI bag里的点云和IMU通常不是严格等间隔发布的。Velodyne是旋转扫描式雷达,一帧点云里的每个点其实是在不同时刻采集的;LIO-SAM在预处理时会用IMU数据对点云做去畸变(deskew),把每个点旋转回该点对应的时刻。这个操作强依赖相邻IMU帧的时间戳连续且单调。
常见的坑是:bag里IMU话题存在断档,或者某几个IMU帧的header.stamp比点云帧还老。LIO-SAM内部用imuQue缓存IMU消息,按时间线性插值;一旦时间戳回跳,插值出来的旋转增量就是错的,表现就是点云被拧成麻花,轨迹在某一帧突然跳一下。
在我经手的KITTI适配方案里,几乎都会单独加一个时间同步检查脚本,先看两个话题的时间偏移:
rosrun rosbag rosbag play kitti.bag --pause # 另开终端 rostopic delay /kitti/velo/pointcloud rostopic delay /kitti/oxts/imu如果延迟值的分布明显不在一个量级,或者时间戳跳变超过几十毫秒,就得先重新录bag或者做时间戳重写。一个偷懒的兜底做法是写一个简单的relay节点,订阅KITTI原始话题后重新发布到LIO-SAM的输入话题,同时把时间戳归一化到bag的启动时刻:
#!/usr/bin/env python3 import rospy from sensor_msgs.msg import PointCloud2, Imu pub_pc = rospy.Publisher('/points_raw', PointCloud2, queue_size=10) pub_imu = rospy.Publisher('/imu_correct', Imu, queue_size=100) start_time = None def pc_cb(msg): global start_time if start_time is None: start_time = msg.header.stamp msg.header.stamp = rospy.Time.from_sec((msg.header.stamp - start_time).to_sec()) pub_pc.publish(msg) def imu_cb(msg): global start_time if start_time is None: start_time = msg.header.stamp msg.header.stamp = rospy.Time.from_sec((msg.header.stamp - start_time).to_sec()) pub_imu.publish(msg) rospy.init_node('kitti_relay') rospy.Subscriber('/kitti/velo/pointcloud', PointCloud2, pc_cb) rospy.Subscriber('/kitti/oxts/imu', Imu, imu_cb) rospy.spin()这段代码的逻辑很简单:以第一次收到的消息时间作为零点,后续所有时间戳都减去这个偏移。注意这里没有做频率控制,如果你的bag里IMU有丢帧,relay不会自动补帧;更稳妥的做法是在转发前检查相邻IMU时间差,超过阈值就丢弃对应的点云帧,避免错误去畸变。很多适配zip里的时间同步改动,核心逻辑就是这几十行,只是加上了一些缓存和阈值判断。
2.3 第三堵墙:KITTI的坐标系需要旋转到LIO-SAM的期望
KITTI的OXTS惯导数据以车体坐标系为参照,而LIO-SAM的IMU预积分模块默认IMU坐标系为z轴朝上、x轴朝前。如果bag发布出来的IMU四元数直接到达LIO-SAM,轨迹通常不会立刻发散,但在转弯时会表现出明显的尺度漂移和偏航误差。
我见过不止一个改好的zip,最关键的改动其实是把OXTS的IMU消息做了一次坐标系变换。常见的做法是绕x轴旋转180度,把NED系的z轴朝下翻成z轴朝上(ENU)。如果KITTI bag里的IMU四元数已经由录制者转成了ROS标准系,那就不需要再转;但大多数直接把OXTS原始消息塞进bag的版本都是NED,所以在转发节点里加一个旋转非常必要。可以先用rqt_tf_tree看/imu_link到/base_link的坐标关系,再决定要不要在relay里做旋转。
3. 把KITTI的IMU/GPS翻译成LIO-SAM认识的输入
3.1 写一个IMU适配节点,别在源码里硬改
确认KITTI的IMU坐标系之后,下一步就是把/kitti/oxts/imu转换为LIO-SAM能吃的/imu_correct。如果坐标系不需要调整,一个纯转发就够;如果需要调整,就得在转发时处理四元数。这里给一个简单的旋转框架:
#!/usr/bin/env python3 import rospy from sensor_msgs.msg import Imu from tf.transformations import quaternion_multiply, quaternion_about_axis # 绕x轴旋转180度:NED -> ENU ROT = quaternion_about_axis(3.141592653589793, (1, 0, 0)) def imu_cb(msg): msg.orientation.x, msg.orientation.y, msg.orientation.z, msg.orientation.w = \ quaternion_multiply(ROT, (msg.orientation.x, msg.orientation.y, msg.orientation.z, msg.orientation.w)) msg.header.frame_id = "imu_correct" pub.publish(msg) rospy.init_node('imu_adapter') rospy.Subscriber('/kitti/oxts/imu', Imu, imu_cb) pub = rospy.Publisher('/imu_correct', Imu, queue_size=100) rospy.spin()这里用quaternion_multiply把NED四元数左乘一个绕x轴的180度旋转。如果KITTI bag里的IMU加速度和角速度也需要同步调整符号,就在消息里同时把linear_acceleration和angular_velocity的对应轴取反。这个操作听起来简单,但非常容易漏;漏掉的后果是LIO-SAM的IMU预积分结果与真实运动相反,点云去畸变后地图出现重影。
一个实用的验证方法:原地旋转激光雷达(或者播放一段车原地转向的bag),用rqt_plot看/imu_correct/angular_velocity.z与偏航角变化方向是否一致。方向反了立即就能看出来。
3.2 GPS数据的处理方式决定长序列闭不闭合
GPS在LIO-SAM里不是用来做全局定位的,而是作为因子图中的一类约束,抑制长距离漂移。原版LIO-SAM订阅的是sensor_msgs/NavSatFix,内部会把经纬高通过UTM转换到局部笛卡尔坐标。KITTI的/kitti/oxts/gps/fix正好也是sensor_msgs/NavSatFix,转发就行:
rosrun topic_tools relay /kitti/oxts/gps/fix /gps但要注意,LIO-SAM的GPS因子默认只在GPS数据可用时添加,而且它需要GPS话题在时间上与点云/IMU大致对齐。KITTI的oxts里同时包含GPS和IMU,时间戳基本同步,直接relay通常可用。如果希望降低GPS异常值对后端的影响,可以在转发时对GPS协方差做修改,把position_covariance适当调大:
msg.position_covariance[0] = msg.position_covariance[0] * 10.0 msg.position_covariance[7] = msg.position_covariance[7] * 10.0 msg.position_covariance[14] = msg.position_covariance[14] * 10.0把协方差调大,相当于告诉后端“这批GPS我信得没那么死”。在KITTI的城区序列里,GPS偶尔会被高楼反射干扰出几米的跳变;如果保持默认协方差,跳变会把整条轨迹往错误方向拽一下,且回环检测也不一定救得回来。我的习惯是第一遍跑KITTI序列先关掉GPS因子,跑通后再打开,两个版本做对比,能看到GPS到底起了正作用还是副作用。
4. 参数文件里的5个关键设置,改不对等于白改
4.1 先核对雷达线数和scan period
KITTI用的是Velodyne HDL-64E,64线。LIO-SAM的params.yaml里需要明确N_SCAN和Horizon_SCAN这两个与雷达结构强相关的参数。常见配置是N_SCAN: 64、Horizon_SCAN: 1800;如果把这两项设成16线激光雷达的配置(比如N_SCAN: 16、Horizon_SCAN: 1800),特征提取会直接在投影环节错乱,map会变成一团乱麻。
另一个容易被忽略的参数是scanPeriod,代表一帧点云的扫描周期,Velodyne 10Hz对应0.1秒。LIO-SAM在去畸变时用这个值推算点云里每个点相对于帧首时刻的时间偏移。如果bag里的点云实际是5Hz或20Hz,而scanPeriod还是0.1,去畸变就会用错误的旋转量去补偿每个点,动态场景下会出现点云“拖尾”。我看过有人跑KITTI轨迹精度死活到不了2米,最后发现是bag录制时降了频,但参数没跟着改。
4.2 IMU噪声参数:先保持默认,等轨迹飞了再动
imuAccNoise、imuGyrNoise、imuAccBiasNoise、imuGyrBiasNoise这几个参数控制IMU预积分的噪声模型。很多适配KITTI的zip把这几个数改得五花八门,但绝大部分情况下默认值就能工作。IMU噪声参数定得太小,会让后端过于信任IMU积分,一旦有未建模的偏差,轨迹会僵硬地出错;定得太大,IMU约束又形同虚设,长序列漂移加重。
我的建议是先原样跑一遍,短序列(比如KITTI 00的前1000帧)看APE;如果轨迹整体平滑、没有突然抖动,就先不碰噪声参数。改这些参数的收益远低于改外参和时间同步的收益,属于“最后才调的旋钮”。
4.3 外参矩阵:KITTI下先设单位阵,但别真的无脑设
LIO-SAM的extrinsicTrans和extrinsicRot描述LiDAR到IMU的外参。KITTI的Velodyne和OXTS都安装在车顶,两者相对位置接近,但官方没有给出精确到毫米的LiDAR-IMU外参标定表,因此大量适配方案直接设为单位阵。单位阵在短序列下通常跑得动,但在长序列上会产生恒定的小角度误差,最后表现为整体轨迹旋转漂移。
我一般会把这个误差留给IMU bias和回环去吸收,如果效果不够,就手写一份粗略外参:量出KITTI设备架上雷达与IMU的物理距离(大约cm级),填进extrinsicTrans。注意这个矩阵是“从IMU到LiDAR”还是“从LiDAR到IMU”,不同版本的LIO-SAM注释不一样,改之前先读代码注释,别照抄一个网上参数就完事,方向反了轨迹会分裂成两半。
4.4 launch里的remap配置,写成显式节点最稳
原版LIO-SAM的launch内部结构在不同分支间有差异,直接修改它的launch文件容易踩到版本雷。更稳的做法是不要动原launch,而是用一个独立launch或者bash脚本,先起自己的适配节点,再起LIO-SAM:
roslaunch lio_sam run.launch & sleep 3 rosrun topic_tools relay /kitti/velo/pointcloud /points_raw & rosrun topic_tools relay /kitti/oxts/imu /imu_correct & rosrun topic_tools relay /kitti/oxts/gps/fix /gps &这种做法的好处是:适配层与LIO-SAM本体解耦,以后换数据集只需要换relay层。缺点是topic_tools relay不会做坐标变换,如果IMU方向不对,还是要写前面的Python适配节点。很多zip里所谓的“修改”,本质就是这样一组relay加一个坐标转换脚本,摸清结构后你自己也能拼出来。
4.5 关键参数速查表
| 参数名 | 作用 | KITTI下的建议值 | 注意事项 |
|---|---|---|---|
N_SCAN | 雷达线数 | 64 | 改成16会直接崩溃 |
Horizon_SCAN | 水平采样数 | 1800 | 与Velodyne 64线出厂配置一致 |
scanPeriod | 一帧扫描周期 | 0.1 | 必须与bag实际帧率一致 |
voxelFilterLeafSize | 体素降采样大小 | 0.5 | 太小特征过多,太大匹配退化 |
useGps | 是否启用GPS因子 | false/true | 先关掉跑一版再开 |
imuAccNoise | IMU加速度噪声 | 保持默认 | 轨迹稳定就别乱调 |
extrinsicRot/Trans | LiDAR-IMU外参 | 单位阵或物理测量值 | 方向别搞反 |
5. 避坑:KITTI跑LIO-SAM最常见的五个翻车现场
5.1 起步1秒内轨迹直接飞掉,十有八九是IMU方向反了
现象:rosbag一播放,LIO-SAM输出的odometry在起始点原地乱转,或者直接往天上飞,地图变成一片散乱点云。
原因:KITTI的OXTS IMU原始坐标系与LIO-SAM期望的ENU坐标系不一致,四元数方向反了。IMU预积分在方向错误时会累积出荒谬的旋转,特征匹配从一开始就找不到对应关系。
解决:用第3章里的坐标转换节点,把IMU四元数绕x轴旋转180度后再发布。做完后播放一小段bag,看/imu_correct/angular_velocity.z在车辆左转时是否为正值,右转时是否为负值;方向正确再跑完整序列。
5.2 时间戳回绕导致点云去畸变把地图拧成麻花
现象:轨迹在某一帧之后出现明显锯齿,点云地图边缘出现“拖尾”或者整块点云扭曲,跑得越久越严重。
原因:bag录制时点云和IMU的时间基准不统一,relay后时间戳出现跳变。LIO-SAM用IMU缓存按时间插值去畸变,一旦时间戳回跳,插值结果就是错的,后续匹配全部受影响。
解决:先用rostopic delay检查两个话题的时间戳分布。如果跳变严重,在relay节点里做时间单调性检查——发现后一帧IMU时间戳小于等于前一帧,直接丢弃该帧;如果点云帧时间戳落不到IMU时间范围内,丢掉这一帧点云,别硬塞给LIO-SAM。这个“宁缺毋滥”的策略能避免大部分去畸变异常。
5.3 开着GPS因子,轨迹在过桥/隧道时被拽出一条分叉
现象:全局轨迹看起来还行,但某一小段突然偏离道路,然后过一会儿又拉回来,APE在这段区域飙到几十米。
原因:KITTI序列中GPS在桥下、高楼密集区出现跳变,而GPS因子以固定协方差加入优化,后端会误认为这个错误观测是真实位置,强行把轨迹拽过去。
解决:第一遍先关掉useGps跑,确认纯LiDAR-IMU的轨迹在局部路段是否正常。如果在遮挡路段依然漂移,再打开GPS因子,并把GPS协方差调大一两个数量级,让后端对GPS的信任度降低。更好的做法是在适配节点里检测GPS定位精度字段(position_covariance_type),把精度差的GPS帧直接丢弃。
5.4 回环检测抽风:同一段路走了两遍却对不齐
现象:KITTI序列里车绕一圈回到原点附近,地图上同一面墙出现两层,回环检测没有触发或触发了但没消掉偏差。
原因:回环检测需要关键帧之间的距离和角度差小于阈值;如果点云降采样过狠,或者雷达视野内重复场景的特征太少,匹配分数不够,回环就不会被接受。
解决:把voxelFilterLeafSize从0.5降到0.3试试,特征密度上去之后回环检测会积极很多。同时检查loopClosureDistance阈值,KITTI的重复路径经常相隔几十米到上百米,阈值设得太小会错过有效回环。这个参数我在实际项目里调整频率最高。
5.5 轨迹精度看起来很漂亮,但地图是双层的
现象:用evo算APE只有几十厘米,但打开点云地图后看到同一个物体有两层重影,前后相差10~30厘米。
原因:APE衡量的是轨迹在时间对齐后的位置误差,它不代表建图质量。LIO-SAM的位姿估计在小范围内可能很准,但全局一致性问题要靠回环和因子图优化解决;如果回环没形成,map就会分层。
解决:建图完成后,用KITTI官方ground truth里程计文件做全局评估,重点看长距离闭合处的误差,而不仅仅是平均APE。如果地图分层明显,优先检查回环是否触发,而不是调IMU噪声。
6. 别只看轨迹漂不漂:用evo把KITTI ground truth和LIO-SAM输出做比较
KITTI数据集最方便的一点是提供了odometry benchmark的ground truth位姿文件(4x4齐次矩阵,一帧一行)。LIO-SAM跑完后,在输出目录下会保存一条估计轨迹(常见是TUM格式的t x y z qx qy qz qw)。要判断适配效果好坏,不能只盯着rviz里的轨迹看,最好用evo把两者对齐后算指标。
如果LIO-SAM只发布了里程计话题,没有保存TUM格式的轨迹文件,可以用rostopic echo抓取然后转格式:
rostopic echo -p /lio_sam/mapping/odometry > odom_raw.csv这个命令输出CSV,包含时间戳、位置和四元数,还需要裁掉第一列再转成TUM格式。更省事的方式是在launch里打开savePCD或相关保存选项,让LIO-SAM自己导出轨迹文件。拿到估计轨迹后,还需要把KITTI的ground truth也转成TUM或KITTI格式,evo才能比较。一个小脚本就能完成:
import numpy as np def kitti_to_tum(kitti_path, tum_path): with open(kitti_path) as f, open(tum_path, 'w') as out: for i, line in enumerate(f): data = list(map(float, line.strip().split())) pose = np.array(data).reshape(3, 4) tx, ty, tz = pose[0, 3], pose[1, 3], pose[2, 3] # 旋转矩阵转四元数,略 out.write(f"{i} {tx} {ty} {tz} 0 0 0 1\n")转换时注意两点:KITTI的ground truth是左相机坐标系,而LIO-SAM输出是IMU或LiDAR坐标系;两者之间有外参偏移。如果直接在原坐标系下比较,APE会包含一个恒定偏差。常见做法是先做SE(3)对齐,evo的-a或-s参数能自动计算最优刚体变换,正好能把坐标系差异吸收掉。
对比命令通常这样写:
evo_ape kitti gt_kitti.txt est_kitti.txt -r full -a -p --plot_mode=xz evo_rpe kitti gt_kitti.txt est_kitti.txt -d 100 -u m -p --plot_mode=xz-r full表示完整刚体变换误差,-a是自动对齐,--plot_mode=xz适合KITTI这种平面行驶数据集。evo_rpe里的-d 100表示按每100米分段计算相对位姿误差,比只看整体APE更能反映里程计的退化路段。
我的个人习惯是:适配改完之后,先取KITTI某个序列的前500帧做快速验证,导出轨迹和ground truth比较,如果起点附近几十米内APE超过0.5米,就不急着跑全长,先回头查时间戳和IMU方向。调参时一次只动一个参数,记录下来它对APE和RPE的影响,否则改到后面就说不清是哪个参数让轨迹变好的。KITTI这套组合拳打完之后再上自己的数据集,心里就有底了。希望帮到你。
本文还有配套的精品资源,点击获取