OpenCV之YOLOv5目标检测

  • 个人主页:风间琉璃
  • 版权: 本文由【风间琉璃】原创、在CSDN首发、需要转载请联系博主
  • 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)订阅专栏

目录

前言

一、YOLOv5简介

二、预处理

1.获取分类名

2.获取输出层名称

3.图像尺度变换

三、模型加载

四、推理和后处理

五、源码


前言

YOLOv5(You Only Look Once version 5)是计算机视觉领域中一种用于目标检测的深度学习模型,它是YOLO(You Only Look Once)系列的最新版本。YOLOv5的目标是实现高效而准确的实时目标检测,其名称"You Only Look Once"意味着它只需一次前向传播(forward pass)即可检测图像中的所有对象,而不需要采用复杂的多步骤流程。

一、YOLOv5简介

yolov5官方给出的目标检测网络中一共有4个版本,分别是Yolov5s、Yolov5m、Yolov5l、Yolov5x四个模型。

其网络结构如下:

OpenCV之YOLOv5目标检测_第1张图片

其基本组成由分为输入端、Backbone、Neck、Prediction四个部分组成。

(1)输入端Mosaic数据增强、自适应锚框计算
(2)BackboneFocus结构,CSP结构
(3)NeckFPN+PAN结构
(4)PredictionGIOU_Loss 

yolov5详情参考:深入浅出Yolo系列之Yolov5核心基础知识完整讲解_江大白*的博客-CSDN博客

C++:mirrors / doleron / yolov5-opencv-cpp-python · GitCode

【模型部署】使用opencv C++ 加速YOLO V5_卖报的大地主的博客-CSDN博客 

二、预处理

1.获取分类名

数据集采用的coco数据集,需要将coco.names包含训练模型的所有类名称加载到内存中。

string class_path = "F:/data/CQU/VS/yolov5_onnx/coco.names";

//获取分类名
vector getClassNames(string class_path)
{
	ifstream ifs(class_path);
	if (!ifs.is_open())
	{
		printf("could not load class file...\n");
	}
	vector classnames;
	string line;
	while (getline(ifs, line))
	{
		classnames.push_back(line);
	}
	return classnames;
}

2.获取输出层名称

获取yolov5网络模型输出层的名称,为后面的推理做准备。

//获取网络的不相连输出层名称
vector getOutpusNames(const Net& net)
{
	vector outputsname = net.getUnconnectedOutLayersNames(); 
	//for (int i = 0; i < outputsname.size(); i++)
	//{
		//printf("Outputs Name%d:%s", i, outputsname.at(i).c_str());
	//}
	return outputsname;
}

3.图像尺度变换

神经网络的输入图像需要采用称为blob的特定格式。从输入图像或视频流中读取帧后,将通过blobFromImage函数将其转换为神经网络的输入blob。

在此过程中,它使用比例因子1/255将图像像素值缩放到0到1的目标范围。它还将图像的大小调整为给定大小(640,640)而不进行裁剪。以下是使用Netron打开yolov5s.onnx的网络结构。
 

OpenCV之YOLOv5目标检测_第2张图片

//定义相关参数值与阈值
const float INPUT_WIDTH = 640.0;
const float INPUT_HEIGHT = 640.0;


//将输入图像进行预处理
Mat format_yolov5(const Mat& source)
{
	int col = source.cols;
	int row = source.rows;
	int _max = MAX(col, row);

	//以最大的边长重构图像
	Mat result = Mat::zeros(_max, _max,CV_8UC3);
	source.copyTo(result(Rect(0, 0, col, row)));
	return result;
}

//预处理
auto input_image = format_yolov5(image);
Mat blob = blobFromImage(input_image, 1 / 255.0, Size(INPUT_WIDTH, INPUT_HEIGHT), Scalar(), true, false);

三、模型加载

加载网络直接使用readNet,可以根据个人的情况设置是否使用CUDA加速。

//加载网络
Net  loadNet(string model_path, bool is_Cuda)
{
	Net net = readNet(model_path);

	//是否使用cuda
	if (is_Cuda)  //CUDA
	{
		net.setPreferableBackend(DNN_BACKEND_CUDA);
		net.setPreferableTarget(DNN_TARGET_CUDA_FP16);
	}
	else  //cpu
	{
		net.setPreferableBackend(DNN_BACKEND_OPENCV);
		net.setPreferableTarget(DNN_TARGET_CPU);
	}
	return net;
}


//加载网络并使用cuda加速
Net net = loadNet(model, true);

四、推理和后处理

网络模型加载完成后,就可以将图片送入网络进行预测。

//YOLOV5网络的数据预处理以及前向推理(包括NMS处理)
void detect(cv::Mat& image, cv::dnn::Net& net, std::vector& output, const std::vector& className)
{
	//预处理
	auto input_image = format_yolov5(image);
	Mat blob = blobFromImage(input_image, 1 / 255.0, Size(INPUT_WIDTH, INPUT_HEIGHT), Scalar(), true, false);

	//设置输入
	net.setInput(blob);

	//前向计算
	vector outputs;
	vector outputnames = getOutpusNames(net);
	net.forward(outputs, outputnames);

	//计算x_factor和y_factor,用于后面还原bounding box的位置和大小
	float x_factor = input_image.cols / INPUT_WIDTH;
	float y_factor = input_image.rows / INPUT_HEIGHT;

	//yolov5s输出层为一层,通过outputs可获得预测信息
	float* data = (float*)outputs[0].data;

	//yolov5s模型的输出大小为[1,25200.85]
	const int dimensions = 85;
	const int rows = 25200;

	//分类类别索引
	std::vector class_ids;
	//置信度
	std::vector confidences;
	//边框坐标信息
	std::vector boxes;

	for (int i = 0; i < rows; ++i )
	{
		//获取自信度
		float confidence = data[4];
		if (confidence >= CONFIDENCE_THRESHOLD) 
		{
			//获取类别概率
			float* classes_scores = data + 5; 
			
			//将概率构造为Mat
			cv::Mat scores(1, className.size(), CV_32FC1, classes_scores);
			cv::Point class_id;
			double max_class_score;

			//获取最大类别分数以及其对应的索引
			minMaxLoc(scores, 0, &max_class_score, 0, &class_id);

			//通过阈值进行筛选,将符合要求的类别、置信度以及框体进行保存
			if (max_class_score > SCORE_THRESHOLD) 
			{
				confidences.push_back(confidence);
				class_ids.push_back(class_id.x);

				//得到边框左上角(x,y)和w,h
				float x = data[0]; //边框中心坐标
				float y = data[1];
				float w = data[2];
				float h = data[3];
				int left = int((x - 0.5 * w) * x_factor);
				int top = int((y - 0.5 * h) * y_factor);
				int width = int(w * x_factor);
				int height = int(h * y_factor);
				boxes.push_back(cv::Rect(left, top, width, height));
			}

		}
		//一个边界框包含85个值:4个坐标信息、1个置信度信息和80个类别得分信息,在遍历一个边界框后,data指向需要向后移动85个位置
		data += 85;
	}

	std::vector nms_result;
	cv::dnn::NMSBoxes(boxes, confidences, SCORE_THRESHOLD, NMS_THRESHOLD, nms_result);

	//将经过NMS处理后的结果加载到const vector output中
	for (int i = 0; i < nms_result.size(); i++) 
	{
		int idx = nms_result[i];
		Detection result;  
		result.class_id = class_ids[idx];
		result.confidence = confidences[idx];
		result.box = boxes[idx];
		output.push_back(result);
	}
}

网络输出的结果都存在outputs中。outputs的大小为[1,15200,85]。下面是其输出层信息。

第一维是batch size,为1。


第二维为每张输入图片生成的预测框数,即anchors数量 x (S1 x S1 + S2 x S2 + S3 x S3),这里的S1, S2, S3分别为输出层的三个特征图的大小,取值为{80, 40, 20},anchors数量为3,因此总的预测框数为25200;


 第三维为每个预测框的信息,包括4个坐标信息、1个置信度信息和80个类别得分信息,共85个信息

OpenCV之YOLOv5目标检测_第3张图片

OpenCV之YOLOv5目标检测_第4张图片 

通过outputs[0]可以获得该输出层的结果,其中包含了该层所有的预测框的信息,包括预测框的位置、大小、置信度和类别概率。这些信息被保存在一个指向连续内存的地址中,可以通过.data来访问。

使用一个指向float类型的连续内存的指针获取outputs[0].data的数据,即该指针指向的是一个float类型的数组,其中包含了该层所有预测框的位置、大小、置信度和类别概率。

因此,将该指针赋值给float* data后,就可以通过data来访问该数组中的每一个元素。同时,由于该数组是连续内存,可以通过指针的算术运算来访问该数组中的每一个元素,即使用data[i]来访问数组中第i个元素。

data[4]:指针所指向的内存中的第5个float类型的数据,存储的是置信度。当置信度大于一定的阈值,检测有效。

data + 5:从第6个float类型的数据开始的一段连续数据,即80个分类类别的概率。我们需要从该80个类别中找到概率最大的类别以及索引值。

在输出信息中每一行代表一个检测到的边界框, 一个边界框包含85个值:4个坐标信息、1个置信度信息和80个类别得分信息。data所指内存地址包含输出层所有预测框的位置、大小、置信度和类别概率,在yolov5s中共有25200个边界框,即data所指内存地址包含25200*85个值。在遍历一个边界框后,data指向需要向后移动85个位置,即 data +85

最后还需要进行非极大值抑制,在目标检测任务中,一个目标可能会被多个边界框检测到,这些边界框可能会有不同的位置和大小,但表示同一个目标。非极大值抑制(Non-Maximum Suppression,NMS)是一种常用的方法,用于抑制这些重叠的边界框,只保留置信度最高的那个边界框,从而得到最终的目标检测结果。

NMS的原理如下:首先,对所有的边界框按照其置信度进行排序,置信度最高的边界框排在最前面。从置信度最高的边界框开始,依次遍历其余边界框。
   

对于当前遍历到的边界框,如果它与前面已经保留的边界框的重叠程度(通过计算IOU值)大于一定阈值(比如0.5),那么就将其抑制掉,不保留。继续遍历下一个边界框,重复上述过程,直到所有的边界框都被处理完毕。


通过这样的处理,NMS可以抑制掉大量重叠的边界框,只保留最好的那个边界框,从而得到最终的目标检测结果。

然后就是将将检测到的目标边框绘制出来。

//画预测的目标bounding box
void drawPred(vector classesnames, int classId, float conf, Rect box, Mat& frame)
{

	//获取类别名称及其置信度
	string label = format("%.2f", conf);
	if (!classesnames.empty())
	{
		CV_Assert(classId < (int)classesnames.size());
		label = classesnames[classId] + ":" + label;
	}

	定义框体颜色:  box 和 text 的颜色
	Scalar rectColor, textColor;
	// 创建随机数生成器
	random_device rd;
	mt19937 generator(rd());
	// 创建均匀分布对象,范围是1到50
	uniform_int_distribution distribution(1, 80);
	// 生成随机数
	int random_number = distribution(generator);

	//设置颜色
	rectColor = Scalar(random_number * 10 % 256, random_number * 20 % 256, random_number * 30 % 256);
	textColor = Scalar(255 - random_number * 10 % 256, 255 - random_number * 20 % 256, 255 - random_number * 30 % 256);


	//绘制边界框
	rectangle(frame, box, rectColor, 3);
	//绘制用于写类别的边框范围,一般就在边框的上面
	rectangle(frame, Point(box.x, box.y - 20), Point(box.x + box.width, box.y), textColor, FILLED);
	//在上面绘制的框界内写出类别以及概率
	putText(frame, label, Point(box.x, box.y - 5), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 0));
}

运行结果:

YOLOv5:高效实时目标检测的新巅峰

五、源码

资源下载:CSDN

// yolov5_onnx.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
#include 
#include 

#include 
#include 

using namespace std;
using namespace cv;
using namespace cv::dnn;



//定义相关参数值与阈值
const float INPUT_WIDTH = 640.0;
const float INPUT_HEIGHT = 640.0;
const float SCORE_THRESHOLD = 0.4;
const float NMS_THRESHOLD = 0.4;
const float CONFIDENCE_THRESHOLD = 0.4;


//定义输出结果的结构体类
struct Detection
{
	int class_id;
	float confidence;
	cv::Rect box; 
};


//获取分类名
vector getClassNames(string class_path)
{
	ifstream ifs(class_path);
	if (!ifs.is_open())
	{
		printf("could not load class file...\n");
	}
	vector classnames;
	string line;
	while (getline(ifs, line))
	{
		classnames.push_back(line);
	}
	return classnames;
}

//获取网络的不相连输出层名称
vector getOutpusNames(const Net& net)
{
	vector outputsname = net.getUnconnectedOutLayersNames(); 
	//for (int i = 0; i < outputsname.size(); i++)
	//{
		//printf("Outputs Name%d:%s", i, outputsname.at(i).c_str());
	//}
	return outputsname;
}

//加载网络
Net  loadNet(string model_path, bool is_Cuda)
{
	Net net = readNet(model_path);

	//是否使用cuda
	if (is_Cuda)  //CUDA
	{
		net.setPreferableBackend(DNN_BACKEND_CUDA);
		net.setPreferableTarget(DNN_TARGET_CUDA_FP16);
	}
	else  //cpu
	{
		net.setPreferableBackend(DNN_BACKEND_OPENCV);
		net.setPreferableTarget(DNN_TARGET_CPU);
	}
	return net;
}

//将输入图像进行预处理
Mat format_yolov5(const Mat& source)
{
	int col = source.cols;
	int row = source.rows;
	int _max = MAX(col, row);

	//以最大的边长重构图像
	Mat result = Mat::zeros(_max, _max,CV_8UC3);
	source.copyTo(result(Rect(0, 0, col, row)));
	return result;
}



//画预测的目标bounding box
void drawPred(vector classesnames, int classId, float conf, Rect box, Mat& frame)
{

	//获取类别名称及其置信度
	string label = format("%.2f", conf);
	if (!classesnames.empty())
	{
		CV_Assert(classId < (int)classesnames.size());
		label = classesnames[classId] + ":" + label;
	}

	定义框体颜色:  box 和 text 的颜色
	Scalar rectColor, textColor;
	// 创建随机数生成器
	random_device rd;
	mt19937 generator(rd());
	// 创建均匀分布对象,范围是1到50
	uniform_int_distribution distribution(1, 80);
	// 生成随机数
	int random_number = distribution(generator);

	//设置颜色
	rectColor = Scalar(random_number * 10 % 256, random_number * 20 % 256, random_number * 30 % 256);
	textColor = Scalar(255 - random_number * 10 % 256, 255 - random_number * 20 % 256, 255 - random_number * 30 % 256);


	//绘制边界框
	rectangle(frame, box, rectColor, 3);
	//绘制用于写类别的边框范围,一般就在边框的上面
	rectangle(frame, Point(box.x, box.y - 20), Point(box.x + box.width, box.y), textColor, FILLED);
	//在上面绘制的框界内写出类别以及概率
	putText(frame, label, Point(box.x, box.y - 5), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 0));
}

//YOLOV5网络的数据预处理以及前向推理(包括NMS处理)
void detect(cv::Mat& image, cv::dnn::Net& net, std::vector& output, const std::vector& className)
{
	//预处理
	auto input_image = format_yolov5(image);
	Mat blob = blobFromImage(input_image, 1 / 255.0, Size(INPUT_WIDTH, INPUT_HEIGHT), Scalar(), true, false);

	//设置输入
	net.setInput(blob);

	//前向计算
	vector outputs;
	vector outputnames = getOutpusNames(net);
	net.forward(outputs, outputnames);

	//计算x_factor和y_factor,用于后面还原bounding box的位置和大小
	float x_factor = input_image.cols / INPUT_WIDTH;
	float y_factor = input_image.rows / INPUT_HEIGHT;

	//yolov5s输出层为一层,通过outputs可获得预测信息
	float* data = (float*)outputs[0].data;

	//yolov5s模型的输出大小为[1,25200.85]
	const int dimensions = 85;
	const int rows = 25200;

	//分类类别索引
	std::vector class_ids;
	//置信度
	std::vector confidences;
	//边框坐标信息
	std::vector boxes;

	for (int i = 0; i < rows; ++i )
	{
		//获取自信度
		float confidence = data[4];
		if (confidence >= CONFIDENCE_THRESHOLD) 
		{
			//获取类别概率
			float* classes_scores = data + 5; 
			
			//将概率构造为Mat
			cv::Mat scores(1, className.size(), CV_32FC1, classes_scores);
			cv::Point class_id;
			double max_class_score;

			//获取最大类别分数以及其对应的索引
			minMaxLoc(scores, 0, &max_class_score, 0, &class_id);

			//通过阈值进行筛选,将符合要求的类别、置信度以及框体进行保存
			if (max_class_score > SCORE_THRESHOLD) 
			{
				confidences.push_back(confidence);
				class_ids.push_back(class_id.x);

				//得到边框左上角(x,y)和w,h
				float x = data[0]; //边框中心坐标
				float y = data[1];
				float w = data[2];
				float h = data[3];
				int left = int((x - 0.5 * w) * x_factor);
				int top = int((y - 0.5 * h) * y_factor);
				int width = int(w * x_factor);
				int height = int(h * y_factor);
				boxes.push_back(cv::Rect(left, top, width, height));
			}

		}
		//一个边界框包含85个值:4个坐标信息、1个置信度信息和80个类别得分信息,在遍历一个边界框后,data指向需要向后移动85个位置
		data += 85;
	}

	std::vector nms_result;
	cv::dnn::NMSBoxes(boxes, confidences, SCORE_THRESHOLD, NMS_THRESHOLD, nms_result);

	//将经过NMS处理后的结果加载到const vector output中
	for (int i = 0; i < nms_result.size(); i++) 
	{
		int idx = nms_result[i];
		Detection result;  
		result.class_id = class_ids[idx];
		result.confidence = confidences[idx];
		result.box = boxes[idx];
		output.push_back(result);
	}
}

int main()
{
	string model = "F:/data/CQU/VS/yolov5_onnx/yolov5s.onnx";
	string class_path = "F:/data/CQU/VS/yolov5_onnx/coco.names";
	string video_path = "F:/data/CQU/VS/yolov5_onnx/street.mp4";

	//加载网络并使用cuda加速
	Net net = loadNet(model, true);

	//获取标签
	vector classesnames = getClassNames(class_path);


	//获取视频流
	VideoCapture capture;
	capture.open(video_path);
	if (!capture.isOpened())
	{
		printf("could not read video...\n");
	}

	Mat frame;
	while (capture.read(frame))
	{
		vector output;
		//获得当前系统的计时间周期数,求FPS
		double t = (double)getTickCount();

		//前向推理
		detect(frame, net, output, classesnames);

		//检测的边界框总数
		int boxs_num = output.size();
		//对每一个边框进行处理
		for (int i = 0; i < boxs_num; ++i)
		{
			auto detection = output[i];
			auto box = detection.box;
			auto classId = detection.class_id;
			auto confidence = detection.confidence;
			drawPred(classesnames, classId, confidence, box, frame);
		}

		//FPS计算
		t = ((double)getTickCount() - t) / getTickFrequency();//求输入帧后经过的周期数/每秒系统计的周期数=一帧用时多少秒
		double fps = 1.0 / t;//求倒数得到每秒经过多少帧,即帧率
		string text = format("FPS:%.2f", fps);
		cv::putText(frame, text, Point(10, 50), FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 255, 0), 2, 8, 0);

		imshow("yolov5s", frame);
		int c = waitKey(1);
		if (c == 27)
		{
			break;
		}
	}
	capture.release();
	waitKey(0);
	return 0;
}

结束语
感谢你观看我的文章呐~本次航班到这里就结束啦

希望本篇文章有对你带来帮助 ,有学习到一点知识~

躲起来的星星也在努力发光,你也要努力加油(让我们一起努力叭)。

最后,博主要一下你们的三连呀(点赞、评论、收藏),不要钱的还是可以搞一搞的嘛~

不知道评论啥的,即使扣个666也是对博主的鼓舞吖 感谢

你可能感兴趣的:(YOLO,OpenCV,opencv,YOLO,目标检测)