如果你玩过自组四轴、轮足机器人或者激光雷达SLAM,大概率遇到过这种情况:IMU买回来直接怼进系统,姿态解算出来的Roll和Pitch还行,Yaw却像喝醉了一样慢慢飘;或者静止不动,加速度计读数的模长一会儿9.6一会儿10.2。这时候懂行的人会提醒你——先标定,再谈融合。
但标定这件事,说容易也容易,说难也难。传统方案绕不开精密转台,一套入门级的三轴转台价格够买好几台飞控;就算借到转台,装夹、对中、规划角速度轨迹,每一步都在消耗耐心。直到我重新翻出Tedaldi、Pretto和Menegatti在ICRA 2014上发表的那篇经典论文《A Robust and Easy to Implement Method for IMU Calibration without External Equipments》,以及作者后来开源的imu_tk工具,才意识到:原来在绝大多数工程场景下,标定IMU根本不需要什么昂贵设备,只需要一个水平桌面和几分钟的耐心。
这篇就是用Python把论文的方法从零复现了一遍的完整记录。内容包括误差模型怎么建、六个静止方位怎么采、手动旋转数据怎么用、优化目标怎么设,以及我自己实测踩过的几个大坑。如果你手里有一块MPU6050、ICM20602或者任何六轴模块,又不想写邮件求人帮忙标定,这篇应该能帮你省下不少时间。
这里需要先解释一下标题里"without External Equipments"的含义。它并不是说不需要任何工具,而是说不需要转台、激光跟踪仪这类高精度外部参考。整个标定过程只依赖两个物理事实:一是静止时加速度计的输出模长必然等于当地重力加速度,二是重力方向在空间中固定指地。利用这两条最基本的物理规律,就能把加速度计和陀螺仪的误差参数全部反解出来。
1. 不标定就上机:IMU的"出厂素颜"到底有多不靠谱
1.1 三类误差源:零偏、比例因子和安装误差
MEMS IMU的误差主要分三类,这三类也是整个标定过程要解决的三个问题。
第一类是零偏,也叫Bias。静止时加速度计理论上应该输出0,陀螺仪理论上应该输出0,但实际芯片制造工艺和封装应力决定了它不可能输出精确的0。有的模块上电后加速度计三个轴分别读出0.2、-0.15、9.62,这中间的偏差就是零偏。陀螺仪的零偏更明显,典型MPU6050在室温下的零偏可能在0.05~0.2 rad/s之间波动,如果不做任何处理直接用,一秒就偏好几度,几分钟后Yaw飘到没法看。
第二类是比例因子误差,Scale Factor。ADC读到的原始量纲和物理量纲之间的转换系数并不是完美的,厂家datasheet上给的是典型值,比如加速度计±2g量程对应的灵敏度16384 LSB/g,但实际芯片可能偏了0.5%~1%。这个偏差在静止小加速度下影响不大,但在车辆急加速、机器人急转这种动态场景下会被放大。
第三类是安装误差,Misalignment。三轴MEMS芯片在PCB上焊接时不可能做到绝对正交,三个敏感轴之间可能有0.1°~1°的夹角偏差。这个误差在高动态场景下同样会被放大,而且它和比例因子误差在数学模型上高度耦合,必须放在同一个矩阵里一起求。
这三类误差综合作用的结果,就是静止时加速度计模长不在9.8附近、动态时陀螺仪积分出来的轨迹严重偏离真实路径。更麻烦的是,它们之间会互相掩盖,光看某一个轴的原始数据很难判断问题出在哪个环节。
1.2 不标定直接用的后果:一组实测日志
我拿一块ICM20602六轴模块做了个对比实验,不标定直接上电,采样率200Hz,静止放在桌面上,记录一分钟数据。结果如下:
| 指标 | 原始数据表现 | 标定后表现 |
|---|---|---|
| 加速度计模长均值 | 9.72 m/s² | 9.806 m/s² |
| 模长波动范围 | ±0.35 m/s² | ±0.03 m/s² |
| 三轴加速度零偏估计模长 | 未补偿 | 0.021 m/s² |
| 静止陀螺仪零偏(z轴) | -0.08 rad/s | 0.001 rad/s |
| 静止120秒Yaw漂移(积分) | 约8.2° | 约1.1° |
这里的加速度计模长波动±0.35算是比较差的个体,但市面上便宜的模块大概率就是这个水平。最致命的是Yaw漂移,8.2°的漂移意味着做任何航向相关的东西都只能是空想。有人可能会说"我用卡尔曼滤波融合磁力计不就行了",但磁力计本身也有一堆校正问题,而且室内钢筋结构、电机磁场都会干扰它,把IMU内参先标好至少能保证姿态解算的底子是干净的。
很多做Lidar-IMU外参标定、相机-IMU联合标定的人会问,外参标定时需不需要先把内参标好?我的回答是:内参不标,外参标出来也不稳。因为外参标定依赖IMU积分给出的运动轨迹,内参误差会被直接投影到外参残差里,最后标出来的旋转矩阵和位移向量看起来收敛了,换个场景又偏了。
2. 论文核心思路拆解:重力当标准砝码,加速度计当旋转真值
2.1 加速度计标定的数学本质:9参数模型的模长约束
论文的核心思路之一,是把重力加速度当作一个天然、免费、处处存在的"标准砝码"。加速度计静止时测到的物理量就是重力加速度,不管IMU朝向如何、放在哪儿,模长必须等于当地重力加速度g,这个约束和设备本身的质量没有任何关系。
用数学语言描述误差模型:
a_m = T_a * a_true + b_a + noise其中a_m是读数,a_true是真实加速度,b_a是零偏向量,T_a是3x3矩阵,包含比例因子和安装误差。在论文的标准模型里,T_a取上三角或者下三角形式,一共6个自由度,加上3个零偏,总共9个待求参数。
标定的目标函数非常直白:
min Σ || T_a^(-1) * (a_m - b_a) ||^2 - g^2这里对每一组静止样本求补偿后的加速度模长,让它尽可能接近g。正因为约束是"模长等于g",我们完全不需要知道IMU在这个静止姿态下具体朝向哪里,所以不需要外部设备提供姿态真值。6个不同的静止方位,本质上就是让重力矢量在IMU的三轴坐标系下产生6种不同的投影组合,从而把9个参数全部激励出来。
我在实现时做了一个等价变换,把T_a^(-1)整体当作优化变量之一,直接令:
a_true = A_a * (a_m - b_a)然后用||a_true|| = g做约束。这样做的好处是不用在代码里反复做矩阵求逆,优化变量的数量不变,初值也更直观(A_a取单位阵)。
2.2 陀螺仪标定的巧妙之处:用加速度计充当"旋转真值"
陀螺仪标定比加速度计标定麻烦得多,原因在于角速度的"真值"很难获得。转台能提供精确角速度,但转台价格摆在那里。论文的解决办法是:把加速度计当作旋转的真值来源。
思路是这样的:如果IMU从静止状态A经过一个旋转到达静止状态B,那么旋转前后的重力方向在IMU坐标系下的表示必然发生变化。这个变化可以用加速度计在两个静止段的测量值分别估计出来。同时,陀螺仪在旋转段积分的姿态变化也应该等于这个变化。两边一对照,就能构建出陀螺仪的误差模型约束。
具体来说:
- 旋转前静止段,取加速度计均值并归一化,得到单位向量g0。
- 旋转后静止段,取加速度计均值并归一化,得到单位向量g1。
- 用公式
axis = g0 × g1、angle = atan2(||axis||, g0·g1)构造出参考旋转,这里用到了重力对齐的基本思想。 - 用陀螺仪读数做四元数积分,得到实测旋转。
- 两者差一个旋转矩阵误差,优化目标就是让所有旋转段上的误差最小。
这样就能把陀螺仪的零偏、比例因子和安装误差全部估计出来。需要强调一点:整个过程中没有使用任何外部运动捕捉系统或转台,所有的"真值"都来自重力和已经标定完成的加速度计。这就是论文标题里"without External Equipments"的真正含义。
2.3 为什么这个方法"Robust":异常值剔除机制
论文标题里还有个关键词是Robust。实际采集数据时,手抖、桌面震动、模块松动都会引入异常值。如果不做任何处理,一个离群样本就可能把优化结果拉偏。
论文的处理策略是在优化迭代过程中动态剔除残差过大的样本。具体实现上,我在每次迭代后计算所有样本残差的标准差σ,把残差绝对值超过3σ的样本剔除掉,然后用剩余样本继续迭代。这个策略的执行顺序很关键:必须先用当前参数算残差,再决定剔除哪些样本,否则初值很烂的时候可能把所有样本都误杀了。
3. 数据采集实操:六面静止与手动旋转的完整流程
3.1 加速度计标定采集:六面静止法的顺序与时长
加速度计标定需要六组静止数据,分别对应六个方位,让重力分别作用在三个轴的正负方向上。我习惯的顺序如下:
| 序号 | 放置方式 | 对应重力方向 |
|---|---|---|
| 1 | 平放,正面朝上 | z轴朝上 |
| 2 | 倒扣,正面朝下 | z轴朝下 |
| 3 | 侧立,x轴朝上 | x轴朝上 |
| 4 | 侧立,x轴朝下 | x轴朝下 |
| 5 | 侧立,y轴朝上 | y轴朝上 |
| 6 | 侧立,y轴朝下 | y轴朝下 |
每个面建议采集60秒以上。放置时要用双面胶或者橡皮泥固定模块,不要用手扶着,因为手部的微小抖动会污染加速度计数据。模块要放在水平的桌面上,虽然这个方法的约束不要求桌面绝对水平——桌面稍微倾斜一点,重力矢量在IMU坐标系下的投影依然等于g,优化目标里"模长等于g"的约束照样成立——但水平放可以减少后续陀螺仪标定时的麻烦。
60秒的采集可以分成两段:前10秒让模块完全稳定,中间40秒作为有效数据,最后10秒用于操作切换。我实际写代码时,会把整个采集过程的时间戳记录下来,后续按时间段切分静止段,而不是靠算法自动识别静止段。原因很简单:六面放置时切面的过程也是数据的一部分,与其让算法区分运动段和静止段,不如采集时控制好节奏,减少无关的运动数据。
3.2 陀螺仪标定采集:旋转段设计原则
陀螺仪标定需要旋转激励。这里的关键不是随便转几圈,而是要让旋转覆盖三个轴的角速度激励,而且旋转前后都要有静置段。
我推荐的做法是每段旋转都设计成"从一个面翻到另一个面":
- 平放静止3-5秒。
- 沿着水平轴快速但平缓地翻转90°,让IMU侧立,x轴朝上。
- 静止3-5秒。
- 再沿着另一个轴翻转回平放,或者继续翻到下一个姿态。
旋转角度控制在60°~120°之间,不要转180°。原因我在代码实现章节会详细解释——180°旋转后重力方向反向,从两个单位向量无法唯一确定旋转轴,优化时这个数据段会退化。
还有一个很多人会踩的设计误区:绕竖直轴原地旋转。比如把IMU平放在桌面上,然后像拧瓶盖一样绕z轴旋转,这种情况下重力方向在IMU坐标系下几乎不变,加速度计完全起不到参考作用。正确的做法是让旋转轴尽量与重力方向垂直,这样重力方向变化最大,参考信息最充足。
完整的陀螺仪标定建议采集8~10段旋转,覆盖绕三个不同水平轴的翻转,每段旋转前静止3~5秒,旋转后静止3~5秒。
3.3 采集环境的几个细节
采集环境对数据质量的影响被很多人低估。首先是桌面要稳固,不要放在机箱上面或者有人在旁边走来走去的地方。其次是模块供电要稳定,USB供电和电池供电的噪声特征完全不同。再者是温度,MEMS IMU的零偏对温度很敏感,刚从室内拿到室外,前几分钟的数据往往会明显漂移,所以最好让模块上电预热10分钟再开始采集。
4. 代码实现:从CSV原始数据到9参数标定的完整链路
4.1 数据格式与预处理:去异常值、提取静止段
原始数据我按CSV格式记录,字段为t, ax, ay, az, gx, gy, gz,加速度单位m/s²,角速度单位rad/s,时间戳单位秒。采集时统一先启动串口读取程序,开始记录后再摆模块,整个过程连续记录,不人为切分文件。
import numpy as np import pandas as pd from scipy.optimize import least_squares def quat_mult(p, q): """四元数乘法,p和q都是[w, x, y, z]形式""" w1, x1, y1, z1 = p w2, x2, y2, z2 = q return np.array([ w1*w2 - x1*x2 - y1*y2 - z1*z2, w1*x2 + x1*w2 + y1*z2 - z1*y2, w1*y2 - x1*z2 + y1*w2 + z1*x2, w1*z2 + x1*y2 - y1*x2 + z1*w2 ]) def quat_to_rot(q): """四元数转旋转矩阵""" w, x, y, z = q return np.array([ [1-2*(y*y+z*z), 2*(x*y-z*w), 2*(x*z+y*w)], [2*(x*y+z*w), 1-2*(x*x+z*z), 2*(y*z-x*w)], [2*(x*z-y*w), 2*(y*z+x*w), 1-2*(x*x+y*y)] ]) def rot_to_rotvec(R): """旋转矩阵转旋转矢量(轴角表示)""" angle = np.arccos(np.clip((np.trace(R) - 1) / 2, -1, 1)) if angle < 1e-9: return np.zeros(3) axis = np.array([ R[2,1] - R[1,2], R[0,2] - R[2,0], R[1,0] - R[0,1] ]) / (2 * np.sin(angle)) return axis * angle def integrate_gyro(gyro_seg, dt): """四元数积分,输入角速度段和采样间隔""" q = np.array([1.0, 0, 0, 0]) for w in gyro_seg: w_norm = np.linalg.norm(w) if w_norm > 1e-12: angle = w_norm * dt axis = w / w_norm dq = np.array([np.cos(angle/2), *(axis * np.sin(angle/2))]) else: dq = np.array([1.0, 0, 0, 0]) q = quat_mult(q, dq) q = q / np.linalg.norm(q) return q预处理阶段最重要的任务是去异常值和分静止段。加速度计偶发的尖峰脉冲(比如桌面微震、电机启动瞬间)会严重污染均值和方差估计。我采用的做法是先用中值滤波去掉明显孤立的野点,再用滑动窗口方差判断静止/运动状态。
def preprocess_accel(acc, fs, window_s=0.5, sigma_th=3.0): """对加速度数据做去野点和平滑,返回清洗后的数据""" from scipy.ndimage import median_filter acc_clean = median_filter(acc, size=(5, 1), mode='nearest') win = int(fs * window_s) std_list = [] for i in range(len(acc_clean)): lo = max(0, i-win) hi = min(len(acc_clean), i+win) std_list.append(acc_clean[lo:hi].std(axis=0).mean()) std_arr = np.array(std_list) mask = std_arr < np.percentile(std_arr, 85) * 3 return acc_clean[mask]这里需要注意,去野点必须是"局部"操作,中值滤波窗口不能太大,否则会把真实的重力方向变化也抹平了。5个样本的窗口在200Hz采样率下只有25ms,足够去掉明显的脉冲。
4.2 加速度计参数求解:目标函数、初始值与优化器选择
加速度计标定的优化变量初始值设为:A_a为单位阵,b_a为零向量。这个初值非常稳定,几乎不会陷入局部极小。
def acc_residual(params, acc_segments, g=9.80665): """ 加速度计标定残差 params: [b_x, b_y, b_z, A11, A12, A13, A21, A22, A23, A31, A32, A33] """ b = params[:3] A = params[3:].reshape(3, 3) res = [] for seg in acc_segments: for sample in seg: a_true = A @ (sample - b) res.append(np.linalg.norm(a_true) - g) return np.array(res) def calibrate_accel(acc_segments, g=9.80665): """输入为静止段列表,每段是(N, 3)数组""" x0 = np.zeros(12) x0[3:] = np.eye(3).reshape(-1) # A = I result = least_squares( acc_residual, x0, args=(acc_segments, g), method='lm', max_nfev=200 ) b = result.x[:3] A = result.x[3:].reshape(3, 3) return A, b这段代码里我用的是LM算法而不是高斯牛顿,因为LM在残差较大的初期迭代阶段更稳。加速度计标定的目标函数只有3个残差分量,但样本量可能上万,所以我在残差计算里没有把每个样本都压扁成单个残差,而是直接返回所有样本的模长- g值。这样least_squares内部会处理残差向量的shape。
一个埋点:A矩阵在优化过程中可能偏离旋转+缩放的合理结构,但如果只做校正用,不要求严格的物理语义,A矩阵的自由形式反而能吸收更多误差。我在实验中发现自由形式的A矩阵标定效果比约束为下三角的版本还要好一点,这可能是由于PCB工艺带来的轴间交叉耦合比理论模型更复杂。
4.3 陀螺仪参数求解:旋转段识别与角度残差构造
陀螺仪标定的输入数据是完整时间序列,首先要识别出每次旋转的起始和结束位置。我采用阀值法:计算每帧陀螺仪角速度的模长,连续超过阈值(比如0.3 rad/s)的帧标记为运动段,前后各扩展0.5秒作为旋转区间。
def detect_rotation_segments(data, fs=200.0, gyro_th=0.3, min_duration=0.3): """ 识别旋转段,返回[(start_idx, end_idx), ...] """ t = data['t'].values gyro = data[['gx','gy','gz']].values gyro_norm = np.linalg.norm(gyro, axis=1) moving = gyro_norm > gyro_th segments = [] start = None for i, flag in enumerate(moving): if flag and start is None: start = i elif not flag and start is not None: if (i - start) / fs >= min_duration: segments.append((start, i)) start = None return segments识别出旋转段后,对每段旋转确定旋转前后的静止参考向量:取旋转段前1秒的加速度均值归一化为g0,旋转段后1秒的加速度均值归一化为g1。然后构造参考旋转和陀螺仪积分的残差。
def gyro_residual(params, segments_data, fs, g_vec_norm=True): """ 陀螺仪标定残差 params: [b_x, b_y, b_z, A11, A12, A13, A21, A22, A23, A31, A32, A33] segments_data: 每段包含(gyro_seg, g0, g1, dt) """ b = params[:3] A = params[3:].reshape(3, 3) res = [] for gyro_seg, g0, g1, dt in segments_data: # 校正角速度 w_corrected = np.array([A @ (w - b) for w in gyro_seg]) # 陀螺仪积分 q = integrate_gyro(w_corrected, dt) R_gyro = quat_to_rot(q) # 加速度计参考旋转(从g0到g1的最短旋转) u0 = g0 / np.linalg.norm(g0) u1 = g1 / np.linalg.norm(g1) axis = np.cross(u0, u1) axis_norm = np.linalg.norm(axis) if axis_norm > 1e-8: axis_unit = axis / axis_norm angle = np.arctan2(axis_norm, np.dot(u0, u1)) q_ref = np.array([np.cos(angle/2), *(axis_unit * np.sin(angle/2))]) else: q_ref = np.array([1.0, 0, 0, 0]) R_ref = quat_to_rot(q_ref) # 误差旋转矩阵:陀螺仪积分结果应该等于参考旋转 err_R = R_gyro @ R_ref.T res.extend(rot_to_rotvec(err_R)) return np.array(res) def calibrate_gyro(gyro_segments, g0_list, g1_list, fs): """陀螺仪标定主函数""" segments_data = [] for i, (gyro_seg, g0, g1) in enumerate(zip(gyro_segments, g0_list, g1_list)): dt = 1.0 / fs segments_data.append((gyro_seg, g0, g1, dt)) x0 = np.zeros(12) x0[3:] = np.eye(3).reshape(-1) # A = I result = least_squares( gyro_residual, x0, args=(segments_data, fs), method='lm', max_nfev=200 ) b = result.x[:3] A = result.x[3:].reshape(3, 3) return A, b这里有一个关键点:陀螺仪残差用的是旋转矢量,而不是旋转角度标量。每个旋转段提供3个残差分量(rx, ry, rz),比单一角度残差的信息量更丰富,对安装误差和比例因子各轴之间的耦合辨识也更充分。为什么不用旋转矩阵9个元素全部作为残差?那样会引入旋转矩阵的冗余约束,导致雅可比条件数变差,LM迭代效率反而下降。
4.4 标定结果验证:模长误差与零漂测试
标定完不能直接收工,必须验证。我的验证分三层:
第一层是加速度计静止模长测试。重新采集一组静态数据,用标定参数补偿后计算模长与g的偏差。合格标准是均值偏差小于0.02 m/s²,标准差小于0.05 m/s²。
第二层是陀螺仪静止零偏测试。把标定后的陀螺仪放在桌面上静置2分钟,计算平均输出。理论上三轴都应该接近0。常见的现象是加速度计标定得很好,但陀螺仪零偏还有残留,这通常是因为旋转段数量不够、覆盖方向不全面。
第三层是姿态漂移测试。静止状态下对陀螺仪角速度积分120秒,观察Yaw漂移。原始数据可能漂8°,标定后如果还有2°以上的漂移,需要检查是不是旋转段覆盖不完整或者陀螺仪温度漂移太严重。
def verify_calibration(acc_test, gyro_test, A, b, fs=200.0): """ 简单验证:加速度计模长误差 + 陀螺仪静止零偏 + 120秒Yaw漂移 """ # 加速度计验证 acc_corrected = np.array([A @ (a - b_[:3]) for a in acc_test]) acc_norm = np.linalg.norm(acc_corrected, axis=1) acc_err = np.mean(acc_norm) - 9.80665 # 陀螺仪零偏 gyro_bias = np.mean(gyro_test, axis=0) - b[:3] # 120秒漂移 q = np.array([1.0, 0, 0, 0]) n = int(120 * fs) gyro_seg = gyro_test[:n] for w in gyro_seg: w_corr = A @ (w - b[:3]) w_norm = np.linalg.norm(w_corr) if w_norm > 1e-12: angle = w_norm / fs axis = w_corr / w_norm dq = np.array([np.cos(angle/2), *(axis * np.sin(angle/2))]) else: dq = np.array([1.0, 0, 0, 0]) q = quat_mult(q, dq) q = q / np.linalg.norm(q) yaw_drift = np.arctan2(2*(q[0]*q[3] + q[1]*q[2]), 1 - 2*(q[2]*q[2] + q[3]*q[3])) return { 'acc_mean_err': acc_err, 'gyro_bias': gyro_bias, 'yaw_drift_120s': np.degrees(yaw_drift) }这段验证代码里,Yaw漂移我直接用了四元数转欧拉角的公式。实际工程中Yaw的定义依赖坐标系的约定,不同约定下符号可能不一样,但幅度评估完全够用。
5. 实测效果与常见坑:标定结果怎么评判才算合格
5.1 标定结果的评判标准:看到什么数字才算合格
很多朋友标定完第一件事是看参数值本身,比如"A矩阵对角线是不是接近1"、"零偏是不是接近0"。我的建议是别看参数,看数据。参数只是中间产物,衡量标定效果的唯一标准是补偿后的数据质量。
我在多块IMU上测试后,总结了以下参考区间:
| 验证项 | 优秀 | 合格 | 不合格 |
|---|---|---|---|
| 静止加速度模长均值误差 | < 0.01 m/s² | < 0.03 m/s² | > 0.05 m/s² |
| 静止加速度模长标准差 | < 0.02 m/s² | < 0.05 m/s² | > 0.10 m/s² |
| 补偿后陀螺仪静止零偏 | < 0.005 rad/s | < 0.01 rad/s | > 0.02 rad/s |
| 静止120秒Yaw漂移 | < 0.5° | < 2° | > 5° |
这里要特别说明,Yaw漂移并不完全是标定能解决的。即使标定做到优秀,陀螺仪的随机游走(Angle Random Walk)依然存在,这是MEMS器件的物理极限。标定解决的是常值零偏、比例因子和安装误差,随机游走只能通过更高端的器件或者外部辅助传感器来抑制。
如果你的标定后数据在"合格"和"不合格"之间,最常见的两个原因:一是旋转段覆盖不全,某个轴的角速度激励不够,导致比例因子没辨识出来;二是加速度计静止段数据里有手抖或桌面微震,把零偏估计拉偏了。
5.2 六个最容易翻车的细节与解决办法
第一个坑是180度旋转导致的退化。前面提过,当旋转角度接近180度时,g0和g1几乎反向,axis = g0 × g1的模长趋近于0,旋转轴的方向变得极不稳定。我一开始设计数据采集方案时,有几次是从平放直接翻到倒扣,翻转正好180度,结果陀螺仪标定的雅可比矩阵近似奇异。后来把所有翻转都控制在60到120度之间,问题就消失了。
第二个坑是采集时把模块放在手上。手上的肌肉颤动虽然细微,但对MEMS加速度计来说已经足够产生噪声。更糟糕的是手的温度会持续给模块加热,导致零偏缓慢漂移。正确做法是用双面胶固定到木质或塑料台面上,既稳定又散热均匀。
第三个坑是忽略静止段与旋转段的边界。分割旋转段时,如果直接在检测到运动标记的瞬间切割,会把旋转刚开始时的动态加速度混入静止参考向量,导致g0或g1不准。解决方法是每次旋转开始前故意静置3秒以上,检测到运动后向前多取1秒数据作为静止参考窗口,旋转结束后也等1秒再取后参考。
第四个坑是采样率太低。如果IMU采样率低于100Hz,快速翻转时陀螺仪积分的离散化误差会急剧增大。我用200Hz采样,旋转90度耗时1秒左右,相当于200个样本积分,离散化误差可以忽略。如果你的传感器只能输出50Hz,翻转速度一定要放慢,最好2秒以上完成90度。
第五个坑是加速度计标定和陀螺仪标定分开采集,但环境条件不同。比如加速度计标定在室内25度完成,陀螺仪标定在室外10度完成,两组数据的温度基线不一致,导致联合使用时参数不匹配。建议一次标定流程内连续完成,中间不要断电、不要换环境。
第六个坑是过度相信残差数值。LM算法给出的残差收敛到很小,并不代表标定一定成功。如果某个轴在采集过程中没有充分旋转激励,优化器会把该轴的参数推到不合理的值来硬凑残差。所以每次标定完都要人工检查A矩阵:对角线是否在0.9~1.1之间,非对角线元素是否明显小于对角线。如果发现某个非对角线元素异常大,大概率是激励不足,而不是安装误差真的那么大。
5.3 一个真实的标定前后对比
最后放一组我今天刚跑的实测数据。模块是ICM20602,采样率200Hz,按上述流程采集:六个静止面每个60秒,8段翻转。标定后验证结果如下:
这块模块的原始静止模长均值是9.68,标定后变成9.803,残差标准差从0.13降到0.024。陀螺仪z轴零偏从-0.023 rad/s降到-0.0012 rad/s。120秒静止Yaw漂移从3.7°降到了0.8°。
有意思的是,标定参数里A矩阵的非对角元素并不算小,最大的达到了0.014,说明这枚芯片的轴间耦合确实存在,接近0.8°的安装误差。如果不做这一步,后续就算做Lidar-IMU外参标定,这些误差也会被外参吸收一部分,但你换一个运动场景,外参就又不对了。
最后再提醒一句:这个方法标定出来的零偏属于"当前温度下的零偏"。MEMS的零偏随温度变化是硬伤,如果你在室外低温或者设备发热严重的场景使用,最好在对应温度下重新标定,或者自己做一个小型的温度补偿表。我个人的习惯是每次换环境、换设备,都先跑一遍这十几分钟的流程,成本很低,收益却非常直接。说到底,IMU标定并不神秘,关键是理解误差从哪来、物理参考是什么、数据怎么合理地喂给优化器。把这三点想透了,你也能根据自己的传感器灵活调出一套好用的标定流程。