简介:基于扩展卡尔曼滤波(EKF)实现GPS与IMU融合的完整工程源码,面向机器人导航、无人机控制及自动驾驶等需要连续高精度定位的开发者。针对GPS易受遮挡干扰、IMU积分漂移等痛点,项目通过预测与更新两个阶段将两者互补,并能直接查看EKF初始化、状态预测、残差更新及模型线性化的具体写法。压缩包共55个文件,约5.14MB,以hpp/cpp源文件为核心,配合h头文件、csv数据文件、CMake构建脚本及yaml配置文件,便于梳理工程结构并快速编译运行。目前已有1682人学习,足以说明其实用价值。代码中提供imu_flow、gps_flow、eskf_flow等模块,并附config.yaml参数配置与测试程序,读者可对照数据流理解多传感器融合的工程实现,也可据此调整协方差、噪声参数以适应不同场景。
1. 把 GPS 和 IMU 拧成一股绳:这份 EKF 融合源码能做什么
在城市峡谷或者隧道里,GPS 信号被遮挡后定位点会像跳蚤一样乱跳,这是做自动驾驶和机器人定位时最容易翻车的场景之一。这套源码包解决的就是这件事:用一个扩展卡尔曼滤波器(EKF)把 GPS 的低频绝对位置和 IMU 的高频相对运动融合起来,GPS 负责长时校准,IMU 负责短时递推,最终输出一条比任何单传感器都稳的轨迹。资源主体是完整 C++ 工程,包含 IMU/GPS 数据流封装、EKF 核心、观测性分析模块以及一组仿真数据,适合写组合导航课设、入门松耦合 EKF 融合,或者拿真实数据回来跑一遍对比定位误差的开发者。它不是黑匣子,核心算法都摊在 src 和 include 里,你可以一行行改参数看效果。
2. 工程结构拆解:先从文件树看懂数据怎么流动
拿到压缩包先别急着编译,我习惯先把目录和调用链摸一遍。这套资源的结构很典型,分 app、src、include、config、test、3rd 六块,外加一堆 CMake 工具链文件。搞清楚谁调谁,后面调参才不会被滤波器的玄学绕晕。
2.1 源码目录与调用链:从 main 到 EKF 核心
解开 eskf-gps-imu-fusion-main.zip 后,第一层目录是这样的:
- app:程序入口 gps_imu_fusion.cpp
- src:所有算法实现,包括 eskf.cpp、imu_flow.cpp、gps_flow.cpp、eskf_flow.cpp、observability_analysis.cpp
- include:对应头文件,以及 global_defination.h.in 这类由 CMake 生成的路径配置
- config:config.yaml 配置文件
- test:test_imu.cpp、test_gps.cpp 两个单模块测试
- 3rd:第三方依赖,里面有 sophus 和 GeographicLib
- data:raw_data 和 my_test.csv,离线仿真数据
调用链很清晰:main 先加载 config.yaml,然后构造 EskfFlow 对象。EskfFlow 内部同时持有 ImuFlow、GpsFlow 和 Eskf 三个实例,每个 IMU 数据帧触发一次预测,每个 GPS 数据帧触发一次更新,最后把融合结果输出。observability_analysis 则在初始化阶段或运行过程中对系统做观测性分析,输出哪些状态量理论可观测。
// app/gps_imu_fusion.cpp 主流程简化示意 int main(int argc, char** argv) { // 1. 读取配置 std::string config_path = "config/config.yaml"; EskfFlow eskf_flow(config_path); // 2. 循环读取 IMU/GPS 数据帧 while (ReadNextFrame(imu_data, gps_data)) { // 3. IMU 帧做预测,GPS 帧做更新 eskf_flow.Run(imu_data, gps_data); } return 0; }逻辑说明:EskfFlow 对上层屏蔽了 IMU 和 GPS 的差异,main 只需要循环喂数据。这样设计的价值在于,你换数据源时不用动滤波器核心,只改 ImuFlow 和 GpsFlow 的读取逻辑即可。
参数说明:config_path 决定了协方差初值、传感器频率、数据路径全部参数,所以第 4 章我会专门讲怎么改这个文件。
2.2 IMU 与 GPS 数据流:imu_flow 和 gps_flow 各管一件事
这两个模块本质是数据适配器。ImuFlow 负责把 IMU 原始数据转成预测阶段需要的加速度和角速度,GpsFlow 负责把 GPS 位置转成更新阶段的观测向量。资源里 data/raw_data 就是给这两个模块喂的仿真数据,my_test.csv 则是更接近传感器原始输出的格式。
// src/imu_flow.cpp 预测数据组织逻辑 bool ImuFlow::ReadData(IMUData& imu_data) { if (!reader_.Read(imu_data)) return false; // 把原始量转成 EKF 需要的格式 imu_data.acc_norm = imu_data.acc.norm(); imu_data.gyro_norm = imu_data.gyro.norm(); return true; }逻辑说明:这里做了一个容易被忽略的预处理——计算加速度和角速度的模长。虽然 EKF 状态更新不直接用模长,但它在观测性分析和数据异常检测里很有用。如果 IMU 数据的模长突然跳变,基本可以断定数据有问题。
参数说明:imu_data 内部字段包括时间戳 timestamp、三轴加速度 acc、三轴角速度 gyro,以及计算出的 norm 值。时间戳是所有数据对齐的基准,后面避坑章节我会专门讲它。
2.3 观测性分析:observability_analysis 在算明白哪些状态能估
这个模块是资源里比较有亮点的部分,很多课设工程不会放它。它通过构造观测性矩阵并计算秩,告诉你在当前 GPS 观测条件下,位置、速度、姿态、零偏这些状态量哪些理论上可观测,哪些不可观测。
// src/observability_analysis.cpp 计算可观测性矩阵秩的示意 bool ObservabilityAnalysis::Run(const Eskf& eskf) { // 1. 构造离散化后的观测性矩阵 Eigen::MatrixXd obs_matrix = BuildObservabilityMatrix(); // 2. 计算秩 Eigen::ColPivHouseholderQR<Eigen::MatrixXd> qr(obs_matrix); int rank = qr.rank(); // 3. 输出哪些状态可观测 for (int i = 0; i < state_dim; ++i) { if (IsObservable(rank, i)) { LOG(INFO) << "State " << i << " observable"; } } return true; }逻辑说明:这段代码不是空架子。BuildObservabilityMatrix 内部会用当前滤波器状态构造线性化后的观测矩阵,再结合系统矩阵做离散化。秩的计算结果直接决定你能不能把零偏加入状态估计——如果观测性不足,强行加零偏只会让滤波器发散。
参数说明:state_dim 一般取 15,对应位置 3、速度 3、姿态 3、陀螺零偏 3、加计零偏 3。但资源里也可能只估 12 维或 9 维,具体看 config.yaml 里 state_dim 的设置。观测性分析跑出来秩不够时,第一反应应该是减少状态量,而不是盲目调大协方差。
3. EKF 核心实现:预测与更新到底怎么算
这一章是整套资源的心脏。EKF 和普通卡尔曼滤波的唯一区别在于:系统模型是非线性的,所以每个周期都要做局部线性化,也就是算雅可比矩阵。资源里 eskf.cpp 就是干这个的。
3.1 状态量与运动模型:15 维状态到底装了啥
这套资源的状态量定义是组合导航里的标准做法:位置、速度、姿态、陀螺零偏、加计零偏。姿态用四元数表示而不是欧拉角,原因在于欧拉角在俯仰接近正负 90 度时会出现万向锁,导致雅可比矩阵奇异。
// include/eskf.h 状态量定义示意 struct State { Eigen::Vector3d position; // NED 坐标系下的位置 Eigen::Vector3d velocity; // NED 坐标系下的速度 Eigen::Quaterniond quat; // 机体到 NED 的姿态四元数 Eigen::Vector3d gyro_bias; // 陀螺零偏 Eigen::Vector3d accel_bias; // 加速度计零偏 };逻辑说明:位置和速度放在 NED 坐标系(北东地)下,因为 GPS 输出的经纬度转换到该坐标系后是近似平面直角坐标,线性化误差小。四元数避免了欧拉角的奇异问题,但更新时要用增量形式,不能直接加减。
参数说明:bias 的单位是 rad/s 和 m/s²。它们的初值通常设 0,协方差初值设 1e-4 到 1e-5 量级。如果偏置初值给得太大,滤波器需要很长时间才能收敛,这就是常见的“启动漂移”现象。
3.2 IMU 预测:用运动学模型往前推一步
预测阶段的核心是把 IMU 测量的加速度和角速度代入运动学模型,更新位置、速度和姿态。资源里用的是离散化后的状态方程,每个 IMU 周期执行一次。
// src/eskf.cpp 预测阶段核心逻辑 void Eskf::Predict(const IMUData& imu, double dt) { // 1. 去除零偏 Eigen::Vector3d acc = imu.acc - state_.accel_bias; Eigen::Vector3d gyro = imu.gyro - state_.gyro_bias; // 2. 姿态四元数更新(一阶积分) Eigen::Quaterniond delta_q; delta_q = AngleAxisToQuat(gyro * dt); state_.quat = state_.quat * delta_q; // 3. 速度更新(加速度转换到 NED 系) state_.velocity += (state_.quat * acc) * dt; // 4. 位置更新 state_.position += state_.velocity * dt; }逻辑说明:这里隐含了两个重要近似。第一,姿态更新只做了一阶积分,如果角速度很大且 dt 不小时误差会累积;第二,加速度从机体坐标系转到 NED 系时用当前姿态,但姿态本身也在变化,严格来说要做积分,资源用当前姿态近似处理,精度够用但高速旋转场景会略差。
参数说明:dt 是 IMU 采样周期,通常 0.005 秒到 0.01 秒。预测完之后,协方差矩阵 P 还要乘上状态转移矩阵 F 和系统噪声矩阵 Q,这部分代码在 eskf.cpp 里通过 Eigen 矩阵运算完成,注意 Q 的量级要和 IMU 噪声匹配,否则预测协方差会膨胀得过快。
3.3 GPS 更新:残差与雅可比决定校准力度
GPS 更新是线性校正过程。GPS 给出的位置和 EKF 预测的位置之间的差值叫残差,残差乘以卡尔曼增益 K 就是修正量。雅可比矩阵 H 在这里是常数矩阵,因为观测就是位置本身。
// src/eskf.cpp GPS 更新阶段核心逻辑 void Eskf::UpdateWithGPS(const GPSData& gps) { // 1. 观测残差 Eigen::Vector3d residual = gps.position - state_.position; // 2. 构造观测矩阵 H Eigen::MatrixXd H = Eigen::MatrixXd::Zero(3, state_dim); H.block<3,3>(0,0) = Eigen::Matrix3d::Identity(); // 3. 计算卡尔曼增益 Eigen::MatrixXd K = P_ * H.transpose() * (H * P_ * H.transpose() + R_gps_).inverse(); // 4. 更新状态 state_.position += K.block<3,3>(0,0) * residual; // 更新协方差矩阵 P_ = (I - K * H) * P_; }逻辑说明:注意这里只更新了位置分量,但 H 矩阵维度是 3×15,理论上卡尔曼增益会给速度和姿态也带来修正。这是因为 GPS 位置和速度、姿态之间存在相关性,协方差矩阵 P 中非对角元素会把这个修正传递过去。很多人只盯着 H 的形状,忽略了 P 中的交叉项,这就是滤波器“学不动”的根源之一。
参数说明:R_gps_ 是 GPS 观测噪声协方差,单位是 m²。城市环境下 GPS 水平误差可能 1 到 3 米,所以 R 取 1 到 10 之间比较合理。R 太大,滤波结果会跟着 IMU 漂;R 太小,输出会被 GPS 噪声带得不平滑。这个值没有唯一答案,必须按数据集试。
4. 编译运行与配置:把项目跑起来的第一步
源码工程用了 CMake 组织,依赖 Sophus、Eigen、GeographicLib 三个库。编译前需要先确认依赖路径,否则会卡在 find_package 上。
4.1 CMake 工具链文件与第三方依赖怎么组织
资源里有一组 cmake 结尾的文件,分别是 sophus.cmake、YAML.cmake、global_defination.cmake、eigen.cmake。它的思路是每个依赖一个 cmake 文件,用 set 指定源码路径,再用 target_include_directories 引入头文件目录,好处是不依赖系统全局安装。
# 编译前先拉取第三方库到 3rd 目录,然后执行标准 CMake 流程 cd eskf-gps-imu-fusion-main mkdir -p build && cd build cmake .. make -j4逻辑说明:cmake 时 global_defination.cmake 会处理 .in 文件生成 global_defination.h,里面包含项目根目录路径。这个路径写死到生成的头文件里,所以你如果移动了工程目录,需要删掉 build 重新 cmake,不然程序会找不到 config 和 data。
参数说明:-j4 表示用 4 个核编译。如果 Sophus 或 yaml-cpp 没有按预期被找到,可以检查 3rd 目录下是否解压完整。注意 Eigen 是头文件库,不需要编译,但 Sophus 需要编译出库文件。
4.2 数据格式与 config.yaml 参数说明
config.yaml 是唯一的参数入口。打开后你会看到以下关键字段,我整理成表格方便对照:
| 参数名 | 单位 | 典型值 | 说明 |
|---|---|---|---|
| imu_file | - | data/raw_data/imu.csv | IMU 数据路径 |
| gps_file | - | data/raw_data/gps.csv | GPS 数据路径 |
| imu_frequency | Hz | 100 | IMU 采样频率 |
| gps_frequency | Hz | 10 | GPS 采样频率 |
| pos_std | m | 1.5 | GPS 位置标准差 |
| vel_std | m/s | 0.05 | 速度量测标准差 |
| gyro_std | rad/s | 0.01 | 陀螺噪声标准差 |
| accel_std | m/s² | 0.1 | 加速度计噪声标准差 |
# 先跑单模块测试,确认数据读取正常 ./build/test/test_imu ./build/test/test_gps逻辑说明:test_imu 和 test_gps 这两个测试会直接读取 imu.csv 和 gps.csv,打印前几帧数据和时间戳范围。如果在测试阶段就发现时间戳不连续或者数据为空,那问题出在数据适配层,根本不需要动 EKF 相关代码。
参数说明:pos_std 和 vel_std 会直接赋给 R 矩阵,gyro_std 和 accel_std 会赋给 Q 矩阵。注意它们是标准差还是方差,资源里如果写的是 std 后缀,那就是标准差,代入协方差矩阵时要先平方。这个细节很多新手会翻车,后面我会单独讲。
4.3 用 my_test.py 快速可视化运行结果
资源里附带了一个 my_test.py,这个脚本可以读取融合输出结果并画轨迹图,非常适合做定性验证。编译完成后,跑 app 生成输出文件,再用 python 脚本绘图。
# 运行主程序 ./build/app/gps_imu_fusion # 生成融合轨迹后,用 python 脚本对比 GPS 原始轨迹和融合轨迹 python3 my_test.py逻辑说明:my_test.py 的核心逻辑是读三份数据:GPS 原始轨迹、IMU 递推轨迹、EKF 融合轨迹,然后画在同一张图上。你一眼就能看到融合轨迹是不是比纯 GPS 更平滑、比纯 IMU 更贴近真实路径。如果融合轨迹出现突发跳变,基本可以断定时间戳对齐出了问题。
参数说明:my_test.py 里可能会配置一个阈值用于判断融合是否发散,例如位置误差超过 5 米就标红。脚本里的绘图参数不用改,但如果数据文件路径不同,记得改文件路径的变量名。
5. 避坑排查:五个我反复踩过的坑
这套资源表面上能编译能跑,但真正用到自己的数据集上时,问题会一个个冒出来。下面五条是我按“现象 → 原因 → 解决”整理的踩坑记录,建议先收藏。
5.1 融合轨迹呈阶梯状,每隔一秒跳一次
现象:输出的融合轨迹不是平滑曲线,而是像台阶一样一段段跳。
原因:IMU 和 GPS 的时间戳没有对齐。IMU 用的是仿真时间,GPS 用的是系统时钟,两个数据流的时间基准不一致,导致 EKF 在不同步的数据上做更新。
解决:在 GpsFlow 里做一个时间戳同步,把 GPS 观测对齐到最近的一个 IMU 帧上。常见做法是把 IMU 时间戳作为主时钟,GPS 数据先缓存,只有 GPS 时间戳落在当前 IMU 时间戳之前时才执行更新。资源里的 gps_flow.cpp 你直接改成双缓冲即可,不要做复杂插值,因为 GPS 本身就有延迟。
5.2 滤波器启动后位置快速发散,残差越滚越大
现象:前几百帧看起来正常,随后位置误差线性增长,最后轨迹飞到天上去。
原因:协方差初值 P0 或噪声矩阵 Q 的量级和真实数据不匹配。最常见的是把标准差当成了方差填入协方差矩阵,比如 pos_std=1.5,填 R 时直接填 1.5,实际应该是 2.25。
解决:把 config.yaml 里所有 std 字段在赋值到协方差矩阵前先取平方。检查代码里有没有类似R = std * std * Matrix3d::Identity()的写法,如果没有,就手动补上。这个问题的坑隐蔽在单位上,建议在初始化阶段打印一下 P0 和 R 的实际值,不用等轨迹飞了才回头看。
5.3 静态场景下融合位置缓慢漂移
现象:设备放在桌面上不动,GPS 也稳定,但融合位置在十分钟内漂了几米。
原因:IMU 的零偏没有有效估计。零偏估计需要激励,如果初始零偏很大且没有加入状态向量,加速度计和陀螺的恒定偏差会被积分成缓慢增长的位置误差。
解决:确认 state_dim 包含了陀螺零偏和加计零偏,这对应 eskf.cpp 里状态向量长度为 15。同时检查 Q 矩阵里 bias 对应的噪声项不为零,通常设 1e-6 到 1e-5 量级。如果观测性分析显示零偏不可观测,则需要在运动中完成初始化,或者用手动方式先标定零偏。
5.4 IMU 频率跑不满,CPU 占用却异常高
现象:明明 imu_frequency 配的是 200Hz,可实际跑起来每秒只处理几十帧,程序 CPU 占用率却超过 100%。
原因:EKF 在每一步预测时都做矩阵求逆或 QR 分解,如果状态维度设得偏高或者协方差矩阵更新逻辑里用了自动微分,计算开销会成倍增长。
解决:检查 config.yaml 里 state_dim 是否真的需要 15 维。如果不需要估计零偏,就降到 9 维,速度会快不少。另外在循环里加一个耗时统计,打印出每帧处理时间,如果单帧超过 5 毫秒,说明瓶颈在矩阵运算。
5.5 换电脑后编译报错,提示找不到 Sophus 或 GeographicLib
现象:同一个工程在原来机器上编译没问题,换一台机器后 make 报错,找不到某些头文件。
原因:工程里 global_defination.cmake 把第三方库路径写死了,换机器后路径失效。这是最常见的“换环境就翻车”。
解决:不要手动改 cmake 文件里的绝对路径,而是把 3rd 目录放到工程根目录下保持相对路径,同时删掉 build 目录重新 cmake。还有一种做法是用 CMake 的 FetchContent 功能在编译时自动拉取依赖,但那样需要联网,离线环境不推荐。
6. 进阶:用观测性分析指导滤波参数调优
跑通只是开始,真正让这套 EKF 在不同场景都稳定下来的关键是掌握两个验证工具:观测性分析和误差对比脚本。
先看观测性分析模块输出的内容。它会在每个 GPS 更新周期后计算观测性矩阵的秩,并给出哪些状态量可观测。我第一次用这套资源时,直接把 15 维状态全部开启,结果位置还是漂。跑了一次观测性分析才发现,当 GPS 观测只有位置时,姿态误差在水平方向上的可观测性其实很弱,而绕重力方向的航向角几乎不可观测。这就是为什么很多工程要在 EKF 前面再加一个磁力计或视觉航向观测。
以这套资源默认的 GPS+IMU 组合来说,可观测的状态通常是位置、速度、俯仰和横滚,航向角需要车辆转弯或加减速激励才能慢慢收敛。这直接决定了参数怎么调:如果数据集里车辆长时间直线行驶,我就不会给航向协方差设太小的初值,否则滤波器的自信会误导后续解算。
接下来是调参的顺序。我的固定流程是先把 R 根据 GPS 的标准差如实设置,再把 Q 按 IMU 噪声的 1.5 倍作为起点,然后跑一遍融合结果,对比纯 GPS 轨迹计算误差。误差曲线如果要像锯齿一样高频抖动,说明 R 太小;如果误差曲线缓慢但持续偏离,说明 Q 太小或 R 太大。这个过程没有任何捷径,必须手动试。为了少走弯路,可以用 my_test.py 增加一个自动扫描功能:把 R 从 0.5 到 10 按步长 0.5 扫一遍,每个参数都算一次均方根误差,最后选出最小误差对应的 R。这个脚本写起来很简单,核心逻辑如下:
# my_test.py 调参模块示意 import numpy as np best_rms = np.inf best_r = 1.0 for r in np.arange(0.5, 10.0, 0.5): rms = run_ekf_and_evaluate(pos_noise=r) if rms < best_rms: best_rms = rms best_r = r print("best R =", best_r, "RMS =", best_rms)这段代码的核心是 run_ekf_and_evaluate 函数,它负责把新的 R 写进 config.yaml,跑一次主程序,读取输出的轨迹,然后和 GPS 原始轨迹计算均方根误差。注意这里不能只改 config.yaml 里 pos_std 一个字段,因为卡尔曼增益是同时由 R 和 P 决定的,P 的初值也会影响收敛前期的表现,所以扫描 R 时最好把 P0 也固定在合理值上。
最后分享一个我的工程习惯:拿到任何新的数据集,第一步永远先跑 test_imu 和 test_gps 验证时间戳连续且频率符合预期,第二步跑观测性分析确认状态量的可观测性,第三步才是调协方差参数。这套流程是从这套资源的源码结构里学到的,从那以后我再也没有因为换数据集而浪费一整天。如果你也正在被组合导航的调参折磨,希望这套顺序帮到你。
本文还有配套的精品资源,点击获取