序文
プロジェクトのトレーニングからソースコードの解釈までを経て、ついに最終デプロイの段階に到達しました。プロジェクトの背景を知らない学生は、最初の 2 つのブログを読むことができます。ここでは例としてWindows システムの C++ デプロイメントを使用しますが、Linux システムでのコードも同様です。このブログは主に C++ コードに基づいており、単語を減らしてコメントを多く書くようにしてください。読者は自分で練習できます。
1. 導入環境の準備
IDE: VS2022; 推論エンジン: OnnxRuntime (CPU バージョン 1.14.1); 開発言語: C++
トレーニングされた ONNX モデルの場合、推論展開のために Microsoft が開発した OnnxRuntime エンジンを使用すると非常に便利です。まず、VS2022 で OnnxRuntime ライブラリを構成する必要があります。OnnxRuntime の Github 公式 Web サイト:
GitHub -microsoft/onnxruntime:ONNX ランタイム:クロスプラットフォーム、高性能 ML 推論およびトレーニング アクセラレータONNX ランタイム:クロスプラットフォーム、高性能 ML 推論およびトレーニング アクセラレータ - GitHub -microsoft/onnxruntime:ONNX ランタイム:クロスプラットフォーム、高性能ML 推論およびトレーニング アクセラレータhttps://github.com/microsoft/onnxruntime releases ディレクトリに移動し、お使いのマシン (Windows システムの場合は x64、アーキテクチャが aarch64 か x64 など) に対応する OnnxRuntime パッケージをダウンロードします。 . Linux システムの場合)。ダウンロードが完了したら、VS でインクルード ディレクトリとライブラリ ディレクトリを構成し、対応する OnnxRuntime dll ダイナミック ライブラリ ファイルをプロジェクト実行フォルダーにコピーします。準備作業が完了したら、コードの記述を開始できます。ここで注意してください。OnnxRuntime ライブラリに加えて、プロジェクトで OpenCV が構成されていない場合は、オンライン チュートリアルに従って構成してください。OpenCV は ONNX モデルのデプロイメントにも依存します。
2 つ目は、C++ デプロイメント コードです。
最初の 2 つのブログでナンセンスな話はすべて終わりました。コードはここに直接記載されています。この考え方は、Anomalib プロジェクトでの Python コードの実装と同じです (したがって、Python コードを読んで理解することが唯一の方法です)。コードはブロガー自身によって実装された Qt プロジェクトからインターセプトされ、主に推論タスクを完了するための Inferencer クラスを定義します。コードはヘッダファイル Inferencer.h とソースファイル Inferencer.cpp に分かれており、コードの説明はコメント形式で記載されています。コードは以下のように表示されます:
Inferenceer.h
#pragma once
#include<onnxruntime_cxx_api.h>
#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/opencv.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc_c.h>
#include <opencv2/dnn.hpp>
#include <iostream>
#include <assert.h>
#include <vector>
#include <fstream>
using namespace std;
//定义推理器类
class Inferencer
{
private:
Ort::Session *session; //onnx运行会话
void preProcess(const cv::Mat& image, cv::Mat& image_blob); //预处理
vector<int64_t> input_dims;
vector<int64_t> output_dims;
vector<char*> input_node_names;
vector<char*> output_node_names;
public:
Inferencer(const wchar_t* modelPath); //使用模型路径构造推推理器
void InitOnnxEnv(); //初始化环境
//分别生成概率热图、二值化图、缺陷边缘图和检测框图
void generateHeatMap(cv::Mat& input, cv::Mat& heatMap, cv::Mat& predMask, cv::Mat& contourMap, cv::Mat& boxMap);
};
Inferenceer.cpp
#include"Inferencer.h"
//此处注意,在Linux系统下,输入参数modelPath应为const char*类型,而不是const wchar_t*
Inferencer::Inferencer(const wchar_t* modelPath)
{
Ort::Env env(ORT_LOGGING_LEVEL_WARNING);
Ort::SessionOptions session_options;
session_options.SetIntraOpNumThreads(1);
session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);
cout << "正在使用Onnxruntime C++ API\n";
session = new Ort::Session(env, modelPath, session_options);
}
void Inferencer::preProcess(const cv::Mat& image, cv::Mat& image_blob)
{
cv::Mat input;
image.copyTo(input);
//数据处理 标准化
std::vector<cv::Mat> channels, channel_p;
split(input, channels);
cv::Mat R, G, B;
B = channels.at(0);
G = channels.at(1);
R = channels.at(2);
//按照ImageNet的均值和方差进行标准化预处理
B = (B / 255. - 0.406) / 0.225;
G = (G / 255. - 0.456) / 0.224;
R = (R / 255. - 0.485) / 0.229;
channel_p.push_back(R);
channel_p.push_back(G);
channel_p.push_back(B);
cv::Mat outt;
merge(channel_p, outt);
image_blob = outt;
}
void Inferencer::InitOnnxEnv()
{
//打印模型的各项信息
auto num_input_nodes = session->GetInputCount();
auto num_output_nodes = session->GetOutputCount();
cout << "Number of inputs = " << num_input_nodes << endl;
cout << "Number of outputs = " << num_output_nodes << endl;
this->input_dims = session->GetInputTypeInfo(0).GetTensorTypeAndShapeInfo().GetShape();
this->output_dims = session->GetOutputTypeInfo(0).GetTensorTypeAndShapeInfo().GetShape();
cout << "input_dims:" << this->input_dims[3] << endl;
cout << "output_dims:" << this->output_dims[3] << endl;
Ort::AllocatorWithDefaultOptions allocator;
//此处早期版本的onnxruntime的API不同
auto input_name_ptr = session->GetInputNameAllocated(0, allocator);
this->input_node_names.push_back(input_name_ptr.get());
auto output_name_ptr = session->GetOutputNameAllocated(0, allocator);
this->output_node_names.push_back(output_name_ptr.get());
}
void Inferencer::generateHeatMap(cv::Mat& input, cv::Mat& heatMap, cv::Mat& predMask, cv::Mat& contourMap, cv::Mat& boxMap)
{
//metadata.json中的信息
float image_threshold = 13.702226638793945;
float pixel_threshold = 13.702226638793945;
float min_val = 5.296699047088623;
float max_val = 22.767864227294922;
cv::Mat det1, det2;
cv::resize(input, det1, cv::Size(256, 256), cv::INTER_AREA);
det1.convertTo(det1, CV_32FC3);
//标准化处理
Inferencer::preProcess(det1, det2);
cv::Mat blob = cv::dnn::blobFromImage(det2, 1., cv::Size(256, 256), cv::Scalar(0, 0, 0), false, true);
cout << "加载成功!" << endl;
clock_t startTime, endTime;
//创建输入tensor
auto memory_info = Ort::MemoryInfo::CreateCpu(OrtAllocatorType::OrtArenaAllocator, OrtMemType::OrtMemTypeDefault);
vector<Ort::Value> input_tensors;
input_tensors.push_back(Ort::Value::CreateTensor<float>(memory_info, blob.ptr<float>(), blob.total(), input_dims.data(), input_dims.size()));
startTime = clock();
//auto output_tensors = session->Run(Ort::RunOptions{ nullptr }, input_node_names.data(), input_tensors.data(), input_node_names.size(), output_node_names.data(), output_node_names.size());
const char* ch_in = "input";
const char* const* p_in = &ch_in;
const char* ch_out = "output";
const char* const* p_out = &ch_out;
//output_tensors这里直接固定输入和输出的结点名p_in和p_out
auto output_tensors = session->Run(Ort::RunOptions{ nullptr }, p_in, input_tensors.data(), 1, p_out, 1);
endTime = clock();
assert(output_tensors.size() == 1 && output_tensors.front().IsTensor());
float* floatarr = output_tensors[0].GetTensorMutableData<float>();
cv::Mat anomalyMap = cv::Mat_<float>(256, 256);
int k = 0;
for (int i = 0; i < 256; i++)
{
for (int j = 0; j < 256; j++) //矩阵列数循环
{
float val = floatarr[++k];
anomalyMap.at<float>(i, j) = val;
}
}
//标准化处理
cv::Mat norm = ((anomalyMap - pixel_threshold) / (max_val - min_val)) + 0.5;
//double minValue, maxValue;
//cv::Point minIdx, maxIdx;
//cv::minMaxLoc(norm, &minValue, &maxValue, &minIdx, &maxIdx);
norm *= 255;
//转换为uint8灰度图
cv::Mat normUint8;
norm.convertTo(normUint8, CV_8UC1);
//转换为伪彩色图
cv::Mat colorMap;
cv::applyColorMap(normUint8, colorMap, cv::COLORMAP_JET);
//与原图叠加生成热图
cv::resize(input, input, cv::Size(256, 256));
cv::addWeighted(colorMap, 0.4, input, 0.6, 0, heatMap);
//生成二值区域
cv::threshold(anomalyMap, predMask, pixel_threshold, 255, CV_THRESH_BINARY);
predMask.convertTo(predMask, CV_8UC1);
//生成缺陷轮廓
cv::resize(input, contourMap, cv::Size(256, 256));
vector<vector<cv::Point>> contours;
cv::findContours(predMask, contours, cv::noArray(), cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
cv::drawContours(contourMap, contours, -1, cv::Scalar(0, 0, 255), 2);
//生成缺陷检测框和尺寸信息
cv::resize(input, boxMap, cv::Size(256, 256));
for (int i = 0; i < contours.size(); i++)
{
cv::Rect rect = cv::boundingRect(contours[i]);
cv::rectangle(boxMap, rect, cv::Scalar(0, 255, 0), 1);
}
}
Qt を使用して GUI プログラムを作成すると、次の図のようになります。
効果はまだ大丈夫のようです。
3. 概要と展望
これで大きなプロジェクトは終わりです~花を散らして~プロジェクトを最初から最後までやり遂げることができたのは、私にとってまだ一定の進歩です。ここでは、CPU によるモデルの推論と展開のみが実装されます。レンガや翡翠を投げたり、GPU を高速化したり、GUI プログラムを書いたりして探索するのは生徒たちに任されています。