做Lidar-IMU标定这件事,我前前后后折腾了小半个月。最难的不是把代码跑起来,而是跑起来之后发现结果根本不对——点云叠加在墙面上有重影,轨迹一长就裂开。后来我回过头去把lidar_align这个工具从头到尾捋了一遍,又把数据采集、参数设置、结果验证每个环节挨个排查了一遍,才算真正把外参标定这件事吃透。这篇东西我不想写成论文式的教程,就按我自己从零到一的实操路径来写,把那些文档里没写清楚、但实际影响结果的细节全部摊开。想搞定Lidar-IMU外参的,直接跟着做就行,能少走很多弯路。
1. 为什么你搞不定外参标定:这件事的本质和常见误区
1.1 外参到底是什么,不标定会怎样
外参描述的是两个传感器坐标系之间的一次刚体变换,包含旋转矩阵和平移向量。放在Lidar-IMU的场景里,就是激光雷达坐标系和IMU坐标系在空间中的相对位置关系。D435i这类自带IMU的深度相机,出厂之前大多已经做过内参和外参的联合标定,但自己搭的激光雷达加IMU组合,外参只能自己想办法。
外参不标定会造成什么问题,我用一个很直观的例子解释一下。激光雷达扫描出来的每一个点,默认是在雷达自身坐标系下的坐标。算法在融合IMU数据时,需要把这一帧点云变换到IMU坐标系下。如果旋转、平移给错了,点云在拼接时就会错位,地面像搓衣板一样起伏,墙面出现双重轮廓。做SLAM建图的时候影响更明显,机器人在走廊转一圈回来,地图的闭合误差飙得极高。
很多朋友觉得外参是固定值,标一次就一劳永逸。这个理解不完整,机械安装结构一旦因为外力撞击、螺丝松动产生位移,外参就变了。所以标定这个事情,不只是新装设备要做,设备重装、碰撞之后都要重新做一遍。这也是为什么把一套可靠的标定流程掌握在手里,比单纯依赖某一个工具重要得多。
1.2 为什么很多人标出来效果还是不对
我观察到一个高频现象:同一份代码,别人跑出来就是准的,自己跑出来怎么都不对。问题往往不在算法本身,而在数据质量和初始值。工具给了一个优化框架,但它不会告诉你,你把rosbag录成什么样、初始外参给成什么样,结果会天差地别。
lidar_align这类基于非线性优化的工具,本质上是在求解一个优化问题。任何非线性优化都有“局部极小值”的问题,也就是说,如果你给的初始外参离真实值太远,优化过程会收敛到某个错误的结果上去,而且这个错误结果在损失函数上看起来还挺合理。很多人第一次标定给的初始值是0,0,0,激光雷达和IMU又有将近10厘米的物理距离,优化器找来找去找不到正确的山头,最后停在了一个四不像的位置。
更隐蔽的问题是数据采集环境。你拿着设备在一个狭小的、特征稀疏的走廊里录了一段包,点云在走廊方向几乎看不出变化。算法想要通过点云对齐来估计位姿,但是走廊方向上的约束弱到可以忽略,Rosbag里那些点云帧对优化器来说,和废数据没什么区别。
1.3 不要只盯着lidar_align,先评估手上的硬件条件
选工具不能只看社区热不热,你得先想清楚手里的传感器是什么类型。lidar_align适合机械式激光雷达,比如Velodyne VLP-16、HDL-32这样在360度范围内均匀扫描的设备。如果你用的是Livox固态激光雷达,那种非重复式扫描模式,点云分布特性和机械式完全不一样,直接套lidar_align大概率效果不好,这时候更适合去找对应的标定工程。
另外,lidar_align依赖里程计输入。大多数人的思路是用轮式里程计或者IMU积分出来的姿态作为输入,但轮子打滑会引入大量误差,IMU积分时间一长就漂移。我后来用的方案是先用LIO算法跑一帧一帧的激光里程计,把得到的里程计话题作为lidar_align的输入,这样点云和里程计之间的关联更好。实测下来,标定结果的稳定性提升了一个档次。
2. lidar_align是怎么工作的:原理只有三步
2.1 核心思路:让两把“尺子”互相校验
lidar_align的核心思想说起来很简单。激光雷达移动的时候,每一帧点云都是在一个连续轨迹上的不同位置采样的。如果外参正确,用IMU/里程计给出的相对位姿把各帧点云变换到一起,场景中的固定物体应该是严格重合的。外参一旦有偏差,点云之间就会产生系统性错位。算法要做的,就是找出那个让所有点云对齐得最好的外参。
具体到实现上,工具把每个激光雷达检测到的点投影到目标坐标系下,搜索其最近邻点,然后计算距离残差。这个思路和ICP点云配准很像,但不同之处在于,lidar_align优化的变量不只是外参,还包含每一帧点的位姿更新量。换句话说,它把位姿估计和外参估计放到同一个非线性优化框架里联合求解了。
这对我们有什么启发?它意味着工具对输入里程计的质量是有容忍度的,因为优化器会自己去修正一部分位姿误差。但容忍度是有限度的,如果输入的位姿轨迹本身就是漂移的、跳变的,优化器会把位姿误差强行塞进外参里,你就得到了一个“看起来在训练集上很准,实际上放到真实环境就崩”的标定结果。
2.2 损失函数与优化策略:理解这些才能调好参数
lidar_align使用Ceres Solver进行非线性优化。默认的核函数是HuberLoss,这个核函数会对大的残差进行降权,提高算法对离群点的鲁棒性。在室内环境中,玻璃墙、窗户、金属反光面会产生很多错误的点,这些点如果以正常权重参与优化,会把外参带偏,核函数的鲁棒性就体现在这里。
工具中还有一个关于点云采样的参数,优化时不会把每一帧的全部点都拿来做匹配,而是按一定比例随机采样。降低采样点数能显著提升优化速度,但如果你的点云噪声大、结构稀疏,采样太少会导致约束不足。我自己的做法是先用默认参数跑一轮,看loss曲线趋势。如果loss下降很快但最终值较大,就把采样点数加大再跑。
有几种方法判断优化是否收敛:一是看Ceres输出的迭代次数和cost下降曲线,二是看优化结束后的loss值是否在一个远小于初始loss的区间,三是把标定结果可视化出来,直接看点云拼接效果。不要只盯数值,数值小不代表一定准,因为算法的损失函数和真实几何误差之间不是绝对等价的关系。
2.3 从原理反推使用条件:三类场景先排除
理解了原理之后,很多问题就有了答案。比如为什么场景特征太少标定效果差?因为点云最近邻搜索需要几何约束,在一个纯平面的天花板、地面和光秃秃的墙壁组成的空间里,激光点云在法线方向的约束很弱,优化器没法准确判断外参中的某些分量。
第一个需要排除的场景是长而狭窄的走廊。沿着走廊方向几乎没有任何特征变化,类似视觉SLAM里的退化场景,算法在走廊方向上的估计就没有信息量。
第二个是空旷的大场地。激光雷达扫出去几十米都是平地,点云找最近邻找到了也是同一片区域,优化问题病态。
第三类是高速旋转的场景。手持设备快速甩动、车辆高速转弯,点云会产生运动畸变,而lidar_align本身是不做运动畸变补偿的,它默认帧内的点云是刚性的。我建议数据采集时保持设备运动平稳,避免急加减速和快速旋转,即便做不到完全匀速,也尽量让位姿的变化连续平滑。
3. 开工前的准备:从源码编译到数据采集
3.1 编译lidar_align:环境依赖和容易踩的坑
lidar_align是一个ROS功能包,依赖ROS、Eigen、Ceres Solver和PCL库。在Ubuntu 16.04 + ROS Kinetic那会,我装的时候最头疼的是Ceres版本和Eigen版本打架。后面换到Ubuntu 18.04 + ROS Melodic,系统自带的Eigen和Ceres版本兼容性好很多,编译基本不用折腾。
我录了一段实际的操作过程:
cd ~/catkin_ws/src git clone https://github.com/ethz-asl/lidar_align.git cd .. catkin_make第一次编译如果报找不到Ceres相关的头文件,先检查有没有安装libceres-dev。用apt安装的老版本Ceres和lidar_align在某些接口上有差异,建议直接从Ceres官网源码编译安装最新稳定版,编译耗时几分钟而已。
另一个容易忽略的问题是PCL版本。lidar_align用的是PCL的kd-tree做最近邻搜索,如果机器上同时装了几个版本的PCL,CMake在链接库的时候可能找错版本。我在新机器上编译时会先看一眼pcl_version,确保和ROS自带的PCL版本一致,否则会出现运行时报错找不到符号的情况。
3.2 数据采集的标准姿势:比想象中更重要
前面花了那么大篇幅讲数据质量,到实际操作的时候更要对数据苛刻一点。我采集数据用的是一条长约80米的室内通道,通道两侧是平整的墙面,地面有规律的纹理,墙面上有门洞、消防栓和凸起的柱子,天花板上挂着灯和管道,这种环境对激光雷达来说信息量非常丰富。
采集时要保证激光雷达能完整扫描到周围360度的环境,不要让人或设备贴墙面太近。手持设备移动的速度控制在每秒0.3米到0.5米,既不能快到来不及扫描,也不能慢到图像帧之间几乎没有位移。整个采集过程控制在两分钟左右,太短了约束不够,太长了IMU积分和里程计的漂移又会被放大。
设备的高度变化也要刻意加进去,我习惯在扫描过程中主动做一个弯腰、一个垫脚的动作,让IMU在pitch方向上有明显的激励。很多人在标定时容易忽略一个现象:如果IMU只在水平面上运动,外参的roll和pitch方向约束严重不足,标出来的旋转矩阵在俯仰方向不准,地面稍微有点坡度就会露馅。
3.3 rosbag的记录与话题检查
录制的时候要把原始话题完整记录下来,不要用--compression压缩,压缩格式在后续处理时会引入不必要的解码开销。用如下命令记录:
rosbag record /velodyne_points /livox/lidar /imu/data -O calib.bag话题名称视你实际的驱动而定,重点是同时记录点云话题和里程计/IMU话题。录制完成后,用rosbag info calib.bag查看话题频率和消息数量。我遇到过IMU话题频率只有10Hz的情况,对lidar_align来说,IMU的消息频率不需要太高,因为它只用里程计做帧间位姿约束,但话题不能长时间丢数据,丢数据会导致相邻点云的位姿变换出现跳变。
还有一个需要提前做的操作:检查点云话题的坐标系定义。lidar_align要求点云消息的frame_id和里程计消息的frame_id在语义上是统一的。实际操作中发现很多驱动把点云frame_id写成了laser,而里程计frame_id是base_link,这本身没问题,但在配置launch文件时,你要保证给lidar_align传递的topic名称和frame_id对应关系是一致且明确的。
4. 手把手跑通lidar_align:配置、运行、结果验证
4.1 launch文件配置:几个关键参数的技术细节
lidar_align的launch文件负责启动标定节点,核心参数包括点云话题、里程计话题、初始外参和优化配置。我放一个典型的配置片段:
<launch> <node pkg="lidar_align" type="lidar_align" name="lidar_align" output="screen"> <param name="point_cloud_topic" value="/velodyne_points"/> <param name="pose_topic" value="/laser_odom"/> <param name="output_topic" value="/lidar_align/transforms"/> <param name="initial_translation" value="0.0 0.0 0.0"/> <param name="initial_rotation" value="0.0 0.0 0.0"/> <param name="visualize" value="true"/> </node> </launch>注意initial_translation和initial_rotation不是随便填的。如果你知道雷达和IMU之间的近似安装位置,比如IMU在雷达正上方5厘米,那就把初始平移设为0 0 0.05。如果你还知道的大致角度偏差,也填进去。一个合理的初始值能让优化器少走弯路,大幅降低陷入局部极小的概率。
还有一个参数叫use_scan或者类似的布尔开关,控制是否在前端预处理阶段滤除地面点和远距离稀疏点。开启后优化速度更快,但滤除地面点会削弱z方向上的约束。对放在轮式机器人上的设备,地面约束本来就很强,可以开启;对手持设备,不建议开。
4.2 运行标定和观察loss曲线
配置完成后,先启动roscore,再运行节点:
roscore roslaunch lidar_align lidar_align.launch节点启动后会自动订阅话题,并从bag文件里回放数据。运行过程中Ceres会不断输出优化迭代信息,主要看两个指标:cost和迭代次数。正常情况是cost从一个较大的值开始快速下降,然后逐渐收敛到一个稳定的平台区。如果cost不降反升,大概率是话题配置错了,或者初始值偏差过大导致优化发散。
整个优化过程可能持续几分钟。跑完之后会生成一个Transform.json文件,里面保存了最终的外参。保存格式包括平移向量和四元数,四元数在使用时要注意归一化,我遇到过手滑直接拿原始四元数去用导致旋转矩阵不对的情况。
4.3 结果验证:别只看数值,点云会说话
标定结果好不好,最大的检验标准是把它放到系统里去看实际表现。我自己摸索出一套三层验证法。
第一层,把标定出来的外参替换到现有算法里,停在原地让设备自转一圈,观察点云构图。如果墙体、桌面边缘锐利清晰,没有重影,说明旋转外参基本准确。如果有横向错位,优先怀疑yaw方向的外参没标准。
第二层,让设备走一个边长2米左右的方形轨迹,最后回到起点。用标定后的外参跑一遍建图,看终点的地图和起点能不能闭合。闭合误差小于20厘米算合格,小于5厘米说明外参标定得相当好了。
第三层,验证平移分量的精度,在设备前方放一个已知高度的箱子,建图完成后用工具量一下箱子顶面的高度误差。这个测试对z方向的平移误差非常敏感,如果箱子高度和真实值差了10厘米以上,回去重新标。
这三层验证都过了,我才会把这个外参写进正式的配置里。这个流程看起来繁琐,但能过滤掉很多看起来数值合格、实际不能用的情况。
5. 高频踩坑现场:这些问题你大概率会遇到
5.1 话题和坐标系相关的坑
坑1:rosbag回放速度不匹配。用rosbag play回放时,如果数据点云频率和里程计频率差很多,或者话题之间的时间戳没有对齐,节点可能产生抖动。解决方法是回放前加--clock参数,让节点使用bag的仿真时间,同时对IMU和雷达都使用同样的时间基准。
坑2:点云frame_id和里程计frame_id不匹配。有时点云的frame_id是空字符串,里程计是odom,节点在内部做坐标系变换时会直接拿不到有效变换。检查方法很简单:回放bag时打开rviz,加一个PointCloud2显示和一个Path显示,看着两者是否在同一个坐标系下运动,如果不一致,需要先做坐标系的统一。
坑3:里程计消息类型不匹配。lidar_align要求里程计消息是nav_msgs/Odometry,有些方案提供的是geometry_msgs/PoseStamped,虽然不是完全不能用,但需要做一个小转换节点。我写过一个简单的pose2odom节点,把PoseStamped包装成Odometry发出去,实测没有问题。
5.2 采集与场景相关的坑
坑4:环境特征太少导致旋转外参不准。在一个标准办公室角落里标定,三面是墙,特征单一,标出来的外参在roll方向上偏差经常超过2度。我的做法是,先在一个特征丰富的环境中标定一轮,得到粗值后,再换一个不同特征的环境跑第二轮,两轮结果的差异在可接受范围内才算稳。
坑5:雷达线束少导致点云过于稀疏。VLP-16这种16线雷达在远距离上的点云分布非常稀疏,直接用原始点云跑优化,kd-tree搜索到的最近邻可能离真实最近点相距很远。一个解决思路是先用voxel filter降采样,再在匹配前做一次点云的表面法线估计,让最近邻匹配对更鲁棒。
5.3 工具本身限制的补充说明
lidar_align假设激光雷达和IMU之间是刚性连接,如果你的设备里加了减震结构,或者装在云台上,这个假设就不成立了。刚性连接是标定的前提条件。
工具对多雷达配置也有一定支持,可以通过launch参数配置多个点云话题,但每个雷达和IMU的外参是分开优化的。多雷达时建议先单独标定每个雷达的外参,然后再做联合优化,直接一开始就联合优化,容易因为某个雷达数据质量差而带偏全部结果。
6. 一份能“抄作业”的标定检查单
6.1 标定前的checklist
我每次给新设备做标定,都会过一遍下面的清单,能避免绝大多数低级错误:
- 传感器固定可靠,没有松动
- 已知雷达和IMU的大致安装位置和朝向,准备好初始值
- 选择一个结构特征丰富、尺寸适中的室内环境
- 规划一条包含直线、转弯、上下俯仰变化的采集路径
- 录制rosbag,并用
rosbag info确认两个核心话题都有数据 - 修改launch文件中的话题名和初始外参
- 启动节点并且回放数据,观察优化过程
这张表看着简单,但每一条背后都有对应的血泪教训。比如第四条规划路径,我早期就是随手拿着设备在办公室里走了一圈,结果因为缺少俯仰激励,标定出来roll和pitch偏差非常大,在地面上跑算法时地图整体倾斜。
6.2 标定结果在真实系统里的应用
拿到Transform.json之后,还要做一次坐标系的整理。多数LIO算法(比如FAST-LIO、LIO-SAM)的配置文件里需要提供外参四元数和平移向量。这里的顺序和坐标系定义一定要和算法约定对齐。
我踩过一回:lidar_align输出的外参定义是“把雷达系变换到IMU系”,而算法里要的是“把IMU系变换到雷达系”,方向反了之后,整个系统的点云全部炸开。后来我在代码库里加了一个外参求逆的检查函数,每次换外参都会自动算一遍变换矩阵的行列式和逆矩阵,避免了这类低级错误。
另一个应用场景是手眼标定的朴素版本:把这一套流程扩展到相机和雷达联合标定。社区里常说的“相机标定”和“手眼标定”技术栈虽然不太一样,但核心思路是一致的,都是求解两个传感器坐标系之间的刚性变换。如果你已经掌握了lidar_align的流程,再去理解基于ArUco标定板的相机-雷达标定,或者基于棋盘格的视觉-惯性标定,上手会快很多。这算是标定这个领域里共通的底层逻辑。
6.3 一些让结果更稳的进阶经验
第一点,条件允许时多跑几轮标定。每一轮用不同的初始值开始,比如在真实安装估计值的基础上加上几度的扰动,跑完看多次优化结果的一致性。一致性好的标定结果,可信度才高。
第二点,做好地面真值的交叉验证。我用过一种简单粗暴的方法:把设备放在地面上的一个精确标记处,用尺子记录雷达和IMU各自到标记的距离和高度,然后计算外参的平移分量。和lidar_align的结果对比,能得到一个非常直观的精度参考。
第三点,标定这件事其实没有“一劳永逸”的答案,设备用久了,结构件可能因为热胀冷缩发生微小的形变,所以定期重新标定也是工程上的标准化操作。
我个人在实际操作中的体会是:拿到一个好的外参标定结果,功夫几乎都花在准备阶段。数据采好了,环境对了,初值合理了,优化器自己就能跑出好结果;反过来,数据一塌糊涂,换再好的工具也是白搭。希望这篇东西能帮你在Lidar-IMU标定的路上少折腾几个来回。