第一步:首先先设置好模型的路径:
std::string model_path = "E:\\2_workcode\\QT\\OpenCVtest\\learn\\moulds\\yolov8n.onnx";
第二步创建运行Session:
2.1建议:我们可以首先创建一个专门用于图像检测的参数结构体
typedef struct _DCSP_INIT_PARAM {
std::string ModelPath;
MODEL_TYPE ModelType = YOLO_ORIGIN_V8;
std::vector<int> imgSize = {640, 640};
double RectConfidenceThreshold = 0.6;
float iouThreshold = 0.5;
bool CudaEnable = false;
int LogSeverityLevel = 3;
int IntraOpNumThreads = 1;
} DCSP_INIT_PARAM;
2.2利用参数定义类内的私有成员变量
DCSP_INIT_PARAM params{ model_path, YOLO_ORIGIN_V8, {640, 640}, 0.1, 0.5, false };
rectConfidenceThreshold = iParams.RectConfidenceThreshold;
iouThreshold = iParams.iouThreshold;
imgSize = iParams.imgSize;
modelType = iParams.ModelType;
env = Ort::Env(ORT_LOGGING_LEVEL_WARNING, "Yolo");
const char *modelPath = iParams.ModelPath.c_str();//模型地址
2.3定义sessionOption,并对session进行初始化
Ort::SessionOptions sessionOption;//定义session
if (iParams.CudaEnable) {//是不是用cuda
cudaEnable = iParams.CudaEnable;
OrtCUDAProviderOptions cudaOption;
cudaOption.device_id = 0;
sessionOption.AppendExecutionProvider_CUDA(cudaOption);
}
sessionOption.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);//初始化优先级
sessionOption.SetIntraOpNumThreads(iParams.IntraOpNumThreads);//初始化线程
sessionOption.SetLogSeverityLevel(iParams.LogSeverityLevel);//设置日志
session = new Ort::Session(env, modelPath, sessionOption);
2.4创建内存分配器(方便对输入输出节点的名字进行迭代转移)
Ort::AllocatorWithDefaultOptions allocator;
size_t inputNodesNum = session->GetInputCount();获得输入的节点数
for (size_t i = 0; i < inputNodesNum; i++) {
Ort::AllocatedStringPtr input_node_name = session->GetInputNameAllocated(i, allocator);获得第i个节点数的名字读取到input_node_name
char *temp_buf = new char[50];
strcpy(temp_buf, input_node_name.get());//get函数通常用于获取原始指针
inputNodeNames.push_back(temp_buf);//这是一个指针数组
}
size_t OutputNodesNum = session->GetOutputCount();
for (size_t i = 0; i < OutputNodesNum; i++) {
Ort::AllocatedStringPtr output_node_name = session->GetOutputNameAllocated(i, allocator);
char *temp_buf = new char[10];
strcpy(temp_buf, output_node_name.get());
outputNodeNames.push_back(temp_buf);
}
第三步:首先先设置好图片路径
std::filesystem::path current_path = "E:/2_workcode/QT/OpenCVtest/learn/";
std::filesystem::path imgs_path = current_path / "images";
std::string img_path = i.path().string();
cv::Mat img = cv::imread(img_path);
第四步运行推理:
4.1 图片前端处理
char *PostProcess(cv::Mat &iImg, std::vector<int> iImgSize, cv::Mat &oImg) {//总之是对图片进行前端处理
cv::Mat img = iImg.clone();
cv::resize(iImg, oImg, cv::Size(iImgSize.at(0), iImgSize.at(1)));//将输入的尺寸进行处理
if (img.channels() == 1) {//判断通道数是否为1
cv::cvtColor(oImg, oImg, cv::COLOR_GRAY2BGR);//将输入通道数进行处理
}
cv::cvtColor(oImg, oImg, cv::COLOR_BGR2RGB);//图片变为BGR
return RET_OK;
}
4.2将前端处理后的图片进行blob处理作为神经网路的输入信息
float *blob = new float[processedImg.total() * 3];//一个一维数组
BlobFromImage(processedImg, blob);
两种方法获得bolb数据:
第一种:利用bolb函数进行转化,后面的创建输入张量采用.data形式输入。
cv::Mat blob = cv::dnn::blobFromImages(processedImg, 1 / 255.0, input_size, cv::Scalar(0, 0, 0), true, false);
Ort::Value::CreateTensor<float>(_OrtMemoryInfo, (float*)blob.data, input_tensor_length, _inputTensorShape.data(), _inputTensorShape.size())
第二种:直接利用手动进行转化(不太推荐,有现有的直接使用也可以,不如OpenCV集成好的)
template<typename T>
char *BlobFromImage(cv::Mat &iImg, T &iBlob) {//这个函数是将转化后的图片变成bold类型(它使用OpenCV库来处理图像并将图像数据转换为一个浮点数数组,通常称为“blob”。这个blob可以用于深度学习框架中的输入)
int channels = iImg.channels();
int imgHeight = iImg.rows;
int imgWidth = iImg.cols;
for (int c = 0; c < channels; c++) {
for (int h = 0; h < imgHeight; h++) {
for (int w = 0; w < imgWidth; w++) {
iBlob[c * imgWidth * imgHeight + h * imgWidth + w] = typename std::remove_pointer<T>::type(//表达式 c * imgWidth * imgHeight + h * imgWidth + w 通常用于多通道图像数据的索引计算。在图像处理中,特别是在处理多维数组或多通道图像时,这种表达式用于计算特定像素点在一维数组中的索引位置。
(iImg.at<cv::Vec3b>(h, w)[c]) / 255.0f);//对于图像中的每个像素,提取对应的通道值,除以255以将其归一化到0到1的范围内,并将结果存储在iBlob中。
}
}
}
return RET_OK;
}
第五步:张量处理
5.1创建一个输入的张量,此处的bolb就是前面由输入图片转成的bolb
std::vector<int64_t> inputNodeDims = {1, 3, imgSize.at(0), imgSize.at(1)};//定义输入节点的尺寸1*3*640*640
Ort::Value inputTensor =
Ort::Value::CreateTensor<typename std::remove_pointer<N>::type>
(
Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU),
blob,
3 * imgSize.at(0) * imgSize.at(1),
inputNodeDims.data(), inputNodeDims.size()
);//定义输入张量.data() 函数通常用于获取指向容器中第一个元素的指针。
//这个指针可以用来访问和修改容器中的元素,但它不提供任何关于容器大小或元素数量的信息。
//.size() 函数用于获取容器中元素的数量。
//它返回一个整数值,表示容器中当前存储的元素个数。
第六步:将处理后的输入张量在创建的session环境中运行(这里虽然用的是auto,其实就是Ort::Value类型的参数)
auto outputTensor = session->Run
(options,
inputNodeNames.data(),
&inputTensor,
inputNodeNames.size(),
outputNodeNames.data(),
outputNodeNames.size());//得到输出张量
第七步:获取输出张量的信息
Ort::TypeInfo typeInfo = outputTensor.front().GetTypeInfo();//获得输出类型信息
auto tensor_info = typeInfo.GetTensorTypeAndShapeInfo();//获得输出张量类型(int/float)和输出维度(长宽高通道)
std::vector<int64_t> outputNodeDims = tensor_info.GetShape();//输出维度(长宽高通道),当然这里也可以将上面的整合起来但是这样会显得很长
auto output = outputTensor.front().GetTensorMutableData<typename std::remove_pointer<N>::type>();//这行代码的目的是获取outputTensor容器中第一个张量的数据指针,并确保指针的类型是正确的,即去除任何指针类型,直接指向数据类型的指针。这样,output变量就可以直接访问张量中的数据,进行读写操作。
delete blob[];//这里之前的输入blob的数据内存记得删除进行内存管理
第八步:将输出的参数进行转化,然后对输出张量信息进行处理,以及定义输出图片的所需参数
int64_t strideNum = outputNodeDims[2];//这是传递给cv::Mat构造函数的第二个参数,8400
int64_t signalResultNum = outputNodeDims[1];//这是传递给cv::Mat构造函数的第一个参数,84
std::vector<int> class_ids;//标签
std::vector<float> confidences;//定义置信度
std::vector<cv::Rect> boxes;//定义处理框
第九步:根据输出张量,生成未处理数据mat(8400是v8模型各尺度输出特征图叠加之后的结果)
YOLOV8训练出来的数据为84行8400列,84代表参数,8400列代表特征,因此我们看原始的数据需要竖着看,转置之后变成横着看,同时指针的更新也更加方便了。
rawData = cv::Mat(signalResultNum, strideNum, CV_32F, output);//这里是创建一个84*8400的矩阵需要与output的1*84*8400相对应要不创建不出来
rawData = rawData.t();//.t():这是 cv::Mat 类的一个成员函数,用于获取矩阵的转置。获得8400*84的mat
第十步:对取得的结果进行边界框绘制
(进行循环的时候,一共8400行,每一行一共84列,行代表特征即循环的变量,列代表参数(包括矩形的长宽高和各类别得分),每次循环都需要在最后对数据指针进行更新以确保后面新的特征的检查)
float *data = (float *) rawData.data;//返回一个指向 cv::Mat 对象数据的指针。
double x_factor = iImg.cols / 640.0;//定义x方向上的缩放比例,宽度
double y_factor = iImg.rows / 640.0;//定义y方向上的缩放比例,高度
for (int i = 0; i < strideNum; ++i) //进行不同步长循环,这里是指的所有特征图叠加之后的结果
{
float *classesScores = data + 4;//获取指向类别得分数组的指针,从 data 指针偏移4个 float 的大小(假设前4个 float 存储的是边界框的坐标和大小)。
cv::Mat scores(1, this->classes.size(), CV_32FC1, classesScores);//指针创建一个 cv::Mat 对象,表示类别得分矩阵。
cv::Point class_id;//用于存储得分最高的类别索引。
double maxClassScore;
cv::minMaxLoc(scores, 0, &maxClassScore, 0, &class_id);//使用 cv::minMaxLoc 函数找到 scores 矩阵中的最大值和它的位置。这里传递 0 作为最小值的指针,因为我们只关心最大值。
if (maxClassScore > rectConfidenceThreshold) {//如果最高类别得分大于某个阈值 rectConfidenceThreshold,则认为检测是有效的。
confidences.push_back(maxClassScore);//将最高类别得分添加到 confidences 向量中。
class_ids.push_back(class_id.x);//:将最高得分类别的索引添加到 class_ids 向量中
float x = data[0];//从 data 指针开始,读取并存储边界框的中心点x坐标。这个就是当时为什么得分要空出四个位置来的原因
float y = data[1];//读取并存储边界框的中心点y坐标。
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);//x_factor 和 y_factor 可能是用于将网络输出映射到输入图像尺寸的比例因子。
int height = int(h * y_factor);
boxes.emplace_back(left, top, width, height);//创建一个边界框
}
data += signalResultNum;//更新 data 指针,跳过当前处理的数据,准备读取下一个检测结果。signalResultNum 可能是每个检测结果的数据总数。
}
第十一步:对生成后的一堆框进行非极大值抑制操作从而更准确的获得我们想要的结果
std::vector<int> nmsResult;//声明一个整数类型的向量 nmsResult,用于存储NMS操作后的索引结果。
cv::dnn::NMSBoxes(boxes, confidences, rectConfidenceThreshold, iouThreshold, nmsResult);
for (int i = 0; i < nmsResult.size(); ++i) {//处理每个保留下来的边界框。
int idx = nmsResult[i];//获取当前处理的边界框在原始列表中的索引。
DCSP_RESULT result;//声明一个 DCSP_RESULT 类型的对象 result,这可能是一个自定义的结构或类,用于存储最终的检测结果。
result.classId = class_ids[idx];//
result.confidence = confidences[idx];//
result.box = boxes[idx];
oResult.push_back(result);//将包含类别ID、置信度和边界框坐标的 result 对象添加到输出结果向量 oResult 中。
第十二步:我们将结果返回后便可以进行最终操作(将结果画到测试图片上)
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
);
}
std::cout << "Press any key to exit" << std::endl;
cv::imshow("Result of Detection", img);
res是一个存放结果的结构体:如下所示
typedef struct _DCSP_RESULT {
int classId;
float confidence;
cv::Rect box;
} DCSP_RESULT;