参数

话题、服务、动作,不知道这三种通信机制大家是否已经了解清楚,本节我们再来介绍一种ROS系统中常用的数据传输方式——参数

类似C++编程中的全局变量,可以便于在多个程序中共享某些数据,参数是ROS机器人系统中的全局字典,可以运行多个节点中共享数据。

通信模型

比如在机器视觉识别的时候,有很多参数都会影响视觉识别的效果。

image-20220528013439656

在NodeA相机驱动节点中,就需要考虑很多问题,相机连接到哪个usb端口,使用的图像分辨率是多少,曝光度和编码格式分别是什么,这些都可以通过参数设置,我们可以通过配置文件或者程序进行设置。

NodeB节点中也是一样,图像识别使用的阈值是多少,整个图像面积很大,那个部分是我们关注的核心区域,识别过程是否需要美颜等等,就像我们使用美颜相机一样,我们可以通过滑动条或者输入框设置很多参数,不同参数设置后,都会改变执行功能的一些效果。

这就是参数的作用。

全局字典

在ROS系统中,参数是以全局字典的形态存在的,什么叫字典?就像真实的字典一样,由名称和数值组成,也叫做键和值,合成键值。或者我们也可以理解为,就像编程中的参数一样,有一个参数名 ,然后跟一个等号,后边就是参数值了,在使用的时候,访问这个参数名即可。

可动态监控

在ROS2中,参数的特性非常丰富,比如某一个节点共享了一个参数,其他节点都可以访问,如果某一个节点对参数进行了修改,其他节点也有办法立刻知道,从而获取最新的数值。这在参数的高级编程中,大家都可能会用到。

案例一:小海龟例程中的参数

在小海龟的例程中,仿真器也提供了不少参数,我们一起来通过这个例程,熟悉下参数的含义和命令行的使用方法。

启动两个终端,分别运行小海龟仿真器和键盘控制节点:

$ ros2 run turtlesim turtlesim_node
$ ros2 run turtlesim turtle_teleop_key

image-20220528013639558

查看参数列表

当前系统中有哪些参数呢?我们可以启动一个终端,并使用如下命令查询:


$ ros2 param list

image-20220528013700730

参数查询与修改

如果想要查询或者修改某个参数的值,可以在param命令后边跟get或者set子命令:


$ ros2 param describe turtlesim background_b   # 查看某个参数的描述信息
$ ros2 param get turtlesim background_b        # 查询某个参数的值
$ ros2 param set turtlesim background_b 10     # 修改某个参数的值

参数文件保存与加载

一个一个查询/修改参数太麻烦了,不如试一试参数文件,ROS中的参数文件使用yaml格式,可以在param命令后边跟dump子命令,将某个节点的参数都保存到文件中,或者通过load命令一次性加载某个参数文件中的所有内容:

$ ros2 param dump turtlesim >> turtlesim.yaml  # 将某个节点的参数保存到参数文件中
$ ros2 param load turtlesim turtlesim.yaml     # 一次性加载某一个文件中的所有参数

image-20220528013752068

案例二:参数编程

接下来就要开始写程序了,在程序中设置参数和读取参数都比较简单,一两句函数就可以实现,我们先来体验一下这几个函数的使用方法。

运行效果

启动一个终端,先运行第一句指令,启动param_declare节点,终端中可以看到循环打印的日志信息,其中的“mbot”就是我们设置的一个参数值,参数名称是“robot_name”,通过命令行修改这个参数,看下终端中会发生什么?

$ ros2 run learning_parameter param_declare
$ ros2 param set param_declare robot_name turtle

image-20220528014153014

代码解析

我们来看下在代码中,如何声明、创建、修改一个参数的值。

learning_parameter/param_declare.py

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

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2参数示例-创建、读取、修改参数
"""

import rclpy                                     # ROS2 Python接口库
from rclpy.node   import Node                    # ROS2 节点类

class ParameterNode(Node):
    def __init__(self, name):
        super().__init__(name)                                    # ROS2节点父类初始化
        self.timer = self.create_timer(2, self.timer_callback)    # 创建一个定时器(单位为秒的周期,定时执行的回调函数)
        self.declare_parameter('robot_name', 'mbot')              # 创建一个参数,并设置参数的默认值

    def timer_callback(self):                                      # 创建定时器周期执行的回调函数
        robot_name_param = self.get_parameter('robot_name').get_parameter_value().string_value   # 从ROS2系统中读取参数的值

        self.get_logger().info('Hello %s!' % robot_name_param)     # 输出日志信息,打印读取到的参数值

        new_name_param = rclpy.parameter.Parameter('robot_name',   # 重新将参数值设置为指定值
                            rclpy.Parameter.Type.STRING, 'mbot')
        all_new_parameters = [new_name_param]
        self.set_parameters(all_new_parameters)                    # 将重新创建的参数列表发送给ROS2系统

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

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

entry_points={
    'console_scripts': [
     'param_declare          = learning_parameter.param_declare:main',
    ],
},

案例三:机器视觉应用

参数大家已经会使用了,如何在机器人中应用呢?

继续优化机器视觉的示例,物体识别对光线比较敏感,不同的环境大家使用的阈值也是不同的,每次在代码中修改阈值还挺麻烦,不如我们就把阈值提炼成参数,运行过程中就可以动态设置,不是大大提高了程序的易用性么?

说干就干,我们先来看下效果如何,再看下代码中的变化。

运行效果

启动三个终端,分别运行:

  1. 相机驱动节点
  2. 视觉识别节点
  3. 修改红色阈值
  4. $ ros2 run usb_cam usb_cam_node_exe 
    $ ros2 run learning_parameter param_object_detect
    $ ros2 param set param_object_detect red_h_upper 180
    
  5. 在启动的视觉识别节点中,我们故意将视觉识别中红色阈值的上限设置为0,如果不修改参数,将无法实现目标识别。

    image-20220530160304929

    为了便于调整阈值,我们在节点中将红色阈值的限位修改为了ROS参数,通过命令行修改该参数的值,就可以实现视觉识别啦。

  6. image-20220528014323915

    image-20220530160505538

    代码解析

    我们来看下在视觉识别的代码中,是如何通过参数来设置阈值的。

    learning_parameter/param_object_detect.py

  7. #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    
    """
    @作者: 古月居(www.guyuehome.com)
    @说明: ROS2参数示例-设置目标识别的颜色阈值参数
    """
    
    import rclpy                      # ROS2 Python接口库
    from rclpy.node import Node       # ROS2 节点类
    from sensor_msgs.msg import Image # 图像消息类型
    from cv_bridge import CvBridge    # ROS与OpenCV图像转换类
    import cv2                        # Opencv图像处理库
    import numpy as np                # Python数值计算库
    
    lower_red = np.array([0, 90, 128])     # 红色的HSV阈值下限
    upper_red = np.array([180, 255, 255])  # 红色的HSV阈值上限
    
    """
    创建一个订阅者节点
    """
    class ImageSubscriber(Node):
      def __init__(self, name):
        super().__init__(name)                                  # ROS2节点父类初始化    
        self.sub = self.create_subscription(Image,              # 创建订阅者对象(消息类型、话题名、订阅者回调函数、队列长度)     
                      'image_raw', self.listener_callback, 10) 
        self.cv_bridge = CvBridge()                             # 创建一个图像转换对象,用于OpenCV图像与ROS的图像消息的互相转换
    
        self.declare_parameter('red_h_upper', 0)                # 创建一个参数,表示阈值上限
        self.declare_parameter('red_h_lower', 0)                # 创建一个参数,表示阈值下限
    
      def object_detect(self, image):
        upper_red[0] = self.get_parameter('red_h_upper').get_parameter_value().integer_value    # 读取阈值上限的参数值
        lower_red[0] = self.get_parameter('red_h_lower').get_parameter_value().integer_value    # 读取阈值下限的参数值
        self.get_logger().info('Get Red H Upper: %d, Lower: %d' % (upper_red[0], lower_red[0])) # 通过日志打印读取到的参数值
    
        hsv_img = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)                                        # 图像从BGR颜色模型转换为HSV模型
        mask_red = cv2.inRange(hsv_img, lower_red, upper_red)                                   # 图像二值化
        contours, hierarchy = cv2.findContours(mask_red, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)  # 图像中轮廓检测
        for cnt in contours:                                                                    # 去除一些轮廓面积太小的噪声
            if cnt.shape[0] < 150:
                continue
    
            (x, y, w, h) = cv2.boundingRect(cnt)                            # 得到苹果所在轮廓的左上角xy像素坐标及轮廓范围的宽和高
            cv2.drawContours(image, [cnt], -1, (0, 255, 0), 2)              # 将苹果的轮廓勾勒出来
            cv2.circle(image, (int(x+w/2), int(y+h/2)), 5, (0, 255, 0), -1) # 将苹果的图像中心点画出来
    
        cv2.imshow("object", image)                                         # 使用OpenCV显示处理后的图像效果
        cv2.waitKey(50)
    
      def listener_callback(self, data):
        self.get_logger().info('Receiving video frame')     # 输出日志信息,提示已进入回调函数
        image = self.cv_bridge.imgmsg_to_cv2(data, "bgr8")  # 将ROS的图像消息转化成OpenCV图像
        self.object_detect(image)                            # 苹果检测
    
    def main(args=None):                                    # ROS2节点主入口main函数
        rclpy.init(args=args)                               # ROS2 Python接口初始化
        node = ImageSubscriber("param_object_detect")       # 创建ROS2节点对象并进行初始化
        rclpy.spin(node)                                    # 循环等待ROS2退出
        node.destroy_node()                                 # 销毁节点对象
        rclpy.shutdown()                                    # 关闭ROS2 Python接口
    
  8. 完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:
  9. entry_points={
        'console_scripts': [
         'param_declare          = learning_parameter.param_declare:main',
         'param_object_detect    = learning_parameter.param_object_detect:main',
        ],
    },
    
  10. 参考链接

    https://docs.ros.org/en/humble/Tutorials/Parameters/Understanding-ROS2-Parameters.html

本系列视频公开课:https://class.guyuehome.com/detail/p_628f4288e4b01c509ab5bc7a/6