2. TF广播器与监听器

这一章我们直接切入核心——TF广播器和TF监听器。说白了,整个TF系统就靠这俩兄弟撑起来。一个负责“说”,一个负责“听”。你想想看,机器人身上那么多坐标系,谁跟谁有关系,总得有人站出来宣布吧?广播器就是干这个的。

TF广播器 发布坐标系关系 TF监听器 查询/变换坐标 TF 树 /tf 话题 坐标变换图 静态广播 / 动态广播 坐标变换查询 (lookupTransform) 广播器 ↔ 监听器 协作

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 的变换。

注意: 查询时一定要处理异常。TF树可能暂时不完整,或者两个坐标系之间没有连通。我曾经在调试多机器人时,因为命名空间没写对,查了半小时才发现是frame_id拼写错误。
// 查询变换:从 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的左右手,搞懂了它们,后面的章节就会顺手很多。