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.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文件 - 解析依赖:读取
<depend>、<exec_depend>等标签; - 映射到系统包:将
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 World"
[INFO] [1765463720.818542895] [topic_helloworld_sub]: I heard: "Hello World"
[INFO] [1765463721.318709238] [topic_helloworld_sub]: I heard: "Hello World"
好啦,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各种资源的一个索引网站
[5] ROS问答网站
[6] ROS论坛
[7] ROS功能包存储的数据库

浙公网安备 33010602011771号