ROS教程15:robot_localization——从轮式里程计与 IMU 到机器人状态估计
摘要:承接差速底盘、TF 与 URDF/Xacro,使用 robot_localization 将原始轮式里程计和 IMU 接入 EKF,理解状态、协方差、时间、frame 与 TF ownership。
@[toc]
第 12~14 章已经分别建立了三层基础:
1 | 第 12 章:Driver 数据契约 |
到这里,一个新的问题出现了:
1 | Driver 已经能发布 /odom |
为什么还需要 robot_localization?
因为 /odom 只是某个里程计来源给出的估计,并不等于“系统最终应该使用的机器人状态”。真实 AGV 中,轮编码器、IMU、视觉里程计、GPS 或其他定位源通常各自只擅长观测一部分状态,而且每个来源都有误差、漂移、延迟和不确定性。
robot_localization 的职责不是替代 Driver,也不是替代 SLAM,而是维护一个连续的机器人状态估计:
1 | Wheel Odometry ─┐ |
本章不把重点放在卡尔曼滤波矩阵证明,而是解决工程上更直接的问题:
1 | 哪些字段应该融合? |
本章新增:
1 | /workspace/ros_ws/src/ros1_localization_lab |
并把第 13、14 章教学阶段使用的 odom_tf_broadcaster 从最终运行链中移除,让 robot_localization 正式接管 odom -> base_link。
1. robot_localization 是什么,它不是什么
robot_localization 是 ROS 中用于实时非线性状态估计的一组 Node。ROS1 Noetic 中最常用的两个状态估计 Node 是:
1 | ekf_localization_node |
分别使用:
1 | EKF:Extended Kalman Filter |
两者对外的输入、frame/time 处理和绝大多数配置相同,但内部传播非线性状态的方式不同。本章以 EKF 作为默认工程路径,同时保留 UKF 对照实现:先理解共同的数据契约,再比较 Jacobian 线性化与 sigma-point 传播的差异。
robot_localization 可以接收的常见消息包括:
1 | nav_msgs/Odometry |
它不是:
1 | 电机 Driver |
它位于这些模块之间:
1 | graph LR |
因此,这一层真正关心的是:
上游给出的观测值是什么、在哪个坐标系、对应哪个时间、可信度多大;然后维护一份连续状态估计并对外发布。
2. 为什么把 /odom 改成 /odom/raw
第 12 章 Driver 默认发布:
1 | /odom |
第 13 章为了学习 TF,又用:
1 | /odom |
这条链在学习 TF 时没有问题,但进入状态估计以后必须重新划分 ownership。
本章把 Driver 的输出改成:
1 | /odom/raw |
它代表:
由轮编码器和差速运动学直接得到的原始 wheel odometry 估计。
EKF 输出:
1 | /odometry/filtered |
并发布:
1 | odom -> base_link |
于是 ownership 变成:
1 | ros1_driver_lab |
这时不再启动:
1 | ros1_tf_lab/odom_tf_broadcaster |
否则会出现两个 Node 同时发布:
1 | odom -> base_link |
TF tree 中同一条 parent/child 变换应有明确的唯一运行时 owner。状态估计上线以后,这条动态变换由 EKF 接管。
3. robot_localization 到底在估计什么
robot_localization 的状态估计 Node 维护 15 个状态变量:
1 | 位置: |
可以写成:
1 | state = [ |
注意这里的 15 维状态不代表必须有 15 个传感器,也不代表每个传感器都要提供全部 15 维。
一台二维差速 AGV 最常关心的通常是:
1 | X |
而:
1 | Z |
在平整地面导航中往往不需要作为自由状态持续漂移。
这就是 two_d_mode 的作用。
本章配置:
1 | two_d_mode: true |
其目的不是“把所有输入消息变成二维消息”,而是让状态估计器把与三维平面外运动相关的状态约束在二维机器人模型中。
因此当前学习对象可以先收敛为:
1 | 平面位置 + 平面朝向 + 前向速度 + 偏航角速度 |
4. EKF 与 UKF:都在做 predict + correct,但处理非线性的方式不同
卡尔曼滤波的核心不是“把多个传感器求平均”,而是持续维护两样东西:
1 | 状态估计 x |
随后不断重复:
1 | graph LR |
EKF 和 UKF 都遵循这条主线,差别主要发生在:
当运动模型或测量模型是非线性的,怎样把“均值和协方差”从当前时刻传播到下一时刻。
4.1 为什么移动机器人不是简单的线性系统
如果状态只有:
1 | x |
并且机器人永远沿世界坐标系 X 轴运动,可以近似写成:
1 | x(k+1) = x(k) + vx * dt |
这是很直观的线性关系。
但移动机器人真正的平面运动还包含 yaw。车体坐标系中的前向速度 Vx 要投影到世界坐标系:
1 | dx = Vx * cos(yaw) * dt |
一旦出现:
1 | sin() |
状态转移就不再是简单的常系数线性矩阵。
robot_localization 的 EKF/UKF 都使用非线性机器人运动模型;两者共享绝大多数 ROS 参数和输入预处理,区别在滤波核心怎样传播这套非线性关系。
4.2 EKF:Extended Kalman Filter / 扩展卡尔曼滤波器
EKF 的核心思想是:
非线性函数本身不好直接传播协方差,就在“当前状态附近”把它局部线性化。
设状态转移为:
1 | x(k+1) = f(x(k), u(k)) |
测量模型为:
1 | z(k) = h(x(k)) |
在通用 EKF 理论中,f()、h() 都可以是非线性的。robot_localization 的主要非线性集中在运动模型和坐标变换;传感器消息经过 frame 转换、字段筛选后,滤波核心的测量更新主要按选中的状态分量建立观测矩阵。
Predict 阶段
状态本身直接通过非线性运动模型预测:
1 | x- = f(x) |
但是协方差传播需要一个线性近似,因此 EKF 在当前状态附近计算 Jacobian:
1 | F = ∂f/∂x |
然后近似传播:
1 | P- = F * P * F^T + Q |
这里的 Jacobian 可以理解成:
当前状态附近,每一个状态量发生一点变化,会让下一时刻各状态量变化多少。
Noetic robot_localization 的实现中确实维护:
1 | transferFunction_ |
EKF predict() 会根据 roll / pitch / yaw、速度、加速度和 dt 建立状态转移关系,并计算对应 Jacobian 来传播协方差。
Correct 阶段
测量到达后,EKF 计算:
1 | innovation = measurement - predicted measurement |
再根据预测协方差和测量协方差得到 Kalman Gain:
1 | K |
可以把 K 理解为:
当前这一维应该更相信预测,还是更相信传感器观测。
于是完成:
1 | 预测状态 |
这就是为什么 covariance 会直接改变融合结果,而不是一个仅用于“描述精度”的附属字段。
EKF 的作用和适用场景
在当前 AGV 学习路径中,EKF 非常合适:
1 | 轮式里程计 + IMU |
工程上通常优先从 EKF 开始,因为它:
- 计算量较低;
- 行为容易分析;
robot_localization中使用成熟;- 对常见移动机器人运动模型通常已经足够。
它的限制来自“局部线性化”:如果非线性很强、状态不确定性很大,单个 Jacobian 对当前概率分布的近似可能不够准确。
4.3 UKF:Unscented Kalman Filter / 无迹卡尔曼滤波器
UKF 不再通过 Jacobian 把非线性模型线性化。
它换了一种思路:
不直接近似非线性函数,而是在当前概率分布周围挑选一组代表性的状态点,让这些点真正穿过非线性函数,再由结果恢复新的均值和协方差。
这些点就是:
1 | Sigma Points |
对于 n 维状态,经典 Unscented Transform 使用:
1 | 2n + 1 |
个 sigma points。
robot_localization 固定维护 15 维状态,因此 Noetic ukf.cpp 中:
1 | STATE_SIZE = 15 |
也就是说,一次 UKF 预测需要把 31 个代表性状态点通过运动模型。
可以把过程理解成:
1 | graph LR |
UKF 的三个专属参数:
1 | alpha: 0.001 |
作用分别可以先这样理解:
| 参数 | 作用 |
|---|---|
alpha |
控制 sigma points 围绕均值展开的尺度 |
kappa |
参与控制 sigma points 的分布范围 |
beta |
注入对状态分布先验形状的知识,默认 2 对高斯分布较合适 |
Noetic 官方文档明确建议:如果并不熟悉 UKF 参数化,保持默认值即可。因此本教程的 UKF profile 不把调 alpha/kappa/beta 当作“优化手段”。
4.4 EKF 与 UKF 对比
| 维度 | EKF | UKF |
|---|---|---|
| 全称 | Extended Kalman Filter | Unscented Kalman Filter |
| 中文 | 扩展卡尔曼滤波器 | 无迹卡尔曼滤波器 |
| 非线性处理 | 当前状态附近做 Jacobian 线性化 | 让 sigma points 通过真实非线性模型 |
| 是否需要 Jacobian | 需要 | 不需要 |
robot_localization 运动模型 |
与 UKF 使用同一套机器人运动模型 | 与 EKF 使用同一套机器人运动模型 |
| 计算量 | 较低 | 较高;15 维状态需要传播 31 个 sigma points |
| 强非线性下的近似 | 依赖局部线性化质量 | 通常能更直接保留非线性传播后的均值/协方差特征 |
| 工程调试 | 相对直接 | 多出 alpha/kappa/beta 和 sigma-point 行为 |
| 本系列默认 | 是 | 作为对照实验 |
不能简单得出:
1 | UKF 一定比 EKF 准 |
滤波效果通常首先受这些因素限制:
1 | 传感器本身是否可信 |
如果这些输入契约本身错误,换成 UKF 不会自动修复系统。
4.5 在本工程里怎样真正切换 EKF / UKF
为了让两种算法保持相同输入条件,localization_lab.launch 增加:
1 | <arg name="filter_node" default="ekf_localization_node" /> |
Node 类型使用:
1 | type="$(arg filter_node)" |
默认仍运行 EKF:
1 | roslaunch ros1_localization_lab localization_lab.launch |
切换 UKF 时,保持 /odom/raw、/imu/data_raw、frame、TF ownership 和选中的状态量不变,只替换滤波核心:
1 | roslaunch ros1_localization_lab localization_lab.launch \ |
这样做的工程意义是:
1 | 同一批输入 |
因此比较结果才有意义。
当前教学 Driver 的“IMU”仍由左右轮数据推导,它不是独立真实传感器,所以这个实验只能验证:
1 | 配置能否切换 |
不能用来证明哪一种滤波器在真实 AGV 上精度更高。
5. P、Q、R 分别代表什么
不展开矩阵推导,也必须建立三个概念:
1 | P:Estimate Error Covariance |
可以把它们放回 predict/correct:
1 | 上一状态 + P |
在 ROS 消息接口里,第 12 章已经见过的:
1 | Odometry.pose.covariance |
主要对应测量侧的不确定性,也就是这里最常讨论的 R。
重要的是:
covariance 不是“为了让消息字段填满而放几个数”。它会直接影响滤波器如何对待这份测量。
方差越小,代表这一个维度声明得越确定;方差越大,代表测量越不可信。
但这不意味着可以通过“把某个 covariance 随便调得很小”来让结果看起来稳定。真实产品中的 covariance 应来自传感器规格、实验统计、标定或可解释的误差模型。
本系列 YAML 中的数值仍然只是教学参数。
6. 15 个 true / false 到底是什么意思
每一个输入都需要一个 _config 数组。
顺序固定为:
1 | X, Y, Z, |
例如本章默认 wheel odometry 配置:
1 | odom0_config: [false, false, false, |
只有第 7 个位置是 true:
1 | Vx = true |
所以当前 wheel odometry 只贡献:
1 | 车体前向线速度 Vx |
IMU 配置:
1 | imu0_config: [false, false, false, |
这里只融合:
1 | Vyaw = angular_velocity.z |
于是默认实验的观测关系非常清楚:
1 | wheel odometry -> Vx |
而不是“消息里有什么字段就全部设置 true”。
7. 为什么默认不融合 /odom/raw 的 X、Y、yaw
第 12 章的 /odom 是怎样得到的?
1 | left/right encoder velocity |
也就是说,在当前教学 Driver 中:
1 | X / Y / yaw |
和:
1 | Vx / Vyaw |
不是互相独立的传感器信息。
前者本来就是后者积分出来的。
如果把:
1 | X |
全部机械设置为 true,就很容易产生一个认知错误:
好像 EKF 同时获得了五份独立证据。
实际上它们高度相关,来源仍然是同一套轮编码器。
因此默认 profile 故意只从 wheel odometry 中取:
1 | Vx |
再从 IMU 接口中取:
1 | Vyaw |
这样配置的教学目的不是宣称它在数值上“最优”,而是建立正确的数据来源意识:
先问这个状态是谁真正测到的,再决定是否把它作为观测输入,而不是按消息字段数量配置滤波器。
本工程中的 Demo IMU 还有一个必须明确的限制:
1 | imu_angular_velocity_z |
目前仍然是由左右轮速度在进程内推导出来的,并不是真实独立的 IMU 硬件测量。
因此本实验可以验证:
1 | 接口 |
但不能拿来证明“融合后定位精度一定提高”。
真实硬件接入后,IMU 和轮编码器才会形成真正不同的误差来源。
8. 为什么当前 IMU 只用 angular_velocity.z
第 12 章的 IMU 消息明确写了:
1 | msg.orientation_covariance[0] = -1.0; |
这表示当前设备没有提供可用的 orientation 估计。
虽然示例消息中:
1 | orientation.w = 1.0 |
但这个四元数只是为了保持消息字段合法,不代表存在真实姿态解算结果。
因此本章不能突然配置:
1 | yaw = true |
去融合 orientation。
当前能够使用的是:
1 | angular_velocity.z |
即:
1 | 绕 Z 轴角速度 |
它在当前 IMU frame:
1 | imu_link |
中表达。
第 14 章已经通过 URDF/Xacro 建立:
1 | base_link -> imu_link |
所以 robot_localization 可以借助 TF 把 IMU 测量正确地转换到机器人状态估计所需的坐标关系中。
这也说明三个章节不是独立知识点:
1 | Driver 给 frame_id |
9. world_frame 决定谁负责哪一条 TF
本章配置:
1 | map_frame: map |
这里最重要的是:
1 | world_frame: odom |
对于当前只融合连续局部运动信息的场景:
1 | wheel odometry |
使用 odom 作为 world frame 最自然。
此时 robot_localization 对外发布:
1 | odom -> base_link |
于是本章 TF ownership 为:
1 | graph TD |
这里故意没有:
1 | map -> odom |
因为本章还没有进入全局定位。
下一章 SLAM / AMCL 才会真正处理:
1 | map -> odom |
这一层设计背后的含义是:
1 | odom |
因此不能为了“TF tree 看起来完整”就在状态估计阶段随便用 static transform 固定 map -> odom。
10. 时间:filter 不是收到消息才简单回调一次
状态估计器有自己的输出频率:
1 | frequency: 30.0 |
它还需要处理:
1 | 输入消息时间戳 |
本章设置:
1 | sensor_timeout: 0.20 |
含义不是:
1 | 0.20 s 没消息 -> Node 退出 |
而是:
当输入传感器在该时间范围内没有提供新的修正信息时,滤波器仍可依据内部模型继续执行 prediction,而不是进行 measurement correction。
这就是为什么:
1 | Predict |
和:
1 | Correct |
必须区分。
两个输入的 queue 也设置为:
1 | odom0_queue_size: 10 |
queue 的价值主要体现在传感器频率高于滤波更新频率,或者短时间内多条消息到达时,避免只依赖一个极小 subscriber queue 丢失待处理测量。
当前:
1 | transform_timeout: 0.0 |
表示不为了等待 TF 长时间阻塞滤波循环。后续真实系统如果存在 TF 发布延迟,需要结合传感器时间戳和整体实时性重新评估,而不是简单把 timeout 调大。
11. differential 和 relative:不是两个“让起点归零”的开关
robot_localization 为包含 pose 信息的输入提供:
1 | [sensor]_differential |
例如:
1 | odom0_differential: false |
这两个参数经常因为都涉及“做差”而被混淆,但它们改变的是完全不同的观测语义。
先假设同一个 pose 传感器连续输出:
1 | t0: x = 10.0 m, yaw = 30° |
11.1 differential 的原理:相邻两帧做差,然后转换成速度观测
设置:
1 | pose0_differential: true |
核心不是简单把:
1 | 10.0 m |
改成:
1 | 0 |
而是:
1 | 当前 pose |
对于最简单的一维平移,可以近似理解成:
1 | vx ≈ (x_t - x_t-1) / dt |
对于姿态,实际实现需要在变换关系中计算相邻姿态变化,而不是直接把 Euler 角机械相减。
因此 differential=true 之后,原本的“绝对 pose 测量”不再以绝对 pose 的形式约束滤波器,而变成对运动变化率的约束。
这也是它名字叫:
1 | differential |
而不是 relative_to_start。
differential 的作用
典型问题是存在两个绝对 yaw 来源:
1 | wheel odometry yaw |
理论上两者都描述 yaw,但长时间后可能逐渐分离:
1 | wheel yaw: 20° -> 40° -> 65° |
如果两个输入都宣称很小 covariance,滤波器会不断被两个彼此冲突的绝对姿态拉扯。
一种策略是:
1 | 最可信的来源 -> 保留 absolute pose |
这样第二个来源不再持续要求状态“回到它的绝对角度”,而主要告诉滤波器:
1 | 这一小段时间转了多少 |
differential 的使用场景
适合考虑:
- 两个独立传感器都提供同一个绝对 pose / orientation 变量;
- 两个绝对值会逐渐发生偏移;
- 第二个来源没有更合适的原生 velocity 输出;
- 更关心它提供的局部变化,而不是绝对零点。
如果传感器本来就有高质量速度,例如 IMU 已经直接给:
1 | angular_velocity.z |
通常优先直接融合这个速度,而不是先拿 orientation 再 differential 回速度。
differential 的代价
把 absolute orientation 转成角速度以后,相当于失去了这个输入提供的“绝对朝向锚点”。
如果所有 orientation 来源最终都被 differential 化,那么 yaw 的绝对误差缺少观测去拉回,姿态相关 covariance 可以持续增长。
因此:
differential=true是改变观测模型,不是一个无代价的“防抖开关”。
另外,官方文档明确要求通过 navsat_transform_node / UTM 融合 GPS 类绝对位置时,相关输入的 _differential 应保持 false。
11.2 relative 的原理:始终减去首帧,但仍然保持 pose 语义
设置:
1 | pose0_relative: true |
处理关系可以概念化为:
1 | 第 0 帧 = reference |
对单独的标量分量,可以直观理解为:
1 | x: 10.0 -> 0.0, 10.5 -> 0.5, 11.2 -> 1.2 |
但完整 2D/3D pose 不是把 x/y/yaw 三个数字彼此独立机械相减。首帧本身带有旋转时,后续平移还要在首帧参考姿态下重新表达;工程上应把它理解为“当前位姿相对首帧位姿的变换”。
关键区别是:
1 | relative 之后仍然是 pose |
它不会除以 dt,也不会把结果转换成 velocity。
所以它表达的是:
相对这个传感器第一次出现的位置,现在在哪里。
relative 的作用
有些传感器启动时会给出一个不方便直接放进当前局部坐标系的绝对初值,例如:
1 | 第一次位置 = (10.0, 2.0) |
如果当前实验只关心“从启动时刻以后运动了多少”,可以让首帧变成局部零点:
1 | (10.0, 2.0, 30°) -> (0, 0, 0) |
之后仍然使用完整 pose 变化:
1 | 当前 pose |
relative 的使用场景
适合考虑:
- 需要让某个 pose 传感器以首帧作为局部原点;
- 关心从启动位置开始的相对位姿;
- 不希望把 pose 转换成 velocity;
- 传感器初始绝对值本身不是当前系统想保留的全局锚点。
它不适合拿来解决:
1 | 两个绝对传感器长期漂移后互相冲突 |
因为 relative 只消除了“初始常量偏移”,不会消除后续不同传感器各自的漂移。
11.3 differential 与 relative 的核心对比
| 维度 | differential |
relative |
|---|---|---|
| 参考对象 | 上一帧 t-1 |
第一帧 t0 |
| 基本处理 | 相邻 pose 做差 | 当前 pose 减首帧 pose |
是否使用 dt |
是 | 否 |
| 输出给滤波器的语义 | velocity | pose |
| 是否保留绝对 pose 锚点 | 不保留该输入的绝对 pose | 保留“相对首帧”的 pose 约束 |
| 主要目的 | 把绝对 pose 来源改造成增量/速度来源 | 让传感器从本地零点开始 |
| 典型场景 | 多个绝对 pose 来源冲突,且缺少直接速度观测 | 初始 pose 非零,但只关心启动后的相对位姿 |
| 主要风险 | 失去绝对姿态约束,相关 covariance 可能持续增长 | 只能去掉初始偏置,不能解决后续漂移 |
可以把两者压缩成一句:
1 | differential: 当前 - 上一帧 -> 再除以 dt -> velocity |
11.4 当前默认工程为什么两个都关闭
本章默认融合:
1 | /odom/raw -> Vx |
也就是说,真正启用的字段已经是:
1 | velocity |
并没有启用:
1 | wheel pose X/Y/yaw |
所以:
1 | odom0_differential: false |
是有意选择。
尤其当前 IMU 已经直接提供:
1 | angular_velocity.z |
再去融合一个 orientation 并设置 imu0_differential=true,本质上是在绕一圈重新生成角速度观测,没有教学和工程必要。
11.5 工程实现:给 differential / relative 单独建立 pose 实验 profile
为了不污染默认 wheel + IMU 链路,本工程新增两个独立配置:
1 | config/ekf_pose_differential.yaml |
它们都订阅:
1 | /demo_pose |
并选择:
1 | X |
作为 pose 输入。
differential profile:
1 | pose0: /demo_pose |
relative profile:
1 | pose0: /demo_pose |
配套 launch:
1 | launch/pose_mode_lab.launch |
默认启动 relative:
1 | roslaunch ros1_localization_lab pose_mode_lab.launch |
切换 differential:
1 | roslaunch ros1_localization_lab pose_mode_lab.launch \ |
这个实验 launch 故意不启动 Driver,也不接 /odom/raw 和 /imu/data_raw:
1 | /demo_pose |
这样观察到的变化只来自这两个参数本身,而不会被其它观测混在一起。
11.6 怎样观察两种模式的区别
本工程新增 demo_pose_sequence.py,固定发布三帧非零初始 pose:
1 | 第 1 帧: x=10.0, y=2.0, yaw=30° |
先启动 relative profile:
1 | roslaunch ros1_localization_lab pose_mode_lab.launch \ |
另开终端发布测试序列:
1 | rosrun ros1_localization_lab demo_pose_sequence.py |
再观察:
1 | rostopic echo /odometry/filtered |
然后停止当前 filter,换成 differential profile:
1 | roslaunch ros1_localization_lab pose_mode_lab.launch \ |
再次执行:
1 | rosrun ros1_localization_lab demo_pose_sequence.py |
两个 pose profile 的 sensor_timeout 设为 2.0 s,大于演示脚本的 1.0 s 发帧间隔,避免在比较参数语义时额外混入 sensor-timeout 的 predict-only 周期。
differential 至少需要前后两帧才能形成第一份差分观测;relative 在收到第一帧时就已经建立了首帧参考。
relative 模式应该重点看:
1 | 首帧是否成为局部零点 |
differential 模式应该重点看:
1 | 绝对的 10 m / 30° 是否不再直接约束状态 |
这里不要拿两种输出数值直接比较“谁更准”。它们本来就在表达不同的观测语义。
12. 创建 ros1_localization_lab
本章新增 package:
1 | ros1_localization_lab/ |
package.xml 的运行时依赖重点是:
1 | <exec_depend>geometry_msgs</exec_depend> |
Docker Image 中也新增:
1 | ros-noetic-robot-localization |
因此更新代码后需要重新构建 Image:
1 | docker compose up -d --build |
进入 Container:
1 | docker compose exec ros1-dev bash |
然后重新构建业务 workspace:
1 | cd /workspace/ros_ws |
这里的 catkin build 是读者在 Noetic Container 中执行的操作;本文交付环境没有运行 ROS Noetic,因此不把静态检查声明为实际构建通过。
13. 默认 EKF 配置为什么这样写
打开:
1 | /workspace/ros_ws/src/ros1_localization_lab/config/ekf_wheel_imu.yaml |
主配置:
1 | frequency: 30.0 |
然后是两个输入:
1 | odom0: /odom/raw |
选择矩阵:
1 | odom0 -> Vx |
这不是一套可以直接复制到所有机器人上的“标准答案”。
它只和当前教学工程的已知数据来源匹配:
1 | Wheel: |
真实项目中必须重新回答:
1 | 每个字段是谁测到的? |
再决定 _config。
UKF 对照文件:
1 | /workspace/ros_ws/src/ros1_localization_lab/config/ukf_wheel_imu.yaml |
与默认 EKF 保持相同的 sensor 配置,只增加 UKF 专属参数:
1 | alpha: 0.001 |
因此两份配置的差异集中在滤波算法本身,而不是偷偷改变输入字段或 covariance。
14. launch 如何重新划分整个运行链
启动文件:
1 | /workspace/ros_ws/src/ros1_localization_lab/launch/localization_lab.launch |
做三件事。
第一,启动第 12 章 Driver,但覆盖里程计 Topic:
1 | <param name="odom_topic" value="/odom/raw" /> |
第二,继续加载第 14 章 Xacro,并启动:
1 | robot_state_publisher |
这是为了保留:
1 | base_link -> imu_link |
第三,通过 launch 参数选择滤波核心:
1 | <arg name="filter_node" default="ekf_localization_node" /> |
默认加载:
1 | ekf_wheel_imu.yaml |
切换 UKF 时同时指定:
1 | filter_node:=ukf_localization_node |
注意整个 launch 中没有:
1 | odom_tf_broadcaster |
这是有意删除,而不是遗漏。
因为:
1 | publish_tf=true |
以后 odom -> base_link 已属于当前启动的状态估计器(EKF 或 UKF)。
15. 第一次启动:先观察 raw,再观察 filtered
启动:
1 | roslaunch ros1_localization_lab localization_lab.launch |
如果需要在完全相同的 wheel + IMU 输入下切换 UKF:
1 | roslaunch ros1_localization_lab localization_lab.launch \ |
两种模式都继续观察同一个输出接口:
1 | /odometry/filtered |
因此上层消费者不需要因为 EKF/UKF 切换而改 Topic 或 TF 契约。
另开终端,加载环境:
1 | source /workspace/ros_ws/devel/setup.bash |
先看 Node:
1 | rosnode list |
至少应看到:
1 | /chassis_driver_node |
然后看输入:
1 | rostopic echo -n 1 /odom/raw |
再看 IMU:
1 | rostopic echo -n 1 /imu/data_raw |
最后看滤波输出:
1 | rostopic echo -n 1 /odometry/filtered |
如果还没有发送速度命令,机器人保持静止,filtered odometry 的位置和速度应保持在初始状态附近。
发送:
1 | rostopic pub -r 10 /cmd_vel geometry_msgs/Twist \ |
再观察:
1 | rostopic echo /odometry/filtered |
重点不是比较某一个瞬间数值“谁更准”,而是确认数据链已经变成:
1 | /cmd_vel |
16. 再检查 TF:现在是谁在发布 odom -> base_link
执行:
1 | rosrun tf tf_echo odom base_link |
还可以:
1 | rosrun tf2_tools view_frames.py |
当前树应该形成:
1 | odom |
但这一次:
1 | odom -> base_link |
不是第 13 章的 odom_tf_broadcaster 发布,而是:
1 | ekf_localization_node |
这是本章最重要的结构变化之一。
如果仍然额外启动:
1 | ros1_tf_lab/odom_tf_broadcaster |
就重新制造了 TF ownership 冲突。
17. wheel-only profile 用来比较“有没有第二种输入”
本 package 还提供:
1 | config/ekf_wheel_only.yaml |
它只使用:
1 | wheel Vx |
启动:
1 | roslaunch ros1_localization_lab localization_lab.launch \ |
默认 profile:
1 | ekf_wheel_imu.yaml |
使用:
1 | wheel Vx |
两个 profile 的目的不是进行“算法性能评测”,而是让输入 ownership 清晰可见:
1 | wheel-only:同一个 Odometry 输入贡献 Vx + Vyaw |
由于当前 Demo IMU 本身仍由 wheel velocity 推导,不能把二者差异解释为真实硬件传感器融合收益。
当后续换成真实 IMU 后,这个接口结构无需改变:
1 | /imu/data_raw |
的 producer 换成真实设备 Driver 即可。
18. 用 launch 参数观察 covariance 契约
localization_lab.launch 暴露两个参数:
1 | imu_angular_velocity_variance |
例如:
1 | roslaunch ros1_localization_lab localization_lab.launch \ |
或者:
1 | roslaunch ros1_localization_lab localization_lab.launch \ |
然后查看原始消息:
1 | rostopic echo -n 1 /imu/data_raw/angular_velocity_covariance |
这些参数改变的是上游测量声明的不确定性。
当前默认 profile 中:
1 | wheel 只观测 Vx |
二者没有直接竞争同一个状态维度,所以不要期待通过这一实验看到“两个 yaw 数值相互抢权重”的效果。
本实验真正验证的是:
covariance 属于传感器数据契约,并且它会随观测进入滤波器;不能在 EKF 配置之外被当作无意义字段。
真正需要比较两个来源对同一状态的权重时,应使用两份物理上独立、都能观测该状态的传感器数据,再分析 covariance,而不是人为制造重复信息。
19. 这一章把前面的三层第一次真正合在一起
现在可以把第 12~15 章重新串成一条链:
1 | graph LR |
其中每个组件的 ownership 已经变得清楚:
| 数据 / 变换 | 当前 owner |
|---|---|
/odom/raw |
ros1_driver_lab |
/imu/data_raw |
ros1_driver_lab |
/joint_states |
ros1_driver_lab |
/odometry/filtered |
ekf_localization_node |
odom -> base_link |
ekf_localization_node |
base_link -> imu_link / laser_link / wheel links |
robot_state_publisher |
map -> odom |
尚未进入,本章无人发布 |
最后这一行非常重要。
当前 TF tree 故意还没有全局层:
1 | map -> odom |
下一章才进入:
1 | SLAM |
也就是从“连续局部状态估计”进入“机器人在全局地图中的位置”。
20. 本章使用的官方资料边界
本章关于 robot_localization 的 15 维状态、EKF/UKF、two_d_mode、sensor_timeout、world_frame、publish_tf、*_config、*_differential 和 *_relative 的行为,依据 ROS robot_localization 官方文档与维护仓库说明:
- ROS Noetic API 文档入口:https://docs.ros.org/en/noetic/api/robot_localization/html/index.html
robot_localization状态估计 Node 文档:https://github.com/cra-ros-pkg/robot_localization/blob/noetic-devel/doc/state_estimation_nodes.rst- 配置说明:https://github.com/cra-ros-pkg/robot_localization/blob/noetic-devel/doc/configuring_robot_localization.rst
- Noetic
FilterBaseAPI:https://docs.ros.org/en/noetic/api/robot_localization/html/api/classRobotLocalization_1_1FilterBase.html - Noetic UKF 源码:https://docs.ros.org/en/noetic/api/robot_localization/html/api/ukf_8cpp_source.html
本工程固定在 ROS1 Noetic;仓库链接也绑定 noetic-devel,避免把 ROS2 分支上的参数或行为误写进本章。









