☰
LiDAR-IMU外参标定实战:B样条连续时间估计与工程避坑指南
2026/10/22 22:05:17 网站建设 项目流程

简介:浙大开源的激光雷达-惯性测量单元联合标定工具,面向自动驾驶、机器人导航等领域的研究者与工程师,用于解决激光雷达与惯性测量单元之间的外参标定与时间同步问题。工具基于相应开源项目改造,适配了特定的数据集,提供从数据预处理、特征匹配到非线性优化的完整标定流程。压缩包共35个文件,约691KB,以C/C++头文件与源文件为主体,头文件负责接口声明,源文件实现核心算法,同时包含脚本、ROS启动文件、依赖清单及说明文档,便于快速编译与运行。此外还设有图形界面模块,可直观查看标定过程与结果。已有1125人学习下载。其中可获取可复现的标定工程,内含Pangolin、Kontiki等可视化与优化第三方库配置,适合希望深入理解联合标定原理并在自有数据集上进行二次开发的读者,能够有效降低传感器融合系统的搭建与调试门槛。

1. 外参标定做不对,点云融合就是白费

做多传感器融合的人应该都有这种体验:LiDAR 和 IMU 单独看都挺好,但装到同一个平台上之后,点云和姿态轨迹就是歪的,建出来的地图带着一层双重影。第一反应是算法写得不行,后来才发现多半错在外参——LiDAR 到 IMU 的旋转和平移没标定好,甚至时间轴都差了几毫秒。再加上 IMU 的 bias 还在里面掺和,整个融合系统就成了黑匣子。某高校开源了一套 LiDAR-IMU 标定工具,核心思路是做连续时间 B 样条轨迹估计,把外参、时间偏移和 IMU bias 放进同一个非线性优化问题里去解。它适配了自己采集的数据集,同时也保留了通用接口:换一套你自己的数据,改几个参数文件一样能跑。这篇笔记从源码结构、编译依赖、数据适配、踩坑经验到结果验证,按实操顺序拆给你看。

2. 源码结构与标定原理:连续时间 B 样条轨迹估计

2.1 压缩包里的源码结构

解压后是 lidar_IMU_calib-main 项目根目录,目录划分比较清晰,先花一分钟认全再动手,后面查问题会快很多。我按自己拆项目的习惯,把关键文件列成了一张表:

路径作用实操说明
include/core核心算法头文件,声明轨迹、残差、优化相关类一般不动,读代码先看这里
src/core标定算法本体,包括误差项构造和优化入口想改残差权重时改这里
src/ui、include/ui标定可视化界面,基于 Pangolin 绘制点云、轨迹和 IMU 数据标定过程可以直接看收敛情况
src/utils数据读取、时间戳处理、坐标变换等工具函数适配自己数据时大概率要改这里
thirdparty/Pangolin三维可视化库的源码副本需要预先编译,版本相对独立
thirdparty/Kontiki连续时间轨迹估计库,B 样条参数化在这里实现整个工具的数学基础
calib.sh一键标定脚本适合批量跑实验
build_submodules.sh批量编译第三方子模块先执行它再编主项目
launch/licalib_gui.launchROS 启动文件有 ROS 环境可直接拉起 GUI
test/li_calib_gui.cppGUI 程序入口从这里能看到 UI 线程和标定主循环的关系

包里还有 .gitmodules 和 depend_pack.rosinstall,说明这套代码原本依赖 git submodule 管理第三方库,拿到压缩包之后,submodule 已经做成了源码目录放进 thirdparty 里,反而省去了手动 clone 的麻烦。LICENSE 是开源协议文件,商用之前先看它一眼,避免后续有授权问题。

2.2 为什么标定要做“连续时间轨迹估计”

很多人第一次接触这套工具,卡住的地方不是编译,而是“连续时间轨迹估计”这个概念。先解释它解决了什么问题。

传统的离散时间标定思路是:把 LiDAR 点云按帧对齐,帧间用 IMU 积分推算位姿,然后拿累积误差去调外参。这个做法在低速小角度场景下能用,但有两个硬伤。第一,LiDAR 本身是旋转扫描设备,一帧点云里每个点是在不同时刻采样的,直接把整帧当成同一个时间戳,模型上和物理事实就不符。第二,IMU 积分对时间偏移非常敏感,如果 LiDAR 和 IMU 之间的时间同步本身差了 5 到 10 毫秒,离散方法很难同时解出外参和时间偏移,因为它们在你手里的观测方程里高度耦合。

连续时间方法换个思路:不把轨迹表示成离散帧,而是用一条 B 样条曲线来参数化平台在整个标定时段内的运动。B 样条有一个特性——给定时间戳,可以直接算出该时刻的位置、姿态、角速度和加速度,而且姿态是解析可导的。这意味着 LiDAR 点云中任意一个点,我都可以用它在采集时刻对应的插值位姿去处理;IMU 每次采样对应的角速度和线加速度,我也可以从 B 样条轨迹的解析导数里求出来。这样一来,外参、时间偏移和轨迹控制点可以放进同一个优化问题,联合求解。

Kontiki 库在这里承担的就是 B 样条参数化和解析导数这部分,Pangolin 则是把 LiDAR 点云、IMU 测量和当前外参下的对齐结果实时画出来。前者解决数学问题,后者解决“标定到底有没有跑对”的直观判断。

2.3 优化问题的残差构造

这套标定要同时估计的量包括:LiDAR 到 IMU 的旋转 R、平移 t、时间偏移 td、IMU 加速度计和陀螺仪的 bias、以及整段轨迹的控制点。优化目标由两类残差构成。

第一类是 LiDAR 几何残差。用当前外参把 LiDAR 点云变换到 IMU 坐标系,再通过 B 样条轨迹变换到世界坐标系,让当前时刻的点与已有的增量式地图匹配,常见做法是点到面的距离残差。点云建出来的地图质量会反过来影响优化效果,所以数据采集时周边环境不能太单调。

第二类是 IMU 运动残差。B 样条轨迹能输出任意时刻的角速度和线加速度预测值,将预测值与 IMU 实际采样值做差,得到运动残差。IMU 的高频测量在整个时间轴上提供了密集约束,这是连续时间方法相比纯点云配准更稳的原因。

下面这段是用类 Python 伪代码表达的优化器结构,方便理解代码里各种类之间的关系,不是项目源码本身:

class LiDARIMUCalibration: def __init__(self): # 连续时间轨迹,保存全部 B 样条控制点 self.trajectory = BSplineTrajectory() # 待估计外参,R 为旋转矩阵,t 为平移向量 self.extrinsic_R = np.eye(3) self.extrinsic_t = np.zeros(3) # 待估计时间偏移:LiDAR 时间戳相对 IMU 时间戳的差 self.time_offset = 0.0 def build_residuals(self, lidar_points, imu_samples): residuals = [] # 遍历 LiDAR 点云中的每个点,每个点有自己的采样时刻 for point in lidar_points: # 在当前时间戳查 B 样条轨迹,得到该时刻的位姿 pose_at_t = self.trajectory.pose(point.timestamp + self.time_offset) # 先转到 IMU 坐标系,再转到世界坐标系 transformed_point = pose_at_t * (self.extrinsic_R @ point.xyz + self.extrinsic_t) # 计算点到增量地图表面的距离 residuals.append(point_to_surface_distance(transformed_point)) # 遍历 IMU 采样,bias 在构造预测值时就减掉 for sample in imu_samples: # 从轨迹解析导数取角速度预测 predicted_omega = self.trajectory.angular_velocity(sample.timestamp) # 从轨迹二阶解析导数取线加速度预测,再叠加重力 predicted_accel = self.trajectory.acceleration(sample.timestamp) residuals.append(sample.gyro - predicted_omega - self.gyro_bias) residuals.append(sample.accel - predicted_accel - self.accel_bias) return residuals

逻辑说明:残差分成空间和时间两条线,LiDAR 点只负责“空间对齐得准不准”,IMU 采样负责“运动预测得准不准”,两边通过 B 样条轨迹的时间戳耦合在一起。优化器用高斯牛顿或 LM 算法迭代时,控制点、外参、时间偏移和 bias 会一起更新。值得注意的一点是,时间偏移直接影响 LiDAR 点去轨迹上查询位姿的那个时间戳,这也是它能和空间外参联合可观测的关键。

参数说明:B 样条控制点的时间间隔是影响标定质量的重要参数。间隔越小,轨迹表达越精细,但优化变量也越多,容易过拟合 IMU 噪声;间隔过大,则无法表达快速旋转时的真实运动。常见做法是设置 0.05 到 0.1 秒的节点间隔,折中表达能力和计算量。如果你的数据集里运动非常剧烈,可以把间隔调小到 0.02 秒,同时适当增加正则项防止轨迹变形。

3. 编译与运行:先解决 Pangolin 和 Kontiki 的版本问题

3.1 依赖梳理与编译顺序

这套工程依赖 Eigen3、OpenCV、Pangolin、OpenGL、GLEW,还有 Kontiki。最容易翻车的是 Pangolin 和 Kontiki 版本之间的头文件接口不兼容,Kontiki 在较新版本里改过 B 样条类的构造函数参数,而压缩包里打包的 Pangolin 又对 OpenGL 版本有要求。稳妥的顺序是:先装系统级依赖,再编第三方库,最后编主项目。

# 系统级依赖,Ubuntu 20.04 / 22.04 均适用 sudo apt install -y libeigen3-dev libopencv-dev libgl1-mesa-dev libglew-dev

说明:Eigen 是模板库,主要是头文件,Kontiki 和主项目编译时都需要它。OpenCV 在可视化界面和某些 utils 函数里被引用。libglew-dev 决定了 Pangolin 能不能正常渲染点云界面,缺了它会报各种 GL 相关的 undefined reference。

然后是编译第三方库。project 根目录自带的 build_submodules.sh 思路很直接:逐个进入子模块目录,创建 build 目录,cmake 加 Release 编译选项,再 make。手动执行的话顺序如下:

# 编译 Pangolin cd thirdparty/Pangolin mkdir -p build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release make -j"$(nproc)" # 编译 Kontiki cd ../../Kontiki mkdir -p build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release make -j"$(nproc)" cd ../../..

说明:nproc 宏会自动使用机器全部 CPU 核心并行编译,Pangolin 编译时间较长,如果内存不太充裕,也可以写死为 4 或 2。两个库编译完成后,再编主项目:

mkdir -p build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release make -j"$(nproc)"

编译完成后,build 目录下会生成可执行文件,test/li_calib_gui.cpp 对应 GUI 程序,如果编译过程中报找不到某个头文件,先回过头确认三个环节:Eigen 是否装好、Pangolin 是否真的 make 成功、Kontiki 的 include 路径是否正确传递。建议用 cmake-gui 或 ccmake 看一次 CMakeCache.txt 里三个库的路径变量,很多所谓“玄学”编译问题其实都是路径没对上。

3.2 用 ROS launch 启动可视化界面

这套库原生支持 ROS,launch 文件在 launch/licalib_gui.launch 里。如果已经有 ROS 环境,roslaunch 是最省的启动方式:

roslaunch lidar_IMU_calib licalib_gui.launch

如果没装 ROS,也可以直接用编译出来的可执行文件加载配置跑,但注意点云和 IMU 话题的获取就需要自己写数据读取部分。启动 GUI 后,界面会显示 LiDAR 点云、IMU 角速度和加速度曲线,以及当前外参下的点云对齐状态。包里自带的 3imu.png 和 ui.png 两张图片,分别展示了多组 IMU 的安装布局示意和 GUI 界面效果,动手之前可以先对照看一眼,了解预期输出长什么样。

4. 适配自己的数据集:数据采集、话题映射与配置修改

4.1 采一节合格的标定数据

标定算法再强,数据质量不合格也白搭。很多人在这一步偷懒,拿一段 SLAM 跑图的 bag 直接喂进去,结果优化不收敛,还怀疑算法有问题。以我跑过的标定流程看,合格的数据集至少要满足三个条件:

第一,前后各留 30 到 60 秒纯静止段。静止段的作用是让 IMU bias 和重力方向可观测,算法必须看到足够长的零速区间才能把加速度计 bias 从重力里剥离出来。第二,运动段要覆盖全姿态。绕 x、y、z 三个轴都做大幅旋转,同时带平动,最好走出类似“8 字绕桩”的轨迹。只在小范围内平移的姿态变化不足,LiDAR 会退化为纯平面匹配,时间偏移和 z 轴方向的平移可观测性很差。第三,周边环境要有丰富的几何结构。室内选有墙面、柱子和桌椅的房间,室外选有建筑物立面、树木或路沿的场景,避免空旷场地和单调走廊。

4.2 数据组织与话题映射

这套工具默认从 ROS bag 里读数据,但你自己采集的 bag,话题名、时间戳单位、消息类型都可能和原数据集不一样。适配的第一步是把话题映射搞清楚。拿到你的 bag 之后,先查话题:

# 查看 bag 内的消息类型和时间范围 rosbag info your_dataset.bag

输出里会列出每个主题(topic)的名称、消息类型、消息数量和时间范围。核对三点:LiDAR 点的消息类型一般是 sensor_msgs/PointCloud2 或自定义点云类型,IMU 消息类型一般是 sensor_msgs/Imu,时间戳单位是秒还是纳秒。如果 LiDAR 消息里每个点自带独立时间戳,那恭喜,时间偏移标定的条件非常好;如果只有帧级时间戳,就需要在代码里把每帧近似成同一个时刻,标定精度会受影响。

4.3 修改配置文件参数

配置部分一般在工程里以 yaml 或 json 形式存放。以常见做法为例,主要参数包括话题名、时间戳单位、初始外参、时间偏移搜索范围、B 样条节点间隔。下面给一段适配示例:

# lidar_imu_calib_config.yaml lidar: topic: "/velodyne_points" # 改成你自己 bag 里的点云话题 has_packet_time: true # 点是否带独立时间戳 time_unit: "s" # 时间戳单位:s 或 ns,写错会发散 imu: topic: "/imu/data" # 改成你自己 bag 里的 IMU 话题 rate: 200.0 # IMU 实际采样频率,必须准确 time_unit: "s" # IMU 时间戳单位 calibration: initial_R: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0] # 初始旋转,按行优先读 initial_t: [0.05, 0.0, 0.2] # 初始平移,粗略测量值即可 time_offset_range: [-0.1, 0.1] # 时间偏移搜索范围,单位秒 knot_interval: 0.05 # B 样条控制点时间间隔,单位秒

参数说明:initial_R 和 initial_t 是当前你对 LiDAR 和 IMU 之间相对安装位置的最好估计,不需要精确,但方向不能错。比如 IMU 的 x 轴朝前、LiDAR 的 x 轴朝左,这种 90 度级别的方向性错误会让优化直接陷入局部最小。time_offset_range 要覆盖你系统真实的同步误差,一般 100 毫秒够用。knot_interval 前面说过,0.05 是一个平衡取值,运动更剧烈时调小。

4.4 先手动粗对齐,再交给优化

优化不是万能的,初始外参如果错得离谱,残差曲面会有大量局部极小值,算法很容易陷进去出不来。我的习惯是,跑完整标定之前先做一次粗对齐:拿第一帧点云在 GUI 里叠加,手动调 R 和 t,直到点云中同一墙角在视觉上不再分离,再启动完整优化。这一步只花几分钟,但能把后续翻车概率降低一大半。粗对齐的另一个作用是判断你的数据坐标系方向定义是否和原工程一致,很多换数据集后“完全不收敛”的情况,根子就在坐标系的轴上。

5. 常见问题与避坑:五条实测踩坑记录

5.1 静止段不足,IMU bias 估计乱跳

现象:标定程序跑起来了,位移和旋转外参初步可看,但 bias 估计结果每次运行差别很大,时间偏移也不稳定,同一个 bag 跑两遍结果对不上。 原因:数据中静止段太短,加速度计 bias 和重力方向在优化里互相耦合,信息不足导致不可观测。 解决:重新采集数据,前后各加 40 到 60 秒纯静止段。如果 bag 已经采完没法补,可以把运动段起始位置附近切成静止段,但效果不如独立静止段。从那以后我每次采集标定数据,第一件事就是确认静止段时间戳长度。

5.2 时间戳单位混用,优化直接发散

现象:优化开始没几步,点云残差狂涨,外参输出变成乱码一样的矩阵,或者直接 nan。 原因:LiDAR 时间戳是纳秒,IMU 时间戳是秒,配置里没有统一,把 1e9 量级的时间当成秒用,B 样条查轨迹时落到几十米开外的位姿上。 解决:先拿 rosbag info 确认两个话题的时间戳范围,单位差 1000 倍的可以直接看出来。配置里把 time_unit 字段如实填写,并在 utils 的读取代码里统一转成 double 秒制。这类问题在代码里看不出来,跑一次才知道,属于最典型的黑匣子问题。

5.3 坐标系方向定义和原数据集冲突

现象:原数据集能跑出漂亮结果,换自己的数据后,即使初始外参给了“正确值”,优化也不收敛或收敛到明显错误的外参。 原因:不同 LiDAR 驱动对点云坐标轴的定义不一样。有的驱动输出 x 轴朝前,有的 x 轴朝左,还有的把 z 轴定为朝下。IMU 的角速度方向也同理,如果拿原工程的初始 R 直接套,等于在错误的方向上加了一个大偏置。 解决:先做一次极慢速直线运动,对比点云轨迹方向和 IMU 积分方向,判断两个坐标系之间差了几个 90 度。在配置文件里修正 initial_R,或者更直接地,读取 LiDAR 和 IMU 数据之前在 utils 统一到同一个坐标系规范下。

5.4 旋转式 LiDAR 点云运动畸变,点面残差下不去

现象:优化迭代很久,残差始终降不到预期,GUI 里点云边缘“发糊”,就像快门慢了拍出来的照片。 原因:旋转式机械 LiDAR 一帧点云扫描周期接近 100 毫秒,运动状态下每个点的采集时间和位姿都不同。标定算法虽然用连续时间轨迹处理了时间戳,但如果你启用的点云预处理环节把一帧点云当成刚体去畸变,误差会留在残差里。 解决:采集时尽量放慢运动速度,避免快速转向;同时确认代码里点云配准用的是原始点时间戳,而非帧时间戳。如果工程实现里做了去畸变,检查去畸变时用的位姿来源是不是当前迭代的轨迹估计结果,别用未优化的外参去补畸变。

5.5 运动范围不足,部分外参不可观测

现象:标定结果旋转外参看起来很合理,但平移外参的 z 方向分量(或者某个特定轴)数值方差巨大,反复标定都不稳定。 原因:标定过程的运动没有充分激励到所有自由度。比如长期在平面上跑,LiDAR 主要看到地面和正前方墙面,激光的几何约束对水平平移和俯仰角很敏感,但对 LiDAR 到 IMU 的 z 向偏移几乎无约束。 解决:采集时加入爬坡或绕立柱旋转的动作,让 LiDAR 扫描到非平面结构。如果实在没条件补采,可以在优化里固定明显不可观的轴参数,或者用卷尺量一个安装距离直接锁死,减少自由维度。

6. 标定结果不用急着信:三段式验证法

标定程序的输出只是一组数字,它是否真的描述了你这台机器的物理外参,要靠验证。我的验证流程固定三段:残差曲线、点云双重影、SLAM 联调。

第一段看优化日志里的残差收敛曲线。一个合格标定,残差曲线应该是前几十次迭代快速下降,之后平坦。如果迭代终止时残差仍然缓慢下降,说明数据质量或者模型自由度设置有问题,果断停下查前面避坑章节里的那几条。

第二段做点云双重影检查。用标定出的外参把同一时刻的 LiDAR 点云投影到 IMU 坐标系,再按时间戳插值到同一个世界坐标系下,叠加多帧点云显示。对准良好的标志是墙面的点云厚度小于 3 厘米,如果看到明显双层结构,说明外参或者时间偏移还有残差。

第三段是和 SLAM 系统联调。把标定结果拿到实际跑一遍建图或者定位,看高频抖动是否明显改善。这也是最接近真实工作负载的验证方式,因为 SLAM 对时间同步误差的敏感度和标定残差不完全等价,时间偏移如果还有几毫秒误差,普通残差曲线看不出问题,但运行 SLAM 时地图会周期性扭曲。

我自己常用的验证技巧是把标定后的外参分别给两个版本的算法:一个带时间偏移补偿,一个不带,对比两条轨迹的终点误差。如果差出一大截,说明时间偏移对这套系统的影响不可忽略,标定结果值得信;如果二者几乎没有区别,说明你的系统时间同步本来就很好,这次标定主要贡献在外参部分。

从那以后,我每次执行标定前,都强制自己走一遍数据质量检查清单:静止段够不够长、时间戳单位对不对、初始外参方向有没有搞反、运动段覆盖率足不足。这些东西几分钟就能查完,却能把一整天的无效调试挡在门外。希望帮到你。

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

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

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

立即咨询