2. TF广播器与监听器
这一章我们直接切入核心——TF广播器和TF监听器。说白了,整个TF系统就靠这俩兄弟撑起来。一个负责“说”,一个负责“听”。你想想看,机器人身上那么多坐标系,谁跟谁有关系,总得有人站出来宣布吧?广播器就是干这个的。
2.1 创建TF广播器
先聊广播器。我个人的习惯是,只要机器人新增了一个传感器或者关节,第一件事就是给它配一个TF广播器。为什么?因为你不广播,别人就不知道这个坐标系在哪儿。
在ROS里,广播器通常通过 tf::TransformBroadcaster (C++) 或 tf.TransformBroadcaster (Python) 实现。你只需要创建一个对象,然后不断发布 transform 即可。
- 包含头文件
#include <tf/transform_broadcaster.h> - 实例化
tf::TransformBroadcaster br; - 填充
geometry_msgs::TransformStamped消息 - 调用
br.sendTransform(msg)
// C++ 示例:广播 base_link → laser 的变换
tf::TransformBroadcaster br;
geometry_msgs::TransformStamped t;
t.header.frame_id = "base_link";
t.child_frame_id = "laser";
t.transform.translation.x = 0.2;
t.transform.translation.y = 0.0;
t.transform.translation.z = 0.15;
t.transform.rotation = tf::createQuaternionFromYaw(0.0);
t.header.stamp = ros::Time::now();
br.sendTransform(t);
嗯,这里要注意:时间戳一定要填对。我曾经因为忘了更新 stamp,导致监听器那边一直拿到旧数据,机器人导航直接抽风。你想想看,一个过时的坐标变换,对运动控制来说就是灾难。
2.2 创建TF监听器
有广播就得有监听。监听器负责接收所有TF广播,并在本地维护一棵TF树。我一般会在需要坐标变换的节点里放一个 tf::TransformListener。
// C++ 示例:监听并查询 base_link → odom 变换
tf::TransformListener listener;
tf::StampedTransform transform;
try {
listener.waitForTransform("odom", "base_link", ros::Time(0), ros::Duration(1.0));
listener.lookupTransform("odom", "base_link", ros::Time(0), transform);
} catch (tf::TransformException &ex) {
ROS_ERROR("%s", ex.what());
}
这里我踩过一个大坑:waitForTransform 的超时时间设得太短,结果在机器人启动初期总是报异常。后来我改成 ros::Duration(3.0),给TF树一点构建时间,问题就解决了。
2.3 静态TF广播 vs 动态TF广播
很多新手分不清静态广播和动态广播。我简单解释一下:
| 类型 | 特点 | 典型场景 | 发布频率 |
|---|---|---|---|
| 静态TF广播 | 坐标系关系固定不变 | 激光雷达与底座、摄像头与基座 | 发布一次或极低频率 |
| 动态TF广播 | 坐标系关系随时间变化 | 关节角度、移动底盘位姿 | 10~100 Hz 持续发布 |
静态TF广播,说白了就是“焊死”的关系。比如你的激光雷达固定在机器人前方10厘米,这个变换永远不会变。用 tf2_ros::StaticTransformBroadcaster 最合适。我习惯在启动文件里用 static_transform_publisher 节点搞定,省得写代码。
# 命令行静态广播示例 (ROS1)
rosrun tf2_ros static_transform_publisher 0.2 0 0.15 0 0 0 base_link laser
动态广播则常用于轮式里程计。底盘每走一步,odom → base_link 的变换都在变。这时候就得用常规的 TransformBroadcaster 高频发布。
2.4 坐标变换查询
监听器拿到TF树之后,最常用的操作就是查询变换。我个人几乎每天都要跟 lookupTransform 打交道。比如你想知道“激光扫描的点在世界坐标系下的位置”,就得查 laser → map 的变换。
// 查询变换:从 base_link 到 map
geometry_msgs::PointStamped laser_point;
laser_point.header.frame_id = "base_link";
laser_point.point.x = 1.0;
laser_point.point.y = 0.5;
laser_point.point.z = 0.0;
try {
listener.transformPoint("map", laser_point, map_point);
ROS_INFO("Map坐标: (%.2f, %.2f, %.2f)",
map_point.point.x, map_point.point.y, map_point.point.z);
} catch (tf::TransformException &ex) {
ROS_WARN("变换失败: %s", ex.what());
}
你看,代码其实就那么几行。但实际项目中,坐标变换查询的成败往往取决于TF树是否完整。我建议你每次查询前,先用 waitForTransform 确保变换可用。
避坑总结:
- 广播器的时间戳必须更新,不要用旧时间戳。
- 监听器尽量复用,不要反复创建。
- 静态变换用专用工具,别用动态广播去刷。
- 查询变换前先等待,并捕获异常。
嗯,这一章的内容差不多就是这些。广播器和监听器是TF的左右手,搞懂了它们,后面的章节就会顺手很多。