第3章 TF广播器与监听器

这一章我们直接动手。写两个节点,一个广播坐标变换,一个监听并消费它。说白了,就是让机器人世界里「谁在哪儿」这件事能被说清楚、听明白。

我个人习惯把TF广播器想象成一个「大喇叭」——它不停地在喊:「我现在的位置是xxx,相对于yyy」。而监听器就是那个竖起耳朵听的家伙,它从一堆喊声中找到自己需要的那条信息。嗯,就这么简单。

TF 广播器 tf::TransformBroadcaster TF 监听器 tf::TransformListener 发布 /tf 话题 核心流程 广播器发布 TransformStamped → 监听器订阅 /tf → 缓存并查询

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 依赖。我见过有人代码写对了,编译死活过不去,最后发现是忘了链接库。这种低级错误,咱们就别犯了。