文章目录
- 前言
- 一、导出yolov8模型为onnx文件
- 二、VS2019配置及opencv环境配置
- 三、opencv部署
- 总结
前言
本文主要研究场景为工业场景下,在工控机与工业相机环境中运行的视觉缺陷检测系统,因此本文主要目的为实现c++环境下,将yolov8已训练好的检测模型部署在opencv中通过cpu进行检测运算
一、导出yolov8模型为onnx文件
Yolo官方支持onnx的使用,直接使用yolo的官方库即可
配置好训练权重文件路径,输出参数按自己需求查手册配置
from ultralytics import YOLO
# Load a model
#model = YOLO("yolov8n.pt") # Load an official model
model = YOLO(r"D:\deep_learning\YOLOv8.2\runs\train\exp9\weights\best.pt") # Load a custom trained model
# Export the model
success = model.export(format="onnx", opset=11, simplify=True)
输出成功的文件会在权重文件同目录下
这里生成的文件为11.6MB,过小的onnx文件可能是生成失败产生的
获取了onnx文件后接下来配置调用onnx文件的部分
二、VS2019配置及opencv环境配置
VS2019在微软官方网站上下载即可
opencv的版本有很多,但经过多次测试发现在cpu场景下要成功调用dnn模块至少需要4.8.0版本
opencv 4.5.4版本
opencv 4.5.5版本
opencv 4.7.0版本
经测试都会在读取onnx文件时发生致命错误
cv::dnn::readNetFromONNX(modelPath);
该条函数在调用时会直接抛出异常终止程序
0x00007FFC088CB699 处(位于 OpencvYoloYest.exe 中)有未经处理的异常:
Microsoft C++ 异常: cv::Exception,位于内存位置 0x0000002AF32FB5B0 处。
opencv需要在官网下载,官网提供X64的release包可以直接下载提取使用
也可通过cmake编译cuda版opencv或是x86的动态库来使用
opencv官方releases下载地址
选取Windows版下载即可,或下载Sources包使用cmake手动编译
本文仅记录采用opencv 4.8.0版本
opencv的环境配置主要分为两部分
系统变量设置:
VS2019动态库配置:
注意C++标准需要选择为ISO C++17标准
包含目录与库目录配置
在链接器的输入附加依赖项中添加opencv动态库
注意Debug版本需要将该opencv_world480.lib改为opencv_world480d.lib
跨平台或老旧工控机需要x86版本则需通过cmake手动编译win32版本
至此环境配置就完成了,接下来是库的调用,即在opencv中的部署
三、opencv部署
本文提供代码,同时也可通过yolo官方处下载代码
链接与代码如下:
YOLOv8-CPP-Inference
inference.cpp
#include "inference.h"
Inference::Inference(const std::string onnxModelPath, const cv::Size modelInputShape= cv::Size(640, 640), const std::string classesTxtFile= "classes.txt", const bool runWithCuda=false)
{
modelPath = onnxModelPath;
modelShape = modelInputShape;
classesPath = classesTxtFile;
cudaEnabled = runWithCuda;
loadOnnxNetwork();
//loadClassesFromFile(); The classes are hard-coded for this example
}
std::vector<Detection> Inference::runInference(const cv::Mat& input)
{
cv::Mat modelInput = input;
if (letterBoxForSquare && modelShape.width == modelShape.height)
modelInput = formatToSquare(modelInput);
cv::Mat blob;
cv::dnn::blobFromImage(modelInput, blob, 1.0 / 255.0, modelShape, cv::Scalar(), true, false);
net.setInput(blob);
std::vector<cv::Mat> outputs;
net.forward(outputs, net.getUnconnectedOutLayersNames());
//cv::Mat cpuOutput;
//outputs[0].copyTo(cpuOutput); // 将数据从 GPU 复制到 CPU 的 cv::Mat 对象中
//float* data = reinterpret_cast<float*>(outputs.data); // 将数据赋值给 float* 指针
int rows = outputs[0].size[1];
int dimensions = outputs[0].size[2];
bool yolov8 = true;
// yolov5 has an output of shape (batchSize, 25200, 85) (Num classes + box[x,y,w,h] + confidence[c])
// yolov8 has an output of shape (batchSize, 84, 8400) (Num classes + box[x,y,w,h])
if (dimensions > rows) // Check if the shape[2] is more than shape[1] (yolov8)
{
yolov8 = true;
rows = outputs[0].size[2];
dimensions = outputs[0].size[1];
outputs[0] = outputs[0].reshape(1, dimensions);
cv::transpose(outputs[0], outputs[0]);
}
//if (cv::cuda::getCudaEnabledDeviceCount() > 0) { // 检查是否启用了GPU计算
//
// cv::cuda::GpuMat gpuData(outputs[0]); // 将 GPU 数据包装到 cv::cuda::GpuMat 中
// cv::Mat cpuData;
// gpuData.download(cpuData); // 将 GPU 数据下载到 CPU 的 cv::Mat 中
// float* data = (float*)cpuData.data; // 获取 CPU 上的数据指针
//}
//else { // 在没有启用GPU计算时,直接使用CPU内存中的数据指针
// data = (float*)outputs[0].data;
//}
//float* data = (float*)outputs[0].data;
//************************GPU和CPU的数据交换
//cv::UMat umatData = outputs[0].getUMat(cv::ACCESS_READ);
//cv::Mat cpuData;
//umatData.copyTo(cpuData);
//********************************
//float* data = (float*)cpuData.data;
float* data = (float*)outputs[0].data;
float x_factor = modelInput.cols / modelShape.width;
float y_factor = modelInput.rows / modelShape.height;
std::vector<int> class_ids;
std::vector<float> confidences;
std::vector<cv::Rect> boxes;
for (int i = 0; i < rows; ++i)
{
if (yolov8)
{
float* classes_scores = data + 4;
cv::Mat scores(1, classes.size(), CV_32FC1, classes_scores);
cv::Point class_id;
double maxClassScore;
minMaxLoc(scores, 0, &maxClassScore, 0, &class_id);
if (maxClassScore > modelScoreThreshold)
{
confidences.push_back(maxClassScore);
class_ids.push_back(class_id.x);
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));
}
}
else // yolov5
{
float confidence = data[4];
if (confidence >= modelConfidenceThreshold)
{
float* classes_scores = data + 5;
cv::Mat scores(1, classes.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 > modelScoreThreshold)
{
confidences.push_back(confidence);
class_ids.push_back(class_id.x);
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);
}
}
}
data += dimensions;
}
std::vector<int> nms_result;
cv::dnn::NMSBoxes(boxes, confidences, modelScoreThreshold, modelNMSThreshold, nms_result);
std::vector<Detection> detections{};
for (unsigned long i = 0; i < nms_result.size(); ++i)
{
int idx = nms_result[i];
Detection result;
result.class_id = class_ids[idx];
result.confidence = confidences[idx];
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int> dis(100, 255);
result.color = cv::Scalar(dis(gen),
dis(gen),
dis(gen));
result.className = classes[result.class_id];
result.box = boxes[idx];
detections.push_back(result);
}
return detections;
}
void Inference::loadClassesFromFile()
{
std::ifstream inputFile(classesPath);
if (inputFile.is_open())
{
std::string classLine;
while (std::getline(inputFile, classLine))
classes.push_back(classLine);
inputFile.close();
}
}
void Inference::loadOnnxNetwork()
{
net = cv::dnn::readNetFromONNX(modelPath);
if (cudaEnabled)
{
std::cout << "\nRunning on CUDA" << std::endl;
net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA_FP16);
}
else
{
std::cout << "\nRunning on CPU" << std::endl;
net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);
}
}
cv::Mat Inference::formatToSquare(const cv::Mat& source)
{
int col = source.cols;
int row = source.rows;
int _max = MAX(col, row);
cv::Mat result = cv::Mat::zeros(_max, _max, CV_8UC3);
source.copyTo(result(cv::Rect(0, 0, col, row)));
return result;
}
inference.h
#ifndef INFERENCE_H
#define INFERENCE_H
// Cpp native
#include <fstream>
#include <vector>
#include <string>
#include <random>
// OpenCV / DNN / Inference
#include <opencv2/imgproc.hpp>
#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>
//Detection结构体用来保存目标检测的结果实例
struct Detection
{
int class_id{ 0 };//整形变量用来存储检测到的目标的类别,默认值为0
std::string className{};//字符串变量用来存储检测到的目标的名称,默认值为空字符串
float confidence{ 0.0 };//目标检测的置信度(即对目标存在的确定程度)。默认值为0.0。
cv::Scalar color{};//OpenCV库中的Scalar类型变量,用于存储颜色信息。它可以表示RGB、BGR或灰度颜色空间中的颜色
cv::Rect box{}; //cv::Rect 类型包含四个成员变量:x、y、width 和 height
};
//Infrence类用来执行目标检测
class Inference
{
public:
//构造函数(modelInputShape是指模型的大小,默认为640,640;classesTxtFile是类别名称的文本文件路径(可选参数,默认为空字符串);runWithCuda是一个布尔值,指示是否使用CUDA加速运行(可选参数,默认为true)。
Inference(std::string onnxModelPath, cv::Size modelInputShape, std::string classesTxtFile, bool runWithCuda);
//公有成员函数,用于执行目标检测推断。它接受一个cv::Mat类型的输入图像,并返回一个std::vector<Detection>类型的检测结果。该函数将执行目标检测算法,将检测到的目标信息封装到Detection结构体中,并将所有检测结果存储在一个向量中。
std::vector<Detection> runInference(const cv::Mat& input);
//私有成员函数,用于内部操作
private:
//loadClassesFromFile函数从文本文件中加载类别名称
void loadClassesFromFile();
//loadOnnxNetwork函数加载ONNX模型
void loadOnnxNetwork();
//formatToSquare函数将输入图像调整为正方形形状。
cv::Mat formatToSquare(const cv::Mat& source);
//这些是私有成员变量
std::string modelPath{};//存储模型文件路径
std::string classesPath{};//类别文件路径
bool cudaEnabled{};//CUDA加速的状态
//字符串向量,用于存储目标检测的类别名称。默认情况下,它包含了一些通用的目标类别名称
std::vector<std::string> classes{ "bright_collision", "dark_collision" };
//std::vector<std::string> classes{ "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch", "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse", "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush" };
//这是一个OpenCV库中的Size2f类型变量,用于存储模型的输入形状(宽度和高度)。
cv::Size2f modelShape{};
//设置目标检测的阈值
float modelConfidenceThreshold{ 0.50 };//目标置信度的阈值
float modelScoreThreshold{ 0.45 };//目标得分的阈值
float modelNMSThreshold{ 0.50 };//非最大抑制的阈值
//布尔变量,指示是否使用letterbox技术将输入图像调整为正方形形状
bool letterBoxForSquare = true;
//该类封装了目标检测推断的相关操作和参数,通过调用构造函数和成员函数,你可以加载模型、执行推断,并获取目标检测的结果
cv::dnn::Net net;//penCV库中的Net类型变量,用于存储加载的目标检测网络模型
};
#endif // INFERENCE_H
main.cpp
#include "inference.h"
#include "opencv_yolo.h"
int main(int argc, char *argv[])
{
//main中调用
//执行视频检测算法和处理
bool runOnGPU = false;
int deviceId = 0; // 指定要使用的GPU设备的索引
//cv::cuda::setDevice(deviceId); //本机为amd卡,无法安装cuda所以此语句屏蔽
//1. 设置你的onnx模型
//注意,在这个例子中类别是硬编码的,'classes.txt'只是一个占位符。
Inference inf("D:/VsEnvironment/dataSet/best.onnx", cv::Size(640, 640), "classes.txt", runOnGPU); // classes.txt 可以缺失
string imgpath = "D:/VsEnvironment/dataSet/deep_screw_roi/test/images/Image_20241126164807270.bmp";
cv::Mat frame = imread(imgpath);
std::vector<Detection> output = inf.runInference(frame);
int detections = output.size();
std::cout << "Number of detections: " << detections << std::endl;
for (int i = 0; i < detections; ++i)
{
Detection detection = output[i];
cv::Rect box = detection.box;
cv::Scalar color = detection.color;
//根据类别不同,显示不同的颜色
if (detection.class_id == 0) {
color = cv::Scalar(0, 255, 0); // 红色 (B, G, R)
}
else if (detection.class_id == 1) {
color = cv::Scalar(0, 0, 255); // 红色 (B, G, R)
}
else {
color = cv::Scalar(255, 0, 0); // 红色 (B, G, R)
}
// Detection box
cv::rectangle(frame, box, color, 2);
// Detection box text
std::string classString = detection.className + ' ' + std::to_string(detection.confidence).substr(0, 4);
cv::Size textSize = cv::getTextSize(classString, cv::FONT_HERSHEY_DUPLEX, 1, 2, 0);
cv::Rect textBox(box.x, box.y - 40, textSize.width + 10, textSize.height + 20);
cv::rectangle(frame, textBox, color, cv::FILLED);
cv::putText(frame, classString, cv::Point(box.x + 5, box.y - 10), cv::FONT_HERSHEY_DUPLEX, 1, cv::Scalar(0, 0, 0), 2, 0);
}
cv::namedWindow("Inference", cv::WINDOW_NORMAL); // 创建具有可调整大小功能的窗口
cv::imshow("Inference", frame); // 在窗口中显示图像
cv::destroyAllWindows();
}
总结
如下为成功运行例程,检测图像涉及科研项目,此处不便展示
以后还将完成VS2019+opencv+onnxruntime cpu与gpu的cuda加速环境下部署目标检测模型的测试,同时上传csdn文章