第3章 TF广播器与监听器
这一章我们直接动手。写两个节点,一个广播坐标变换,一个监听并消费它。说白了,就是让机器人世界里「谁在哪儿」这件事能被说清楚、听明白。
我个人习惯把TF广播器想象成一个「大喇叭」——它不停地在喊:「我现在的位置是xxx,相对于yyy」。而监听器就是那个竖起耳朵听的家伙,它从一堆喊声中找到自己需要的那条信息。嗯,就这么简单。
3.1 编写第一个TF广播节点
先来个最基础的。假设我们有一个机器人,它的基座叫 base_link,上面装了个激光雷达叫 laser_frame。我们需要告诉系统:激光雷达相对于机器人基座的位置。
代码其实不长。我刚开始写的时候也以为有多复杂,结果核心就几行。
#include <ros/ros.h>
#include <tf/transform_broadcaster.h>
int main(int argc, char** argv) {
ros::init(argc, argv, "tf_broadcaster");
ros::NodeHandle nh;
tf::TransformBroadcaster broadcaster;
tf::Transform transform;
ros::Rate rate(10); // 10 Hz 广播
while (ros::ok()) {
// 设置平移: 激光雷达在基座前方0.3m, 上方0.2m
transform.setOrigin(tf::Vector3(0.3, 0.0, 0.2));
// 设置旋转: 绕Z轴转0度 (这里用四元数)
tf::Quaternion q;
q.setRPY(0, 0, 0);
transform.setRotation(q);
// 发布变换: parent -> child
broadcaster.sendTransform(
tf::StampedTransform(transform, ros::Time::now(),
"base_link", "laser_frame"));
rate.sleep();
}
return 0;
}
sendTransform 的频率设在 10~50 Hz。低于 5 Hz 的话,监听器那边容易报「 extrapolation 」错误,说白了就是数据太稀疏,插值插不出来了。
这里有个细节:parent 和 child 的顺序千万别搞反。我曾经在项目里把 base_link 和 laser_frame 写反了,结果整个导航系统都在抽风——机器人以为激光雷达长在脚底下。排查了半天,就这一行代码的问题。
3.2 编写TF监听节点
有广播就得有人听。监听器做的事情很简单:从 /tf 话题上拿到变换数据,然后问你「我想知道 laser_frame 相对于 base_link 现在在哪儿?」
#include <ros/ros.h>
#include <tf/transform_listener.h>
int main(int argc, char** argv) {
ros::init(argc, argv, "tf_listener");
ros::NodeHandle nh;
tf::TransformListener listener;
ros::Rate rate(10);
while (ros::ok()) {
tf::StampedTransform transform;
try {
// 等待变换可用,最多等3秒
listener.waitForTransform("base_link", "laser_frame",
ros::Time(0), ros::Duration(3.0));
listener.lookupTransform("base_link", "laser_frame",
ros::Time(0), transform);
ROS_INFO("激光雷达位置: (%.2f, %.2f, %.2f)",
transform.getOrigin().x(),
transform.getOrigin().y(),
transform.getOrigin().z());
} catch (tf::TransformException &ex) {
ROS_WARN("变换查询失败: %s", ex.what());
}
rate.sleep();
}
return 0;
}
lookupTransform 之前忘了调用 waitForTransform,结果程序一启动就崩溃。原因是监听器还没收到任何广播,缓存是空的。加上等待机制后,稳如老狗。
3.3 理解 tf::TransformBroadcaster 与 tf::TransformListener
这两个类,说白了就是 ROS 坐标变换的「嘴」和「耳朵」。咱们用表格对比一下,一目了然。
| 特性 | tf::TransformBroadcaster | tf::TransformListener |
|---|---|---|
| 角色 | 发布坐标变换 | 订阅并缓存变换 |
| 核心方法 | sendTransform() |
lookupTransform(), waitForTransform() |
| 数据流向 | 主动推送 | 被动接收 + 缓存查询 |
| 典型频率 | 10~100 Hz | 与广播频率匹配或更低 |
| 常见坑 | parent/child 颠倒 | 未等待变换直接查询 |
你想想看,广播器就像个只管喊话的喇叭,它不关心有没有人听。而监听器呢,它默默收数据,存到自己的缓存里,等你来问。这个缓存机制特别重要——它保存了过去几秒的所有变换,所以即使广播偶尔丢一帧,监听器也能用历史数据推算出来。
/tf 话题解耦。性能优化的第一步,就是搞清楚你的系统里谁在广播、谁在监听、频率是否匹配。
我记得有一次调试多机器人系统,三个机器人各自广播自己的 TF,结果监听器那边缓存爆炸了。后来一查,广播频率设成了 100 Hz,三个机器人就是 300 条/秒。缓存默认只保留 10 秒,算下来 3000 条变换,内存倒还好,但查询延迟上去了。嗯,这就是后面章节要聊的性能优化了。
最后说一句:写 TF 节点的时候,记得在 CMakeLists.txt 里加上 tf 依赖。我见过有人代码写对了,编译死活过不去,最后发现是忘了链接库。这种低级错误,咱们就别犯了。