
做传感器融合和机器人位姿解算这些年我踩过最大的坑就是“IMU数据没校准就往上怼”。拿到一块MPU6050或者ICM20602上电就读寄存器然后直接跑姿态解算结果就是静态误差大、yaw漂得飞快LiDAR和相机外参也怎么都对齐不了。前阵子翻到一篇论文标题是“A Robust and Easy to Implement Method for IMU Calibration without External Equipments”看完之后直接把之前的标定流程推翻了——不需要转台、不需要六面体、不需要水平台只需要让IMU静置在十几个不同姿态用重力当参考基准就能把加速度计的零偏、尺度因子、轴间非正交误差全估出来陀螺仪的零偏也能顺带解决。这篇文章我就围绕这个方法讲讲背后的数学模型、Python代码实现以及我在实操中踩过的坑。无论你是做位姿解算、相机IMU联合标定还是LiDAR-IMU外参标定IMU内参校准都是绕不开的前置步骤把这部分做扎实了后面的融合工作才能省心。1. 项目来龙去脉为什么不用外部设备也能校准1.1 这篇论文解决的核心问题市面上的IMU出厂前都会做标定但出厂标定只代表那一片传感器在出厂那一刻的“标准水平”。MEMS器件的零偏和尺度因子受温度、应力、老化的影响非常大同一颗芯片在不同环境下测出来的值可能差了好几个数量级。我实测过一批国产六轴模块常温下零偏0.05度/秒左右加热到60度之后直接飙到0.3度/秒这种误差如果你不做内参校准后面用什么高级算法都救不回来。传统的高精度标定需要转台把IMU装在精密旋转台上输入已知角速度反推尺度因子和轴间误差。转台便宜的一套也要几万块而且很多实验室根本没有。还有一些标定方法需要水平台、直角棱镜之类的辅助工装操作起来贼麻烦。这篇论文的价值在于它把标定问题变成了一个纯粹的数学优化问题。你不需要知道IMU当前处于什么姿态只需要保证它静止。因为地球重力是天然存在、恒定大小、无处不在的参考基准任何姿态下加速度计的真实读数模长都等于g这就是整个方法的物理基础。1.2 无外部设备标定的理论依据当你把IMU平放在桌上时加速度计的三个轴输出大约是(0, 0, g)这个大家都熟悉。但如果你把IMU竖着放、倒着放、斜着放加速度计三个轴的读数会变化但有一个东西始终不变——三个轴的平方和开根号也就是矢量的模长始终等于g约9.80665 m/s²。代码里处理原始数据时很多人会犯一个错误直接用加速度计的模长来检测运动状态。比如跑动过程中模长可能在8到12之间跳变。但静止时就算姿态任意理想加速度计模长应该严格等于g。这个特性就是多位置法的核心。打个比方你用一把尺子量物体的长度尺子本身有零位偏差相当于加速度计零偏、刻度不均匀相当于尺度因子误差、尺子是弯的相当于轴间非正交。你不知道物体的真实长度但你知道同一个物体的长度不会变。那你只要换十几个不同姿态去量这个物体就能反推出尺子的偏差、刻度和弯曲程度。1.3 从“椭球约束”到参数估计多位置法把每个静置姿态下的加速度计读数看成三维空间中的一个点。真实重力向量在空间中以不同姿态分布时读数的“真值”都在一个半径等于g的球面上。但受到零偏、尺度因子、轴间非正交的扭曲之后测量值不再落在球面上而是落在了一个椭球面上——球被拉伸、旋转、平移之后就是椭球。我们的标定任务就是采集一系列静置姿态下的测量点拟合出这个椭球的参数然后通过逆变换把椭球拉回标准球面。这个拟合过程用到的数学工具叫做椭球拟合本质上是一个线性最小二乘问题。论文里说“Easy to Implement”就是因为整个算法不需要迭代、不需要初值猜测只需要解一次超定方程就能得到全部参数。理解这一点之后加速度计的校准流程就非常清晰了采集N个静置姿态的数据、取平均降噪、构建设计矩阵、最小二乘求解、从椭球参数反解校正矩阵和零偏。2. 加速度计自动校准12参数模型与代码实现2.1 误差模型为什么是12个参数加速度计的实际输出和真实加速度之间的关系可以写成a_meas S × R × a_true b n其中S是对角矩阵表示三个轴的尺度因子误差R表示轴间非正交和安装误差是一个3×3的矩阵b是零偏向量n是测量噪声。S和R合起来相当于一个3×3的矩阵记作T那么误差模型化简为a_meas T × a_true bT矩阵一共有9个元素再加上b的3个元素总共12个参数。这就是IMU标定里常说的“12参数模型”。有的地方把T拆成尺度因子3个加上轴间非正交3个再加上零偏3个总共9个参数区别在于是否把交叉轴灵敏度单独算。但本质上用3×3矩阵统一表达更简洁代码实现也更好写。轴间非正交是什么概念理想情况下加速度计的三个轴应该两两垂直但MEMS器件的封装、贴片、焊接都会导致实际轴系和理想轴系有微小夹角。这些夹角会导致一个轴的加速度泄漏到另一个轴上去。生活化的理解就是理想房间的三个墙角都是90度但实际房间的角落可能略大于或小于90度你用这个房间的墙角当参照系去测方向就会产生系统性偏差。2.2 最少需要多少个静置姿态12个待估计参数理论上至少需要12个方程才能解出来。但每个静置姿态只能提供一个约束方程即模长平方等于g²所以数据点数至少得大于等于10才能保证超定方程组可解。为什么是10而不是12因为椭球方程对称矩阵只有6个独立元素加上线性项3个和常数项1个总共10个参数这和12参数模型是对应关系中间有维度简并。实际操作中我建议最少采集15到20个姿态。原因是姿态分布要尽可能均匀地覆盖三维空间如果只采集12个姿态又分布不好设计矩阵可能接近奇异解出来的参数会非常不稳定。曾经试过只放8个姿态而且大部分集中在Z轴附近结果拟合出来的椭球参数有一项差了30%完全没法用。摆放方式没有严格限制但我的经验是先让IMU平放Z轴向上、倒放Z轴向下、左倒、右倒、前倾、后倾各采一组再补一些45度斜放、不同方位角旋转的姿态。重点是要让加速度计的三个轴都能充分感知重力在各个方向上的投影不要全部落在同一个平面上。2.3 椭球拟合的线性最小二乘求解设静置姿态下的加速度计测量值为a [x, y, z]ᵀ真实重力向量满足||g_true||² g²。代入误差模型反解a_true T⁻¹(a - b)得到椭球约束(a - b)ᵀ M (a - b) g²其中M T⁻ᵀ T⁻¹是一个对称正定矩阵。展开之后得到aᵀ M a - 2bᵀ M a bᵀ M b - g² 0写成一般二次曲面方程A₁x² A₂y² A₃z² 2A₄xy 2A₅xz 2A₆yz 2A₇x 2A₈y 2A₉z A₁₀ 0这里的10个系数就是我们要求解的对象。每组测量数据代入后就构成一个线性方程。把所有N组数据堆叠起来就是一个超定线性方程组用最小二乘求解即可。import numpy as np def calibrate_accelerometer(samples): 基于多位置静置数据的加速度计自动校准 参数: samples: numpy数组, shape(N, 3) 每行是一个静置姿态下加速度计三轴读数的均值 返回: A_mat: 3x3 校正矩阵 bias: 3x1 零偏向量 校正公式: a_corrected A_mat (a_measured - bias) samples np.asarray(samples, dtypenp.float64) N samples.shape[0] if N 10: raise ValueError(至少需要10个静置姿态的数据点) x samples[:, 0] y samples[:, 1] z samples[:, 2] # 构建设计矩阵 G: 每行对应一个方程 # [x^2, y^2, z^2, 2xy, 2xz, 2yz, 2x, 2y, 2z] G np.column_stack([ x**2, y**2, z**2, 2*x*y, 2*x*z, 2*y*z, 2*x, 2*y, 2*z ]) # 求解 G p ones # 等价于拟合方程: G p 1, 即常数项归一化为-1 p, residuals, rank, svals np.linalg.lstsq(G, np.ones(N), rcondNone) # 二次型矩阵 A np.array([ [p[0], p[3], p[4]], [p[3], p[1], p[5]], [p[4], p[5], p[2]] ]) # 线性项系数 c np.array([p[6], p[7], p[8]]) # 椭球中心即零偏: A bias -c bias -inv(A) c bias -np.linalg.solve(A, c) # 半径平方: R^2 bias^T A bias 1 (因为常数项归一化为 -1) R2 bias A bias 1.0 R np.sqrt(R2) # 把椭球拉回标准球 # 先对A做特征值分解, A V diag(eig) V^T eigvals, eigvecs np.linalg.eigh(A) # 确保特征值为正 if np.any(eigvals 0): print(警告: 二次型矩阵非正定, 数据分布可能不合理) # 缩放矩阵: 对应椭球半轴长度的倒数 T eigvecs np.diag(1.0 / np.sqrt(eigvals)) eigvecs.T # 乘上半径因子, 让校正后模长精确等于 g A_mat T / R return A_mat, bias这段代码的核心就是10秒钟能跑完的最小二乘。lstsq函数内部用的是SVD分解数值稳定性很好不需要手动求逆。p的9个系数直接对应椭球方程的各项提取A矩阵和线性项后椭球中心就是零偏。有个细节值得注意为什么G矩阵里线性项是2x而不是x因为展开式(a-b)ᵀM(a-b)中交叉项展开会自然产生2的系数。这样做的好处是代码里的参数提取更规整A矩阵对角线和交叉项的对应关系一目了然。2.4 从椭球参数到校正矩阵的还原求解出A矩阵之后需要恢复T矩阵。从前面的定义A T⁻ᵀT⁻¹也就是说A的特征值开根号取倒数就是T的奇异值特征向量对应T的旋转方向。因此T V diag(1/sqrt(eigvals)) Vᵀ这个T矩阵就是椭球到单位球的线性变换。再除以半径R就得到了最终的校正矩阵A_mat。校正后的加速度计读数为a_corrected A_mat (a_measured - bias)注意这个校正公式是先减零偏、再乘校正矩阵。这个顺序很重要因为零偏是“加性误差”必须先扣除尺度因子和轴间误差是“乘性误差”后处理。我在代码里加了特征值检查。如果数据分布不好比如所有姿态都集中在一个小扇区内A的特征值可能为负说明拟合出来的二次型是双曲面而不是椭球。这种情况下求出的“半径”是虚数校正矩阵完全不可用。遇到这个问题先别怀疑代码先去补采几个姿态让分布更均匀。3. 陀螺仪校准与重力对齐3.1 陀螺仪零偏的快速估计陀螺仪校准比加速度计简单一些因为陀螺仪静止时的输出就是零偏。把IMU水平放置静止采集1000个样本取平均就得到了三轴零偏的估计值。这个值可以直接在姿态解算前减掉。def estimate_gyro_bias(gyro_samples_static): 静止状态下估计陀螺仪零偏 参数: gyro_samples_static: numpy数组, shape(M, 3) 静止状态下连续采样的角速度数据 返回: bias: 3x1 零偏向量, 单位与输入一致 gyro_samples_static np.asarray(gyro_samples_static, dtypenp.float64) bias np.mean(gyro_samples_static, axis0) return bias注意到我这里用的只是均值没有去中位数或截尾均值。因为静止采样数据中可能会有少量异常值中位数更稳健。实测下来如果采样环境振动干扰大取中位数比取均值效果明显好。可以在函数里加个开关实际用的时候看数据质量决定。零偏估计的精度取决于采样时间。理论上采样时间越长零偏估计的方差越小。但MEMS陀螺仪的零偏本身是随时间缓变的不是固定常数所以并不是采得越久越准。我的经验是对消费级IMU如MPU6050静置采样10秒就够了再久也不会带来明显提升反而拖慢流程。3.2 陀螺仪尺度因子的手动旋转法没有转台的情况下陀螺仪的尺度因子和轴间误差标定要稍微费点功夫。一个可行的方法是利用已知角度旋转。比如手动把IMU绕Z轴旋转90度理论上积分角速度应该得到90度积分值和90度之间的比值就是Z轴的尺度因子。具体操作流程是先把IMU静止放置记录姿态然后快速绕一个轴旋转尽可能接近90度再次静止记录姿态。用两个静止姿态之间的角度差作为参考角度与陀螺仪积分角度对比。但这里有个先有鸡还是先有蛋的问题参考角度怎么得到如果IMU还带有加速度计而且加速度计已经校准好了那么可以用加速度计在两段静止状态下的重力向量夹角来估计参考角度。这个方法虽然不如转台精确但是完全不需要外部设备符合论文的“without External Equipments”理念。def calibrate_gyro_scale(gz_samples, dt, gravity_vec_start, gravity_vec_end): 利用两段静止姿态之间的重力向量夹角, 估计Z轴陀螺仪的尺度因子 参数: gz_samples: 旋转过程中Z轴角速度采样数据 dt: 采样间隔 gravity_vec_start: 旋转前加速度计归一化向量 gravity_vec_end: 旋转后加速度计归一化向量 返回: scale_z: Z轴陀螺仪尺度因子修正系数 angle_reference np.arccos(np.clip( np.dot(gravity_vec_start, gravity_vec_end), -1.0, 1.0 )) angle_measured np.sum(gz_samples) * dt scale_z angle_reference / angle_measured if angle_measured 1e-6 else 1.0 return scale_z这个方法非常粗糙因为手动旋转的角速度很难保证只绕单一轴。如果轴间误差本身较大绕Z轴旋转时X轴和Y轴也会有角速度输出。更精确的做法是用三轴角速度同时积分得到旋转向量再对比姿态变化。但对于大多数应用场景陀螺仪尺度因子偏差通常在1%~3%这个量级手动法估算到5%以内的精度是够用的。3.3 重力对齐与yaw慢漂的本质“imu重力对齐”是很多同学在做位姿解算时经常听到的概念。它本质上是用加速度计校准后的重力方向来初始化或者修正姿态估计中的pitch和roll。重力对齐做得好不好完全取决于加速度计的零偏和尺度因子是否校准准确。如果加速度计零偏没校准最直接的后果就是水平姿态角有常数偏差。比如Z轴零偏0.1gpitch角就会误差大约5.7度。在视觉惯性融合或者LiDAR-IMU融合中这个水平角度误差会直接导致建图出现倾斜。至于“基于imu的位姿解算 yaw 仍会慢漂”这个问题的根源不在算法而在陀螺仪零偏。yaw角的观测来源一般是磁力计或者视觉/雷达辅助纯IMU积分模式下yaw不可观测只能靠陀螺仪积分。如果陀螺仪零偏没完全校准干净哪怕只残留0.01度/秒的零偏积分10分钟就会积累6度的yaw误差。所以做纯IMU位姿解算之前先把零偏估准是保质的第一步。4. 完整校准流程与实测效果评估4.1 数据采集自动判定静置姿态整个校准流程里面最费时间的其实是数据采集环节。如果只靠人手动切换姿态、手动记录不仅累还容易出错。可以写一个简单的自动采集脚本逻辑是检测IMU输出模长变化连续一段时间模长波动小于阈值就判定为当前姿态静止自动采集一段数据然后提示用户切换到下一个姿态。def auto_collect(imu, target_count15, static_threshold0.02, window50): 自动采集多姿态静置数据 参数: imu: IMU对象, 提供read_accel()方法 target_count: 目标采集姿态数 static_threshold: 静止判定阈值, 加速度模长波动标准差 window: 判定窗口大小 返回: samples: list, 每个元素是静态姿态的加速度均值 samples [] buffer [] while len(samples) target_count: accel imu.read_accel() buffer.append(accel) if len(buffer) window: buffer.pop(0) if len(buffer) window: arr np.array(buffer) norm np.linalg.norm(arr, axis1) if np.std(norm) static_threshold: # 达到静止条件, 再多采集100个样本取平均 static_samples [] for _ in range(100): static_samples.append(imu.read_accel()) time.sleep(0.01) samples.append(np.mean(static_samples, axis0)) print(f已采集 {len(samples)}/{target_count} 组姿态) time.sleep(2) # 提示用户去转动手持设备 else: time.sleep(0.01) return np.array(samples)静止阈值的选取很关键。我用0.02这个值是针对常见的震动环境下静态加速度模长标准差一般小于0.01动态运动时标准差大于0.05。如果你在非常平整的实验台上操作可以把阈值调到0.01提高灵敏度如果环境振动较大需要放宽到0.03~0.05否则会一直判定为“运动中”采集流程卡住。4.2 校准主程序串联数据采集完成之后整个校准主程序只需要几行代码。def run_calibration(imu, sample_count15): 完整IMU内参校准流程 # 1. 采集加速度计多姿态数据 accel_samples auto_collect(imu, target_countsample_count) # 2. 加速度计校准 A_accel, bias_accel calibrate_accelerometer(accel_samples) # 3. 采集静止数据估计陀螺仪零偏 gyro_static [] for _ in range(200): gyro_static.append(imu.read_gyro()) time.sleep(0.01) bias_gyro estimate_gyro_bias(np.array(gyro_static)) # 4. 如需更高精度, 可手动旋转补采陀螺仪尺度因子 # scale_z calibrate_gyro_scale(...) calib_result { accel_matrix: A_accel, accel_bias: bias_accel, gyro_bias: bias_gyro } return calib_result这种代码结构通用性很强。无论是串口连接的独立IMU模块还是ROS话题发布的数据只要你把read_accel和read_gyro换成自己的读取方式其他部分直接复用。我自己在嵌入式端和PC端都跑过这套逻辑只需要维护不同的数据接口层。4.3 校准效果怎么评估校准做没做好不能光看代码跑完没报错要有量化的指标。最直观的评估方法是采集一组独立的验证数据注意不能用训练数据评估分别计算校正前后的加速度模长方差。def evaluate_calibration(samples, A_mat, bias): 评估校准效果, 返回校正前后的模长统计 before_norm np.linalg.norm(samples, axis1) corrected (A_mat (samples - bias).T).T after_norm np.linalg.norm(corrected, axis1) print(校正前模长均值: {:.4f}, 标准差: {:.4f}.format( np.mean(before_norm), np.std(before_norm) )) print(校正后模长均值: {:.4f}, 标准差: {:.4f}.format( np.mean(after_norm), np.std(after_norm) )) return before_norm, after_norm我实测过一套典型的消费级模块数据校正前模长标准差约0.15~0.2均值和g的偏差约0.1左右校正后标准差降到0.005以内均值非常贴近9.80665。如果校正后标准差还大于0.01先检查数据采集时是否真的静止再检查姿态分布是否均匀。另外一个评估角度是水平姿态角的静态精度。把校准后的IMU放在水平台上解算pitch和roll理想情况下应该都是0度附近。校准前可能偏差5~10度校准后一般能控制到0.2度以内这个精度对于大多数机器人应用足够了。4.4 实操过程中的几个坑第一个坑数据采集的时候不要用手捏着IMU。手上的抖动虽然是微小的但会叠加在重力向量上相当于给每个姿态的采样点引入了随机误差。最好是找一个平整的小块用橡皮筋把IMU绑在上面然后用手扶着块体不同面去摆放这样既保证了姿态变化又减少了直接传到手部的抖动。第二个坑自动采集流程中切换姿态之后的第一个200ms的数据不要用。因为手切换姿态的过程必然有运动加速度即使采样窗口判定为“静止”前几个样本可能还带有减速过程中的残余运动。我是在静止判定通过后再等2秒才开始采均值数据牺牲一点采集时间换来更高的数据质量很划算。第三个坑不要在一个水平面上打转。把IMU放在桌面上绕Z轴转几个角度看起来采集了很多姿态其实重力方向始终沿着Z轴所有数据点在三维空间里都落在同一个水平圆上。这种情况下属性是退化数据解出来的M矩阵不唯一。一定要让重力在三个轴方向上都有充分的投影分量也就是要有“立起来”的摆放姿态。5. 常见问题与排查技巧实录5.1 问题速查表我把这段时间遇到的典型问题和对应解法整理成了一张表方便检索问题现象可能原因解决方案拟合出的A矩阵特征值为负静置姿态分布不够广数据集中在某个局部区域增加姿态数量确保覆盖多个象限尤其加入45度斜放姿态校正后模长标准差大0.01数据采集时存在微小振动或未完全静止加长静止等待时间绑定在平整块体上操作校正矩阵元素异常大10某个轴向几乎没有数据覆盖最小二乘解不稳定检查姿态是否有“立起来”的状态避免全部平放陀螺仪零偏减掉后yaw仍然缓慢漂移温度变化导致零偏随时间缓变非恒定值开机预热几分钟再校准或使用温度补偿模型陀螺仪尺度因子标定后误差反而变大手动旋转时没有过绕单一轴积分结果包含其他轴分量改用旋转前后姿态四元数差作为参考或直接放弃尺度因子标定误差小时影响有限与LiDAR/相机联合标定结果不稳定IMU内参不准确传导到外参估计先做IMU内参校准并用验证集评估确认内参可靠后再做外参标定5.2 一次典型的失败案例复盘我遇到最诡异的一次是同一颗IMU校准两次得到完全不同的零偏差了一倍。排查半天发现两次校准的环境温度差了好几度。MEMS加速度计的零偏温度系数很大可能达到0.5mg/℃按这个系数估算温差10度就会带来5mg的零偏变化换算成重力模长误差就是0.005g足够影响姿态解算的精度了。从那以后我养成了两个习惯第一校准前先把IMU通电预热至少3分钟等内部温度稳定后再开始采数据第二标定结果和使用环境温度尽量保持一致如果设备工作环境温度范围很大那就按不同温度点分别做标定插值使用。消费级IMU没有温控电路这个方法虽然笨但实测有效。另一个值得注意的情况是有时候校准代码跑出来结果似乎正常但校正后数据在特定姿态下有明显的“方向性偏差”。比如水平放置时很好倒放时就偏差比较大。这种情况通常是轴间非正交参数没有估准把手臂竖直举起来、让Z轴水平横放、再倒过来横放的姿态多采几组能够大大提升非正交参数的观测性。5.3 与多传感器标定的联动经验做完IMU内参校准之后再做LiDAR-IMU外参标定或者相机IMU联合标定成功率会高很多。我见过太多人在跑标定工具比如一些开源的外参标定包时出来的结果反复横跳一个常见原因就是IMU内参没校准到位。外参标定工具的内核是基于优化的IMU内参误差会被当作外参误差去补偿最终结果就是两种误差混在一起分都分不开。还有一点Gravity Alignment在视觉惯性初始化中也会用到。当你有相机和IMU时系统启动阶段需要估计IMU零偏以及重力向量。如果加速度计内参是歪的初始化阶段估算出来的重力方向和尺度就是歪的后面整个VIO系统的轨迹会在地面上漂移。所以我的建议是不管最终做视觉还是雷达方案IMU内参校准都是必做的第一步顺序千万别反。结尾前前后后把这套方法在好几颗不同型号的IMU上跑过实际体验和论文标题说的一样确实Robust且Easy to Implement。整个校准过程从采数据到出结果熟练了十分钟之内就能搞定对设备的要求不过是一块能放稳的小平面以及匀出十个不同方位去摆放它。我个人在实际操作中的体会是这套方法最大的价值并不在于“顺便省了转台的钱”而是它让校准这个原本需要实验条件的环节变成了一项日常维护操作——换了一批传感器、换了一块板子、甚至只是温度变化明显我都可以随手重新标一遍不再需要求人借设备或者对着工具手册研究半天。最后再分享一个小技巧校准完把校正矩阵和零偏存成固定格式的参数文件在代码里预留一个加载参数的接口后续每一次开机都自动加载同时把“是否完成校准”的检查逻辑加在启动流程里。你会发现因为校准问题导致的数据异常排查工作量几乎是断崖式下降。