3 · 静态坐标变换
静态坐标变换,说白了就是那些一辈子都不会动的坐标系关系。比如机器人底座和激光雷达之间,或者相机和惯性测量单元之间——装好了,角度位移就固定了。你想想看,如果每次都要重新计算这些固定关系,那得多浪费算力?
3.1 静态TF广播器编写
我个人习惯用 static_transform_publisher 这个工具,ROS 里已经封装好了。你不需要自己写循环,一条命令或者一个 launch 文件就能搞定。但如果你非要自己写 C++ 或 Python 节点,我也拦不住——不过我得提醒你,别把时间戳填错了。
最标准的做法是用 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());
}
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。这样管理起来特别清晰,别人接手项目也能一眼看懂。