YOLOv8目标检测——详细记录使用OpenCV的DNN模块进行推理部署C++实现

发布于:2024-12-23 ⋅ 阅读:(16) ⋅ 点赞:(0)

概述

在之前博客中有介绍YOLOv8从环境安装到训练的完整过程,本节主要介绍OpenCV DNN的原理以及使用其进行推理加速,使用C++语言来实现。
https://blog.csdn.net/MariLN/article/details/143924548?spm=1001.2014.3001.5501

1. DNN

OpenCV 是一个用于计算机视觉任务的库,它的 DNN 模块提供了一种使用深度神经网络进行推理的方式。这使得开发者可以轻松地将预训练的深度学习模型集成到自己的计算机视觉应用中,而不需要从头开始构建和训练复杂的神经网络。

  • 支持多种常见的深度学习模型格式,包括 .pb(TensorFlow)、.prototxt + .caffemodel(Caffe)、.onnx(ONNX)等。它不直接支持加载 .pt 格式的 PyTorch 模型。
  • OpenCV 本身具有良好的跨平台性,其 DNN 模块也不例外。无论是在 Windows、Linux 还是 MacOS 等操作系统上,都可以使用 DNN 模块进行深度学习推理。这对于开发在不同环境下运行的计算机视觉应用非常重要,例如开发一个既可以在服务器端(Linux)运行,又可以在客户端(Windows 或 MacOS)运行的图像识别应用。
  • OpenCV DNN 通过优化模型加载和推理过程,能够在 CPU 上实现相对高效的运算。同时,它也支持 GPU 加速(如果系统配置了合适的 GPU 和相关的库)。例如,在目标检测任务中,使用 GPU 加速可以显著提高检测帧率,使得实时目标检测应用成为可能。
  • 适用于在资源充足的桌面端、服务器端上运行计算机视觉任务,尤其是在与 OpenCV 的图像处理功能结合时。

2. 模型转换

.pt 模型是 PyTorch 模型的一种常见存储格式。本质上它是一个二进制文件,它包含了模型的结构定义和参数。模型的结构定义包括网络的层数、每层的类型(如线性层、卷积层、池化层等)、激活函数的类型等信息。

.onnx 模型是一种开放的神经网络交换格式文件。它是为了解决不同深度学习框架之间模型转换和互用的问题。许多深度学习框架(如 PyTorch、TensorFlow 等)都可以将自己的模型转换为 ONNX 格式。主要用于模型的跨框架部署和推理。

将训练好的 YOLOv8 的.pt模型转换为.onnx模型。可以使用ultralytics库来进行转换。

yolo task=detect mode=export model=./runs/detect/train/weights/best.pt format=onnx

from ultralytics import YOLO

# Load a model
model = YOLO('./runs/detect/train/weights/best.pt')  # load a custom trained

# Export the model
success = model.export(format='onnx')

3. 模型推理

3.1 推理流程

(1)读取图像,并可以先将输入图像进行尺寸调整以及添加必要的边框填充等操作,以适配模型输入要求,保证输入图像的尺寸符合模型预期同时尽量减少图像内容的形变等情况。

cv::Mat image = cv::imread("test.jpg");

(2)加载模型,建议在部署时将 PyTorch 和 TensorFlow 等框架的模型转换为 ONNX 格式,以确保兼容性和性能。

model_path = "model.onnx"
cv::dnn::Net net = cv::dnn::readNet(model_path)

(3)设置 GPU 推理(如果需要),OpenCV DNN 支持 CUDA 加速,但前提是需要编译支持 CUDA 的 OpenCV。

net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)

如果是CPU版本的OpenCV。

net.setPreferableBackend(cv::dnn::DNN_BACKEND_DEFAULT);
net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);

(4)将输入图像转换为神经网络接受的输入Blob,主要用于处理深度学习模型输入数据的预处理阶段。创建blob之前可以先将输入图像调整为网络所要求的输入大小。

cv::Mat blob = cv::dnn::blobFromImage(image, 1 / 255.0, cv::Size(640, 640), cv::Scalar(0, 0, 0), true, false);

原函数:

cv::Mat cv::dnn::blobFromImage(
    InputArray image, 
    double scalefactor = 1.0, 
    Size size = Size(), 
    Scalar mean = Scalar(), 
    bool swapRB = false, 
    bool crop = false, 
    int ddepth = CV_32F
);

参数解释:

  • image:输入图像,可以是cv::Mat。
  • scalefactor:缩放系数,图像像素值会乘以该值(例如 1/255.0 用于将 0-255 的像素值归一化到 0-1)。
  • size:目标图像的空间尺寸(宽度 x 高度),通常为网络输入要求的尺寸(例如 224x224、416x416)。
  • mean:均值,用于每个通道进行减去操作(如 Scalar(104, 117, 123) 用于 BGR 格式)。
  • swapRB:布尔值,是否交换 R 和 B 通道。true 表示输入图像的 BGR 通道转换为 RGB。
  • crop:布尔值,是否进行裁剪,true 表示裁剪到目标大小。
  • ddepth:输出 Blob 数据类型,默认 CV_32F(32位浮点)。

返回值:
返回一个 cv::Mat 类型的 Blob,其形状为 (N, C, H, W):
(5)设置模型输入。

net.setInput(blob);

(6)执行前向传播,获取输出。

std::vector<cv::Mat> outputs;
net.forward(outputs, net.getUnconnectedOutLayersNames());

(7)解析输出并在图像上绘制检测结果。

3.2 代码部署

utils.cpp

#pragma once
#include "yolov8_utils.h"

void LetterBox(const cv::Mat& image, cv::Mat& outImage, cv::Vec4d& params, const cv::Size& newShape,
	bool autoShape, bool scaleFill, bool scaleUp, int stride, const cv::Scalar& color)
{
	if (false) {
		int maxLen = MAX(image.rows, image.cols);
		outImage = cv::Mat::zeros(cv::Size(maxLen, maxLen), CV_8UC3);
		image.copyTo(outImage(cv::Rect(0, 0, image.cols, image.rows)));
		params[0] = 1;
		params[1] = 1;
		params[3] = 0;
		params[2] = 0;
	}

	cv::Size shape = image.size();
	float r = std::min((float)newShape.height / (float)shape.height,
		(float)newShape.width / (float)shape.width);
	if (!scaleUp)
		r = std::min(r, 1.0f);

	float ratio[2]{ r, r };
	int new_un_pad[2] = { (int)std::round((float)shape.width * r),(int)std::round((float)shape.height * r) };

	auto dw = (float)(newShape.width - new_un_pad[0]);
	auto dh = (float)(newShape.height - new_un_pad[1]);

	if (autoShape)
	{
		dw = (float)((int)dw % stride);
		dh = (float)((int)dh % stride);
	}
	else if (scaleFill)
	{
		dw = 0.0f;
		dh = 0.0f;
		new_un_pad[0] = newShape.width;
		new_un_pad[1] = newShape.height;
		ratio[0] = (float)newShape.width / (float)shape.width;
		ratio[1] = (float)newShape.height / (float)shape.height;
	}

	dw /= 2.0f;
	dh /= 2.0f;

	if (shape.width != new_un_pad[0] && shape.height != new_un_pad[1])
	{
		cv::resize(image, outImage, cv::Size(new_un_pad[0], new_un_pad[1]));
	}
	else {
		outImage = image.clone();
	}

	int top = int(std::round(dh - 0.1f));
	int bottom = int(std::round(dh + 0.1f));
	int left = int(std::round(dw - 0.1f));
	int right = int(std::round(dw + 0.1f));
	params[0] = ratio[0];
	params[1] = ratio[1];
	params[2] = left;
	params[3] = top;
	cv::copyMakeBorder(outImage, outImage, top, bottom, left, right, cv::BORDER_CONSTANT, color);
}

void DrawPred(cv::Mat& img, std::vector<OutputSeg> result, std::vector<std::string> classNames, std::vector<cv::Scalar> color, bool isVideo) 
{
	cv::Mat mask = img.clone();
	for (int i = 0; i < result.size(); i++) 
	{
		int left, top;
		left = result[i].box.x;
		top = result[i].box.y;
		int color_num = i;
		rectangle(img, result[i].box, color[result[i].id], 1.4, 8);
		if (result[i].boxMask.rows && result[i].boxMask.cols > 0)
			mask(result[i].box).setTo(color[result[i].id], result[i].boxMask);
		std::string label = classNames[result[i].id] + ":" + std::to_string(result[i].confidence);
		int baseLine;
		cv::Size labelSize = cv::getTextSize(label, cv::FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);
		top = std::max(top, labelSize.height);
		//rectangle(frame, Point(left, top - int(1.5 * labelSize.height)), Point(left + int(1.5 * labelSize.width), top + baseLine), Scalar(0, 255, 0), FILLED);
		putText(img, label, cv::Point(left, top), cv::FONT_HERSHEY_SIMPLEX, 0.6, color[result[i].id], 2);
	}
	addWeighted(img, 0.5, mask, 0.5, 0, img); //add mask to src
	/*cv::namedWindow("1", 0);
	imshow("1", img);
	if (!isVideo)
		cv::waitKey();*/
	//destroyAllWindows();

}
detect.cpp
```cpp
#include "YoloV8Detect.h"

YoloV8Detect::YoloV8Detect()
{
}

bool YoloV8Detect::detect(cv::Mat& cv_src,std::vector<OutputSeg>& output)
{
	cv::Mat blob;
	output.clear();
	int col = cv_src.cols;//w
	int row = cv_src.rows;//h
	cv::Mat net_input_img;
	cv::Vec4d params;
	LetterBox(cv_src, net_input_img, params, cv::Size(_net_width, _net_height));//调整图像大小
	cv::dnn::blobFromImage(net_input_img, blob, 1 / 255.0, cv::Size(_net_width, _net_height), cv::Scalar(0, 0, 0), true, false);

	_net.setInput(blob);
	std::vector<cv::Mat> net_output_img;

	_net.forward(net_output_img, _net.getUnconnectedOutLayersNames()); //get outputs

	std::vector<int> class_ids;// res-class_id
	std::vector<float> confidences;// res-conf 
	std::vector<cv::Rect> boxes;// res-box
	cv::Mat output0 = cv::Mat(cv::Size(net_output_img[0].size[2], net_output_img[0].size[1]), CV_32F, (float*)net_output_img[0].data).t();  //[bs,116,8400]=>[bs,8400,116]
	int net_width = output0.cols;
	int rows = output0.rows;
	float* pdata = (float*)output0.data;
	for (int r = 0; r < rows; ++r) {
		cv::Mat scores(1, _class_name.size(), CV_32FC1, pdata + 4);
		cv::Point class_id_point;
		double max_class_socre;
		minMaxLoc(scores, 0, &max_class_socre, 0, &class_id_point);
		max_class_socre = (float)max_class_socre;
		if (max_class_socre >= _class_threshold) {
			//rect [x,y,w,h]
			float x = (pdata[0] - params[2]) / params[0];
			float y = (pdata[1] - params[3]) / params[1];
			float w = pdata[2] / params[0];
			float h = pdata[3] / params[1];
			int left = MAX(int(x - 0.5 * w + 0.5), 0);
			int top = MAX(int(y - 0.5 * h + 0.5), 0);
			class_ids.push_back(class_id_point.x);
			confidences.push_back(max_class_socre);
			boxes.push_back(cv::Rect(left, top, int(w + 0.5), int(h + 0.5)));
		}
		pdata += net_width;//next line
	}
	//NMS
	std::vector<int> nms_result;
	cv::dnn::NMSBoxes(boxes, confidences, _class_threshold, _nms_threshold, nms_result);
	std::vector<std::vector<float>> temp_mask_proposals;
	cv::Rect holeImgRect(0, 0, cv_src.cols, cv_src.rows);
	for (int i = 0; i < nms_result.size(); ++i) {
		int idx = nms_result[i];
		OutputSeg result;
		result.id = class_ids[idx];
		result.confidence = confidences[idx];
		result.box = boxes[idx] & holeImgRect;
		output.push_back(result);
	}
	if (output.size())
		return true;
	else
		return false;
}

void YoloV8Detect::image_detect(cv::Mat &cv_src,std::vector<cv::Scalar> color) {
	if (cv_src.empty())
	{
		std::cout << "read image empty!" << std::endl;
	}
	std::vector<OutputSeg> result;
	detect(cv_src, result);
	DrawPred(cv_src, result, _class_name, color);
	cv::namedWindow("aod", 0);
	cv::imshow("aod", cv_src);
	cv::waitKey();
}

void YoloV8Detect::video_detect(cv::VideoCapture &cap, std::vector<cv::Scalar> color) {
	std::string output = "output.mp4";
	int fps = int(cap.get(5));
	cv::VideoWriter videoWriter;
	while (true) 
	{
		cv::Mat cv_src;
		cap.read(cv_src);
		if (cv_src.empty()) 
		{
			break;
		}
		std::vector<OutputSeg> result;
		detect(cv_src, result);
		DrawPred(cv_src, result, _class_name, color);

		if (!videoWriter.isOpened()) 
		{
			int fourcc=cv::VideoWriter::fourcc('m', 'p', '4', 'v');
			videoWriter.open(output, fourcc, fps, cv::Size(cv_src.cols, cv_src.rows));
		}
		videoWriter.write(cv_src);
		cv::namedWindow("aod", 0);
		cv::imshow("aod", cv_src);
		cv::waitKey(25);
		/*if(cv::getWindowProperty("aod", cv::WND_PROP_AUTOSIZE) < 1){
			break;
		}*/
	}
	cap.release();
	videoWriter.release();
	cv::destroyAllWindows();
}

main.cpp

#include "YoloV8Detect.h"
#include <iostream>
#include <time.h>

int main()
{
	YoloV8Detect yolo_detect;
	yolo_detect.read_model("best.onnx");

	cv::Mat cv_src = cv::imread("4.jpg");
	cv::VideoCapture cap("F20.mp4");

	std::vector<cv::Scalar> color;
	std::srand(time(0));//使用当前时间作为随机数种子
	for (int i = 0; i < 3; i++)
	{
		int b = rand() % 256;
		int g = rand() % 256;
		int r = rand() % 256;
		color.push_back(cv::Scalar(b, g, r));
	}

	yolo_detect.image_detect(cv_src, color);
	//yolo_detect.video_detect(cap, color);
}

4. 推理测试

在这里插入图片描述