第一次拿 Kalibr 做 Camera-IMU 联合标定的时候,我吃了不少苦头。当时手上的 VIO 系统在室内跑起来漂移得像只无头苍蝇,排查到最后才发现问题根本不在前端算法,而是相机和 IMU 之间的外参从一开始就是错的。用 Kalibr 把整个流程走通之后,我对标定这件事的理解才彻底改变:它不是“跑一个命令等结果”的流程,而是一整套从标定板物理尺寸、数据激励质量到结果验证的工程闭环。这篇文章会把 Kalibr 联合标定 Camera-IMU 的完整经验做一个总结,适合正准备做视觉惯性里程计、相机 IMU 融合定位,或者已经在用 Kalibr 但结果不稳定的开发者参考。
1. 为什么 Camera-IMU 标定决定 VIO 上线前的一切
1.1 外参误差怎么影响融合结果
先解释一个容易被低估的问题:外参误差为什么会让 VIO 直接崩溃。
视觉惯性里程计的核心是把相机观测到的视觉几何信息和 IMU 测量到的角速度、加速度放在同一个运动模型里优化。IMU 给出的是自身坐标系下的增量,相机给出的是图像坐标系下的投影关系,两者必须通过一个刚体变换对齐。这个变换就是相机到 IMU 的外参(旋转矩阵和平移向量)。如果外参偏差超过一定范围,IMU 预积分出来的位姿和相机观测估计出来的位姿会互相矛盾,后端优化要么反复震荡,要么干脆发散。
举个例子,假设 IMU 安装在相机右侧 5 厘米处,但标定出来的平移写成了左侧 5 厘米,那么在一个快速平移的数据段里,IMU 加速度投影到相机坐标系后会出现一个明显的系统偏差。这个偏差在短时间窗口里看着不大,但积几秒之后就会变成几厘米甚至几十厘米的位置误差,VIO 的漂移就会变得肉眼可见。
更麻烦的是旋转外参。哪怕只有 1 到 2 度的旋转误差,在线速度较大的场景下,IMU 角速度投影到相机坐标系的误差会被放大。这就是为什么很多人调了半天 VIO 参数都没效果,最后重新标了一次外参,系统就像换了个算法一样。
1.2 Kalibr 为什么是这个活的事实标准
到现在为止,Kalibr 依然是 Camera-IMU 联合标定里使用最广泛的开源工具。原因有几个:一是它把相机内参标定、相机间外参标定、相机和 IMU 外参标定统一在了一套框架里,数据采集方式也基本一致;二是它支持多种相机模型,普通镜头、鱼眼镜头都能处理;三是它自带时间偏移估计,能够在标定外参的同时把相机与 IMU 的时间差也算出来,这一点对时钟不同步的硬件太重要了。
不过我这里想强调一下:Kalibr 不是无脑就能用的工具。它有很多隐含条件,最大的隐含条件就是标定用的 Aprilgrid 物理尺寸必须准,录制的数据必须能把 IMU 的六个轴充分激励起来。这两个条件不满足,命令再对也白搭。
1.3 我建议你先回答这三个问题
在开始之前,先问自己三个问题:
- 你的 IMU 数据手册里能不能找到噪声密度和随机游走参数?如果找不到,后面需要额外花时间测。
- 你的相机图像是否存在运动模糊、自动曝光、卷帘快门明显畸变?这些都会直接干扰 Aprilgrid 角点提取。
- 你的相机和 IMU 在硬件上是否已经做了时间同步?如果没有,Kalibr 能不能处理?能,但时间偏移的初始范围不能太离谱。
这三个问题的答案,决定了你是半小时出结果,还是折腾一整天。
2. 标定板制作里的物理尺寸和棋盘格陷阱
2.1 为什么选 Aprilgrid 而不是 OpenCV 棋盘格
很多人第一次接触标定,脑子里首先想到的是 OpenCV 那种黑白棋盘格。棋盘格用来标相机内参当然没问题,但做 Camera-IMU 联合标定的时候,Kalibr 默认用的是 Aprilgrid。两者的核心区别在于 Aprilgrid 的每个 tag 都带有独立的编码信息,即使图像里只有一部分 tag,算法也能通过编码判断出当前看到的是板子的哪个区域,从而提供可靠的角点对应关系。
这在实际采集中非常关键。标定 Camera-IMU 时,你需要拿着设备在空间里做各种姿态运动,标定板经常只有一部分出现在画面边缘。如果是普通棋盘格,边缘切掉一部分后,算法很难确定当前看到的到底是第几个格子,容易出现角点匹配错误。Aprilgrid 每个黑白方块的编码就是用来解决这个问题的。
2.2 标定板 PDF 的生成参数与毫米级误差
Kalibr 自带生成标定板 PDF 的工具,命令是这样:
kalibr_create_target_pdf --nx 6 --ny 6 --tsize 0.088 --tspace 0.3参数含义:--nx和--ny是横向和纵向的 tag 数量,--tsize是单个 tag 的物理边长,单位是米,--tspace是 tag 间距与 tag 边长的比值。比如--tsize 0.088 --tspace 0.3表示每个 tag 边长 8.8 厘米,tag 之间的空白间距约为 2.64 厘米。
这里有一个很多人踩过的坑:PDF 生成之后,打印时千万不能勾选“适应纸张大小”或者“缩放”。一旦 PDF 被缩放,打印出来的 tag 实际物理尺寸就和--tsize不一致了。Kalibr 检测角点时依赖你在 YAML 里填的tag_size来计算角点的空间坐标,如果实际尺寸偏了 5%,标定出来的平移和焦距都会带上对应的系统误差。
提示:打印完成后,用游标卡尺量几个 tag 的实际边长。如果偏差超过 1 毫米,要么重新打印,要么把实测值填进 YAML。实践中这个毫米级误差直接影响焦距标定精度,尤其是短焦镜头更敏感。
2.3 target YAML 怎么正确填写
标定前要写一个 target YAML,Kalibr 靠它知道标定板长什么样。Aprilgrid 的配置如下:
target_type: aprilgrid tag_cols: 6 tag_rows: 6 tag_size: 0.088 tag_spacing: 0.3tag_size是单个 tag 的实际边长,单位米,tag_spacing是 tag 间距与 tag 边长的比值。注意tag_spacing保存的是比例而不是绝对距离。这个文件会在相机内参标定和 Camera-IMU 联合标定里重复使用,所以必须和实际打印出来的板子严格一致。
另外,标定板本身要贴在平整的硬板上。我试过贴在略微弯曲的纸板背面,结果角点检测没问题,但最终重投影误差明显偏大。Aprilgrid 的角点坐标建立在“板子是个平面”的假设上,板子不平,这个假设就崩了。
3. 数据采集环节的完整动作清单
3.1 采数前的环境与相机参数检查
开始录数据之前,先把环境检查一遍。光照最好是均匀明亮的室内环境,不要让阳光或者点光源在标定板上形成强反光。标定板周围不要有太多和 Aprilgrid 黑白方块相似的物体,否则检测时可能出现误识别。
相机这边,如果有自动曝光、自动白平衡、自动对焦,尽量全部关掉,手动固定曝光参数。运动过程中,自动曝光会因为场景亮度变化不断调整增益,导致标定板黑白对比度不稳定,角点提取的亚像素精度会受影响。自动对焦更危险,标定板一旦靠近或远离,焦点变化会让图像模糊,Aprilgrid 角点直接提取失败。
对于全局快门相机,这个问题会小一些,但如果是卷帘快门相机,运动过快时图像顶部和底部会明显错位,标定出来的内参也会带误差。这种硬件限制无法完全消除,只能靠控制运动速度来缓解。
3.2 话题、时间戳与 rosbag 录制命令
数据采集用 rosbag 录制,命令很简单:
rosbag record -O imu_cam_calib.bag /cam0/image_raw /imu0/data但命令简单不代表数据没问题。录制前先用rostopic hz确认两个话题的频率是否稳定,尤其是 IMU,频率波动过大说明驱动有问题。
时间戳是 Kalibr 标定过程中的关键因素。Kalibr 会估计相机和 IMU 之间的时间偏移,但如果时间戳本身是乱序的、有跳变的,或者相机图像时间戳滞后非常严重,估计结果就会不稳。录制时最好保证相机和 IMU 的消息时间戳来自同一套时钟源。硬件同步做不了的话,也要确保两个驱动在同一个系统里,用同一个/clock时间源。
3.3 让 IMU 充分激励的一套动作流程
数据质量很大程度取决于你如何运动设备。IMU 标定的本质是从运动数据里反推相机和 IMU 的相对位姿,如果某个轴没有被充分激励,对应的外参分量就不可观。
我常用的一套流程是这样的:
- 先让设备静止 10 到 20 秒,让初始化阶段拿到足够的静态数据。
- 缓慢绕 X 轴往复旋转,幅度尽量到正负 45 度以上。
- 再绕 Y 轴往复旋转,然后绕 Z 轴往复旋转。
- 做几次快速平移,前后、左右、上下都要有,每次平移后立刻停下再反向移动。
- 最后做一段自由运动,包含旋转和平移的组合,类似拿着设备画一个空间里的“8”字。
这里有两个容易犯的错误。一是运动太快,导致图像严重运动模糊;二是运动太“温柔”,旋转和平移幅度都很小,IMU 信号被噪声淹没。理想情况是角速度变化和加速度变化都比较明显,但图像又不能糊。实际操作中,我一般在室内中等光照下,用较快的姿态变化但相对较慢的平移速度来平衡这两个要求。
3.4 录制结束后的快速自检
录完 bag 之后,别急着跑标定,先花两分钟看一眼数据。用rosbag info查看消息数量、时长和话题频率,如果图像只有几十帧,或者 IMU 消息数量明显过少,直接重新录。
我一般会在录完后随机抽几帧图像看一眼,确认标定板大部分时间都在画面里,而且没有大面积高光或遮挡。如果某一帧标定板只占了画面角落的一小块,Kalibr 可能检测不到足够的角点。标定板最好覆盖画面面积的三分之一以上,运动过程中可以有一些部分出画,但核心区域不能被完全甩出画面。
4. 相机内参标定与重投影误差判断
4.1 内参模型选择:pinhole-radtan 还是 pinhole-equi
Kalibr 支持多种相机模型,最常用的是pinhole-radtan和pinhole-equi。普通工业镜头、一般监控镜头,用pinhole-radtan就够了;视角比较广的鱼眼镜头,或者畸变明显偏离普通模型的大广角镜头,用pinhole-equi更合适。
选错模型的典型症状是:内参标定可以跑完,但重投影误差很大,而且联合标定结果反复跳变。我曾经在一个视角约 130 度的镜头上用了pinhole-radtan,标定完重投影误差在 1 像素以上,后来换成pinhole-equi,误差直接降到 0.2 像素左右。
4.2 kalibr_calibrate_cameras 实际用法
相机内参标定的命令是这样:
kalibr_calibrate_cameras --bag cam_calib.bag --topics /cam0/image_raw --models pinhole-radtan --target april_6x6_80x80cm.yaml关键参数是--topics,指定 bag 里的图像话题;--models,指定相机模型;--target,指定标定板 YAML。如果录制的 bag 里图像数量很多,可以加--bag-from-to参数截取中间一段时间,避免开头静止阶段和结尾多余数据拖慢优化。
我建议加上--show-extraction参数,标定过程中会画出每一帧检测到的角点图,能直观看到哪些帧检测失败、哪些帧角点太靠边缘。这个参数在第一次标定时几乎必开,它比任何 log 都能更快暴露数据问题。
4.3 怎么读 camchain 输出文件
标定完会在当前目录下生成一个类似camchain-cam0.yaml的文件,里面包含相机模型类型、分辨率、内参矩阵和畸变系数。最关键的判断指标是标定过程中打印出的重投影误差。
我个人的经验是:一般镜头的重投影均方根误差在 0.2 到 0.4 像素之间,说明标定结果比较可靠;如果超过 0.5 像素,先排查标定板尺寸、光照和运动模糊。需要注意的是,这个误差给的是一个整体均值,实际观察误差直方图里是否有离群点也很重要。少量的离群点往往来自个别模糊帧或检测错误,可以尝试重新选一段数据做标定,而不是把全部帧都塞进去。
5. IMU 参数配置与联合标定命令拆解
5.1 imu.yaml 里的四个关键参数从哪来
联合标定之前,需要准备一个 IMU 的 YAML 文件,里面最重要的四个参数是:
rostopic: /imu0/data update_rate: 200.0 accelerometer_noise_density: 0.001867 accelerometer_random_walk: 0.000424 gyroscope_noise_density: 0.000169 gyroscope_random_walk: 0.000033update_rate就是 IMU 数据频率,必须和驱动实际发布的频率一致。四个噪声参数分别是加速度计和陀螺仪的噪声密度(角度随机游走/速度随机游走相关)和偏置随机游走。这些参数在标定外参时会被作为先验信息使用,如果和真实情况偏差太大,联合标定的优化结果会不稳定。
如果 IMU 数据手册里有这些参数,直接填进去。数据手册没有的话,需要用 Allan 方差分析法从一段静止 IMU 数据里估算。我常用的做法是录制一两个小时静止数据,用现成的 Allan 方差工具分析出这些参数。不清楚参数来源直接搜网上的默认值,会导致标定结果看起来正常但 VIO 上就是不准。
5.2 kalibr_calibrate_imu_camera 执行过程
联合标定的命令如下:
kalibr_calibrate_imu_camera --bag imu_cam_calib.bag --cam camchain-cam0.yaml --imu imu.yaml --target april_6x6_80x80cm.yaml注意这里的--cam传入的是上一步相机内参标定生成的camchain-cam0.yaml,--imu传入的是 IMU 配置 YAML。--target还是那个标定板 YAML。
整个优化过程分几步:先初始化相机位姿和 IMU 偏置,然后联合优化外参、时间偏移和部分 IMU 参数。运行时的 log 通常会给出每一轮优化的残差变化。正常情况下残差应该逐步下降并收敛,如果中途出现 NaN 或者残差发散,大概率是数据激励不足或者时间戳问题。
这里有个细节:Kalibr 对 bag 中 IMU 消息的连续性较敏感。如果 IMU 话题中途丢了几秒数据,时间偏移估计可能跳到错误值。录制时最好保证从设备静止开始,到设备静止结束,中间一气呵成,不要中途停止重启节点。
5.3 输出文件:T_ci、时间偏移和各残差
标定完成后会生成camchain-imu.yaml和imu-camchain.yaml两个文件。绝大多数下游系统只需要关注camchain-imu.yaml,里面除了相机内参,还有外参和时间偏移。
外参字段在不同版本里写法有差异,我见过的有T_ci,也有T_ic,含义恰好相反。T_ci通常表示从相机坐标系到 IMU 坐标系的变换,T_ic表示从 IMU 坐标系到相机坐标系的变换。接入你自己的 VIO 时,一定先搞清楚系统里需要的是哪个方向,否则外参方向反了,系统会表现得极其不稳定。
时间偏移项同样需要关注。它表示相机和 IMU 消息时间戳之间的相对偏移,单位是秒。正常情况下应该是毫秒量级,如果偏移量到了几十毫秒甚至更大,说明时间戳本身有问题,要么重新处理时间同步,要么检查驱动是不是用了过滤后的时间戳。
5.4 如何判断标定是否成功
Kalibr 最后会打印一些误差统计,但不要只看“没有报错”就觉得成功了。我更关注三个信号:
- 重投影误差基本均匀分布,没有明显的随时间变化的趋势。
- 外参中旋转部分和平移部分和你的机械安装方式基本吻合,至少方向是对的。
- IMU 的时间偏移在传感器合理范围内,不是异常大。
如果外参中的旋转矩阵和你预装的机械角度差距很大,比如你是水平安装但标定结果出现 30 度旋转,那几乎可以肯定有坐标系定义问题,而不是 Kalibr 错了。
6. 常见失败场景与排查链路
6.1 标定板检测不到或角点提取太少
这是遇到最多的报错。Kalibr 在标定开始时需要从图像里提取 Aprilgrid 角点,如果提取不到,会报类似找不到标定板的信息。
排查顺序是:先看图像亮度,尤其是不是逆光导致黑白方块对比度不够;再用--show-extraction跑一遍,看哪些帧能检测到、哪些不能。常见原因包括标定板运动过快糊了、标定板太小在画面里占比低、镜头畸变太大导致边缘 tag 变形严重。我遇到过最无语的情况是标定板反光灯膜,角点提取时边缘全是高光溢出的白斑,换成哑光材料后立刻好了。
6.2 优化失败与结果不稳定
联合标定时报 “Optimization failed” 或结果跳变,通常不是 Kalibr 本身的问题,而是运动激励不够或者 IMU 参数设置不合理。
一个典型的错误运动是:绕一个轴匀速旋转,旋转速度保持恒定。这种运动对 IMU 来说只是一个恒定角速度,重力向量和加速度信号的时间变化不充分,外参的可观性很差。解决办法就是回到第 3.3 节的激励流程,确保每个轴都有往复加减速,而且包含平移和旋转的组合。
另一个原因可能是imu.yaml里的噪声参数比实际小太多。Kalibr 会把噪声参数作为权重,如果写的噪声过小,相当于告诉优化器“IMU 测量非常准”,那么一些正常的 IMU 偏差就会被误解为外参误差。我的习惯是先用量级差不多的参数跑一遍,如果结果不收敛再手动调整一到两个数量级。
6.3 时间戳引发的诡异误差
时间同步问题在外参标定里有个很迷惑的表现:平移外参标出来非常离谱,但旋转外参看着还算合理。原因是时间偏移误差主要影响的是加速度计和相机运动之间的对位,而对旋转的影响相对小一些。
排查时间戳问题时,我会先看 bag 里图像的帧间时间戳差。正常相机的帧间间隔应该和标称帧率基本一致,比如 30 帧相机,帧间隔应该在 33 毫秒左右。如果出现间隔突然变成几倍、几十倍的跳变,那说明图像驱动丢帧或者时间戳软同步有问题。IMU 消息同理,帧间间隔应该是1 / update_rate,也看看有没有间断。
如果确实是两个设备的时间源不统一,Kalibr 虽然能估计时间偏移,但仍存在一个可信范围。理想情况下先做一个粗同步,把时间差压到几十毫秒以内,再交给 Kalibr 精调,比完全靠 Kalibr 从零估计要稳得多。
6.4 标定一次后如何快速验证是否可用
标定完成之后,我一般不会直接拿结果上真机,而是先用一段带标定板的短数据做个快速回环验证。做法很简单:把标定得到的camchain-imu.yaml拿到一个现有的 VIO 系统里,用之前录的一段运动数据离线跑一遍,看输出轨迹是否平滑、姿态是否有明显抖动。
如果没有现成的 VIO,也可以做一个更简单的检查:把相机和 IMU 固定不动,记录一段静止数据,然后用标定得到的外参把 IMU 的加速度投影到相机坐标系,观察投影后的重力方向是否和预设的相机重力方向一致。方向偏差在几度以内,说明外参旋转基本可信;如果偏差很大,那就是坐标方向反了,把T_ci逆过来再看一次。
最后再分享一个小技巧:每次标定完,我都会把标定板 PDF、target YAML、imu YAML、录制时的相机曝光参数和房间光照情况记在一个小本子里。这样下次换镜头、换 IMU 或者换场地时,可以直接复用能跑通的配置,而不是重新踩一遍“光照太暗导致角点检测失败”的坑。标定这种东西,一次跑通了,后面最花时间的往往不是算法,而是那些看起来不起眼的工程细节。