从本节开始,我们将以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.git
复制代码 1.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.sh
复制代码 source 命令将工作空间的路径添加到环境变量中。
为了使所有终端均生效,执行如下命令;- 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 pi 915 Dec 10 14:02 CMakeLists.txt
- drwxrwxr-x 3 pi pi 4096 Dec 10 14:02 include/
- -rw-rw-r-- 1 pi pi 602 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 pi 637 Dec 10 14:03 package.xml
- drwxrwxr-x 2 pi pi 4096 Dec 10 14:03 resource/
- -rw-rw-r-- 1 pi pi 123 Dec 10 14:03 setup.cfg
- -rw-rw-r-- 1 pi pi 708 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_node
复制代码 2.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
- [INFO] [1765376447.481679427] [node_helloworld]: Hello ROS2
- [INFO] [1765376447.984952328] [node_helloworld]: Hello ROS2
- [INFO] [1765376448.487107243] [node_helloworld]: Hello ROS2
- [INFO] [1765376448.989235494] [node_helloworld]: Hello ROS2
- [INFO] [1765376449.492952293] [node_helloworld]: Hello ROS2
- [INFO] [1765376449.996062841] [node_helloworld]: Hello ROS2
复制代码 2.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
- [INFO] [1765377767.127477791] [node_helloworld_class]: Hello ROS2
- [INFO] [1765377767.630748912] [node_helloworld_class]: Hello ROS2
- [INFO] [1765377768.134523430] [node_helloworld_class]: Hello ROS2
- [INFO] [1765377768.637336571] [node_helloworld_class]: Hello ROS2
复制代码 2.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([0, 90, 128]) # 红色的HSV阈值下限
- upper_red = np.array([180, 255, 255]) # 红色的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[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(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
- [INFO] [1765380073.155551331] [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([10, 100, 100]) # 橙色的HSV阈值下限
- upper_orange = np.array([25, 255, 255]) # 橙色的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[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 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
- ......
- [12731.276915] usb 1-1.1: new high-speed USB device number 19 using xhci-hcd
- [12731.408824] usb 1-1.1: New USB device found, idVendor=0c45, idProduct=6340, bcdDevice= 0.00
- [12731.408879] usb 1-1.1: New USB device strings: Mfr=2, Product=1, SerialNumber=0
- [12731.408903] usb 1-1.1: Product: USB 2.0 Camera
- [12731.408922] usb 1-1.1: Manufacturer: Sonix Technology Co., Ltd.
- [12731.452930] 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
- [INFO] [1765382534.514690053] [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_topic
复制代码 3.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
- [INFO] [1765463674.845518990] [topic_helloworld_pub]: Publishing: "Hello World"
- [INFO] [1765463675.317160733] [topic_helloworld_pub]: Publishing: "Hello World"
- [INFO] [1765463675.817255529] [topic_helloworld_pub]: Publishing: "Hello World"
- [INFO] [1765463676.316123657] [topic_helloworld_pub]: Publishing: "Hello World"
复制代码 启动第二个终端,运行话题的订阅者节点:- pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_topic topic_helloworld_sub
- [INFO] [1765463719.846173171] [topic_helloworld_sub]: I heard: "Hello World"
- [INFO] [1765463720.318453637] [topic_helloworld_sub]: I heard: "Hello ROS2"
- [INFO] [1765463720.818542895] [topic_helloworld_sub]: I heard: "Hello ROS2"
- [INFO] [1765463721.318709238] [topic_helloworld_sub]: 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([10, 100, 100]) # 橙色的HSV阈值下限
- upper_orange = np.array([25, 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的图像消息的互相转换
- 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[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(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
- [INFO] [1765465104.247097408] [topic_webcam_pub]: Publishing video frame
- [INFO] [1765465104.258114866] [topic_webcam_pub]: Publishing video frame
- [INFO] [1765465104.349890317] [topic_webcam_pub]: Publishing video frame
- [INFO] [1765465104.450891892] [topic_webcam_pub]: Publishing video frame
复制代码 启动第二个终端,运行话题的订阅者节点,第二个节点订阅图像话题并实现视觉识别;- pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_topic topic_webcam_sub
- [INFO] [1765465166.071236938] [topic_webcam_sub]: Receiving video frame
- [INFO] [1765465166.302814671] [topic_webcam_sub]: Receiving video frame
- [INFO] [1765465166.320103964] [topic_webcam_sub]: Receiving video frame
- [INFO] [1765465166.336370407] [topic_webcam_sub]: Receiving video frame
复制代码 将橘色物体放入相机范围内,即可看到识别效果;
3.5 机器视觉识别优化
常用的USB相机驱动一般都是通用的,ROS中也集成了usb相机的标准驱动ros-humble-usb-cam,我们只需要通过这样一行指令,就可以安装好;- pi@NanoPC-T6:~/dev_ws$ sudo apt install ros-humble-usb-cam
复制代码 ros-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"
- [INFO] [1765465734.988520589] [usb_cam]: camera_name value: default_cam
- [WARN] [1765465734.988688000] [usb_cam]: framerate: 30.000000
- [INFO] [1765465734.990625191] [usb_cam]: using default calibration URL
- [INFO] [1765465734.990685273] [usb_cam]: camera calibration URL: file:///home/pi/.ros/camera_info/default_cam.yaml
- [ERROR] [1765465734.990811852] [camera_calibration_parsers]: Unable to open camera calibration file [/home/pi/.ros/camera_info/default_cam.yaml]
- [WARN] [1765465734.990832560] [usb_cam]: 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
- [INFO] [1765465735.062760082] [usb_cam]: Starting 'default_cam' (/dev/video21) at 640x480 via mmap (yuyv) at 30 FPS
- [swscaler @ 0x557ca2b4e0] 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)
- [INFO] [1765465735.140475927] [usb_cam]: Setting 'brightness' to 50
- unknown control 'white_balance_temperature_auto'
- [INFO] [1765465735.169124348] [usb_cam]: Setting 'white_balance_temperature_auto' to 1
- [INFO] [1765465735.169258219] [usb_cam]: Setting 'exposure_auto' to 3
- unknown control 'exposure_auto'
- [INFO] [1765465735.177921539] [usb_cam]: Setting 'focus_auto' to 0
- unknown control 'focus_auto'
- [INFO] [1765465735.232495838] [usb_cam]: Timer triggering every 33 ms
复制代码 视觉识别节点不需要做任何变化;- pi@NanoPC-T6:~/dev_ws$ ros2 run learning_topic topic_webcam_sub
- [INFO] [1765465843.063603694] [topic_webcam_sub]: Receiving video frame
- [INFO] [1765465843.103270173] [topic_webcam_sub]: Receiving video frame
- [INFO] [1765465843.144104574] [topic_webcam_sub]: Receiving video frame
- [INFO] [1765465843.183642515] [topic_webcam_sub]: Receiving video frame
复制代码 3.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 [/image_raw]
- 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 思考题
关于话题通信的原理和实现方法我们就讲到这里,给大家留一个思考题:话题通信的特性是单向传输,适合周期性的数据传递,对于一个复杂的机器人系统来讲,这种特性肯定无法满足所有数据传输的需求,大家是否能够举几个例子,是话题通信无法完成的呢?
参考文章
[1] 古月居ROS2入门教程学习笔记
[2] 神仙级ROS2入门教程(非常详细),从零基础入门到精通,从看这篇开始
[3] ROS各种资源的一个索引网站
[4] Robot Operating System
[5] ROS问答网站
[6] ROS论坛
[7] ROS功能包存储的数据库
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |