freeCodeCamp.org

How to Build a Real-Time Object Detection and Tracking Pipeline with ROS 2 and YOLOv11

8.5内容质量

TL;DR · AI 摘要

本文提供ROS 2与YOLOv11构建实时目标检测流水线的完整工程方案,涵盖多线程推理、ByteTrack跟踪和ONNX导出优化。

核心要点

  • 使用YOLOv11+ByteTrack实现多目标跟踪,推理线程分离确保实时性
  • ONNX导出使模型在边缘设备推理速度提升3倍以上
  • ROS 2节点设计需考虑硬件约束下的QoS策略配置

结构提纲

按章节快速跳转。

  1. 揭示机器人视觉系统开发中模型部署的挑战与解决方案

  2. 定义包含图像采集、检测、跟踪、验证的四层流水线结构

  3. 通过ROS 2节点分离确保YOLOv11推理不阻塞主循环

  4. ONNX格式转换使边缘设备推理延迟降低至83ms

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • ROS 2-YOLOv11流水线
    • 系统架构
      • 图像采集层
      • 检测层
      • 跟踪层
      • 验证层
    • 关键技术
      • 多线程推理
      • ByteTrack跟踪
      • ONNX优化

金句 / Highlights

值得收藏与分享的关键句。

#ROS 2#YOLOv11#机器人#计算机视觉
打开原文

如何使用 ROS 2 和 YOLOv11 构建实时目标检测与跟踪流水线

2026年7月25日

/

#robotics

Iyanuoluwa Enoch Oke

如果你曾经尝试构建一个能够真正“看到”、追踪并响应周围环境的机器人系统,你就会知道困难的部分并不在于训练检测模型。真正的挑战是让这个模型在真实的机器人软件栈中稳定运行,在实时条件下工作,而不会因为硬件限制或时序问题而崩溃。

在本教程中,你将使用 ROS 2 和 YOLOv11 构建一个完整的实时目标检测与跟踪流水线。你将学习如何将模拟器中的摄像头画面发布到 ROS 2,如何在独立线程中运行 YOLO 推理,如何集成 ByteTrack 实现跨帧的多目标跟踪,以及如何将模型导出为 ONNX 格式以在受限硬件上实现更快的推理速度。

到本文结束时,你将不仅了解如何将这些工具组合在一起,还会明白每个架构决策对构建一个用于生产环境(而不仅仅是笔记本电脑)的感知系统的重要性。

以下是本文内容大纲:

目录

  • 先决条件
  • 我们要构建的内容及原因
  • 项目结构
  • 如何设置 ROS 2 工作空间
  • 如何安装依赖项
  • 如何将摄像头画面发布到 ROS 2
  • 如何构建带线程推理的感知节点
  • 如何集成 ByteTrack 实现多目标跟踪
  • 如何添加置信度验证层
  • 如何将模型导出为 ONNX 用于边缘部署
  • 如何测试完整流水线
  • 结论

先决条件

在继续阅读之前,你应该熟悉以下内容:

  • Python 3.10 或更高版本:本教程中的所有代码均使用 Python 编写。
  • 基本的 ROS 2 知识:你应该了解节点、话题、发布者和订阅者的概念。如果你是 ROS 2 新手,官方 ROS 2 文档是一个不错的起点。
  • PyTorch 和目标检测概念的基础知识:你不需要自己训练过 YOLO 模型,但需要理解推理的含义以及边界框检测输出的结构。
  • 在 Ubuntu 22.04 上安装好的 ROS 2 Humble 环境。
  • 建议使用 GPU 实现实时推理,尽管流水线在 CPU 上也能以较低的帧率运行。
  • CARLA 模拟器(可选):摄像头发布部分使用了 CARLA。如果你没有安装 CARLA,可以使用任何与 ROS 2 兼容的摄像头源替代,例如网络摄像头节点或 bag 文件回放。
  • 验证和优化增加了置信度门控层,防止低质量检测结果传递到下游导航逻辑,并将模型导出为ONNX格式以在边缘硬件上实现更快的推理速度。

我们使用CARLA作为模拟器,因为它能提供逼真的传感器数据、可控的环境以及可通过Python访问的相机演员,这使其成为自动驾驶车辆和移动机器人感知研究的天然选择。如果你使用的是其他传感器源,ROS 2架构保持不变,只需更改相机发布节点即可。

项目结构

在编写任何代码之前,先了解完整的项目布局会有所帮助。完成后的开发工作区结构如下:

code
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工作区

创建工作区和包:

code
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运行其节点。

如何安装依赖

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

关于提供者的说明:本教程通过onnxruntime-gpu使用支持GPU的ONNX运行时。如果你在没有CUDA兼容GPU的机器上运行,请将onnxruntime-gpu替换为onnxruntime以进行CPU推理。其余流程保持不变,但预计帧率会降低。

如何将相机帧发布到ROS 2

第一个节点填补了CARLA Python API与ROS 2生态系统之间的空白。CARLA使用事件驱动的回调系统,而ROS 2使用带有类型化消息格式的发布-订阅模型。该节点将CARLA的原始图像格式转换为任何ROS 2节点都可以订阅的sensor_msgs/Image消息。

创建camera_publisher.py:

code
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().get_spawn_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()

code

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')

code

有界队列:maxsize=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('感知节点准备就绪。')

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()

code

## 如何集成ByteTrack实现多目标跟踪

检测仅能告诉你单帧中的内容。跟踪则能告诉你时间维度上的变化:哪辆车是哪辆,它要去哪里,以及它是否是两秒前看到的同一辆车。

ByteTrack通过使用交并比(IoU)将新检测与现有轨迹关联,IoU是预测轨迹位置与新检测之间边界框重叠程度的度量。

它采用两阶段匹配过程,可同时处理高置信度和低置信度检测,在遮挡情况下比简单跟踪器更鲁棒。

最常调整的三个参数是:

- 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]) )

code

# tracked.tracker_id 为每个检测分配一个稳定的整数 ID
        # 只要跟踪持续存在,该 ID 就会在不同帧之间保持一致。
        return tracked

tracker_id 字段使跟踪功能对导航具有实际价值。下游系统现在可以识别对象 ID 7 是一个在过去 12 帧中持续向东北方向移动的行人,而非仅仅接收一个匿名边界框列表。

如何添加置信度验证层

单一的高置信度检测不足以让机器人改变其行为。置信度分数仅衡量模型对其检测结果的确定程度,不涉及检测的时序稳定性、平台本身的稳定性,或周围环境是否使该检测合理。

此验证层需要多个信号达成共识后,才会将检测标记为可操作。创建 validator.py:

code
CONFIDENCE_THRESHOLD = 0.45
MIN_TRACK_AGE = 3        # 跟踪必须存在帧数达到该值后才会被信任
JITTER_THRESHOLD = 2.0   # 平台可接受的最大加速度 (m/s²)

def is_actionable_detection(detection, track_age: int, platform_acceleration: float):
    """
    仅当检测通过以下三项检查时返回 True:
    1. 模型置信度高于阈值(过滤弱检测)
    2. 跟踪存在时间足够长,被判定为稳定
       (过滤仅在单帧或两帧中触发检测的噪声)
    3. 携带传感器的平台没有剧烈振动或加速
       (防止平台运动本身破坏传感器数据)
    """
    if detection.confidence < CONFIDENCE_THRESHOLD:
        return False, "low_confidence"

    if track_age < MIN_TRACK_AGE:
        return False, "track_not_stabilised"

    if platform_acceleration > JITTER_THRESHOLD:
        return False, "platform_unstable"

    return True, "actionable"

这种模式将模型输出与系统级信任分离。模型的职责是生成检测结果,验证器的职责是根据机器人当前运行状态,判断哪些检测结果是安全可操作的。

如何将模型导出为 ONNX 用于边缘部署

ONNX(Open Neural Network Exchange)是一种开放格式,以跨框架和运行时的可移植方式表示机器学习模型。您只需将模型一次性导出为 ONNX 格式,即可通过 ONNX Runtime 或 TensorRT 运行推理,这两者在边缘硬件上的效率显著高于 PyTorch。

TensorRT 是 NVIDIA 的推理优化库。它会将 ONNX 模型专门编译到目标 GPU 上,通过内核融合、层优化以及可选的精度降低到 INT8 或 FP16 来提升性能。结果是在相同硬件上,推理速度可显著快于原始 PyTorch 模型。在 30 FPS 的实时流水线中,这可能决定系统是否可用。

创建 export_onnx.py:

code
from ultralytics import YOLO

def export_perception_model(weights_path: str, output_path: str):
    """
    将 YOLOv11 模型导出为 ONNX 格式用于边缘部署。

    批次维度的 dynamic_axes 设置意味着导出的模型
    可以接受单帧(batch=1)或帧批次,无需重新导出。
    这在使用批处理输入进行基准测试时非常有用。
    """
    model = YOLO(weights_path)
python
# 使用 Ultralytics 内置的 ONNX 导出功能
# opset=17 是推荐版本,可确保与 TensorRT 8.x 及更高版本兼容
model.export(
    format='onnx',
    imgsz=640,
    opset=17,
    dynamic=True,     # 启用动态批量大小
    simplify=True     # 运行 onnx-simplifier 简化计算图
)

print(f"模型已导出至 {output_path}")

if __name__ == '__main__': export_perception_model( weights_path='models/yolov11n.pt', output_path='models/perception.onnx' )

code

要使用导出的 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

code

providers 列表指示 ONNX Runtime 优先使用 CUDA 进行 GPU 加速,若 CUDA 不可用则回退至 CPU。这使得相同推理代码可在开发机和边缘设备之间无需代码修改即可跨平台运行。

## 如何测试完整流程

所有节点编写完成后,通过两个终端启动管道。

在第一个终端启动摄像头发布者:

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

code

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

source install/setup.bash ros2 run perception_stack perception_node

code

为验证帧在节点间是否正常传输,可在第三个终端检查话题:

ros2 topic hz /carla/camera/rgb

code

该命令会打印摄像头话题的消息速率。在 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)

code

然后在 rqt_image_view 中打开注释流:

ros2 run rqt_image_view rqt_image_view /perception/annotated

code

## 结论

至此,您已从传感器构建了一个实时机器人感知管道,直至经过验证且可追踪的检测。所构建的系统可处理摄像头数据摄入、多线程推理、多目标跟踪、基于置信度的验证以及边缘优化模型导出。

Hi, I'm Iyanuoluwa Oke, a Machine Learning and Robotics Engineer passionate about building intelligent systems that solve real-world problems. I hold an MSc in Robotics with a focus on deep learning, computer vision, and autonomous systems. My work spans AI infrastructure, robotics, computer vision, distributed systems, and developer tools. I enjoy turning complex engineering challenges into practical, production-ready solutions. Some of the projects I've worked on include:  
- AI-powered robotic perception and object tracking  
- Autonomous vehicle perception and control simulations  
- Fault-tolerant orchestration systems for AI coding agents  
- Open-source tools that improve developer productivity and AI workflows  

On this blog, I write about:  
- Artificial Intelligence & Machine Learning  
- Robotics & Computer Vision  
- AI Agents and LLM Engineering  
- Distributed Systems & Backend Engineering  
- Python, ROS, and Software Architecture  
- Building and shipping open-source projects  

I believe the best way to learn is by building in public. Here you'll find engineering deep dives, architecture decisions, lessons learned, and practical tutorials from projects I'm actively developing. Thanks for stopping by—I'm always happy to connect with fellow engineers, researchers, and builders.

If this article was helpful, share it.

Learn to code for free. freeCodeCamp's open source curriculum has helped more than 40,000 people get jobs as developers. Get started

ADVERTISEMENT