← 返回蜂巢洞察

如何使用ROS 2和YOLOv11构建实时物体检测与跟踪系统

如果你曾经尝试过构建一个能够真正感知周围环境、对其进行追踪并作出相应反应的机器人系统,你就会知道:真正的难点并不在于训练检测模型,而在于如何让这个模型在真实的机器人软件系统中实时稳定地运行——尤其是在遇到硬件限制或时间调度问题时,也能保证系统的正常运作。 在这个教程中,你将使用ROS 2和YOLOv11构建一个完整的实时物体检测与追踪系统。你会学习如何将模拟器中的摄像头数据发送到ROS 2系统中,在单独的线程中运行YOLO推理算法,利用ByteTrack技术实现跨帧的多物体追踪功能,以及如何将训练好的模型导出为ONNX格式,以便在性能有限的硬件上更快地执行推理任务。 通过学习这个教程,你不仅会

如果你曾经尝试过构建一个能够真正感知周围环境、对其进行追踪并作出相应反应的机器人系统,你就会知道:真正的难点并不在于训练检测模型,而在于如何让这个模型在真实的机器人软件系统中实时稳定地运行——尤其是在遇到硬件限制或时间调度问题时,也能保证系统的正常运作。

在这个教程中,你将使用ROS 2和YOLOv11构建一个完整的实时物体检测与追踪系统。你会学习如何将模拟器中的摄像头数据发送到ROS 2系统中,在单独的线程中运行YOLO推理算法,利用ByteTrack技术实现跨帧的多物体追踪功能,以及如何将训练好的模型导出为ONNX格式,以便在性能有限的硬件上更快地执行推理任务。

通过学习这个教程,你不仅会了解如何将这些工具组合起来使用,还会明白:对于一个真正需要在实际环境中运行的感知系统而言,每一个架构设计决策都至关重要——而不仅仅是在笔记本上测试时才重要。

接下来我们将介绍以下内容:

目录

先决条件

在开始学习之前,你需要确保自己已经掌握了以下内容:

  • Python 3.10或更高版本:本教程中的所有代码都是用Python编写的。

  • 基本的ROS 2知识:你应该了解节点、主题、发布者以及订阅者的概念。如果你是ROS 2的新手,官方的ROS 2文档是一个很好的学习起点。

  • 对PyTorch及物体检测技术的了解:你不需要自己训练YOLO模型,但需要理解推理的含义以及边界框检测结果的格式。

  • 在Ubuntu 22.04系统上安装了ROS 2 Humble版本

  • 建议使用GPU进行实时推理,不过在没有GPU的情况下,该系统也可以在CPU上运行,只不过帧率会降低。

  • CARLA模拟器(可选):

    摄像头数据发布功能的实现依赖于CARLA模拟器。如果你没有安装CARLA,也可以使用其他与ROS 2兼容的摄像头源,比如网络摄像头节点或bag文件播放功能。

我们正在构建什么以及为何要构建这些内容

感知系统是机器人系统中负责理解机器人周围环境信息的部分。它接收原始的传感器数据(通常是摄像头采集的图像帧),并将其转化为结构化信息:确定物体的位置、种类及其运动方式。

本教程构建的感知系统包含四个层次:

  1. 摄像头数据采集:从模拟器中获取原始图像帧,并以ROS 2消息的形式发布出来,以便其他机器人组件能够使用这些数据。

  2. 物体检测:对每一帧图像应用YOLOv11算法,识别出物体及其位置,并附带置信度评分。

  3. 多物体跟踪:利用ByteTrack技术将不同帧中的物体检测结果关联起来,使每个物体在时间序列中保持稳定的身份标识,而不会将每一帧都视为全新的场景。

  4. 验证与优化:添加置信度筛选机制,防止质量较低的检测结果影响后续的导航逻辑;同时将模型导出为ONNX格式,以便在边缘设备上快速进行推理计算。

我们选择CARLA作为模拟器,是因为它能够提供真实的传感器数据、可控制的环境环境,而且其摄像头组件支持Python编程语言,因此非常适合用于自动驾驶车辆和移动机器人的感知系统研究。如果你使用的是其他传感器源,ROS 2的架构是相同的,只需要修改负责发布摄像头数据的节点即可。

项目结构

在编写任何代码之前,了解整个项目的整体结构是非常有帮助的。完成后的工作空间布局如下:

ros2_perception_ws/
├── src/
│   └── perception_stack/
│       ├── perception_stack/
│       │   ├── __init__.py
│       │   ├── camera_publisher.py      # 将CARLA图像帧发布到ROS 2系统中
│       │   ├── perception_node.py       # 多线程YOLO检测节点
│       │   ├── tracker.py               # ByteTrack跟踪模块
│       │   └── validator.py             | 置信度筛选层
│       │   └── export_onnx.py           | ONNX模型导出脚本
│       ├── models/
│       │   └── yolov11n.pt              | 下载的YOLO模型权重文件
│       ├── package.xml
│       ├── setup.py
│       └── setup.cfg
├── requirements.txt
└── README.md

每个文件都承担着特定的功能。对于机器人软件来说,这种模块化的设计尤为重要,因为感知、跟踪和验证等功能的发展速度往往不同,因此需要分别进行测试。

如何设置你的ROS 2工作空间

首先创建工作空间和相应的软件包:

mkdir -p ~/ros2_perception_ws/src
cd ~/ros2_perception_ws/src
ros2 pkg create --build-type ament_python perception_stack
cd ~/ros2_perception_ws
colcon build
source install/setup.bash

colcon build 命令用于编译工作区代码。通过执行 install/setup.bash,ROS 2系统会识别到你新创建的包,这样你就可以使用 ros2 run 命令来运行该包中的节点了。

如何安装依赖项

pip install ultralytics opencv-python-headless cv_bridge \
            torch torchvision onnx onnxruntime-gpu \
            numpy supervision

关于提供者程序的说明:本教程使用了支持GPU功能的ONNX Runtime,具体通过 onnxruntime-gpu 实现。如果你使用的机器没有兼容CUDA的GPU,那么可以将该依赖项替换为 onnxruntime,以便仅使用CPU进行推理。其余配置保持不变,但需要注意的是,使用CPU进行推理时,帧率可能会降低。

如何将相机采集的图像数据发布到ROS 2系统中

第一个节点的作用是打通CARLA的Python API与ROS 2生态系统之间的连接。CARLA采用事件驱动的回调机制,而ROS 2则使用基于类型化消息格式的发布者-订阅者模型。这个节点会将CARLA生成的原始图像数据转换为sensor_msgs/Image类型的消息,任何ROS 2节点都可以订阅这类消息。

创建文件 camera_publisher.py,并编写如下代码:

import carla
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import numpy as np

class CARLACameraNode(Node):
    def __init__(self):
        super().__init '__carla_camera_node')
        self.publisher = self.create_publisher(Image, '/carla/camera/rgb', 10)
        self.bridge = CvBridge()
        self.get_logger().info('CARLA camera node started.')

    def camera_callback(self, image):
        # CARLA生成的原始图像数据为BGRA格式,我们需要将其转换为BGR格式才能被OpenCV处理
        array = np.frombuffer(image.raw_data, dtype=np.uint8)
        array = array.reshape((image.height, image.width, 4))
        bgr = array[:, :, :3]

        msg = self.bridge.cv2_to_imgmsg(bgr, encoding='bgr8')

        # 为消息添加当前ROS 2系统的时钟时间戳
        # 这一点非常重要。因为下游节点(如跟踪系统或SLAM算法)会通过消息之间的时间差来计算物体的运动速度和位移,如果没有准确的时间戳,这些计算就会出错,从而导致系统无法正常工作。
        msg.header.stamp = self.get_clock().now().to_msg()

        self.publisher.publish(msg)

def main():
    rclpy.init()
    node = CARLACameraNode()

    client = carla.Client('localhost', 2000)
    world = client.get_world()
    blueprint_library = world.get_blueprint_library()

    camera_bp = blueprint_library.find('sensor.camera.rgb')
    camera.bp.set_attribute('image_size_x', '1280')
    camera(bp.set_attribute('image_size_y', '720')
    camera bp.set_attribute('fov', '90')

    spawn_point = world.get_map().getspawn_points()[0]
    camera = world.spawn_actor(camera_bp, spawn_point)
    camera.listen(node.camera_callback)

    rclpy.spin(node)
    camera.destroy()
    node.destroy_node()
    rclpy.shutdown()

cv_bridge库负责处理OpenCV数组与ROS 2图像消息之间的转换。bgr8编码方式用于告知后续订阅者应期待何种颜色格式。如果没有这种编码,颜色通道可能会被随意交换,从而导致检测器在接收到完全有效的输入时仍产生错误的结果。

如何构建具有线程推理功能的感知节点

这是整个处理流程中架构上最为重要的节点,在查看代码之前,有必要先了解其设计原理。

默认情况下,ROS 2进程会通过单个执行线程来处理订阅者的回调函数。如果你的YOLO推理操作发生在回调函数内部,那么在整个推理过程中,该线程都会被阻塞,从而导致订阅者无法接收新的消息。

根据队列的大小和帧率的不同,这意味着在推理完成时,你可能已经在处理一些已经过时的帧了。这样一来,跟踪系统接收到的数据流就会变得不连续、时间不一致,而无法保持流畅性。

为了解决这个问题,需要将数据的接收与处理过程分离开来。回调函数的作用仅仅是将新收到的帧放入一个有容量限制的队列中,然后立即返回;另一个独立的线程则会从该队列中取出帧并执行推理操作。

这个队列是有最大容量的。当推理速度跟不上数据生成的速度且队列已满时,新的帧会被直接丢弃,而不会无限期地堆积在队列中。这种设计是经过深思熟虑的:在实时系统中,延迟处理的过时帧往往比被直接丢弃的帧更糟糕,因为它们会使得跟踪系统看到的是过去的状态,而不是当前的实际情况。

创建perception_node.py文件:

import threading
import queue

import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
from ultralytics import YOLO
import cv2

CONFIDENCE_THRESHOLD = 0.45

class PerceptionNode(Node):
    def __init__(self):
        super().__init '__perception_node')

        self.bridge = CvBridge()
        self.model = YOLO('models/yolov11n.pt')

        # 有容量限制的队列:最大容量为5,可以防止过时帧的堆积。
        # 当队列已满时,image_callback函数会直接丢弃新收到的帧,而不会等待,从而保证数据流的连续性。
        self.frame_queue = queue.Queue(maxsize=5)

        self.subscription = self.create_subscription(
            Image,
            '/carla/camera/rgb',
            self.image_callback,
            10
        )

        self.inference_thread = threading.Thread(
            target=self.run_inference,
            daemon=True
        )
        self.inference_thread.start()
        self.get_logger().info('Perception node ready.')

    def image_callback(self, msg):
        # 如果推理操作跟不上数据生成的速度,就直接丢弃该帧。
        # 我们绝对不能让回调线程被阻塞。
        if not self.frame_queue.full():
            self.frame_queue.put(msg)

    def run_inference(self):
        while rclpy.ok():
            msg = self.frame_queue.get()
            frame = self.bridge.imgmsg_to_cv2(msg, desired_encoding='bgr8')

            results = self.model(frame, conf=CONFIDENCE_THRESHOLD, verbose=False)

            detections = results[0].boxes
            self.get_logger().info(
                f'检测到{len(detections)}个对象,置信度均高于{CONFIDENCE_threshold}'
            )

def main():
    rclpy.init()
    node = PerceptionNode()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

如何将ByteTrack集成到多目标跟踪系统中

仅通过检测,我们只能知道单帧图像中有哪些物体;而跟踪功能则能帮助我们了解这些物体在时间推移过程中的变化情况:比如哪辆车是哪辆、它正在朝哪个方向移动,以及它是否就是两秒钟前我们看到的那辆车。

ByteTrack通过使用“交并比”(IoU)这一指标来将新的检测结果与现有的跟踪对象关联起来。IoU用于衡量预测的物体位置与新检测到的物体位置之间的边界框重叠程度。

该系统采用了两阶段匹配流程,既能处理高置信度的检测结果,也能处理低置信度的检测结果,因此相比简单的跟踪算法,在物体被其他物体遮挡的情况下,ByteTrack的表现更为稳定。

您最常需要调整的三个参数分别是:

  • track_thresh:启动或确认某个跟踪对象所需的最小检测置信度
  • match_thresh:使某个检测结果能与现有跟踪对象匹配所需的最小IoU值
  • track_buffer:一个跟踪对象在连续多少帧内没有新的检测结果与之关联时,才会被系统删除

创建文件tracker.py:

from supervision import ByteTracker, Detections
import numpy as np

class RoboticsTracker:
    def __init__(self):
        # track_buffer决定了一个跟踪对象在连续多少帧内没有新的检测结果时才会被删除
        # 较高的值可以在物体短暂被遮挡的情况下帮助系统继续进行跟踪,但也会导致一些已经离开场景的物体仍然被保留下来。
        self.tracker = ByteTracker(
            track_thresh=0.45,
            match_thresh=0.8,
            track_buffer=30,
            frame_rate=30
        )

    def update(self, yolo_results, frame_shape):
        boxes = yolo_results[0].boxes

        if len(boxes) == 0:
            return []

        xyxy = boxes.xyxy.cpu().numpy()
        confidence = boxes.conf(cpu().numpy()
        class_ids = boxes.cls.cpu().numpy().astype(int)

        detections = Detections(
            xyxy=xyxy,
            confidence=confidence,
            class_id=class_ids
        )

        tracked = self.tracker.update(
            detections=detections,
            frame_resolution_wh=(frame_shape[1], frame_shape[0])
        )

        # tracked.tracker_id为每个检测结果分配了一个稳定的整数ID,只要这个跟踪对象仍然存在,这个ID就会在不同的帧中保持不变。
        return tracked

tracker_id这一字段使得跟踪功能在导航应用中变得非常有用。下游系统现在能够根据这个ID识别出某个物体:例如,ID为7的物体在过去的12帧中一直朝东北方向移动。

如何添加置信度验证层

仅仅依靠一次高置信度的检测结果,机器人是无法改变自身行为的。置信度分数只是用来衡量模型对所检测到的物体的判断准确程度,并不能说明该检测结果在时间上是稳定的,也不能证明平台本身是否稳定,更无法判断周围环境是否使这个检测结果具有合理性。

这一验证层需要在多个信号都达成一致意见后,才会将某个检测结果认定为可采取行动的。请创建文件validator.py

CONFIDENCE_THRESHOLD = 0.45
MIN TRACK_AGE = 3        # 一个轨迹必须存在至少3帧才能被认为稳定
JITTER_threshold = 2.0   # 平台允许的最大加速度(米/秒²)

def is_actionable_detection(detection, track_age: int, platform_acceleration: float):
    """
    只有当检测结果通过以下三项检查时,才会返回True:
    1. 模型的置信度高于阈值(从而过滤掉弱检测结果)
    2. 该轨迹已经存在足够长的时间,因此可以被认为是稳定的
       (这样就能排除那些仅在一两帧内被检测到的噪声干扰)
    3. 承载传感器的平台没有发生剧烈振动或加速,以免影响传感器数据的准确性)
    """
    if detection.confidence < CONFIDENCE_THRESHOLD:
        return False, "置信度过低"

    if track_age < MIN TRACK_AGE:
        return False, "轨迹不稳定"

    if platform_acceleration > JITTER_threshold:
        return False, "平台状态不稳定"

    return True, "可采取行动"

这种机制将模型的输出结果与系统层面的信任判断区分开来。模型的职责是生成检测结果,而验证器的职责则是根据机器人当前的运行状态,来判断这些检测结果中哪些是可以被实际采用的。

如何将模型导出为ONNX格式以便在边缘设备上使用

ONNX是一种开放式的机器学习模型格式,它能够使模型在不同框架和运行环境中都能被顺利使用。你不需要通过PyTorch来进行推理计算,只需将模型导出为ONNX格式,然后通过ONNX Runtime或TensorRT来运行它——这两种工具在边缘设备上的运行效率要高得多。

TensorRT是NVIDIA开发的推理优化工具。它能够接收ONNX格式的模型,并针对目标GPU对其进行优化处理,包括内核融合、层级优化以及精度调整(可将精度设置为INT8或FP16)。经过优化后的模型,在相同的硬件上运行速度会比原始的PyTorch模型快很多;在实时应用场景中,这种速度差异可能会决定系统是否能够正常使用。

请创建文件export_onnx.py

from ultralytics import YOLO

def export_perception_model(weights_path: str, output_path: str):
    """
    将YOLOv11模型导出为ONNX格式,以便在边缘设备上使用。

    参数`dynamic Axes`设置为`True`,意味着导出的模型既可以处理单帧数据,也可以处理多帧数据,而无需重新进行导出操作。这对于在基准测试中使用批量输入数据进行测试非常有用。
    """
    model = YOLO(weights_path)

    # 使用Ultralytics内置的ONNX导出功能进行导出
    # 参数`opset=17`是推荐版本,能够保证与TensorRT 8.x及更高版本兼容
    model.export(
        format='onnx',
        imgsz=640,
        opset=17,
        dynamic=True,     # 启用动态批量大小处理
        simplify=True     # 运行ONNX简化工具来优化模型结构
    )

    print(f"模型已成功导出到{output_path}路径")
if __name__ == '__main__':
    export_perception_model(
        weights_path='models/yolov11n.pt',
        output_path='models/perception.onnx'
    )

如果想要使用导出的ONNX模型而不是PyTorch来进行推理,只需将perception_node.py中的YOLO推理代码替换为ONNX Runtime会话即可:

import onnxruntime as ort
import numpy as np
import cv2

session = ort.InferenceSession(
    'models/perception.onnx',
    providers=['CUDAExecutionProvider', 'CPUExecutionProvider']
)

def run_onnx_inference(frame: np.ndarray):
    # 预处理:调整图像尺寸、进行归一化处理、添加批量维度,并将数据类型转换为float32
    img = cv2.resize.frame, (640, 640))
    img = img.astype(np.float32) / 255.0
    img = img.transpose(2, 0, 1)          # 将数据格式从HWC转换为CHW
    img = np.expand_dims(img, axis=0)     # 添加批量维度

    outputs = session.run(None, {'images': img})
    return outputs

providers列表告诉ONNX Runtime在支持GPU加速的情况下优先使用CUDA,如果CUDA不可用,则转而使用CPU。这样一来,相同的推理代码就可以在开发机器和边缘设备上直接使用,而无需进行任何修改。

如何测试整个流程

当所有节点的代码都编写完成后,在两个终端中分别启动这个流程。

在第一个终端中,启动相机发布器:

cd ~/ros2_perception_ws
source install/setup.bash
ros2 run perception_stack camera_publisher

在第二个终端中,启动感知节点:

source install/setup.bash
ros2 run perception_stack perception_node

为了验证数据帧是否能够在各个节点之间正确传输,你可以在第三个终端中查看相关主题信息:

ros2 topic hz /carla/camera/rgb

这条命令会显示相机主题上的消息发送频率。如果帧率为30 FPS,那么你应该能看到每秒钟大约有30条消息被发送;如果这个数字明显偏低,那就说明CARLA回调功能出现了问题,或者节点之间的网络传输已经达到饱和状态。

如果你想直观地查看感知节点检测到了什么内容,可以在run_inference函数中添加代码来发布带有标注的图像信息:

annotated = results[0].plot()  # 在图像上绘制框和标签
annotated_msg = self.bridge.cv2_to_imgmsg(annotated, encoding='bgr8')
annotated_msg.header.stamp = self.get_clock().now().to_msg()
self.annotated_publisher.publish(annotated_msg)

然后,在rqt_image_view中打开这些标注后的图像:

ros2 run rqt_image_view rqt_image_view /perception/annotated

结论

你现在已经构建了一个从传感器开始,一直到生成经过验证且可追踪的检测结果为止的实时机器人感知系统。你所开发的这个系统能够处理图像采集、并行推理、多目标跟踪、基于置信度的检测结果验证,以及针对边缘设备进行优化的模型导出功能。

所有这些内容所传达的核心道理都是一样的:机器人的感知能力其实是一个系统层面的问题,而不是某个具体模型所能解决的问题。一个训练有素的模型固然必不可少,但仅仅拥有这样的模型还不够。在实际应用中,真正重要的是该系统能否正确处理各种时间安排问题、在负载增加时仍能保持稳定的运行状态、是否定义了各种故障情况,以及其架构是否能够清晰地划分各个功能模块,从而便于在现场进行调试和优化。

<接下来的步骤是:用合适的 ROS 2 消息发布器替换 `perception_node.py` 中用于检测结果的日志记录功能,将检测结果集成到 Nav2 导航系统中,并在目标硬件上运行整个流程,以验证 ONNX 优化技术在实际应用环境中的有效性。

<如果你正在开发类似的项目,或者对其中的任何环节有疑问,请随时与我们联系。对于那些真正需要投入实际使用的机器人系统来说,总有许多值得探讨的话题。

相关文章

技术实践

如何使用Node.js和Google Gemini通过函数调用来构建一个人工智能代理

github.com/ziaongit/nodejs-gemini-agent 。 目录 功能调用机制的工作原理 我们正在构建什么 先决条件 项目设置 工具的定义 工具功能的实现 构建智能代理的循环机制 命令行入口点的设置 添加Express HTTP服务器 智能代理的测试 故障排除 接下来要构建什么 功能调用机制的工作原理 这里有一个让人感到惊讶的地方:Gemini并不会直接运行你的代码。它只会返回一个结构化对象,其中包含诸如“调用 get_weather 函数、将 city 设置为柏林”这样的指令。你的代码会接收到这些指令,然后执行相应的功能并将结果反馈回去。Gemini会检查这些结果是否

阅读全文
技术实践

如何利用提示工程与上下文工程来开发人工智能代理

在这个教程中,我将向您展示提示工程和上下文工程如何提升人工智能模型的性能。 我们将构建一个简单的本地模型,从基础输入开始,然后通过使用更合适的提示语和更丰富的上下文信息来改进它,这样您就能看到每一项改变对最终输出结果的影响。 我们将会使用LangChain v1、Ollama、Qwen以及Python。所有操作都在您的个人电脑上完成,因此您无需支付任何API费用。 目录 背景知识 什么是提示工程? 什么是上下文工程? 为什么提示工程和上下文工程对人工智能模型如此重要 动机与架构 步骤1:安装Ollama并下载模型 步骤2:安装Python相关依赖库 步骤3:编写代理代码 示例输出结果 提示语优

阅读全文
技术实践

如何利用Gemini构建人工智能功能:面向开发者的提示工程实用指南

大多数关于提示工程的教学教程都遵循相同的流程:安装SDK,输入API密钥,调用 generateContent 函数,然后打印输出结果。模型会生成一些看似合理的内容,之后教学教程也就结束了。 但当你真正尝试将这个系统投入实际使用时,才会发现其实真正的准备工作根本还没有开始。 “API返回的文本”与“让用户感到可信的实际功能”之间的差距,正是需要耗费大量精力去解决的地方。 这个差距中充满了各种棘手的问题:模型生成的内容听起来和其他聊天机器人没什么两样;它会编造用户从未说过的话;它返回的数据会被用Markdown格式包裹起来;系统会在凌晨2点出现故障;而对于那些只是想得到答案的用户来说,系统展示的

阅读全文
技术实践

如何使用LangSmith来追踪和监控人工智能代理的行为

在本教程中,我将向您展示如何使用LangSmith来追踪和监控本地的AI代理。我们会构建一个简单的本地AI代理,然后为其启用LangSmith追踪功能,这样我们就能通过Web界面查看模型调用情况、工具使用情况以及请求处理延迟等信息。 我们将使用LangChain v1、Ollama、Qwen以及Python这些工具。除了用于实现观测功能的组件外,所有操作都在您的本地机器上完成,因此代理本身不会产生任何与模型API相关的费用。 目录 背景知识 什么是可观测性与监控? 什么是LangSmith? 开发动机与架构设计 步骤1:安装Ollama并下载模型 步骤2:安装Python相关依赖库 步骤3:启

阅读全文