简介本资源为基于轮式 MEMS IMU 的航位推算系统完整项目包面向计算机、自动化、电子信息等专业的在校学生、教师及企业研发人员也适合作为毕设、课设或项目立项的参考方案。项目仅使用一个安装在车轮上的 IMU 实现轮式机器人航位推算无需车轮编码器即可直接获取车速并借助车轮连续旋转的旋转调制效应在一定程度上抑制惯性导航误差积累涵盖算法实现、数据处理与实验验证等环节。压缩包共约 2000 个文件整体 182.03MB以 h 头文件、cc 与 cpp 源码、py 脚本、md 与 txt 说明文档、sh 脚本及 bazel 构建文件为主另含数据集与论文资料目录结构清晰便于按模块阅读与二次开发。目前已有 93 人学习下载读者可从中获取完整源码、文档说明、数据集与论文参考理解轮式 IMU 航位推算的实现思路与排错方法并在此基础上修改扩展功能。1. 轮式 MEMS IMU 航位推算从漂移发散到厘米级定位的工程落地做轮式机器人定位的工程师迟早会撞上一堵墙轮式里程计在打滑、过坎、急转弯时给出的位姿跳变和 MEMS IMU 零偏随时间累积的漂移两者叠加后让航位推算结果在几十秒内就发散到不可用。基于轮式 MEMS IMU 的航位推算系统本质上是把轮速脉冲和六轴惯性数据做紧耦合融合用运动学约束把 IMU 的积分误差压回可控范围。它解决的是 GNSS 拒止环境室内、地下、隧道、林下下连续定位的问题适合做移动机器人、AGV、巡检车、农业机械的从业者。一套完整的落地材料通常包含源码、文档说明、数据集和论文但真正决定能不能跑通的是误差模型建得对不对、标定做得够不够细、融合策略选得合不合理。下面按我实际搭过的一套方案把选型、实现、参数和坑逐层拆开。2. 系统架构与传感器选型为什么不是简单地把两个数据加起来2.1 轮式里程计与 MEMS IMU 的误差特性对比轮式里程计的核心问题是标定参数不准和打滑。左右轮半径差异、轮距测量误差、编码器分辨率这三项直接决定航向角解算的精度。一个常见的经验值是轮距标定偏差 1mm在 10m 直线行驶后航向误差约 0.1°但如果发生一次单轮打滑航向误差可能瞬间跳到 5°以上。MEMS IMU 的问题则是零偏不稳定性。消费级 MEMS 陀螺的零偏稳定性通常在 520°/h 量级换算下来每秒漂移 0.00140.0056°看起来很小但积分 60 秒后就是 0.080.34° 的姿态误差再通过重力分量耦合进位置解算位置漂移会以时间平方增长。两者的互补性在于轮式里程计在短时间尺度上15 秒相对可靠但会被打滑破坏MEMS IMU 在短时间尺度上角速度测量可靠但零偏积分会发散。融合的目标不是让两个传感器互相“平均”而是用轮式里程计的速度观测去约束 IMU 的速度积分同时用 IMU 的角速度去平滑轮式航向的跳变。2.2 融合框架选型EKF、UKF 还是因子图我实际对比过三种方案。EKF 最直接状态量取位置、速度、姿态、陀螺零偏、加速度计零偏共 15 维观测量取轮式里程计的速度和航向。UKF 在姿态非线性强时略好但计算量增加约 40%对嵌入式平台不友好。因子图优化如 GTSAM在后端平滑上效果最好但需要滑动窗口和边缘化工程复杂度高。对于轮式平台我的建议是如果主控是树莓派级别以上用 EKF 足够如果是 MCU 级别考虑误差状态卡尔曼滤波ESKF把姿态误差放在切空间里做线性化数值稳定性更好。下面是一个 ESKF 的预测步核心代码结构import numpy as np from scipy.spatial.transform import Rotation as R class ESKF: def __init__(self): # 名义状态位置、速度、姿态四元数、陀螺零偏、加计零偏 self.p np.zeros(3) self.v np.zeros(3) self.q np.array([1.0, 0, 0, 0]) # wxyz self.bg np.zeros(3) self.ba np.zeros(3) # 误差状态协方差 15x15 self.P np.eye(15) * 0.01 # 过程噪声陀螺噪声、加计噪声、零偏随机游走 self.sigma_g 0.005 # rad/s/sqrt(Hz) self.sigma_a 0.05 # m/s^2/sqrt(Hz) self.sigma_bg 1e-5 # rad/s^2/sqrt(Hz) self.sigma_ba 1e-4 # m/s^3/sqrt(Hz) def predict(self, omega, acc, dt): # 去零偏 omega omega - self.bg acc acc - self.ba # 姿态更新四元数积分 theta np.linalg.norm(omega) * dt if theta 1e-8: axis omega / np.linalg.norm(omega) dq np.array([np.cos(theta/2), *(axis * np.sin(theta/2))]) self.q self._quat_multiply(self.q, dq) self.q / np.linalg.norm(self.q) # 速度更新世界系下重力补偿 g np.array([0, 0, -9.81]) rot R.from_quat([self.q[1], self.q[2], self.q[3], self.q[0]]) a_world rot.apply(acc) g self.v a_world * dt self.p self.v * dt # 协方差传播简化形式实际需构造 F 和 Q F self._build_F(omega, acc, dt) Q self._build_Q(dt) self.P F self.P F.T Q def _quat_multiply(self, 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 ])这段代码的关键点在于姿态用四元数更新避免万向锁速度更新时把加计测量转到世界系并补偿重力协方差传播用 F 矩阵和 Q 矩阵。实际工程中F 矩阵的构造需要把误差状态方程线性化这里为了可读性做了简化。参数方面sigma_g和sigma_a需要根据 IMU 数据手册的噪声密度填写sigma_bg和sigma_ba根据零偏重复性填写。如果这两个值设得过大滤波器会过度信任观测导致轮式打滑时航向被带偏设得过小IMU 零偏收敛慢初始阶段漂移明显。2.3 轮式里程计观测模型与标定流程轮式里程计的观测模型分两部分线速度和航向角速度。线速度由左右轮速平均得到航向角速度由左右轮速差除以轮距得到。标定流程我一般分三步走第一步直线标定轮半径。让车在 10m 直线上跑 5 次记录编码器脉冲数和实际距离用最小二乘拟合左右轮半径。第二步原地旋转标定轮距。让车原地转 10 圈记录左右轮脉冲差和实际航向变化反解轮距。第三步交叉验证。跑一个 5m×5m 的方形轨迹看闭合误差。如果闭合误差大于 0.1m说明标定参数还有问题。# 标定脚本示例直线标定轮半径 python calibrate_wheel.py --mode straight --distance 10.0 --runs 5 \ --left_encoder_topic /left_encoder --right_encoder_topic /right_encoder \ --output wheel_params.yaml # 原地旋转标定轮距 python calibrate_wheel.py --mode rotate --turns 10 \ --imu_topic /imu/data --output wheel_params.yaml标定脚本的核心逻辑是采集编码器计数和参考真值卷尺或全站仪用线性回归求参数。注意编码器计数要取上升沿或四倍频取决于驱动器配置。如果编码器分辨率是 1024 线四倍频后每圈 4096 计数轮半径 0.1m 时每计数对应 0.00015m这个精度对航位推算足够。3. 从源码到跑通数据流、时间同步与滤波器初始化3.1 数据集格式与回放验证一套完整的数据集通常包含 IMU 原始数据角速度、加速度、轮式编码器计数、参考轨迹RTK 或动捕。文件格式常见的是 CSV 或 ROS bag。我拿到数据后第一件事不是跑算法而是做时间戳对齐检查。IMU 频率通常 100200Hz轮式编码器 2050Hz两者时间戳如果来自不同时钟源会有几十毫秒的偏移这个偏移在高速转弯时会导致航向估计明显滞后。import pandas as pd import numpy as np # 读取 IMU 和轮式数据 imu pd.read_csv(imu.csv) # columns: timestamp, gx, gy, gz, ax, ay, az wheel pd.read_csv(wheel.csv) # columns: timestamp, left_count, right_count # 时间戳对齐以 IMU 时间为基准对轮式数据做线性插值 wheel_interp np.interp(imu[timestamp], wheel[timestamp], wheel[left_count]) wheel_interp_r np.interp(imu[timestamp], wheel[timestamp], wheel[right_count]) # 检查时间戳范围 print(fIMU time range: {imu[timestamp].min():.3f} - {imu[timestamp].max():.3f}) print(fWheel time range: {wheel[timestamp].min():.3f} - {wheel[timestamp].max():.3f}) # 如果两者起始时间差超过 0.1s需要做时钟同步或丢弃重叠段时间对齐之后用参考轨迹做真值对比。我习惯先画三张图航向角随时间变化、位置 XY 轨迹、位置误差随时间变化。如果航向角在静止时就有缓慢漂移说明陀螺零偏没标定如果位置误差在直线段就线性增长说明轮半径或轮距标定有误。3.2 滤波器初始化静止对齐与零偏估计ESKF 初始化最忌讳直接拿第一帧 IMU 数据当姿态。正确做法是让车静止 35 秒用加速度计测重力方向做横滚和俯仰对齐用陀螺均值做零偏初值。航向角无法从 IMU 单独观测通常设为零或从轮式里程计初始航向给定。def initialize_eskf(imu_buffer, wheel_buffer): # 静止段检测加速度模长接近 9.81角速度模长接近 0 acc_norm np.linalg.norm(imu_buffer[:, 3:6], axis1) gyro_norm np.linalg.norm(imu_buffer[:, 0:3], axis1) static_mask (np.abs(acc_norm - 9.81) 0.2) (gyro_norm 0.05) if static_mask.sum() 50: # 至少 0.5 秒静止数据 raise ValueError(静止段不足无法初始化) # 零偏初值静止段陀螺和加计均值 bg_init imu_buffer[static_mask, 0:3].mean(axis0) ba_init imu_buffer[static_mask, 3:6].mean(axis0) ba_init[2] - 9.81 # 去掉重力分量 # 横滚俯仰对齐用重力向量 g_body imu_buffer[static_mask, 3:6].mean(axis0) roll np.arctan2(-g_body[1], -g_body[2]) pitch np.arctan2(g_body[0], np.sqrt(g_body[1]**2 g_body[2]**2)) yaw 0.0 # 航向由轮式或外部参考给定 return bg_init, ba_init, roll, pitch, yaw这段代码的静止检测阈值需要根据 IMU 噪声水平调整。消费级 IMU 的加计噪声约 0.010.05 m/s²陀螺噪声约 0.0010.005 rad/s阈值设太紧会找不到静止段设太松会把缓慢运动误判为静止。我一般用 0.2 m/s² 和 0.05 rad/s 作为起点再根据实际数据微调。3.3 观测更新轮式速度与航向的融合策略观测更新是 ESKF 里最需要小心的地方。轮式线速度观测的噪声协方差不能设成固定值因为打滑时轮速和真实速度偏差很大。我的做法是加一个打滑检测比较 IMU 积分得到的加速度和轮式速度差分得到的加速度如果偏差超过阈值就放大轮式观测的噪声协方差。def update_wheel_observation(eskf, v_wheel, omega_wheel, dt): # 预测的轮式速度从 ESKF 状态里取机体速度 rot R.from_quat([eskf.q[1], eskf.q[2], eskf.q[3], eskf.q[0]]) v_body rot.inv().apply(eskf.v) # 打滑检测轮式速度与 IMU 积分速度的偏差 v_error np.abs(v_wheel - v_body[0]) if v_error 0.5: # 超过 0.5 m/s 认为打滑 R_wheel np.diag([1.0, 1.0]) # 放大噪声 else: R_wheel np.diag([0.01, 0.01]) # 正常噪声 # 观测矩阵观测机体 x 方向速度和航向角速度 H np.zeros((2, 15)) H[0, 3] 1.0 # 速度 x 分量 H[1, 8] 1.0 # 陀螺 z 零偏航向角速度观测 # 卡尔曼增益和状态更新 z np.array([v_wheel, omega_wheel]) y z - H eskf._get_error_state() S H eskf.P H.T R_wheel K eskf.P H.T np.linalg.inv(S) dx K y eskf._inject_error_state(dx) eskf.P (np.eye(15) - K H) eskf.P这里的打滑检测阈值 0.5 m/s 是经验值实际要根据轮式编码器精度和地面条件调整。观测矩阵 H 的构造需要和误差状态的定义严格对应如果误差状态顺序变了H 也要跟着改。航向角速度观测用陀螺 z 零偏对应的行是因为轮式航向角速度直接约束陀螺零偏的积分误差。4. 避坑与排查航位推算系统最常见的 5 个翻车点4.1 现象静止时航向缓慢漂移10 分钟偏了 5°原因陀螺零偏没有做温度补偿。MEMS 陀螺零偏随温度变化通常有 0.10.5°/s/°C 的系数如果标定时温度和运行时温度差 10°C零偏就偏了 15°/s。解决在静止段做零偏估计时记录温度用一阶线性模型补偿。如果 IMU 自带温度输出直接读寄存器如果没有贴一个 NTC 热敏电阻在 IMU 旁边标定零偏-温度曲线。4.2 现象直线行驶时位置误差线性增长每 10m 偏 0.3m原因轮半径标定不准。左右轮半径差 1%直线 10m 后航向偏 0.57°位置偏 0.05m但如果轮距也标偏了航向误差会放大。解决重新做直线标定用最小二乘拟合左右轮半径不要用单次测量值。标定距离至少 20m跑 5 次取平均。4.3 现象转弯时航向估计滞后滤波器输出比真实航向慢半拍原因轮式航向角速度观测的噪声协方差设得过大滤波器过度信任 IMU 积分。解决检查轮式航向角速度的计算公式确认轮距参数正确把轮式航向观测的噪声协方差降低到 0.001 量级同时提高 IMU 陀螺的噪声参数。如果还是滞后检查时间同步轮式数据可能比 IMU 晚 50ms 以上。4.4 现象过坎或颠簸路面时位置突然跳变原因加速度计受到冲击ESKF 的速度更新被污染。解决加一个加速度计异常检测如果加速度模长超过 2g 或与重力方向偏差超过 30°暂时关闭速度更新只做姿态更新。同时把加计的过程噪声调大让滤波器在冲击时更信任轮式观测。4.5 现象长时间运行后协方差矩阵非正定滤波器发散原因ESKF 的误差状态注入后没有做协方差对称化数值误差累积导致 P 矩阵失去正定性。解决每次更新后强制P (P P.T) / 2并定期做 Cholesky 分解检查。如果还是发散把误差状态从 15 维降到 9 维去掉加计零偏牺牲一点精度换稳定性。5. 进阶技巧用轮式运动学约束做零速修正与航向重观测5.1 零速修正ZUPT的触发条件与实现轮式平台在静止时轮式编码器计数不变但 IMU 陀螺仍有零偏输出如果不做修正航向会缓慢漂移。零速修正的逻辑是当轮式速度连续 0.5 秒小于 0.01 m/s且 IMU 角速度模长小于 0.02 rad/s 时认为车静止把 ESKF 的速度观测设为 0同时把陀螺零偏的观测噪声调小让零偏快速收敛。def zero_velocity_update(eskf, v_wheel, omega_imu, static_counter): if abs(v_wheel) 0.01 and np.linalg.norm(omega_imu) 0.02: static_counter 1 else: static_counter 0 if static_counter 50: # 0.5 秒 100Hz # 速度观测为 0 H np.zeros((3, 15)) H[0, 3] 1.0 H[1, 4] 1.0 H[2, 5] 1.0 R_zupt np.diag([0.001, 0.001, 0.001]) z np.zeros(3) y z - H eskf._get_error_state() S H eskf.P H.T R_zupt K eskf.P H.T np.linalg.inv(S) dx K y eskf._inject_error_state(dx) eskf.P (np.eye(15) - K H) eskf.P eskf.P (eskf.P eskf.P.T) / 2 return static_counterZUPT 的关键参数是静止检测的阈值和持续时间。阈值太松会把低速运动误判为静止导致速度被错误拉回 0阈值太紧则静止时无法触发。我一般用 0.01 m/s 和 0.02 rad/s 作为起点静止持续时间 0.5 秒。如果车在静止时仍有微小振动可以把持续时间延长到 1 秒。5.2 航向重观测用轮式差速做航向约束轮式航向角速度的精度在正常行驶时其实不差差的是打滑时的跳变。我的做法是在打滑检测未触发时把轮式航向角速度作为观测噪声协方差设小打滑触发时切换到 IMU 航向积分同时把陀螺零偏的噪声调大让滤波器知道当前航向不可靠。def heading_update(eskf, omega_wheel, slip_detected): if not slip_detected: # 轮式航向可靠观测陀螺 z 零偏 H np.zeros((1, 15)) H[0, 8] 1.0 R_heading np.array([[0.0001]]) z np.array([omega_wheel]) y z - H eskf._get_error_state() S H eskf.P H.T R_heading K eskf.P H.T np.linalg.inv(S) dx K y eskf._inject_error_state(dx) eskf.P (np.eye(15) - K H) eskf.P else: # 打滑时放大陀螺零偏噪声 eskf.P[8, 8] 0.001 eskf.P[9, 9] 0.001这个策略的核心是让滤波器根据打滑状态动态调整信任权重。实际跑下来在瓷砖地面和水泥地面航向误差可以控制在 0.5° 以内在草地和沙地打滑频繁航向误差会到 23°但比纯轮式里程计好很多。5.3 参数调试的优先级与验证方法调 ESKF 参数最忌讳一次改多个。我的顺序是先调陀螺零偏噪声sigma_bg让静止时航向不漂再调加计零偏噪声sigma_ba让直线行驶时位置误差不累积然后调轮式观测噪声让转弯时航向跟得上最后调打滑检测阈值让颠簸路面不跳变。每调一个参数跑同一段数据看三张图航向误差、位置误差、协方差对角线。如果协方差对角线在某个状态上持续增长说明该状态的观测不足需要检查对应的观测模型。验证方法我一般用两种一是闭合轨迹跑一个方形或 8 字形看起点和终点的位置差二是参考对比如果有 RTK 或动捕真值直接算 RMSE。没有真值时用轮式里程计单独跑一遍做基线看融合后提升了多少。如果融合后比纯轮式还差说明滤波器参数或观测模型有问题不要硬调回去检查时间同步和标定。这套方案我前后调了三个月最大的教训是不要迷信论文里的参数那些参数是在特定 IMU 和特定地面上调出来的换一个硬件、换一种地面全部要重调。我现在的习惯是每换一个平台先跑静止 10 分钟看零偏稳定性再跑直线 50m 看轮半径再跑原地旋转 10 圈看轮距最后跑闭合轨迹看整体误差。这三步做完心里就有底了。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?