目录
注:使用C++库为17,onnxruntime、opencv等各版本要大于等于官方推荐
注: +=后面不要有注释以及不要空一行;除最后一个路径的之后都要加\
2.YOLO_V8::TensorProcess需要修改(坑)
3.创建myYoloDetect(目前先试用检测功能,暂不分类)
一、YOLOv8下载和环境搭建(Python版本为3.8)
- github上下载项目ultralytics:https://github.com/ultralytics/ultralytics,可以选择git clone或者直接下载ZIP解压到本地。
- 本项目采用windows操作系统,使用CPU进行推理(默认操作),IDE为pycharm
- 由于目前yolov8官方已经将requirements.txt换成了pyproject.toml,只能在终端(terminal)使用pip install ultralytics命令安装需要的库(感觉更方便),或者使用命令pdm install pyproject.toml,但是这样路径会安装到项目中而虚拟环境识别不到,故建议使用前者。
二、Pycharm中YOLOv8模型导出
1.pytorch模型pt导出为onnx格式
对于不同平台需要,如嵌入式、移动端等需要的模型需要推理有更快的速度,官方也给出了C++和onnxruntime构建代码例子example,如下:
直接将cpp和h文件拷贝进入qt就可以了,CMakeLists.txt用来配置需要的链接库和路径,在QT中也可以换成QMake(.pro)使用,README.md有指出怎么导出onnx模型,如下(32位)
from ultralytics import YOLO
# Load a YOLOv8 model
model = YOLO("yolov8n.pt")
# Export the model,参数opset为导出版本,dynamic=True表示输入图片是否可以是动态的,反之则限制为一定尺寸,imgsz即是限定尺寸为640x640(没有onnx就pip install onnx)
model.export(format="onnx", opset=12, simplify=True, dynamic=False, imgsz=640)
如果你想使用16位浮点数的模型,一般是用NVDIA训练和推理的模型,而可以简化计算量而加快推理,得使用以下代码,导出的模型默认都在工作目录或当前运行的.py文件的同级目录
import onnx
from onnxconverter_common import float16
model = onnx.load(R"YOUR_ONNX_PATH")
model_fp16 = float16.convert_float_to_float16(model)
onnx.save(model_fp16, R"YOUR_FP16_ONNX_PATH")
其中模型文件yolov8n.pt的n对应的是网络参数特征(深度,宽度,最大输出特征图数量),可以见cfg/models/v8/yolov8.yaml,详细代码如下
scales: # model compound scaling constants, i.e. 'model=yolov8n.yaml' will call yolov8.yaml with scale 'n'
# [depth, width, max_channels]
n: [0.33, 0.25, 1024] # YOLOv8n summary: 225 layers, 3157200 parameters, 3157184 gradients, 8.9 GFLOPs
s: [0.33, 0.50, 1024] # YOLOv8s summary: 225 layers, 11166560 parameters, 11166544 gradients, 28.8 GFLOPs
m: [0.67, 0.75, 768] # YOLOv8m summary: 295 layers, 25902640 parameters, 25902624 gradients, 79.3 GFLOPs
l: [1.00, 1.00, 512] # YOLOv8l summary: 365 layers, 43691520 parameters, 43691504 gradients, 165.7 GFLOPs
x: [1.00, 1.25, 512] # YOLOv8x summary: 365 layers, 68229648 parameters, 68229632 gradients, 258.5 GFLOPs
2.官方COCO数据集coco.yaml介绍
yolov8模型共有80个检测目标,在coco.yaml(可以在这里coco数据集下载)中names里面就是检测到类别总数,不过在后面qt中使用它替换成txt文件也可以,只是用来读取类别并显示。
3.yolov8的onnx格式介绍
yolov8的onnx模型输出结构为1x84x8400,其中1为batch size即一次处理图片数量,84前4个分别是锚框中心的坐标位置(x,y),宽度、高度,后面80个为类别,8400为锚框总数,后面的80x8400个数为类别得分。
注:使用C++库为17,onnxruntime、opencv等各版本要大于等于官方推荐
三、QT中的配置(UI不介绍了)
1.opencv库和onnxruntime库安装
官网下载Releases - OpenCV,本人使用的是4.8.0,试过4.0.0也可以
官网下载https://github.com/microsoft/onnxruntime/releases/tag/v1.16.0,1.14.1也行,下载win-x64不带GPU的zip
2.QMake配置
在.pro文件中使用LIBS配置lib的位置,onnxruntime.lib,opencv_world480.lib以及opencv_world480d.lib,具体取决于你的路径,不过我直接装在E和F盘下所以看后面大部分即可
CONFIG(release,debug|release):LIBS +=-LE:/ONNX/onnxruntime-win-x64-1.16.0/onnxruntime-win-x64-1.16.0/lib/ -lonnxruntime\
-LF:/opencv/build/x64/vc16/lib/ -lopencv_world480
else:CONFIG(debug,debug|release):LIBS += -LE:/ONNX/onnxruntime-win-x64-1.16.0/onnxruntime-win-x64-1.16.0/lib/ -lonnxruntime\
-LF:/opencv/build/x64/vc16/lib/ -lopencv_world480d
配置头文件路径
INCLUDEPATH += F:/opencv/build/include\
F:/opencv/build/include/opencv2\
E:/ONNX/onnxruntime-win-x64-1.16.0/onnxruntime-win-x64-1.16.0/include
DEPENDPATH +=F:/opencv/build/include\
F:/opencv/build/include/opencv2\
E:/ONNX/onnxruntime-win-x64-1.16.0/onnxruntime-win-x64-1.16.0/include
注: +=后面不要有注释以及不要空一行;除最后一个路径的之后都要加\
3.动态链接库导入(注意)
在debug和release目录中要导入onnxruntime.dll和带shared的dll(在下载的),因为win系统中也有同名的dll库而会冲突,而如果环境变量里没有配置opencv的路径时也需要导入
四、QT+opencv视频流获取代码
1.视频捕获
CameraDetect.h
#ifndef CAMERADETECT_H
#define CAMERADETECT_H
#include "mainwindow.h"
#include "QObject"
#include "QTimer"
#include "QPixmap"
#include "myYoloDetect.h"
using namespace cv;
using namespace std;
class MainWindow;
class CameraDetect:public QObject
{
Q_OBJECT
public:
explicit CameraDetect(QObject *parent=nullptr);
void openCamera();
void closeCamera();
private:
VideoCapture *cap;//捕获视频流
QTimer *capTimer;
bool iSOpen=false; //是否打开相机标志
signals:
void cameraShowImage(QPixmap);
void cameraIsOpen(bool);
};
#endif // CAMERADETECT_H
CameraDetect.cpp
#include "cameraDetect.h"
#include "QDebug"
#include "QThread"
CameraDetect::CameraDetect(QObject *parent):QObject(parent)
{
}
void CameraDetect::openCamera()
{
//开启相机
int cameraDevId=0;
cap=new VideoCapture;
//使用默认相机,CAP_DSHOW表示win系统下的DirrectShow
cap->open(cameraDevId,CAP_DSHOW);
if(!cap->isOpened())
{
cap->release();
return;
}
else
iSOpen=true;
//将相机打开的信号发送出去
emit cameraIsOpen(true);
//qDebug()<<QString("相机参数:(%1,%2),%3 FPS").arg(cap->get(3)).arg(cap->get(4)).arg(cap->get(5));
//使用定时器定时捕获视频并显示,节省负载
capTimer=new QTimer;
connect(capTimer,&QTimer::timeout,[=]{
cv::Mat frame;
cap->read(frame);
if(frame.empty())
{
closeCamera();
return;
}
//导入YOLOv8时需要的检测
frame=myDetectTest(frame);
//相机RGB的转换,opencv默认是BGR的
cvtColor(frame,frame,COLOR_BGR2RGB);
QPixmap pixmap=QPixmap::fromImage(QImage((const uchar*)(frame.data),frame.cols,frame.rows,frame.step,QImage::Format_RGB888));
//将显示图像的信号发送出去
emit cameraShowImage(pixmap);
});
//采样时间
capTimer->start(1);
}
//关闭摄像头
void CameraDetect::closeCamera()
{
if(!iSOpen)
return;
if(capTimer->isActive())
{
capTimer->stop();
capTimer->deleteLater();
iSOpen=false;
emit cameraIsOpen(false);
cap->release();
cv::destroyAllWindows();
}
}
2.视频显示(缺乏的类直接include)
/*********实时检测************/
CameraDetect* camera;
QThread* cameraThread;
void MainWindow::time_Detect()
{
camera=new CameraDetect;
cameraThread=new QThread;
//将相机捕获放进新的线程中
camera->moveToThread(cameraThread);
//绑定线程与对象,如当线程发送finished信号时自动结束线程和删除相机对象,释放内存
connect(cameraThread,&QThread::finished,cameraThread,&QThread::deleteLater);
connect(cameraThread,&QThread::finished,camera,&CameraDetect::deleteLater);
//将camera对象的cameraShowImage函数与主界面的展示视频绑定在一起
connect(camera,&CameraDetect::cameraShowImage,this,&MainWindow::showVideoImage);
cameraThread->start();
//将自己设定的捕获开启按钮和结束按钮绑定在一起
connect(ui->startDetButton_2,&QPushButton::clicked,camera,&CameraDetect::openCamera);
connect(ui->clearButton_2,&QPushButton::clicked,camera,&CameraDetect::closeCamera);
}
void MainWindow::showVideoImage(QPixmap img)
{
//这是使用QGraphicsPixmapItem,QGraphicsScene和QGraphicView这样显示能够缩放
item1=new QGraphicsPixmapItem(img);
scene1->clear();
scene1->addItem(item1);
ui->input_Image->setScene(scene1);
//适应窗口缩放
ui->input_Image->fitInView(item1,Qt::KeepAspectRatio);
}
MainWindow::~MainWindow()
{
//结束时要先退出线程释放内存
if(cameraThread->isRunning())
{
cameraThread->quit();
cameraThread->wait();
}
delete ui;
}
五、YOLOv8官方cpp源码介绍和修改
1.(推理)inference.cpp/h(写在注释里)
typedef struct _DL_INIT_PARAM
{
std::string modelPath;
MODEL_TYPE modelType = YOLO_DETECT_V8;
std::vector<int> imgSize = { 640, 640 };
float rectConfidenceThreshold = 0.1; //选择锚框的置信度阈值
float iouThreshold = 0.5;//锚框如果重叠,其交并比如果大于0.5,则置信度低的会被抑制,不显示
int keyPointsNum = 2;//Note:kpt number for pose
bool cudaEnable = false;//选择你是否使用CUDA,需要在开头宏定义USE_CUDA
int logSeverityLevel = 3;
int intraOpNumThreads = 1;
} DL_INIT_PARAM;
2.YOLO_V8::TensorProcess需要修改(坑)
在onnx模型提取时要注意它的结构,使用yolov8时要对rawData进行一次翻转rawData.t(),如下。
cv::Mat rawData;
if (modelType == YOLO_DETECT_V8)
{
// FP32
rawData = cv::Mat(strideNum, signalResultNum, CV_32F, output);
rawData=rawData.t();//需要翻转
}
而在遍历得分时,源码的strideNum(84)和signalResultNum(8400)要调换,因为如上所述84是锚框中的位置大小和类别个数,8400这是锚框的数量(感觉它名字好像起错了?),每个点则是得分,得分最高的类别则存储它的框的坐标位置和大小,遍历8400次。修改如下
for (int i = 0; i < signalResultNum; ++i)
{
float* classesScores = data+4; //获取得分,前4个是坐标宽长
cv::Mat scores(1, this->classes.size(), CV_32FC1, classesScores); //80个类别的得分
cv::Point class_id;
double maxClassScore=0;
cv::minMaxLoc(scores, 0, &maxClassScore, 0, &class_id); //根据得分找出id索引
/***测试max***/
// QString test=QString::fromStdString(std::to_string(maxClassScore));
// qDebug()<<"测试max:"<<test;
if (maxClassScore > rectConfidenceThreshold)
{
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];
// qDebug()<<x<<" "<<y<<" "<<w<<" "<<h<<" "<<data[4];
int left = int((x - 0.5 * w) * resizeScales);
int top = int((y - 0.5 * h) * resizeScales);
int width = int(w * resizeScales);
int height = int(h * resizeScales);
boxes.push_back(cv::Rect(left, top, width, height));
}
data += strideNum; //地址跳转到下一个框
}
3.创建myYoloDetect(目前先试用检测功能,暂不分类)
myYoloDetect.h
#ifndef MYYOLODETECT_H
#define MYYOLODETECT_H
#include <iostream>
#include <iomanip>
#include "inference.h"
#include <filesystem>
#include <fstream>
#include <random>
#include "QImage"
cv::Mat videoDetector(YOLO_V8*& p,cv::Mat &img);
int ReadCocoYaml(YOLO_V8*& p);
cv::Mat myDetectTest(cv::Mat &img);//目标检测使用
#endif // MYYOLODETECT_H
myYoloDetect.cpp
#include "myYoloDetect.h"
#include "QString"
#include "QDebug"
#include "QDir"
#include "QFile"
#include "QTemporaryFile"
#include "QCoreApplication"
//实时视频检测
cv::Mat videoDetector(YOLO_V8*& p,cv::Mat &img) {
std::vector<DL_RESULT> res;
p->RunSession(img, res);
for (auto& re : res)
{
cv::RNG rng(cv::getTickCount());
cv::Scalar color(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256));
cv::rectangle(img, re.box, color, 3);
float confidence = floor(100*re.confidence) / 100;
std::cout << std::fixed << std::setprecision(2);
std::string label = p->classes[re.classId] + " " +
std::to_string(confidence).substr(0, std::to_string(confidence).size() - 4);
//标签绘制
cv::rectangle(
img,
cv::Point(re.box.x, re.box.y - 25),
cv::Point(re.box.x + label.length() * 15, re.box.y),
color,
cv::FILLED
);
cv::putText(
img,
label,
cv::Point(re.box.x, re.box.y - 5),
cv::FONT_HERSHEY_SIMPLEX,
0.75,
cv::Scalar(0, 0, 0),
2
);
// QString labelstr=QString::fromStdString(std::to_string(re.classId)+p->classes[re.classId]);
// qDebug()<<"类别为:"<<labelstr;
}
return img;
}
int ReadCocoYaml(YOLO_V8*& p) {
// Open the YAML file,用来创建临时文件
QString relativePath=":/Yolov8/build/Desktop_Qt_6_2_4_MSVC2019_64bit-Release/YOLOv8/coco.yaml";
QString tempPathDir=QCoreApplication::applicationDirPath();
QString tempFileName=tempPathDir+"/temp_coco_xx.yaml";
QString copyFileName=tempPathDir+"/temp_coco.txt";
QFile copyFile(copyFileName);
QTemporaryFile tempfile(tempFileName);
if(copyFile.size()==0)
//打开临时文件进行写入
{
if(tempfile.open())
{
copyFile.setPermissions(QFileDevice::ReadUser | QFileDevice::WriteUser);
tempfile.setPermissions(QFileDevice::ReadUser | QFileDevice::WriteUser);
QFile resourceFile(relativePath);
resourceFile.open(QIODevice::ReadOnly);
tempfile.write(resourceFile.readAll());
tempfile.flush();
tempFileName=tempfile.fileName();
tempfile.close();
QFile::copy(tempFileName,copyFileName);
}
}
std::ifstream file(copyFileName.toStdString());
if (!file.is_open())
{
std::cerr << "Failed to open file" << std::endl;
return 1;
}
// Read the file line by line
std::string line;
std::vector<std::string> lines;
while (std::getline(file, line))
{
lines.push_back(line);
}
// Find the start and end of the names section
std::size_t start = 0;
std::size_t end = 0;
for (std::size_t i = 0; i < lines.size(); i++)
{
if (lines[i].find("names:") != std::string::npos)
{
start = i + 1;
}
else if (start > 0 && lines[i].find(':') == std::string::npos)
{
end = i;
break;
}
}
// Extract the names
std::vector<std::string> names;
for (std::size_t i = start; i < end; i++)
{
std::stringstream ss(lines[i]);
std::string name;
std::getline(ss, name, ':'); // Extract the number before the delimiter
std::getline(ss, name); // Extract the string after the delimiter
names.push_back(name);
}
p->classes = names;
return 0;
}
cv::Mat myDetectTest(cv::Mat &img)
{
YOLO_V8* yoloDetector = new YOLO_V8;
QString relativePath=":/Yolov8/build/Desktop_Qt_6_2_4_MSVC2019_64bit-Release/YOLOv8/yolov8n.onnx";
// 获取应用程序目录
QString appDirPath = QCoreApplication::applicationDirPath();
// 指定临时文件的路径和文件名模板
QString tempFileTemplate = appDirPath + "/temp_yolov8n_xxx.onnx";
QTemporaryFile tempfile(tempFileTemplate);
QString tempFileName,onnxName= appDirPath + "/temp_yolov8n.onnx";
QFile file(onnxName);
//打开临时文件进行写入
if(file.size()==0)
{
if(tempfile.open())
{
tempfile.setPermissions(QFileDevice::ReadUser | QFileDevice::WriteUser);
QFile resourceFile(relativePath);
resourceFile.open(QIODevice::ReadOnly);
tempfile.write(resourceFile.readAll());
tempfile.flush();
tempFileName=tempfile.fileName();
tempfile.close();
//复制文件至onnx文件格式,否则临时文件随机后缀加载不出来
QFile::copy(tempFileName,onnxName);
tempfile.destroyed();
}
}
//QString relativePath=modelPathQstr+"/YOLOv8/yolov8n.onnx";
ReadCocoYaml(yoloDetector);
DL_INIT_PARAM params;
params.rectConfidenceThreshold = 0.1;
params.iouThreshold = 0.5;
params.modelPath = onnxName.toStdString();
params.imgSize = { 640, 640 };
#ifdef USE_CUDA
params.cudaEnable = true;
// GPU FP32 inference
params.modelType = YOLO_DETECT_V8;
// GPU FP16 inference
//Note: change fp16 onnx model
//params.modelType = YOLO_DETECT_V8_HALF;
#else
// CPU inference
params.modelType = YOLO_DETECT_V8;
params.cudaEnable = false;
#endif
yoloDetector->CreateSession(params);
cv::Mat detect_Img= videoDetector(yoloDetector,img);
return detect_Img;
}
代码解释和补充
由于考虑需要打包处理,模型onnx文件和yaml文件都最好以资源形式导入,即添加到资源管理器中,但是资源文件的路径不能被yolov8使用的标准文件IO流读取,故创建临时文件读取并复制,复制后就不会临时再创建了,不过其实可以直接复制资源文件出来,只是当时笔者想偏了又改了。
YOLO_V8* yoloDetector = new YOLO_V8;
QString relativePath=":/Yolov8/build/Desktop_Qt_6_2_4_MSVC2019_64bit-Release/YOLOv8/yolov8n.onnx";
// 获取应用程序目录
QString appDirPath = QCoreApplication::applicationDirPath();
// 指定临时文件的路径和文件名模板
// QString tempFileTemplate = appDirPath + "/temp_yolov8n_xxx.onnx";
// QTemporaryFile tempfile(tempFileTemplate);
QString tempFileName,onnxName= appDirPath + "/temp_yolov8n.onnx";
QFile file(onnxName);
//打开临时文件进行写入
if(file.size()==0)
{
// tempfile.setPermissions(QFileDevice::ReadUser | QFileDevice::WriteUser);
// QFile resourceFile(relativePath);
// resourceFile.open(QIODevice::ReadOnly);
// tempfile.write(resourceFile.readAll());
// tempfile.flush();
// tempFileName=tempfile.fileName();
// tempfile.close();
//复制文件至onnx文件格式,否则临时文件随机后缀加载不出来
QFile::copy(relativePath,onnxName);
}
4.运行调试(意料之内有点卡,我的CPU推理速度很慢)
六、QT打包
1.使用MSVC终端打包
将build中release里的exe复制到新的文件夹里,然后打开MSVC终端(如果你用的是MSVC构建的)并到达当前exe的目录,输入命令windeployqt xxx.exe,就会初步打包完成
2.使用工具打包工具再次封装
Software Protection, Software Licensing, Software Virtualization在官网下载Enigma Virtual Box,同时,要把opencv_world480.dll,onnxruntime.dll,onnxruntime_xxx_shared.dll复制到刚刚到exe打包后的目录,然后在这个打包工具中添加这个目录,点击Browse添加exe等等之类的操作,具体可以看别的朋友的链接http://t.csdnimg.cn/HsZet,注意动态链接库导入就行,不然没有该环境变量的电脑打不开的。