3、坐标系与TF变换:机器人坐标系定义(base_link、odom、map)、TF树的理解与发布、静态与动态TF变换
坐标系这东西,说白了就是给机器人一个「空间定位」的基准。没有坐标系,你让机器人往左走,它都不知道往哪边才是左。我刚开始做机器人那会儿,觉得坐标系就是个数学概念,随便搞搞就行。结果有一次,机器人明明看到障碍物在左边,却一头撞上去了——后来才发现,是激光雷达的坐标系和底盘的坐标系没对齐。
嗯,从那以后,我对坐标系和TF变换就再也不敢马虎了。
3.1 机器人坐标系定义
在ROS2的导航体系中,有三个核心坐标系你必须搞明白:base_link、odom、map。它们各自承担不同的角色,共同构成了机器人的空间认知框架。
3.1.1 base_link:机器人的身体坐标系
base_link 是附着在机器人本体上的坐标系。你可以把它想象成机器人的「肚脐眼」——通常位于机器人底盘的几何中心,X轴指向正前方,Y轴指向左侧,Z轴指向上方。
我个人习惯把 base_link 作为所有传感器坐标系的父节点。激光雷达、IMU、轮式里程计,它们的坐标系最终都要通过TF变换关联到 base_link 上。
关键点:base_link 是机器人自身的参考系,它随着机器人移动而移动。所有与机器人本体相关的数据(如速度、加速度、激光扫描)都在这个坐标系下表达。
3.1.2 odom:里程计坐标系
odom 坐标系是机器人通过里程计(轮式编码器、IMU等)推算出来的位置参考系。它的特点是:连续、平滑、无跳变。
你想想看,轮子转了多少圈,IMU积分了多少角度,这些数据累积起来,就形成了 odom 坐标系下的位姿估计。但问题是,里程计有累积误差——走久了,位置就会漂移。
我在项目中遇到过这样的情况:机器人沿着走廊走了50米,里程计显示走了50.3米,误差0.3米。这在短距离内可以接受,但长距离导航就不行了。
3.1.3 map:全局地图坐标系
map 坐标系是全局固定的参考系,通常与建图时的原点对齐。它不随时间漂移,是导航决策的最终依据。
说白了,map 坐标系就是「真实世界」的坐标。激光SLAM算法会不断修正 map 到 odom 的变换,消除里程计的累积误差。
| 坐标系 | 特点 | 漂移 | 用途 |
|---|---|---|---|
| base_link | 随机器人移动 | 无(本体参考) | 传感器数据、控制指令 |
| odom | 连续平滑 | 有(累积误差) | 局部规划、里程计融合 |
| map | 全局固定 | 无(参考) | 全局路径规划、定位 |
3.2 TF树的理解与发布
TF(Transform)是ROS2中管理坐标系变换的核心机制。它本质上是一棵树,每个节点代表一个坐标系,每条边代表两个坐标系之间的变换关系。
为什么要用树结构?因为坐标系之间的关系是层级化的。比如:
map→odom:由SLAM或定位模块发布odom→base_link:由里程计发布base_link→laser_frame:由机器人URDF模型定义
TF树必须是一棵树,不能有环。如果出现环,TF系统会报错,机器人就不知道该怎么变换了。
注意:TF树中每个坐标系只能有一个父节点,但可以有多个子节点。这是ROS2的硬性约束。
3.2.1 发布TF变换
在ROS2中,发布TF变换有两种方式:
- 使用
tf2_ros::TransformBroadcaster:手动发布变换 - 使用
robot_state_publisher:通过URDF模型自动发布
下面是一个手动发布 odom 到 base_link 变换的示例:
// C++ 示例:发布 odom → base_link 变换
#include <tf2_ros/transform_broadcaster.h>
#include <geometry_msgs/msg/transform_stamped.h>
tf2_ros::TransformBroadcaster broadcaster(this);
geometry_msgs::msg::TransformStamped transform;
transform.header.stamp = this->get_clock()->now();
transform.header.frame_id = "odom";
transform.child_frame_id = "base_link";
// 设置平移(x, y, z)
transform.transform.translation.x = 1.0;
transform.transform.translation.y = 0.5;
transform.transform.translation.z = 0.0;
// 设置旋转(四元数)
transform.transform.rotation.x = 0.0;
transform.transform.rotation.y = 0.0;
transform.transform.rotation.z = 0.0;
transform.transform.rotation.w = 1.0;
broadcaster.sendTransform(transform);
这段代码看起来简单,但有个坑:时间戳。如果你发布的时间戳比当前时间早太多,TF系统会认为这个变换已经过期,直接丢弃。我曾经因为这个原因,调试了一整个下午才发现问题。
3.3 静态与动态TF变换
TF变换按更新频率分为两类:静态变换和动态变换。
3.3.1 静态TF变换
静态变换是指两个坐标系之间的相对关系固定不变。比如激光雷达安装在机器人底盘上,它们之间的位置关系是固定的。
在ROS2中,静态变换可以通过 tf2_ros::StaticTransformBroadcaster 发布,或者直接在启动文件中声明:
# 启动文件中声明静态变换
node:
package: tf2_ros
executable: static_transform_publisher
arguments:
- "--x"
- "0.2"
- "--y"
- "0.0"
- "--z"
- "0.1"
- "--yaw"
- "0.0"
- "--pitch"
- "0.0"
- "--roll"
- "0.0"
- "--frame-id"
- "base_link"
- "--child-frame-id"
- "laser_frame"
静态变换只需要发布一次,TF系统会自动缓存。我个人习惯把所有传感器到 base_link 的变换都做成静态的,省心。
3.3.2 动态TF变换
动态变换是随时间变化的。最常见的例子就是 odom 到 base_link 的变换——机器人每走一步,这个变换都在变。
动态变换的发布频率通常与里程计更新频率一致,一般是50Hz到100Hz。频率太低,导航算法会跟不上;频率太高,CPU吃不消。
经验之谈:动态变换的发布频率建议与里程计数据频率一致。如果里程计是50Hz,TF就50Hz。不要为了省CPU而降低频率,否则导航效果会大打折扣。
3.4 TF树结构图
下面这张图展示了典型的机器人TF树结构。你可以看到,map 在最顶层,下面是 odom,再下面是 base_link,然后分支出各个传感器。
3.5 避坑指南
坐标系和TF变换是导航系统中最容易出问题的地方。我总结几个常见坑:
- TF树不完整:缺少某个坐标系变换,导致导航节点找不到路径。用
tf2_tools view_frames.py查看TF树,一目了然。 - 时间戳不一致:传感器数据的时间戳和TF变换的时间戳对不上,导致数据被丢弃。确保所有节点使用同一个时钟源。
- 坐标系命名混乱:有人把激光雷达坐标系叫
laser,有人叫lidar,还有人叫scan_frame。统一命名规范,避免混淆。
曾经踩过的坑:有一次我在调试导航时,机器人原地打转,就是不往前走。查了半天,发现是 base_link 和 odom 的变换方向反了——里程计说往东走,TF却说往西走。嗯,从那以后,我每次发布TF变换都会先用手动模式验证一下方向。
3.6 本章小结
坐标系和TF变换是机器人导航的基石。你需要记住三点:
- 三个核心坐标系:
map(全局固定)、odom(连续平滑)、base_link(随机器人移动) - TF树是层级结构:每个坐标系只有一个父节点,不能有环
- 静态变换 vs 动态变换:固定关系用静态,变化关系用动态
把这些搞明白了,后面的导航实战才能顺利推进。