项目已开源到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_initNav2 只应该看到标准接口:
/odom odom → base_footprint /registered_scan所以中间必须有一层接口转换。
这层桥接模块主要做几件事:
接收 LIO 原始里程计;
把 LIO 内部坐标系转换到底盘坐标系;
发布标准的
odom → base_footprint;发布标准
/odom;输出后续重定位和点云切片需要用的
/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_link、base_footprint、chassis混用
这三个 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_frame2. 是否只有一个节点发布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 → odom3. 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,而是没人看。