☰
KITTI数据集适配LIO-SAM:参数调整与代码修改实战指南
2026/9/25 1:47:42 网站建设 项目流程

简介:这份资源是面向SLAM算法学习者与自动驾驶感知研究者的LIO-SAM修改版工程包,针对KITTI数据集做了专门适配,可解决原始LIO-SAM在KITTI数据格式、传感器同步与城市道路场景下定位漂移等问题。压缩包共44个文件、约76.06MB,包含cpp源码、launch启动文件、conf与yaml参数配置、msg与srv消息定义、rviz可视化配置、xacro模型、Dockerfile及README文档等,覆盖编译、配置、运行与评估全流程。已有1072人学习下载,说明其在激光雷达与IMU融合建图方向具有较高参考价值。读者可据此了解数据预处理、特征提取匹配、滤波器调参、回环检测与点云地图构建等改进细节,并对照KITTI标准序列复现实验、分析定位精度,适合具备ROS与SLAM基础、希望深入理解多传感器融合建图的进阶学习者。

1. 从 KITTI 到 LIO-SAM:为什么原版跑不动、改完才像回事

手里有一份 LIO-SAM 源码,激光雷达用的是 Velodyne,IMU 用的是 9 轴 MEMS,跑官方给的样例数据一切正常。换成 KITTI 的 raw data 或者 odometry 数据,rviz 里轨迹要么直接飘走,要么建出来的图重影严重,甚至启动几秒就报 TF 外推失败。这不是配置写错了,而是 LIO-SAM 从设计之初就是围绕「机械式激光雷达 + 高频 IMU + 紧耦合九轴」这套组合来写的,KITTI 的传感器配置、时间戳体系、外参定义跟它默认假设对不上。

「为适配 KITTI 数据集修改的 LIO-SAM」这件事,本质上是把一套为特定硬件调好的 SLAM 系统,迁移到另一个公开数据集上,让它能稳定输出轨迹和点云地图。做这件事的人通常是三类:想用 KITTI 做算法对比的研究生、想把 LIO-SAM 当基线再改自己模块的工程师、以及拿 KITTI 验证多传感器融合方案的从业者。它解决的不是「LIO-SAM 好不好用」,而是「我手上只有 KITTI,怎么让这套代码跑出可信结果」。下面按我实际改过的顺序,把选型、改参、改代码、排错一条条讲清楚。

2. 先搞清楚 KITTI 和 LIO-SAM 默认假设差在哪

2.1 传感器配置的错位:64 线雷达配单目 IMU 的尴尬

LIO-SAM 默认的传感器模型是:一个多线机械激光雷达(常见 16/32/64 线),一个 9 轴 IMU,两者刚性连接,IMU 频率通常在 100Hz 以上,雷达 10Hz。它依赖 IMU 提供高频姿态先验,用雷达做 scan-to-map 匹配,再用因子图把两者紧耦合起来。

KITTI 的 raw data 里,激光雷达是 Velodyne HDL-64E,10Hz,这个没问题。问题出在 IMU:KITTI 的 IMU 数据来自 GPS/IMU 组合导航系统,频率只有 100Hz 左右,但它的坐标系定义、重力对齐方式、以及和雷达之间的外参,跟 LIO-SAM 默认的extrinsicRot、extrinsicRPY完全不是一回事。更麻烦的是,KITTI odometry 数据集里根本没有 IMU,只有雷达和灰度/彩色图像,很多人拿 odometry 跑 LIO-SAM 时直接卡在「没有 IMU 输入」这一步。

所以第一件事是确认你手上是 KITTI raw 还是 KITTI odometry。raw 有 IMU 但需要做时间同步和外参标定;odometry 没有 IMU,要么补一个虚拟 IMU,要么改用 LIO-SAM 的纯雷达模式(但那样就不是 LIO-SAM 了)。我一般建议用 raw data 里的2011_09_26_drive_0005这类序列做验证,因为它同时有雷达、IMU、GPS,方便对照真值。

2.2 时间戳体系:KITTI 的「秒 + 纳秒」和 ROS 的「秒 + 纳秒」不是一回事

KITTI 的时间戳文件timestamps.txt格式是2011-09-26 13:02:25.724199232,精确到纳秒。ROS 的ros::Time也是秒 + 纳秒,看起来能直接转,但坑在于:KITTI 的雷达和 IMU 时间戳是各自独立采集的,没有硬件同步,两者之间存在几十毫秒的固定偏移。LIO-SAM 的imuPreintegration节点对时间戳非常敏感,偏移超过 10ms 就会导致预积分残差爆炸,表现为轨迹在转弯时突然跳变。

常见做法是先用rosbag把 KITTI 转成 ROS bag,转换时把雷达和 IMU 的时间戳都统一到同一个时钟源(通常是雷达的第一个时间戳),然后手动估计一个timeOffset填进 LIO-SAM 的 IMU 参数里。这个偏移量没有通用值,每个序列都不一样,我一般用rqt_plot把 IMU 角速度和雷达帧间旋转对齐,肉眼估一个初值,再微调。

2.3 外参矩阵:KITTI 的标定文件和 LIO-SAM 的 YAML 对不上

KITTI raw 里每个日期目录下有个calib_imu_to_velo.txt和calib_velo_to_cam.txt,给的是 IMU 到雷达、雷达到相机的变换。LIO-SAM 的params.yaml里需要的是extrinsicRot(IMU 到雷达的旋转)和extrinsicRPY(IMU 到雷达的旋转,但只取偏航角部分)。很多人直接把calib_imu_to_velo.txt的 3x4 矩阵前 3x3 填进去,结果发现符号反了或者轴序不对。

原因是 KITTI 的 IMU 坐标系是右前上(RFU),雷达是右前上但原点不同,而 LIO-SAM 默认的extrinsicRot期望的是「IMU 到雷达」的旋转,且内部还会做一次重力对齐。我实际改的时候,先把calib_imu_to_velo.txt读进来,取旋转部分,然后手动检查:IMU 的 Z 轴是否指向天,雷达的 Z 轴是否指向天,如果不是就补一个轴交换矩阵。这一步没有捷径,只能对着坐标系图一个个试。

2.4 用 Python 把 KITTI 原始数据转成 LIO-SAM 能吃的 ROS bag

下面这段脚本是我常用的转换骨架,把 KITTI raw 的雷达、IMU、GPS 打包成 bag,时间戳统一到雷达起始时刻。

import os import rospy import rosbag from sensor_msgs.msg import PointCloud2, Imu, NavSatFix from std_msgs.msg import Header import numpy as np # KITTI raw 目录结构:date/drive_xxxx_sync/ date = "2011_09_26" drive = "0005" base = f"/data/kitti/raw/{date}/{date}_drive_{drive}_sync" # 读取时间戳 def read_timestamps(path): ts = [] with open(path, "r") as f: for line in f: # 格式:2011-09-26 13:02:25.724199232 parts = line.strip().split(" ") sec = int(parts[1].split(".")[0]) nsec = int(parts[1].split(".")[1].ljust(9, "0")[:9]) ts.append(rospy.Time(sec, nsec)) return ts velo_ts = read_timestamps(os.path.join(base, "velodyne_points/timestamps.txt")) imu_ts = read_timestamps(os.path.join(base, "oxts/timestamps.txt")) # 以雷达第一帧为基准,计算相对时间 t0 = velo_ts[0] velo_ts = [t - t0 for t in velo_ts] imu_ts = [t - t0 for t in imu_ts] bag = rosbag.Bag("kitti_0005.bag", "w") # 写雷达点云(这里假设你已经把 .bin 转成了 PointCloud2,转换函数略) for i, ts in enumerate(velo_ts): pc = load_kitti_velo(os.path.join(base, f"velodyne_points/data/{i:010d}.bin")) pc.header.stamp = ts pc.header.frame_id = "velodyne" bag.write("/velodyne_points", pc, ts) # 写 IMU(KITTI oxts 是 GPS/IMU 组合,需要转成 Imu 消息) for i, ts in enumerate(imu_ts): imu = load_kitti_oxts(os.path.join(base, f"oxts/data/{i:010d}.txt")) imu.header.stamp = ts imu.header.frame_id = "imu_link" bag.write("/imu/data", imu, ts) bag.close()

逻辑说明:这段脚本的核心是「时间戳对齐」和「坐标系声明」。t0取雷达第一帧,所有后续时间戳都减去它,保证 bag 内时间从 0 开始,避免 LIO-SAM 启动时因为时间戳过大导致 TF 缓存溢出。雷达和 IMU 分别写入/velodyne_points和/imu/data,frame_id 必须和 LIO-SAM 参数里的lidarFrame、imuFrame一致。

参数说明:velodyne_points/data/下的.bin文件是 KITTI 的二进制点云格式,每点 4 个 float(x, y, z, intensity),需要自己写load_kitti_velo转成PointCloud2。oxts/data/下的.txt每行 30 个数值,前 6 个是经纬高和姿态,需要转成Imu消息的角速度和线加速度,这里要注意 KITTI 的加速度单位是 m/s²,角速度是 rad/s,但坐标系是 RFU,转 ROS 的 ENU 需要做轴变换。

提示:转换完的 bag 先用rosbag info看一眼雷达和 IMU 的帧数是否对得上,KITTI raw 的雷达约 10Hz,IMU 约 100Hz,如果 IMU 帧数明显偏少,检查oxts/timestamps.txt是否读全。

3. 改 LIO-SAM 参数:从 YAML 到 launch 的逐项调整

3.1 params.yaml 里必须改的 6 个参数

LIO-SAM 的config/params.yaml是改动的核心。下面这张表是我在 KITTI raw 上验证过的参数值,直接抄作业即可,但要注意每个序列的timeOffset可能不同。

参数名默认值KITTI 建议值作用
lidarFramevelodynevelodyne雷达坐标系名,和 bag 里一致
imuFrameimu_linkimu_linkIMU 坐标系名
extrinsicRot单位阵从calib_imu_to_velo.txt取旋转IMU 到雷达的旋转
extrinsicRPY单位阵同上,但只取偏航用于重力对齐
timeOffset0.00.0~0.05 之间试IMU 相对雷达的时间偏移
imuRate200100KITTI IMU 实际频率

extrinsicRot的填法:打开calib_imu_to_velo.txt,里面是一个 3x4 矩阵,前 3x3 是旋转,后 3x1 是平移。LIO-SAM 只要旋转,按行展开成 9 个数填进 YAML 的列表里。注意 KITTI 的矩阵是行主序,YAML 里也是行主序,直接抄。

timeOffset是最玄学的参数。我的做法是:先设 0,跑一遍看轨迹,如果转弯时轨迹往内侧偏,说明 IMU 滞后,把timeOffset调成正的(比如 0.02);如果往外侧偏,调成负的。每次调 0.01,跑短序列(比如 100 帧)看效果,别一上来就跑全程。

3.2 launch 文件里的 frame_id 和 topic 映射

run.launch里需要改的是 topic 名和 frame_id。默认订阅/velodyne_points和/imu/data,如果你转换 bag 时用了别的名字,这里要对应改。另外pointCloudTopic和imuTopic两个参数在params.yaml里也有,两处保持一致。

<launch> <param name="lidarFrame" value="velodyne" /> <param name="imuFrame" value="imu_link" /> <node pkg="lio_sam" type="lio_sam_imageProjection" name="lio_sam_imageProjection" output="screen"> <rosparam file="$(find lio_sam)/config/params.yaml" command="load" /> </node> <!-- 其他节点略 --> </launch>

逻辑说明:LIO-SAM 有四个主要节点:imageProjection、featureExtraction、mapOptimization、imuPreintegration。KITTI 适配主要影响imageProjection(点云去畸变)和imuPreintegration(IMU 预积分)。launch 里确保这四个节点都启动,且params.yaml被正确加载。

参数说明:output="screen"方便看日志,调试阶段建议打开。如果跑的时候发现某个节点没起来,先rosnode list看节点在不在,再看rostopic hz看数据有没有进来。

3.3 点云去畸变:KITTI 的雷达运动补偿和 LIO-SAM 的假设冲突

LIO-SAM 的imageProjection节点会用 IMU 的角速度对雷达点云做去畸变,假设是「雷达帧内匀速运动」。KITTI 的 HDL-64E 是机械旋转式,一帧 100ms 左右,车辆在高速上 100ms 能跑 3 米,去畸变不做的话点云会明显拖尾。

但 KITTI 的 IMU 频率只有 100Hz,一帧雷达只有 10 个 IMU 样本,插值精度不够。我实际改的时候,在imageProjection.cpp里把 IMU 插值从线性改成二次,并且把imuQueLength从 200 调到 100,减少队列延迟。改完重影明显减轻,但代价是计算量增加约 15%。

// imageProjection.cpp 中 IMU 插值部分 // 原版:线性插值 // 改为:二次插值,用前后三帧 IMU 拟合 double t1 = imuQueImu.front().time.toSec(); double t2 = imuQueImu.back().time.toSec(); // 取最近三帧做二次拟合 if (imuQueImu.size() >= 3) { auto it = imuQueImu.end(); it--; auto imu3 = *it; it--; auto imu2 = *it; it--; auto imu1 = *it; // 二次插值系数计算略 }

逻辑说明:这段改动只影响去畸变精度,不改因子图结构。二次插值比线性插值更贴合车辆实际运动,但要求 IMU 队列里至少有 3 帧数据,所以imuQueLength不能太小。

参数说明:imuQueLength默认 200,对应 2 秒的 IMU 数据。KITTI 100Hz 下 200 帧就是 2 秒,够用。如果内存紧张可以降到 100,但别低于 50,否则插值会失败。

4. 避坑与排查:KITTI 跑 LIO-SAM 最常见的 5 个翻车现场

4.1 现象:启动后 rviz 里点云一闪一闪,轨迹不动

原因:TF 树没建起来。LIO-SAM 依赖velodyne到imu_link的静态 TF,如果extrinsicRot填错或者 frame_id 对不上,TF 查询会失败,imageProjection拿不到 IMU 姿态,点云无法去畸变,直接卡住。

解决:先rosrun tf view_frames生成 TF 树图,看velodyne和imu_link之间有没有连线。如果没有,检查params.yaml里的extrinsicRot是否填了 9 个数,以及 launch 里有没有启动static_transform_publisher。我一般会在 launch 里加一个静态 TF 作为兜底:

<node pkg="tf" type="static_transform_publisher" name="velo_to_imu" args="0 0 0 0 0 0 velodyne imu_link 100" />

4.2 现象:轨迹在直道上正常,一转弯就飞

原因:timeOffset没调对,IMU 和雷达时间戳不同步。转弯时角速度大,时间偏移被放大,预积分残差把轨迹拉偏。

解决:用rqt_plot同时画/imu/data/angular_velocity/z和/lio_sam/odometry/pose/pose/angular_velocity/z,看两条曲线的峰值是否对齐。如果 IMU 峰值超前,把timeOffset调正;滞后则调负。每次调 0.005,跑 200 帧看效果。

4.3 现象:建图重影,墙面变成两层

原因:extrinsicRPY没设对,重力方向估计有偏差。LIO-SAM 用extrinsicRPY做重力对齐,如果这个旋转矩阵不对,IMU 的 Z 轴和世界 Z 轴不重合,点云会整体倾斜。

解决:把extrinsicRPY设成和extrinsicRot一样,先跑一遍看重力方向。如果还是斜的,手动在imuPreintegration.cpp里加一个重力对齐初始化,用前 100 帧 IMU 数据估计重力向量,再修正。

4.4 现象:跑着跑着报TF extrapolation into the future

原因:bag 播放速度太快,或者rosbag play --clock没加。LIO-SAM 的 TF 缓存默认 10 秒,如果 bag 里时间戳跳变太大,TF 查询会超时。

解决:rosbag play --clock -r 0.5降速播放,并且在 launch 里加<param name="use_sim_time" value="true" />。如果还报,检查 bag 里雷达和 IMU 的时间戳是否单调递增,KITTI 的timestamps.txt偶尔有重复帧,需要去重。

4.5 现象:内存暴涨,跑几分钟就 OOM

原因:mapOptimization节点默认保存所有关键帧点云,KITTI 序列长,点云累积快。另外imuQueLength太大也会占内存。

解决:把params.yaml里的savePCD设为false,mapFrame设为map但不要频繁保存。imuQueLength降到 100。如果还爆,在mapOptimization.cpp里加一个关键帧降采样,每 5 帧取 1 帧。

5. 进阶:用 KITTI 真值验证轨迹精度,以及一个省时间的技巧

5.1 用 evo 对比 LIO-SAM 输出和 KITTI 真值

KITTI odometry 提供了真值位姿(poses/目录下的 12 列矩阵),raw data 也有 GPS/IMU 组合的经纬高。我一般用evo工具做轨迹对比,先把 LIO-SAM 的/lio_sam/odometry存成 TUM 格式,再和真值对齐。

# 录制 LIO-SAM 轨迹 rosbag record /lio_sam/odometry -O lio_sam_odom.bag # 转成 TUM 格式(用 evo 自带工具) evo_traj bag lio_sam_odom.bag /lio_sam/odometry --save_as_tum # 和 KITTI 真值对比(真值需要先转成 TUM) evo_ape tum kitti_gt.tum lio_sam_odom.tum -va --plot

逻辑说明:evo_ape做绝对位姿误差评估,-va输出详细统计,--plot画轨迹对比图。KITTI 真值的 TUM 格式需要自己写脚本转,把 12 列矩阵拆成平移和四元数。

参数说明:--align做轨迹对齐,--correct_scale做尺度校正。LIO-SAM 输出的是米制,KITTI 真值也是米制,一般不需要尺度校正,但如果有累积漂移,可以加--align看对齐后的误差。

5.2 一个省时间的技巧:先用短序列调参,再跑全程

KITTI raw 的drive_0005只有 100 多帧,跑一遍不到 1 分钟。我习惯先用这个序列把timeOffset、extrinsicRot、extrinsicRPY调稳,再换drive_0011这种长序列验证。调参时只启动imageProjection和imuPreintegration两个节点,关掉mapOptimization,看/lio_sam/deskew/cloud_deskewed的点云是否清晰。点云不重影了,再开全节点跑建图。

这个习惯帮我省了很多次「跑半小时发现参数错了」的后悔药。LIO-SAM 的因子图一旦跑偏,后面很难拉回来,所以前期验证比后期调参重要得多。

注意:KITTI 的drive_0005是城市道路,有大量动态车辆,建图时会有拖影。验证去畸变效果时只看静态建筑物,别被动态物体干扰。

最后说个我自己的教训:第一次改 LIO-SAM 适配 KITTI 时,我花了三天调extrinsicRot,结果发现是 bag 里 IMU 的 frame_id 写成了imu而不是imu_link,TF 树根本没连上。从那以后,我每次转换完 bag 第一件事就是rosrun tf view_frames,确认 TF 树完整再往下走。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询