简介:面向惯性导航算法学习者与科研人员,这份资源提供基于惯性测量单元(IMU)数据的解算与轨迹生成仿真实现,适用于航空、航天、无人驾驶及GPS信号受限场景下的高精度定位研究。通过加速度与角速度的积分处理,结合racekpf(卡尔曼滤波类算法)进行数据融合与误差校正,可完成从原始传感器数据到位置、航向、经纬度索引的完整推算流程,并支持生成实验轨迹进行对比,用于评估定位精度、漂移率、收敛速度等关键指标。资源包共7个文件,全部为MATLAB脚本(.m),涵盖INS_generator、Trace_generator、Heading_cal、LonLat2Index、INS_solve、Interp_Map、Matching3等功能模块,整体仅7KB,结构紧凑,便于直接阅读与二次开发。目前已有229人学习下载,适合需要快速上手惯性导航解算流程、开展轨迹匹配与滤波器对比研究的用户。
1. 惯性导航轨迹:一个压缩包的名字,牵连出整条技术线
第一次拿到“惯性导航.rar”这种命名乱七八糟、关键词叠了好几层的压缩包时,大多数人以为解压出来就有一条现成的导航轨迹可以看,点开才发现不是那么回事。压缩包里往往是一堆传感器原始数据、姿态解算脚本、轨迹后处理模块和参数文件堆在一起,核心的惯性导航轨迹需要自己动手从 IMU 原始数据里一步一步算出来。文件名后缀里那几个 racekpf、导航、轨迹的标签,更像是打包的人顺着用途反复修改留下来的,并不代表里面有一个能直接入库的成品。
这篇文章要把惯性导航轨迹重建的整条链路拆开讲清楚:确定坐标系、做姿态解算、算位移、做零速修正,最后用闭合误差去验收。适合手头有 IMU 数据、想做室内或短时动态轨迹重现,或者刚收到一个命名混乱的导航数据包、想判断它值不值得继续投入的工程师。后面的命令、参数和判断标准,都是这类轨迹重建任务里反复用过的套路,不是教科书推导,也不是某个标准库的说明书。
2. 惯性导航轨迹重建的基本盘:坐标系、姿态和积分
2.1 坐标系约定不定,轨迹图直接翻脸
打开数据之后第一步不是算路程,而是确认坐标系约定。惯性导航轨迹必须依赖一个参考系,工程里最常用的是导航系 n 系(东北天或北东地),载体固定坐标系 b 系则各家定义不同。同一个传感器,如果厂家手册写的是“右-前-上”,算法里按“前-右-下”解算,轨迹的 X、Y、Z 轴向就会对不上,画出来的图要么前后颠倒,要么左右镜像,看起来完全不像真实运动。
我拿到数据的第一件事,是把设备平放静止 30 秒,随便转几个固定角度,打印每一轴的加速度和角速度均值。静止状态下重力应该落在 Z 轴或者负 Z 轴上,数值接近 9.8 m/s²,水平轴接近零;水平翻转 90 度时,重力读数应该换到对应的水平轴。这个检查最多五分钟,却能把坐标系问题和安装方向问题暴露在最早阶段,避免后面所有时间都浪费在“轨迹为什么长得这么怪”这种玄学问题里。
下表是几种常见的载体坐标系定义和它们静止时的重力方向,可以直接用来对照自己手里的数据:
| 载体系定义 | X 轴方向 | Y 轴方向 | Z 轴方向 | 静止时重力读数特征 |
|---|---|---|---|---|
| 前右下 | 前 | 右 | 下 | Z 轴接近 +9.8,X、Y 接近 0 |
| 前左上 | 前 | 左 | 上 | Z 轴接近 -9.8,X、Y 接近 0 |
| 右前上 | 右 | 前 | 上 | Z 轴接近 -9.8,X、Y 接近 0 |
| 东北天导航坐标系 | 东 | 北 | 天 | 重力为 -g,Z 轴朝向天顶的负方向 |
表格只能当参考,最终要以静止段实测为准。如果传感器内部安装方向和外壳标注不一致,表格里的判断也可能失效,所以静态测试永远优先于文档。
2.2 姿态是轨迹的地基
惯性导航轨迹不是把加速度直接积分两遍就能得到的。加速度计测到的是比力,包含了重力分量,如果不知道当前姿态,就无法把重力从比力里扣除。整个解算过程分三步:先用陀螺仪积分姿态,再用加速度计和磁力计修正姿态漂移,最后把加速度变换到导航系,积分出速度和位置。
这里面最关键的是第一步姿态。只要姿态角误差能控制在 1° 以内,几十秒内的位置误差主要由加速度计噪声和零偏决定;姿态误差一旦超过 5°,重力投影误差就会被线性放大,轨迹在几秒钟之内就会飘出地图范围。
工程上习惯把载体系下的比力 f^b,通过旋转矩阵 C_b^n 转到导航系,再减去重力矢量 g^n,得到真正让载体产生位移的加速度:
a^n = C_b^n (f^b - b_a) - g^n
这个公式是整个惯性导航轨迹重建的原点,后面所有代码都是在实现它。b_a 是加速度计零偏,不同芯片、不同温度下都会有差异,所以静态对齐不只是为了验坐标系,还要顺手估掉这个偏置。
2.3 从角速度到姿态:四元数更新的离散形式
姿态表达方式有三种:欧拉角、旋转矩阵、四元数。轨迹重建这类任务我常用四元数,原因很实际:它没有万向锁问题,更新时只需要四次乘法和归一化,在代码里很好写。
四元数离散更新公式长这样:
q(k+1) = normalize( q(k) + 0.5 * dt * q(k) ⊗ ω(k) )
ω(k) 是陀螺仪当前时刻的角速度向量,⊗ 表示四元数乘法。这个式子的物理含义是:每一小段时间内,姿态在原有基础上绕着角速度方向旋转一个微小角度。离散化让实现变得很容易,但也埋了个雷:dt 必须和真实采样间隔一致,如果采样不均匀,这个更新一多就会累积出可观测的漂移,后面避坑章节还会专门说。
3. 把 IMU 数据跑成惯性导航轨迹:最小可运行的 Python 链路
3.1 读入 IMU 数据并统一采样率
拿到压缩包里的原始数据,常见格式是 CSV 或者带注释的文本,列顺序大概率为时间、陀螺仪 xyz、加速度计 xyz。先把数据读进来,统一成 NumPy 数组,顺手检查时间戳是否均匀:
import numpy as np import pandas as pd imu = pd.read_csv("imu_data.csv", comment="#") # 约定列名:time_s, gx, gy, gz, ax, ay, az t = imu["time_s"].to_numpy() gyro = imu[["gx", "gy", "gz"]].to_numpy() acc = imu[["ax", "ay", "az"]].to_numpy() dt = np.diff(t) print("采样间隔均值:", np.mean(dt), "最大偏差:", np.max(np.abs(dt - np.mean(dt))))如果最大偏差明显大于均值的 10%,就不要直接用原始时间戳做积分。常见做法是用插值把数据重采样到固定频率,比如 100 Hz:
fs = 100.0 time_grid = np.arange(t[0], t[-1], 1.0 / fs) gyro_i = np.stack([np.interp(time_grid, t, gyro[:, j]) for j in range(3)], axis=1) acc_i = np.stack([np.interp(time_grid, t, acc[:, j]) for j in range(3)], axis=1) dt = 1.0 / fs插值之后,所有后续积分都用同一个 dt,姿态更新和位移积分才不会因为采样间隔忽长忽短而失真。这一步看起来没有技术含量,但在后续排查轨迹异常时,往往是决定成败的前置条件。
3.2 姿态解算:Mahony 互补滤波的落地写法
纯陀螺仪积分会让姿态随时间漂移,纯加速度计求解姿态又受运动加速度干扰。最常见的做法是 Mahony 互补滤波:角速度负责短时预测,加速度计负责修正低频漂移。下面这段是我在工程里经常用的最小实现:
def quat_mul(q1, q2): w1, x1, y1, z1 = q1 w2, x2, y2, z2 = q2 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_rotate(q, v): vq = np.array([0.0, v[0], v[1], v[2]]) q_inv = np.array([q[0], -q[1], -q[2], -q[3]]) return quat_mul(quat_mul(q, vq), q_inv)[1:] def mahony_step(q, gyro, acc, dt, kp, ki, err_integral): # 将加速度计测量值归一化,得到载体坐标下的重力方向 a_meas = acc / np.linalg.norm(acc) # 将导航系重力矢量投影到载体坐标,作为参考 g_b = quat_rotate(q, np.array([0, 0, 9.80665])) g_b = g_b / np.linalg.norm(g_b) # 叉积误差:两个重力方向不一致的程度 err = np.cross(g_b, a_meas) err_integral += err * dt # 用误差修正角速度 omega = gyro + kp * err + ki * err_integral # 四元数更新:一阶近似 dq = np.empty(4) dq[0] = 0.0 dq[1:] = 0.5 * omega * dt q_new = quat_normalize(q + quat_mul(dq, q)) return q_new, err_integral这段代码的用意是让陀螺仪的角速度值先被误差修正再积分,kp 决定修正力度,ki 负责消除静态偏差。kp 太小,姿态容易被陀螺仪零偏带偏;kp 太大,加速度计里的运动噪声又会污染姿态。我一般从 kp=1.0、ki=0.05 起步,然后再根据轨迹效果微调。
3.3 重力扣除与轨迹的二重积分
姿态算出来之后,轨迹就只剩下机械的坐标变换和两次积分。先把载体系加速度转到导航系,扣掉重力,再对速度积分一次、对位置积分一次:
n = len(gyro_i) q = np.tile(np.array([1.0, 0.0, 0.0, 0.0]), (n, 1)) err_integral = np.zeros(3) vel = np.zeros((n, 3)) pos = np.zeros((n, 3)) for i in range(1, n): q[i], err_integral = mahony_step( q[i-1], gyro_i[i], acc_i[i], dt, kp=1.0, ki=0.05, err_integral=err_integral ) # 导航系下的加速度 = 载体系加速度旋到导航系后再减去重力 acc_n = quat_rotate(q[i], acc_i[i]) - np.array([0.0, 0.0, 9.80665]) vel[i] = vel[i-1] + acc_n * dt pos[i] = pos[i-1] + 0.5 * (vel[i] + vel[i-1]) * dt解释一下两个关键参数:减去重力那一步,前提是姿态正确;如果姿态有误差,重力残余会混进水平加速度,让轨迹产生系统性漂移。速度积分用的是上一个时刻和当前时刻的均值,相当于梯形积分,对离散噪声的敏感度比普通矩形积分低一些,代价是会让位置更新看上去慢半个采样周期,对 100 Hz 数据影响可以忽略。
到这里,一条原始的惯性导航轨迹就出来了。但实践经验表明,直接用上图会很难看,因为积分漂移在十几秒内就会把轨迹拉飞。下一章说怎么把轨迹拉回来。
3.4 racekpf 这类命名模块接进流程前,先搞清楚三件事
压缩包里经常出现 racekpf 这种看不出含义的命名,说明这个包多半是某位开发者按自己的习惯整理的,不是某个公开标准库的一部分。拿到这种模块时,我不会急着分析它的算法结构,而是先确认三件事。
第一,它的输入是什么,输出是什么。有的模块接收原始 IMU,有的只接收姿态序列,有的只是把算好的轨迹做一次俯仰角修正,盲目拼接最浪费工时。第二,它内部默认的坐标系和采样率是多少。上一步刚统一好的数据,如果在这个模块里又被按另一个采样率解释,后果就是轨迹尺寸对不上。第三,它和主流程的耦合程度,能不能独立替换。我的习惯是先画一张模块输入输出图,再决定把 racekpf 放在姿态解算之后还是轨迹生成之后,而不是摸着石头过河。
4. 后处理:零速修正和滤波参数这样调
4.1 静止时速度不归零,用零速修正拉回来
人在步行时每一步都有一个短暂的站立相,此时脚面相对地面速度为零。惯性导航轨迹里最常见的误差就是速度积分不归零,导致每一步都留下一点残余速度,轨迹逐渐膨胀。解决办法是检出站立相,把速度强制归零,这个操作简称 ZUPT。
检测站立相的方法不止一种,我用的比较稳定的是滑动窗口加速度方差法。静止时加速度模值接近重力,方差很小;运动时方差会明显变大:
acc_mag = np.linalg.norm(acc_i, axis=1) zupt = np.zeros(n, dtype=bool) window = 30 # 在 100Hz 采样下对应 0.3 秒 for i in range(window, n): seg = acc_mag[i-window:i] if np.var(seg) < 0.05: zupt[i] = True for i in range(1, n): if zupt[i]: vel[i] = 0.0阈值 0.05 是在 100 Hz 采样下试出来的,传感器噪声低时可以适当收紧,噪声大的设备要放宽。这里要注意:ZUPT 只作用于速度,位置不受直接影响,它的价值是阻止速度误差累积,从而让位置不会越飘越多、发散得更快。
纯 ZUPT 不能让轨迹在多秒内变得精确,但能让步行轨迹的形态恢复得比较像真实路径。它和后面要说的平滑滤波是两个层面的手段,经常配合使用,但不要期望它能替代加速度计校准。
4.2 陀螺仪零偏与加速度计零偏校准
严格来说,设备静止时陀螺仪读到的角速度均值就是零偏估计。在正式解算之前,取前 2 到 3 秒静态段的均值,从整段数据里直接减掉,是最廉价也最有效的校准手段。
加速度计零偏的估计稍微麻烦一点。常规做法是把设备正面朝上和反面朝上分别静止,计算两组数据 Z 轴的差异,从而分离零偏和重力。如果数据采集时没有专门做这个动作,也可以利用轨迹末段静止时的速度漂移反推一个常数,再回头补偿加速度。不过在 ZUPT 已经介入的情况下,单纯靠算法反推的加速度计零偏并不稳定,我通常只把它当作排查辅助,不作为正式处理步骤。
4.3 平滑滤波不是窗口越大越好
轨迹生成之后,很多人习惯再加一道滑动平均滤波,让线变得光滑一些。这没错,但窗口长度一旦过大会让真实转弯被抹掉。
下面这个表是我常用的参数参考,前提是 100 Hz 采样:
| 滤波器类型 | 窗口长度 | 适用场景 | 副作用 |
|---|---|---|---|
| 滑动平均 | 5 点(0.05s) | 轻微噪声,保持高频动态 | 基本无感 |
| 滑动平均 | 30 点(0.3s) | 步行轨迹,波形明显 | 转弯半径稍微变大 |
| 中值滤波 | 5 点 | 剔除跳点和野值 | 保留边缘,但抖动未完全消除 |
| 大窗口滑动平均 | 100 点以上 | 只看大概路径方向 | 真实转弯和急停会丢失 |
我处理步行轨迹时一般先用中值滤波去跳点,再用 5 点到 10 点的小窗口滑动平均,很少直接上 100 点窗口。判断滤波是否过度的办法很直接:在滤波前后各算一次总里程和转弯点数量,如果总里程明显缩水,说明转弯被吃掉了,需要收窄窗口。
5. 惯性导航轨迹实战避坑:5 个常见的翻车点
5.1 刚解压就跑,轨迹出现镜像翻转
现象:一段明明是向前直线走的数据,画出来却是左右翻转或前后颠倒,路线形状像做了镜像变换。
原因:载体坐标系定义和算法假设不一致。压缩包里的数据使用的是几百赫兹的原始采样,但解算脚本里默认的轴方向和数据实际记录的方向对不上。
解决:先用第 2.1 节的静态测试,把三轴符号和重力方向对准。最简单做法是手动翻转数据列,比如把 Z 轴取反,重新跑一遍,对比两次轨迹哪个更符合实际运动。翻车一次之后,我养成了习惯,任何新数据源都要先打印三轴静止均值,再进主流程。
5.2 陀螺仪零偏没减,静止时轨迹画成飞机航线
现象:设备放在桌上静止不动,姿态却在十几秒内缓慢旋转,轨迹图看起来像在绕圈。
原因:陀螺仪输出里有零偏,即使角速度真实值接近零,输出值可能是每秒零点几度。积分一多,角度就溜走了,进而带动加速度错误投影,位置也跟着飞。
解决:取静止段 2 到 3 秒的角速度均值,作为零偏从整段数据里减掉。如果零偏随时间漂移比较厉害,需要分段估计零偏,或者用 Mahony 滤波里的 ki 项来吸收这个低频误差。
5.3 采样时间戳忽快忽慢,同样的数据得到两套轨迹
现象:同一份数据,用不同预处理方式重跑,轨迹形态差很多,有的还出现明显突变或者走不到头。
原因:数据采集过程中时间戳不均匀,积分时 dt 丢掉或变化量过大,位置更新被迫按不均匀的间隔进行。
解决:先打印时间戳差值的统计结果,如果最大偏差超过均值的 10%,就统一插值到 100 Hz 再开始积分。后续所有模块都引用这份插值后的数据,不要部分模块用原始时间、部分模块用插值时间。
5.4 加速度计零偏残留,轨迹整体上坡或下坡
现象:水平行走的轨迹,高度通道却在一直上升或下降,甚至到了最后高度差有几十米。
原因:重力扣除不彻底。加速度计零偏或者姿态误差导致重力方向被计算错一点,在高度通道里形成常数加速度。
解决:先确认姿态解算质量;再对高度通道做趋势项移除。如果数据里有往返段,强制闭合回来是最彻底的办法。
5.5 过度平滑,真实转弯消失
现象:轨迹画出来很短、很平滑,但明显不像人走的路线,一些急转弯变成了大圆弧。
原因:平滑滤波窗口太大,或者连续做了多次滤波,把正常动态当噪声抹掉。
解决:滤波前后对比总里程、转弯点数量和轨迹端点位置。滤波后指标明显缩水,就把窗口改小,或者换成中值滤波只去野值,不过度平滑。
6. 验证惯性导航轨迹的技巧:回环闭合误差说了算
6.1 最便宜的闭环验证:走一圈,量首尾距离
验证惯性导航轨迹到底靠不靠谱,我最常用的是回环闭合误差。做法很简单:让设备携带者在原地绕一圈或走一个矩形回到起点,然后对比轨迹的起点坐标和终点坐标。如果轨迹算法没问题,起点和终点应该接近重合;如果误差大,就能直观看到漂移方向和大小。
closure_error = np.linalg.norm(pos[-1] - pos[0]) total_distance = np.sum(np.linalg.norm(np.diff(pos, axis=0), axis=1)) percent_error = closure_error / total_distance * 100.0 print(f"闭合误差: {closure_error:.2f} m, 占总路程: {percent_error:.1f}%")我的经验是,步行条件下这个比例在 2% 以内基本可用,超过 5% 就需要回去检查姿态参数或者 ZUPT 阈值。闭合误差只是一个整体指标,它不能告诉你问题出在哪,但要快速判断一条轨迹能不能用,它是性价比最高的检查方法。
6.2 误差拆到三个方向,能找到问题源头
闭合误差只给一个数字,定位问题要靠把误差拆到方向上去。水平面的 X、Y 误差主要来自姿态角度误差、加速度计噪声和 ZUPT 阈值;高度误差 Z 主要来自重力扣除和加速度计零偏;如果 Z 误差很小但水平飘,那大概率是偏航角漂移,而不是高度问题。
| 误差方向 | 主要来源 | 优先检查项 |
|---|---|---|
| X/Y 水平误差 | 姿态误差、加速度噪声、ZUPT 阈值 | Mahony 的 kp、ki,站住相检测窗口 |
| Z 高度误差 | 重力扣除、加速度计零偏 | 静态对齐、高度通道趋势项 |
| 整体旋转误差 | 偏航角漂移 | 磁力计校准或外部航向参考 |
最后一个技巧:如果环境里允许用磁力计做偏航修正,可以给航向增加一个外部观测量,水平误差能肉眼可见地减小。但磁力计容易受金属物体干扰,室内实测经常吞掉这些好处,所以我会先做闭合误差测试,再决定要不要引入磁力计。
我现在的习惯是,任何一条惯性导航轨迹,最后都走一遍往返或者绕一圈的测试,把首尾距离当成下线来评价;如果那条路线不具备闭合条件,至少要拿两个已知长度的点做比尺。这个验证方法虽然朴素,但每次都能提前暴露问题,省掉大量调试时间,希望帮到你。
本文还有配套的精品资源,点击获取