简介:这份资源是面向机器人导航、虚拟现实、飞行控制及移动设备开发者的IMU标定工具包imu-utils,用于解决惯性测量单元中加速度计、陀螺仪、磁力计的系统误差与随机误差问题,帮助工程师完成偏差、尺度因子、非正交性等参数的自动校正。压缩包共235个文件,约4.32MB,以txt说明、sample样例、h/hpp头文件、cpp源码、yaml与launch配置、m脚本等为主,并包含code_utils与imu_utils两个核心模块,兼顾标定算法与数据处理辅助工具。资源已修正代码中的错误引用,提升了稳定性,用户可据此将IMU集成到项目中,并按硬件环境定制标定流程。目前已有438人学习下载,适合需要掌握IMU标定流程、排查标定误差并提升产品精度的中高级开发者参考。
1. 从一次姿态漂移说起:imu-utils 到底解决什么问题
去年帮一个做移动机器人底盘的团队排查定位跑偏的问题,现象很典型:机器人直线走三米,航向角能偏出去七八度,跑一圈回到原点能差出半米。机械安装、轮速里程计、控制参数全查了一遍,最后定位到 IMU 的零偏和尺度因子没标定——加速度计和陀螺仪出厂给的只是粗略值,装到板子上还有轴间非正交误差,这些误差积分之后就是肉眼可见的漂移。imu-utils 就是冲着这类问题来的:它是一套围绕 IMU 标定与数据处理的工具集,把静态零偏估计、六面法加速度计标定、陀螺仪零偏与尺度标定、Allan 方差分析这些活儿串成可复现的流程。适合谁?做 SLAM、组合导航、飞控、机器人底盘的工程师,尤其是手里有 IMU 原始数据、但不确定标定该从哪下手的人。它不负责帮你选硬件,负责的是把手里这颗 IMU 的真实误差摸清楚,让后面的融合算法少背锅。
2. 标定前先搞懂:IMU 误差模型与 imu-utils 的模块划分
2.1 为什么不能直接拿出厂参数用
出厂标定通常在理想温度、理想安装条件下完成,标定报告里给的零偏和尺度因子是"裸传感器"级别的。但实际装配后,误差来源至少叠了三层:第一层是传感器本身的确定性误差,包括零偏、尺度因子、三轴非正交;第二层是安装误差,IMU 坐标系和载体坐标系之间的旋转没对齐;第三层是随机误差,零偏随温度和时间缓慢游走,还有白噪声和随机游走。前两层是确定性误差,可以用标定补偿掉;第三层只能靠 Allan 方差分析出噪声参数,交给滤波器去建模。
imu-utils 的设计思路就是把这三层拆开处理。确定性误差用静态多位置法标,随机误差用长时间静态数据做 Allan 方差。很多新手一上来就想用一段动态数据把全部参数拟合出来,结果标定结果比不标还差,这是血泪经验——动态数据里混着运动加速度和真实角速度,解耦不开。
2.2 模块划分与数据流
常见做法是把工具集分成四个模块:数据读取层负责解析不同格式的 IMU 日志(CSV、rosbag 导出的 txt、二进制包),标定计算层实现最小二乘和椭球拟合,分析层做 Allan 方差和频谱,输出层生成 YAML 或 JSON 参数文件供下游使用。数据流是单向的:原始日志 → 时间对齐与去重 → 静态段切分 → 标定解算 → 参数文件。
这里有个容易忽略的点:时间戳对齐。IMU 日志里如果时间戳有跳变或者重复,标定解算会把同一时刻的样本当成两个独立样本,导致零偏估计偏移。我一般会先跑一遍时间戳单调性检查,把非单调的段直接丢掉。
import numpy as np def check_timestamp(ts): # ts: 一维时间戳数组,单位秒 diff = np.diff(ts) # 找出非正的时间间隔,即时间戳回退或重复 bad_idx = np.where(diff <= 0)[0] if len(bad_idx) > 0: print(f"发现 {len(bad_idx)} 处时间戳异常,位置: {bad_idx[:10]}") # 统计采样间隔的均值与标准差,判断是否有丢帧 print(f"平均间隔: {diff.mean():.6f}s, 标准差: {diff.std():.6f}s") return bad_idx这段代码做两件事:一是用np.diff找非正间隔,定位时间戳回退或重复的位置;二是统计间隔的均值和标准差,如果标准差远大于均值,说明采样不均匀,后续标定要按实际时间戳加权。参数上,ts必须是单调递增的浮点数组,单位统一成秒,别混用毫秒和秒。
2.3 静态段切分:标定的地基
标定精度很大程度上取决于静态段选得好不好。所谓静态段,就是 IMU 静止不动的那几段数据。判断标准不是"看起来没动",而是用滑动窗口算角速度和加速度的方差,方差低于阈值才认定为静态。窗口长度一般取 0.5 到 1 秒,阈值根据 IMU 噪声密度定,消费级 IMU 角速度方差阈值可以放到 1e-4 (rad/s)^2 量级。
def find_static_segments(gyro, acc, win=100, gyro_th=1e-4, acc_th=1e-3): # gyro, acc: N x 3 数组 n = len(gyro) static_mask = np.zeros(n, dtype=bool) for i in range(0, n - win, win // 2): g_var = gyro[i:i+win].var(axis=0).sum() a_var = acc[i:i+win].var(axis=0).sum() if g_var < gyro_th and a_var < acc_th: static_mask[i:i+win] = True return static_mask逻辑说明:以半窗步长滑动,分别算三轴角速度和加速度的方差和,两个都低于阈值才标记为静态。win是窗口样本数,gyro_th和acc_th要按你的 IMU 手册里的噪声密度换算,别照抄。如果静态段切出来太碎,把win调大;如果漏掉了明显的静止段,把阈值放宽。
3. 六面法与 Allan 方差:把标定参数一个个算出来
3.1 加速度计六面法标定
加速度计标定的经典方法是六面法:把 IMU 分别以六个面朝下静置,每个位置采集一段数据,理论上每个位置的重力矢量在 IMU 坐标系下的投影是已知的(±g 落在某一轴上)。用这六组观测去拟合一个 3x3 的校正矩阵和零偏向量。
数学模型是a_meas = M * a_true + b,其中 M 是包含尺度因子和非正交误差的矩阵,b 是零偏。六面法提供六组方程,未知数有 12 个(M 九个元素加 b 三个),六组观测每组三个分量共 18 个方程,超定,用最小二乘解。
def calibrate_accel(acc_samples, g=9.80665): # acc_samples: list of (N_i x 3) 数组,对应六个朝向 # 每个朝向的理论真值:重力在 IMU 系下的方向 true_dirs = np.array([ [ g, 0, 0], [-g, 0, 0], [ 0, g, 0], [ 0,-g, 0], [ 0, 0, g], [ 0, 0,-g] ]) A, y = [], [] for samples, t in zip(acc_samples, true_dirs): mean_a = samples.mean(axis=0) # 每个朝向取均值降噪 # 构造方程: a_meas = M @ t + b # 展开成 3 个方程,未知数 [M11..M33, b1..b3] for k in range(3): row = np.zeros(12) row[k*3:(k+1)*3] = t row[9+k] = 1.0 A.append(row) y.append(mean_a[k]) A = np.array(A); y = np.array(y) x, *_ = np.linalg.lstsq(A, y, rcond=None) M = x[:9].reshape(3, 3) b = x[9:] return M, b逻辑说明:对每个朝向先取均值,把随机噪声压下去;然后为每个轴构造一行方程,未知数排列成[M 的 9 个元素, b 的 3 个元素]。np.linalg.lstsq解超定方程。参数g按当地重力加速度填,别用 9.8 凑合,高精度场景差这一点会体现在标定残差里。解出来的 M 如果不是接近单位阵,说明安装或者数据有问题,先别急着用。
3.2 陀螺仪零偏与尺度标定
陀螺仪没法像加速度计那样用重力做绝对参考,常见做法是用转台给已知角速度,或者用静态数据估零偏、用已知转动角度估尺度。没有转台的情况下,至少要把零偏估准:长时间静态数据的均值就是零偏估计。
def calibrate_gyro_bias(gyro_static): # gyro_static: 静态段角速度,N x 3 bias = gyro_static.mean(axis=0) # 用标准差评估零偏估计的置信度 std = gyro_static.std(axis=0) print(f"零偏: {bias}, 标准差: {std}") return bias逻辑说明:静态段越长,均值越接近真实零偏,但要注意温度漂移——如果静态段跨越了明显温升,均值会被拉偏。我一般会把静态段按时间切成几段分别估零偏,看零偏随时间的漂移趋势,如果漂移明显,说明这颗 IMU 需要做温度补偿,光靠一个常数零偏不够。
3.3 Allan 方差:把噪声参数交给滤波器
Allan 方差是分析 IMU 随机误差的标准工具。对长时间静态数据做分簇平均,簇时间从短到长,算不同簇时间下的方差,画成双对数曲线。曲线上的斜率对应不同噪声类型:斜率 -1/2 是白噪声(角度随机游走),斜率 0 是零偏不稳定性,斜率 +1/2 是速率随机游走。
def allan_variance(data, fs, max_cluster=None): # data: 单轴静态数据,一维数组 # fs: 采样率 Hz N = len(data) if max_cluster is None: max_cluster = N // 10 taus, avars = [], [] for m in range(1, max_cluster): # 簇长度 m,簇时间 m/fs n_clusters = N // m if n_clusters < 2: break cluster_means = data[:n_clusters*m].reshape(n_clusters, m).mean(axis=1) diff = np.diff(cluster_means) avar = 0.5 * np.mean(diff**2) taus.append(m / fs) avars.append(avar) return np.array(taus), np.array(avars)逻辑说明:m是簇内样本数,簇时间tau = m/fs。对簇均值序列做一阶差分,取平方均值再乘 0.5,就是该簇时间下的 Allan 方差。max_cluster控制最大簇时间,一般取总样本数的十分之一,再大统计意义就不够了。算出来的taus和avars画双对数图,从曲线上读白噪声系数和零偏不稳定性系数,填到滤波器的噪声模型里。
4. 避坑与排查:标定翻车的五个典型场景
4.1 标定后反而更差
现象:标定完把参数写进融合算法,轨迹精度比不标还差。原因通常是标定数据和实际使用工况不一致,比如标定时温度 20 度,实际跑起来板子发热到 50 度,零偏漂了一大截。解决:要么做温度分段标定,要么在融合算法里保留在线零偏估计,别把离线零偏当死值。
4.2 六面法解出的 M 矩阵离谱
现象:解出来的校正矩阵对角线元素偏离 1 很远,非对角元素也大得离谱。原因多半是六个朝向没摆正,或者某个朝向的数据里混了运动加速度。解决:每个朝向静置时间拉长到 30 秒以上,摆的时候用水平仪确认,数据里如果有明显尖峰先做剔除。
4.3 Allan 方差曲线尾部翘起
现象:双对数曲线在长簇时间处往上翘,读不出零偏不稳定性。原因是静态数据里有温漂或者低频干扰,把长簇时间的方差抬高了。解决:截取温度稳定的那一段数据做 Allan 方差,或者先做去趋势处理,把低频漂移去掉再看。
4.4 时间戳单位混用
现象:标定结果时好时坏,换一段数据就崩。原因是有的日志时间戳是毫秒,有的是微秒,代码里没统一。解决:读数据的第一件事就是确认时间戳单位,统一转成秒,并且检查单调性。
4.5 静态段阈值拍脑袋定
现象:静态段要么切得太碎,要么把运动段也切进去。原因是阈值没按 IMU 噪声密度换算,直接抄了别人的数。解决:查手册里的噪声密度,换算成方差阈值,再用可视化工具把切出来的静态段叠在原始数据上看一眼,确认没切错。
5. 进阶技巧:用标定残差反查数据质量
标定做完不是终点,残差分析才是判断标定可不可信的关键。加速度计标定后,把六个朝向的数据代回模型,算预测值和实测值的差,残差应该在噪声量级。如果某个朝向残差明显偏大,说明那个位置的数据有问题。
def accel_residual(acc_samples, M, b, true_dirs): for samples, t in zip(acc_samples, true_dirs): pred = M @ t + b meas = samples.mean(axis=0) res = meas - pred print(f"朝向 {t}: 残差 {res}, 模长 {np.linalg.norm(res):.4f}")残差模长如果超过 0.05 m/s²,消费级 IMU 就该警惕了;超过 0.1 m/s²,基本可以判定那个朝向的数据不可用。我一般会把六个朝向的残差模长列个表,一眼就能看出哪个位置翻车了。
| 朝向 | 残差模长 (m/s²) | 判断 |
|---|---|---|
| +X | 0.012 | 正常 |
| -X | 0.015 | 正常 |
| +Y | 0.011 | 正常 |
| -Y | 0.089 | 偏大,检查摆放 |
| +Z | 0.013 | 正常 |
| -Z | 0.014 | 正常 |
陀螺仪这边,标定后可以用静态段验证:把零偏补偿掉之后,静态段角速度的均值应该接近零,标准差应该和 Allan 方差读出的白噪声系数对得上。如果对不上,说明零偏估计被温漂污染了。
还有一个实用技巧:把标定参数写成带时间戳和温度的文件,别只存一个最终值。同一颗 IMU 在不同温度下多标几次,后面做温度补偿或者在线估计时,这些历史参数就是现成的先验。我吃过亏,早期标定只存了最终 YAML,后来想回头分析温度影响,原始数据早删了,只能重标。从那以后我每次标定都强制把原始静态数据、标定脚本输出、环境温度一起归档,哪怕当时觉得用不上。希望帮到你。
本文还有配套的精品资源,点击获取