ROS1从入门到精通 9: TF坐标变换(机器人的空间认知)

【ROS1从入门到精通】第9篇:TF坐标变换(机器人的空间认知)

🎯 本文目标:深入理解ROS TF(Transform)系统,掌握坐标变换的原理和实现,学会管理机器人的坐标系树,能够处理复杂的空间变换问题,实现机器人的精确定位和导航。

📑 目录

  1. TF系统概述
  2. 坐标系基础理论
  3. 静态坐标变换
  4. 动态坐标变换
  5. TF树管理
  6. TF监听和查询
  7. TF工具使用
  8. TF2高级特性
  9. 实战案例:机器人坐标系统
  10. 总结与最佳实践

1. TF系统概述

1.1 什么是TF?

TF(Transform)是ROS中管理坐标系变换关系的系统。它维护了一个随时间变化的坐标系树,使得我们能够跟踪多个坐标系之间的关系,并在任意两个坐标系之间进行坐标变换。
TF坐标系树 map - 地图坐标系 odom - 里程计坐标系 base_link - 机器人基座 laser_link - 激光雷达 camera_link - 相机 imu_link - IMU wheel_left_link - 左轮 wheel_right_link - 右轮 arm_base_link - 机械臂基座 arm_link_1 arm_link_2 gripper_link - 夹爪

1.2 TF系统特点

特性 描述
分布式 变换可以由不同节点发布
时间缓冲 保存历史变换信息
插值 自动插值计算中间时刻的变换
树结构 确保无环的层次结构
双向查询 可以查询任意两个坐标系间的变换
可视化 支持RViz等工具的可视化

1.3 TF vs TF2

TF2是TF的改进版本,主要区别:

特性 TF TF2
API设计 较旧,有些混乱 更清晰、模块化
性能 一般 更高效
多机器人 支持有限 完整支持
静态变换 需要节点持续发布 独立的static_transform
缓冲区 全局缓冲 每个监听器独立缓冲
Python支持 完整 完整且更高效

2. 坐标系基础理论

2.1 坐标变换数学基础

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np
import tf
import tf2_ros
from geometry_msgs.msg import Quaternion, Vector3, Transform

class TransformMath:
    """坐标变换数学基础"""

    def __init__(self):
        self.demonstrate_transform_math()

    def demonstrate_transform_math(self):
        """演示坐标变换数学"""

        # 1. 欧拉角与四元数转换
        self.euler_quaternion_conversion()

        # 2. 旋转矩阵
        self.rotation_matrix_demo()

        # 3. 齐次变换矩阵
        self.homogeneous_transform()

        # 4. 坐标变换链
        self.transform_chain()

    def euler_quaternion_conversion(self):
        """欧拉角与四元数转换"""
        print("=== Euler to Quaternion Conversion ===")

        # 欧拉角 (roll, pitch, yaw) - 弧度
        roll = 0.1   # 绕X轴
        pitch = 0.2  # 绕Y轴
        yaw = 0.3    # 绕Z轴

        # 转换为四元数
        quaternion = tf.transformations.quaternion_from_euler(roll, pitch, yaw)
        print(f"Euler angles: roll={roll:.2f}, pitch={pitch:.2f}, yaw={yaw:.2f}")
        print(f"Quaternion: x={quaternion[0]:.3f}, y={quaternion[1]:.3f}, "
              f"z={quaternion[2]:.3f}, w={quaternion[3]:.3f}")

        # 四元数转回欧拉角
        euler = tf.transformations.euler_from_quaternion(quaternion)
        print(f"Back to Euler: roll={euler[0]:.2f}, pitch={euler[1]:.2f}, "
              f"yaw={euler[2]:.2f}")

        # 四元数归一化(重要!)
        norm = np.linalg.norm(quaternion)
        quaternion_normalized = quaternion / norm
        print(f"Normalized quaternion norm: {np.linalg.norm(quaternion_normalized):.6f}")

    def rotation_matrix_demo(self):
        """旋转矩阵演示"""
        print("\n=== Rotation Matrix ===")

        # 从欧拉角创建旋转矩阵
        roll, pitch, yaw = 0.1, 0.2, 0.3
        R = tf.transformations.euler_matrix(roll, pitch, yaw)
        print(f"Rotation matrix from Euler angles:")
        print(R[:3, :3])

        # 从旋转矩阵提取欧拉角
        euler = tf.transformations.euler_from_matrix(R)
        print(f"Extracted Euler: {euler}")

        # 绕轴旋转
        axis = [0, 0, 1]  # Z轴
        angle = np.pi / 4  # 45度
        R_axis = tf.transformations.rotation_matrix(angle, axis)
        print(f"Rotation matrix around Z-axis by 45°:")
        print(R_axis[:3, :3])

    def homogeneous_transform(self):
        """齐次变换矩阵"""
        print("\n=== Homogeneous Transform ===")

        # 创建变换矩阵
        # 平移
        translation = [1.0, 2.0, 3.0]

        # 旋转(四元数)
        quaternion = tf.transformations.quaternion_from_euler(0, 0, np.pi/4)

        # 组合成齐次变换矩阵
        T = tf.transformations.quaternion_matrix(quaternion)
        T[0:3, 3] = translation

        print(f"Homogeneous transform matrix:")
        print(T)

        # 应用变换
        point = [1, 0, 0, 1]  # 齐次坐标
        transformed_point = T @ point
        print(f"Original point: {point[:3]}")
        print(f"Transformed point: {transformed_point[:3]}")

        # 逆变换
        T_inv = tf.transformations.inverse_matrix(T)
        point_back = T_inv @ transformed_point
        print(f"Inverse transformed: {point_back[:3]}")

    def transform_chain(self):
        """坐标变换链"""
        print("\n=== Transform Chain ===")

        # A到B的变换
        T_AB = tf.transformations.euler_matrix(0, 0, np.pi/4)
        T_AB[0:3, 3] = [1, 0, 0]

        # B到C的变换
        T_BC = tf.transformations.euler_matrix(0, 0, np.pi/4)
        T_BC[0:3, 3] = [0, 1, 0]

        # A到C的变换 = A到B * B到C
        T_AC = T_AB @ T_BC

        print("Transform A->B->C:")
        print(f"Translation: {T_AC[0:3, 3]}")

        # 从变换矩阵提取平移和旋转
        translation = T_AC[0:3, 3]
        quaternion = tf.transformations.quaternion_from_matrix(T_AC)
        euler = tf.transformations.euler_from_matrix(T_AC)

        print(f"Final translation: {translation}")
        print(f"Final rotation (euler): {euler}")

2.2 坐标系约定

python 复制代码
class CoordinateConventions:
    """ROS坐标系约定"""

    def __init__(self):
        """
        ROS使用右手坐标系:
        - X轴:向前(红色)
        - Y轴:向左(绿色)
        - Z轴:向上(蓝色)

        旋转约定:
        - Roll:绕X轴旋转
        - Pitch:绕Y轴旋转
        - Yaw:绕Z轴旋转
        """
        self.standard_frames = {
            'map': '全局固定坐标系,通常是建图的起点',
            'odom': '里程计坐标系,连续但会漂移',
            'base_link': '机器人本体坐标系',
            'base_footprint': '机器人在地面的投影',
            'laser_link': '激光雷达坐标系',
            'camera_link': '相机坐标系',
            'imu_link': 'IMU坐标系',
            'gps_link': 'GPS坐标系'
        }

    def print_conventions(self):
        """打印坐标系约定"""
        print("=== ROS Coordinate Conventions ===")
        print("Right-handed coordinate system:")
        print("  X-axis: Forward (Red)")
        print("  Y-axis: Left (Green)")
        print("  Z-axis: Up (Blue)")
        print("\nStandard frames:")
        for frame, description in self.standard_frames.items():
            print(f"  {frame}: {description}")

3. 静态坐标变换

3.1 静态变换发布器

cpp 复制代码
// static_transform_publisher.cpp
#include <ros/ros.h>
#include <tf2_ros/static_transform_broadcaster.h>
#include <tf2/LinearMath/Quaternion.h>
#include <geometry_msgs/TransformStamped.h>

class StaticTransformPublisher {
public:
    StaticTransformPublisher() {
        // 发布静态变换
        publishStaticTransforms();
    }

    void publishStaticTransforms() {
        // 创建静态变换广播器
        static tf2_ros::StaticTransformBroadcaster static_broadcaster;

        // 1. base_link到laser_link的变换
        geometry_msgs::TransformStamped laser_transform;
        laser_transform.header.stamp = ros::Time::now();
        laser_transform.header.frame_id = "base_link";
        laser_transform.child_frame_id = "laser_link";

        // 设置平移(激光雷达在机器人前方0.2m,高0.1m)
        laser_transform.transform.translation.x = 0.2;
        laser_transform.transform.translation.y = 0.0;
        laser_transform.transform.translation.z = 0.1;

        // 设置旋转(无旋转)
        tf2::Quaternion q;
        q.setRPY(0, 0, 0);
        laser_transform.transform.rotation.x = q.x();
        laser_transform.transform.rotation.y = q.y();
        laser_transform.transform.rotation.z = q.z();
        laser_transform.transform.rotation.w = q.w();

        // 2. base_link到camera_link的变换
        geometry_msgs::TransformStamped camera_transform;
        camera_transform.header.stamp = ros::Time::now();
        camera_transform.header.frame_id = "base_link";
        camera_transform.child_frame_id = "camera_link";

        // 相机在机器人前方0.15m,高0.3m,向下倾斜15度
        camera_transform.transform.translation.x = 0.15;
        camera_transform.transform.translation.y = 0.0;
        camera_transform.transform.translation.z = 0.3;

        q.setRPY(0, -0.2618, 0);  // pitch = -15度
        camera_transform.transform.rotation.x = q.x();
        camera_transform.transform.rotation.y = q.y();
        camera_transform.transform.rotation.z = q.z();
        camera_transform.transform.rotation.w = q.w();

        // 3. base_link到imu_link的变换
        geometry_msgs::TransformStamped imu_transform;
        imu_transform.header.stamp = ros::Time::now();
        imu_transform.header.frame_id = "base_link";
        imu_transform.child_frame_id = "imu_link";

        imu_transform.transform.translation.x = 0.0;
        imu_transform.transform.translation.y = 0.0;
        imu_transform.transform.translation.z = 0.05;

        // IMU可能有安装角度偏差
        q.setRPY(0, 0, 0);
        imu_transform.transform.rotation.x = q.x();
        imu_transform.transform.rotation.y = q.y();
        imu_transform.transform.rotation.z = q.z();
        imu_transform.transform.rotation.w = q.w();

        // 发布所有静态变换
        std::vector<geometry_msgs::TransformStamped> static_transforms;
        static_transforms.push_back(laser_transform);
        static_transforms.push_back(camera_transform);
        static_transforms.push_back(imu_transform);

        static_broadcaster.sendTransform(static_transforms);

        ROS_INFO("Published static transforms");
    }

private:
    ros::NodeHandle nh_;
};

int main(int argc, char** argv) {
    ros::init(argc, argv, "static_transform_publisher");
    StaticTransformPublisher publisher;
    ros::spin();
    return 0;
}

3.2 Python静态变换发布

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import rospy
import tf2_ros
from geometry_msgs.msg import TransformStamped
import tf.transformations as tf_trans

class StaticTransformPublisher:
    """静态坐标变换发布器"""

    def __init__(self):
        rospy.init_node('static_transform_publisher')

        # 创建静态变换广播器
        self.static_broadcaster = tf2_ros.StaticTransformBroadcaster()

        # 发布静态变换
        self.publish_static_transforms()

    def publish_static_transforms(self):
        """发布静态变换"""

        static_transforms = []

        # 1. base_link到laser_link
        laser_transform = TransformStamped()
        laser_transform.header.stamp = rospy.Time.now()
        laser_transform.header.frame_id = "base_link"
        laser_transform.child_frame_id = "laser_link"

        laser_transform.transform.translation.x = 0.2
        laser_transform.transform.translation.y = 0.0
        laser_transform.transform.translation.z = 0.1

        q = tf_trans.quaternion_from_euler(0, 0, 0)
        laser_transform.transform.rotation.x = q[0]
        laser_transform.transform.rotation.y = q[1]
        laser_transform.transform.rotation.z = q[2]
        laser_transform.transform.rotation.w = q[3]

        static_transforms.append(laser_transform)

        # 2. base_link到camera_link
        camera_transform = TransformStamped()
        camera_transform.header.stamp = rospy.Time.now()
        camera_transform.header.frame_id = "base_link"
        camera_transform.child_frame_id = "camera_link"

        camera_transform.transform.translation.x = 0.15
        camera_transform.transform.translation.y = 0.0
        camera_transform.transform.translation.z = 0.3

        # 相机向下倾斜15度
        q = tf_trans.quaternion_from_euler(0, -0.2618, 0)
        camera_transform.transform.rotation.x = q[0]
        camera_transform.transform.rotation.y = q[1]
        camera_transform.transform.rotation.z = q[2]
        camera_transform.transform.rotation.w = q[3]

        static_transforms.append(camera_transform)

        # 3. base_link到多个传感器
        sensor_configs = [
            {'name': 'imu_link', 'x': 0.0, 'y': 0.0, 'z': 0.05, 'roll': 0, 'pitch': 0, 'yaw': 0},
            {'name': 'gps_link', 'x': -0.1, 'y': 0.0, 'z': 0.2, 'roll': 0, 'pitch': 0, 'yaw': 0},
            {'name': 'ultrasonic_front', 'x': 0.25, 'y': 0.0, 'z': 0.05, 'roll': 0, 'pitch': 0, 'yaw': 0},
            {'name': 'ultrasonic_back', 'x': -0.25, 'y': 0.0, 'z': 0.05, 'roll': 0, 'pitch': 0, 'yaw': 3.14159},
        ]

        for config in sensor_configs:
            transform = self.create_transform(
                "base_link",
                config['name'],
                config['x'], config['y'], config['z'],
                config['roll'], config['pitch'], config['yaw']
            )
            static_transforms.append(transform)

        # 发布所有静态变换
        self.static_broadcaster.sendTransform(static_transforms)
        rospy.loginfo(f"Published {len(static_transforms)} static transforms")

    def create_transform(self, parent_frame, child_frame, x, y, z, roll, pitch, yaw):
        """创建变换消息"""
        transform = TransformStamped()

        transform.header.stamp = rospy.Time.now()
        transform.header.frame_id = parent_frame
        transform.child_frame_id = child_frame

        transform.transform.translation.x = x
        transform.transform.translation.y = y
        transform.transform.translation.z = z

        q = tf_trans.quaternion_from_euler(roll, pitch, yaw)
        transform.transform.rotation.x = q[0]
        transform.transform.rotation.y = q[1]
        transform.transform.rotation.z = q[2]
        transform.transform.rotation.w = q[3]

        return transform

    def publish_from_urdf(self):
        """从URDF参数发布静态变换"""
        # 这通常由robot_state_publisher完成
        # 但我们可以手动解析URDF并发布

        if rospy.has_param('robot_description'):
            robot_description = rospy.get_param('robot_description')
            # 解析URDF并提取固定关节的变换
            # 这里简化处理
            rospy.loginfo("Would parse URDF and publish fixed joints")

def main():
    publisher = StaticTransformPublisher()
    rospy.spin()

if __name__ == '__main__':
    main()

3.3 Launch文件配置静态变换

xml 复制代码
<!-- launch/static_transforms.launch -->
<launch>
  <!-- 方法1:使用static_transform_publisher节点 -->
  <node pkg="tf2_ros" type="static_transform_publisher"
        name="laser_broadcaster"
        args="0.2 0 0.1 0 0 0 base_link laser_link" />

  <node pkg="tf2_ros" type="static_transform_publisher"
        name="camera_broadcaster"
        args="0.15 0 0.3 0 -0.2618 0 base_link camera_link" />

  <!-- 方法2:使用tf包的static_transform_publisher(旧版本) -->
  <node pkg="tf" type="static_transform_publisher"
        name="imu_broadcaster"
        args="0 0 0.05 0 0 0 base_link imu_link 100" />

  <!-- 方法3:从参数加载 -->
  <rosparam>
    static_transforms:
      - frame_id: base_link
        child_frame_id: gps_link
        x: -0.1
        y: 0.0
        z: 0.2
        roll: 0.0
        pitch: 0.0
        yaw: 0.0
  </rosparam>

  <node name="param_static_publisher" pkg="my_tf_pkg" type="param_static_publisher.py" />

  <!-- 方法4:从URDF自动生成 -->
  <param name="robot_description" command="$(find xacro)/xacro $(find my_robot_description)/urdf/robot.urdf.xacro" />

  <node name="robot_state_publisher" pkg="robot_state_publisher" type="robot_state_publisher">
    <param name="publish_frequency" value="50" />
  </node>
</launch>

4. 动态坐标变换

4.1 动态变换广播器

cpp 复制代码
// dynamic_transform_broadcaster.cpp
#include <ros/ros.h>
#include <tf2_ros/transform_broadcaster.h>
#include <tf2/LinearMath/Quaternion.h>
#include <geometry_msgs/TransformStamped.h>
#include <nav_msgs/Odometry.h>
#include <sensor_msgs/JointState.h>

class DynamicTransformBroadcaster {
public:
    DynamicTransformBroadcaster() : nh_("~") {
        // 初始化
        tf_broadcaster_ = std::make_unique<tf2_ros::TransformBroadcaster>();

        // 订阅里程计
        odom_sub_ = nh_.subscribe("/odom", 10,
            &DynamicTransformBroadcaster::odomCallback, this);

        // 订阅关节状态
        joint_sub_ = nh_.subscribe("/joint_states", 10,
            &DynamicTransformBroadcaster::jointCallback, this);

        // 定时发布变换
        timer_ = nh_.createTimer(ros::Duration(0.02),  // 50Hz
            &DynamicTransformBroadcaster::timerCallback, this);

        ROS_INFO("Dynamic transform broadcaster initialized");
    }

private:
    void odomCallback(const nav_msgs::Odometry::ConstPtr& msg) {
        // 发布odom到base_link的变换
        geometry_msgs::TransformStamped odom_transform;

        odom_transform.header.stamp = msg->header.stamp;
        odom_transform.header.frame_id = "odom";
        odom_transform.child_frame_id = "base_link";

        // 从里程计消息复制位置
        odom_transform.transform.translation.x = msg->pose.pose.position.x;
        odom_transform.transform.translation.y = msg->pose.pose.position.y;
        odom_transform.transform.translation.z = msg->pose.pose.position.z;

        // 从里程计消息复制姿态
        odom_transform.transform.rotation = msg->pose.pose.orientation;

        // 发送变换
        tf_broadcaster_->sendTransform(odom_transform);
    }

    void jointCallback(const sensor_msgs::JointState::ConstPtr& msg) {
        // 发布关节变换(例如机械臂)
        for (size_t i = 0; i < msg->name.size(); ++i) {
            if (msg->name[i] == "arm_joint_1") {
                publishJointTransform("arm_base_link", "arm_link_1",
                    0, 0, 0.1,  // 平移
                    msg->position[i], 0, 0);  // 旋转(绕Z轴)
            }
            else if (msg->name[i] == "arm_joint_2") {
                publishJointTransform("arm_link_1", "arm_link_2",
                    0.2, 0, 0,  // 平移
                    0, msg->position[i], 0);  // 旋转(绕Y轴)
            }
            // ... 更多关节
        }
    }

    void timerCallback(const ros::TimerEvent& event) {
        // 发布时变的变换(例如旋转的传感器)
        ros::Time now = ros::Time::now();

        // 旋转的激光雷达
        double angle = 2 * M_PI * fmod(now.toSec(), 10.0) / 10.0;  // 10秒一圈

        geometry_msgs::TransformStamped rotating_transform;
        rotating_transform.header.stamp = now;
        rotating_transform.header.frame_id = "base_link";
        rotating_transform.child_frame_id = "rotating_laser";

        rotating_transform.transform.translation.x = 0.0;
        rotating_transform.transform.translation.y = 0.0;
        rotating_transform.transform.translation.z = 0.2;

        tf2::Quaternion q;
        q.setRPY(0, 0, angle);
        rotating_transform.transform.rotation.x = q.x();
        rotating_transform.transform.rotation.y = q.y();
        rotating_transform.transform.rotation.z = q.z();
        rotating_transform.transform.rotation.w = q.w();

        tf_broadcaster_->sendTransform(rotating_transform);
    }

    void publishJointTransform(const std::string& parent_frame,
                               const std::string& child_frame,
                               double x, double y, double z,
                               double roll, double pitch, double yaw) {
        geometry_msgs::TransformStamped transform;

        transform.header.stamp = ros::Time::now();
        transform.header.frame_id = parent_frame;
        transform.child_frame_id = child_frame;

        transform.transform.translation.x = x;
        transform.transform.translation.y = y;
        transform.transform.translation.z = z;

        tf2::Quaternion q;
        q.setRPY(roll, pitch, yaw);
        transform.transform.rotation.x = q.x();
        transform.transform.rotation.y = q.y();
        transform.transform.rotation.z = q.z();
        transform.transform.rotation.w = q.w();

        tf_broadcaster_->sendTransform(transform);
    }

    ros::NodeHandle nh_;
    std::unique_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster_;
    ros::Subscriber odom_sub_;
    ros::Subscriber joint_sub_;
    ros::Timer timer_;
};

int main(int argc, char** argv) {
    ros::init(argc, argv, "dynamic_transform_broadcaster");
    DynamicTransformBroadcaster broadcaster;
    ros::spin();
    return 0;
}

4.2 Python动态变换

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import rospy
import tf2_ros
import tf
from geometry_msgs.msg import TransformStamped
from nav_msgs.msg import Odometry
from sensor_msgs.msg import JointState
import math

class DynamicTransformBroadcaster:
    """动态坐标变换广播器"""

    def __init__(self):
        rospy.init_node('dynamic_transform_broadcaster')

        # TF广播器
        self.tf_broadcaster = tf2_ros.TransformBroadcaster()

        # 订阅者
        self.odom_sub = rospy.Subscriber(
            '/odom', Odometry, self.odom_callback)
        self.joint_sub = rospy.Subscriber(
            '/joint_states', JointState, self.joint_callback)

        # 定时器
        self.timer = rospy.Timer(rospy.Duration(0.02), self.timer_callback)

        # 状态
        self.robot_x = 0.0
        self.robot_y = 0.0
        self.robot_theta = 0.0

        rospy.loginfo("Dynamic transform broadcaster started")

    def odom_callback(self, msg):
        """里程计回调"""
        # 发布odom到base_link的变换
        transform = TransformStamped()

        transform.header.stamp = msg.header.stamp
        transform.header.frame_id = "odom"
        transform.child_frame_id = "base_link"

        # 位置
        transform.transform.translation.x = msg.pose.pose.position.x
        transform.transform.translation.y = msg.pose.pose.position.y
        transform.transform.translation.z = msg.pose.pose.position.z

        # 姿态
        transform.transform.rotation = msg.pose.pose.orientation

        # 广播变换
        self.tf_broadcaster.sendTransform(transform)

        # 更新机器人位置(用于其他计算)
        self.robot_x = msg.pose.pose.position.x
        self.robot_y = msg.pose.pose.position.y

        quaternion = (
            msg.pose.pose.orientation.x,
            msg.pose.pose.orientation.y,
            msg.pose.pose.orientation.z,
            msg.pose.pose.orientation.w
        )
        euler = tf.transformations.euler_from_quaternion(quaternion)
        self.robot_theta = euler[2]

    def joint_callback(self, msg):
        """关节状态回调"""
        # 发布关节变换
        for i, joint_name in enumerate(msg.name):
            if joint_name == "arm_joint_1":
                self.publish_joint_transform(
                    "arm_base_link", "arm_link_1",
                    0, 0, 0.1,  # 平移
                    msg.position[i], 0, 0  # 旋转
                )
            elif joint_name == "arm_joint_2":
                self.publish_joint_transform(
                    "arm_link_1", "arm_link_2",
                    0.2, 0, 0,  # 平移
                    0, msg.position[i], 0  # 旋转
                )
            elif joint_name == "gripper_joint":
                # 夹爪(平移关节)
                self.publish_joint_transform(
                    "arm_link_2", "gripper_link",
                    msg.position[i], 0, 0,  # 平移
                    0, 0, 0  # 无旋转
                )

    def timer_callback(self, event):
        """定时器回调"""
        current_time = rospy.Time.now()

        # 1. 发布旋转的传感器
        self.publish_rotating_sensor(current_time)

        # 2. 发布摆动的天线
        self.publish_swinging_antenna(current_time)

        # 3. 发布模拟的轮子旋转
        self.publish_wheel_rotation(current_time)

    def publish_rotating_sensor(self, timestamp):
        """发布旋转的传感器"""
        # 10秒转一圈
        period = 10.0
        angle = 2 * math.pi * (timestamp.to_sec() % period) / period

        transform = TransformStamped()
        transform.header.stamp = timestamp
        transform.header.frame_id = "base_link"
        transform.child_frame_id = "rotating_sensor"

        transform.transform.translation.x = 0.0
        transform.transform.translation.y = 0.0
        transform.transform.translation.z = 0.3

        q = tf.transformations.quaternion_from_euler(0, 0, angle)
        transform.transform.rotation.x = q[0]
        transform.transform.rotation.y = q[1]
        transform.transform.rotation.z = q[2]
        transform.transform.rotation.w = q[3]

        self.tf_broadcaster.sendTransform(transform)

    def publish_swinging_antenna(self, timestamp):
        """发布摆动的天线"""
        # 正弦摆动
        amplitude = 0.5  # 弧度
        frequency = 0.5  # Hz
        angle = amplitude * math.sin(2 * math.pi * frequency * timestamp.to_sec())

        transform = TransformStamped()
        transform.header.stamp = timestamp
        transform.header.frame_id = "base_link"
        transform.child_frame_id = "antenna"

        transform.transform.translation.x = -0.1
        transform.transform.translation.y = 0.0
        transform.transform.translation.z = 0.4

        q = tf.transformations.quaternion_from_euler(angle, 0, 0)
        transform.transform.rotation.x = q[0]
        transform.transform.rotation.y = q[1]
        transform.transform.rotation.z = q[2]
        transform.transform.rotation.w = q[3]

        self.tf_broadcaster.sendTransform(transform)

    def publish_wheel_rotation(self, timestamp):
        """发布轮子旋转"""
        # 基于机器人速度估算轮子转速
        wheel_radius = 0.05  # 轮子半径
        linear_velocity = math.sqrt(self.robot_x**2 + self.robot_y**2) / timestamp.to_sec()

        if timestamp.to_sec() > 0:
            wheel_angle = linear_velocity * timestamp.to_sec() / wheel_radius
        else:
            wheel_angle = 0

        # 左轮
        transform_left = TransformStamped()
        transform_left.header.stamp = timestamp
        transform_left.header.frame_id = "base_link"
        transform_left.child_frame_id = "wheel_left"

        transform_left.transform.translation.x = 0.0
        transform_left.transform.translation.y = 0.15
        transform_left.transform.translation.z = -0.05

        q = tf.transformations.quaternion_from_euler(0, wheel_angle, 0)
        transform_left.transform.rotation.x = q[0]
        transform_left.transform.rotation.y = q[1]
        transform_left.transform.rotation.z = q[2]
        transform_left.transform.rotation.w = q[3]

        # 右轮
        transform_right = TransformStamped()
        transform_right.header.stamp = timestamp
        transform_right.header.frame_id = "base_link"
        transform_right.child_frame_id = "wheel_right"

        transform_right.transform.translation.x = 0.0
        transform_right.transform.translation.y = -0.15
        transform_right.transform.translation.z = -0.05

        transform_right.transform.rotation = transform_left.transform.rotation

        self.tf_broadcaster.sendTransform([transform_left, transform_right])

    def publish_joint_transform(self, parent_frame, child_frame,
                               x, y, z, roll, pitch, yaw):
        """发布关节变换"""
        transform = TransformStamped()

        transform.header.stamp = rospy.Time.now()
        transform.header.frame_id = parent_frame
        transform.child_frame_id = child_frame

        transform.transform.translation.x = x
        transform.transform.translation.y = y
        transform.transform.translation.z = z

        q = tf.transformations.quaternion_from_euler(roll, pitch, yaw)
        transform.transform.rotation.x = q[0]
        transform.transform.rotation.y = q[1]
        transform.transform.rotation.z = q[2]
        transform.transform.rotation.w = q[3]

        self.tf_broadcaster.sendTransform(transform)

def main():
    broadcaster = DynamicTransformBroadcaster()
    rospy.spin()

if __name__ == '__main__':
    main()

5. TF树管理

5.1 TF树监听器

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import rospy
import tf2_ros
import tf2_geometry_msgs
from geometry_msgs.msg import PointStamped, PoseStamped
from sensor_msgs.msg import PointCloud2
import sensor_msgs.point_cloud2 as pc2

class TFListener:
    """TF监听器和查询"""

    def __init__(self):
        rospy.init_node('tf_listener')

        # 创建TF缓冲区和监听器
        self.tf_buffer = tf2_ros.Buffer()
        self.tf_listener = tf2_ros.TransformListener(self.tf_buffer)

        # 等待TF树建立
        rospy.sleep(2.0)

        # 演示各种TF查询
        self.demonstrate_tf_queries()

        # 设置定时器持续查询
        self.timer = rospy.Timer(rospy.Duration(1.0), self.timer_callback)

    def demonstrate_tf_queries(self):
        """演示TF查询"""

        try:
            # 1. 查询最新的变换
            self.query_latest_transform()

            # 2. 查询特定时间的变换
            self.query_transform_at_time()

            # 3. 变换点
            self.transform_point()

            # 4. 变换位姿
            self.transform_pose()

            # 5. 查询所有坐标系
            self.list_all_frames()

            # 6. 检查变换是否存在
            self.check_transform_existence()

        except (tf2_ros.LookupException, tf2_ros.ConnectivityException,
                tf2_ros.ExtrapolationException) as e:
            rospy.logerr(f"TF Error: {e}")

    def query_latest_transform(self):
        """查询最新的变换"""
        try:
            # 获取base_link到laser_link的最新变换
            transform = self.tf_buffer.lookup_transform(
                'base_link', 'laser_link', rospy.Time())

            rospy.loginfo("Latest transform from base_link to laser_link:")
            rospy.loginfo(f"  Translation: ({transform.transform.translation.x:.3f}, "
                         f"{transform.transform.translation.y:.3f}, "
                         f"{transform.transform.translation.z:.3f})")

            # 提取欧拉角
            import tf.transformations as tf_trans
            quaternion = (
                transform.transform.rotation.x,
                transform.transform.rotation.y,
                transform.transform.rotation.z,
                transform.transform.rotation.w
            )
            euler = tf_trans.euler_from_quaternion(quaternion)
            rospy.loginfo(f"  Rotation (RPY): ({euler[0]:.3f}, {euler[1]:.3f}, {euler[2]:.3f})")

        except tf2_ros.TransformException as e:
            rospy.logwarn(f"Could not get transform: {e}")

    def query_transform_at_time(self):
        """查询特定时间的变换"""
        try:
            # 查询1秒前的变换
            past_time = rospy.Time.now() - rospy.Duration(1.0)

            if self.tf_buffer.can_transform('odom', 'base_link', past_time):
                transform = self.tf_buffer.lookup_transform(
                    'odom', 'base_link', past_time)

                rospy.loginfo(f"Transform at {past_time.to_sec():.2f}:")
                rospy.loginfo(f"  Position: ({transform.transform.translation.x:.3f}, "
                             f"{transform.transform.translation.y:.3f})")

        except tf2_ros.TransformException as e:
            rospy.logwarn(f"Could not get past transform: {e}")

    def transform_point(self):
        """变换点"""
        # 创建一个在laser_link坐标系中的点
        point_laser = PointStamped()
        point_laser.header.frame_id = "laser_link"
        point_laser.header.stamp = rospy.Time()
        point_laser.point.x = 1.0  # 激光雷达前方1米
        point_laser.point.y = 0.0
        point_laser.point.z = 0.0

        try:
            # 变换到base_link坐标系
            point_base = self.tf_buffer.transform(point_laser, "base_link")

            rospy.loginfo("Point transformation:")
            rospy.loginfo(f"  In laser_link: ({point_laser.point.x:.3f}, "
                         f"{point_laser.point.y:.3f}, {point_laser.point.z:.3f})")
            rospy.loginfo(f"  In base_link: ({point_base.point.x:.3f}, "
                         f"{point_base.point.y:.3f}, {point_base.point.z:.3f})")

        except tf2_ros.TransformException as e:
            rospy.logwarn(f"Could not transform point: {e}")

    def transform_pose(self):
        """变换位姿"""
        # 创建一个位姿
        pose_camera = PoseStamped()
        pose_camera.header.frame_id = "camera_link"
        pose_camera.header.stamp = rospy.Time()

        # 相机坐标系中检测到的物体
        pose_camera.pose.position.x = 2.0
        pose_camera.pose.position.y = 0.5
        pose_camera.pose.position.z = 0.0
        pose_camera.pose.orientation.w = 1.0

        try:
            # 变换到map坐标系
            pose_map = self.tf_buffer.transform(pose_camera, "map",
                                               timeout=rospy.Duration(1.0))

            rospy.loginfo("Object pose in map frame:")
            rospy.loginfo(f"  Position: ({pose_map.pose.position.x:.3f}, "
                         f"{pose_map.pose.position.y:.3f}, "
                         f"{pose_map.pose.position.z:.3f})")

        except tf2_ros.TransformException as e:
            rospy.logwarn(f"Could not transform pose: {e}")

    def list_all_frames(self):
        """列出所有坐标系"""
        frames = self.tf_buffer.all_frames_as_yaml()
        rospy.loginfo("All TF frames:")
        # 解析YAML输出
        lines = frames.split('\n')
        frame_names = [line.split(':')[0].strip()
                      for line in lines if ':' in line]
        for frame in frame_names[:10]:  # 只显示前10个
            rospy.loginfo(f"  - {frame}")

    def check_transform_existence(self):
        """检查变换是否存在"""
        frame_pairs = [
            ('map', 'odom'),
            ('odom', 'base_link'),
            ('base_link', 'laser_link'),
            ('base_link', 'non_existent_frame')
        ]

        for source, target in frame_pairs:
            if self.tf_buffer.can_transform(target, source, rospy.Time()):
                rospy.loginfo(f"Transform {source} -> {target}: EXISTS")
            else:
                rospy.loginfo(f"Transform {source} -> {target}: NOT FOUND")

    def timer_callback(self, event):
        """定时查询回调"""
        try:
            # 持续跟踪机器人位置
            transform = self.tf_buffer.lookup_transform(
                'map', 'base_link', rospy.Time())

            x = transform.transform.translation.x
            y = transform.transform.translation.y

            rospy.loginfo(f"Robot position in map: ({x:.2f}, {y:.2f})")

        except tf2_ros.TransformException:
            pass  # 忽略错误

    def transform_point_cloud(self, cloud_msg, target_frame):
        """变换点云"""
        try:
            # 获取变换
            transform = self.tf_buffer.lookup_transform(
                target_frame,
                cloud_msg.header.frame_id,
                cloud_msg.header.stamp,
                rospy.Duration(1.0))

            # 变换点云(需要tf2_sensor_msgs)
            # transformed_cloud = tf2_sensor_msgs.do_transform_cloud(cloud_msg, transform)

            rospy.loginfo(f"Transformed point cloud from {cloud_msg.header.frame_id} to {target_frame}")

        except tf2_ros.TransformException as e:
            rospy.logwarn(f"Could not transform point cloud: {e}")

def main():
    listener = TFListener()
    rospy.spin()

if __name__ == '__main__':
    main()

6. TF监听和查询

6.1 高级TF查询

cpp 复制代码
// tf_advanced_listener.cpp
#include <ros/ros.h>
#include <tf2_ros/transform_listener.h>
#include <tf2_ros/buffer.h>
#include <tf2_geometry_msgs/tf2_geometry_msgs.h>
#include <geometry_msgs/PointStamped.h>
#include <geometry_msgs/PoseStamped.h>
#include <geometry_msgs/Vector3Stamped.h>

class AdvancedTFListener {
public:
    AdvancedTFListener() : tfListener_(tfBuffer_) {
        // 等待TF缓冲区填充
        ros::Duration(2.0).sleep();

        // 创建定时器
        timer_ = nh_.createTimer(ros::Duration(0.1),
            &AdvancedTFListener::timerCallback, this);

        ROS_INFO("Advanced TF listener initialized");
    }

private:
    void timerCallback(const ros::TimerEvent& event) {
        // 1. 获取变换矩阵
        getTransformMatrix();

        // 2. 计算相对速度
        calculateRelativeVelocity();

        // 3. 跟踪移动物体
        trackMovingObject();

        // 4. 坐标系链查询
        queryTransformChain();
    }

    void getTransformMatrix() {
        try {
            // 获取变换
            geometry_msgs::TransformStamped transform =
                tfBuffer_.lookupTransform("map", "base_link", ros::Time(0));

            // 转换为矩阵形式
            tf2::Transform tf_transform;
            tf2::fromMsg(transform.transform, tf_transform);

            // 获取矩阵元素
            tf2::Matrix3x3 rotation = tf_transform.getBasis();
            tf2::Vector3 translation = tf_transform.getOrigin();

            ROS_INFO_THROTTLE(5, "Transform matrix from map to base_link:");
            for (int i = 0; i < 3; ++i) {
                ROS_INFO_THROTTLE(5, "  [%.3f, %.3f, %.3f, %.3f]",
                    rotation[i][0], rotation[i][1], rotation[i][2],
                    i == 0 ? translation.x() : (i == 1 ? translation.y() : translation.z()));
            }
        }
        catch (tf2::TransformException& ex) {
            ROS_WARN_THROTTLE(1, "Transform error: %s", ex.what());
        }
    }

    void calculateRelativeVelocity() {
        try {
            // 获取两个时间点的变换
            ros::Time now = ros::Time::now();
            ros::Time past = now - ros::Duration(0.1);

            // 确保变换可用
            if (!tfBuffer_.canTransform("odom", "base_link", now) ||
                !tfBuffer_.canTransform("odom", "base_link", past)) {
                return;
            }

            geometry_msgs::TransformStamped transform_now =
                tfBuffer_.lookupTransform("odom", "base_link", now);
            geometry_msgs::TransformStamped transform_past =
                tfBuffer_.lookupTransform("odom", "base_link", past);

            // 计算位移
            double dx = transform_now.transform.translation.x -
                       transform_past.transform.translation.x;
            double dy = transform_now.transform.translation.y -
                       transform_past.transform.translation.y;

            // 计算速度
            double dt = (now - past).toSec();
            double vx = dx / dt;
            double vy = dy / dt;

            ROS_INFO_THROTTLE(1, "Robot velocity: vx=%.2f, vy=%.2f m/s", vx, vy);
        }
        catch (tf2::TransformException& ex) {
            // 忽略错误
        }
    }

    void trackMovingObject() {
        // 模拟跟踪移动物体
        static double object_x = 5.0;
        static double object_y = 0.0;

        // 物体在世界坐标系中移动
        object_x += 0.01;
        object_y = 2.0 * sin(object_x * 0.5);

        // 创建物体位置(世界坐标系)
        geometry_msgs::PointStamped object_world;
        object_world.header.frame_id = "map";
        object_world.header.stamp = ros::Time();
        object_world.point.x = object_x;
        object_world.point.y = object_y;
        object_world.point.z = 0.0;

        try {
            // 变换到机器人坐标系
            geometry_msgs::PointStamped object_robot =
                tfBuffer_.transform(object_world, "base_link");

            // 计算距离和角度
            double distance = sqrt(pow(object_robot.point.x, 2) +
                                 pow(object_robot.point.y, 2));
            double angle = atan2(object_robot.point.y, object_robot.point.x);

            ROS_INFO_THROTTLE(1, "Object relative position: distance=%.2f, angle=%.2f°",
                             distance, angle * 180.0 / M_PI);
        }
        catch (tf2::TransformException& ex) {
            ROS_WARN("Could not track object: %s", ex.what());
        }
    }

    void queryTransformChain() {
        // 查询变换链
        std::vector<std::string> chain = {"map", "odom", "base_link", "laser_link"};

        for (size_t i = 0; i < chain.size() - 1; ++i) {
            try {
                geometry_msgs::TransformStamped transform =
                    tfBuffer_.lookupTransform(chain[i], chain[i+1], ros::Time(0));

                ROS_INFO_THROTTLE(10, "Transform %s -> %s exists",
                    chain[i].c_str(), chain[i+1].c_str());
            }
            catch (tf2::TransformException& ex) {
                ROS_WARN("Transform %s -> %s missing: %s",
                    chain[i].c_str(), chain[i+1].c_str(), ex.what());
            }
        }
    }

    ros::NodeHandle nh_;
    tf2_ros::Buffer tfBuffer_;
    tf2_ros::TransformListener tfListener_;
    ros::Timer timer_;
};

int main(int argc, char** argv) {
    ros::init(argc, argv, "tf_advanced_listener");
    AdvancedTFListener listener;
    ros::spin();
    return 0;
}

7. TF工具使用

7.1 命令行工具

bash 复制代码
# 1. 查看TF树
rosrun tf2_tools view_frames.py
# 生成frames.pdf文件,显示完整的TF树

# 2. 打印TF树
rosrun tf tf_echo source_frame target_frame
# 实时显示两个坐标系之间的变换

# 3. 监控TF
rosrun tf tf_monitor
# 显示所有TF广播器的统计信息

# 4. 静态变换发布
rosrun tf2_ros static_transform_publisher x y z yaw pitch roll frame_id child_frame_id

# 5. RViz中查看TF
# 启动RViz并添加TF显示插件

# 6. rqt中的TF树
rosrun rqt_tf_tree rqt_tf_tree
# 图形化显示TF树结构

# 7. 保存和回放TF
rosbag record -O tf_data.bag /tf /tf_static
rosbag play tf_data.bag

# 8. 查询特定时刻的变换
rosrun tf tf_echo -t 1234567890.123 map base_link

# 9. 检查TF延迟
rostopic echo /tf_static
rostopic hz /tf

7.2 Python TF工具

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import rospy
import tf2_ros
import tf2_py as tf2
from tf2_msgs.msg import TFMessage
import yaml
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import networkx as nx

class TFAnalyzer:
    """TF分析工具"""

    def __init__(self):
        rospy.init_node('tf_analyzer')

        self.tf_buffer = tf2_ros.Buffer()
        self.tf_listener = tf2_ros.TransformListener(self.tf_buffer)

        # TF统计
        self.tf_stats = {}
        self.tf_sub = rospy.Subscriber('/tf', TFMessage, self.tf_callback)

        rospy.sleep(2.0)

    def tf_callback(self, msg):
        """TF消息回调"""
        for transform in msg.transforms:
            key = f"{transform.header.frame_id}->{transform.child_frame_id}"

            if key not in self.tf_stats:
                self.tf_stats[key] = {
                    'count': 0,
                    'last_time': None,
                    'frequency': 0
                }

            stats = self.tf_stats[key]
            stats['count'] += 1

            if stats['last_time']:
                dt = (transform.header.stamp - stats['last_time']).to_sec()
                if dt > 0:
                    stats['frequency'] = 1.0 / dt

            stats['last_time'] = transform.header.stamp

    def analyze_tf_tree(self):
        """分析TF树结构"""
        # 获取所有坐标系
        yaml_str = self.tf_buffer.all_frames_as_yaml()
        frames_data = yaml.safe_load(yaml_str)

        print("=== TF Tree Analysis ===")
        print(f"Total frames: {len(frames_data)}")

        # 构建图
        G = nx.DiGraph()

        for frame_name, frame_data in frames_data.items():
            if 'parent' in frame_data:
                parent = frame_data['parent']
                G.add_edge(parent, frame_name)

        # 分析图结构
        print(f"Tree depth: {nx.dag_longest_path_length(G)}")
        print(f"Leaf nodes: {[n for n in G.nodes() if G.out_degree(n) == 0]}")

        # 查找根节点
        roots = [n for n in G.nodes() if G.in_degree(n) == 0]
        print(f"Root frames: {roots}")

        return G

    def print_tf_statistics(self):
        """打印TF统计信息"""
        print("\n=== TF Statistics ===")
        print(f"{'Transform':<30} {'Count':<10} {'Frequency':<10}")
        print("-" * 50)

        for key, stats in sorted(self.tf_stats.items()):
            print(f"{key:<30} {stats['count']:<10} {stats['frequency']:<10.1f}")

    def check_tf_delays(self):
        """检查TF延迟"""
        print("\n=== TF Delays ===")

        current_time = rospy.Time.now()

        for key, stats in self.tf_stats.items():
            if stats['last_time']:
                delay = (current_time - stats['last_time']).to_sec()
                if delay > 0.5:  # 超过0.5秒认为是延迟
                    print(f"{key}: {delay:.2f} seconds delay")

    def visualize_tf_tree(self, graph):
        """可视化TF树"""
        plt.figure(figsize=(12, 8))

        # 使用层次布局
        pos = nx.spring_layout(graph)

        # 绘制节点和边
        nx.draw(graph, pos, with_labels=True,
                node_color='lightblue',
                node_size=2000,
                font_size=8,
                font_weight='bold',
                arrows=True,
                edge_color='gray')

        plt.title("TF Tree Structure")
        plt.axis('off')
        plt.tight_layout()
        plt.show()

    def monitor_transform_rate(self, source_frame, target_frame, duration=10):
        """监控变换频率"""
        print(f"\nMonitoring {source_frame} -> {target_frame} for {duration} seconds...")

        start_time = rospy.Time.now()
        count = 0
        errors = 0

        while (rospy.Time.now() - start_time).to_sec() < duration:
            try:
                transform = self.tf_buffer.lookup_transform(
                    target_frame, source_frame, rospy.Time())
                count += 1
            except tf2_ros.TransformException:
                errors += 1

            rospy.sleep(0.01)

        elapsed = (rospy.Time.now() - start_time).to_sec()
        rate = count / elapsed

        print(f"Results:")
        print(f"  Successful lookups: {count}")
        print(f"  Failed lookups: {errors}")
        print(f"  Average rate: {rate:.1f} Hz")

    def find_transform_path(self, source_frame, target_frame):
        """查找变换路径"""
        print(f"\nFinding path from {source_frame} to {target_frame}...")

        # 构建TF图
        yaml_str = self.tf_buffer.all_frames_as_yaml()
        frames_data = yaml.safe_load(yaml_str)

        G = nx.DiGraph()
        for frame_name, frame_data in frames_data.items():
            if 'parent' in frame_data:
                parent = frame_data['parent']
                G.add_edge(parent, frame_name)
                G.add_edge(frame_name, parent)  # 双向

        # 查找路径
        try:
            path = nx.shortest_path(G, source_frame, target_frame)
            print(f"Path found: {' -> '.join(path)}")
            return path
        except nx.NetworkXNoPath:
            print(f"No path found between {source_frame} and {target_frame}")
            return None

def main():
    analyzer = TFAnalyzer()

    # 等待TF数据
    rospy.sleep(3.0)

    # 执行分析
    graph = analyzer.analyze_tf_tree()
    analyzer.print_tf_statistics()
    analyzer.check_tf_delays()

    # 监控特定变换
    if analyzer.tf_buffer.can_transform("map", "base_link", rospy.Time()):
        analyzer.monitor_transform_rate("map", "base_link", duration=5)

    # 查找路径
    analyzer.find_transform_path("map", "laser_link")

    # 可视化
    # analyzer.visualize_tf_tree(graph)

    rospy.spin()

if __name__ == '__main__':
    main()

8. TF2高级特性

8.1 TF2特性示例

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import rospy
import tf2_ros
import tf2_py as tf2
from geometry_msgs.msg import TransformStamped, PoseStamped, PointStamped

class TF2AdvancedFeatures:
    """TF2高级特性"""

    def __init__(self):
        rospy.init_node('tf2_advanced_features')

        # 创建多个缓冲区(TF2特性)
        self.main_buffer = tf2_ros.Buffer()
        self.main_listener = tf2_ros.TransformListener(self.main_buffer)

        # 专用缓冲区(用于不同用途)
        self.planning_buffer = tf2_ros.Buffer(rospy.Duration(30.0))  # 30秒历史
        self.planning_listener = tf2_ros.TransformListener(self.planning_buffer)

        rospy.sleep(2.0)

        # 演示高级特性
        self.demonstrate_advanced_features()

    def demonstrate_advanced_features(self):
        """演示TF2高级特性"""

        # 1. 时间旅行查询
        self.time_travel_queries()

        # 2. 变换缓存管理
        self.buffer_management()

        # 3. 异步变换查询
        self.async_transform_queries()

        # 4. 自定义插值
        self.custom_interpolation()

        # 5. 坐标系权威管理
        self.frame_authority()

    def time_travel_queries(self):
        """时间旅行查询"""
        print("=== Time Travel Queries ===")

        try:
            # 查询过去的变换
            past_time = rospy.Time.now() - rospy.Duration(5.0)

            if self.planning_buffer.can_transform("map", "base_link", past_time):
                past_transform = self.planning_buffer.lookup_transform(
                    "map", "base_link", past_time)
                print(f"Position 5 seconds ago: "
                      f"({past_transform.transform.translation.x:.2f}, "
                      f"{past_transform.transform.translation.y:.2f})")

            # 查询最早可用的变换时间
            try:
                earliest_time = self.planning_buffer.get_latest_common_time(
                    "map", "base_link")
                print(f"Earliest common time: {earliest_time.to_sec():.2f}")
            except:
                pass

        except tf2_ros.TransformException as e:
            print(f"Time travel query failed: {e}")

    def buffer_management(self):
        """缓冲区管理"""
        print("\n=== Buffer Management ===")

        # 清除特定变换
        # self.main_buffer.clear()  # 清除所有

        # 设置缓冲区大小
        custom_buffer = tf2_ros.Buffer(cache_time=rospy.Duration(60.0))

        # 检查缓冲区状态
        if self.main_buffer.can_transform("map", "base_link", rospy.Time()):
            print("Transform available in buffer")

        # 等待变换可用
        try:
            self.main_buffer.lookup_transform(
                "map", "base_link",
                rospy.Time(),
                timeout=rospy.Duration(1.0))
            print("Transform lookup successful")
        except:
            print("Transform lookup timeout")

    def async_transform_queries(self):
        """异步变换查询"""
        print("\n=== Async Transform Queries ===")

        # 注册回调
        def transform_callback(transform):
            print(f"Async transform received: "
                  f"({transform.transform.translation.x:.2f}, "
                  f"{transform.transform.translation.y:.2f})")

        # 异步查询(注意:这是概念演示,实际API可能不同)
        # future = self.main_buffer.lookup_transform_async(
        #     "map", "base_link", rospy.Time())
        # future.add_done_callback(transform_callback)

    def custom_interpolation(self):
        """自定义插值"""
        print("\n=== Custom Interpolation ===")

        try:
            # 获取两个时间点的变换
            time1 = rospy.Time.now() - rospy.Duration(1.0)
            time2 = rospy.Time.now()

            if (self.main_buffer.can_transform("odom", "base_link", time1) and
                self.main_buffer.can_transform("odom", "base_link", time2)):

                transform1 = self.main_buffer.lookup_transform(
                    "odom", "base_link", time1)
                transform2 = self.main_buffer.lookup_transform(
                    "odom", "base_link", time2)

                # 线性插值位置
                alpha = 0.5  # 插值系数
                interp_x = (1-alpha) * transform1.transform.translation.x + \
                          alpha * transform2.transform.translation.x
                interp_y = (1-alpha) * transform1.transform.translation.y + \
                          alpha * transform2.transform.translation.y

                print(f"Interpolated position: ({interp_x:.2f}, {interp_y:.2f})")

        except tf2_ros.TransformException as e:
            print(f"Interpolation failed: {e}")

    def frame_authority(self):
        """坐标系权威管理"""
        print("\n=== Frame Authority ===")

        # 检查变换的权威性
        try:
            transform = self.main_buffer.lookup_transform(
                "map", "base_link", rospy.Time())

            # 获取发布者信息
            authority = transform.header.frame_id
            child = transform.child_frame_id
            print(f"Transform authority: {authority} -> {child}")

            # 时间戳信息
            stamp = transform.header.stamp
            age = (rospy.Time.now() - stamp).to_sec()
            print(f"Transform age: {age:.3f} seconds")

        except tf2_ros.TransformException:
            pass

class TF2MessageFilter:
    """TF2消息过滤器"""

    def __init__(self):
        import message_filters
        from sensor_msgs.msg import PointCloud2

        self.tf_buffer = tf2_ros.Buffer()
        self.tf_listener = tf2_ros.TransformListener(self.tf_buffer)

        # 创建消息过滤器
        self.point_cloud_sub = message_filters.Subscriber(
            '/velodyne_points', PointCloud2)

        # TF2消息过滤器
        self.tf_filter = tf2_ros.MessageFilter(
            self.point_cloud_sub,
            self.tf_buffer,
            'map',  # 目标坐标系
            10,     # 队列大小
            rospy.Duration(0.1))  # 允许的时间差

        self.tf_filter.registerCallback(self.point_cloud_callback)

    def point_cloud_callback(self, msg):
        """点云回调(已经可以变换到map坐标系)"""
        rospy.loginfo(f"Received point cloud that can be transformed to map frame")

        try:
            # 此时保证可以进行变换
            transform = self.tf_buffer.lookup_transform(
                'map', msg.header.frame_id, msg.header.stamp)

            # 处理点云...
            rospy.loginfo("Processing point cloud in map frame")

        except tf2_ros.TransformException as e:
            rospy.logwarn(f"Transform error: {e}")

def main():
    features = TF2AdvancedFeatures()
    # filter = TF2MessageFilter()
    rospy.spin()

if __name__ == '__main__':
    main()

9. 实战案例:机器人坐标系统

9.1 完整的机器人TF系统

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import rospy
import tf2_ros
import tf
from geometry_msgs.msg import TransformStamped
from nav_msgs.msg import Odometry
from sensor_msgs.msg import JointState, LaserScan
from visualization_msgs.msg import MarkerArray, Marker
import numpy as np

class RobotTFSystem:
    """完整的机器人TF系统"""

    def __init__(self):
        rospy.init_node('robot_tf_system')

        # TF广播器
        self.static_broadcaster = tf2_ros.StaticTransformBroadcaster()
        self.dynamic_broadcaster = tf2_ros.TransformBroadcaster()

        # TF监听器
        self.tf_buffer = tf2_ros.Buffer()
        self.tf_listener = tf2_ros.TransformListener(self.tf_buffer)

        # 初始化机器人状态
        self.robot_x = 0.0
        self.robot_y = 0.0
        self.robot_theta = 0.0

        # 设置静态变换
        self.setup_static_transforms()

        # 订阅者
        self.odom_sub = rospy.Subscriber('/odom', Odometry, self.odom_callback)
        self.joint_sub = rospy.Subscriber('/joint_states', JointState, self.joint_callback)
        self.scan_sub = rospy.Subscriber('/scan', LaserScan, self.scan_callback)

        # 发布者
        self.marker_pub = rospy.Publisher('/tf_markers', MarkerArray, queue_size=10)

        # 定时器
        self.timer = rospy.Timer(rospy.Duration(0.1), self.timer_callback)

        rospy.loginfo("Robot TF System initialized")

    def setup_static_transforms(self):
        """设置静态坐标变换"""
        static_transforms = []

        # 机器人传感器配置
        sensor_configs = [
            # (parent, child, x, y, z, roll, pitch, yaw)
            ('base_link', 'laser_link', 0.2, 0.0, 0.1, 0, 0, 0),
            ('base_link', 'camera_link', 0.15, 0.0, 0.3, 0, -0.26, 0),
            ('base_link', 'imu_link', 0.0, 0.0, 0.05, 0, 0, 0),
            ('base_link', 'gps_link', -0.1, 0.0, 0.2, 0, 0, 0),
            # 轮子
            ('base_link', 'wheel_left_link', 0.0, 0.15, -0.05, 0, 0, 0),
            ('base_link', 'wheel_right_link', 0.0, -0.15, -0.05, 0, 0, 0),
            ('base_link', 'caster_link', -0.15, 0.0, -0.05, 0, 0, 0),
            # 机械臂基座
            ('base_link', 'arm_base_link', 0.1, 0.0, 0.1, 0, 0, 0),
        ]

        for config in sensor_configs:
            transform = self.create_static_transform(*config)
            static_transforms.append(transform)

        # 发布所有静态变换
        self.static_broadcaster.sendTransform(static_transforms)
        rospy.loginfo(f"Published {len(static_transforms)} static transforms")

    def create_static_transform(self, parent, child, x, y, z, roll, pitch, yaw):
        """创建静态变换"""
        transform = TransformStamped()

        transform.header.stamp = rospy.Time.now()
        transform.header.frame_id = parent
        transform.child_frame_id = child

        transform.transform.translation.x = x
        transform.transform.translation.y = y
        transform.transform.translation.z = z

        q = tf.transformations.quaternion_from_euler(roll, pitch, yaw)
        transform.transform.rotation.x = q[0]
        transform.transform.rotation.y = q[1]
        transform.transform.rotation.z = q[2]
        transform.transform.rotation.w = q[3]

        return transform

    def odom_callback(self, msg):
        """里程计回调 - 发布odom->base_link变换"""
        # 发布变换
        odom_trans = TransformStamped()
        odom_trans.header.stamp = msg.header.stamp
        odom_trans.header.frame_id = "odom"
        odom_trans.child_frame_id = "base_link"

        odom_trans.transform.translation.x = msg.pose.pose.position.x
        odom_trans.transform.translation.y = msg.pose.pose.position.y
        odom_trans.transform.translation.z = msg.pose.pose.position.z
        odom_trans.transform.rotation = msg.pose.pose.orientation

        self.dynamic_broadcaster.sendTransform(odom_trans)

        # 更新机器人位置
        self.robot_x = msg.pose.pose.position.x
        self.robot_y = msg.pose.pose.position.y

        q = (msg.pose.pose.orientation.x,
             msg.pose.pose.orientation.y,
             msg.pose.pose.orientation.z,
             msg.pose.pose.orientation.w)
        euler = tf.transformations.euler_from_quaternion(q)
        self.robot_theta = euler[2]

    def joint_callback(self, msg):
        """关节状态回调 - 发布机械臂关节变换"""
        joint_transforms = []

        for i, name in enumerate(msg.name):
            transform = None

            if name == "arm_joint_1":
                # 第一个关节(旋转)
                transform = self.create_joint_transform(
                    "arm_base_link", "arm_link_1",
                    0, 0, 0.1,
                    0, 0, msg.position[i]
                )
            elif name == "arm_joint_2":
                # 第二个关节(俯仰)
                transform = self.create_joint_transform(
                    "arm_link_1", "arm_link_2",
                    0.2, 0, 0,
                    0, msg.position[i], 0
                )
            elif name == "arm_joint_3":
                # 第三个关节
                transform = self.create_joint_transform(
                    "arm_link_2", "arm_link_3",
                    0.2, 0, 0,
                    0, msg.position[i], 0
                )
            elif name == "gripper_joint":
                # 夹爪(平移关节)
                transform = self.create_joint_transform(
                    "arm_link_3", "gripper_link",
                    0.1 + msg.position[i], 0, 0,
                    0, 0, 0
                )

            if transform:
                joint_transforms.append(transform)

        if joint_transforms:
            self.dynamic_broadcaster.sendTransform(joint_transforms)

    def create_joint_transform(self, parent, child, x, y, z, roll, pitch, yaw):
        """创建关节变换"""
        transform = TransformStamped()

        transform.header.stamp = rospy.Time.now()
        transform.header.frame_id = parent
        transform.child_frame_id = child

        transform.transform.translation.x = x
        transform.transform.translation.y = y
        transform.transform.translation.z = z

        q = tf.transformations.quaternion_from_euler(roll, pitch, yaw)
        transform.transform.rotation.x = q[0]
        transform.transform.rotation.y = q[1]
        transform.transform.rotation.z = q[2]
        transform.transform.rotation.w = q[3]

        return transform

    def scan_callback(self, msg):
        """激光扫描回调 - 演示坐标变换应用"""
        try:
            # 将激光点变换到map坐标系
            if self.tf_buffer.can_transform("map", msg.header.frame_id, msg.header.stamp):
                # 获取变换
                transform = self.tf_buffer.lookup_transform(
                    "map", msg.header.frame_id, msg.header.stamp)

                # 变换一些关键点(最近点、最远点等)
                min_range = min(r for r in msg.ranges if msg.range_min < r < msg.range_max)
                min_index = msg.ranges.index(min_range)
                min_angle = msg.angle_min + min_index * msg.angle_increment

                # 激光坐标系中的点
                laser_x = min_range * np.cos(min_angle)
                laser_y = min_range * np.sin(min_angle)

                # 这里可以应用变换...
                # (简化处理,实际需要完整的变换计算)

        except tf2_ros.TransformException:
            pass

    def timer_callback(self, event):
        """定时器回调"""
        # 发布一些动态变换
        self.publish_dynamic_transforms()

        # 发布可视化标记
        self.publish_tf_markers()

        # 执行TF查询示例
        self.perform_tf_queries()

    def publish_dynamic_transforms(self):
        """发布动态变换"""
        now = rospy.Time.now()

        # 1. 模拟map->odom的变换(SLAM或定位的输出)
        map_to_odom = TransformStamped()
        map_to_odom.header.stamp = now
        map_to_odom.header.frame_id = "map"
        map_to_odom.child_frame_id = "odom"

        # 简单的漂移模拟
        drift_x = 0.01 * np.sin(now.to_sec() * 0.1)
        drift_y = 0.01 * np.cos(now.to_sec() * 0.1)

        map_to_odom.transform.translation.x = drift_x
        map_to_odom.transform.translation.y = drift_y
        map_to_odom.transform.translation.z = 0.0

        q = tf.transformations.quaternion_from_euler(0, 0, 0)
        map_to_odom.transform.rotation.x = q[0]
        map_to_odom.transform.rotation.y = q[1]
        map_to_odom.transform.rotation.z = q[2]
        map_to_odom.transform.rotation.w = q[3]

        self.dynamic_broadcaster.sendTransform(map_to_odom)

        # 2. 旋转的传感器
        rotating_sensor = TransformStamped()
        rotating_sensor.header.stamp = now
        rotating_sensor.header.frame_id = "base_link"
        rotating_sensor.child_frame_id = "rotating_sensor"

        angle = (now.to_sec() % 10) * 2 * np.pi / 10

        rotating_sensor.transform.translation.x = 0.0
        rotating_sensor.transform.translation.y = 0.0
        rotating_sensor.transform.translation.z = 0.35

        q = tf.transformations.quaternion_from_euler(0, 0, angle)
        rotating_sensor.transform.rotation.x = q[0]
        rotating_sensor.transform.rotation.y = q[1]
        rotating_sensor.transform.rotation.z = q[2]
        rotating_sensor.transform.rotation.w = q[3]

        self.dynamic_broadcaster.sendTransform(rotating_sensor)

    def publish_tf_markers(self):
        """发布TF可视化标记"""
        marker_array = MarkerArray()

        # 获取所有坐标系
        try:
            # 创建坐标系标记
            frames = ['map', 'odom', 'base_link', 'laser_link', 'camera_link']

            for i, frame in enumerate(frames):
                if self.tf_buffer.can_transform("map", frame, rospy.Time()):
                    transform = self.tf_buffer.lookup_transform(
                        "map", frame, rospy.Time())

                    # 创建标记
                    marker = Marker()
                    marker.header.frame_id = "map"
                    marker.header.stamp = rospy.Time.now()
                    marker.id = i
                    marker.type = Marker.TEXT_VIEW_FACING
                    marker.action = Marker.ADD

                    marker.pose.position.x = transform.transform.translation.x
                    marker.pose.position.y = transform.transform.translation.y
                    marker.pose.position.z = transform.transform.translation.z + 0.1

                    marker.text = frame
                    marker.scale.z = 0.1

                    marker.color.a = 1.0
                    marker.color.r = 0.0
                    marker.color.g = 1.0
                    marker.color.b = 0.0

                    marker_array.markers.append(marker)

        except tf2_ros.TransformException:
            pass

        self.marker_pub.publish(marker_array)

    def perform_tf_queries(self):
        """执行TF查询示例"""
        try:
            # 查询机器人在地图中的位置
            if self.tf_buffer.can_transform("map", "base_link", rospy.Time()):
                transform = self.tf_buffer.lookup_transform(
                    "map", "base_link", rospy.Time())

                x = transform.transform.translation.x
                y = transform.transform.translation.y

                rospy.loginfo_throttle(5, f"Robot in map: ({x:.2f}, {y:.2f})")

        except tf2_ros.TransformException:
            pass

def main():
    system = RobotTFSystem()
    rospy.spin()

if __name__ == '__main__':
    main()

10. 总结与最佳实践

10.1 本文总结

通过本文的学习,你已经掌握了:

  1. TF系统原理:理解坐标变换的数学基础和ROS实现
  2. 静态变换:设置固定的坐标系关系
  3. 动态变换:处理时变的坐标系
  4. TF树管理:构建和维护坐标系树结构
  5. TF监听查询:获取任意坐标系间的变换
  6. TF工具使用:调试和可视化工具
  7. TF2新特性:缓冲区管理、异步查询等
  8. 实战应用:完整的机器人坐标系统

10.2 TF最佳实践

方面 建议 原因
命名规范 使用描述性名称,添加_link后缀 提高可读性
树结构 保持单一父节点,避免循环 确保计算效率
更新频率 静态变换用static_transform 减少网络流量
时间戳 使用消息的时间戳而非now() 保证时间一致性
坐标系选择 遵循REP-105标准 保证兼容性
错误处理 总是捕获TF异常 提高鲁棒性

10.3 常见问题解决

  1. "未来"时间戳错误

    • 问题:时间戳在未来
    • 解决:检查时钟同步,使用消息时间戳
  2. 变换不存在

    • 问题:无法查询变换
    • 解决:检查TF树完整性,增加等待时间
  3. TF树断裂

    • 问题:坐标系未连接
    • 解决:使用tf_monitor检查,补充缺失变换
  4. 性能问题

    • 问题:TF查询缓慢
    • 解决:减少查询频率,使用缓存

10.4 下一步学习

在下一篇文章中,我们将学习:

  • URDF机器人建模:从零构建机器人模型
  • 关节和连杆定义:机器人结构描述
  • 可视化和碰撞模型:3D模型集成
  • Gazebo仿真集成:物理仿真

版权声明:本文为原创文章,转载请注明出处

💡 学习建议:TF是ROS中最重要的概念之一,建议通过实践深入理解。从简单的静态变换开始,逐步学习动态变换和复杂的坐标系管理。使用RViz等可视化工具帮助理解坐标系关系。


下一篇预告:《【ROS1从入门到精通】第10篇:URDF机器人建模(从零构建机器人模型)》

敬请期待!

相关推荐
tap.AI2 小时前
(二)Stable Diffusion 3.5硬件准备与环境配置 —— 低配显卡也能跑大模型
人工智能·stable diffusion
LDG_AGI2 小时前
【推荐系统】深度学习训练框架(十七):TorchRec之KeyedJaggedTensor
人工智能·pytorch·深度学习·机器学习·数据挖掘·embedding
imooos2 小时前
使用小程序AI推理能力识别车牌号
人工智能·小程序
神州数码云基地2 小时前
首次开发陌生技术?用 AI 赋能前端提速开发!
前端·人工智能·开源·ai开发
weixin_446260852 小时前
用于构建和部署AI智能代理工作流的开源平台
人工智能
一招定胜负2 小时前
支持向量机
人工智能·机器学习·支持向量机
paopao_wu2 小时前
深度学习3:理解神经网络
人工智能·深度学习·神经网络
梦帮科技2 小时前
量子计算+AI:下一代智能的终极形态?(第二部分)
人工智能·机器学习·ai编程·量子计算
周杰伦_Jay2 小时前
【深度拆解智能体技术底层逻辑】从架构到实现的完整解析
人工智能·机器学习·架构·开源·论文·peai2026