3 · 静态TF发布器

用 static_transform_publisher 发布固定坐标变换,比如激光雷达和底盘中心的关系。这玩意儿是导航里最基础的 TF 节点之一,但很多人一开始会忽略它的细节。

static_transform_publisher 激光雷达 → 底盘 IMU → 基座 固定偏移 + 固定旋转 发布一次,TF树永久有效

为什么需要静态TF?

说白了,机器人身上很多传感器和底盘之间的位置是固定的。激光雷达拧在支架上,IMU 焊在主板上,摄像头锁死在结构件上——这些变换不会随时间改变。你想想看,如果每次启动都要重新计算一遍,那多浪费算力。

我个人习惯把静态变换单独拎出来,用专门的发布器管理。这样 TF 树干净,排查问题也快。

核心用法:命令行一把梭

ROS 自带的 tf2_ros 包提供了 static_transform_publisher 节点。我最常用的就是命令行启动,一行搞定:

rosrun tf2_ros static_transform_publisher x y z yaw pitch roll frame_id child_frame_id

举个例子,激光雷达装在底盘前方 0.3 米,高度 0.15 米,完全水平:

rosrun tf2_ros static_transform_publisher 0.3 0 0.15 0 0 0 base_link laser

注意顺序:平移在前,旋转在后。旋转用的是 RPY(绕固定轴),单位是弧度。我见过有人把角度填成度数,结果雷达数据直接飞到天上去……

💡 我的习惯: 把所有静态变换写到一个 launch 文件里,用 node 标签启动。这样团队里其他人一看就知道传感器是怎么装的。

launch 文件里的标准姿势

在实际项目中,我更推荐用 launch 文件来管理。下面这个例子是我在巡检机器人上用的:

<launch>
    <node pkg="tf2_ros" type="static_transform_publisher" name="lidar_to_base" 
          args="0.28 0.0 0.18 0.0 0.0 0.0 base_link laser" />
    <node pkg="tf2_ros" type="static_transform_publisher" name="imu_to_base"
          args="0.0 0.0 0.05 0.0 0.0 0.0 base_link imu_link" />
</launch>

每个 node 的 name 要唯一,不然 ROS 会默默覆盖。嗯,这里要注意:如果你用相同的 name 启动两次,后面的会踢掉前面的,而且不会有任何报错。

用 Python 或 C++ 发布静态TF

有时候你需要更灵活的控制,比如从参数服务器读取偏移量。我一般用 Python 写个简单节点:

import rospy
import tf2_ros
import geometry_msgs.msg

if __name__ == '__main__':
    rospy.init_node('my_static_tf_broadcaster')
    broadcaster = tf2_ros.StaticTransformBroadcaster()
    t = geometry_msgs.msg.TransformStamped()

    t.header.frame_id = "base_link"
    t.child_frame_id  = "laser"
    t.transform.translation.x = 0.3
    t.transform.translation.y = 0.0
    t.transform.translation.z = 0.15
    t.transform.rotation.w = 1.0   # 无旋转

    broadcaster.sendTransform(t)
    rospy.spin()

这段代码发布一次就挂起等待。实际上 StaticTransformBroadcaster 内部会按一定间隔重发,确保后来的监听器也能收到。

🔧 避坑指南: 我曾经在调试时发现 TF 树里 laser 的父坐标系变成了 map……查了半天,原来是 static_transform_publisher 的 child_frame_id 写成了 map。记住:child_frame_id 是传感器本身,不是目标坐标系。

静态TF vs 动态TF

很多新手会问:为什么不用普通的 TF 发布器?我简单对比一下:

特性static_transform_publisher普通 TF 发布器
发布频率自动定期重发(约 100s)按需发布,通常 10-100 Hz
资源占用极低,几乎不占 CPU每次发布都要序列化
适用场景传感器固定安装、车轮位置关节运动、SLAM 位姿
TF 树表现静态边,不会跳动动态边,随时间变化

说白了,如果你的变换是固定的,就别用动态发布器。我见过有人用 tf2_ros::TransformBroadcaster 在循环里发同样的变换,白白浪费带宽。

验证你的静态TF

发布之后怎么确认对不对?我最常用的三板斧:

  • tf_echo base_link laser —— 看平移旋转数值是否和预期一致。
  • view_frames —— 生成 PDF 图,检查树结构是否正确。
  • 在 Rviz 里添加 TF 显示,看坐标系箭头是否在正确的位置。

有一次我同事说雷达数据总是偏左,我让他跑了一下 tf_echo,发现 y 偏移填了 0.1 而不是 0.0。这种低级错误,用工具一眼就能揪出来。

⚠️ 重要提醒: 静态 TF 发布器默认使用 latest common time,如果你在仿真中使用了 --clock,记得确保时间同步。否则 TF 可能会因为时间戳混乱而被忽略。

总结一下

静态 TF 发布器是导航调试的基石。你只要记住三点:

  1. 用 static_transform_publisher 处理所有固定传感器。
  2. launch 文件里把 name 写清楚,方便以后维护。
  3. 发布完一定用 tf_echo 或 Rviz 验证。

我自己的习惯是,每装一个新传感器,第一件事就是写一个静态 TF 并验证。这一步做扎实了,后面导航调参才能少踩坑。