- 个人主页:风间琉璃
- 版权: 本文由【风间琉璃】原创、在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官方给出的目标检测网络中一共有4个版本,分别是Yolov5s、Yolov5m、Yolov5l、Yolov5x四个模型。
其网络结构如下:
其基本组成由分为输入端、Backbone、Neck、Prediction四个部分组成。
(1)输入端:Mosaic数据增强、自适应锚框计算
(2)Backbone:Focus结构,CSP结构
(3)Neck:FPN+PAN结构
(4)Prediction:GIOU_Loss
yolov5详情参考:深入浅出Yolo系列之Yolov5核心基础知识完整讲解_江大白*的博客-CSDN博客
C++:mirrors / doleron / yolov5-opencv-cpp-python · GitCode
【模型部署】使用opencv C++ 加速YOLO V5_卖报的大地主的博客-CSDN博客
数据集采用的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;
}
获取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;
}
神经网络的输入图像需要采用称为blob的特定格式。从输入图像或视频流中读取帧后,将通过blobFromImage函数将其转换为神经网络的输入blob。
在此过程中,它使用比例因子1/255将图像像素值缩放到0到1的目标范围。它还将图像的大小调整为给定大小(640,640)而不进行裁剪。以下是使用Netron打开yolov5s.onnx的网络结构。
//定义相关参数值与阈值
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个信息。
通过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也是对博主的鼓舞吖 感谢