第四章 相机-IMU联合标定实战:Kalibr工具链深度解析
说实话,做多传感器融合这几年,我踩过最大的坑就是标定。你想想看,相机和IMU各自的数据都挺准的,但一融合就飘得离谱——十有八九是标定没做好。今天咱们就手把手把Kalibr这套工具链走一遍,从相机内参、IMU噪声,到相机-IMU外参,全给它标明白了。
核心知识点速览
- 相机内参标定:使用Kalibr的kalibr_calibrate_cameras
- IMU噪声标定:基于Allan方差分析
- 相机-IMU外参标定:kalibr_calibrate_imu_camera联合优化
4.1 相机内参标定——Kalibr的第一步
相机内参标定,说白了就是搞清楚镜头到底是怎么把三维世界映射到二维图像上的。我最早用OpenCV的棋盘格标定,后来转到Kalibr,发现它支持多种标定板,尤其是Aprilgrid,鲁棒性好了不止一个档次。
我个人习惯用Aprilgrid,因为它每个格子都有独立ID,即使部分遮挡也能识别。你想想看,在实车上采集数据时,标定板经常被遮挡一部分,这时候Aprilgrid的优势就体现出来了。
我的经验:标定板打印时一定要用哑光纸,反光太强会导致角点检测失败。我曾经用普通打印纸,结果阳光下反光严重,白白浪费了半天时间。
先看看标定板的配置文件,我一般这样写:
# april_6x6.yaml
target_type: 'aprilgrid'
tagCols: 6
tagRows: 6
tagSize: 0.088 # 每个格子边长,单位米
tagSpacing: 0.3 # 格子间距比例,相对于tagSize
然后采集数据。这里有个关键点——要保证标定板在画面各个区域都出现,尤其是边缘和角落。我习惯录一段30秒左右的视频,缓慢移动标定板,覆盖整个视野。
标定命令其实很简单:
kalibr_calibrate_cameras \
--target april_6x6.yaml \
--bag cam_data.bag \
--models pinhole-equi \
--topics /camera/image_raw \
--bag-from-to 5 35
这里--bag-from-to 5 35的意思是取bag文件第5秒到第35秒的数据。为什么要跳过前几秒?因为刚开始录制时手抖得厉害,数据质量差。
标定完成后,会生成一个camchain.yaml文件,里面包含了相机内参和畸变系数。我一般会看一眼重投影误差,如果超过0.5个像素,说明数据质量有问题,需要重新采集。
输出示例(camchain.yaml关键字段):
cam0:
camera_model: pinhole
distortion_model: equidistant
intrinsics: [458.2, 457.8, 320.1, 240.5] # fx, fy, cx, cy
distortion_coeffs: [-0.283, 0.082, -0.001, 0.0004]
resolution: [640, 480]
reprojection_error: 0.32 # 像素,小于0.5就算合格
4.2 IMU噪声标定——Allan方差分析法
IMU的噪声参数,很多人直接抄芯片手册,但实际用起来往往对不上。为什么?因为芯片手册给的是理想环境下的值,你焊在板子上、装在设备里,噪声特性早就变了。
我建议自己标定。方法很简单——让IMU静止不动,采集2小时以上的数据,然后用Allan方差分析。
采集数据的命令:
rosbag record -O imu_static.bag /imu/data_raw
然后让设备静置至少2小时。嗯,这里要注意,一定要放在稳固的平台上,别放桌上,桌子可能被人碰到。我一般放地上,周围用东西围起来防止被踢到。
分析Allan方差的Python脚本,我常用这个:
import numpy as np
import matplotlib.pyplot as plt
from allantools import allan
# 读取IMU数据
data = np.loadtxt('imu_data.txt')
gyro_x = data[:, 0] # 陀螺仪x轴数据
# 计算Allan方差
(taus, adev, _, _) = allan(gyro_x, rate=200.0) # 200Hz采样率
# 拟合参数
# 噪声密度 = adev在tau=1处的值
# 随机游走 = adev在tau=3处的斜率
plt.loglog(taus, adev)
plt.xlabel('tau (s)')
plt.ylabel('Allan Deviation')
plt.grid(True)
plt.show()
从Allan方差曲线上,我们可以读出两个关键参数:
| 参数 | 含义 | 典型值(消费级IMU) |
|---|---|---|
| 噪声密度(noise_density) | 白噪声水平,单位:°/s/√Hz | 0.01 - 0.05 |
| 随机游走(random_walk) | 偏置稳定性,单位:°/s/√(Hz^3) | 0.0001 - 0.001 |
避坑指南:我曾经有一次标定结果特别差,查了半天发现是数据采集时空调风吹到了设备。IMU对气流非常敏感,哪怕微小的空气流动都会引入噪声。所以标定时最好关掉空调和风扇。
标定完成后,把参数写入imu.yaml:
# imu.yaml
accelerometer:
noise_density: 0.0015 # m/s^2/√Hz
random_walk: 0.00002 # m/s^2/√(Hz^3)
gyroscope:
noise_density: 0.0002 # rad/s/√Hz
random_walk: 0.000003 # rad/s/√(Hz^3)
4.3 相机-IMU外参标定——联合优化的艺术
外参标定,说白了就是搞清楚相机坐标系和IMU坐标系之间的旋转和平移关系。这一步做不好,后面VIO、SLAM全白搭。
Kalibr的外参标定需要同时输入相机数据和IMU数据。采集时要注意:
- 标定板要始终在视野内
- 运动要包含六个自由度(平移+旋转)
- 避免纯旋转,因为纯旋转对IMU激励不够
我一般这样采集:拿着设备走一个"8"字形,同时上下左右晃动,持续2-3分钟。说白了就是让IMU感受到各个方向的加速度和角速度。
标定命令:
kalibr_calibrate_imu_camera \
--target april_6x6.yaml \
--cam camchain.yaml \
--imu imu.yaml \
--bag data.bag \
--bag-from-to 10 130
这里--bag-from-to 10 130同样跳过了前10秒,因为刚开始运动时数据不稳定。
标定完成后,会输出外参矩阵。我一般会检查一下旋转矩阵的模是否接近1,平移向量的量级是否合理(通常几厘米到十几厘米)。
输出示例:
cam0_to_imu:
rotation:
- [0.9998, -0.0123, 0.0156]
- [0.0125, 0.9999, -0.0089]
- [-0.0155, 0.0091, 0.9998]
translation:
- [0.052, -0.018, 0.035] # 米
看到这个旋转矩阵接近单位阵,平移量5厘米左右,说明标定结果合理。如果旋转矩阵偏离单位阵太多,或者平移量达到几十厘米,那就要怀疑数据质量了。
4.4 结果分析与常见问题
标定完成后,我习惯做三件事来验证结果:
- 重投影误差检查:相机内参标定的重投影误差应小于0.5像素
- IMU积分验证:用标定后的IMU参数做纯积分,看短时间内位置漂移是否合理
- 联合初始化测试:把标定结果放到VIO系统中,看初始化是否稳定
我的小技巧:标定完成后,把设备放在桌上静止10秒,然后快速拿起、晃动、放下。看VIO系统能否准确回到初始位置。如果回环闭合误差小于5%,说明标定质量不错。
常见问题及解决方法:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 重投影误差>1像素 | 标定板不平整或图像模糊 | 更换标定板,检查相机对焦 |
| IMU积分发散太快 | 噪声参数不准 | 重新采集2小时以上静止数据 |
| 外参标定失败 | 运动激励不够 | 增加六自由度运动,避免纯旋转 |
| 时间戳不同步 | 硬件触发或驱动问题 | 检查相机和IMU的时间戳对齐 |
嗯,说到时间戳同步,这是个容易被忽略的坑。Kalibr要求相机和IMU的时间戳在同一个时钟域。如果用的是不同时钟源,一定要做硬件同步或者软件插值对齐。我遇到过最离谱的一次,相机用系统时间,IMU用板载时间,差了整整200毫秒,标定结果自然一塌糊涂。
最后总结一下我的标定流程:
- 第一步:打印Aprilgrid标定板,用哑光纸
- 第二步:采集相机标定数据,覆盖全视野
- 第三步:采集IMU静止数据,至少2小时
- 第四步:采集联合标定数据,六自由度运动
- 第五步:依次运行三个标定命令
- 第六步:验证结果,检查各项指标
整套流程走下来,大概需要半天时间。但磨刀不误砍柴工,标定做好了,后面融合的精度才能有保障。