ROS TF Tutorial

本文深入解析了ROS中的tf系统,详细介绍了tf的工作原理,包括坐标系之间的转换、tf树的构建以及如何通过tf进行时间同步。通过具体代码示例,展示了如何在ROS节点中实现坐标变换,并探讨了tf与传感器数据流的关系。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

跑不同的SLAM总会遇到frame transform的问题。

再加上我一直好奇odom信息为什么从/tf来而不是从/odom来。

于是就找了一下tf_tutorial。

 

理解可能有完全错误和不到位的地方。

 

frame 就是一个一个坐标系,他们之间的transform就是各个坐标系之间的转换。类似刚体rigid之间的转换。

一个机器人身上各个固定的sensor和机器人之间的transform是固定的,但是机器人/sensor参考世界坐标系的transform是实时在变化的。

所以/tf话题就是时刻在记录这种变化。

 

官方tf_tutorial来举例

键入rqt找到tf tree

可以看到两个frame turtle1 tuetle2各自参考父母frame world。

用tf tf_echo world turtle1 可以看到echo出这个transform是时刻在变化的。

 

打开node graph

可见我们两个node /turtle2_tf_broadcast 和/turtle1_tf_broadcaster 各自在subscribe topic /turtle2/pose and /turtle1/pose。

topic /turtleX/pose 的数据由node /sim发布。

同时我们的node /lister 在subsribe topic /tf, 而turtleX_tf_broadcast 节点在各自发布信息到/tf话题中。

最后node /listener 根据监/tf 得到的frame transform信息计算出对应的速度信息,发布到/tirtle2/cmd_vel话题,让node /sim接受过去。

完成整个乌龟2追乌龟1的功能。

 

turtle_tf_broadcaster.cpp

#include <ros/ros.h>
#include <tf/transform_broadcaster.h>
#include <turtlesim/Pose.h>

std::string turtle_name;

void poseCallback(const turtlesim::PoseConstPtr &msg)
{
  static tf::TransformBroadcaster br;
  tf::Transform transform;
  transform.setOrigin(tf::Vector3(msg->x, msg->y, 0.0));
  tf::Quaternion q;
  q.setRPY(0, 0, msg->theta);
  transform.setRotation(q);
  //parameters : transform information, time stamp, ref frame, local frame
  br.sendTransform(tf::StampedTransform(transform, ros::Time::now(), "world", turtle_name));
}

int main(int argc, char **argv)
{
  ros::init(argc, argv, "my_tf_broadcaster");
  if (argc != 2)
  {
    ROS_ERROR("need turtle name as argument");
    return -1;
  };
  turtle_name = argv[1]; //given by launch file

  ros::NodeHandle node;
  //subscribe to /turtleX/pose topic
  ros::Subscriber sub = node.subscribe(turtle_name + "/pose", 10, &poseCallback);

  ros::spin();
  return 0;
};

turtle_tf_listener.cpp

#include <ros/ros.h>
#include <tf/transform_listener.h>
#include <geometry_msgs/Twist.h>
#include <turtlesim/Spawn.h>

int main(int argc, char **argv)
{
  ros::init(argc, argv, "my_tf_listener");

  ros::NodeHandle node;

  //call spawn service to spawn second turtle
  ros::service::waitForService("spawn");
  ros::ServiceClient add_turtle =
      node.serviceClient<turtlesim::Spawn>("spawn");
  turtlesim::Spawn srv;
  add_turtle.call(srv);

  //set up this node to publish velocity information to topic /turtle2/cmd_vel
  ros::Publisher turtle_vel =
      node.advertise<geometry_msgs::Twist>("turtle2/cmd_vel", 10);

  
  tf::TransformListener listener;

  ros::Rate rate(10.0);
  while (node.ok())
  {
    tf::StampedTransform transform;
    try
    {
      //lookup transform infomration of from turtle2 ref to turtle1.
      //Seems the listener automatically listen to /tf?
      listener.waitForTransform("/turtle2", "/turtle1", ros::Time(0), ros::Duration(3.0));
      listener.lookupTransform("/turtle2", "/turtle1", ros::Time(0), transform);
    }
    catch (tf::TransformException &ex)
    {
      ROS_ERROR("%s", ex.what());
      ros::Duration(1.0).sleep();
      continue;
    }
    
    //publish velocity cmd to topic /turtle2/cmd_vel
    geometry_msgs::Twist vel_msg;
    vel_msg.angular.z = 4.0 * atan2(transform.getOrigin().y(),
                                    transform.getOrigin().x());
    vel_msg.linear.x = 0.5 * sqrt(pow(transform.getOrigin().x(), 2) +
                                  pow(transform.getOrigin().y(), 2));
    turtle_vel.publish(vel_msg);

    rate.sleep();
  }
  return 0;
};

 

所以Node和frame什么关系呢?好像完全没关系。

只是可以在node里面用tf namespace 下的某些类的实例直接去操作frame罢了。

下面的代码就是新建了一个node,不停地发布frame carrot1 ref to turtle1的trasnformation。

frame_tf_broadcaster.cpp

#include <ros/ros.h>
#include <tf/transform_broadcaster.h>

int main(int argc, char **argv)
{
    ros::init(argc, argv, "my_tf_broadcaster");
    ros::NodeHandle node;

    tf::TransformBroadcaster br;
    tf::Transform transform;

    ros::Rate rate(10.0);
    while (node.ok())
    {
        transform.setOrigin(tf::Vector3(0.0, 2.0, 0.0));
        transform.setRotation(tf::Quaternion(0, 0, 0, 1));
        //send a transform to /tf, ref frame turtl1, local frame carrot1
        br.sendTransform(tf::StampedTransform(transform, ros::Time::now(), "turtle1", "carrot1"));
        rate.sleep();
    }
}

 

tf_tree可以看到多了turtle1多了一个child frame

然后tutorial介绍tf和时间的关系。

因为transform时刻都在变化(哪怕变化量为0),/tf时刻在记录被broadcaster发布出来的transform。好像是记录最近10秒。

所以当我们请求某两个frame之间的变化的时候,我们可以请求某个特定时间的transform。(Nice,做融合,后验都要这个)

要看到这个特性,可以用

      ros::Time now = ros::Time::now();
      //wait for transform from frame turtle2 ref to turtle1, at time "now", wait for up to 3 second
      listener.waitForTransform("/turtle2", "/turtle1", now, ros::Duration(3.0));
      listener.lookupTransform("/turtle2", "/turtle1", now, transform);

但是一般用time(0)获取buffer里最后一个transform会比较好,毕竟now时刻的transformation不一定已经写入了buffer。

 

随后教程给了一个,如果要让turtle2去追五秒前的turtle1,要怎么做。

ros::Time past = ros::Time::now() - ros::Duration(5.0);
listener.waitForTransform("/turtle2", "/turtle1", past, ros::Duration(1.0));
listener.lookupTransform("/turtle2", "/turtle1", past, transform);

如果是上述代码,我们只能拿到五秒前turtle2相对于turtle1的关系。并不是现在的turtle2相对于五秒前turtle1的关系。

 

实际应该这么用,给指定的frame带上时间戳,同时指定一个不随着时间变化的frame作为参考。

      ros::Time now = ros::Time::now();
      ros::Time past = now - ros::Duration(5.0);
      listener.waitForTransform("/turtle2", now,
                                "/turtle1", past,
                                "/world", ros::Duration(1.0));
      listener.lookupTransform("/turtle2", now,
                               "/turtle1", past,
                               "/world", transform);

实际运行过程中可能会有

[ERROR] [1583952187.300482857]: Lookup would require extrapolation into the past.  Requested time 1583952181.298486551 but the earliest data is at time 1583952186.696968043, when looking up transform from frame [turtle1] to frame [world]

因为系统刚开始前五秒的时候,是找不到5秒前的turtlr1的transform信息的。

这张图解释了实际我们用的是5秒前turtle1 ref to world和当前turtle2 ref to world 来计算当前turtle2 ref to turtle1 (5sec ago)的。

 

 

 

 

 

 

 

to read 

navigation_tutorials, 里面包含laser_scan_publisher_tutorial和odometry_publisher_tutorial

http://wiki.ros.org/navigation_tutorials 

Setting up your robot using tf, 结尾提到的Publishing Sensor Streams Over ROS

http://wiki.ros.org/navigation/Tutorials/RobotSetup/TF

Setting up your robot with tf 5.2 robot state publisher 和 5.3 urdf with robot_state_publisher

机器人操作系统 ROS(机器人操作系统,Robot Operating System),是专为机器人软件开发所设计出来的一套电脑操作系统架构。它是一个开源的元级操作系统(后操作系统),提供类似于操作系统的服务,包括硬件抽象描述、底层驱动程序管理、共用功能的执行、程序间消息传递、程序发行包管理,它也提供一些工具和库用于获取、建立、编写和执行多机融合的程序。 ROS的运行架构是一种使用ROS通信模块实现模块间P2P的松耦合的网络连接的处理架构,它执行若干种类型的通讯,包括基于服务的同步RPC(远程过程调用)通讯、基于Topic的异步数据流通讯,还有参数服务器上的数据存储。 1 发展目标 2 ROS的概念 2.1 ROS 的 Filesystem Level 2.2 ROS 的 Computation Graph Level 3 参考文献 4 外部链接 发展目标 ROS的首要设计目标是在机器人研发领域提高代码复用率。ROS是一种分布式处理框架(又名Nodes)。这使可执行文件能被单独设计,并且在运行时松散耦合。这些过程可以封装到数据包(Packages)和堆栈(Stacks)中,以便于共享和分发。ROS还支持代码库的联合系统。使得协作亦能被分发。这种从文件系统级别到社区一级的设计让独立地决定发展和实施工作成为可能。上述所有功能都能由ROS的基础工具实现。 为了实现“共享与协作”这一首要目标,人们制订了ROS架构中的其他支援性目标: “轻便”:ROS是设计得尽可能方便简易。您不必替换主框架与系统,因为ROS编写的代码可以用于其他机器人软件框架中。毫无疑问的,ROS更易于集成与其他机器人软件框架。事实上ROS已完成与OpenRAVE、Orocos和Player的整合。 ROS-agnostic库:【agnostic:不可知论】建议的开发模型是使用clear的函数接口书写ROS-agnostic库。 语言独立性:ROS框架很容易在任何编程语言中执行。我们已经能在Python和C++中顺利运行,同时添加有Lisp、Octave和Java语言库。 测试简单:ROS有一个内建的单元/组合集测试框架,称为“rostest”。这使得集成调试和分解调试很容易。 扩展性:ROS适合于大型实时系统与大型的系统开发项目。 ROS的概念 ROS有三个层次的概念:分别为Filesystem level,Computation graph level, 以及Communication level。 以下内容具体的总结了这些层次及概念。除了这三个层次的概念, ROS也定义了两种名称-- Package资源名称和Graph资源名称。同样会在以下内容中提及。 ROS 的 Filesystem Level 文件系统层概念就是你在碟片里面遇到的资源,例如: Packages:ROS的基本组织,可以包含任意格式文件。一个Package 可以包含ROS执行时处理的文件(nodes),一个ROS的依赖库,一个数据集合,配置文件或一些有用的文件在一起。 Manifests:Manifests (manifest.xml) 提供关于Package元数据,包括它的许可信息和Package之间依赖关系,以及语言特性信息像编译旗帜(编译优化参数)。 Stacks: Stacks 是Packages的集合,它提供一个完整的功能,像“navigation stack” Stack与版本号关联,同时也是如何发行ROS软件方式的关键。 Manifest Stack Manifests: Stack manifests (stack.xml) 提供关于Stack元数据,包括它的许可信息和Stack之间依赖关系。 Message (msg) types: 信息描述, 位置在路径:my_package/msg/MyMessageType.msg, 定义数据类型在ROS的 messages ROS里面。 Service (srv) types: 服务描述,位置在路径:my_package/srv/MyServiceType.srv, 定义这个请求和相应的数据结构 在ROS services 里面。 ROS 的 Computation Graph Level Computation Graph Level(计算图)就是用ROS的P2P(peer-to-peer网络传输协议)网络集中处理所有的数据。基本的Computation Graph的概念包括Node, Master, Parameter Sever,messages, services, topics, 和bags, 以上所有的这些都以不同的方式给Graph传输数据。 Nodes: Nodes(节点)是一系列运行中的程序。ROS被设计成在一定颗粒度下的模块化系统。一个机器人控制系统通常包含许多Nodes。比如一个Node控制激光雷达,一个Node控制车轮马达,一个Node处理定位,一个Node执行路径规划,另外一个提供图形化界面等等。一个ROS节点是由Libraries ROS client library写成的, 例如 roscpp 和 rospy. Master: ROS Master 提供了登记列表和对其他计算图的查找。没有Master,节点将无法找到其他节点,交换消息或调用服务。 Server Parameter Server: 参数服务器使数据按照钥匙的方式存储。目前,参数服务器是主持的组成部分。 Messages:节点之间通过messages来传递消息。一个message是一个简单的数据结构,包含一些归类定义的区。支持标准的原始数据类型(整数、浮点数、布尔数,等)和原始数组类型。message可以包含任意的嵌套结构和数组(很类似于C语言的结构structs) Topics: Messages以一种发布/订阅的方式传递。一个node可以在一个给定的topic中发布消息。Topic是一个name被用于描述消息内容。一个node针对某个topic关注与订阅特定类型的数据。可能同时有多个node发布或者订阅同一个topic的消息;也可能有一个topic同时发布或订阅多个topic。总体上,发布者和订阅者不了解彼此的存在。主要的概念在于将信息的发布者和需求者解耦、分离。逻辑上,topic可以看作是一个严格规范化的消息bus。每个bus有一个名字,每个node都可以连接到bus发送和接受符合标准类型的消息。 Services:发布/订阅模型是很灵活的通讯模式,但是多对多,单向传输对于分布式系统中经常需要的“请求/回应”式的交互来说并不合适。因此,“请求/回应” 是通过services来实现的。这种通讯的定义是一种成对的消息:一个用于请求,一个用于回应。假设一个节点提供了一个服务提供下一个name和客户使用服务发送请求消息并等待答复。ROS的客户库通常以一种远程调用的方式提供这样的交互。 Bags: Bags是一种格式,用于存储和播放ROS消息。对于储存数据来说Bags是一种很重要的机制。例如传感器数据很难收集但却是开发与测试中必须的。 在ROS的计算图中,ROS的Master以一个name service的方式工作。它给ROS的节点存储了topics和service的注册信息。Nodes 与Master通信从而报告它们的注册信息。当这些节点与master通信的时候,它们可以接收关于其他以注册节点的信息并且建立与其它以注册节点之间的联系。当这些注册信息改变时Master也会回馈这些节点,同时允许节点动态创建与新节点之间的连接。 节点之间的连接是直接的; Master仅仅提供了查询信息,就像一个DNS服务器。节点订阅一个topic将会要求建立一个与发布该topics的节点的连接,并且将会在同意连接协议的基础上建立该连接。ROS里面使用最广的连接协议是TCPROS,这个协议使用标准的TCP/IP 接口。 这样的架构允许脱钩工作(decoupled operation),通过这种方式大型或是更为复杂的系统得以建立,其中names方式是一种行之有效的手段。names方式在ROS系统中扮演极为重要的角色: topics, services, and parameters 都有各自的names。每一个ROS客户端库都支持重命名,这等同于,每一个编译成功的程序能够以另一种形似【名字】运行。 例如,为了控制一个北阳激光测距仪(Hokuyo laser range-finder),我们可以启动这个hokuyo_node 驱动,这个驱动可以给与激光仪进行对话并且在"扫描"topic下可以发布sensor_msgs/LaserScan 的信息。为了处理数据,我们也许会写一个使用laser_filters的node来订阅"扫描"topic的信息。订阅之后,我们的过滤器将会自动开始接收激光仪的信息。 注意两边是如何脱钩工作的。 所有的hokuyo_node的节点都会完成发布"扫描",不需要知道是否有节点被订阅了。所有的过滤器都会完成"扫描"的订阅,不论知道还是不知道是否有节点在发布"扫描"。 在不引发任何错误的情况下,这两个nodes可以任何的顺序启动,终止,或者重启。 以后我们也许会给我们的机器人加入另外一个激光器,这会导致我们重新设置我们的系统。我们所需要做的就是重新映射已经使用过的names。当我们开始我们的第一个hokuyo_node时,我们可以说它用base_scan代替了映射扫描,并且和我们的过滤器节点做相同的事。现在,这些节点将会用base_scan的topic来通信从而代替,并且将不再监听"扫描"topic的信息。然后我们就可以为我们的新激光测距仪启动另外一个hokuyo_node。 参考文献 http://www.ros.org/wiki/ros http://bbs.axnzero.com/index.php http://blog.sina.com.cn/digital2image2processing
### 关于Fish ROS RViz Tutorial 或 Feature 的探讨 在当前提供的引用中,并未提及与 `fish` 功能或者具体针对 `ROS` 和 `RViz` 教程的相关内容。然而,可以基于已知的 ROS 和 RViz 基础知识来构建可能的方向。 #### 1. **关于 ROS 中 Fish 功能的可能性** 在机器人操作系统(ROS)领域,“fish” 并不是一个标准的功能模块或包名称[^3]。如果存在特定的 “fish” 功能,则可能是某个社区开发的自定义包或者是模拟鱼类行为的研究项目的一部分。通常情况下,在 ROS 社区中寻找此类功能可以通过以下方式实现: - 使用 `rospack find` 查找本地安装的 ROS 包。 - 访问 [ROS Index](http://rosindex.github.io/) 进行全局搜索。 例如,假设有一个名为 `fish_simulator` 的包,其结构可能会如下所示: ```bash $ roscd fish_simulator/ $ ls launch/ scripts/ package.xml CMakeLists.txt ``` #### 2. **RViz 配置中的 Fish 表现形式** RViz 是 ROS 提供的强大可视化工具,用于显示传感器数据、路径规划以及仿真环境等内容。要展示类似于“fish”的对象,通常会涉及以下几个方面: - 创建自定义 Marker 类型以表示鱼的行为和外观[^4]。 - 利用 URDF 文件描述鱼的身体结构并加载到 Gazebo 或其他仿真环境中。 - 编写节点发布鱼的位置和姿态信息至 `/tf` 主题以便 RViz 可视化。 下面是一个简单的 Python 节点示例,演示如何向 RViz 发送标记消息: ```python import rospy from visualization_msgs.msg import Marker def publish_fish_marker(): rospy.init_node('fish_publisher', anonymous=True) marker_pub = rospy.Publisher('/visualization_marker', Marker, queue_size=10) rate = rospy.Rate(10) # Hz while not rospy.is_shutdown(): marker = Marker() marker.header.frame_id = "map" marker.type = marker.SPHERE marker.action = marker.ADD marker.scale.x = 0.5 marker.scale.y = 0.5 marker.scale.z = 0.5 marker.color.a = 1.0 marker.color.r = 0.0 marker.color.g = 1.0 marker.color.b = 0.0 marker.pose.position.x = 1.0 marker.pose.position.y = 2.0 marker.pose.position.z = 0.0 marker_pub.publish(marker) rate.sleep() if __name__ == '__main__': try: publish_fish_marker() except rospy.ROSInterruptException: pass ``` 此脚本创建了一个绿色球体作为鱼的简单模型,并将其放置在一个固定位置上[^5]。 #### 3. **教程资源推荐** 对于希望深入了解 ROS 和 RViz 结合使用的开发者来说,建议参考官方文档以及其他高质量的学习材料: - 官方 ROS Tutorials 页面提供了丰富的入门指南[^6]。 - YouTube 上有许多专注于不同主题的实际操作视频系列可供学习。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值