3 · 坐标变换与TF树

做机器人开发,绕不开坐标系。你想想看,激光雷达扫描的点是在雷达自己的坐标系里,里程计数据在底盘坐标系,而导航目标点又是在地图坐标系。这些坐标系怎么对齐?靠的就是 TF 树。说白了,TF 就是机器人的“坐标系翻译官”。

3.1 为什么机器人需要那么多坐标系?

每个传感器都有自己固定的安装位置。比如我把激光雷达装在底盘前方 10 厘米、高 20 厘米的地方。那它测到的障碍物距离,就必须先转换到底盘坐标系,才能用于导航。我在项目里见过有人直接把雷达数据当全局坐标用,结果机器人原地转圈……嗯,这就是没搞懂坐标变换的后果。

核心概念: 每个连杆(link)都有一个自己的坐标系。URDF 里通过 <origin> 标签定义子坐标系相对于父坐标系的偏移和旋转。TF 树就是这些父子关系的“族谱”。

3.2 TF 树:机器人的坐标系家谱

TF 树是一个树形结构,根节点通常是 base_link 或 odom。每个节点代表一个坐标系,边代表变换关系(平移 + 旋转)。

举个例子:

  • base_link → laser_frame:激光雷达相对底盘的位姿
  • base_link → camera_frame:相机相对底盘的位姿
  • odom → base_link:里程计估计的底盘位姿

我习惯在调试时用 tf2_tools 里的 view_frames 工具,直接生成 PDF 图,一眼就能看出哪个坐标系断开了。

map odom base_link laser_frame camera_frame left_wheel tf (map→odom) odom→base_link x:0.1, y:0, z:0.2 x:0.05, z:0.15 x:0, y:0.2

▲ 典型的机器人 TF 树结构:map → odom → base_link → 传感器/轮子

3.3 在 URDF 中定义父子坐标系

URDF 里用 <joint> 来连接两个 link,并指定它们的相对位姿。来看一个实际例子:

<robot name="my_robot">
    <link name="base_link">
        <visual>
            <geometry><box size="0.4 0.3 0.1"/></geometry>
        </visual>
    </link>

    <link name="laser_frame">
        <visual>
            <geometry><cylinder radius="0.05" length="0.05"/></geometry>
        </visual>
    </link>

    <joint name="laser_joint" type="fixed">
        <parent link="base_link"/>
        <child link="laser_frame"/>
        <origin xyz="0.1 0 0.2" rpy="0 0 0"/>
    </joint>
</robot>

这里 laser_frame 是子坐标系,base_link 是父坐标系。origin xyz="0.1 0 0.2" 表示激光雷达安装在底盘前方 0.1 米,上方 0.2 米。rpy 是旋转角,这里没旋转。

💡 我的习惯: 所有传感器都单独用一个 link 表示,哪怕它只是一个虚拟坐标系。这样 TF 树更清晰,后期换传感器位置只需要改 joint 的 origin,不用动其他代码。

3.4 静态 TF 与动态 TF

URDF 里定义的 joint 都是静态变换——机器人造好了,激光雷达和底盘的相对位置就不变了。但像 odom→base_link 这种是动态变换,随着机器人移动不断更新,通常由里程计或定位模块发布。

变换类型发布方式示例
静态 TFURDF 或 static_transform_publisherbase_link → laser_frame
动态 TF里程计 / 定位节点实时发布odom → base_link
动态 TFmap → odom(AMCL 或 SLAM)map → odom

⚠️ 我曾经踩过的坑: 有一次我忘了给轮子单独建坐标系,直接把轮子视觉放在 base_link 下。结果做里程计时,轮子速度没法正确转换到 base_link,导航直接崩了。记住:每个运动关节都要有独立的坐标系。

3.5 检查 TF 树的实用命令

调车的时候,我几乎每五分钟就要用一次这些命令:

  • ros2 run tf2_tools view_frames —— 生成 TF 树 PDF
  • ros2 run tf2_ros tf2_echo base_link laser_frame —— 查看两个坐标系间的实时变换
  • ros2 run tf2_ros tf2_monitor —— 监控所有 TF 的发布频率和延迟

你想想看,如果 tf2_echo 输出一直不变,那肯定是静态 TF 没发布,或者 URDF 没加载成功。

核心总结: TF 树是机器人的坐标系骨架。URDF 定义静态父子关系,动态变换由导航或里程计节点填充。调试时先看 view_frames,确认树是完整的,再查具体变换值。