Nav2 跑不起来?先查 TF 树
2026/6/22 21:23:19 网站建设 项目流程

项目已开源到GitHub,欢迎Star

https://github.com/Ikunio/Lidar_nav2_wshttps://github.com/Ikunio/Lidar_nav2_ws

Nav2 跑不起来,真不一定是算法问题:我用一棵 TF 树接通了 LIO、重定位和导航

很多人第一次调 Nav2 的时候,都会经历一个经典阶段:

机器人不动,怀疑 Nav2。
机器人乱动,怀疑 DWB。
地图飞了,怀疑 FAST-LIO。
RViz 一片红,开始怀疑人生。

但我后来发现,很多时候问题并不在算法本身。

不是 FAST-LIO 不行。
不是 Point-LIO 不行。
也不是 Nav2 看你不顺眼。

真正的问题可能只有一个:

TF 树没接对。

在 ROS 2 机器人系统里,TF 就像机器人的“族谱”。
谁是爸爸,谁是儿子,谁负责发布谁的变换,一旦乱了,机器人就会出现各种玄学症状:

  • RViz 里机器人原地漂移;

  • Nav2 一直等待 transform;

  • 路径规划正常,但机器人就是不走;

  • 重定位成功了,但导航还是像喝多了一样;

  • LIO 明明输出了位姿,Nav2 却完全不买账。

这篇文章就围绕我开源的Lidar_nav2_ws项目,讲清楚一个入门阶段非常容易踩坑的问题:

如何用一棵标准 TF 树,把 LIO 里程计、3D 点云重定位和 Nav2 导航真正接起来。


一、先说结论:Nav2 不是要“一个位姿”,而是要“一套关系”

很多初学者刚接触 Nav2,会有一个误区:

我已经有 LIO 位姿了,为什么 Nav2 还不能跑?

这句话表面看没问题,但实际少了一半。

Nav2 不只是要你告诉它:

机器人现在在哪里?

它更关心的是:

机器人在 odom 里怎么动? odom 在 map 里怎么偏? 雷达在底盘上的位置是多少? 底盘中心到底是谁?

也就是说,Nav2 要的不是一个孤零零的坐标,而是一整棵 TF 树。

在我的项目中,核心 TF 树是:

map └── odom └── base_footprint └── chassis └── livox_frame

这棵树看起来很简单,但每一层都有明确分工。

如果你把它接反了,Nav2 可能不会立刻报错,但它会用非常抽象的方式提醒你:

我不知道我是谁。
我不知道我在哪。
我不知道我要去哪。
我知道你很急,但你先别急。


二、这几个坐标系到底是谁?

先把几个核心 frame 讲清楚。

1.map

map是全局地图坐标系。

你可以理解成:

机器人世界里的“地球坐标系”。

它通常来自:

  • 2D 栅格地图;

  • 3D 先验点云地图;

  • SLAM 建图结果;

  • 重定位系统。

map是全局稳定的。
机器人从哪里开机不重要,只要重定位成功,它最终都应该知道自己在map里的位置。


2.odom

odom是局部里程计坐标系。

它的特点是:

短时间连续、平滑,但长期可能会漂。

LIO 输出的位姿,本质上更接近里程计。
它适合描述机器人短时间内怎么运动,例如:

我向前走了 1 米 我左转了 30 度 我又漂了一点,但是我不说

所以 LIO 更适合维护:

odom → base_footprint

而不是直接硬塞成:

map → base_footprint

因为 LIO 会漂,map不应该跟着它一起漂。


3.base_footprint

base_footprint是机器人底盘在地面上的投影中心。

对 Nav2 来说,它通常比base_link更友好。

为什么?

因为导航关心的是机器人在地面上怎么走:

  • x;

  • y;

  • yaw。

它通常不关心机器人因为地面不平产生的 roll 和 pitch。

所以在移动机器人导航里,base_footprint常常作为 Nav2 的核心底盘 frame。

你可以把它理解成:

机器人贴在地面上的影子。

Nav2 真正规划的是这个“影子”怎么在地图上移动。


4.chassis

chassis是机器人真实底盘结构坐标系。

它可以和base_footprint重合,也可以有一个固定变换。

比如:

base_footprint → chassis

这个变换通常是静态 TF。

它表示:

底盘结构中心相对于地面投影中心在哪里。


5.livox_frame

livox_frame是 Livox MID-360 雷达坐标系。

它通常挂在底盘上:

chassis → livox_frame

这个变换也是静态 TF。

这一步非常关键。

因为点云是雷达坐标系下的,如果 Nav2 或重定位系统不知道雷达安装在哪里,它看到的点云就像一个人戴反了眼镜:

墙在前面,它以为墙在屁股后面。
门在左边,它觉得门在天上。
然后你开始怀疑算法。


三、LIO 输出的不是 Nav2 想要的“标准底盘里程计”

FAST-LIO / Point-LIO 本身是很强的 LiDAR-Inertial Odometry 算法。

但要注意:

LIO 的输出是为了估计 LiDAR/IMU 的运动,不是为了直接伺候 Nav2。

很多 LIO 系统内部会有自己的坐标命名,例如:

camera_init body aft_mapped lidar imu

这些坐标系在 LIO 算法内部很正常,但 Nav2 不认识这些“黑话”。

Nav2 更喜欢的是:

map odom base_footprint base_link laser

所以问题来了:

LIO 输出的camera_init → body,不能直接当成 Nav2 的odom → base_footprint用。

这就像你拿着一份英文论文去问食堂阿姨今天有什么菜。

不是对方能力不行,是接口语言不一致。

因此,我在项目中加了一层桥接逻辑:
把 FAST-LIO / Point-LIO 输出的内部位姿,转换成 Nav2 更能理解的标准里程计关系:

odom → base_footprint

这一步的意义非常大。

它让上游 LIO 算法可以自由切换,而下游 Nav2 完全不用关心你今天用的是 FAST-LIO 还是 Point-LIO。


四、一棵正确的 TF 树,应该谁发布谁?

这部分是重点。

一个比较清晰的分工应该是:

map → odom 由重定位模块发布 odom → base_footprint 由 LIO 桥接模块发布 base_footprint → chassis 由静态 TF 发布 chassis → livox_frame 由静态 TF 发布

展开来看就是:

map └── odom ← 3D 重定位维护 └── base_footprint ← LIO 里程计维护 └── chassis ← 机器人结构静态 TF └── livox_frame ← 雷达安装外参静态 TF

为什么要这样拆?

因为每一段的物理意义不同。


odom → base_footprint:回答“我短时间怎么动了?”

这一段由 LIO 负责。

LIO 的优势是短时间连续运动估计。
它可以高频输出机器人运动状态。

例如:

机器人从 odom 原点出发 向前走了 2 米 左转 45 度 再向前走 1 米

这部分运动是连续、平滑的,很适合给 Nav2 做局部控制和速度规划。


map → odom:回答“我的里程计整体偏到哪里去了?”

这一段由重定位模块负责。

为什么不直接让重定位发布:

map → base_footprint

因为这样会把全局校正和局部运动混在一起。

比较好的做法是:

map → odom

由重定位不断修正 odom 在 map 中的位置。

这样一来:

  • LIO 继续负责局部连续运动;

  • 重定位负责全局纠偏;

  • Nav2 看到的是一条完整、连续、可解释的 TF 链。

这就是:

map → odom → base_footprint

的核心意义。


五、千万别让两个节点同时发布map → odom

这是一个非常经典的坑。

有些人会同时启动:

  • AMCL;

  • SLAM Toolbox;

  • small_gicp 重定位;

  • KISS-Matcher 重定位;

  • 自己写的 map_to_odom 发布器。

然后系统里同时出现多个节点发布:

map → odom

这时候 TF 树就开始精神分裂。

一个节点说:

odom 在 map 左边 1 米

另一个节点说:

不对,odom 在 map 右边 3 米

第三个节点说:

你们都错了,我觉得它在楼上

最后 Nav2 看着这群人吵架,默默选择罢工。

所以原则非常简单:

同一时间,只能有一个模块负责发布map → odom

如果你用 small_gicp 重定位,就让 small_gicp 发布。
如果你用 KISS-Matcher + small_gicp,就让它发布。
如果你用 AMCL,就别再启动另一个 map_to_odom 发布器。

map → odom是全局定位权威。
权威可以换,但不能一群人同时当皇帝。


六、为什么我的项目要做 LIO 桥接?

Lidar_nav2_ws中,我希望实现一个目标:

上游 LIO 算法可以切换,下游 Nav2 接口保持不变。

也就是说,今天我用 FAST-LIO,明天我换 Point-LIO,Nav2 不应该感知到变化。

Nav2 不应该关心:

你上游是 FAST-LIO 你上游是 Point-LIO 你的原始话题叫 /Odometry 还是 /aft_mapped_to_init

Nav2 只应该看到标准接口:

/odom odom → base_footprint /registered_scan

所以中间必须有一层接口转换。

这层桥接模块主要做几件事:

  1. 接收 LIO 原始里程计;

  2. 把 LIO 内部坐标系转换到底盘坐标系;

  3. 发布标准的odom → base_footprint

  4. 发布标准/odom

  5. 输出后续重定位和点云切片需要用的/registered_scan

这样做之后,系统就变成了:

FAST-LIO / Point-LIO ↓ LIO Interface ↓ 标准 odom 与 TF ↓ 重定位 + Nav2

这就是工程系统里非常重要的一件事:

算法可以换,但接口要稳定。

否则你每换一个 LIO,Nav2 配置、TF、话题、重定位模块都要跟着改。
那不是机器人系统,那是大型连连看。


七、初学者最容易犯的 5 个 TF 错误

下面这些错误,我建议直接贴在桌面上。

错误 1:把 LIO 的原始坐标系直接当成odom

很多 LIO 输出的是算法内部坐标关系。
比如camera_init → body这类关系。

它不一定等价于:

odom → base_footprint

你需要确认:

  • 原点在哪里?

  • 旋转方向是否一致?

  • body 是 IMU,LiDAR,还是底盘?

  • 是否已经补偿了雷达到底盘的外参?

不要看到一个 odometry 就直接喂给 Nav2。
这就像看到一个轮子就说自己造了辆车。


错误 2:base_linkbase_footprintchassis混用

这三个 frame 很容易混。

一般可以这样理解:

base_footprint:机器人在地面上的投影中心 base_link:机器人本体坐标系 chassis:具体底盘结构坐标系

如果你的系统主要是平面导航,Nav2 更常用base_footprint

如果你把 Nav2 参数里写的是base_footprint,但 TF 里只有base_link,那 Nav2 就会很委屈:

你说你有底盘,但我找不到。

错误 3:雷达外参写错

雷达安装外参一般是:

chassis → livox_frame

如果这个静态 TF 写错,点云就会整体错位。

典型现象:

  • 障碍物位置不对;

  • 地图和现实不重合;

  • 机器人以为自己旁边有墙;

  • 实际前方有障碍,但 costmap 没反应;

  • RViz 里点云像被人拧了一下。

这时候不要急着骂点云算法。
先检查雷达安装 TF。


错误 4:重复发布map → odom

这个前面已经讲过。

一句话:

map → odom只能有一个权威发布者。

不要让 AMCL、SLAM Toolbox、small_gicp、KISS-Matcher 同时抢这个位置。

不然 TF 树会变成宫斗剧。


错误 5:TF 时间不同步

有些错误不是坐标错,而是时间错。

典型报错包括:

Lookup would require extrapolation into the future Lookup would require extrapolation into the past

常见原因:

  • 仿真时use_sim_time没统一;

  • 部分节点用系统时间;

  • 部分节点用仿真时间;

  • TF 发布频率太低;

  • 传感器消息时间戳异常。

如果是仿真,就统一用仿真时间。
如果是实机,就不要让某个节点还活在 Gazebo 的梦里。


八、如何检查自己的 TF 树?

调 TF 不要靠感觉。
感觉在机器人面前通常不太值钱。

建议直接用工具看。

1. 查看 TF 树

ros2 run tf2_tools view_frames

或者项目中封装脚本:

./show_tf_tree.sh

生成之后重点看:

map 是否连到 odom odom 是否连到 base_footprint base_footprint 是否连到 chassis chassis 是否连到 livox_frame

你要看到的是一棵树,不是一片森林。

如果是这样:

map → odom base_footprint → chassis → livox_frame

中间断了,那 Nav2 就找不到机器人。


2. 检查map → odom

ros2 run tf2_ros tf2_echo map odom

如果你启动了重定位模块,这个变换应该存在。

如果不存在,说明重定位没有正常发布全局校正。

如果疯狂跳变,说明重定位不稳定,或者有多个节点同时发布。


3. 检查odom → base_footprint

ros2 run tf2_ros tf2_echo odom base_footprint

推着机器人动,或者在仿真里遥控机器人走一走。

正常情况下,这个变换应该连续变化。

如果完全不变,说明 LIO 桥接没有正常工作。

如果变化方向很奇怪,例如机器人向前走,x 没动,y 在狂飙,那就要检查坐标轴定义。


4. 检查雷达外参

ros2 run tf2_ros tf2_echo chassis livox_frame

这个通常是静态的。

你要确认:

  • 平移是否对应实际安装位置;

  • 雷达朝向是否正确;

  • z 轴高度是否合理;

  • 没有把角度单位写错。

角度单位写错是经典事故。

你以为写的是 90 度,程序以为是 90 弧度。
机器人看完直接开始研究抽象艺术。


九、一个比较标准的启动逻辑

在我的项目里,导航系统大致可以这样理解:

第一步:启动 LIO

LIO 负责估计机器人短时间运动。

LiDAR + IMU → FAST-LIO / Point-LIO

这一步得到的是原始里程计。


第二步:启动 LIO 桥接

桥接模块负责把 LIO 内部位姿转成 Nav2 能用的标准关系:

odom → base_footprint

同时输出:

/odom /registered_scan

第三步:启动 3D 点云重定位

重定位模块根据先验 PCD 地图,对机器人进行全局定位。

它负责发布:

map → odom

这一步解决的问题是:

机器人开机以后,怎么知道自己在地图中的哪里?

第四步:点云转 LaserScan

Nav2 成熟的 2D 导航链路通常更习惯使用 LaserScan。

所以系统会把 3D 点云切片,生成 2D 激光数据:

PointCloud2 → LaserScan

这一步负责让 3D LiDAR 更好地接入 2D Nav2。


第五步:启动 Nav2

Nav2 看到的是:

map → odom → base_footprint

以及障碍物感知数据。

这时候它就可以正常进行:

  • 全局路径规划;

  • 局部避障;

  • 速度控制;

  • 到点导航。

至此,一套完整链路才真正闭合。


十、当 Nav2 不动时,先别急着调参数

很多人看到 Nav2 不动,第一反应是改参数。

max_vel_x: 改一下 acc_lim_x: 改一下 inflation_radius: 改一下 xy_goal_tolerance: 改一下

改着改着,参数文件像腌咸菜一样越来越入味,但机器人还是不走。

我的建议是:

Nav2 不动,先查 TF,再查 topic,最后再调参数。

优先级应该是:

1. TF 是否连通 2. frame_id 是否一致 3. 时间戳是否正常 4. /scan 或点云是否正常 5. /odom 是否正常 6. Nav2 参数是否合理

不要一上来就调 DWB。
DWB 很冤。
它只是个局部规划器,不是驱魔师。


十一、最小排查清单

如果你也在调 ROS 2 + 3D LiDAR + Nav2,我建议按这个顺序查:

1. TF 树是否完整?

ros2 run tf2_tools view_frames

必须有:

map → odom → base_footprint → chassis → livox_frame

2. 是否只有一个节点发布map → odom

检查是否同时启动了多个定位模块。

如果用了 small_gicp,就不要再让别的节点抢map → odom


3.odom → base_footprint是否连续变化?

ros2 run tf2_ros tf2_echo odom base_footprint

机器人运动时,这个值应该连续变化。


4. 雷达 frame 是否接到底盘?

ros2 run tf2_ros tf2_echo chassis livox_frame

雷达必须挂在机器人身上。
不要让雷达成为孤儿 frame。


5. Nav2 参数里的 base frame 是否和 TF 一致?

如果参数里写的是:

robot_base_frame: base_footprint

那 TF 里就必须真的有base_footprint

不要参数里写一个,TF 里发另一个。
Nav2 不擅长猜谜。


6. 时间是否统一?

仿真时确认所有节点:

use_sim_time: true

实机时确认不要误开仿真时间。


十二、这个设计的核心价值

这套 TF 设计真正解决的是系统解耦问题。

简单说:

LIO 管短期运动,重定位管全局纠偏,Nav2 管路径规划。
每个模块只干自己的活,不互相抢饭碗。

最终形成:

FAST-LIO / Point-LIO ↓ odom → base_footprint ↓ small_gicp / KISS-Matcher ↓ map → odom ↓ Nav2

这样设计之后,系统有几个好处:

1. LIO 可以切换

FAST-LIO 和 Point-LIO 可以根据场景切换。
但对 Nav2 来说,接口不变。


2. 重定位算法可以切换

可以用:

  • 纯 small_gicp;

  • KISS-Matcher + small_gicp;

  • ICP 初始化方案。

但它们最终都应该服务于同一个目标:

发布正确的 map → odom

3. Nav2 不需要理解上游算法细节

Nav2 不需要知道你用了什么 LIO。
它只需要看到标准 TF 和传感器数据。

这就是一个工程系统应该有的样子:

上游可以升级,下游不用陪葬。


十三、总结

ROS 2 导航系统里,TF 不是配角。

它不是“随便发几个静态变换就行”的东西。
它是 LIO、重定位、Nav2 之间的骨架。

如果 TF 树错了,再强的算法也只能在错误的坐标关系里努力表演。

对于 3D LiDAR + LIO + Nav2 这种系统,我建议初学者牢牢记住这句话:

LIO 负责odom → base_footprint,重定位负责map → odom,静态 TF 负责机器人本体结构。

也就是:

map → odom → base_footprint → chassis → livox_frame

当这棵树接通以后,Nav2 才真正知道:

我在哪 我怎么动 我的雷达在哪 我要怎么去目标点

所以,下次 Nav2 跑不起来,不要第一时间怀疑算法。

先看看 TF 树。

很多时候,不是机器人不聪明。
是你还没把它的“家庭关系”交代清楚。


项目地址

如果你也在做 ROS 2、3D LiDAR、FAST-LIO、Point-LIO、点云重定位和 Nav2 导航,可以参考我的开源项目:

GitHub - Ikunio/Lidar_nav2_ws: 基于 Livox MID-360 3D LiDAR 的 ROS 2 自主导航工作空间,集成 LIO 里程计、重定位、Nav2 导航,支持仿真与实机部署。 · GitHub

项目包含:

  • FAST-LIO / Point-LIO 接入;

  • LIO 到 Nav2 的 TF 桥接;

  • 3D 点云转 2D LaserScan;

  • small_gicp 重定位;

  • KISS-Matcher + small_gicp 全局重定位;

  • Gazebo 仿真与实机复用;

  • Nav2 导航完整流程。

如果这个项目对你有帮助,也欢迎点一个 Star。
毕竟开源项目最怕的不是 bug,而是没人看。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询