3 · 静态坐标变换

静态坐标变换,说白了就是那些一辈子都不会动的坐标系关系。比如机器人底座和激光雷达之间,或者相机和惯性测量单元之间——装好了,角度位移就固定了。你想想看,如果每次都要重新计算这些固定关系,那得多浪费算力?

静态坐标变换 · 核心逻辑 静态TF广播器 发布固定坐标系 静态TF监听器 查询坐标变换 命令行工具 tf_echo / view_frames 一次发布,全局可用 · 频率极低 · 适合传感器外参

3.1 静态TF广播器编写

我个人习惯用 static_transform_publisher 这个工具,ROS 里已经封装好了。你不需要自己写循环,一条命令或者一个 launch 文件就能搞定。但如果你非要自己写 C++ 或 Python 节点,我也拦不住——不过我得提醒你,别把时间戳填错了。

💡 我的经验: 在项目里,我经常把静态广播写在 launch 文件里,而不是代码里。这样改外参时不用重新编译,直接改个数字就行。有一次现场调试,激光雷达装歪了,我蹲在地上改 launch 里的偏航角,三分钟搞定。

最标准的做法是用 tf2_ros::StaticTransformBroadcaster。看下面这个 C++ 片段:

#include <tf2_ros/static_transform_broadcaster.h>
#include <geometry_msgs/TransformStamped.h>

// 创建广播器
static tf2_ros::StaticTransformBroadcaster broadcaster;
geometry_msgs::TransformStamped static_transform;

static_transform.header.stamp = ros::Time::now();
static_transform.header.frame_id = "base_link";
static_transform.child_frame_id = "laser_frame";

static_transform.transform.translation.x = 0.1;
static_transform.transform.translation.y = 0.0;
static_transform.transform.translation.z = 0.2;

// 四元数:绕Z轴旋转 0.1 弧度
static_transform.transform.rotation.x = 0;
static_transform.transform.rotation.y = 0;
static_transform.transform.rotation.z = 0.04998;
static_transform.transform.rotation.w = 0.99875;

broadcaster.sendTransform(static_transform);

嗯,这里要注意:时间戳虽然填了 now(),但静态变换其实不依赖时间。你甚至可以填 0,不过为了兼容性,我建议还是用当前时间。

3.2 静态TF监听器编写

监听器就更有意思了。你想想看,机器人跑起来之后,其他节点怎么知道激光雷达在哪儿?靠的就是监听器。写一个 tf2_ros::Buffer 加 TransformListener,然后调用 lookupTransform。

#include <tf2_ros/buffer.h>
#include <tf2_ros/transform_listener.h>

tf2_ros::Buffer tf_buffer;
tf2_ros::TransformListener listener(tf_buffer);

// 等待变换可用
tf_buffer.canTransform("base_link", "laser_frame", ros::Time(0), ros::Duration(3.0));

geometry_msgs::TransformStamped t;
try {
    t = tf_buffer.lookupTransform("base_link", "laser_frame", ros::Time(0));
} catch (tf2::TransformException &ex) {
    ROS_WARN("变换查询失败: %s", ex.what());
}
🔧 避坑指南: 我曾经在 lookupTransform 里传了 ros::Time::now(),结果一直报错。后来才发现,静态变换的缓存有延迟,用 ros::Time(0) 才能拿到最新可用的变换。记住,静态变换用 Time(0) 最稳。

说白了,监听器就是问 TF 树:“嘿,base_link 和 laser_frame 之间什么关系?” 然后 TF 树把之前广播器发布的数据返回给你。整个过程非常轻量。

3.3 使用命令行工具查看TF

我调试机器人时,90% 的时间都在用命令行。图形工具固然好看,但命令行直接、快速。ROS 提供了几个非常趁手的工具。

命令作用常用参数
tf_echo打印两个坐标系间的变换tf_echo base_link laser_frame
view_frames生成 TF 树 PDF 图view_frames (生成 frames.pdf)
tf_monitor监控 TF 发布频率和延迟tf_monitor base_link

举个例子,你想看看激光雷达相对于机器人底座的位置:

$ rosrun tf tf_echo base_link laser_frame
At time 0.000
- Translation: [0.100, 0.000, 0.200]
- Rotation: in Quaternion [0.000, 0.000, 0.050, 0.999]

看到没?平移和旋转都出来了。我一般先用 view_frames 看一眼整个 TF 树的结构,确认所有坐标系都连上了,再用 tf_echo 检查具体数值。

⚠️ 注意: 如果你运行 tf_echo 后没有任何输出,八成是广播器没启动,或者 frame_id 拼写错了。我曾经因为大小写问题折腾了半小时——base_link 和 Base_Link 在 TF 里是两个完全不同的坐标系。

3.4 静态变换的典型应用场景

静态变换最常见的场景就是传感器外参标定。比如你装了一个双目相机,左右相机之间的变换是固定的;或者机械臂的基座和世界坐标系之间的变换。你想想看,这些关系一旦确定,在整个机器人生命周期里都不会变。

  • 激光雷达 ↔ 底盘: 装在车顶,位置固定。
  • IMU ↔ 基座: 通常刚性连接。
  • 相机 ↔ 机械臂末端: 手眼标定后就是静态的。

我个人习惯把所有静态变换都集中到一个 launch 文件里,用 node 标签启动 static_transform_publisher。这样管理起来特别清晰,别人接手项目也能一眼看懂。