第2章 TF树与坐标系

这一章我们聊聊TF树和坐标系。说白了,TF就是机器人世界里所有坐标系的“族谱”。你想想看,一个机器人身上有激光雷达、摄像头、轮子、机械臂……每个部件都有自己的坐标系。怎么让它们知道彼此在哪?靠的就是TF树。

2.1 坐标系的定义

坐标系就是一个三维参考框架,包含原点、X/Y/Z轴。在ROS里,每个坐标系都有一个唯一的名字,比如 base_link、laser_frame、camera_optical_frame。我个人习惯把坐标系命名得清晰一点,别用模糊的缩写,不然调试的时候自己都懵。

每个坐标系都对应一个刚体(或者一个虚拟参考点)。定义坐标系时,你需要明确它的父级是谁。没有父级的坐标系?那就是世界坐标系 map 或 odom,它们是TF树的根。

核心原则: 每个坐标系只能有一个父坐标系,但可以有多个子坐标系。这就是TF树能形成树形结构的原因。

2.2 父子坐标系关系

父子关系说白了就是“谁相对于谁”。比如 base_link 是父,laser_frame 是子。那么激光雷达的位置就是相对于机器人基座定义的。这种关系用平移 + 旋转(即齐次变换矩阵)来描述。

我在项目中遇到过一个问题:有人把 camera_link 直接挂在 map 下,而不是挂在 base_link 下。结果机器人一移动,摄像头坐标还钉在原地……嗯,这就是父子关系搞错了。记住:随机器人移动的部件,一定要挂在 base_link 或它的子节点下。

父坐标系子坐标系典型关系
mapodom世界原点 → 里程计漂移修正
odombase_link里程计 → 机器人本体
base_linklaser_frame机器人中心 → 激光雷达
base_linkcamera_link机器人中心 → 摄像头

2.3 TF树的构建与维护

TF树不是写死的,它随着时间变化。构建TF树,其实就是发布坐标系之间的变换。你可以用静态变换(比如激光雷达固定在车头),也可以用动态变换(比如机械臂关节角度变化)。

我早期做项目时,TF树经常断掉——就是某个变换没发布,导致下游节点拿不到坐标。后来我养成一个习惯:先画TF树草图,再写代码。你想想看,连树都没想清楚,代码怎么可能对?

map odom base_link laser_frame camera_link left_wheel right_wheel arm_base gripper 静态变换 动态/关节变换

▲ 典型的机器人TF树结构:map → odom → base_link,再挂载传感器与执行器

2.4 维护TF树的避坑指南

我曾经踩过的坑: 有一次我同时发布了两个不同父级的 base_link 变换,结果TF树直接报“不连续”错误。记住:一个坐标系只能有一个父级,如果你发现TF树分叉了,那一定是哪里搞错了。
  • 检查频率: 用 tf2_monitor 看发布频率,低于10Hz就要警惕。
  • 时间戳同步: 所有变换的时间戳最好用共同的时钟源,不然容易出现“未来变换”。
  • 命名规范: 我习惯用 _frame 后缀,比如 left_wheel_frame,一目了然。
小技巧: 在启动文件里用 static_transform_publisher 发布固定变换,比如激光雷达相对于 base_link 的位置。这样就不用手动写发布节点了。

2.5 用代码构建TF树

下面是一个简单的Python示例,发布 base_link 到 laser_frame 的静态变换。我个人喜欢用 tf2_ros.StaticTransformBroadcaster,省心。

import tf2_ros
from geometry_msgs.msg import TransformStamped

broadcaster = tf2_ros.StaticTransformBroadcaster()
t = TransformStamped()
t.header.stamp = rospy.Time.now()
t.header.frame_id = "base_link"
t.child_frame_id = "laser_frame"
t.transform.translation.x = 0.2
t.transform.translation.y = 0.0
t.transform.translation.z = 0.15
# 旋转:四元数 (x,y,z,w)
t.transform.rotation.x = 0.0
t.transform.rotation.y = 0.0
t.transform.rotation.z = 0.0
t.transform.rotation.w = 1.0
broadcaster.sendTransform(t)

你看,就这么几行。但要注意:静态变换只发布一次,动态变换需要循环发布。嗯,这里要提醒一下:动态变换一定要用 TransformBroadcaster,并且放在定时器里。

总结一下: TF树就是坐标系的家族关系。定义好父子关系,用正确的发布方式,你的机器人就能“知道”每个部件在哪。我见过太多因为TF树混乱导致的“鬼影”问题——明明传感器有数据,但就是定位不准。八成是TF树没搭对。