第4章 坐标变换与TF树

坐标变换,说白了就是让机器人知道“我在哪,传感器朝哪看”。你想想看,激光雷达扫出来的点是在雷达坐标系下,IMU的数据在imu_link,底盘里程计在odom——没有TF树,这些数据就是一堆孤岛。我做了这么多年SLAM,见过太多因为TF没配好导致定位漂移的案例。这一章,咱们就把TF2的底裤扒干净。

TF树 · 坐标变换核心逻辑 base_link laser_frame imu_link camera_optical 静态TF (固定) 静态TF 静态TF odom 动态TF (里程计) map SLAM 发布 传感器帧 里程计帧 地图帧

4.1 TF2 原理:别再搞混父子帧

TF2 本质上是一棵有向无环图,每个节点代表一个坐标系,边代表变换关系。我习惯把 base_link 当作树干,所有传感器都挂在它下面。记住一条铁律:父帧到子帧的变换,描述的是子帧在父帧下的位姿。举个例子,laser_frame 相对于 base_link 的平移和旋转,就是激光雷达在车体上的安装位置。

🔑 核心概念: TF2 使用“时间戳+帧名”来缓存变换,你随时可以查询任意两帧在历史时刻的关系。内部用插值处理不同频率的数据,这也是为什么它比老版 TF 更鲁棒。

我在项目里遇到过一个问题:两个传感器时间戳差了几毫秒,TF2 查不到变换直接抛异常。后来我养成了习惯——发布变换时务必用 ros::Time::now() 或者带上精确时间戳,别偷懒用 0。

4.2 静态坐标变换发布

静态变换,说白了就是“一辈子不变”的关系。比如激光雷达装在机器人前方 0.3 米,仰角 5 度。这种变换只需要发布一次,TF2 会帮你缓存起来。

我个人喜欢用 tf2_ros::StaticTransformBroadcaster,代码非常简洁:

#include <tf2_ros/static_transform_broadcaster.h>

geometry_msgs::TransformStamped t;
t.header.stamp = ros::Time::now();
t.header.frame_id = "base_link";
t.child_frame_id = "laser_frame";
t.transform.translation.x = 0.3;
t.transform.translation.y = 0.0;
t.transform.translation.z = 0.15;
t.transform.rotation = tf2::toMsg(tf2::Quaternion(0, 0, 0.0436, 0.999)); // 绕Z轴5°

static tf2_ros::StaticTransformBroadcaster br;
br.sendTransform(t);
💡 我的习惯: 把所有静态变换写在一个 launch 文件里,用 static_transform_publisher 节点统一管理。这样换传感器时改一个文件就行,不用到处翻代码。

4.3 动态坐标变换发布

动态变换,典型的就是里程计 odom → base_link。每一帧里程计数据来了,都要更新这个变换。我早期做差分轮小车时,直接用轮子编码器算位姿,然后发布 TF。

看一个发布动态 TF 的典型写法:

tf2_ros::TransformBroadcaster br;
geometry_msgs::TransformStamped transformStamped;

transformStamped.header.stamp = ros::Time::now();
transformStamped.header.frame_id = "odom";
transformStamped.child_frame_id = "base_link";
transformStamped.transform.translation.x = odom_x;
transformStamped.transform.translation.y = odom_y;
transformStamped.transform.translation.z = 0.0;
transformStamped.transform.rotation = tf2::toMsg(yaw_to_quat(yaw));

br.sendTransform(transformStamped);
⚠️ 我曾经踩过的坑: 动态变换的时间戳必须单调递增,而且不能跳变太大。有一次我里程计回调里忘了更新 stamp,导致 TF 树回退,定位直接崩了。记住:时间戳就是命根子。

4.4 多传感器坐标系统一与可视化

多传感器融合时,最头疼的就是坐标系统一。激光雷达、摄像头、IMU 各有各的安装位置和朝向。我的做法是:

  • 第一步: 用标定板或手动测量得到每个传感器相对于 base_link 的偏移量。
  • 第二步: 写成静态变换,启动时一次性发布。
  • 第三步: 在 Rviz 里添加 TF 显示,肉眼检查是否对齐。

你想想看,如果摄像头坐标系和激光雷达坐标系差了一个旋转,那融合出来的点云就是重影。我习惯在 Rviz 里同时显示 PointCloud2 和 Image,然后看投影是否吻合。

传感器推荐父帧变换类型备注
2D 激光雷达base_link静态通常安装在底盘前方
3D 激光雷达base_link静态注意俯仰角标定
IMUbase_link静态方向对齐很重要
深度相机base_link静态RGB 与 depth 之间也有 TF
里程计odom动态由轮速或视觉里程计发布
🔧 可视化调试技巧: 在 Rviz 中把 Fixed Frame 设为 base_link,然后勾选 TF 插件。如果看到所有传感器帧都整齐地挂在 base_link 下,且没有红色警告,说明坐标系统一基本没问题。

嗯,这里要注意:不要手动修改 TF 树的结构。我曾经为了省事,直接把 camera 帧挂到了 laser 帧下面,结果整个树乱了套,定位模块死活找不到变换。老老实实按树形结构来,父帧统一用 base_link 或 odom。