——Franka机器人渠道、市场、销售负责
——学术背景来自于哈尔滨工业大学、多伦多大学
全国咨询热线:
400 188 3810
Franka机器人专线
18018175378(同微信)

PNP机器人ROS专题系列之2-ROS2基础入门:TF坐标系管理与可视化工具

发表时间:2025-08-17 21:21作者:禾可1228
PNP机器人专注具身智能方向, 是Franka战略合作伙伴。

德国慕尼黑—PNP机器人成为Franka Robotics卓越战略伙伴,推动具身智能机器人全球生态

具身智能方向:具身智能技术群

Franka机器人:具身智能方案/渠道/商务咨询


提要:ROS开源生态庞大,社区活跃,驱动、算法、可视化工具即装即用,大幅降低科研门槛;FRANKA机器人、KINOVA等主流机型原生支持ROS/ROS2,接口统一,实验成果可无缝迁移。PNP机器人更新ROS系列教程,从节点通信、MoveIt!到Gazebo仿真,全流程讲解,并附实验室级实例源码,助力机器人、AI、自动化等方向学者快速复现顶会算法,加速论文与原型落地,敬请关注后续深度篇章!

牢骚:博主的习惯就是“能省就省”,所以出现过一次的全称,且后续上下文没有歧义项的时候,此名称第二次出现就是省略的简写模式,比如:Gazebo -> GZBWorkSpace -> WSPackage -> PKGPython -> Py官方文档 -> 官文官方网站 -> 官网官方Git仓库 -> 官仓等等,但是仅仅局限于讲解/讲道理的文本区间,其余的比如具体代码等等则仍然采用真实名称,可放心CV。且除了及其基础的篇章或者使用方式差异较大的接口会特殊提及外,像Py:str(xxx.abc)/Cpp:xxx->abc.c_str()这种差异的代码一般只会使用最简单或者打字最少的语言单次描述。(原文链接:https://www.eeworld.com.cn/ar5qrb1

牢骚:同义词会使用XX/XX这种形式,有助于阅读和理解和“偷换概念(bushi”,比如:张量/向量松弛操作/短路径数值迭代/Relaxtion等等。

介绍:ROS2只是一个管理不同节点-Node及其数据传输的系统,面对基础的图像、音频、温湿度、蓝牙等等数据使用基础的消息传递机制如:话题-Topic即可实现。但是面对更复杂的场景,比如坐标系管理,代码新手使用基础的消息传递机制管理机器人的各项坐标就会变得比较困难。这时就可以使用官方推荐的TF2包来管理你的坐标系关系。

介绍:TF2会生成与维护一个坐标树,在这里所有的坐标系都是通过描述坐标之间的关系来构建的。比如:"B坐标系位于A坐标系X=10处,C坐标系位于B坐标系Y=2处,D坐标系位于A坐标系X=-10处"这句话就描述/构建了一个坐标树。当其中某个坐标系位置发生改变,但是基于此坐标系的其余坐标系的相对位置未发生改变,则只需要在缓存更新此坐标系的相对位置即可,不需要更新每一个坐标系的位置。比如B位于A的X=10处,即使A坐标系改变,B仍然位于A的X=10处,无需改变缓存内B相对于A的位置关系/广播。

介绍:一个三维世界的刚体,使用位置/偏移-translation角度-rotation就能描述其位姿(位置+姿态)-Pose,其相对于旋转轴的偏转角度/欧拉角(x y z)常用四元数来表示[w x y z],工具库可以直接使用tf_transformations.quaternion_from_euler(x, y, z)来获得四元数,同理也有相应的函数将四元数转换为欧拉角。

抽象:可以将TF坐标树当作一个绘制地图的绘图员,TF广播器就是一个雷达兵,雷达兵随时上报位置,TF监听器就是一个战略家,随时问绘图员:在XX时间的时候雷达兵在哪?

  1. graph TD
  2.     A[A坐标系] -->|X=10| B[B坐标系]
  3.     B -->|Y=2| C[C坐标系]
  4.     A -->|X=-10| D[D坐标系]

  5. # 万一不支持Mermaid示意图,会看到这些注释
  6. # A (0,0)
  7. # |
  8. # |-- B (10,0)
  9. # |   |
  10. # |   `-- C (10,2)
  11. # |
  12. # `-- D (-10,0)

之后的坐标例子默认使用此关系,直到“海龟跟随示例”小节,旋转矩阵、四元数、增广变换矩阵等将不赘述!其中旋转角度将会直接使用四元数[w x y z]表示:

  1. q=[w,v]=[w,[x,y,z]]=w+xi+yj+zk


广播坐标使用


TF使用“广播”坐标来维护坐标树,每个坐标系拥有唯一的名称对应,节点若想获取自己某个时间点相对于某个坐标系的坐标可以使用坐标Buffer以及监听器进行坐标转化。思路类似于:

C自身为原点的坐标系为坐标系C
C要相对于坐标系B运动,C现在在B中的坐标偏移是(2, 3, 0),旋转偏移为(1, 0, 0, 0)
那么可以将此坐标变化关系广播出去,这样子其他节点就可以定位到坐标系C相对于其他坐标系的位置

注意:TF只管根据广播的坐标系关系维护坐标系树,以便于其他节点随时通过监听器获取位置和姿态
其中位置控制还得靠自己对应的功能模块!比如你即使广播B在A的X=233处,但是你实际上没有让海龟Move(X=233),那么你就仅仅是谎报坐标了而已!一般情况下,携带里程计的机器人模块会有里程计回调自动更新里程计坐标系相对于世界/地图坐标系的偏移。


TF2的坐标广播与获取/监听:

  1. from tf2_ros import TransformBroadcaster, StaticTransformBroadcaster
  2. from geometry_msgs.msg import TransformStamped

  3. from tf2_ros import TransformException, Buffer, TransformListener

  4. """
  5. TransformStamped: 位姿信息+坐标系信息(源坐标系名+目标坐标系名)+时间戳,表O在时间N有位姿X
  6. TransformBroadcaster: 动态广播器
  7. StaticTransformBroadcaster: 静态广播器

  8. 对于静态广播相对于动态广播的区别:
  9. - 类似于Python的Tuple相对于List:
  10. - 如果数据固定不更新为啥不使用开销小且符合语义且安全的元组?
  11. - 且使用方式一致,参数一致,效果(若数据不会变化的情况下)一致

  12. TransformListener:监听所有的坐标系变化,并存于一个Buffer

  13. 下面是描述位置关系的例子:
  14. """
  15. classNode1(Node):

  16. def__init__(self, node_name):
  17.         super.__init__(node_name)

  18. # 定义TF广播器,此广播器与当前对象生命周期绑定
  19.         self.tf_broadcaster = TransformBroadcaster(self)

  20. # 绘制初始/上述的ABCD坐标系位置关系
  21.         self.tf_broadcaster.sendTransform(B)    # BCD是什么之后讲
  22.         self.tf_broadcaster.sendTransform(C)    # 这里为了梳理逻辑就先省略了
  23.         self.tf_broadcaster.sendTransform(D)    # 就当BCD是TransformStamped就好

  24. # 此时TF的坐标树已经更新了对应的位置,可以使用监听器获取他们的坐标关系:
  25. # 创建监听器,监听器负责将监听到的变换存到Buffer
  26.         self.tf_buffer = Buffer()
  27.         self.tf_listener = TransformListener(self.tf_buffer, self)

  28. # 获取 `当前时间` 下的相对于 `坐标系B原点` 的 `坐标系A` 所在的位姿关系
  29.         now = rclpy.time.Time()
  30. try:
  31.             trans = self.tf_buffer.lookup_transform(
  32. "B", "A", now
  33.             )
  34. except TransformException as ex:
  35.             self.get_logger().info(f'无法获取变换: {ex},可能是坐标系名称错误')
  36. else:
  37.             self.get_logger().info(f'此时位姿关系为:{trans}')

  38. # 现在可以设置一个位于坐标系A的(0 0 0)点,查看其在坐标系B中的位置:
  39. from geometry_msgs.msg import PointStamped
  40. from tf2_geometry_msgs import do_transform_point

  41.         point = PointStamped()
  42.         point.header.frame_id = 'A'
  43.         point.point.x = 0.0;point.point.y = 0.0;point.point.z = 0.0
  44.         self.get_logger().info(f'在坐标系B中,点的坐标为:{
  45.             do_transform_point(point, trans)
  46.         }')

  47. ---
  48. # OK,主要内容讲完,那么来看看BCD都是什么,以B举例子:
  49. B = TransformStamped()
  50. B.header.stamp = self.get_clock().now().to_msg()
  51. B.header.frame_id = 'A'# 定义B坐标系相对于A坐标系的偏移
  52. B.child_frame_id =  'B'# 即:在A坐标系看,B位于:(10, 0, 0)-(0,0,0,0)
  53. # 由于此时提到了坐标系A和B,那么TF就会记录此时含有两个坐标系A和B
  54. B.transform.translation.x = 10.0
  55. B.transform.translation.y = 0.0
  56. B.transform.translation.z = 0.0
  57. B = self.euler_to_quaternion(0, 0, pose.theta) # 将欧拉角转换为四元数
  58. B.transform.rotation.x = q[0]
  59. B.transform.rotation.y = q[1]
  60. B.transform.rotation.z = q[2]
  61. B.transform.rotation.w = q[3]
  62. # 这里就是上面的广播坐标关系,在广播后,TF就会把两个坐标关系计入缓存/内存
  63. self.tf_broadcaster.sendTransform(B)
  64. # C和D同理,无非一个是CB关系,一个是DA关系,改改上面的数字即可……


海龟跟随示例



前情提要:你安装了ROS2,且通过键盘上下左右控制代码创建生产者节点发布话题到'/<turtle_name>/cmd_vel'命令行发布话题使用-rate控制频率ros2 bag等等手段尝试了调教小海龟。所以上述内容不再赘述。

前情提要:你会创建工作空间、会编译以及启动自己的节点,会使用Launch批量启动节点。所以上述内容不再赘述。

前情提要:除了命令行的-rate外,你会使用Node::create_timer创建定时器来定时触发hello消息的发布。所以上述内容不再赘述。

前情提要:你会创建TF的广播器,当海龟1运动的时候,你能够创建一个节点订阅海龟的位置,从而使用TF广播器将海龟坐标广播/公布/更新到TF坐标树。所以上述内容不再赘述。

前情提要:你会创建TF的监听器,你可以使用定时器不断获取海龟1的位置。所以上述内容不再赘述。

所以现在,创建一个“海龟跟随示例”还缺什么?答:初中物理,计算距离和速度,当知道海龟1海龟2当前的坐标,若想海龟2在N秒内到达海龟1的位置需要哪个方向需要多少速度,并作为话题发布到'/<turtle_name>/cmd_vel'!过于简单,所以上述内容不再赘述。

  1. # 创建工作空间与包
  2. mkdir -p heke_example/src
  3. cd heke_example/src

  4. # 创建包,安装对应依赖
  5. ros2 pkg create --build-type ament_python turtle2node \
  6. --dependencies rclpy geometry_msgs turtlesim tf2_ros tf2_geometry_msgs

  7. # 新建并编辑turtle2node.py:
  8. vim turtle2node/turtle2node.py
  1. # 编辑之后,将此文件加入setup.py编译指导,后编译,后依次启动节点:
  2. ...
  3.     entry_points={
  4. 'console_scripts': [
  5. 'heke = turtle2node.turtle2node:main',
  6.         ],
  7.     },
  8. ...
  9. colcon build --packages-select turtle2node

至于源代码:

  1. import rclpy
  2. from rclpy.node import Node
  3. from geometry_msgs.msg import Twist, PointStamped, TransformStamped
  4. from turtlesim.msg import Pose
  5. from tf2_ros import TransformException, Buffer, TransformListener, TransformBroadcaster
  6. from tf2_geometry_msgs import do_transform_point
  7. from turtlesim.srv import Spawn
  8. import math

  9. classTurtleTfFollower(Node):
  10. def__init__(self):
  11. super().__init__('turtle_tf_follower')

  12. # 创建TF2缓冲区和监听器
  13. self.tf_buffer = Buffer()
  14. self.tf_listener = TransformListener(self.tf_buffer, self)

  15. # 创建TF广播器,用于发布turtle1和turtle2的TF变换
  16. self.tf_broadcaster = TransformBroadcaster(self)

  17. # 创建发布器,用于发布控制turtle2速度的消息
  18. self.publisher = self.create_publisher(Twist, '/turtle2/cmd_vel', 10)

  19. # 订阅turtle1和turtle2的位姿
  20. self.turtle1_pose_sub = self.create_subscription(Pose, '/turtle1/pose', self.turtle1_pose_callback, 10)
  21. self.turtle2_pose_sub = self.create_subscription(Pose, '/turtle2/pose', self.turtle2_pose_callback, 10)

  22. # 初始化turtle1和turtle2的位姿
  23. self.turtle1_pose = Pose()
  24. self.turtle2_pose = Pose()

  25. # 创建定时器,定期执行跟随逻辑
  26. self.timer = self.create_timer(0.1, self.follow_turtle)

  27. # 生成第二个海龟
  28. self.spawn_turtle2()

  29. defspawn_turtle2(self):
  30. """生成第二个海龟(turtle2)"""
  31. self.get_logger().info('正在生成turtle2...')

  32. # 创建客户端,调用/spawn服务生成第二个海龟
  33. self.spawn_client = self.create_client(Spawn, '/spawn')
  34. whilenotself.spawn_client.wait_for_service(timeout_sec=1.0):
  35. self.get_logger().info('等待/spawn服务...')

  36. # 设置生成海龟的请求参数
  37.         request = Spawn.Request()
  38.         request.x = 5.0# 初始x坐标
  39.         request.y = 5.0# 初始y坐标
  40.         request.theta = 0.0# 初始角度
  41.         request.name = 'turtle2'# 海龟名称

  42. # 异步调用服务
  43.         future = self.spawn_client.call_async(request)
  44.         future.add_done_callback(self.spawn_callback)

  45. defspawn_callback(self, future):
  46. """生成海龟的回调函数"""
  47. try:
  48.             response = future.result()
  49. if response.name == 'turtle2':
  50. self.get_logger().info('成功生成turtle2')
  51. exceptExceptionas e:
  52. self.get_logger().error(f'生成turtle2失败: {e}')

  53. defturtle1_pose_callback(self, msg):
  54. """turtle1位姿回调函数"""
  55. self.turtle1_pose = msg
  56. self.publish_tf('turtle1', self.turtle1_pose)

  57. defturtle2_pose_callback(self, msg):
  58. """turtle2位姿回调函数"""
  59. self.turtle2_pose = msg
  60. self.publish_tf('turtle2', self.turtle2_pose)

  61. defpublish_tf(self, turtle_name, pose):
  62. """发布turtle的TF变换"""
  63.         t = TransformStamped()
  64.         t.header.stamp = self.get_clock().now().to_msg()
  65.         t.header.frame_id = 'world'
  66.         t.child_frame_id = turtle_name
  67.         t.transform.translation.x = pose.x
  68.         t.transform.translation.y = pose.y
  69.         t.transform.translation.z = 0.0

  70. # 将欧拉角转换为四元数
  71.         q = self.euler_to_quaternion(0, 0, pose.theta)
  72.         t.transform.rotation.x = q[0]
  73.         t.transform.rotation.y = q[1]
  74.         t.transform.rotation.z = q[2]
  75.         t.transform.rotation.w = q[3]

  76. # 发布TF变换
  77. self.tf_broadcaster.sendTransform(t)

  78. defeuler_to_quaternion(self, roll, pitch, yaw):
  79. """将欧拉角转换为四元数"""
  80.         cy = math.cos(yaw * 0.5)
  81.         sy = math.sin(yaw * 0.5)
  82.         cp = math.cos(pitch * 0.5)
  83.         sp = math.sin(pitch * 0.5)
  84.         cr = math.cos(roll * 0.5)
  85.         sr = math.sin(roll * 0.5)

  86.         q = [0] * 4
  87. q[0] = sr * cp * cy - cr * sp * sy
  88. q[1] = cr * sp * cy + sr * cp * sy
  89. q[2] = cr * cp * sy - sr * sp * cy
  90. q[3] = cr * cp * cy + sr * sp * sy
  91. return q

  92. deffollow_turtle(self):
  93. """实现turtle2跟随turtle1的逻辑"""
  94. try:
  95. # 获取从turtle2到turtle1的变换
  96.             transform = self.tf_buffer.lookup_transform(
  97. 'turtle2',  # 目标坐标系
  98. 'turtle1',  # 源坐标系
  99.                 rclpy.time.Time(),  # 获取最新的变换
  100.                 timeout=rclpy.duration.Duration(seconds=1.0))

  101. # 创建一个PointStamped消息,表示turtle1的位置
  102.             point = PointStamped()
  103.             point.header.frame_id = 'turtle1'
  104.             point.point.x = 0.0
  105.             point.point.y = 0.0
  106.             point.point.z = 0.0

  107. # 将turtle1的位置转换到turtle2的坐标系中
  108.             transformed_point = do_transform_point(point, transform)

  109. # 计算turtle2需要移动的速度
  110.             cmd_vel = Twist()
  111.             cmd_vel.linear.x = 0.5 * transformed_point.point.x  # 线性速度
  112.             cmd_vel.angular.z = 4.0 * transformed_point.point.y  # 角速度

  113. # 发布速度命令
  114. self.publisher.publish(cmd_vel)

  115. except TransformException as ex:
  116. self.get_logger().info(f'无法获取变换: {ex}')

  117. defmain(args=None):
  118.     rclpy.init(args=args)
  119.     node = TurtleTfFollower()
  120.     rclpy.spin(node)
  121.     node.destroy_node()
  122.     rclpy.shutdown()

  123. if __name__ == '__main__':
  124.     main()

之后的启动节点(可自行编写Launch文件):

  1. # 海龟1:
  2. ros2 run turtlesim turtlesim_node
  3. # 海龟1的键盘控制
  4. ros2 run turtlesim turtle_teleop_key
  5. # 海龟2,别忘记启用ROS2工作空间/install/setup.bash的环境!
  6. ros2 run turtle2node heke

效果展示:

查看坐标系关系:

TF2命令行工具:

  1. ros2 run tf2_tools view_frames        # 查看TF树,生成的.gv和.pdf自行找工具查看!
  2. ros2 run tf2_ros tf2_echo <target_frame_id> <source_frame_id>  # 查看坐标系关系


可视化坐标系



啊,受够了干巴巴的PDF坐标系指示,就没有直接一点纵览全局的坐标系展示方案?RViz:😀。

吐槽:本来想一次性讲完GRR(见开头src部分)三幻神的,果然还是放到下一篇比较好……

  1. ros2 run rviz2 rviz2

弱弱提示:RViz可以点击左下角的Add按钮添加显示类型,添加两个坐标轴之后,设置引用的坐标系为对应的海龟即可。另外记得把全局坐标系设置为world

之后在通过键盘控制第一只海归就能看到两个坐标轴追着跑了!当跑完还想下次一键展示,则可以保存配置为.rviz文件,下次启动的时候直接ros2 run rviz2 rviz2 -d xxx.rviz即可加载配置!们面对面交流经验


<<<  END >>>



FRANKA机器人因其高精度力控与开放式架构,在学术界广泛应用,成为具身智能与操作研究的主流平台。斯坦福、伯克利、CMU等顶尖实验室构建了丰富的软件生态,ROS、MoveIt 等工具无缝集成,使其在 ICRA、IROS、RSS 等顶会中成为最佳论文常用的验证平台。PNP机器人作为 FRANKA 在国内的官方合作伙伴,负责其技术支持、渠道建设与销售,并基于 FRANKA 开发了一系列生态工具,如遥操作、视觉、移动平台等,具身智能方向如有部署需求,可联系 PNP机器人获取支持。
图片

热点文章参考:

PNP&Franka机器人活动

WRC具身觉醒:当机器人初步长出“人类”的直觉——2025世界机器人大会热点讨论纪实

在具身智能火热加持下,看 2025 年机器人学术年会中的热点主题。PNP机器人展示力控、灵巧手捕捉等案例

具身智能在线活动总结:Franka Robotics与PNP机器人在具身智能领域的技术分享

具身直播活动:机器人大讲堂联合PNP机器人关于Franka机器人具身智能专题直播讨论

具身活动总结:PNP机器人在FAIR plus 2025机器人链接会中呈现具身智能机器人数据集和操作等多项技术

中国具身智能大会:具身智能发展迅速,PNP机器人展出和分享感悟全力控感知特点、操作策略局限以及数据采集等

PNP具身智能数据集总结

双臂类人形具身智能方向:一文汇总Franka机器人在科研、医疗等双臂机器人研究案例和双臂方案参考

机器人数据集:一文汇总机器人数据集RoboDataset的意义与机器人数据采集方法

机器人数据集:数据集越来越成为重要具身智能方向的基础设施—PNP机器人近期活动总结

PNP&FRANKA机器人发展

Franka机器人中国业务全新启航——Franka机器人的10个基本问题,一文掌握归来的力控机器人最新产品和趋势

技术要点分享:Franka机器人常见的10个问题——硬件篇,一文干货汇总。

中国具身智能大会:具身智能发展迅速,PNP机器人展出和分享感悟全力控感知特点、操作策略局限以及数据采集等

机器人操作策略

斯坦福大学李飞飞携Franka机器人创业空间智能提ReKep

具身方案和配置:基于“扩散策略”模仿学习训练机器人以及常用配置方案

OpenVLA:7B 参数开源 VLA模型,可以 HuggingFace下载和微调,支持 Open X-Embodiment


<<<  END >>>


关于集智联机器/PNP机器人

集智联机器人(Plug & Play Robotics),简称PNP机器人。PNP机器人团队成员均来自于ABB、Uninversal Robots(优傲机器人)等国内外机器人行业知名企业,学术背景来自于哈尔滨工业大学、多伦多大学、滑铁卢大学等,具有较强的学术背景。PNP机器人致力于为客户提供从硬件到软件的全方位支持,帮助客户快速实现机器人的部署与应用,提升生产效率和智能化水平


PNP机器人成立以来,先后获得“江苏省双创人才”、“姑苏领军”、“崇本领军、“吴江领军”等人才领军企业称号,公司具有较强科研能力,公司持续研发投入拥有技术发明等多项专利,是高新技术企业,科技中小企业;得益于推动在具身智能领域的最新技术和落地,PNP机器人获得”2024年中国科研贡献奖“

PNP机器人在具身智能方向和思灵机器人以及旗下Franka机器人金牌合作,聚焦面向生活和工业场景的单臂/双臂数据采集场景,致力于机器人即插即用(Plug & Play)技术和具身智能通用解决方案。

www.pnprobotics.com   (PNP机器人官方网站)

sales@pnprobotics.com (官方邮箱/Email Add.)

180 1817 5378(微信同号)


关注具身智能,关注PNP公众号

图片

具身智能技术对接/具身群

180 1817 5378(微信同号)

图片





Plug & Play Robotics
集智联机器人(苏州)有限公司

联系信息                            联系电话:86 138 1609 4093         联系邮箱:sales@plugplayrobotics.com            联系地址:江苏省苏州市吴江智能制造产业园C1栋