艾曼语 发表于 前天 23:50

ROS2节点和话题

从本节开始,我们将以ROS2的核心概念为线索,详细讲解ROS2的应用开发方法,其中包括但不限于:

[*]工作空间(Workspace):项目文件夹,包含所有ROS2包的容器;
[*]功能包(Package):软件模块,包含实现特定功能的代码和资源;
[*]节点(Node):机器人的工作细胞;
[*]话题(Topic):异步通信通道,用于发布/订阅数据流;
[*]服务(Service):同步请求/响应,用于一次性任务;
[*]通信接口(Interface):数据传输的标准结构;
[*]参数(Parameter):运行时配置,可在节点运行时动态调整;
[*]动作(Action):完整行为的流程管理;
[*]分布式通信(Distributed Communication):多计算平台的任务分配;
[*]DDS(Data Distribution Service):机器人的神经网络。
一、工作空间和功能包

1.1 工作空间

在之前的学习和开发中,我们应该有接触过某些集成开发环境,比如Visual Studio、Eclipse、Qt Creator等,当我们想要编写程序之前,都会在这些开发环境的工具栏中,点击一个“创建新工程”的选项,此时就产生一个文件夹,后续所有工作产生的文件,都会放置在这个文件夹中,这个文件夹以及里边的内容,就叫做是工程。
1.1.1 什么是工作空间

类似的,在ROS机器人开发中,我们针对机器人某些功能进行代码开始时,各种编写的代码、参数、脚本等文件,也需要放置在某一个文件夹里进行管理,这个文件夹在ROS系统中就叫做工作空间。
所以工作空间是一个存放项目开发相关文件的文件夹,也是开发过程中存放所有资料的大本营。
ROS系统中一个典型的工作空间结构如图所示,这个dev_ws就是工作空间的根目录,里边会有四个子目录,或者叫做四个子空间;
其中:

[*]src:代码空间,未来编写的代码、脚本,都需要人为的放置到这里;
[*]build:编译空间,保存编译过程中产生的中间文件;对于每个包,将创建一个子文件夹,在其中调用调用编译命令;
[*]install:安装空间,是每个软件包将安装到的位置,默认情况下,每个包都将安装到单独的子目录中;
[*]log:日志空间,编译和运行过程中,保存各种警告、错误、信息等日志。
总体来讲,这四个空间的文件夹,我们绝大部分操作都是在src中进行的,编译成功后,就会执行install里边的可执行文件,build和log两个文件夹用的很少。
1.1.2 创建工作空间

了解了工作空间的概念和结果,接下来我们可以使用如下命令创建一个工作空间,并且下载教程的代码:
pi@NanoPC-T6:~$ mkdir -p ~/dev_ws/src注意:实际上在上一篇博客中我们已经创建了这个工作目录。
下载教程代码:
pi@NanoPC-T6:~$ cd ~/dev_ws/src
pi@NanoPC-T6:~/dev_ws/src$ pwd
/home/pi/dev_ws/src
pi@NanoPC-T6:~/dev_ws/src$ git clone https://gitee.com/guyuehome/ros2_21_tutorials.git1.1.3 自动安装依赖

我们从社区中下载的各种代码,多少都会有一些依赖,我们可以手动一个一个安装,也可以使用rosdep工具自动安装。
rosdep功能能够自动解决依赖关系,让ROS开发从依赖地狱变成一键配置,是专业ROS开发的必备工具。
rosdepc是通过pip安装的Python工具,因此需要先安装Python包管理工具pip3:
pi@NanoPC-T6:~/dev_ws/src$ sudo apt install -y python3-pip注意:该命令需要修改系统级目录 /usr/bin, /usr/lib 等,因此需要root权限。
安装 rosdepc,原版是rosdep,但国内访问慢,rosdepc使用国内镜像,速度快;
pi@NanoPC-T6:~/dev_ws/src$ suo pip3 install rosdepc创建配置文件,设置国内镜像源:
pi@NanoPC-T6:~/dev_ws/src$ sudo rosdepc init注意:需要在 /etc/ros/rosdep/sources.list.d/ 创建配置文件,因此需要root权限。
从配置的源下载最新的包依赖信息,下载数据到用户目录 ~/.ros/rosdep/;
pi@NanoPC-T6:~/dev_ws/src$ rosdepc update自动安装src目录下所有ROS包的依赖;
pi@NanoPC-T6:~/dev_ws/src$ cd ..
pi@NanoPC-T6:~/dev_ws$ rosdepc install -i --from-path src --rosdistro humble -y具体流程如下:

[*]扫描 src 目录:查找所有 package.xml 文件
[*]解析依赖:读取 、 等标签;
[*]映射到系统包:将ROS包名映射到ubuntu/debian包名;
[*]执行安装:实际上调用的是sudo apt install ros-humble-package1 ros-humble-package2 ...。
1.1.4 编译工作空间

依赖安装完成后,就可以使用colcon命令编译工作空间。
我们首先安装colcon ,colcon是ROS2的标准化构建工具,用于编译ROS2工作空间的所有功能包;
pi@NanoPC-T6:~/dev_ws$ sudo apt install python3-colcon-ros执行编译命令;
pi@NanoPC-T6:~/dev_ws$ colcon build该命令执行如下操作:

[*]扫描src/目录下的所有ROS包;
[*]根据包类型(CMake/Python)选择构建方式;
[*]编译所有包;
[*]安装到install/目录。
编译成功后,就可以在工作空间中看到自动生成的build、log、install文件夹了;

1.1.5 设置环境变量

系统默认只搜索系统路径 /opt/ros/humble/,编译成功后,为了让系统能够找到我们的功能包和可执行文件,还需要设置环境变量:
pi@NanoPC-T6:~/dev_ws$ source install/local_setup.shsource 命令将工作空间的路径添加到环境变量中。
为了使所有终端均生效,执行如下命令;
pi@NanoPC-T6:~/dev_ws$ echo "source ~/dev_ws/install/local_setup.sh" >> ~/.bashrc至此,我们就完成了工作空间的创建、编译和配置。
1.2 功能包

想象你正在开发羽绒服分拣机器人,它有多个核心功能:

[*]视觉识别羽绒服;
[*]控制宇树G1底盘移动
[*]操控灵巧手抓取;
[*]规划最优路径;
如果把所有代码混在一个文件夹里,当你只想分享视觉识别模块给同事时,就得像在杂乱的豆子堆里挑出黄豆一样费力。功能包解决了这个痛点,一个工作空间下可以有多个功能包,一个功能包可以有多个节点存在。
如何在ROS2中创建一个功能包呢?我们可以使用这个指令:
ros2 pkg create --build-type <build-type> <package_name>其中:

[*]pkg:表示功能包相关的功能;
[*]create:表示创建功能包;
[*]build-type:表示新创建的功能包是C++还是Python的;

[*]如果使用C++或者C,那这里就跟ament_cmake;
[*]如果使用Python,就跟ament_python;

[*]package_name:新建功能包的名字。
1.2.1 C++版本

比如我们创建一个视觉识别羽绒服的C++版本的功能包;
pi@NanoPC-T6:~/dev_ws$ cd src/
pi@NanoPC-T6:~/dev_ws/src$ ros2 pkg create --build-type ament_cmake jacket_detection_pkg_c首先看下C++类型的功能包;
pi@NanoPC-T6:~/dev_ws/src$ ll jacket_detection_pkg_c/
-rw-rw-r-- 1 pi pi915 Dec 10 14:02 CMakeLists.txt
drwxrwxr-x 3 pi pi 4096 Dec 10 14:02 include/
-rw-rw-r-- 1 pi pi602 Dec 10 14:02 package.xml
drwxrwxr-x 2 pi pi 4096 Dec 10 14:02 src/其中必然存在两个文件:package.xml和CMakerLists.txt。
1.2.1.1 package.xml

package.xml文件的主要内容如下,包含功能包的版权描述,和各种依赖的声明;
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>jacket_detection_pkg_c</name>
<version>0.0.0</version>
<description>TODO: Package description</description>
<maintainer email="root@todo.todo">root</maintainer>
<license>TODO: License declaration</license>

<buildtool_depend>ament_cmake</buildtool_depend>

<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>

<export>
    <build_type>ament_cmake</build_type>
</export>
</package>1.2.1.2 CMakeLists.txt

CMakeLists.txt文件是编译规则,C++代码需要编译才能运行,所以必须要在该文件中设置如何编译,使用CMake语法;
cmake_minimum_required(VERSION 3.8)
project(jacket_detection_pkg_c)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# find dependencies
find_package(ament_cmake REQUIRED)
# uncomment the following section in order to fill in
# further dependencies manually.
# find_package(<dependency> REQUIRED)

if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
# the following line skips the linter which checks for copyrights
# comment the line when a copyright and license is added to all source files
set(ament_cmake_copyright_FOUND TRUE)
# the following line skips cpplint (only works in a git repo)
# comment the line when this package is in a git repo and when
# a copyright and license is added to all source files
set(ament_cmake_cpplint_FOUND TRUE)
ament_lint_auto_find_test_dependencies()
endif()

ament_package()1.2.2 Python版本

比如我们创建一个视觉识别羽绒服的Python版本的功能包;
pi@NanoPC-T6:~/dev_ws$ cd src
pi@NanoPC-T6:~/dev_ws/src$ ros2 pkg create --build-type ament_python jacket_detection_pkg_python首先看下Python类型的功能包;
pi@NanoPC-T6:~/dev_ws/src$ ll jacket_detection_pkg_python/
drwxrwxr-x 2 pi pi 4096 Dec 10 14:03 jacket_detection_pkg_python/
-rw-rw-r-- 1 pi pi637 Dec 10 14:03 package.xml
drwxrwxr-x 2 pi pi 4096 Dec 10 14:03 resource/
-rw-rw-r-- 1 pi pi123 Dec 10 14:03 setup.cfg
-rw-rw-r-- 1 pi pi708 Dec 10 14:03 setup.py
drwxrwxr-x 2 pi pi 4096 Dec 10 14:03 test/C++功能包需要将源码编译成可执行文件,但是Python语言是解析型的,不需要编译,所以会有一些不同,但也会有这两个文件:package.xml和setup.py。
1.2.2.1 package.xml

package.xml文件的主要内容和C++版本功能包一样,包含功能包的版权描述,和各种依赖的声明;
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>jacket_detection_pkg_python</name>
<version>0.0.0</version>
<description>TODO: Package description</description>
<maintainer email="root@todo.todo">root</maintainer>
<license>TODO: License declaration</license>

<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>python3-pytest</test_depend>

<export>
    <build_type>ament_python</build_type>
</export>
</package>1.2.2.2 setup.py

setup.py文件里边也包含一些版权信息,除此之外,还有entry_points配置的程序入口,在后续编程讲解中,我们会给介绍如何使用;
#from setuptools import find_packages, setup

package_name = 'jacket_detection_pkg_python'

setup(
    name=package_name,
    version='0.0.0',
    packages=find_packages(exclude=['test']),
    data_files=[
      ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
      ('share/' + package_name, ['package.xml']),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='root',
    maintainer_email='root@todo.todo',
    description='TODO: Package description',
    license='TODO: License declaration',
    extras_require={
      'test': [
            'pytest',
      ],
    },
    entry_points={
      'console_scripts': [
      ],
    },
)1.3 编译功能包

在创建好的功能包中,我们可以继续完成代码的编写,之后需要编译和配置环境变量,才能正常运行:
pi@NanoPC-T6:~/dev_ws$ colcon build
pi@NanoPC-T6:~/dev_ws$ source install/local_setup.sh二、节点

机器人是各种功能的综合体,每一项功能就像机器人的一个工作细胞,众多细胞通过一些机制连接到一起,成为了一个机器人整体。
2.1 定义

在ROS中,我们给这些细胞取了一个名字,那就是节点。完整的机器人系统可能并不是一个物理上的整体,比如这样一个的机器人:
在机器人身体里搭载了一台计算机A,它可以通过机器人的眼睛——摄像头,获取外界环境的信息,也可以控制机器人的腿——轮子,让机器人移动到想要去的地方。
除此之外,可能还会有另外一台计算机B,放在你的桌子上,它可以远程监控机器人看到的信息,也可以远程配置机器人的速度和某些参数,还可以连接一个摇杆,人为控制机器人前后左右运动。
这些功能虽然位于不同的计算机中,但都是这款机器人的工作细胞,也就是节点,他们共同组成了一个完整的机器人系统;

[*]节点在机器人系统中的职责就是执行某些具体的任务,从计算机操作系统的角度来看,也叫做进程;
[*]每个节点都是一个可以独立运行的可执行文件,比如执行某一个python程序,或者执行C++编译生成的结果,都算是运行了一个节点;
[*]既然每个节点都是独立的执行文件,那自然就可以想到,得到这个执行文件的编程语言可以是不同的,比如C++、Python,乃至Java、Ruby等更多语言;
[*]这些节点是功能各不相同的细胞,根据系统设计的不同,可能位于计算机A,也可能位于计算机B,还有可能运行在云端,这叫做分布式,也就是可以分布在不同的硬件载体上;
[*]每一个节点都需要有唯一的命名,当我们想要去找到某一个节点的时候,或者想要查询某一个节点的状态时,可以通过节点的名称来做查询。
节点也可以比喻是一个一个的工人,分别完成不同的任务,他们有的在一线厂房工作,有的在后勤部门提供保障,他们互相可能并不认识,但却一起推动机器人这座“工厂”,完成更为复杂的任务。
2.2 创建功能包

接下来,我们就来看看, 节点这个工作细胞,到底该如何实现。
创建my_learning_node的Python版本的功能包;
pi@NanoPC-T6:~/dev_ws$ cd src
pi@NanoPC-T6:~/dev_ws/src$ ros2 pkg create --build-type ament_python my_learning_node2.3 Hello World节点(面向过程)

2.3.1 代码实现

使用VS Code加载功能包my_learning_node,在my_learning_node文件夹下创建node_helloworld.py;
"""
ROS2节点示例-发布“Hello ROS2”日志信息, 使用面向过程的实现方式

@author: zy
@since 2025/12/10
"""

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

def main(args=None):                        # ROS2节点主入口main函数
    rclpy.init(args=args)                     # ROS2 Python接口初始化
    node = Node("node_helloworld")            # 创建ROS2节点对象并进行初始化

    while rclpy.ok():                         # ROS2系统是否正常运行
      node.get_logger().info("Hello ROS2")# ROS2日志输出
      time.sleep(0.5)                     # 休眠控制循环时间

    node.destroy_node()                     # 销毁节点对象   
    rclpy.shutdown()                        # 关闭ROS2 Python接口完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:
    entry_points={
      'console_scripts': [
            'node_helloworld       = my_learning_node.node_helloworld:main',
      ],
    },2.3.2 编译运行

编译程序:
pi@NanoPC-T6:~/dev_ws$ colcon build --paths src/my_learning_node运行程序:
pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_node node_helloworld
: Hello ROS2
: Hello ROS2
: Hello ROS2
: Hello ROS2
: Hello ROS2
: Hello ROS22.3.3 流程分析

代码中出现的函数未来会经常用到,我们先不用纠结函数的具体使用方法,更重要的是理解节点的编码流程。
总结一下,想要实现一个节点,代码的实现流程是这样做;

[*]编程接口初始化;
[*]创建节点并初始化;
[*]实现节点功能;
[*]销毁节点并关闭接口;
如果有学习过C++或者Pyhton的话,应该可以发现这里我们使用的是面向过程的编程方法,这种方式虽然实现简单,但是对于稍微复杂一点的机器人系统,就很难做到模块化编码。
2.4 Hello World节点(面向对象)

在ROS2的开发中,我们更推荐使用面向对象的编程方式,比如刚才的代码就可以改成这样,虽然看上去复杂了一些,但是代码会具备更好的可读性和可移植性,调试起来也会更加方便。
2.4.1 代码实现

在my_learning_node文件夹下创建node_helloworld_class.py;
"""
ROS2节点示例-发布“Hello ROS2”日志信息, 使用面向对象的实现方式

@author: zy
@since 2025/12/10
"""

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

"""
创建一个HelloWorld节点, 初始化时输出“hello ROS2”日志
"""
class HelloWorldNode(Node):
    def __init__(self, name):
      super().__init__(name)                     # ROS2节点父类初始化
      while rclpy.ok():                        # ROS2系统是否正常运行
            self.get_logger().info("Hello ROS2")   # ROS2日志输出
            time.sleep(0.5)                        # 休眠控制循环时间

def main(args=None):                               # ROS2节点主入口main函数
    rclpy.init(args=args)                        # ROS2 Python接口初始化
    node = HelloWorldNode("node_helloworld_class") # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                               # 循环等待ROS2退出
    node.destroy_node()                            # 销毁节点对象
    rclpy.shutdown()                               # 关闭ROS2 Python接口完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:
    entry_points={
      'console_scripts': [
            'node_helloworld       = my_learning_node.node_helloworld:main',
            'node_helloworld_class = my_learning_node.node_helloworld_class:main'
      ],
    },2.4.2 编译运行

编译程序:
pi@NanoPC-T6:~/dev_ws$ colcon build --paths src/my_learning_node运行程序:
pi@NanoPC-T6:~/dev_ws$ros2 run my_learning_node node_helloworld_class
: Hello ROS2
: Hello ROS2
: Hello ROS2
: Hello ROS22.4.3 流程分析

所以总体而言,节点的实现方式依然是这四个步骤,只不过编码方式做了一些改变而已;

[*]编程接口初始化;
[*]创建节点并初始化;
[*]实现节点功能;
[*]销毁节点并关闭接口;
到这里为止,大家是不是心里还有一个疑惑,机器人中的节点不能只是打印Hello ROS2吧,是不是得完成一些具体的任务。
2.5 物体识别节点

接下来我们就以机器视觉的任务为例,模拟实际机器人中节点的实现过程。我们先从网上找到一张苹果的图片,通过编写一个节点来识别图片中的苹果;
2.5.1 代码实现

在my_learning_node文件夹下创建node_object.py;
"""
ROS2节点示例-通过颜色识别检测图片中出现的苹果

@author: zy
@since 2025/12/10
"""

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

import cv2                              # OpenCV图像处理库   
import numpy as np                      # Python数值计算库


lower_red = np.array()   # 红色的HSV阈值下限
upper_red = np.array()# 红色的HSV阈值上限


def object_detect(image):
    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 < 150:
            continue

      (x, y, w, h) = cv2.boundingRect(cnt)            # 得到苹果所在轮廓的左上角xy像素坐标及轮廓范围的宽和高
      cv2.drawContours(image, , -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(0)
    cv2.destroyAllWindows()

def main(args=None):                                    # ROS2节点主入口main函数
    rclpy.init(args=args)                                 # ROS2 Python接口初始化
    node = Node("node_object")                            # 创建ROS2节点对象并进行初始化
    node.get_logger().info("ROS2节点示例:检测图片中的苹果")

    image = cv2.imread('/home/pi/dev_ws/src/my_learning_node/my_learning_node/apple.png')# 读取图像
    object_detect(image)                                 # 苹果检测
    rclpy.spin(node)                                       # 循环等待ROS2退出
    node.destroy_node()                                    # 销毁节点对象
    rclpy.shutdown()                                       # 关闭ROS2 Python接口这里主要使用了轮廓检测算法,有关轮廓检测可以参考我们之前的文章《第五节、轮廓检测、直线和圆、多边形检测》。
完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:
    entry_points={
      'console_scripts': [
            'node_helloworld       = my_learning_node.node_helloworld:main',
            'node_helloworld_class = my_learning_node.node_helloworld_class:main',
            'node_object         = my_learning_node.node_object:main',
      ],
    },2.5.2 编译运行

在这个例程中,我们将用到一个图像处理的库OpenCV,运行前请使用如下指令安装;
pi@NanoPC-T6:~/dev_ws$ sudo apt install python3-opencv注意:这里通过系统级apt包管理器进行安装,安装到系统标准目录,与使用Python的pip包管理器安装的相比更加稳定。
查看已安装的包文件;
pi@NanoPC-T6:~/dev_ws$ dpkg -L python3-opencv
/usr
/usr/lib
/usr/lib/python3
/usr/lib/python3/dist-packages
/usr/lib/python3/dist-packages/cv2.cpython-310-aarch64-linux-gnu.so
/usr/share
/usr/share/doc
/usr/share/doc/python3-opencv
/usr/share/doc/python3-opencv/copyright
/usr/share/doc/python3-opencv/changelog.Debian.gz编译程序:
pi@NanoPC-T6:~/dev_ws$ colcon build --paths src/my_learning_node运行程序:
pi@NanoPC-T6:~/dev_ws$ros2 run my_learning_node node_object
: ROS2节点示例:检测图片中的苹果运行结果如下:
2.6 机器视觉识别节点

用图片进行识别好像还不太合理,机器人应该有眼睛呀,没问题,接下来我们就让节点读取摄像头的图像,动态识别其中的橘子,或者类似颜色的物体。
2.6.1 代码实现

在my_learning_node文件夹下创建node_object_webcam.py;
"""
ROS2节点示例-通过摄像头识别检测图片中出现的橘子

@author: zy
@since 2025/12/10
"""

import rclpy                            # ROS2 Python接口库
from rclpy.node import Node             # ROS2 节点类
import cv2                              # OpenCV图像处理库
import numpy as np                      # Python数值计算库

# 橘子的HSV颜色范围
lower_orange = np.array()   # 橙色的HSV阈值下限
upper_orange = np.array()   # 橙色的HSV阈值上限

def object_detect(image):
    hsv_img = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)       # 图像从BGR颜色模型转换为HSV模型
    mask_orange = cv2.inRange(hsv_img, lower_orange, upper_orange)# 图像二值化

    contours, hierarchy = cv2.findContours(mask_orange, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE) # 图像中轮廓检测

    for cnt in contours:                                 # 去除一些轮廓面积太小的噪声
      if cnt.shape < 150:
            continue

      (x, y, w, h) = cv2.boundingRect(cnt)               # 得到橘子所在轮廓的左上角xy像素坐标及轮廓范围的宽和高
      cv2.drawContours(image, , -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 main(args=None):                                       # ROS2节点主入口main函数
    rclpy.init(args=args)                                  # ROS2 Python接口初始化
    node = Node("node_object_webcam")                      # 创建ROS2节点对象并进行初始化
    node.get_logger().info("ROS2节点示例:检测图片中的橘子")

    cap = cv2.VideoCapture('/dev/video21')                  # 使用设备路径

    while rclpy.ok():
      ret, image = cap.read()          # 读取一帧图像

      if ret == True:
            object_detect(image)         # 橘子检测

    cap.release()                        # 释放摄像头
    cv2.destroyAllWindows()            # 关闭所有窗口
    node.destroy_node()                  # 销毁节点对象
    rclpy.shutdown()                     # 关闭ROS2 Python接口

if __name__ == '__main__':
    main()完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:
    entry_points={
      'console_scripts': [
            'node_helloworld       = my_learning_node.node_helloworld:main',
            'node_helloworld_class = my_learning_node.node_helloworld_class:main',
            'node_object         = my_learning_node.node_object:main',
            'node_object_webcam    = my_learning_node.node_object_webcam:main',
      ],
    },2.6.2 硬件连接

接着我们给开发板接上USB摄像头,使用dmeg查看内核打印信息:
pi@NanoPC-T6:~/dev_ws$ dmesg
......
usb 1-1.1: new high-speed USB device number 19 using xhci-hcd
usb 1-1.1: New USB device found, idVendor=0c45, idProduct=6340, bcdDevice= 0.00
usb 1-1.1: New USB device strings: Mfr=2, Product=1, SerialNumber=0
usb 1-1.1: Product: USB 2.0 Camera
usb 1-1.1: Manufacturer: Sonix Technology Co., Ltd.
usb 1-1.1: Found UVC 1.00 device USB 2.0 Camera (0c45:6340)在Linux中,USB摄像头通常对应/dev/videoX设备文件。我们可以通过下面的方法查找摄像头设备文件:
查看所有视频设备及其信息:
pi@NanoPC-T6:~/dev_ws$ v4l2-ctl --list-devices
rk_hdmirx (fdee0000.hdmirx-controller):
      /dev/video20

rkisp-statistics (platform: rkisp):
      /dev/video18
      /dev/video19

rkcif-mipi-lvds2 (platform:rkcif-mipi-lvds2):
      /dev/media0

rkisp_mainpath (platform:rkisp0-vir0):
      /dev/video11
      /dev/video12
      /dev/video13
      /dev/video14
      /dev/video15
      /dev/video16
      /dev/video17
      /dev/media1

USB 2.0 Camera: USB Camera (usb-xhci-hcd.3.auto-1.1):
      /dev/video21
      /dev/video22
      /dev/media2

Failed to open /dev/video0: No such device查看特定摄像头的详细信息:
pi@NanoPC-T6:~/dev_ws$ v4l2-ctl -d /dev/video21 --all
Driver Info:
      Driver name      : uvcvideo
      Card type      : USB 2.0 Camera: USB Camera
      Bus info         : usb-xhci-hcd.3.auto-1.1
      Driver version   : 6.1.118
      Capabilities   : 0x84a00001
                Video Capture
                Metadata Capture
                Streaming
                Extended Pix Format
                Device Capabilities
      Device Caps      : 0x04200001
                Video Capture
                Streaming
                Extended Pix Format
Media Driver Info:
      Driver name      : uvcvideo
      Model            : USB 2.0 Camera: USB Camera
      Serial         :
      Bus info         : usb-xhci-hcd.3.auto-1.1
      Media version    : 6.1.118
      Hardware revision: 0x00000000 (0)
      Driver version   : 6.1.118
Interface Info:
      ID               : 0x03000002
      Type             : V4L Video
Entity Info:
      ID               : 0x00000001 (1)
      Name             : USB 2.0 Camera: USB Camera
      Function         : V4L2 I/O
      Flags            : default
      Pad 0x01000007   : 0: Sink
          Link 0x02000010: from remote pad 0x100000a of entity 'Extension 4' (Video Pixel Formatter): Data, Enabled, Immutable
Priority: 2
Video input : 0 (Input 1: ok)
Format Video Capture:
      Width/Height      : 640/480
      Pixel Format      : 'YUYV' (YUYV 4:2:2)
      Field             : None
      Bytes per Line    : 1280
      Size Image      : 614400
      Colorspace      : sRGB
      Transfer Function : Rec. 709
      YCbCr/HSV Encoding: ITU-R 601
      Quantization      : Default (maps to Limited Range)
      Flags             :
Crop Capability Video Capture:
      Bounds      : Left 0, Top 0, Width 640, Height 480
      Default   : Left 0, Top 0, Width 640, Height 480
      Pixel Aspect: 1/1
Selection Video Capture: crop_default, Left 0, Top 0, Width 640, Height 480, Flags:
Selection Video Capture: crop_bounds, Left 0, Top 0, Width 640, Height 480, Flags:
Streaming Parameters Video Capture:
      Capabilities   : timeperframe
      Frames per second: 25.000 (25/1)
      Read buffers   : 0

User Controls

                     brightness 0x00980900 (int)    : min=-64 max=64 step=1 default=-20 value=-20
                     contrast 0x00980901 (int)    : min=0 max=64 step=1 default=32 value=32
                     saturation 0x00980902 (int)    : min=1 max=128 step=1 default=65 value=65
                            hue 0x00980903 (int)    : min=-40 max=40 step=1 default=-6 value=-6
      white_balance_automatic 0x0098090c (bool)   : default=1 value=1
                        gamma 0x00980910 (int)    : min=72 max=500 step=1 default=110 value=110
                           gain 0x00980913 (int)    : min=0 max=100 step=1 default=0 value=0
         power_line_frequency 0x00980918 (menu)   : min=0 max=2 default=1 value=1 (50 Hz)
                              0: Disabled
                              1: 50 Hz
                              2: 60 Hz
      white_balance_temperature 0x0098091a (int)    : min=2800 max=6500 step=1 default=4600 value=4600 flags=inactive
                      sharpness 0x0098091b (int)    : min=0 max=5 step=1 default=1 value=1
         backlight_compensation 0x0098091c (int)    : min=0 max=2 step=1 default=1 value=1
         
pi@NanoPC-T6:~/dev_ws$ v4l2-ctl -d /dev/video22 --all
Driver Info:
      Driver name      : uvcvideo
      Card type      : USB 2.0 Camera: USB Camera
      Bus info         : usb-xhci-hcd.3.auto-1.1
      Driver version   : 6.1.118
      Capabilities   : 0x84a00001
                Video Capture
                Metadata Capture
                Streaming
                Extended Pix Format
                Device Capabilities
      Device Caps      : 0x04a00000
                Metadata Capture
                Streaming
                Extended Pix Format
Media Driver Info:
      Driver name      : uvcvideo
      Model            : USB 2.0 Camera: USB Camera
      Serial         :
      Bus info         : usb-xhci-hcd.3.auto-1.1
      Media version    : 6.1.118
      Hardware revision: 0x00000000 (0)
      Driver version   : 6.1.118
Interface Info:
      ID               : 0x03000005
      Type             : V4L Video
Entity Info:
      ID               : 0x00000004 (4)
      Name             : USB 2.0 Camera: USB Camera
      Function         : V4L2 I/O
Priority: 2
Format Metadata Capture:
      Sample Format   : 'UVCH' (UVC Payload Header Metadata)
      Buffer Size   : 10240在UVC(USB Video Class)摄像头中,通常会创建多个设备文件,每个对应不同的功能,其中:
特性主视频流设备 (/dev/video21)元数据设备 (/dev/video22)主要功能传输实际的视频帧数据传输视频帧的元数据信息数据内容YUYV/MJPG等编码的视频像素数据时间戳、帧类型、错误标志等使用场景OpenCV、视频录制、实时显示帧同步、时间戳对齐、错误检测数据量较大(如640x480 YUYV ≈ 614KB/帧)较小(如10240字节/帧)OpenCV支持✅ 完全支持❌ 不支持(特殊用途)典型用户应用程序开发者系统级/底层开发者我们如果要拍照,使用的设备应该是/dev/video21,因此我们需要修改node_object_webcam.py文件中使用的摄像头。
2.6.3 编译运行

编译程序:
pi@NanoPC-T6:~/dev_ws$ colcon build --paths src/my_learning_node运行程序:
pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_node node_object_webcam
: ROS2节点示例:检测图片中的橘子
[ WARN:0] global ./modules/videoio/src/cap_gstreamer.cpp (2075) handleMessage OpenCV | GStreamer warning: Embedded video playback halted; module source reported: Could not read from resource.
[ WARN:0] global ./modules/videoio/src/cap_gstreamer.cpp (1053) open OpenCV | GStreamer warning: unable to start pipeline
[ WARN:0] global ./modules/videoio/src/cap_gstreamer.cpp (616) isPipelinePlaying OpenCV | GStreamer warning: GStreamer: pipeline have not been created运行结果如下:
2.7 节点命令操作

查看节点列表:
pi@NanoPC-T6:~/dev_ws$ ros2 node list
/node_object_webcam查看节点信息:
pi@NanoPC-T6:~/dev_ws$ ros2 node info /node_object_webcam
/node_object_webcam
Subscribers:

Publishers:
    /parameter_events: rcl_interfaces/msg/ParameterEvent
    /rosout: rcl_interfaces/msg/Log
Service Servers:
    /node_object_webcam/describe_parameters: rcl_interfaces/srv/DescribeParameters
    /node_object_webcam/get_parameter_types: rcl_interfaces/srv/GetParameterTypes
    /node_object_webcam/get_parameters: rcl_interfaces/srv/GetParameters
    /node_object_webcam/list_parameters: rcl_interfaces/srv/ListParameters
    /node_object_webcam/set_parameters: rcl_interfaces/srv/SetParameters
    /node_object_webcam/set_parameters_atomically: rcl_interfaces/srv/SetParametersAtomically
Service Clients:

Action Servers:

Action Clients:2.8 思考题

现在,我们已经熟悉节点这个工作细胞的概念和实现方法了,回到这个机器人系统的框架图,我们还会发现另外一个问题。
电脑B中的摇杆,要控制机器人运动,这两个节点岂不是应该有某种连接,比如摇杆节点发送一个速度指令给运动节点,收到后机器人开始运动。
同理,如果我们想要改变机器人的速度,负责配置参数的节点就得发送一个指令给运动节点,如果电脑B想要显示机器人看到的图像,电脑A中的摄像头节点就得把图像发送过来。
没错,在一个ROS机器人的系统中,节点并不是孤立的,他们之间会有很多种机制保持联系,接下来,我们将介绍这些机制中最为常用的一种。
三、话题

节点实现了机器人各种各样的功能,但这些功能并不是独立的,之间会有千丝万缕的联系,其中最重要的一种联系方式就是话题,它是节点间传递数据的桥梁。
3.1 通信模型

以两个机器人节点为例。A节点的功能是驱动相机这个硬件设备,获取得到相机拍摄的图像信息,B节点的功能是视频监控,将相机拍摄到的图像实时显示给用户查看。
我们可以想一下,这两个节点是不是必然存在某种关系?没错,节点A要将获取的图像数据传输给节点B,有了数据,节点B才能做这样可视化的渲染。
此时从节点A到节点B传递图像数据的方式,在ROS中,我们就称之为话题,它作为一个桥梁,实现了节点之间某一个方向上的数据传输。
3.1.1 发布/订阅模型

从话题本身的实现角度来看,使用了基于DDS的发布/订阅模型,什么叫发布和订阅呢?
话题数据传输的特性是从一个节点到另外一个节点,发送数据的对象称之为发布者,接收数据的对象称之为订阅者,每一个话题都需要有一个名字,传输的数据也需要有固定的数据类型。
打一个比方,我们平时会看微信公众号,比如有一个公众号,它的名字叫做“硕博直通车”;

[*]这个硕博直通车就是话题名称;
[*]公众号的发布者是硕博直通车的小编;
[*]他会把组织好的机器人知识排版成要求格式的公众号文章,发布出去,这个文章格式,就是话题的数据类型;
[*]如果大家对这个话题感兴趣,就可以订阅“硕博直通车”,成为订阅者之后自然就可以收到硕博直通车的公众号文章,没有订阅的话,也就无法收到。
类似这样的发布/订阅模型在生活中随处可见,比如订阅报纸、订阅杂志等等。
3.1.2 多对多通信

我们再仔细想下这些可以订阅的东西,是不是并不是唯一的,我们每个人可以订阅很多公众号、报纸、杂志,这些公众号、报纸、杂志也可以被很多人订阅,没错,ROS里的话题也是一样,发布者和订阅者的数量并不是唯一的,可以称之为是多对多的通信模型。
因为话题是多对多的模型;

[*]发布控制指令的摇杆可以有1个,也可以有2个、3个;
[*]订阅控制指令的机器人可以有1个,也可以有2个、3个;
如果存在多个发送指令的节点,建议大家要注意区分优先级,不然机器人可能不知道该听谁的了。
3.1.3 异步通信

话题通信还有一个特性,那就是异步,这个词可能有同学是第一次听说?所谓异步,只要是指发布者发出数据后,并不知道订阅者什么时候可以收到,类似硕博直通车公众号发布一篇文章,你什么时候阅读的,硕博直通车根本不知道,报社发出一份报纸,你什么时候收到,报社也是不知道的。这就叫做异步。
异步的特性也让话题更适合用于一些周期发布的数据,比如传感器的数据,运动控制的指令等等,如果某些逻辑性较强的指令,比如修改某一个参数,用话题传输就不太合适了。
3.1.4 消息接口

最后,既然是数据传输,发布者和订阅者就得统一数据的描述格式,不能一个说英文,一个理解成了中文。
在ROS中,话题通信数据的描述格式称之为消息,对应编程语言中数据结构的概念。比如这里的一个图像数据,就会包含图像的长宽像素值、每个像素的RGB等等,在ROS中都有标准定义。
消息是ROS中的一种接口定义方式,与编程语言无关,我们也可以通过.msg后缀的文件自行定义,有了这样的接口,各种节点就像积木块一样,通过各种各样的接口进行拼接,组成复杂的机器人系统。
3.2 创建功能包

创建my_learning_topic的Python版本的功能包;
pi@NanoPC-T6:~/dev_ws$ cd src
pi@NanoPC-T6:~/dev_ws/src$ ros2 pkg create --build-type ament_python my_learning_topic3.3 Hello World话题

了解了话题的基本原理,接下来我们就要开始编写代码啦。

还是从Hello World例程开始;

[*]我们来创建一个发布者,发布话题chatter,周期发送Hello World这个字符串,消息类型是ROS中标准定义的String;
[*]再创建一个订阅者,订阅chatter这个话题,从而接收到Hello World这个字符串。
3.3.1 发布者

使用VS Code加载功能包my_learning_topic,在my_learning_topic文件夹下创建topic_helloworld_pub.py;
"""
OS2话题示例-发布“Hello World”话题

@author: zy
@since 2025/12/11
"""

import rclpy                                     # ROS2 Python接口库
from rclpy.node import Node                      # ROS2 节点类
from std_msgs.msg import String                  # 字符串消息类型

"""
创建一个发布者节点
"""
class PublisherNode(Node):
    def __init__(self, name):
      super().__init__(name)                                    # ROS2节点父类初始化
      self.pub = self.create_publisher(String, "chatter", 10)   # 创建发布者对象(消息类型、话题名、队列长度)
      self.timer = self.create_timer(0.5, self.timer_callback)# 创建一个定时器(单位为秒的周期,定时执行的回调函数)

    def timer_callback(self):                                     # 创建定时器周期执行的回调函数
      msg = String()                                          # 创建一个String类型的消息对象
      msg.data = 'Hello World'                                  # 填充消息对象中的消息数据
      self.pub.publish(msg)                                     # 发布话题消息
      self.get_logger().info('Publishing: "%s"' % msg.data)   # 输出日志信息,提示已经完成话题发布

def main(args=None):                                 # ROS2节点主入口main函数
    rclpy.init(args=args)                            # ROS2 Python接口初始化
    node = PublisherNode("topic_helloworld_pub")   # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                                 # 循环等待ROS2退出
    node.destroy_node()                              # 销毁节点对象
    rclpy.shutdown()                                 # 关闭ROS2 Python接口完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:
    entry_points={
      'console_scripts': [
            'topic_helloworld_pub= my_learning_topic.topic_helloworld_pub:main',
      ],
    },对以上程序进行分析,如果我们想要实现一个发布者,流程如下:

[*]编程接口初始化;
[*]创建节点并初始化;
[*]创建发布者对象;
[*]创建并填充话题消息;
[*]发布话题消息;
[*]销毁节点并关闭接口。
3.3.2订阅者

在my_learning_topic文件夹下创建topic_helloworld_sub.py;
"""
OS2话题示例-订阅“Hello World”话题消息

@author: zy
@since 2025/12/11
"""

import rclpy                      # ROS2 Python接口库
from rclpy.node   import Node   # ROS2 节点类
from std_msgs.msg import String   # ROS2标准定义的String消息

"""
创建一个订阅者节点
"""
class SubscriberNode(Node):
    def __init__(self, name):
      super().__init__(name)                           # ROS2节点父类初始化
      self.sub = self.create_subscription(\
            String, "chatter", self.listener_callback, 10) # 创建订阅者对象(消息类型、话题名、订阅者回调函数、队列长度)

    def listener_callback(self, msg):                      # 创建回调函数,执行收到话题消息后对数据的处理
      self.get_logger().info('I heard: "%s"' % msg.data) # 输出日志信息,提示订阅收到的话题消息

def main(args=None):                               # ROS2节点主入口main函数
    rclpy.init(args=args)                        # ROS2 Python接口初始化
    node = SubscriberNode("topic_helloworld_sub")# 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                               # 循环等待ROS2退出
    node.destroy_node()                            # 销毁节点对象
    rclpy.shutdown()                               # 关闭ROS2 Python接口完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:
    entry_points={
      'console_scripts': [
            'topic_helloworld_pub= my_learning_topic.topic_helloworld_pub:main',
            'topic_helloworld_sub= my_learning_topic.topic_helloworld_sub:main',
      ],
    },对以上程序进行分析,如果我们想要实现一个订阅者,流程如下:

[*]编程接口初始化;
[*]创建节点并初始化;
[*]创建订阅者对象;
[*]回调函数处理话题数据;
[*]销毁节点并关闭接口。
3.3.3 编译运行

编译程序:
pi@NanoPC-T6:~/dev_ws$ colcon build --paths src/my_learning_topic启动第一个终端,运行话题的发布者节点:
pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_topic topic_helloworld_pub
: Publishing: "Hello World"
: Publishing: "Hello World"
: Publishing: "Hello World"
: Publishing: "Hello World"启动第二个终端,运行话题的订阅者节点:
pi@NanoPC-T6:~/dev_ws$ros2 run my_learning_topic topic_helloworld_sub
: I heard: "Hello World"
: I heard: "Hello ROS2"
: I heard: "Hello ROS2"
: I heard: "Hello ROS2"好啦,Hello World例程大家一定还不过瘾,接下来我们基于话题通信,继续优化下之前的机器视觉例程。
3.4 机器视觉识别

在节点概念的讲解过程中,我们通过一个节点驱动了相机,并且实现了对橙色物体的识别。
功能虽然没问题,但是对于机器人开发来讲,并没有做到程序的模块化,更好的方式是将相机驱动和视觉识别做成两个节点,节点间的联系就是这个图像数据,通过话题周期传输即可。

这个图像消息在ROS中是标准定义好的,如果未来要更换另一个相机,只需要修改驱动节点,视觉识别节点完全是软件功能,就可以保持不变了,这种模块化的设计思想,可以更好的保证软件的可移植性。
3.4.1 发布者

在my_learning_topic文件夹下创建topic_webcam_pub.py;
"""
ROS2话题示例-发布图像话题

@author: zy
@since 2025/12/11
"""

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图像处理库

"""
创建一个发布者节点
"""
class ImagePublisher(Node):

    def __init__(self, name):
      super().__init__(name)                                           # ROS2节点父类初始化
      self.publisher_ = self.create_publisher(Image, 'image_raw', 10)# 创建发布者对象(消息类型、话题名、队列长度)
      self.timer = self.create_timer(0.1, self.timer_callback)         # 创建一个定时器(单位为秒的周期,定时执行的回调函数)
      self.cap = cv2.VideoCapture(21)                                  # 创建一个视频采集对象,驱动相机采集图像(相机设备号)
      self.cv_bridge = CvBridge()                                    # 创建一个图像转换对象,用于稍后将OpenCV的图像转换成ROS的图像消息

    def timer_callback(self):
      ret, frame = self.cap.read()                         # 一帧一帧读取图像

      if ret == True:                                    # 如果图像读取成功
            self.publisher_.publish(
                self.cv_bridge.cv2_to_imgmsg(frame, 'bgr8')) # 发布图像消息

      self.get_logger().info('Publishing video frame')   # 输出日志信息,提示已经完成图像话题发布

def main(args=None):                                 # ROS2节点主入口main函数
    rclpy.init(args=args)                            # ROS2 Python接口初始化
    node = ImagePublisher("topic_webcam_pub")      # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                                 # 循环等待ROS2退出
    node.destroy_node()                              # 销毁节点对象
    rclpy.shutdown()                                 # 关闭ROS2 Python接口完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:
    entry_points={
      'console_scripts': [
            'topic_helloworld_pub= my_learning_topic.topic_helloworld_pub:main',
            'topic_helloworld_sub= my_learning_topic.topic_helloworld_sub:main',
            'topic_webcam_pub      = my_learning_topic.topic_webcam_pub:main',
      ],
    },3.4.2 订阅者

在my_learning_topic文件夹下创建topic_webcam_sub.py;
"""
ROS2话题示例-订阅图像话题

@author: zy
@since 2025/12/11
"""

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数值计算库

# 橘子的HSV颜色范围
lower_orange = np.array()   # 橙色的HSV阈值下限
upper_orange = np.array()   # 橙色的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的图像消息的互相转换

    def object_detect(self, image):
      hsv_img = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)               # 图像从BGR颜色模型转换为HSV模型
      mmask_orange = cv2.inRange(hsv_img, lower_orange, upper_orange)# 图像二值化
      contours, hierarchy = cv2.findContours(
            mmask_orange, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)          # 图像中轮廓检测

      for cnt in contours:                                             # 去除一些轮廓面积太小的噪声
            if cnt.shape < 150:
                continue

            (x, y, w, h) = cv2.boundingRect(cnt)            # 得到橘子所在轮廓的左上角xy像素坐标及轮廓范围的宽和高
            cv2.drawContours(image, , -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(10)

    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("topic_webcam_sub")# 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                            # 循环等待ROS2退出
    node.destroy_node()                         # 销毁节点对象
    rclpy.shutdown()                            # 关闭ROS2 Python接口完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:
    entry_points={
      'console_scripts': [
            'topic_helloworld_pub= my_learning_topic.topic_helloworld_pub:main',
            'topic_helloworld_sub= my_learning_topic.topic_helloworld_sub:main',
            'topic_webcam_pub      = my_learning_topic.topic_webcam_pub:main',
            'topic_webcam_sub      = my_learning_topic.topic_webcam_sub:main',
      ],
    },3.4.3 编译运行

编译程序:
pi@NanoPC-T6:~/dev_ws$ colcon build --paths src/my_learning_topic启动第一个终端,运行话题的发布者节点,第一个节点驱动相机并发布图像话题;
pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_topic topic_webcam_pub
[ WARN:0] global ./modules/videoio/src/cap_gstreamer.cpp (1100) open OpenCV | GStreamer warning: Cannot query video position: status=0, value=-1, duration=-1
: Publishing video frame
: Publishing video frame
: Publishing video frame
: Publishing video frame启动第二个终端,运行话题的订阅者节点,第二个节点订阅图像话题并实现视觉识别;
pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_topic topic_webcam_sub
: Receiving video frame
: Receiving video frame
: Receiving video frame
: Receiving video frame将橘色物体放入相机范围内,即可看到识别效果;
3.5 机器视觉识别优化

常用的USB相机驱动一般都是通用的,ROS中也集成了usb相机的标准驱动ros-humble-usb-cam,我们只需要通过这样一行指令,就可以安装好;
pi@NanoPC-T6:~/dev_ws$ sudo apt install ros-humble-usb-camros-humble-usb-cam是一个用于在ROS2环境中与USB摄像头交互的软件包,其主要目的是简化摄像头数据的采集和集成,为机器人视觉应用提供基础支持。该软件包的核心功能包括:

[*]‌图像数据采集:它能够从连接的USB摄像头中定期获取图像或视频流,并将其转换为ROS2可识别的消息格式,以便其它节点订阅和使用;
[*]‌参数配置:支持通过ROS2的参数服务器调整摄像头参数,例如分辨率、帧率、亮度、对比度等,以适应不同场景的需求;
[*]‌与其它节点集成:采集的图像数据可以轻松与其它ROS2节点结合,用于目标检测、视觉导航或图像处理等高级任务,提升机器人环境感知能力。
这样就可以直接使用ROS中的相机驱动节点发布标准的图像话题了,我们将刚才自己写的图像发布节点换成这样一句指令;
pi@NanoPC-T6:~/dev_ws$ros2 run usb_cam usb_cam_node_exe --ros-args -p video_device:="/dev/video21"
: camera_name value: default_cam
: framerate: 30.000000
: using default calibration URL
: camera calibration URL: file:///home/pi/.ros/camera_info/default_cam.yaml
: Unable to open camera calibration file
: Camera calibration file /home/pi/.ros/camera_info/default_cam.yaml not found
Cannot open device: `/dev/video7`, double-check read / write permissions for device
Cannot open device: `/dev/video5`, double-check read / write permissions for device
Cannot open device: `/dev/video3`, double-check read / write permissions for device
Could not retrieve device capabilities: `/dev/v4l-subdev2`
Cannot open device: `/dev/video1`, double-check read / write permissions for device
Could not retrieve device capabilities: `/dev/v4l-subdev0`
Cannot open device: `/dev/video8`, double-check read / write permissions for device
Cannot open device: `/dev/video6`, double-check read / write permissions for device
Cannot open device: `/dev/video4`, double-check read / write permissions for device
Cannot open device: `/dev/video2`, double-check read / write permissions for device
Could not retrieve device capabilities: `/dev/v4l-subdev1`
Cannot open device: `/dev/video0`, double-check read / write permissions for device
Cannot open device: `/dev/video10`, double-check read / write permissions for device
Cannot open device: `/dev/video9`, double-check read / write permissions for device
: Starting 'default_cam' (/dev/video21) at 640x480 via mmap (yuyv) at 30 FPS
No accelerated colorspace conversion found from yuv422p to rgb24.
This device supports the following formats:
      YUYV 4:2:2 640 x 480 (30 Hz)
      YUYV 4:2:2 640 x 480 (25 Hz)
      YUYV 4:2:2 640 x 480 (20 Hz)
      YUYV 4:2:2 640 x 480 (15 Hz)
      YUYV 4:2:2 640 x 480 (10 Hz)
      YUYV 4:2:2 640 x 480 (5 Hz)
      YUYV 4:2:2 352 x 288 (30 Hz)
      YUYV 4:2:2 352 x 288 (25 Hz)
      YUYV 4:2:2 352 x 288 (20 Hz)
      YUYV 4:2:2 352 x 288 (15 Hz)
      YUYV 4:2:2 352 x 288 (10 Hz)
      YUYV 4:2:2 352 x 288 (5 Hz)
      YUYV 4:2:2 320 x 240 (30 Hz)
      YUYV 4:2:2 320 x 240 (25 Hz)
      YUYV 4:2:2 320 x 240 (20 Hz)
      YUYV 4:2:2 320 x 240 (15 Hz)
      YUYV 4:2:2 320 x 240 (10 Hz)
      YUYV 4:2:2 320 x 240 (5 Hz)
      YUYV 4:2:2 176 x 144 (30 Hz)
      YUYV 4:2:2 176 x 144 (25 Hz)
      YUYV 4:2:2 176 x 144 (20 Hz)
      YUYV 4:2:2 176 x 144 (15 Hz)
      YUYV 4:2:2 176 x 144 (10 Hz)
      YUYV 4:2:2 176 x 144 (5 Hz)
      YUYV 4:2:2 160 x 120 (30 Hz)
      YUYV 4:2:2 160 x 120 (25 Hz)
      YUYV 4:2:2 160 x 120 (20 Hz)
      YUYV 4:2:2 160 x 120 (15 Hz)
      YUYV 4:2:2 160 x 120 (10 Hz)
      YUYV 4:2:2 160 x 120 (5 Hz)
: Setting 'brightness' to 50
unknown control 'white_balance_temperature_auto'

: Setting 'white_balance_temperature_auto' to 1
: Setting 'exposure_auto' to 3
unknown control 'exposure_auto'

: Setting 'focus_auto' to 0
unknown control 'focus_auto'

: Timer triggering every 33 ms视觉识别节点不需要做任何变化;
pi@NanoPC-T6:~/dev_ws$ ros2 run learning_topic topic_webcam_sub
: Receiving video frame
: Receiving video frame
: Receiving video frame
: Receiving video frame3.6 话题命令操作

查看话题列表:
pi@NanoPC-T6:~/dev_ws$ ros2 topic list
/camera_info
/image_raw
/image_raw/compressed
/image_raw/compressedDepth
/image_raw/theora
/parameter_events
/rosout查看话题信息:
pi@NanoPC-T6:~/dev_ws$ ros2 topic info /image_raw
Type: sensor_msgs/msg/Image
Publisher count: 1
Subscription count: 0查看话题发布频率:
pi@NanoPC-T6:~/dev_ws$ ros2 topic hz /image_raw
average rate: 25.045
      min: 0.036s max: 0.044s std dev: 0.00204s window: 26
average rate: 25.021
      min: 0.036s max: 0.044s std dev: 0.00187s window: 52
average rate: 25.021
      min: 0.035s max: 0.045s std dev: 0.00207s window: 78查看话题传输带宽:
pi@NanoPC-T6:~/dev_ws$ ros2 topic bw /image_raw
Subscribed to
15.32 MB/s from 24 messages
      Message size mean: 0.61 MB min: 0.61 MB max: 0.61 MB
15.03 MB/s from 48 messages
      Message size mean: 0.61 MB min: 0.61 MB max: 0.61 MB
15.14 MB/s from 73 messages
      Message size mean: 0.61 MB min: 0.61 MB max: 0.61 MB
14.89 MB/s from 96 messages
      Message size mean: 0.61 MB min: 0.61 MB max: 0.61 MB查看话题数据:
pi@NanoPC-T6:~/dev_ws$ ros2 topic echo /image_raw
header:
stamp:
    sec: 1765466162
    nanosec: 121770000
frame_id: default_cam
height: 480
width: 640
encoding: yuv422_yuy2
is_bigendian: 0
step: 1280
data:
.....发布话题消息,格式如下:
pi@NanoPC-T6:~/dev_ws$ ros2 topic pub <topic_name> <msg_type> <msg_data>3.7 思考题

关于话题通信的原理和实现方法我们就讲到这里,给大家留一个思考题:话题通信的特性是单向传输,适合周期性的数据传递,对于一个复杂的机器人系统来讲,这种特性肯定无法满足所有数据传输的需求,大家是否能够举几个例子,是话题通信无法完成的呢?
参考文章
古月居ROS2入门教程学习笔记
神仙级ROS2入门教程(非常详细),从零基础入门到精通,从看这篇开始
ROS各种资源的一个索引网站
Robot Operating System
ROS问答网站
ROS论坛
ROS功能包存储的数据库

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: ROS2节点和话题