前阵子在做一套视觉惯性里程计方案的时候,被相机和IMU之间的外参折磨得够呛:明明单看相机标定和内参都挺正常,一旦把IMU融进来做紧耦合,轨迹一会儿飘一会儿抖,最后排查了半天才发现,罪魁祸首就是外参不准。
后来老老实实把Kalibr的相机-IMU外参标定全流程跑了一遍,前后花了一天时间,把采集规范、参数配置、结果校验这些环节彻底理顺之后,整个系统的稳度直接上了一个台阶。这篇文章我就把自己踩过的坑、总结出的操作细节,以及每一步背后的原理,完整记录下来。Kalibr这个工具箱在视觉惯性SLAM、无人机、自动驾驶多传感器融合领域几乎是绕不开的标配工具,无论你是刚入门VIO,还是已经在项目里被外参问题折磨过,这份全流程解析应该能帮你省下不少弯路。
1. 为什么相机-IMU外参标定如此重要,Kalibr到底解决什么问题
1.1 外参标定在实际系统中的角色
先想清楚一个核心问题:相机和IMU是两套完全独立的传感器,一个以光速采集图像,一个用MEMS惯性器件测量角速度和加速度。要让这两个传感器的数据能在同一个坐标系下融合,就必须知道它们之间的相对位姿关系,这个关系就是外参。
很多人觉得外参不就是个6自由度的旋转加平移吗?拿个尺子量一量、或者查一下产品图纸不就行了?实际工程里还真不是这么回事。相机光学中心和IMU测量中心往往是物理上无法直接测量的,机械安装公差、镜头固定误差、电路板焊接偏移都会让图纸上的设计值和实际值差出好几毫米甚至几度。对于视觉惯性导航这种对精度极其敏感的系统,哪怕外参旋转误差超过1度,融合后的轨迹就会明显漂移,如果平移误差在毫米级以上,近距离场景里的重投影误差也会变得不可接受。
更麻烦的是,相机和IMU之间还存在时间戳不同步的问题。图像曝光时刻和IMU采样时刻往往不在同一个时间基准上,这个时间偏移如果不标定,相当于你把一个延迟了几十毫秒的传感器数据当成实时数据去融合,后果比外参差一点还要严重。
Kalibr这个工具箱,本质上就是在做两件事:估计传感器之间的空间外参(包括相机间的外参、相机-IMU间的外参),以及估计它们之间的时间偏移。它把这两个问题放进一个统一的优化框架里求解,这也是它比很多“手写标定”方案可靠的原因。
1.2 Kalibr工具箱的核心模块与标定思路
Kalibr出自苏黎世联邦理工ASL实验室,全称是“A Unified Camera Calibration Toolbox”,最初是给视觉惯性导航系统服务的。它包含几个模块:多相机标定、IMU噪声特性标定、相机-IMU外参标定。其中相机-IMU外参标定是它的招牌功能。
这个模块的核心思路是:标定过程中,手持设备在标定板前做各种激励运动,同时录制图像和IMU数据。算法会利用视觉信息,通过检测Aprilgrid标定板的角点,估计出相机在每个时刻的位姿轨迹。这个位姿轨迹被建模为连续时间B样条曲线,而不是离散的位姿序列。为什么用连续时间B样条?因为这样可以很自然地把IMU测量的角速度、加速度和轨迹的导数对应起来,还能同时估计时间偏移。
在优化过程中,视觉重投影误差和IMU积分误差被放在一起联合优化。优化变量包括外参旋转矩阵、平移向量、时间偏移、重力方向、甚至IMU的零偏。最终输出的就是使得所有误差最小的那一组外参和时间偏移。
1.3 为什么项目里优先选Kalibr而不是其他方案
市面上做相机-IMU标定的方案其实不少,比如港科大VINS-Mono里自带的外参在线标定,Apollo的OpenCalib,还有一些公司内部自研的标定工具。我在几个项目里对比过,Kalibr的优势在于:它的时间偏移估计是显式建模的,并且和空间外参联合优化,其他很多方案要么需要外部时间同步设备,要么只估计空间外参忽略时间偏移。对于消费级传感器,时间偏移往往是不可忽略的,Kalibr这种联合估计的方式更实用。
另外Kalibr对Aprilgrid的支持非常完善,Aprilgrid通过编码信息能抗遮挡、抗误匹配,即使标定板部分出画,也依然能稳定检测角点。这在手持设备采集数据时特别重要。当然,Kalibr也不是没有缺点,后面我会讲到它的安装依赖、编译版本这些让人头大的地方,以及这些坑怎么绕过去。
2. 标定前的准备工作:硬件条件、安装部署与标定板选择
2.1 硬件准备与数据同步方式
开始标定之前,先把硬件环境梳理清楚。我这里说的相机-IMU组合,既可以是一个视觉惯性模组,比如Realsense D435i、Mynt Eye,也可以是自己拼的独立相机加IMU,比如工业相机+ICM20602。关键不是设备档次,而是数据是否真的能录出来、时间戳是否可用。
对于自己拼的相机和IMU,建议尽量用硬件触发同步,或者至少保证两个传感器驱动的时间戳来自同一个时钟源。如果两个传感器完全是两套独立系统,时间戳各走各的,那即便Kalibr能估计时间偏移,估计出来的结果也会不稳定,因为偏移本身可能随着系统漂移在变。我踩过的一个坑是用两台电脑分别录相机和IMU数据,然后离线合并成bag,结果每次标定出来的时间偏移都不一样,后来改成单机单进程录制,把两个话题写进同一个bag,问题才消失。
录制工具一般用ROS的rosbag。相机话题发布频率建议30Hz以上,IMU频率建议100Hz到200Hz。IMU频率太低,B样条对运动激励的插值会不够平滑,影响标定质量。另外建议把IMU数据的话题频率写入配置文件,后面标定IMU噪声和做外参标定都会用到。
2.2 Kalibr安装与ROS环境配置的几个关键点
Kalibr的安装是新手最容易卡住的地方。它依赖ROS,并且早期版本只支持Python 2,所以网上很多教程默认使用Ubuntu 16.04加ROS Kinetic,或者Ubuntu 18.04加ROS Melodic。如果你用的是Ubuntu 20.04加ROS Noetic,也能装,但需要装Python 3版本的依赖,部分脚本要做兼容处理。
我自己的建议是,如果是新环境,直接用Ubuntu 18.04加ROS Melodic最省心,编译过程基本一条龙。如果项目里已经有Noetic环境不想切换,那就多花点时间处理依赖问题,思路是先把python3-catkin-pkg、python3-rospkg这些Python 3版本的包装好,再编译Kalibr。编译命令大概是:
cd ~/catkin_ws/src git clone https://github.com/ethz-asl/kalibr.git cd ~/catkin_ws catkin_make这里有个小提醒:Kalibr仓库里包含一些子模块,如果直接clone后编译报找不到头文件,检查一下子模块有没有拉全。我用的一条稳妥命令是:
git clone --recursive https://github.com/ethz-asl/kalibr.git编译过程中如果报关于sparse-block-matrix或者sophus相关的错误,多半是依赖版本和ROS版本不匹配,优先确认是否安装了libsuitesparse-dev和libeigen3-dev。
2.3 标定板的选择与Aprilgrid配置细节
Kalibr官方最推荐的是Aprilgrid标定板,光说“棋盘格”其实不够严谨。相比普通棋盘格,Aprilgrid每个格子内部有编码图案,算法可以通过编码识别每个角点的唯一身份,这样即使标定板被部分遮挡,也能准确对应角点,不会出现整张图匹配错乱的情况。
生成Aprilgrid的时候,需要自己写一个YAML配置文件。这个文件里的参数每一项都不能错,以我常用的6x6标定板为例:
target_type: 'aprilgrid' tagCols: 6 tagRows: 6 tagSize: 0.088 tagSpacing: 0.3tagSize的单位是米,代表每个黑色tag的边长。tagSpacing是一个无量纲比例,代表黑白块之间的间隔相对于tagSize的比例。假如tagSize是0.088米,tagSpacing是0.3,那么两个tag之间的实际间隔就是0.088乘以0.3,等于0.0264米。
标定板打印时有一个很关键的点:必须保证打印出来的尺寸和配置里的尺寸一致,而且要尽量平整。我试过用普通A4纸打印,贴在硬纸板上,结果标定出的内参焦距和实际偏差比较大。后来换成户外防水广告布贴在铝塑板上,效果好很多。如果只是桌面级实验,至少也要用相纸打印并贴在亚克力板或者玻璃板上,保证表面没有褶皱。
2.4 数据采集的几条硬性要求
采集数据是整个标定流程中决定成败的一步。Kalibr官方文档里对激励运动的要求,我用自己的话总结就是:六个自由度都要得到充分激励,旋转和平移都要有,速度不要太快也不要太慢,要让图像中的特征点清晰、IMU不饱和。
具体操作时,我一般会手持设备站在标定板前大概0.5到1米的位置,让标定板尽量占据画面的一大半,然后做慢速但大幅度的旋转运动,围绕相机的三个轴分别转,同时伴随上下左右平移。需要注意的是,不要做纯旋转,纯旋转会让视觉位姿估计在某些方向上退化,也不要做纯平移,纯平移对IMU的角速度信息激励不足。动作幅度要大,速度要缓,让图像不出现运动模糊,IMU输出不超出量程。
采集时长上,我一般录1到2分钟,包含至少30秒的有效运动。录太短,优化容易不收敛;录太长,中间如果出现标定板大面积出画,反而会引入坏数据。录完之后我会先看一眼bag的时间戳是否连续,有没有掉帧或者跳跃。相机话题和IMU话题的时间戳都应该是单调递增的,如果发现时间戳跳变,先修驱动,再标定。
3. 完整实操:从相机内参标定到相机-IMU外参标定的三步流程
3.1 第一步:相机内参标定
相机-IMU标定不是一上来就做联合标定的,第一步先把相机的内参和畸变系数标定好。虽然Kalibr可以联合优化,但如果内参本身不准,外参标定的可靠性会大打折扣。我的习惯是先把相机内参固定住,再去做相机-IMU外参。
录制一个专门的标定包,内容就是设备在Aprilgrid前做各种姿态的运动,至少要覆盖图像边缘区域,因为畸变参数主要靠边缘信息约束。录制好bag之后,运行相机内参标定命令:
rosrun kalibr kalibr_calibrate_cameras \ --bag ~/bag/cam_april.bag \ --topics /cam0/image_raw \ --models pinhole-radtan \ --target ~/bag/april_6x6_88x88cm.yaml--models参数指的是相机模型和畸变模型。pinhole-radtan是针孔模型加布朗畸变模型,也是最常见的选择。如果你的相机是鱼眼镜头,就要改成pinhole-equidistant或者omni-radtan。这个选择需要根据镜头视场角和畸变特性来定,选错模型会导致重投影误差一直下不去。
内参标定完成后,会在当前目录生成几个文件,包括camchain.yaml和results-cam.yaml。camchain.yaml是后续相机-IMU标定的输入文件,里面包含了图像分辨率、相机内参矩阵、畸变系数、畸变模型等关键信息。我每次都会打开这个文件检查一眼,确认内参数值在合理范围内。
3.2 第二步:IMU噪声参数标定
相机-IMU标定还需要一个IMU的噪声模型参数文件,包括陀螺仪和加速度计的噪声密度、随机游走。这些参数在Kalibr的优化中会作为先验信息,如果设置得太离谱,优化结果也会偏。
标定IMU噪声的常用工具是imu_utils,来自港科大的沈老师团队,配合code_utils使用。操作上,把IMU水平静止放置,录制至少两小时的数据。两小时听起来很长,但这是为了把随机游走估计准,如果时间太短,随机游走项很容易和噪声密度耦合在一起。
录制完成后,运行:
rosrun imu_utils imu_utils /imu0它会扫描bag中的数据,输出一个imu.yaml文件,内容大致是这样:
rostopic: /imu0 update_rate: 200.0 gyro_noise_density: 0.0002 gyro_random_walk: 0.0001 accel_noise_density: 0.01 accel_random_walk: 0.0005这些参数的单位需要特别注意。噪声密度的单位通常是连续时间下的deg/s/sqrt(Hz)或者m/s^2/sqrt(Hz),随机游走的单位是deg/s^2/sqrt(Hz)之类的。Kalibr对单位很敏感,如果搞错,标定出来的结果会异常但未必报错,这一步非常容易踩坑。
3.3 第三步:相机-IMU外参标定核心命令与参数
前面两步都准备好之后,终于到了最关键的一步。录制一个同时包含相机图像和IMU数据的bag,这时的运动激励要比内参标定时更丰富,要确保IMU的角速度和加速度变化足够大。然后运行:
rosrun kalibr kalibr_calibrate_imu_camera \ --target ~/bag/april_6x6_88x88cm.yaml \ --cam ~/bag/camchain.yaml \ --imu ~/bag/imu.yaml \ --bag ~/bag/cam_imu.bag \ --cam0 /cam0/image_raw \ --imu0 /imu0 \ --time-calib \ --timeoffset-padding 0.02这里解释几个容易踩坑的参数。--cam0指定相机话题,--imu0指定IMU话题,两者要和bag里的实际话题名完全一致。--time-calib表示启用时间偏移估计,正常都要开启。--timeoffset-padding是时间偏移搜索的扩展范围,单位是秒,如果传感器时间戳本身偏差比较大,可以适当增大,但也不要太大,否则优化空间太大会影响精度。
外参标定运行过程中,终端会打印大量信息。我通常会关注几个关键输出:一个是优化后的重投影误差,另一个是IMU误差项,还有最终估计的时间偏移。如果看到一条类似“Initialization failed”的报错,大概率是初始值给得太差,或者数据质量太差,后面问题排查部分我会细说。
外参标定完成后,会生成camchain-imu.yaml,里面包含了相机到IMU的外参变换矩阵,一般用T_ci表示,意思是从IMU坐标系变换到相机坐标系的位姿矩阵。这个文件就是我们在后续VIO系统里直接加载使用的最终产物。
3.4 标定结果怎么看:矩阵、时间偏移和误差指标
很多人拿到结果后直接把参数丢给系统用,但其实结果里有很多信息值得检查一遍。camchain-imu.yaml里的外参会以4x4的齐次变换矩阵形式给出,格式像这样:
T_ci: - [0.003913, -1.0, 0.99999, 0.015] ... timeshift_cam_imu: -0.019其中T_ci的旋转部分应该是近似正交的,行列式接近1。如果旋转矩阵明显不正交,说明优化过程有问题。平移部分对应相机光学中心在IMU坐标系下的位置,数值的大小应该和硬件布局大致相符。比如IMU在相机正前方20毫米处,平移量的符号和数量级就应该和这个布局匹配,如果出现完全不符合物理常识的数值,即使重投影误差看着不大,也要怀疑结果是否陷入了局部最优。
时间偏移timeshift_cam_imu的单位是秒,含义是相机时间戳相对IMU时间戳的偏移。不同硬件差异很大,可能在正负几十毫秒之间。如果这个值超过0.1秒,我会先怀疑驱动时间戳是不是有问题,而不是直接接受这个结果。
误差指标方面,Kalibr会输出重投影误差的均方根值。我自己的经验是,如果重投影RMS在0.5像素以内,说明视觉部分拟合得非常好;如果超过1像素,就需要检查标定板平整度、图像清晰度等数据质量问题。IMU误差项则要看数量级是否和输入的噪声密度匹配,如果大了好几个数量级,说明运动激励可能不足或者加速度计饱和了。
4. 常见问题与排查技巧实录
4.1 外参标定失败、不收敛到底是谁的锅
我见过最多的问题是标定过程中报错“max iterations exceeded”或者“Initialization failed”。很多人第一反应是Kalibr不够好,但根据我反复试验的经验,绝大多数情况下问题出在数据质量上。
首先检查运动激励。如果整个bag里你只是拿着设备慢悠悠晃了一圈,角速度变化不够剧烈,视觉轨迹和IMU之间的约束太弱,优化就会发散。解决办法是重录数据,用前面说的大幅度慢速旋转加平移,让每个轴都被激活。其次是检查标定板检测率。Kalibr会输出每帧图像检测到的角点数量,如果大量图像只检测到零星几个角点,视觉约束也不够,这类数据尽量剔除。
另外就是初始外参的影响。Kalibr默认会先做一个线性初始化,如果初始外参离真实值太远,迭代优化容易掉进局部最优。有些情况下可以自己给一个比较接近的外参作为初始值。具体做法是在camchain-imu.yaml里预先填一个估计值,或者通过命令行参数传入,这样能显著提高收敛概率。
4.2 时间戳不同步问题怎么排查
如果你发现每次标定出来的时间偏移都不稳定,甚至符号都在变,先别怀疑Kalibr,大概率是数据本身的时间戳有问题。我遇到过一次相机驱动是USB摄像头,时间戳是采集时刻而不是曝光时刻,IMU是高频设备,两者之间天然存在一个未知且抖动很大的延迟。这种问题只能从驱动层解决,比如用硬件触发或者用曝光时刻戳。
还有一种情况是bag录制时CPU占用过高,导致图像话题回调被延迟,时间戳出现锯齿状跳动。排查方法很简单,把bag里的/cam0/image_raw话题导入PlotJuggler,看时间戳间隔是否均匀。如果间隔忽大忽小,建议关闭占用CPU高的程序重录。
4.3 标定结果误差偏大,几个容易被忽略的因素
标定板不平整是最隐蔽的坑。如果标定板有弧度或者表面反光严重,Aprilgrid检测到的角点会有亚像素级的偏差,累积起来会让重投影误差变大。解决办法是换更平整的标定板,同时在采集时注意不要让强光直接照射导致反光。我之前用亮面亚克力板时,反光问题困扰了很久,后来贴了一层哑光膜,效果立竿见影。
另外,运动模糊也常被忽略。设备转动稍快一点,图像里Aprilgrid的边界就会糊掉,导致检测精度下降。kalibr在检测标定板时会输出自信度信息,如果大量图像的检测自信度都很低,说明运动速度太快,需要放慢动作。
还有一个因素是IMU量程。很多消费级IMU满量程是±4g或者±8g,如果你做大幅度的快速甩动,加速度计可能瞬间饱和,产生削顶失真。标定时尽量控制运动平缓,让IMU工作在线性区间内。要确认IMU有没有饱和,可以录制数据后查看加速度计的最大范数是否接近量程上限。
4.4 标定结果如何验证,不能只信一次结果
拿到外参之后,不要直接上线跑系统,先做几个验证。最简单的验证方式是用Kalibr提供的可视化工具,把IMU坐标系和相机坐标系在图像上画出来,肉眼确认两个坐标系之间的相对关系是否和实际硬件安装一致。比如IMU在相机上方,那在图像里画出的IMU坐标系原点就应该比相机坐标系原点偏上。
更严格一点,用录制的bag离线跑一遍VINS-Mono或者自己的VIO系统,观察初始化收敛速度、轨迹闭环误差。如果外参正确,系统初始化阶段的重投影误差应该很快收敛到较小值,跑完一圈轨迹,起终点误差也不会太大。如果外参有偏差,系统往往表现为“初始化失败”或者“运行中轨迹发散”。我自己习惯标定两次,用不同运动模式的bag分别标定,比较两次结果的差异,如果外参矩阵之间相差超过1度和1毫米,就说明至少有一次数据有问题。
还有个实用的小技巧:标定完成后,让设备静止不动,通过VIO系统后端估计的IMU零偏来判断外参是否合理。如果外参正确,静止时陀螺仪零偏会收敛到接近出厂值的稳定状态;如果外参不准,后端往往会用错误的零偏去补偿外参误差,零偏估计值和出厂值差得离谱。
最后再分享一个我自己的习惯。每次标定完,我会把bag、camchain.yaml、imu.yaml、camchain-imu.yaml这四样东西打包归档,标注好硬件型号、固件版本、采集日期和运动模式备注。别小看这个习惯,当项目做到后面发现传感器性能异常时,翻出原始标定数据重新分析,往往能快速定位是硬件退化还是标定失效。我遇到过两次“神秘故障”,最后都是靠原始bag排查出来的。标定这件事,一次到位是运气,能重复复现才是能力。