
简介这是浙大开源的一套 LiDAR 与 IMU 联合标定工具面向自动驾驶、机器人导航等领域的研究者、算法工程师及无人机方向学习者。其核心作用是估计激光雷达与惯性测量单元之间的外参关系从而提升多传感器融合后的定位、建图与感知精度。包内针对作者采集的数据集做了适配说明工具已在特定硬件与环境下验证过具备实际可运行参考价值。资源共 35 个文件压缩包约 691KB。代码以 14 个 h 与 7 个 cpp 为主另有 2 个 hpp、2 个 sh 运行脚本、launch 与 rosinstall 配置、PNG 示意图、README 等整体涵盖标定算法、数据预处理、可视化界面、第三方依赖Pangolin、Kontiki管理与实验引导。目前已有 1125 人学习/下载。通过该资源可获取一套从数据准备、参数配置、标定运行到结果评估的完整思路与代码骨架对研究 LiDAR-IMU 标定或搭建同类传感器系统的人员而言能够有效缩短前期环境搭建与算法调试时间是一份贴近真实工程场景的开源参考。1. 开源 LiDAR-IMU 标定工具的 zip 到手后先别急着跑官方样例一套开源的 LiDAR-IMU 标定工具解决的是激光雷达和惯性测量单元之间的外参问题——旋转加平移一共六个自由度。很多人拿到 zip 包后的习惯是先编译、先跑自带数据集demo 顺利跑通就以为大功告成换上自己采集的 bag 后才发现完全不是一回事残差不收敛、输出外参离谱甚至一启动就崩溃。这个工具的数学框架大多成熟真正吃时间的是把原始数据适配成工具认识的样子。它适合正在做激光建图、多传感器融合和组合导航的工程师特别是手里有一台带 LiDAR 和 IMU 的样机、但没有专业标定台的团队。下面按“一套自己的数据怎么喂进去、标定怎么调参、结果怎么验证”的顺序展开照着走基本能跑通。2. 标定原理与数据自采条件外参是怎么优化出来的你的数据够不够格2.1 外参优化的数学模型六个自由度是怎么被观测出来的LiDAR 和 IMU 在平台上刚性连接它们之间的刚体变换 T (R, t) 有六个自由度三个旋转角加上三个平移量。外参标定要做的事情就是从两类传感器各自独立的数据里把 R 和 t 估计出来并且让这个估计值同时满足两个约束LiDAR 点云在几何上对齐IMU 预积分得到的运动轨迹在动力学上自洽。这类开源工具绝大多数走两阶段路线。第一阶段是粗标定利用角速度和角加速度做一次坐标对齐把旋转初值拉到一个比较接近的区间。第二阶段是精标定进入联合优化代价函数里主要有两类残差一类是 LiDAR 点云到局部平面的点到面距离一类是 IMU 预积分给出的位姿增量与当前轨迹模型之间的误差。两个残差项放进同一个图优化框架里迭代求解最终得到一个同时让几何对齐和惯性约束都满意的 T。为什么不能靠手量或者 CAD 直接填因为安装面的实际间隙、线束方向、机械公差都会让理论值偏出几个毫米到几度而这个量级对建图来说已经足以产生重影。外参优化又对初值敏感旋转初值给偏 10 度以上或者平移初值方向不对优化极易掉进局部极小最终输出一个“数据拟合得很好但物理上说不通”的结果。理解这一点就能明白为什么拿到 zip 之后不能拿官方样例的成功去类推自己的数据——官方数据里的初值、噪声模型和场景结构都是配套的换成你的传感器这些前提全部变了。2.2 自采数据的三个硬条件运动激励、场景纹理、时间同步自己的数据集能不能直接进标定工具最先卡住的往往不是代码而是三个硬条件。第一个是运动激励。外参的可观测性来自两个传感器对同一段运动的不同描述想让六个自由度都被观测到必须同时激发角速度和线加速度而且方向要足够丰富。很多人只围着场地转圈、只在一个平面上推拉这类数据里横滚和俯仰几乎没有被激励旋转外参中相应方向就会退化。我一般会按“八字形 带俯仰的变速 偶尔上下坡”的轨迹采集每段 2 到 3 分钟重复两遍中间不做急刹车。急停会带来点云畸变后面的点到面残差就会把优化带偏。第二个是场景纹理。工具依赖点云里的平面特征来做几何约束所以数据里必须有墙、地面、柱子这类稳定平面同时要有墙角、台阶这类立体结构。开阔的空地、超长的对称走廊、大面积的玻璃幕墙是典型的退化场景会直接削弱甚至冻结某些自由度的观测。动态物体也要避开车辆行人过多的路口会让点到面残差出现系统性外点。第三个是时间同步。LiDAR 一般在 10 Hz 左右IMU 通常在 100 Hz 以上两个话题的时间基准必须一致。时间戳偏移如果超过几十毫秒优化残差会明显发散。有硬件同步最好没有硬件同步时至少采集时要把两个设备接到同一个时钟源并在标定前用下面的命令记录数据rosbag record -O calibration_data.bag \ /points_raw \ /imu/data \ --duration180-O 指定输出的 bag 文件名两个话题参数分别对应激光点云和 IMU 数据--duration 限制采集时长为 180 秒。录制时尽量不要夹带其他话题无关话题会拖慢 bag 读写速度也让后面检查多出干扰项。录制完成后先看 bag 基础信息rosbag info calibration_data.bag这条命令会输出每个话题的消息数量、时间范围、频率和总时长。重点看三个地方点云话题和 IMU 话题的消息数量是否满足“IMU 数量 / 点云数量”大致等于两者频率比两个话题的起始和结束时间是否对齐有没有明显的时间戳跳变。这三项正常数据才具备进入标定的前提。2.3 从 bag 里梳理话题话题频率与时间戳检查比硬套格式更重要拿到 bag 之后下一步就是确认数据格式。不同工具对话题名称和消息类型的约定不一样但共同要求是点云和 IMU 消息都要带有效时间戳消息类型要匹配坐标系帧 id 要一致。此时如果直接把 bag 丢进去跑大概率失败在类型检查或时间戳匹配上。我会先写一个短的检查脚本遍历 bag 统计每个话题的时间戳范围和中位频率。脚本用 ROS 的 bag 读取接口不依赖具体工具逻辑如下import rosbag from collections import defaultdict bag rosbag.Bag(calibration_data.bag) stats defaultdict(lambda: {n: 0, t0: None, t1: None}) for topic, msg, t in bag.read_messages(): s stats[topic] s[n] 1 if s[t0] is None: s[t0] t.to_sec() s[t1] t.to_sec() bag.close() for topic, s in sorted(stats.items()): span s[t1] - s[t0] freq s[n] / span if span 0 else 0.0 print(f{topic}: n{s[n]}, span{span:.2f}s, ffreq{freq:.2f}Hz, t0{s[t0]:.2f})read_messages()按时间顺序遍历 bagt.to_sec()把 ROS 时间转成秒最后按话题打印消息数、覆盖时长和平均频率。如果某个话题消息数太少或频率远低于预期说明录制时丢帧严重如果两个话题的t0相差 0.5 秒以上说明两者没有从同一时刻开始记录后面要做时间裁剪或重对齐。做完检查之后再处理话题命名。工具如果要求/points_raw而你的 bag 里叫/rslidar_points就在 bag 层面重映射不必改工具源码。常用的过滤命令是rosbag filter calibration_data.bag output.bag \ topic /points_raw or topic /imu/datafilter 按表达式筛选话题并生成新 bag。表达式里的话题名要和工具约定完全一致否则后面节点跑起来收不到订阅。上述步骤全部通过后数据才算真正进到“可以用”的状态接下来才轮到编译和启动。3. 把 zip 变成能跑的工具依赖检查、编译与最小跑通3.1 解压之后先看目录结构把依赖对齐拿到 zip 后先不要急着 make先解压看目录。这类工具包里一般会有一个说明文档、若干源码目录、一个或几个配置/launch 文件有些还附带一组官方样例数据。第一件事是读文档里关于运行环境的部分确认它用在 ROS 1 还是 ROS 2运行的前提依赖是哪些。常见依赖包括 Eigen3、PCL、ROS 的点云和 TF 相关库以及一个图优化库比如 Ceres 或 GTSAM 这一档具体以工具文档为准。环境对齐这一步做扎实后面就少很多翻车。我会用几条命令把关键依赖先确认一遍rosversion -d # 查看当前 ROS 发行版 pkg-config --modversion eigen3 # 查看 Eigen3 版本 dpkg -l | grep -i pcl # 查看 PCL 是否已安装rosversion -d输出当前 ROS 发行版名称用来判断文档要求的版本与你机器是否匹配pkg-config查 Eigen3dpkg -l查 PCL 相关包。如果缺依赖优先用系统包管理器补齐不要一开始就自己编译这些底层库。版本匹配的关键不是“装上就行”而是与工具声明的基础版本接近文档要求某个库的大版本时就不要用相差过大的小版本凑合。依赖对齐后我习惯先做一个最小测试直接编译一次哪怕什么都没配。这一步能暴露工具源码里有没有缺头文件、CMake 版本过旧、编译器标准不支持等问题。问题越早暴露越容易定位。3.2 编译顺序先官方样例再自有数据编译这一步取决于工程结构。如果工作空间是 catkin 布局常见做法是在src里放好源码后回到工作空间根目录执行cd ~/catkin_ws catkin_make -j4 source devel/setup.bash-j4限制并行编译任务数避免大项目把内存吃满导致编译进程被杀source devel/setup.bash让当前终端识别新编译出来的节点和库。如果工具已经迁移到 ROS 2 或源码按 colcon 布局组织就把catkin_make换成colcon build输出目录和 source 命令也会变成install/setup.bash。判断依据很简单看工作空间的src下有没有package.xml。编译通过之后先用官方样例数据跑通一次完整流程。启动命令因工具而异但整体形态大多是这样roslaunch your_pkg calibration.launch \ bag_file:$(pwd)/sample.bag \ config_file:$(pwd)/config/calib_config.yamlyour_pkg是占位按工具实际包名替换。launch 里通常已经串好了点云预处理、IMU 预处理和优化节点bag_file参数指向样例数据config_file指向参数配置文件。官方样例能跑通说明你的环境与工具兼容这是连接自有数据前的最后一道闸门。样例能跑出结果但你的数据不行问题就在数据和参数侧不在环境和编译侧排查范围小很多。3.3 适配自有数据话题重映射、时间对齐与格式转换脚本样例跑通后把自有数据换成官方数据。这里最常踩的坑是话题名、消息类型或坐标系不一致做法用上一节的rosbag filter做筛选和重命名。如果筛选后工具仍然报“topic not found”就看 launch 里实际订阅的话题名检查配置里的名字和 bag 里的名字大小写或前缀是否一致。另一类问题是时间对齐。有些工具要求 IMU 消息按点云帧的时间戳取一段历史窗口做预积分如果两个话题的采集起始时间不一致预积分窗口里就会混入空白。处理办法是把长的那个话题裁掉首尾让两个话题的公共时间范围对齐。裁剪可以用rosbag filter配合时间判断条件或者用脚本写回。更细的对齐可以写成一个小工具把 IMU 消息按点云时间戳做线性插值重采样标定精度要求高时尤其值得做。一个轻量的插值对齐脚本片段如下import numpy as np def resample_imu(imu_times, imu_data, query_times): # 每个测量维度单独做线性插值 imu_data np.asarray(imu_data) cols [np.interp(query_times, imu_times, imu_data[:, i]) for i in range(imu_data.shape[1])] return np.column_stack(cols)np.interp做一维线性插值把任意时刻的 IMU 测量映射到点云帧时间上。它假设 IMU 频率远高于点云频率且时间戳基本单调这符合多数平台的情况。插值完后把点云和插值 IMU 一起按点云帧的时间顺序写入标准 bag工具读起来就不会再有时间步不齐的问题。做完这一步仍然失败的十有八九是异常值或点云畸变进入下一章的参数调节环节。4. 标定参数三个必调项与五条避坑记录调参这件事我习惯按一条固定流水线走先验证数据再确认时间戳然后调阈值与噪声权重最后才怀疑算法本身。这个顺序能省掉一大半无效尝试下面先说三个必调参数再给五条高频踩坑记录。4.1 三个必调参数初值、点面阈值、IMU 噪声权重换了自己的数据集后参数不调就硬跑是不行的。三个参数几乎每次都要动。第一个是初始外参猜测值。这个值的来源通常是安装图纸或者直接测量平移部分可以量到厘米级旋转部分大多按安装角直接填。经验上旋转初值误差超过 10 度时优化大概率发散平移初值方向填反结果也没法看。初值给不准时我的习惯是先放一个“只标旋转、固定平移”的配置跑一遍或者手动调整到一个肉眼看点云对齐的状态再放开全部自由度做精标定。这一步省不得初值是后面所有优化的地基。提示初值不是标定结果不必追求精确重点是别让优化走进错误的山谷。第二个是点到面距离阈值。它决定残差计算时哪些点算内点距离阈值以内的点参与优化超过阈值的点认为是外点或动态点被丢弃。阈值设太大会把噪声点和动态目标硬拉进优化设太小又会丢失墙面这类大面积特征。典型起点是 0.05 米激光点云噪声大或者场景里有轻微植被时放宽到 0.1 米。不少工具内部会随迭代收敛自动收紧这个值但外部参数层面依然要先给一个合理初始值。第三个是 IMU 噪声密度包括陀螺仪和加速度计的白噪声与随机游走。这个参数决定 IMU 预积分残差在总代价里的权重设得太大等于告诉优化器“IMU 数据不可信”几何约束独挑大梁退化场景容易飘设得太小又等于把 IMU 当成金标准噪声会被当成运动估计出来残差同样难收敛。有 Allan 方差数据就直接用没有可以先从手册标称值起步手调时每次改一个数量级观察收敛变化。下表是我常用的一组调参起点不同传感器可以在这个范围内试探参数含义起点建议偏大后果偏小后果初始旋转误差猜测外参与的旋转偏差不超过 10 度收敛到局部极小无显著影响点到面阈值判定内点的最大距离0.05 m残差被外点污染平面约束丢失陀螺白噪声IMU 角速度白噪声密度传感器手册值IMU 权重过低残留噪声进入轨迹加计白噪声IMU 线加速度白噪声密度传感器手册值IMU 权重过低残留噪声进入轨迹后两项拿不到手册值时建议用 Allan 方差离线估计或者先看工具在官方样例里的默认值再从自己的数据出发微调。4.2 五条避坑记录现象、原因、解决第一条官方样例秒过换了自己的 bag 一启动就段错误。现象是节点跑起来没几秒进程直接退出日志里没有明显报错。原因通常是 bag 里的点云话题频率或点数远超样例比如 128 线雷达没做降采样直接灌进去内存被瞬时点云塞满。解决是在预处理环节加体素滤波把点云降到工具预期的密度或者在做rosbag filter时就把点云话题按比例抽稀。第二条优化第一轮就发散残差直接爆掉。现象是日志里目标函数值从几千冲到几十万不再下降。原因排第一的是时间戳不对齐特别是 IMU 和点云来自不同时钟源时排第二的是初始外参给了一个大的旋转错值。解决是先回到 2.3 的检查脚本把两个话题的起始时间差打印出来如果时间戳没问题就把初始旋转改成单位阵、平移改成手动测量值再固定平移只标旋转试一轮。第三条标出来的平移 z 分量特别离谱旋转却比较合理。现象是结果里 x/y 平移正常z 方向却有一米级的偏差。原因是场景退化数据里只有大面积地面或水平面竖直方向的平移观测被几何结构锁死优化把误差塞进了 z 轴。解决是检查采集场景是否包含足够多的高度变化比如上下坡、楼梯、货架或者先固定 z 平移不参与优化用其他自由度的结果去反推安装高度。第四条每跑一次结果都不一样而且相差不小。现象是同一个 bag 连续标两三次输出外参数值各有不同。原因是运动激励不足轨迹太单调优化在多个局部极小之间摇摆。解决是重新采集按 2.2 里说的“八字 俯仰 变速”轨迹再录一份而不是反复调参数硬跑。标定问题里数据质量比参数更值得投入。第五条标定结果放到里程计或建图里转弯处出现重影。现象是外参验证阶段看着没问题但融合系统一跑长距离就露馅。原因是 IMU 内参零偏如果没单独校准会以系统性偏差的形式混进外参结果另外机械结构松动也会让外参随时间漂移。解决是配合一次 IMU 内参标定并用两段不同时间采集的数据做交叉验证看外参结果是否稳定不稳定就检查传感器安装固定方式而不是继续调参数。5. 验证标定结果与进阶技巧交叉验证比看残差更可靠标定跑完输出外参文件并不代表结束我至少会做三件事再决定是否上机。第一件是看残差曲线。工具日志里通常有目标函数随迭代的下降记录正常情况是前期快速下降、后期平稳收敛如果曲线在收敛后仍然有明显周期波动说明数据里还残留动态物体或时间戳抖动结果不值得信任。第二件是交叉验证把采集数据按时间切成 A、B 两段分别跑一次标定比较两组外参的差异。旋转差在 0.5 度以内、平移差在 5 厘米以内我认为可以接受超过这个范围问题大概率出在数据而不是参数。比较外参的脚本可以写得很短并且不依赖额外工具箱import numpy as np def compare_extrinsic(R1, t1, R2, t2): dR R1.T R2 cos_a np.clip((np.trace(dR) - 1.0) / 2.0, -1.0, 1.0) angle np.degrees(np.arccos(cos_a)) dt np.linalg.norm(t1 - t2) return angle, dtR1/t1和R2/t2分别来自两段数据的标定结果R1.T R2计算两个旋转之间的相对旋转np.trace取迹得到夹角余弦平移差用欧氏距离。交叉验证通过后我还有一个习惯把标定结果作为初值重新跑一次全自由度精标定看输出是否基本保持在初值附近。如果两次结果偏差明显说明优化过程对初值敏感数据仍然有问题不建议直接上机。这个方向值不值得投入我的答案是值得但建议把时间花在数据采集和验证上不要耗在反复调参里。标定工具本身只是把外参问题转成一个可解的优化问题真正决定结果质量的始终是运动激励、场景结构、时间同步这三个前提。有次我跳过交叉验证拿着一段数据的标定结果直接去建图转弯处轨迹分叉回头查下来还是时间戳没对齐。从那以后每次标定我都会留出两到三段数据做对比验证宁在标定阶段多花半小时也不在后面积累的里程计问题里反复排查玄学。希望帮到你。本文还有配套的精品资源点击获取