OpenCV
开源计算机视觉库
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
YOLO DNNs

上一教程: OpenCV 与 OpenVINO 的使用
下一教程: 如何在浏览器中运行深度神经网络

原作者Alessandro de Oliveira Faria
扩展者Abduragim Shtanchaev
兼容性OpenCV >= 4.9.0

在 OpenCV 中运行预训练的 YOLO 模型

部署预训练模型是机器学习中的一项常见任务,尤其是在使用不支持 PyTorch 等某些框架的硬件时。本指南全面概述了如何从 PyTorch 导出预训练的 YOLO 系列模型,并使用 OpenCV 的 DNN 框架进行部署。为了演示,我们将重点介绍 YOLOX 模型,但该方法也适用于其他受支持的模型。

注意
目前,OpenCV 支持以下 YOLO 模型:

此支持包括特定于这些模型的预处理和后处理程序。虽然 OpenCV 也支持 Darknet 格式的其他旧版 YOLO 模型,但它们不在本教程的范围之内。

假设我们已成功训练 YOLOX 模型,下一步涉及使用 OpenCV 导出和运行此模型。在继续此过程之前,需要考虑几个关键因素。让我们深入探讨这些方面。

YOLO 的预处理和输出

了解与 YOLO 系列检测器相关的输入和输出的性质至关重要。这些检测器与大多数深度神经网络 (DNN) 类似,其输入大小通常会根据模型的规模而有所不同。

模型规模输入大小
小型模型 1416x416
中型模型 2640x640
大型模型 31280x1280

此表提供了快速参考,用于了解各种 YOLO 模型输入中常用的不同输入维度。这些是标准输入形状。如果训练模型的输入大小与表中提到的大小不同,请确保使用训练模型的输入大小。

此过程中下一个关键要素是了解 YOLO 检测器的图像预处理细节。虽然 YOLO 系列的基本预处理方法保持一致,但仍存在细微但至关重要的差异,必须加以考虑以避免性能下降。其中关键的是应用后调整大小的resize 类型填充值。例如,YOLOX 模型 使用LetterBox调整大小方法和114.0的填充值。务必确保这些参数以及归一化常数与要导出的模型相匹配。

关于模型的输出,它通常采用 [BxNxC+5] 或 [BxNxC+4] 维度的张量形式,其中“B”表示批大小,“N”表示锚点的数量,“C”表示类的数量(例如,如果模型在 COCO 数据集上进行训练,则为 80 个类)。前一个张量结构中的额外 5 对应于目标得分 (obj)、置信度得分 (conf) 和边界框坐标 (cx, cy, w, h)。值得注意的是,YOLOv8 模型的输出形状为 [BxNxC+4],其中没有明确的目标得分,目标得分直接从类得分推断。特别是对于 YOLOX 模型,还需要结合锚点才能将预测重新缩放回图像域。此步骤将集成到 ONNX 图中,我们将在后续部分详细介绍此过程。

PyTorch 模型导出

既然我们已经了解了预处理的参数,我们就可以继续将模型从 Pytorch 导出到 ONNX 图。由于本教程使用 YOLOX 作为示例模型,因此让我们使用它的导出进行演示(除了YOLOv10 模型外,其余 YOLO 检测器的过程相同,稍后将在文章中详细介绍如何导出它)。要导出 YOLOX,我们只需使用 导出脚本。特别是我们需要以下命令:

git clone https://github.com/Megvii-BaseDetection/YOLOX.git
cd YOLOX
wget https://github.com/Megvii-BaseDetection/YOLOX/releases/download/0.1.1rc0/yolox_s.pth # 下载预训练权重
python3 -m tools.export_onnx --output-name yolox_s.onnx -n yolox-s -c yolox_s.pth --decode_in_inference

注意: 这里的--decode_in_inference 用于在 ONNX 图本身中包含锚框创建。它将 此值 设置为True,随后包含锚点生成函数。

下面我们演示了导出脚本的最小版本(可用于除 YOLOX 之外的其他模型),以备需要时使用。但是,通常每个 YOLO 存储库都有预定义的导出脚本。

import onnx
import torch
from onnxsim import simplify
# 加载模型状态字典
ckpt = torch.load(ckpt_file, map_location="cpu")
model.load_state_dict(ckpt)
# 准备虚拟输入
dummy_input = torch.randn(args.batch_size, 3, exp.test_size[0], exp.test_size[1])
# 导出模型
torch.onnx._export(
model,
dummy_input,
"yolox.onnx",
input_names=["input"],
output_names=["output"],
dynamic_axes={"input": {0: 'batch'},
"output": {0: 'batch'}})
# 使用 onnx-simplifier 减少冗余模型。
onnx_model = onnx.load(args.output_name)
model_simp, check = simplify(onnx_model)
assert check, "简化的 ONNX 模型无法验证"
onnx.save(model_simp, args.output_name)

导出 YOLOv10 模型

为了运行 YOLOv10,需要从 torch 中去除动态形状的后处理,然后将其转换为 ONNX。如果您正在寻找如何去除后处理的方法,可以参考 YOLOv10 的这个 分支版本。该分支通过 返回模型在后处理过程之前的输出 来去除后处理。要将 torch 模型转换为 ONNX,请按照以下步骤操作。

git clone [已屏蔽邮箱地址]:Abdurrahheem/yolov10.git
conda create -n yolov10 python=3.9
conda activate yolov10
pip install -r requirements.txt
python export_opencv.py --model=<模型名称> --imgsz=<输入图像大小>

默认情况下,--model="yolov10s"--imgsz=(480,640)。这将生成文件 yolov10s.onnx,该文件可用于 OpenCV 中的推理。

使用 OpenCV 示例运行 Yolo ONNX 检测器

一旦我们有了模型的 ONNX 图,就可以简单地使用 OpenCV 的示例运行它。为此,我们需要确保:

  1. OpenCV 使用 -DBUILD_EXAMLES=ON 标记构建。
  2. 导航到 OpenCV 的 build 目录
  3. 运行以下命令:
./bin/example_dnn_yolo_detector --input=<输入文件路径> \
--classes=<类名文件路径> \
--thr=<置信度阈值> \
--nms=<非最大抑制阈值> \
--mean=<均值归一化值> \
--scale=<缩放因子> \
--yolo=<yolo 模型版本> \
--padvalue=<填充值> \
--paddingmode=<填充模式> \
--backend=<计算后端> \
--target=<目标计算设备> \
--width=<模型输入宽度> \
--height=<模型输入高度> \
  • –input: 输入图像或视频的文件路径。如果省略,则将从摄像头捕获帧。
  • –classes: 包含目标检测类名的文本文件的路径。
  • –thr: 检测的置信度阈值(例如,0.5)。
  • –nms: 非最大抑制阈值(例如,0.4)。
  • –mean: 均值归一化值(例如,0.0 表示无均值归一化)。
  • –scale: 输入归一化的缩放因子(例如,1.0、1/255.0 等)。
  • –yolo: YOLO 模型版本(例如,YOLOv3、YOLOv4 等)。
  • –padvalue: 预处理中使用的填充值(例如,114.0)。
  • –paddingmode: 处理图像调整大小和填充的方法。选项:0(调整大小无需额外处理)、1(调整大小后裁剪)、2(保持纵横比调整大小)。
  • –backend: 计算后端的选项(0 为自动,1 为 Halide,2 为 OpenVINO 等)。
  • –target: 目标计算设备的选项(0 为 CPU,1 为 OpenCL 等)。
  • –device: 摄像头设备编号(0 为默认摄像头)。如果未提供 --input,则默认使用索引为 0 的摄像头。
  • –width: 模型输入宽度。不要与图像宽度混淆。(例如,416、480、640、1280 等)。
  • –height: 模型输入高度。不要与图像高度混淆。(例如,416、480、640、1280 等)。

此处 meanscalepadvaluepaddingmode 应与我们在预处理部分讨论的内容完全匹配,以便模型与 PyTorch 中的结果匹配。

要演示如何在没有您自己的预训练模型的情况下运行 OpenCV YOLO 示例,请按照以下说明操作:

  1. 确保您的平台上已安装 Python。
  2. 确认 OpenCV 使用 -DBUILD_EXAMPLES=ON 标记构建。

运行 YOLOX 检测器(使用默认值)

git clone https://github.com/opencv/opencv_extra.git
cd opencv_extra/testdata/dnn
python download_models.py yolox_s_inf_decoder
cd ..
export OPENCV_TEST_DATA_PATH=$(pwd)
cd <OpenCV 的构建目录>
./bin/example_dnn_yolo_detector

这将使用您的摄像头执行 YOLOX 检测器。对于 YOLOv8(例如),请执行以下额外步骤:

cd opencv_extra/testdata/dnn
python download_models.py yolov8
cd ..
export OPENCV_TEST_DATA_PATH=$(pwd)
cd <OpenCV 的构建目录>
./bin/example_dnn_yolo_detector --model=onnx/models/yolov8n.onnx --yolo=yolov8 --mean=0.0 --scale=0.003921568627 --paddingmode=2 --padvalue=144.0 --thr=0.5 --nms=0.4 --rgb=0

对于 YOLOv10,请按照以下步骤操作:

cd opencv_extra/testdata/dnn
python download_models.py yolov10
cd ..
export OPENCV_TEST_DATA_PATH=$(pwd)
cd <OpenCV 的构建目录>
./bin/example_dnn_yolo_detector --model=onnx/models/yolov10s.onnx --yolo=yolov10 --width=640 --height=480 --scale=0.003921568627 --padvalue=114

这将在您的系统上找到的第一个摄像头运行 YOLOv10 检测器。如果您想在图像/视频文件上运行它,可以使用 --input 选项指定文件的路径。

视频演示

构建自定义管道

有时需要对推理管道进行一些自定义调整。使用 OpenCV DNN 模块,这也很容易实现。下面我们将概述示例实现细节。

  • 导入所需的库
#include <opencv2/dnn.hpp>
#include <fstream>
#include <sstream>
#include "iostream"
#include "common.hpp"
  • 读取 ONNX 图并创建神经网络模型
Net net = readNet(weightPath);
int backend = parser.get<int>("backend");
net.setPreferableBackend(backend);
net.setPreferableTarget(parser.get<int>("target"));
  • 读取图像并进行预处理
float paddingValue = parser.get<float>("padvalue");
bool swapRB = parser.get<bool>("rgb");
int inpWidth = parser.get<int>("width");
int inpHeight = parser.get<int>("height");
Scalar scale = parser.get<Scalar>("scale");
Scalar mean = parser.get<Scalar>("mean");
ImagePaddingMode paddingMode = static_cast<ImagePaddingMode>(parser.get<int>("paddingmode"));
Size size(inpWidth, inpHeight);
Image2BlobParams imgParams(
scale,
size,
mean,
swapRB,
DNN_LAYOUT_NCHW,
paddingMode,
paddingValue);
// 将边界框重新缩放回原始图像
Image2BlobParams paramNet;
paramNet.scalefactor = scale;
paramNet.size = size;
paramNet.mean = mean;
paramNet.swapRB = swapRB;
paramNet.paddingmode = paddingMode;
inp = blobFromImageWithParams(img, imgParams);
  • 推理
std::vector<Mat> outs;
std::vector<int> keep_classIds;
std::vector<float> keep_confidences;
std::vector<Rect2d> keep_boxes;
std::vector<Rect> boxes;
net.setInput(inp);
  • 后处理

所有后处理步骤都在函数yoloPostProcess中实现。请注意,NMS步骤不包含在onnx图中。示例使用OpenCV函数来实现它。

yoloPostProcessing(
outs, keep_classIds, keep_confidences, keep_boxes,
confThreshold, nmsThreshold,
yolo_model,
nc);
  • 绘制预测框
for (auto box : keep_boxes)
{
boxes.push_back(Rect(cvFloor(box.x), cvFloor(box.y), cvFloor(box.width - box.x), cvFloor(box.height - box.y)));
}
paramNet.blobRectsToImageRects(boxes, boxes, img.size());
for (size_t idx = 0; idx < boxes.size(); ++idx)
{
Rect box = boxes[idx];
drawPrediction(keep_classIds[idx], keep_confidences[idx], box.x, box.y,
box.width + box.x, box.height + box.y, img);
}
const std::string kWinName = "Yolo目标检测器";
namedWindow(kWinName, WINDOW_NORMAL);
imshow(kWinName, img);