古月·ROS2入门21讲——学习笔记(二)常用工具部分15-21讲

上篇:古月·ROS2入门21讲——学习笔记(一)核心概念部分1-14讲-CSDN博客

目录

第十五讲:Launch:多节点启动与配置脚本

1. Launch文件

2. 多节点启动

运行效果

文件解析

3. 命令行参数配置

运行效果

文件解析

4. 资源重映射

运行效果

文件解析

5. ROS参数设置

运行效果

文件解析

加载参数文件

Launch文件包含

文件解析

功能包编译配置

第十六讲:TF:机器人坐标系管理神器

1. 机器人中的坐标系

2. TF命令行操作

小海龟跟随例程

查看TF树

查询坐标变换信息

坐标系可视化

3. 静态TF广播

运行效果

代码解析

4. TF监听

运行效果

代码解析

5. 海龟跟随功能解析

运行效果

Launch文件解析

坐标系动态广播

海龟跟随

第十七讲:URDF:机器人建模方法

1. 机器人的组成

2. URDF

关节Joint描述

完整机器人模型

3. 创建机器人模型

功能包结构

模型可视化效果

查看URDF模型结构

模型文件解析

第十八讲:Gazebo:三维物理仿真平台

1. 介绍

安装

运行

2. XACRO机器人模型优化

常量定义

数学计算

宏定义

文件包含

3. 机器人仿真模型配置

完善物理参数

添加Gazebo标签

配置传动装置

添加控制器插件

4. 构建仿真环境

机器人运动仿真

Ignition:下一代Gazebo

第十九讲:Rviz:三维可视化显示平台 

1. Rviz三维可视化平台

Rviz介绍

运行方法

2. 彩色相机仿真与可视化

仿真插件配置

运行仿真环境

图像数据可视化

3. 三维相机仿真与可视化

仿真插件配置

运行仿真环境

点云数据可视化

4. 激光雷达仿真与可视化

仿真插件配置

运行仿真环境

点云数据可视化

5. Rviz vs Gazebo

第二十讲:RQT:模块化可视化工具

rqt介绍

日志显示

图像显示

发布话题数据/调用服务请求

绘制数据曲线

数据包管理

节点可视化


第十五讲:Launch:多节点启动与配置脚本

到目前为止,每当我们运行一个ROS节点,都需要打开一个新的终端运行一个命令。机器人系统中节点很多,每次都这样启动好麻烦呀。有没有一种方式可以一次性启动所有节点呢?答案当然是肯定的,那就是Launch启动文件,它是ROS系统中多节点启动与配置的一种脚本。

1. Launch文件

这是一个完整的Launch文件,乍看上去,好像Python代码呀,没错,ROS2中的Launch文件就是基于Python描述的

Launch的核心目的是启动节点,我们在命令行中输入的各种参数,在Launch文件中,通过类似这样的很多代码模版,也可以进行配置,甚至还可以使用Python原有的编程功能,大大丰富了启动过程中的多样化配置。

Launch文件在ROS系统中出现的频次相当之高,它就像粘合剂一样,可以自由组装和配置各个节点,那如何编写或者阅读一个Launch文件呢,我们通过一系列例程带领大家来了解。

2. 多节点启动

先来看看如何启动多个节点。

运行效果

启动终端,使用ros2中的launch命令来启动第一个launch文件示例:

$ ros2 launch learning_launch simple.launch.py

运行成功后,就可以在终端中看到发布者和订阅者两个节点的日志信息啦。

文件解析

这两个节点是如何启动的呢?我们来分析下这个launch文件。

learning_launch/simple.launch.py

from launch import LaunchDescription           # launch文件的描述类
from launch_ros.actions import Node            # 节点启动的描述类

def generate_launch_description():             # 自动生成launch文件的函数
    return LaunchDescription([                 # 返回launch文件的描述信息
        Node(                                  # 配置一个节点的启动
            package='learning_topic',          # 节点所在的功能包
            executable='topic_helloworld_pub', # 节点的可执行文件
        ),
        Node(                                  # 配置一个节点的启动
            package='learning_topic',          # 节点所在的功能包
            executable='topic_helloworld_sub', # 节点的可执行文件名
        ),
    ])

3. 命令行参数配置

我们使用ros2命令在终端中启动节点时,还可以在命令后配置一些传入程序的参数,使用launch文件一样可以做到。

运行效果

比如我们想要运行一个Rviz可视化上位机,并且加载某一个配置文件,使用命令行的话,是这样的:

$ ros2 run rviz2 rviz2 -d <PACKAGE-PATH>/rviz/turtle_rviz.rviz

命令后边还得跟一长串配置文件的路径,如果放在launch文件里,启动就优雅很多了:

$ ros2 launch learning_launch rviz.launch.py

文件解析

命令行后边的参数是如何通过launch传入节点的呢?来看下这个launch文件。

learning_launch/rviz.launch.py

import os

from ament_index_python.packages import get_package_share_directory # 查询功能包路径的方法

from launch import LaunchDescription    # launch文件的描述类
from launch_ros.actions import Node     # 节点启动的描述类


def generate_launch_description():      # 自动生成launch文件的函数
   rviz_config = os.path.join(          # 找到配置文件的完整路径
      get_package_share_directory('learning_launch'),
      'rviz',
      'turtle_rviz.rviz'
      )

   return LaunchDescription([           # 返回launch文件的描述信息
      Node(                             # 配置一个节点的启动
         package='rviz2',               # 节点所在的功能包
         executable='rviz2',            # 节点的可执行文件名
         name='rviz2',                  # 对节点重新命名
         arguments=['-d', rviz_config]  # 加载命令行参数
      )
   ])

4. 资源重映射

ROS社区中的资源非常多,当我们使用别人代码的时候,经常会发现通信的话题名称不太符合我们的要求,能否对类似的资源重新命名呢?

为了提高软件的复用性,ROS提供了资源重映射的机制,可以帮助我们解决类似的问题。

运行效果

启动一个终端,运行如下例程,很快会看到出现了两个小海龟仿真器界面;再打开一个终端,发布如下话题,让海龟1动起来,海龟2也会一起运动:

$ ros2 launch learning_launch rviz.launch.py
$ ros2 topic pub --rate 1 /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 2.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 1.8}}"

文件解析

为什么两个海龟都会动呢?这里要用到turtlesim功能包里另外一个节点,叫做mimic,它的功能是订阅某一个海龟的Pose位置,通过计算,变换成一个同样运动的速度指令,发布出去。

至于mimic节点订阅或者发布的话题名叫什么呢?我们就可以通过重映射修改成对应任意海龟的名字。

learning_launch/remapping.launch.py

from launch import LaunchDescription      # launch文件的描述类
from launch_ros.actions import Node       # 节点启动的描述类

def generate_launch_description():        # 自动生成launch文件的函数
    return LaunchDescription([            # 返回launch文件的描述信息
        Node(                             # 配置一个节点的启动
            package='turtlesim',          # 节点所在的功能包
            namespace='turtlesim1',       # 节点所在的命名空间
            executable='turtlesim_node',  # 节点的可执行文件名
            name='sim'                    # 对节点重新命名
        ),
        Node(                             # 配置一个节点的启动
            package='turtlesim',          # 节点所在的功能包
            namespace='turtlesim2',       # 节点所在的命名空间
            executable='turtlesim_node',  # 节点的可执行文件名
            name='sim'                    # 对节点重新命名
        ),
        Node(                             # 配置一个节点的启动
            package='turtlesim',          # 节点所在的功能包
            executable='mimic',           # 节点的可执行文件名
            name='mimic',                 # 对节点重新命名
            remappings=[                  # 资源重映射列表
                ('/input/pose', '/turtlesim1/turtle1/pose'),         # 将/input/pose话题名修改为/turtlesim1/turtle1/pose
                ('/output/cmd_vel', '/turtlesim2/turtle1/cmd_vel'),  # 将/output/cmd_vel话题名修改为/turtlesim2/turtle1/cmd_vel
            ]
        )
    ])

5. ROS参数设置

ROS系统中的参数,也可以在Launch文件中设置。

运行效果

启动一个终端,运行如下命令:

$ ros2 launch learning_launch parameters.launch.py

在启动的海龟仿真器中,我们看到背景颜色被改变了,这个颜色参数的设置就是在launch文件中完成的。

文件解析

我们看下在launch文件中如何来设置参数的。

learning_launch/parameters.launch.py

from launch import LaunchDescription                   # launch文件的描述类
from launch.actions import DeclareLaunchArgument       # 声明launch文件内使用的Argument类
from launch.substitutions import LaunchConfiguration, TextSubstitution

from launch_ros.actions import Node                    # 节点启动的描述类


def generate_launch_description():                     # 自动生成launch文件的函数
   background_r_launch_arg = DeclareLaunchArgument(
      'background_r', default_value=TextSubstitution(text='0')     # 创建一个Launch文件内参数(arg)background_r
   )
   background_g_launch_arg = DeclareLaunchArgument(
      'background_g', default_value=TextSubstitution(text='84')    # 创建一个Launch文件内参数(arg)background_g
   )
   background_b_launch_arg = DeclareLaunchArgument(
      'background_b', default_value=TextSubstitution(text='122')   # 创建一个Launch文件内参数(arg)background_b
   )

   return LaunchDescription([                                      # 返回launch文件的描述信息
      background_r_launch_arg,                                     # 调用以上创建的参数(arg)
      background_g_launch_arg,
      background_b_launch_arg,
      Node(                                                        # 配置一个节点的启动
         package='turtlesim',
         executable='turtlesim_node',                              # 节点所在的功能包
         name='sim',                                               # 对节点重新命名
         parameters=[{                                             # ROS参数列表
            'background_r': LaunchConfiguration('background_r'),   # 创建参数background_r
            'background_g': LaunchConfiguration('background_g'),   # 创建参数background_g
            'background_b': LaunchConfiguration('background_b'),   # 创建参数background_b
         }]
      ),
   ])

 Attention

launch文件中出现的argument和parameter,虽都译为“参数”,但含义不同: - argument:仅限launch文件内部使用,方便在launch中调用某些数值; - parameter:ROS系统的参数,方便在节点见使用某些数值。

加载参数文件

以上例程我们在launch文件中一个一个的设置参数,略显麻烦,当参数比较多的时候,建议使用参数文件进行加载。

learning_launch/parameters_yaml.launch.py

import os

from ament_index_python.packages import get_package_share_directory  # 查询功能包路径的方法

from launch import LaunchDescription   # launch文件的描述类
from launch_ros.actions import Node    # 节点启动的描述类


def generate_launch_description():     # 自动生成launch文件的函数
   config = os.path.join(              # 找到参数文件的完整路径
      get_package_share_directory('learning_launch'),
      'config',
      'turtlesim.yaml'
      )

   return LaunchDescription([          # 返回launch文件的描述信息
      Node(                            # 配置一个节点的启动
         package='turtlesim',          # 节点所在的功能包
         executable='turtlesim_node',  # 节点的可执行文件名
         namespace='turtlesim2',       # 节点所在的命名空间
         name='sim',                   # 对节点重新命名
         parameters=[config]           # 加载参数文件
      )
   ])

Launch文件包含

在复杂的机器人系统中,launch文件也会有很多,此时我们可以使用类似编程中的include机制,让launch文件互相包含。

文件解析

learning_launch/namespaces.launch.py

import os

from ament_index_python.packages import get_package_share_directory  # 查询功能包路径的方法

from launch import LaunchDescription                 # launch文件的描述类
from launch.actions import IncludeLaunchDescription  # 节点启动的描述类
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.actions import GroupAction               # launch文件中的执行动作
from launch_ros.actions import PushRosNamespace      # ROS命名空间配置

def generate_launch_description():                   # 自动生成launch文件的函数
   parameter_yaml = IncludeLaunchDescription(        # 包含指定路径下的另外一个launch文件
      PythonLaunchDescriptionSource([os.path.join(
         get_package_share_directory('learning_launch'), 'launch'),
         '/parameters_nonamespace.launch.py'])
      )

   parameter_yaml_with_namespace = GroupAction(      # 对指定launch文件中启动的功能加上命名空间
      actions=[
         PushRosNamespace('turtlesim2'),
         parameter_yaml]
      )

   return LaunchDescription([                        # 返回launch文件的描述信息
      parameter_yaml_with_namespace
   ])

功能包编译配置

    ...

    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
        (os.path.join('share', package_name, 'launch'), glob(os.path.join('launch', '*.launch.py'))),
        (os.path.join('share', package_name, 'config'), glob(os.path.join('config', '*.*'))),
        (os.path.join('share', package_name, 'rviz'), glob(os.path.join('rviz', '*.*'))),
    ],

    ...

第十六讲:TF:机器人坐标系管理神器

个人感觉这讲比较难懂,建议多听几遍

1. 机器人中的坐标系

机器人中都有哪些坐标系呢?

比如在机械臂形态的机器人中,机器人安装的位置叫做基坐标系Base Frame,机器人安装位置在外部环境下的参考系叫做世界坐标系World Frame,机器人末端夹爪的位置叫做工具坐标系,外部被操作物体的位置叫做工件坐标系,在机械臂抓取外部物体的过程中,这些坐标系之间的关系也在跟随变化。

在移动机器人系统中,坐标系一样至关重要,比如一个移动机器人的中心点是基坐标系Base Link,雷达所在的位置叫做雷达坐标系laser link,机器人要移动,里程计会累积位置,这个位置的参考系叫做里程计坐标系odom,里程计又会有累积误差和漂移,绝对位置的参考系叫做地图坐标系map。

一层一层坐标系之间关系复杂,有一些是相对固定的,也有一些是不断变化的,看似简单的坐标系也在空间范围内变得复杂,良好的坐标系管理系统就显得格外重要。

关于坐标系变换关系的基本理论,在每一本机器人学的教材中都会有讲解,可以分解为平移和旋转两个部分,通过一个四乘四的矩阵进行描述,在空间中画出坐标系,那两者之间的变换关系,其实就是向量的数学描述。

ROS中TF功能的底层原理,就是对这些数学变换进行了封装,详细的理论知识大家可以参考机器人学的教材,我们主要讲解TF坐标管理系统的使用方法。

2. TF命令行操作

ROS中的TF该如何使用呢?我们先通过两只小海龟的示例,了解下基于坐标系的一种机器人跟随算法。

小海龟跟随例程

这个示例需要我们先安装相应的功能包,然后就可以通过一个launch文件启动,之后我们可以控制其中的一只小海龟,另外一只小海龟会自动跟随运动。

$ sudo apt install ros-humble-turtle-tf2-py ros-humble-tf2-tools
$ sudo pip3 install transforms3d

具体运行的效果如何?我们来试一试。

$ ros2 launch turtle_tf2_py turtle_tf2_demo.launch.py
$ ros2 run turtlesim turtle_teleop_key

当我们控制一只海龟运动时,另外一只海龟也会跟随运动。

查看TF树

在当前运行的两只海龟中,有哪些坐标系呢,我们可以通过这个小工具来做查看。

$ ros2 run tf2_tools view_frames 

默认在当前终端路径下生成了一个frames.pdf文件,打开之后,就可以看到系统中各个坐标系的关系了。

查询坐标变换信息

只看到坐标系的结构还不行,如果我们想要知道某两个坐标系之间的具体关系,可以通过tf2_echo这个工具查看:

$ ros2 run tf2_ros tf2_echo turtle2 turtle1

 运行成功后,终端中就会循环打印坐标系的变换数值了,由平移和旋转两个部分组成,还有旋转矩阵。

坐标系可视化

看数值还不直观?可以试试用可视化软件来做显示:

$ ros2 run rviz2 rviz2 -d $(ros2 pkg prefix --share turtle_tf2_py)/rviz/turtle_rviz.rviz

再让小海龟动起来,Rviz中的坐标轴就会开始运动,这样是不是更加直观了呢!

3. 静态TF广播

我们说TF的主要作用是对坐标系进行管理,那就管理一个试试呗?

坐标变换中最为简单的应该是相对位置不发生变化的情况,比如你家的房子在哪个位置,只要房子不拆,这个坐标应该就不会变化。

在机器人系统中也很常见,比如激光雷达和机器人底盘之间的位置关系,安装好之后基本不会变化。

在TF中,这种情况也称之为静态TF变换,我们来看看在程序中该如何实现?

运行效果

启动终端,运行如下命令:

$ ros2 run learning_tf static_tf_broadcaster
$ ros2 run tf2_tools view_frames 

可以看到当前系统中存在两个坐标系,一个是world,一个是house,两者之间的相对位置不会发生改变,通过一个静态的TF对象进行维护。

代码解析

来看下在代码中是如何创建坐标系并且发布静态变换的。

learning_tf/static_tf_broadcaster.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2 TF示例-广播静态的坐标变换
"""

import rclpy                                                                 # ROS2 Python接口库
from rclpy.node import Node                                                  # ROS2 节点类
from geometry_msgs.msg import TransformStamped                               # 坐标变换消息
import tf_transformations                                                    # TF坐标变换库
from tf2_ros.static_transform_broadcaster import StaticTransformBroadcaster  # TF静态坐标系广播器类

class StaticTFBroadcaster(Node):
    def __init__(self, name):
        super().__init__(name)                                                  # ROS2节点父类初始化
        self.tf_broadcaster = StaticTransformBroadcaster(self)                  # 创建一个TF广播器对象

        static_transformStamped = TransformStamped()                            # 创建一个坐标变换的消息对象
        static_transformStamped.header.stamp = self.get_clock().now().to_msg()  # 设置坐标变换消息的时间戳
        static_transformStamped.header.frame_id = 'world'                       # 设置一个坐标变换的源坐标系
        static_transformStamped.child_frame_id  = 'house'                       # 设置一个坐标变换的目标坐标系
        static_transformStamped.transform.translation.x = 10.0                  # 设置坐标变换中的X、Y、Z向的平移
        static_transformStamped.transform.translation.y = 5.0                    
        static_transformStamped.transform.translation.z = 0.0
        quat = tf_transformations.quaternion_from_euler(0.0, 0.0, 0.0)          # 将欧拉角转换为四元数(roll, pitch, yaw)
        static_transformStamped.transform.rotation.x = quat[0]                  # 设置坐标变换中的X、Y、Z向的旋转(四元数)
        static_transformStamped.transform.rotation.y = quat[1]
        static_transformStamped.transform.rotation.z = quat[2]
        static_transformStamped.transform.rotation.w = quat[3]

        self.tf_broadcaster.sendTransform(static_transformStamped)              # 广播静态坐标变换,广播后两个坐标系的位置关系保持不变

def main(args=None):
    rclpy.init(args=args)                                # ROS2 Python接口初始化
    node = StaticTFBroadcaster("static_tf_broadcaster")  # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                                     # 循环等待ROS2退出
    node.destroy_node()                                  # 销毁节点对象
    rclpy.shutdown()

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

    entry_points={
        'console_scripts': [
            'static_tf_broadcaster = learning_tf.static_tf_broadcaster:main',
        ],
    },

经过这段代码,两个坐标系的变化是描述清楚了,到了使用的时候,我们又该如何查询呢?

4. TF监听

我们再来学习下如何查询两个坐标系之间的位置关系。

运行效果

启动一个终端,运行如下节点,就可以在终端中看到周期显示的坐标关系了。

$ ros2 run learning_tf tf_listener

代码解析

这个节点中是如何查询坐标关系的,我们来看下代码。

learning_tf/tf_listener.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2 TF示例-监听某两个坐标系之间的变换
"""

import rclpy                                              # ROS2 Python接口库
from rclpy.node import Node                               # ROS2 节点类
import tf_transformations                                 # TF坐标变换库
from tf2_ros import TransformException                    # TF左边变换的异常类
from tf2_ros.buffer import Buffer                         # 存储坐标变换信息的缓冲类
from tf2_ros.transform_listener import TransformListener  # 监听坐标变换的监听器类

class TFListener(Node):

    def __init__(self, name):
        super().__init__(name)                                      # ROS2节点父类初始化

        self.declare_parameter('source_frame', 'world')             # 创建一个源坐标系名的参数
        self.source_frame = self.get_parameter(                     # 优先使用外部设置的参数值,否则用默认值
            'source_frame').get_parameter_value().string_value

        self.declare_parameter('target_frame', 'house')             # 创建一个目标坐标系名的参数
        self.target_frame = self.get_parameter(                     # 优先使用外部设置的参数值,否则用默认值
            'target_frame').get_parameter_value().string_value

        self.tf_buffer = Buffer()                                   # 创建保存坐标变换信息的缓冲区
        self.tf_listener = TransformListener(self.tf_buffer, self)  # 创建坐标变换的监听器

        self.timer = self.create_timer(1.0, self.on_timer)          # 创建一个固定周期的定时器,处理坐标信息

    def on_timer(self):
        try:
            now = rclpy.time.Time()                                 # 获取ROS系统的当前时间
            trans = self.tf_buffer.lookup_transform(                # 监听当前时刻源坐标系到目标坐标系的坐标变换
                self.target_frame,
                self.source_frame,
                now)
        except TransformException as ex:                            # 如果坐标变换获取失败,进入异常报告
            self.get_logger().info(
                f'Could not transform {self.target_frame} to {self.source_frame}: {ex}')
            return

        pos  = trans.transform.translation                          # 获取位置信息
        quat = trans.transform.rotation                             # 获取姿态信息(四元数)
        euler = tf_transformations.euler_from_quaternion([quat.x, quat.y, quat.z, quat.w])
        self.get_logger().info('Get %s --> %s transform: [%f, %f, %f] [%f, %f, %f]' 
          % (self.source_frame, self.target_frame, pos.x, pos.y, pos.z, euler[0], euler[1], euler[2]))

def main(args=None):
    rclpy.init(args=args)                       # ROS2 Python接口初始化
    node = TFListener("tf_listener")            # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                            # 循环等待ROS2退出
    node.destroy_node()                         # 销毁节点对象
    rclpy.shutdown()                            # 关闭ROS2 Python接口

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

    entry_points={
        'console_scripts': [
            'static_tf_broadcaster = learning_tf.static_tf_broadcaster:main',
            'tf_listener = learning_tf.tf_listener:main',
        ],
    },

5. 海龟跟随功能解析

还是之前小海龟跟随的示例,我们自己通过代码来实现一下。

运行效果

先看下实现的效果,启动终端后,通过如下命令启动例程:

$ ros2 launch learning_tf turtle_following_demo.launch.py
$ ros2 run turtlesim turtle_teleop_key

看到的效果和ROS自带的例程相同。

原理解析 

在两只海龟的仿真器中,我们可以定义三个坐标系,比如仿真器的全局参考系叫做world,turtle1和turtle2坐标系在两只海龟的中心点,这样,turtle1和world坐标系的相对位置,就可以表示海龟1的位置,海龟2也同理。

要实现海龟2向海龟1运动,我们在两者中间做一个连线,再加一个箭头,怎么样,是不是有想起高中时学习的向量计算?我们说坐标变换的描述方法就是向量,所以在这个跟随例程中,用TF就可以很好的解决。

向量的长度表示距离,方向表示角度,有了距离和角度,我们随便设置一个时间,不就可以计算得到速度了么,然后就是速度话题的封装和发布,海龟2也就可以动起来了。

所以这个例程的核心就是通过坐标系实现向量的计算,两只海龟还会不断运动,这个向量也得按照某一个周期计算,这就得用上TF的动态广播与监听了。

我们一起看下代码该如何实现。

Launch文件解析

先来看下刚才运行的launch文件,里边启动了四个节点,分别是:

  • 小海龟仿真器
  • 海龟1的坐标系广播
  • 海龟2的坐标系广播
  • 海龟跟随控制

其中,两个坐标系的广播复用了turtle_tf_broadcaster节点,通过传入的参数名修改维护的坐标系名称。

learning_tf/launch/turtle_following_demo.launch.py

from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():
    return LaunchDescription([
        Node(
            package='turtlesim',
            executable='turtlesim_node',
            name='sim'
        ),
        Node(
            package='learning_tf',
            executable='turtle_tf_broadcaster',
            name='broadcaster1',
            parameters=[
                {'turtlename': 'turtle1'}
            ]
        ),
        DeclareLaunchArgument(
            'target_frame', default_value='turtle1',
            description='Target frame name.'
        ),
        Node(
            package='learning_tf',
            executable='turtle_tf_broadcaster',
            name='broadcaster2',
            parameters=[
                {'turtlename': 'turtle2'}
            ]
        ),
        Node(
            package='learning_tf',
            executable='turtle_following',
            name='listener',
            parameters=[
                {'target_frame': LaunchConfiguration('target_frame')}
            ]
        ), 
    ])

坐标系动态广播

海龟1和海龟2在world坐标系下的坐标变换,在turtle_tf_broadcaster节点中实现,除了海龟坐标系的名字不同之外,针对两个海龟的功能是一样的。

learning_tf/turtle_tf_broadcaster.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2 TF示例-广播动态的坐标变换
"""

import rclpy                                       # ROS2 Python接口库
from rclpy.node import Node                        # ROS2 节点类
from geometry_msgs.msg import TransformStamped     # 坐标变换消息
import tf_transformations                          # TF坐标变换库
from tf2_ros import TransformBroadcaster           # TF坐标变换广播器
from turtlesim.msg import Pose                     # turtlesim小海龟位置消息

class TurtleTFBroadcaster(Node):

    def __init__(self, name):
        super().__init__(name)                                # ROS2节点父类初始化

        self.declare_parameter('turtlename', 'turtle')        # 创建一个海龟名称的参数
        self.turtlename = self.get_parameter(                 # 优先使用外部设置的参数值,否则用默认值
            'turtlename').get_parameter_value().string_value

        self.tf_broadcaster = TransformBroadcaster(self)      # 创建一个TF坐标变换的广播对象并初始化

        self.subscription = self.create_subscription(         # 创建一个订阅者,订阅海龟的位置消息
            Pose,
            f'/{self.turtlename}/pose',                       # 使用参数中获取到的海龟名称
            self.turtle_pose_callback, 1)

    def turtle_pose_callback(self, msg):                              # 创建一个处理海龟位置消息的回调函数,将位置消息转变成坐标变换
        transform = TransformStamped()                                # 创建一个坐标变换的消息对象

        transform.header.stamp = self.get_clock().now().to_msg()      # 设置坐标变换消息的时间戳
        transform.header.frame_id = 'world'                           # 设置一个坐标变换的源坐标系
        transform.child_frame_id = self.turtlename                    # 设置一个坐标变换的目标坐标系
        transform.transform.translation.x = msg.x                     # 设置坐标变换中的X、Y、Z向的平移
        transform.transform.translation.y = msg.y
        transform.transform.translation.z = 0.0
        q = tf_transformations.quaternion_from_euler(0, 0, msg.theta) # 将欧拉角转换为四元数(roll, pitch, yaw)
        transform.transform.rotation.x = q[0]                         # 设置坐标变换中的X、Y、Z向的旋转(四元数)
        transform.transform.rotation.y = q[1]
        transform.transform.rotation.z = q[2]
        transform.transform.rotation.w = q[3]

        # Send the transformation
        self.tf_broadcaster.sendTransform(transform)     # 广播坐标变换,海龟位置变化后,将及时更新坐标变换信息

def main(args=None):
    rclpy.init(args=args)                                # ROS2 Python接口初始化
    node = TurtleTFBroadcaster("turtle_tf_broadcaster")  # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                                     # 循环等待ROS2退出
    node.destroy_node()                                  # 销毁节点对象
    rclpy.shutdown()                                     # 关闭ROS2 Python接口

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

    entry_points={
        'console_scripts': [
            'static_tf_broadcaster = learning_tf.static_tf_broadcaster:main',
            'turtle_tf_broadcaster = learning_tf.turtle_tf_broadcaster:main',
            'tf_listener = learning_tf.tf_listener:main',
        ],
    },

海龟跟随

坐标系都正常广播了,接下来我们就可以订阅两只海龟的位置关系,并且变换成速度指令进行控制啦。

learning_tf/turtle_following.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2 TF示例-通过坐标变化实现海龟跟随功能
"""

import math
import rclpy                                              # ROS2 Python接口库
from rclpy.node import Node                               # ROS2 节点类
import tf_transformations                                 # TF坐标变换库
from tf2_ros import TransformException                    # TF左边变换的异常类
from tf2_ros.buffer import Buffer                         # 存储坐标变换信息的缓冲类
from tf2_ros.transform_listener import TransformListener  # 监听坐标变换的监听器类
from geometry_msgs.msg import Twist                       # ROS2 速度控制消息
from turtlesim.srv import Spawn                           # 海龟生成的服务接口
class TurtleFollowing(Node):

    def __init__(self, name):
        super().__init__(name)                                      # ROS2节点父类初始化

        self.declare_parameter('source_frame', 'turtle1')           # 创建一个源坐标系名的参数
        self.source_frame = self.get_parameter(                     # 优先使用外部设置的参数值,否则用默认值
            'source_frame').get_parameter_value().string_value

        self.tf_buffer = Buffer()                                   # 创建保存坐标变换信息的缓冲区
        self.tf_listener = TransformListener(self.tf_buffer, self)  # 创建坐标变换的监听器

        self.spawner = self.create_client(Spawn, 'spawn')           # 创建一个请求产生海龟的客户端
        self.turtle_spawning_service_ready = False                  # 是否已经请求海龟生成服务的标志位
        self.turtle_spawned = False                                 # 海龟是否产生成功的标志位

        self.publisher = self.create_publisher(Twist, 'turtle2/cmd_vel', 1) # 创建跟随运动海龟的速度话题

        self.timer = self.create_timer(1.0, self.on_timer)         # 创建一个固定周期的定时器,控制跟随海龟的运动

    def on_timer(self):
        from_frame_rel = self.source_frame                         # 源坐标系
        to_frame_rel   = 'turtle2'                                 # 目标坐标系

        if self.turtle_spawning_service_ready:                     # 如果已经请求海龟生成服务
            if self.turtle_spawned:                                # 如果跟随海龟已经生成
                try:
                    now = rclpy.time.Time()                        # 获取ROS系统的当前时间
                    trans = self.tf_buffer.lookup_transform(       # 监听当前时刻源坐标系到目标坐标系的坐标变换
                        to_frame_rel,
                        from_frame_rel,
                        now)
                except TransformException as ex:                   # 如果坐标变换获取失败,进入异常报告
                    self.get_logger().info(
                        f'Could not transform {to_frame_rel} to {from_frame_rel}: {ex}')
                    return

                msg = Twist()                                      # 创建速度控制消息
                scale_rotation_rate = 1.0                          # 根据海龟角度,计算角速度
                msg.angular.z = scale_rotation_rate * math.atan2(
                    trans.transform.translation.y,
                    trans.transform.translation.x)

                scale_forward_speed = 0.5                          # 根据海龟距离,计算线速度
                msg.linear.x = scale_forward_speed * math.sqrt(
                    trans.transform.translation.x ** 2 +
                    trans.transform.translation.y ** 2)

                self.publisher.publish(msg)                        # 发布速度指令,海龟跟随运动
            else:                                                  # 如果跟随海龟没有生成
                if self.result.done():                             # 查看海龟是否生成
                    self.get_logger().info(
                        f'Successfully spawned {self.result.result().name}')
                    self.turtle_spawned = True                     
                else:                                              # 依然没有生成跟随海龟
                    self.get_logger().info('Spawn is not finished')
        else:                                                      # 如果没有请求海龟生成服务
            if self.spawner.service_is_ready():                    # 如果海龟生成服务器已经准备就绪
                request = Spawn.Request()                          # 创建一个请求的数据
                request.name = 'turtle2'                           # 设置请求数据的内容,包括海龟名、xy位置、姿态
                request.x = float(4)
                request.y = float(2)
                request.theta = float(0)

                self.result = self.spawner.call_async(request)     # 发送服务请求
                self.turtle_spawning_service_ready = True          # 设置标志位,表示已经发送请求
            else:
                self.get_logger().info('Service is not ready')     # 海龟生成服务器还没准备就绪的提示


def main(args=None):
    rclpy.init(args=args)                       # ROS2 Python接口初始化
    node = TurtleFollowing("turtle_following")  # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                            # 循环等待ROS2退出
    node.destroy_node()                         # 销毁节点对象
    rclpy.shutdown()                            # 关闭ROS2 Python接口

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

    entry_points={
        'console_scripts': [
            'static_tf_broadcaster = learning_tf.static_tf_broadcaster:main',
            'turtle_tf_broadcaster = learning_tf.turtle_tf_broadcaster:main',
            'tf_listener = learning_tf.tf_listener:main',
            'turtle_following = learning_tf.turtle_following:main',
        ],
    },

第十七讲:URDF:机器人建模方法

ROS是机器人操作系统,当然要给机器人使用啦,不过在使用之前,还得让ROS认识下我们使用的机器人,如何把一个机器人介绍给ROS呢?

为此,ROS专门提供了一种机器人建模方法——URDF,用来描述机器人外观、性能等各方面属性。

1. 机器人的组成

建模描述机器人的过程中,我们自己需要先熟悉机器人的组成和参数,比如机器人一般是由硬件结构、驱动系统、传感器系统、控制系统四大部分组成,市面上一些常见的机器人,无论是移动机器人还是机械臂,我们都可以按照这四大组成部分进行分解。

  • 硬件结构就是底盘、外壳、电机等实打实可以看到的设备;
  • 驱动系统就是可以驱使这些设备正常使用的装置,比如电机的驱动器,电源管理系统等;
  • 传感系统包括电机上的编码器、板载的IMU、安装的摄像头、雷达等等,便于机器人感知自己的状态和外部的环境;
  • 控制系统就是我们开发过程的主要载体了,一般是树莓派、电脑等计算平台,以及里边的操作系统和应用软件。

机器人建模的过程,其实就是按照类似的思路,通过建模语言,把机器人每一个部分都描述清楚,再组合起来的过程。

2. URDF

ROS中的建模方法叫做URDF,全称是统一机器人描述格式,不仅可以清晰描述机器人自身的模型,还可以描述机器人的外部环境,比如这里的桌子,也可以算作一个模型。 

URDF模型文件使用的是XML格式,右侧就是一个机器人的URDF描述,乍看上去,有点像网页开发的源代码,都是由一系列尖括号包围的标签和其中的属性组合而成。

如何使用这样一个文件描述机器人呢?比如这个机械臂,大家可以看下自己的手臂,我们的手臂是由大臂和小臂组成,他们独自是无法运动的,必须通过一个手肘关节连接之后,才能通过肌肉驱动,产生相对运动。

在建模中,大臂和小臂就类似机器人的这些独立的刚体部分,称为连杆Link,手肘就类似于机器人电机驱动部分,称为关节joint

所以在URDF建模过程中,关键任务就是通过这里的<link>和<joint>,理清楚每一个连杆和关节的描述信息。

<link>标签用来描述机器人某个刚体部分的外观和物理属性,外观包括尺寸、颜色、形状,物理属性包括质量、惯性矩阵、碰撞参数等。

 以这个机械臂连杆为例,它的link描述如下:

link标签中的name表示该连杆的名称,我们可以自定义,未来joint连接link的时候,会使用到这个名称。

link里边的<visual>部分用来描述机器人的外观,比如:

  • <geometry>表示几何形状,里边使用<mesh>调用了一个在三维软件中提前设计好的蓝色外观,就是这个stl文件,看上去和真实机器人是一致的
  • <origin>表示坐标系相对初始位置的偏移,分别是x、y、z方向上的平移,和roll、pitch、raw旋转,不需要偏移的话,就全为0。

第二个部分<collision>,描述碰撞参数,里边的内容似乎和<visual>一样,也有<geometry>和<origin>,看似相同,其实区别还是比较大的。

  • <visual>部分重在描述机器人看上去的状态,也就是视觉效果;
  • <collision>部分则是描述机器人运动过程中的状态,比如机器人与外界如何接触算作碰撞。

在这个机器人模型中,蓝色部分是通过<visual>来描述的,在实际控制过程中,这样复杂的外观在计算碰撞检测时,要求的算力较高,为了简化计算,我们将碰撞检测用的模型简化为了绿色框的圆柱体,也就是<collision>里边<geometry>描述的形状。<origin>坐标系偏移也是类似,可以描述刚体质心的偏移。

如果是移动机器人的话,link也可以用来描述小车的车体、轮子等部分。

关节Joint描述

机器人模型中的刚体最终要通过关节joint连接之后,才能产生相对运动。

URDF中的关节有六种运动类型。

  1. continuous,描述旋转运动,可以围绕某一个轴无限旋转,比如小车的轮子,就属于这种类型。
  2. revolute,也是旋转关节,和continuous类型的区别在于不能无限旋转,而是带有角度限制,比如机械臂的两个连杆,就属于这种运动。
  3. prismatic,是滑动关节,可以沿某一个轴平移,也带有位置的极限,一般直线电机就是这种运动方式。
  4. fixed,固定关节,是唯一一种不允许运动的关节,不过使用还是比较频繁的,比如相机这个连杆,安装在机器人上,相对位置是不会变化的,此时使用的连接方式就是Fixed。
  5. Floating是浮动关节,第六种planar是平面关节,这两种使用相对较少。

在URDF模型中,每一个link都使用这样一段xml内容描述,比如关节的名字叫什么,运动类型是哪一种。

  • parent标签:描述父连杆;
  • child标签:描述子连杆,子连杆会相对父连杆发生运动;
  • origin:表示两个连杆坐标系之间的关系,也就是图中红色的向量,可以理解为这两个连杆该如何安装到一起;
  • axis表示关节运动轴的单位向量,比如z等于1,就表示这个旋转运动是围绕z轴的正方向进行的;
  • limit就表示运动的一些限制了,比如最小位置,最大位置,和最大速度等。

ROS中关于平移的默认单位是m,旋转是弧度(不是度),所以这里的3.14就表示可以在-180度到180度之间运动,线速度是m/s,角速度是rad/s。

完整机器人模型

最终所有的link和joint标签完成了对机器人每个部分的描述和组合,全都放在一个robot标签中,就形成了完整的机器人模型。

 

所以大家在看某一个URDF模型时,先不着急看每一块代码的细节,先来找link和joint,看下这个机器人是由哪些部分组成的,了解完全局之后,再看细节。

3. 创建机器人模型

好啦,讲了这么多,还是要看一个完整的示例。

我们以这款移动机器人模型为例,一起看下它的URDF建模过程。

功能包结构

机器人的模型放置在learning_urdf功能包中,功能包中包含的文件夹如下:

  • urdf:存放机器人模型的URDF或xacro文件
  • meshes:放置URDF中引用的模型渲染文件
  • launch:保存相关启动文件
  • rviz:保存rviz的配置文件

模型可视化效果

我们先来看下这个模型的效果,尝试逆向分下一下机器人的结构。

$ ros2 launch learning_urdf display.launch.py

​​​​​​​

从可视化的效果来看,这个机器人由五个link和4个joint组成。

查看URDF模型结构

我们分析的对不对呢,可以在模型文件的路径下,使用urdf_to_graphviz这个小工具来分析下。

$ urdf_to_graphviz mbot_base.urdf  # 在模型文件夹下运行

模型文件解析

具体URDF模型什么样的,还是要打开模型来研究。

learning_urdf/urdf/mbot_base.urdf

<?xml version="1.0" ?>
<robot name="mbot">

    <link name="base_link">
        <visual>
            <origin xyz=" 0 0 0" rpy="0 0 0" />
            <geometry>
                <cylinder length="0.16" radius="0.20"/>
            </geometry>
            <material name="yellow">
                <color rgba="1 0.4 0 1"/>
            </material>
        </visual>
    </link>

    <joint name="left_wheel_joint" type="continuous">
        <origin xyz="0 0.19 -0.05" rpy="0 0 0"/>
        <parent link="base_link"/>
        <child link="left_wheel_link"/>
        <axis xyz="0 1 0"/>
    </joint>

    <link name="left_wheel_link">
        <visual>
            <origin xyz="0 0 0" rpy="1.5707 0 0" />
            <geometry>
                <cylinder radius="0.06" length = "0.025"/>
            </geometry>
            <material name="white">
                <color rgba="1 1 1 0.9"/>
            </material>
        </visual>
    </link>

    <joint name="right_wheel_joint" type="continuous">
        <origin xyz="0 -0.19 -0.05" rpy="0 0 0"/>
        <parent link="base_link"/>
        <child link="right_wheel_link"/>
        <axis xyz="0 1 0"/>
    </joint>

    <link name="right_wheel_link">
        <visual>
            <origin xyz="0 0 0" rpy="1.5707 0 0" />
            <geometry>
                <cylinder radius="0.06" length = "0.025"/>
            </geometry>
            <material name="white">
                <color rgba="1 1 1 0.9"/>
            </material>
        </visual>
    </link>

    <joint name="front_caster_joint" type="continuous">
        <origin xyz="0.18 0 -0.095" rpy="0 0 0"/>
        <parent link="base_link"/>
        <child link="front_caster_link"/>
        <axis xyz="0 1 0"/>
    </joint>

    <link name="front_caster_link">
        <visual>
            <origin xyz="0 0 0" rpy="0 0 0"/>
            <geometry>
                <sphere radius="0.015" />
            </geometry>
            <material name="black">
                <color rgba="0 0 0 0.95"/>
            </material>
        </visual>
    </link>

    <joint name="back_caster_joint" type="continuous">
        <origin xyz="-0.18 0 -0.095" rpy="0 0 0"/>
        <parent link="base_link"/>
        <child link="back_caster_link"/>
        <axis xyz="0 1 0"/>
    </joint>

    <link name="back_caster_link">
        <visual>
            <origin xyz="0 0 0" rpy="0 0 0"/>
            <geometry>
                <sphere radius="0.015" />
            </geometry>
            <material name="black">
                <color rgba="0 0 0 0.95"/>
            </material>
        </visual>
    </link>

</robot>

后续我将learning_urdf文件夹复制到一个新的工作空间中并改名为learning_urdf_2,然后需要将所有learning_urdf的位置都改为learning_urdf_2,重新使用colcon build编译,并检查install文件夹中的setup.sh文件中的名字是否正确,然后使用source setup.sh命令使得修改生效,才成功运行的

第十八讲:Gazebo:三维物理仿真平台

1. 介绍

Gazebo是ROS系统中最为常用的三维物理仿真平台,支持动力学引擎,可以实现高质量的图形渲染,不仅可以模拟机器人及周边环境,还可以加入摩擦力、弹性系数等物理属性。

比如我们要开发一个火星车,那就可以在Gazebo中模拟火星表面的环境,再比如我们做无人机,续航和限飞都导致我们没有办法频繁用实物做实验,此时不妨使用Gazebo先做仿真,等算法开发的差不多了,再部署到实物上来运行。

所以类似Gazebo这样的仿真平台,可以帮助我们验证机器人算法、优化机器人设计、测试机器人场景应用,为机器人开发提供更多可能。

安装

Gazebo如何使用呢?我们不妨先把它给跑起来,互相认识一下。

为了确保系统中已经完整安装了Gazebo相关的功能包,大家可以通过这样一个命令,简单直接的把和gazebo相关的包都给装上:

$ sudo apt install ros-humble-gazebo-*

运行

通过这句命令就可以启动啦:

$ ros2 launch gazebo_ros gazebo.launch.py

 这行命令输入没有反应的话参考:ros2 launch gazebo_ros gazebo.launch.py无法启动-CSDN博客

为保证模型顺利加载,请将离线模型下载并放置到~/.gazebo/models路径下

下载链接如下:https://github.com/osrf/gazebo_models

2. XACRO机器人模型优化

我们之前设计好的URDF模型此时还不能直接放到Gazebo中,需要我们做一些优化。这里给大家介绍一个URDF文件格式的升级版本——XACRO文件

同样也是对机器人URDF模型的创建,XACRO文件加入了更多编程化的实现方法,可以让模型创建更友好。

比如:

  • 宏定义,一个小车有4个轮子,每个轮子都一样,我们就没必要创建4个一样的link,像函数定义一样,做一个可重复使用的模块就可以了。

  • 文件包含,复杂机器人的模型文件可能会很长,为了切分不同的模块,比如底盘、传感器,我们还可以把不同模块的模型放置在不同的文件中,然后再用一个总体文件做包含调用。

  • 可编程接口,比如在XACRO模型文件中,定义一些常量,描述机器人的尺寸,定义一些变量,在调用宏定义的时候传递数据,还可以在模型中做数据计算,甚至加入条件语句,比如你的机器人叫A,就有摄像头,如果叫B,就没有摄像头。

XACRO建模过程就像写代码一样,功能更为丰富了。

接下来,我们就通过XACRO文件对移动机器人的模型做一下优化,大家先要使用这句命令安装必要的功能包。

$ sudo apt install ros-humble-xacro

以下是一些常用的XACRO文件语法,大家了解下。

常量定义

<xacro:property>标签用来定义一些常量,比如这样定义一个PI的常量名为“M_PI”,值为“3.14159”,在调用的时候,通过$加大括号,里边就可以使用定义好的常量了。

针对原本移动机器人的URDF文件,我们就可以把底盘的质量、尺寸,轮子的质量、尺寸、安装位置,这些不会变化的数据,都通过常量定义,未来需要修改的时候也很方便,就不需要在模型文件中一行一行找了。

数学计算

​​​​​​​

如果需要做数学计算,同样是在“${}”中进行,比如某一个位置,我们可以通过这两个常量做运算得到,就加入了加法和除法运算。

在移动机器人的模型中,很多有相对关系的数据,我们尽量都改成公式计算,如果直接写结果的数值,未来修改的时候,可能根本想不起来这个数据是怎么来的。

所有数学运算都会转换成浮点数进行,以保证运算精度

宏定义

机器人的轮子我们也做成宏定义,定义方式是通过这个<xacro:macro>标签描述的,还可以像函数一样,设置里边会用到的一些参数,比如这里的A、B、C。

当需要使用这个宏的时候,就可以像这样,通过宏名字的标签,来调用,同时要记得把几个参数设置好。

比如在模型中,轮子的宏定义是这样的,包含了link描述和joint关节设置,link的名称和关节的位置,是通过输入的参数来区分的,在使用的时候,通过这两句调用,两个轮子就出现了。

这里的1和-1,是设置关节位置的,刚好是一个镜像关系。

文件包含

宏定义是可以嵌套的,于是我们把机器人的底盘也做成了一个宏,然后使用另外一个模型文件,对底盘宏定义的文件做了一个包含,然后再调用。

这种流程是不是似曾相识,很像C语言中的include文件包含,然后再去调用里边的某些函数。

到这里为止,仿真使用的模型优化还没有结束,接下来我们还得加入一些仿真必备的模块和参数。

3. 机器人仿真模型配置

完善物理参数

第一步是确保每一个link都有惯性参数和碰撞属性,因为Gazebo是物理仿真平台,必要的物理参数是一定需要的。

添加Gazebo标签

第二步是为link添加gazebo标签,主要是为了可以在gazebo中渲染每一个link的颜色,因为URDF中的颜色系统和gazebo中的不同,所以得做一步这样的冗余配置。

配置传动装置

第三步是要给运动的joint配置传动装置,可以理解为仿真了一个电机。

添加控制器插件

第四步,要添加一个gazebo的控制器插件,小车是差速控制的,那就添加差速控制器插件,这样在不同角度下两个电机的速度分配,就可以交给控制器插件来完成了。

4. 构建仿真环境

接下来就考虑如何把模型加载到Gazebo中了,需要用到一个gazebo提供的功能节点spwan_entity。

learning_gazebo/launch/load_urdf_into_gazebo.launch.py

import os

from ament_index_python.packages import get_package_share_directory


from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource

from launch_ros.actions import Node


def generate_launch_description():


    # Include the robot_state_publisher launch file, provided by our own package. Force sim time to be enabled
    # !!! MAKE SURE YOU SET THE PACKAGE NAME CORRECTLY !!!

    package_name='learning_gazebo' #<--- CHANGE ME
    world_file_path = 'worlds/neighborhood.world'

    pkg_path = os.path.join(get_package_share_directory(package_name))
    world_path = os.path.join(pkg_path, world_file_path)  

    # Pose where we want to spawn the robot
    spawn_x_val = '0.0'
    spawn_y_val = '0.0'
    spawn_z_val = '0.0'
    spawn_yaw_val = '0.0'

    mbot = IncludeLaunchDescription(
                PythonLaunchDescriptionSource([os.path.join(
                    get_package_share_directory(package_name),'launch','mbot.launch.py'
                )]), launch_arguments={'use_sim_time': 'true', 'world':world_path}.items()
    )

    # Include the Gazebo launch file, provided by the gazebo_ros package
    gazebo = IncludeLaunchDescription(
                PythonLaunchDescriptionSource([os.path.join(
                    get_package_share_directory('gazebo_ros'), 'launch', 'gazebo.launch.py')]),
             )

    # Run the spawner node from the gazebo_ros package. The entity name doesn't really matter if you only have a single robot.
    spawn_entity = Node(package='gazebo_ros', executable='spawn_entity.py',
                        arguments=['-topic', 'robot_description',
                                   '-entity', 'mbot',
                                   '-x', spawn_x_val,
                                   '-y', spawn_y_val,
                                   '-z', spawn_z_val,
                                   '-Y', spawn_yaw_val],
                        output='screen')



    # Launch them all!
    return LaunchDescription([
        mbot,
        gazebo,
        spawn_entity,
    ])

机器人运动仿真

万事俱备,接下来就是见证奇迹的时刻。

我们需要运行两句命令,第一句启动仿真环境,第二句启动键盘控制节点。

$ ros2 launch learning_gazebo load_urdf_into_gazebo.launch.py
$ ros2 run teleop_twist_keyboard teleop_twist_keyboard 

虚拟机中运行时需要关闭硬件加速:echo " export SVGA_VGPU10=0" >> ~/.bashrc

通过键盘上的“i、j、,、l”几个按键,就可以控制机器人前后左右运动啦。 

Ignition:下一代Gazebo

随着技术的进步,Gazebo仿真平台也在不断迭代,新一代的Gazebo命名为Ignition,从渲染效果和仿真流畅度上都有较大的变化,我们不妨也来试一下。

$ sudo apt install ros-humble-ros-ign 
$ ros2 launch ros_ign_gazebo_demos rgbd_camera_bridge.launch.py

运行成功后,会打开Ignition的仿真界面和Rviz上位机,我们可以看到RGBD相机仿真后发布的图像数据。

更多新版本仿真器的信息,大家也可以参考官方网站:

www.ignitionrobotics.org/

第十九讲:Rviz:三维可视化显示平台 

1. Rviz三维可视化平台

机器人开发过程中,各种各样的功能,如果我们只是从数据层面去做分析,很难快速理解数据的效果,比如给你一堆0到255的数字,问这幅图像描述的内容是什么?你肯定一脸懵。但如果我们把这些数字通过颜色渲染出来,岂不就一目了然么?

类似的场景还有很多,比如机器人模型,我们需要知道自己设计的模型长啥样,还有模型内部众多坐标系在运动过程中都在哪些位置。

再比如机械臂运动规划和移动机器人自主导航,我们希望可以看到机器人周边的环境、规划的路径,当然还有传感器的信息,摄像头、三维相机、激光雷达等等,数据是用来做计算的,可视化的效果才是给人看的。

所以,数据可视化可以大大提高开发效率,Rviz就是这样一款机器人开发过程中的数据可视化软件,机器人模型、传感器信息、环境信息等等,全都可以在这里搞定。

Rviz介绍

一句话说明Rviz的功能,只要有数据,它就可以可视化,只有我们想不到的,没有Rviz做不到的。

Rviz的核心框架是基于Qt可视化工具打造的一个开放式平台,官方出厂就自带了很多机器人常用的可视化显示插件,只要我们按照ROS中的消息发布对应的话题,就可以看到图形化的效果了。如果我们对显示的效果不满意,或者想添加某些新的显示项,也可以在Rviz这个平台中,开发更多可视化效果,方便打造我们自己的上位机。

运行方法

启动一个终端,使用如下命令即可启动:

$ ros2 run rviz2 rviz2

2. 彩色相机仿真与可视化

摄像头肯定是最为常用的一种传感器了,我们先来给机器人装上摄像头。

仿真插件配置

关于传感器的仿真,都需要使用Gazebo提供的插件,摄像头对应的插件叫做libgazebo_ros_camera.so,我们对照模型的代码给大家介绍这个插件的使用方法。

learning_gazebo/urdf/sensers/camera_gazebo.xacro

<gazebo reference="${prefix}_link">
    <sensor type="camera" name="camera_node">
        <update_rate>30.0</update_rate>
        <camera name="head">
            <horizontal_fov>1.3962634</horizontal_fov>
            <image>
                <width>1280</width>
                <height>720</height>
                <format>R8G8B8</format>
            </image>
            <clip>
                <near>0.02</near>
                <far>300</far>
            </clip>
            <noise>
                <type>gaussian</type>
                <mean>0.0</mean>
                <stddev>0.007</stddev>
            </noise>
        </camera>
        <plugin name="gazebo_camera" filename="libgazebo_ros_camera.so">
            <ros>
                <!-- <namespace>stereo</namespace> -->
                <remapping>~/image_raw:=image_raw</remapping>
                <remapping>~/camera_info:=camera_info</remapping>
            </ros>
            <camera_name>${prefix}</camera_name>
            <frame_name>${prefix}_link</frame_name>
            <hack_baseline>0.2</hack_baseline>
        </plugin>
    </sensor>
</gazebo>

主要配置项如下:

  • <sensor>标签:描述传感器

type:传感器类型,camera

name:摄像头命名,自由设置

  • <camera>标签:描述摄像头参数

分辨率,编码格式,图像范围,噪音参数等

  • <plugin>标签:加载摄像头仿真插件

运行仿真环境

$ ros2 launch learning_gazebo load_mbot_camera_into_gazebo.launch.py

可以使用命令行看下仿真出来的图像话题:

图像数据可视化

我们使用Rviz可视化显示图像信息,先来启动Rviz:

$ ros2 run rviz2 rviz2

启动成功后,在左侧Displays窗口中点击“Add”,找到Image显示项,OK确认后就可以加入显示列表啦,然后配置好该显示项订阅的图像话题,就可以顺利看到机器人的摄像头图像啦。

3. 三维相机仿真与可视化

二维摄像头不过瘾,想不想试试三维相机,比如我们常用的Kinect体感传感器,或者Intel的Realsense,可以获取外部环境的点云数据。这种相机的价格比usb摄像头可贵不少,不过我们也可以通过仿真,一分钱不用,就可以玩起来。

仿真插件配置

三维相机使用的Gazebo插件也是libgazebo_ros_camera.so,配置方法如下:

learning_gazebo/urdf/sensers/kinect_gazebo.xacro

<gazebo reference="${prefix}_link">
    <sensor type="depth" name="${prefix}">
        <always_on>true</always_on>
        <update_rate>15.0</update_rate>
        <pose>0 0 0 0 0 0</pose>
        <camera name="kinect">
            <horizontal_fov>${60.0*M_PI/180.0}</horizontal_fov>
            <image>
                <format>R8G8B8</format>
                <width>640</width>
                <height>480</height>
            </image>
            <clip>
                <near>0.05</near>
                <far>8.0</far>
            </clip>
        </camera>
        <plugin name="${prefix}_controller" filename="libgazebo_ros_camera.so">
            <ros>
                <!-- <namespace>${prefix}</namespace> -->
                <remapping>${prefix}/image_raw:=rgb/image_raw</remapping>
                <remapping>${prefix}/image_depth:=depth/image_raw</remapping>
                <remapping>${prefix}/camera_info:=rgb/camera_info</remapping>
                <remapping>${prefix}/camera_info_depth:=depth/camera_info</remapping>
                <remapping>${prefix}/points:=depth/points</remapping>
            </ros>
            <camera_name>${prefix}</camera_name>
            <frame_name>${prefix}_frame_optical</frame_name>
            <hack_baseline>0.07</hack_baseline>
            <min_depth>0.001</min_depth>
            <max_depth>300.0</max_depth>
        </plugin>
    </sensor>
</gazebo>

运行仿真环境

使用如下命令启动仿真环境:

$ ros2 launch learning_gazebo load_mbot_rgbd_into_gazebo.launch.py

点云数据可视化

运行Rviz:

$ ros2 run rviz2 rviz2

同样的流程,点击Add,添加PointCloud2,设置订阅的点云话题,还要配置Rviz的参考系是odom,就可以看到点云数据啦,每一个点都是由xyz位置和rgb颜色组成。

4. 激光雷达仿真与可视化

除了摄像头和三维相机,激光雷达也是很多移动机器人常备的传感器,包括自动驾驶汽车,我们也来试一试。

仿真插件配置

雷达使用的Gazebo插件是libgazebo_ros_ray_sensor.so,配置方法如下:

learning_gazebo/urdf/sensers/lidar_gazebo.xacro

<gazebo reference="${prefix}_link">
    <sensor type="ray" name="rplidar">
        <update_rate>20</update_rate>
        <ray>
            <scan>
              <horizontal>
                <samples>360</samples>
                <resolution>1</resolution>
                <min_angle>-3</min_angle>
                <max_angle>3</max_angle>
              </horizontal>
            </scan>
            <range>
              <min>0.10</min>
              <max>30.0</max>
              <resolution>0.01</resolution>
            </range>
            <noise>
              <type>gaussian</type>
              <mean>0.0</mean>
              <stddev>0.01</stddev>
            </noise>
        </ray>
        <plugin name="gazebo_rplidar" filename="libgazebo_ros_ray_sensor.so">
      <ros>
    <namespace>/</namespace>
    <remapping>~/out:=scan</remapping>
      </ros>
      <output_type>sensor_msgs/LaserScan</output_type>
        </plugin>
    </sensor>
</gazebo>

运行仿真环境

使用如下命令启动仿真环境:

$ ros2 launch learning_gazebo load_mbot_laser_into_gazebo.launch.py

在话题列表中也可以看到激光雷达啦。

点云数据可视化

启动Rviz:

$ ros2 run rviz2 rviz2

点击Add,选择Laserscan,然后配置订阅的话题名,rviz的固定坐标系依然是odom,此时就可以看到激光点啦。

5. Rviz vs Gazebo

好啦,通过这几个案例,相信大家对Rviz可视化平台的使用流程已经非常熟悉了,也了解了常用传感器的仿真方法。

讲到这里,Gazebo和Rviz这两个软件的具体功能,大家是不是会有一些混淆。​​​​​​​

我们再来强调下:

  • Gazebo是仿真平台,核心功能是创造数据,我们没有机器人或者传感器,它可以帮我们做一个虚拟的;
  • Rviz是可视化平台,核心功能是显示数据,如果没有数据,它也巧妇难为无米之炊。

所以在很多时候,我们使用Gazebo做机器人仿真的时候,也会启动Rviz来显示仿真环境的信息,如果自己手上有真实机器人的话,Gazebo就用不到了,不过还是会用Rviz显示真实机器人传感器的信息。

第二十讲:RQT:模块化可视化工具

ROS中的Rviz功能已经很强大了,不过有些场景下,我们可能更需要一些简单的模块化的可视化工具,比如只显示一个摄像头的图像,使用Rviz的话,难免会觉得操作有点麻烦。

此时,我们就会用到ROS提供的另外一种模块化可视化工具——rqt

rqt介绍

正如RQT的命名,它和Rviz一样,也是基于QT可视化工具开发而来,在使用前,我们需要通过这样一句指令进行安装,然后就可以通过rqt这个命令启动使用了。

$ sudo apt install ros-humble-rqt
$ rqt

类似这个界面一样,里边可以加载很多小模块,每个模块都可以实现一个具体的小功能,一些常用的功能如下:

日志显示

图像显示

​​​​​​​

发布话题数据/调用服务请求

绘制数据曲线

数据包管理

节点可视化

​​​​​​​

  • 24
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值