国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

【模型部署 01】C++實現(xiàn)GoogLeNet在OpenCV DNN、ONNXRuntime、TensorRT、OpenVINO上的推理部署

這篇具有很好參考價值的文章主要介紹了【模型部署 01】C++實現(xiàn)GoogLeNet在OpenCV DNN、ONNXRuntime、TensorRT、OpenVINO上的推理部署。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

深度學習領(lǐng)域常用的基于CPU/GPU的推理方式有OpenCV DNN、ONNXRuntime、TensorRT以及OpenVINO。這幾種方式的推理過程可以統(tǒng)一用下圖來概述。整體可分為模型初始化部分和推理部分,后者包括步驟2-5。

【模型部署 01】C++實現(xiàn)GoogLeNet在OpenCV DNN、ONNXRuntime、TensorRT、OpenVINO上的推理部署

以GoogLeNet模型為例,測得幾種推理方式在推理部分的耗時如下:

【模型部署 01】C++實現(xiàn)GoogLeNet在OpenCV DNN、ONNXRuntime、TensorRT、OpenVINO上的推理部署

結(jié)論:

  1. GPU加速首選TensorRT;
  2. CPU加速,單圖推理首選OpenVINO,多圖并行推理可選擇ONNXRuntime;
  3. 如果需要兼具CPU和GPU推理功能,可選擇ONNXRuntime。

下一篇內(nèi)容:【模型部署 02】Python實現(xiàn)GoogLeNet在OpenCV DNN、ONNXRuntime、TensorRT、OpenVINO上的推理部署

1. 環(huán)境配置

1.1 OpenCV DNN

  【模型部署】OpenCV4.6.0+CUDA11.1+VS2019環(huán)境配置

1.2 ONNXRuntime

  【模型部署】在C++和Python中配置ONNXRuntime環(huán)境

1.3 TensorRT

  【模型部署】在C++和Python中搭建TensorRT環(huán)境?

1.4 OpenVINO2022

  【模型部署】在C++和Python中配置OpenVINO2022環(huán)境

2. PyTorch模型文件(pt/pth/pkl)轉(zhuǎn)ONNX

2.1 pt/pth/pkl互轉(zhuǎn)

PyTorch中支持導(dǎo)出三種后綴格式的模型文件:pt、pth和pkl,這三種格式在存儲方式上并無區(qū)別,只是后綴不同。三種格式之間的轉(zhuǎn)換比較簡單,只需要創(chuàng)建模型并加載模型參數(shù),然后再保存為其他格式即可。

以pth轉(zhuǎn)pt為例:

import torch
import torchvision

# 構(gòu)建模型
model = torchvision.models.googlenet(num_classes=2, init_weights=True)
# 加載模型參數(shù),pt/pth/pkl三種格式均可
model.load_state_dict(torch.load("googlenet_catdog.pth"))
model.eval()
# 重新保存為所需要轉(zhuǎn)換的格式
torch.save(model.state_dict(), 'googlenet_catdog.pt')

2.2 pt/pth/pkl轉(zhuǎn)ONNX

PyTorch中提供了現(xiàn)成的函數(shù)torch.onnx.export(),可將模型文件轉(zhuǎn)換成onnx格式。該函數(shù)原型如下:

export(model, args, f, export_params=True, verbose=False, training=TrainingMode.EVAL,
           input_names=None, output_names=None, operator_export_type=None,
           opset_version=None, do_constant_folding=True, dynamic_axes=None,
           keep_initializers_as_inputs=None, custom_opsets=None,
           export_modules_as_functions=False)

主要參數(shù)含義:

  • model?(torch.nn.Module, torch.jit.ScriptModule or torch.jit.ScriptFunction)?:需要轉(zhuǎn)換的模型。
  • args?(tuple or torch.Tensor) :args可以被設(shè)置為三種形式:
    • 一個tuple,這個tuple應(yīng)該與模型的輸入相對應(yīng),任何非Tensor的輸入都會被硬編碼入onnx模型,所有Tensor類型的參數(shù)會被當做onnx模型的輸入。
      args = (x, y, z)
    • 一個Tensor,一般這種情況下模型只有一個輸入。
      args = torch.Tensor([1, 2, 3])
    • 一個帶有字典的tuple,這種情況下,所有字典之前的參數(shù)會被當做“非關(guān)鍵字”參數(shù)傳入網(wǎng)絡(luò),字典中的鍵值對會被當做關(guān)鍵字參數(shù)傳入網(wǎng)絡(luò)。如果網(wǎng)絡(luò)中的關(guān)鍵字參數(shù)未出現(xiàn)在此字典中,將會使用默認值,如果沒有設(shè)定默認值,則會被指定為None。
      args = (x,
              {'y': input_y,
               'z': input_z})
      

      NOTE:一個特殊情況,當網(wǎng)絡(luò)本身最后一個參數(shù)為字典時,直接在tuple最后寫一個字典則會被誤認為關(guān)鍵字傳參。所以,可以通過在tuple最后添加一個空字典來解決。

      # 錯誤寫法:
      torch.onnx.export(
          model,
          (x,
           # WRONG: will be interpreted as named arguments
           {y: z}),
          "test.onnx.pb")
       
      # 糾正
      torch.onnx.export(
          model,
          (x,
           {y: z},
           {}),
          "test.onnx.pb")?
  • f:一個文件類對象或一個路徑字符串,二進制的protocol buffer將被寫入此文件,即onnx文件。
  • export_params?(bool,?default False)?:如果為True則導(dǎo)出模型的參數(shù)。如果想導(dǎo)出一個未訓(xùn)練的模型,則設(shè)為False。
  • verbose?(bool,?default False) :如果為True,則打印一些轉(zhuǎn)換日志,并且onnx模型中會包含doc_string信息。
  • training?(enum,?default TrainingMode.EVAL)?:枚舉類型包括:
    • TrainingMode.EVAL - 以推理模式導(dǎo)出模型。
    • TrainingMode.PRESERVE - 如果model.training為False,則以推理模式導(dǎo)出;否則以訓(xùn)練模式導(dǎo)出。
    • TrainingMode.TRAINING - 以訓(xùn)練模式導(dǎo)出,此模式將禁止一些影響訓(xùn)練的優(yōu)化操作。
  • input_names?(list of str,?default empty list)?:按順序分配給onnx圖的輸入節(jié)點的名稱列表。
  • output_names?(list of str,?default empty list)?:按順序分配給onnx圖的輸出節(jié)點的名稱列表。
  • operator_export_type?(enum,?default None)?:默認為OperatorExportTypes.ONNX, 如果Pytorch built with DPYTORCH_ONNX_CAFFE2_BUNDLE,則默認為OperatorExportTypes.ONNX_ATEN_FALLBACK。枚舉類型包括:
    • OperatorExportTypes.ONNX - 將所有操作導(dǎo)出為ONNX操作。
    • OperatorExportTypes.ONNX_FALLTHROUGH - 試圖將所有操作導(dǎo)出為ONNX操作,但碰到無法轉(zhuǎn)換的操作(如onnx未實現(xiàn)的操作),則將操作導(dǎo)出為“自定義操作”,為了使導(dǎo)出的模型可用,運行時必須支持這些自定義操作。支持自定義操作方法見鏈接。
    • OperatorExportTypes.ONNX_ATEN - 所有ATen操作導(dǎo)出為ATen操作,ATen是Pytorch的內(nèi)建tensor庫,所以這將使得模型直接使用Pytorch實現(xiàn)。(此方法轉(zhuǎn)換的模型只能被Caffe2直接使用)
    • OperatorExportTypes.ONNX_ATEN_FALLBACK - 試圖將所有的ATen操作也轉(zhuǎn)換為ONNX操作,如果無法轉(zhuǎn)換則轉(zhuǎn)換為ATen操作(此方法轉(zhuǎn)換的模型只能被Caffe2直接使用)。例如:
      # 轉(zhuǎn)換前:
      graph(%0 : Float):
        %3 : int = prim::Constant[value=0]()
        # conversion unsupported
        %4 : Float = aten::triu(%0, %3)
        # conversion supported
        %5 : Float = aten::mul(%4, %0)
        return (%5)
      
      
      # 轉(zhuǎn)換后:
      graph(%0 : Float):
        %1 : Long() = onnx::Constant[value={0}]()
        # not converted
        %2 : Float = aten::ATen[operator="triu"](%0, %1)
        # converted
        %3 : Float = onnx::Mul(%2, %0)
        return (%3)
  • opset_version?(int,?default 9) :取值必須等于_onnx_main_opset或在_onnx_stable_opsets之內(nèi)。具體可在torch/onnx/symbolic_helper.py中找到。例如:
    _default_onnx_opset_version = 9
    _onnx_main_opset = 13
    _onnx_stable_opsets = [7, 8, 9, 10, 11, 12]
    _export_onnx_opset_version = _default_onnx_opset_version
  • do_constant_folding?(bool,?default False)?:是否使用“常量折疊”優(yōu)化。常量折疊將使用一些算好的常量來優(yōu)化一些輸入全為常量的節(jié)點。
  • example_outputs?(T or a tuple of T, where T is Tensor or convertible to Tensor, default None)?:當需輸入模型為ScriptModule 或 ScriptFunction時必須提供。此參數(shù)用于確定輸出的類型和形狀,而不跟蹤(tracing)模型的執(zhí)行。
  • dynamic_axes?(dict<string, dict<python:int, string>> or dict<string, list(int)>, default empty dict)?:通過以下規(guī)則設(shè)置動態(tài)的維度:
    • KEY(str) - 必須是input_names或output_names指定的名稱,用來指定哪個變量需要使用到動態(tài)尺寸。
    • VALUE(dict or list) - 如果是一個dict,dict中的key是變量的某個維度,dict中的value是我們給這個維度取的名稱。如果是一個list,則list中的元素都表示此變量的某個維度。

代碼實現(xiàn):

import torch
import torchvision

weight_file = 'googlenet_catdog.pt'
onnx_file = 'googlenet_catdog.onnx'

model = torchvision.models.googlenet(num_classes=2, init_weights=True)
model.load_state_dict(torch.load(weight_file, map_location=torch.device('cpu')))

model.eval()

# 單輸入單輸出,固定batch
input = torch.randn(1, 3, 224, 224)
input_names = ["input"]
output_names = ["output"]
torch.onnx.export(model=model,
                  args=input,
                  f=onnx_file,
                  input_names=input_names,
                  output_names=output_names,
                  opset_version=11,
                  verbose=True)

通過netron.app可視化onnx的輸入輸出: 

【模型部署 01】C++實現(xiàn)GoogLeNet在OpenCV DNN、ONNXRuntime、TensorRT、OpenVINO上的推理部署

如果需要多張圖片同時進行推理,可以通過設(shè)置export的dynamic_axes參數(shù),將模型輸入輸出的指定維度設(shè)置為變量。

import torch
import torchvision

weight_file = 'googlenet_catdog.pt'
onne_file = 'googlenet_catdog.onnx'

model = torchvision.models.googlenet(num_classes=2, init_weights=True)
model.load_state_dict(torch.load(weight_file, map_location=torch.device('cpu')))

model.eval()

# 單輸入單輸出,動態(tài)batch
input = torch.randn(1, 3, 224, 224)
input_names = ["input"]
output_names = ["output"]
torch.onnx.export(model=model,
                  args=input,
                  f=onnx_file,
                  input_names=input_names,
                  output_names=output_names,
                  opset_version=11,
                  verbose=True,
                  dynamic_axes={'input': {0: 'batch'}, 'output': {0: 'batch'}})

動態(tài)batch的onnx文件輸入輸出在netron.app可視化如下,其中batch維度是變量的形式,可以根據(jù)自己需要設(shè)置為大于0的任意整數(shù)。

【模型部署 01】C++實現(xiàn)GoogLeNet在OpenCV DNN、ONNXRuntime、TensorRT、OpenVINO上的推理部署

如果模型有多個輸入和輸出,按照以下形式導(dǎo)出:

# 模型有兩個輸入和兩個輸出,動態(tài)batch
input1 = torch.randn(1, 3, 256, 192).to(opt.device)
input2 = torch.randn(1, 3, 256, 192).to(opt.device)
input_names = ["input1", "input2"]
output_names = ["output1", "output2"]
torch.onnx.export(model=model,
                  args=(input1, input2),
                  f=opt.onnx_path,
                  input_names=input_names,
                  output_names=output_names,
                  opset_version=16,
                  verbose=True,
                  dynamic_axes={'input1': {0: 'batch'},
                                'input2': {0: 'batch'},
                                'output1': {0: 'batch'},
                                'output2': {0: 'batch'}})

3. OpenCV DNN部署GoogLeNet

3.1 推理過程及代碼實現(xiàn)

【模型部署 01】C++實現(xiàn)GoogLeNet在OpenCV DNN、ONNXRuntime、TensorRT、OpenVINO上的推理部署

整個推理過程可分為前處理、推理、后處理三部分。具體細節(jié)請閱讀代碼,包括單圖推理、動態(tài)batch推理的實現(xiàn)。

#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>
#include <chrono>
#include <fstream>

using namespace std;
using namespace cv;
using namespace cv::dnn;

std::string onnxPath = "E:/inference-master/models/engine/googlenet-pretrained_batch.onnx";
std::string imagePath = "E:/inference-master/images/catdog";
std::string classNamesPath = "E:/inference-master/imagenet-classes.txt";			// 標簽名稱列表(類名)

cv::dnn::Net net;
std::vector<std::string> classNameList;		// 標簽名,可以從文件讀取
int batchSize = 32;

int softmax(const cv::Mat& src, cv::Mat& dst)
{
	float max = 0.0;
	float sum = 0.0;

	max = *max_element(src.begin<float>(), src.end<float>());
	cv::exp((src - max), dst);
	sum = cv::sum(dst)[0];
	dst /= sum;

	return 0;
}

// GoogLeNet模型初始化
void ModelInit(string onnxPath)
{
	net = cv::dnn::readNetFromONNX(onnxPath);
	// net = cv::dnn::readNetFromCaffe("E:/inference-master/2/deploy.prototxt", "E:/inference-master/2/default.caffemodel");

	// 設(shè)置計算后臺和計算設(shè)備
	// CPU(默認)
	// net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
	// net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);
	// CUDA
	net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
	net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA);

	// 讀取標簽名稱
	ifstream fin(classNamesPath.c_str());
	string strLine;
	classNameList.clear();
	while (getline(fin, strLine))
		classNameList.push_back(strLine);
	fin.close();
}

// 單圖推理
bool ModelInference(cv::Mat srcImage, std::string& className, float& confidence)
{
	auto start = chrono::high_resolution_clock::now();

	cv::Mat image = srcImage.clone();

	// 預(yù)處理(尺寸變換、通道變換、歸一化)
	cv::cvtColor(image, image, cv::COLOR_BGR2RGB);
	cv::resize(image, image, cv::Size(224, 224));
	image.convertTo(image, CV_32FC3, 1.0 / 255.0);
	cv::Scalar mean(0.485, 0.456, 0.406);
	cv::Scalar std(0.229, 0.224, 0.225);
	cv::subtract(image, mean, image);
	cv::divide(image, std, image);

	// blobFromImage操作順序:swapRB交換通道 -> scalefactor比例縮放 -> mean求減 -> size進行resize;
	// mean操作時,ddepth不能選取CV_8U;
	// crop=True時,先等比縮放,直到寬高之一率先達到對應(yīng)的size尺寸,另一個大于或等于對應(yīng)的size尺寸,然后從中心裁剪;
	// 返回4-D Mat維度順序:NCHW
	// cv::Mat blob = cv::dnn::blobFromImage(image, 1., cv::Size(224, 224), cv::Scalar(0, 0, 0), false, false);
	cv::Mat blob = cv::dnn::blobFromImage(image);

	// 設(shè)置輸入
	net.setInput(blob);

	auto end1 = std::chrono::high_resolution_clock::now();
	auto ms1 = std::chrono::duration_cast<std::chrono::microseconds>(end1 - start);
	std::cout << "PreProcess time: " << (ms1 / 1000.0).count() << "ms" << std::endl;

	// 前向推理
	cv::Mat preds = net.forward();

	auto end2 = std::chrono::high_resolution_clock::now();
	auto ms2 = std::chrono::duration_cast<std::chrono::microseconds>(end2 - end1);
	std::cout << "Inference time: " << (ms2 / 1000.0).count() << "ms" << std::endl;

	// 結(jié)果歸一化(每個batch分別求softmax)
	softmax(preds, preds);

	Point minLoc, maxLoc;
	double minValue = 0, maxValue = 0;
	cv::minMaxLoc(preds, &minValue, &maxValue, &minLoc, &maxLoc);
	int labelIndex = maxLoc.x;
	double probability = maxValue;

	className = classNameList[labelIndex];
	confidence = probability;

	// std::cout << "class:" << className << endl << "confidence:" << confidence << endl;

	auto end3 = std::chrono::high_resolution_clock::now();
	auto ms3 = std::chrono::duration_cast<std::chrono::microseconds>(end3 - end2);
	std::cout << "PostProcess time: " << (ms3 / 1000.0).count() << "ms" << std::endl;

	auto ms = chrono::duration_cast<std::chrono::microseconds>(end3 - start);
	std::cout << "opencv_dnn 推理時間:" << (ms / 1000.0).count() << "ms" << std::endl;
}

// 多圖并行推理(動態(tài)batch)
bool ModelInference_Batch(std::vector<cv::Mat> srcImages, std::vector<string>& classNames, std::vector<float>& confidences)
{
	auto start = chrono::high_resolution_clock::now();

	// 預(yù)處理(尺寸變換、通道變換、歸一化)
	std::vector<cv::Mat> images;
	for (size_t i = 0; i < srcImages.size(); i++)
	{
		cv::Mat image = srcImages[i].clone();
		cv::cvtColor(image, image, cv::COLOR_BGR2RGB);
		cv::resize(image, image, cv::Size(224, 224));
		image.convertTo(image, CV_32FC3, 1.0 / 255.0);
		cv::Scalar mean(0.485, 0.456, 0.406);
		cv::Scalar std(0.229, 0.224, 0.225);
		cv::subtract(image, mean, image);
		cv::divide(image, std, image);

		images.push_back(image);
	}

	cv::Mat blob = cv::dnn::blobFromImages(images);

	auto end1 = std::chrono::high_resolution_clock::now();
	auto ms1 = std::chrono::duration_cast<std::chrono::microseconds>(end1 - start);
	std::cout << "PreProcess time: " << (ms1 / 1000.0).count() << "ms" << std::endl;

	// 設(shè)置輸入
	net.setInput(blob);

	// 前向推理
	cv::Mat	preds =  net.forward();

	auto end2 = std::chrono::high_resolution_clock::now();
	auto ms2 = std::chrono::duration_cast<std::chrono::microseconds>(end2 - end1) / 100.0;
	std::cout << "Inference time: " << (ms2 / 1000.0).count() << "ms" << std::endl;

	int rows = preds.size[0];	// batch
	int cols = preds.size[1];	// 類別數(shù)(每一個類別的得分)
	for (int row = 0; row < rows; row++)
	{
		cv::Mat scores(1, cols, CV_32FC1, preds.ptr<float>(row));
		softmax(scores, scores);	// 結(jié)果歸一化

		Point minLoc, maxLoc;
		double minValue = 0, maxValue = 0;
		cv::minMaxLoc(scores, &minValue, &maxValue, &minLoc, &maxLoc);
		int labelIndex = maxLoc.x;
		double probability = maxValue;

		classNames.push_back(classNameList[labelIndex]);
		confidences.push_back(probability);
	}

	auto end3 = std::chrono::high_resolution_clock::now();
	auto ms3 = std::chrono::duration_cast<std::chrono::microseconds>(end3 - end2);
	std::cout << "PostProcess time: " << (ms3 / 1000.0).count() << "ms" << std::endl;

	auto ms = chrono::duration_cast<std::chrono::microseconds>(end3 - start);
	std::cout << "opencv_dnn batch" << rows << " 推理時間:" << (ms / 1000.0).count() << "ms" << std::endl;
}

int main(int argc, char** argv)
{
	// 模型初始化
	ModelInit(onnxPath);

	// 讀取圖像
	vector<string> filenames;
	glob(imagePath, filenames);

	// 單圖推理測試
	for (int n = 0; n < filenames.size(); n++)
	{
		// 重復(fù)100次,計算平均時間
		auto start = chrono::high_resolution_clock::now();
		cv::Mat src = imread(filenames[n]);
		std::string classname;
		float confidence;
		for (int i = 0; i < 101; i++) {
			if (i==1)
				start = chrono::high_resolution_clock::now();
			ModelInference(src, classname, confidence);
		}
		auto end = chrono::high_resolution_clock::now();
		auto ms = chrono::duration_cast<std::chrono::microseconds>(end - start) / 100;
		std::cout << "opencv_dnn 平均推理時間:---------------------" << (ms / 1000.0).count() << "ms" << std::endl;
	}

	// 批量(動態(tài)batch)推理測試
	std::vector<cv::Mat> srcImages;
	for (int n = 0; n < filenames.size(); n++)
	{
		cv::Mat image = imread(filenames[n]);
		srcImages.push_back(image);
		if ((n + 1) % batchSize == 0 || n == filenames.size() - 1)
		{
			// 重復(fù)100次,計算平均時間
			auto start = chrono::high_resolution_clock::now();
			for (int i = 0; i < 101; i++) {
				if (i == 1)
					start = chrono::high_resolution_clock::now();
				std::vector<std::string> classNames;
				std::vector<float> confidences;
				ModelInference_Batch(srcImages, classNames, confidences);	
			}	
			srcImages.clear();
			auto end = chrono::high_resolution_clock::now();
			auto ms = chrono::duration_cast<std::chrono::microseconds>(end - start) / 100;
			std::cout << "opencv_dnn batch" << batchSize << " 平均推理時間:---------------------" << (ms / 1000.0).count() << "ms" << std::endl;	
		}	
	}

	return 0;
}

3.2 選擇CPU/GPU

OpenCV DNN切換CPU和GPU推理,只需要通過下邊兩行代碼設(shè)置計算后臺和計算設(shè)備。

CPU推理

net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);

GPU推理

net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA); 

以下兩點需要注意:

  • 在不做任何設(shè)置的情況下,默認使用CPU進行推理。
  • 在設(shè)置為GPU推理時,如果電腦沒有搜索到CUDA環(huán)境,則會自動轉(zhuǎn)換成CPU進行推理。

3.3 多輸出模型推理

當模型有多個輸出時,使用forward的重載方法,返回Mat類型的數(shù)組:

// 模型多輸出
std::vector<cv::Mat> preds;
net.forward(preds);

cv::Mat pred1 = preds[0];
cv::Mat pred2 = preds[1];

4. ONNXRuntime部署GoogLeNet

4.1 推理過程及代碼實現(xiàn)

【模型部署 01】C++實現(xiàn)GoogLeNet在OpenCV DNN、ONNXRuntime、TensorRT、OpenVINO上的推理部署

代碼:

#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>
#include <onnxruntime_cxx_api.h>
#include <vector>
#include <fstream>
#include <chrono>

using namespace std;
using namespace cv;
using namespace Ort;

// C++表示字符串的方式:char*、string、wchar_t*、wstring、字符串數(shù)組
const wchar_t* onnxPath = L"E:/inference-master/models/GoogLeNet/googlenet-pretrained_batch1.onnx";
std::string imagePath = "E:/inference-master/images/catdog";
std::string classNamesPath = "E:/inference-master/imagenet-classes.txt";			// 標簽名稱列表(類名)

std::vector<std::string> classNameList;		// 標簽名,可以從文件讀取
int batchSize = 1;

Ort::Env env{ nullptr };
Ort::SessionOptions* sessionOptions;

Ort::Session* session;
size_t inputCount;
size_t outputCount;
std::vector<const char*> inputNames;
std::vector<const char*> outputNames;
std::vector<int64_t> inputShape;
std::vector<int64_t> outputShape;

// 對數(shù)組元素求softmax
std::vector<float> softmax(std::vector<float> input)
{
	float total = 0;
	for (auto x : input)
		total += exp(x);
	std::vector<float> result;
	for (auto x : input)
		result.push_back(exp(x) / total);

	return result;
}

int softmax(const cv::Mat& src, cv::Mat& dst)
{
	float max = 0.0;
	float sum = 0.0;

	max = *max_element(src.begin<float>(), src.end<float>());
	cv::exp((src - max), dst);
	sum = cv::sum(dst)[0];
	dst /= sum;

	return 0;
}


// 前(預(yù))處理(通道變換、標準化等)
void PreProcess(cv::Mat srcImage, cv::Mat& dstImage)
{
	// 通道變換,BGR->RGB
	cvtColor(srcImage, dstImage, cv::COLOR_BGR2RGB);
	resize(dstImage, dstImage, Size(224, 224));
	// 圖像歸一化
	dstImage.convertTo(dstImage, CV_32FC3, 1.0 / 255.0);
	cv::Scalar mean(0.485, 0.456, 0.406);
	cv::Scalar std(0.229, 0.224, 0.225);
	subtract(dstImage, mean, dstImage);
	divide(dstImage, std, dstImage);
}

// 模型初始化
int ModelInit(const wchar_t* onnxPath, bool useCuda, int deviceId)
{
	// 讀取標簽名稱
	std::ifstream fin(classNamesPath.c_str());
	std::string strLine;
	classNameList.clear();
	while (getline(fin, strLine))
		classNameList.push_back(strLine);
	fin.close();

	// 環(huán)境設(shè)置,控制臺輸出設(shè)置
	env = Ort::Env(ORT_LOGGING_LEVEL_WARNING, "GoogLeNet");

	sessionOptions = new Ort::SessionOptions();
	// 設(shè)置線程數(shù)
	sessionOptions->SetIntraOpNumThreads(16);
	// 優(yōu)化等級:啟用所有可能的優(yōu)化
	sessionOptions->SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);

	if (useCuda) {
		// 開啟CUDA加速,需要cuda_provider_factory.h頭文件
		OrtSessionOptionsAppendExecutionProvider_CUDA(*sessionOptions, deviceId);
	}	

	// 創(chuàng)建session
	session = new Ort::Session(env, onnxPath, *sessionOptions);

	// 獲取輸入輸出數(shù)量
	inputCount = session->GetInputCount();
	outputCount = session->GetOutputCount();
	std::cout << "Number of inputs = " << inputCount << std::endl;
	std::cout << "Number of outputs = " << outputCount << std::endl;

	// 獲取輸入輸出名稱
	Ort::AllocatorWithDefaultOptions allocator;
	const char* inputName = session->GetInputName(0, allocator);
	const char* outputName = session->GetOutputName(0, allocator);
	inputNames = { inputName };
	outputNames = { outputName };
	std::cout << "Name of inputs = " << inputName << std::endl;
	std::cout << "Name of outputs = " << outputName << std::endl;

	// 獲取輸入輸出維度信息,返回類型std::vector<int64_t>
	inputShape = session->GetInputTypeInfo(0).GetTensorTypeAndShapeInfo().GetShape();
	outputShape = session->GetOutputTypeInfo(0).GetTensorTypeAndShapeInfo().GetShape();
	std::cout << "Shape of inputs = " << "(" << inputShape[0] << "," << inputShape[1] << "," << inputShape[2] << "," << inputShape[3] << ")" << std::endl;
	std::cout << "Shape of outputs = " << "(" << outputShape[0] << "," << outputShape[1] << ")" << std::endl;

	return 0;
}

// 單圖推理
void ModelInference(cv::Mat srcImage, std::string& className, float& confidence)
{
	auto start = chrono::high_resolution_clock::now();
	
	// 輸入圖像預(yù)處理
	cv::Mat image;
	//PreProcess(srcImage, image);  // 這里使用調(diào)用函數(shù)的方式,處理時間莫名變長很多,很奇怪

	// 通道變換,BGR->RGB
	cvtColor(srcImage, image, cv::COLOR_BGR2RGB);
	resize(image, image, Size(224, 224));
	// 圖像歸一化
	image.convertTo(image, CV_32FC3, 1.0 / 255.0);
	cv::Scalar mean(0.485, 0.456, 0.406);
	cv::Scalar std(0.229, 0.224, 0.225);
	subtract(image, mean, image);
	divide(image, std, image);

	cv::Mat blob = cv::dnn::blobFromImage(image);

	auto end1 = std::chrono::high_resolution_clock::now();
	auto ms1 = std::chrono::duration_cast<std::chrono::microseconds>(end1 - start);
	std::cout << "PreProcess time: " << (ms1 / 1000.0).count() << "ms" << std::endl;

	// 創(chuàng)建輸入tensor
	auto memoryInfo = Ort::MemoryInfo::CreateCpu(OrtAllocatorType::OrtArenaAllocator, OrtMemType::OrtMemTypeDefault);
	std::vector<Ort::Value> inputTensors;
	inputTensors.emplace_back(Ort::Value::CreateTensor<float>(memoryInfo,
		blob.ptr<float>(), blob.total(), inputShape.data(), inputShape.size()));

	// 推理
	auto outputTensors = session->Run(Ort::RunOptions{ nullptr },
		inputNames.data(), inputTensors.data(), inputCount, outputNames.data(), outputCount);

	auto end2 = std::chrono::high_resolution_clock::now();
	auto ms2 = std::chrono::duration_cast<std::chrono::microseconds>(end2 - end1);
	std::cout << "Inference time: " << (ms2 / 1000.0).count() << "ms" << std::endl;

	// 獲取輸出
	float* preds = outputTensors[0].GetTensorMutableData<float>();	// 也可以使用outputTensors.front();

	int64_t numClasses = outputShape[1];
	cv::Mat output = cv::Mat_<float>(1, numClasses);
	for (int j = 0; j < numClasses; j++) {
		output.at<float>(0, j) = preds[j];
	}

	Point minLoc, maxLoc;
	double minValue = 0, maxValue = 0;
	cv::minMaxLoc(output, &minValue, &maxValue, &minLoc, &maxLoc);
	int labelIndex = maxLoc.x;
	double probability = maxValue;

	className = classNameList[1];
	confidence = probability;

	auto end3 = std::chrono::high_resolution_clock::now();
	auto ms3 = std::chrono::duration_cast<std::chrono::microseconds>(end3 - end2);
	std::cout << "PostProcess time: " << (ms3 / 1000.0).count() << "ms" << std::endl;

	auto ms = chrono::duration_cast<std::chrono::microseconds>(end3 - start);
	std::cout << "onnxruntime單圖推理時間:" << (ms / 1000.0).count() << "ms" << std::endl;
}

// 單圖推理
void ModelInference_Batch(std::vector<cv::Mat> srcImages, std::vector<string>& classNames, std::vector<float>& confidences)
{
	auto start = chrono::high_resolution_clock::now();

	// 輸入圖像預(yù)處理
	std::vector<cv::Mat> images;
	for (size_t i = 0; i < srcImages.size(); i++)
	{
		cv::Mat image = srcImages[i].clone();

		// 通道變換,BGR->RGB
		cvtColor(image, image, cv::COLOR_BGR2RGB);
		resize(image, image, Size(224, 224));
		// 圖像歸一化
		image.convertTo(image, CV_32FC3, 1.0 / 255.0);
		cv::Scalar mean(0.485, 0.456, 0.406);
		cv::Scalar std(0.229, 0.224, 0.225);
		subtract(image, mean, image);
		divide(image, std, image);

		images.push_back(image);
	}

	// 圖像轉(zhuǎn)blob格式
	cv::Mat blob = cv::dnn::blobFromImages(images);

	auto end1 = std::chrono::high_resolution_clock::now();
	auto ms1 = std::chrono::duration_cast<std::chrono::microseconds>(end1 - start);
	std::cout << "PreProcess time: " << (ms1 / 1000.0).count() << "ms" << std::endl;

	// 創(chuàng)建輸入tensor
	std::vector<Ort::Value> inputTensors;
	auto memoryInfo = Ort::MemoryInfo::CreateCpu(OrtAllocatorType::OrtArenaAllocator, OrtMemType::OrtMemTypeDefault);
	inputTensors.emplace_back(Ort::Value::CreateTensor<float>(memoryInfo,
		blob.ptr<float>(), blob.total(), inputShape.data(), inputShape.size()));
	
	// 推理
	std::vector<Ort::Value> outputTensors = session->Run(Ort::RunOptions{ nullptr },
		inputNames.data(), inputTensors.data(), inputCount, outputNames.data(), outputCount);

	auto end2 = std::chrono::high_resolution_clock::now();
	auto ms2 = std::chrono::duration_cast<std::chrono::microseconds>(end2 - end1)/100;
	std::cout << "inference time: " << (ms2 / 1000.0).count() << "ms" << std::endl;

	// 獲取輸出
	float* preds = outputTensors[0].GetTensorMutableData<float>();	// 也可以使用outputTensors.front();

	// cout << preds[0] << "," << preds[1] << "," << preds[1000] << "," << preds[1001] << endl;

	int batch = outputShape[0];
	int numClasses = outputShape[1];

	cv::Mat output(batch, numClasses, CV_32FC1, preds);

	int rows = output.size[0];	// batch
	int cols = output.size[1];	// 類別數(shù)(每一個類別的得分)
	for (int row = 0; row < rows; row++)
	{
		cv::Mat scores(1, cols, CV_32FC1, output.ptr<float>(row));
		softmax(scores, scores);	// 結(jié)果歸一化

		Point minLoc, maxLoc;
		double minValue = 0, maxValue = 0;
		cv::minMaxLoc(scores, &minValue, &maxValue, &minLoc, &maxLoc);
		int labelIndex = maxLoc.x;
		double probability = maxValue;

		classNames.push_back(classNameList[labelIndex]);
		confidences.push_back(probability);
	}

	auto end3 = std::chrono::high_resolution_clock::now();
	auto ms3 = std::chrono::duration_cast<std::chrono::microseconds>(end3 - end2);
	std::cout << "PostProcess time: " << (ms3 / 1000.0).count() << "ms" << std::endl;

	auto ms = chrono::duration_cast<std::chrono::microseconds>(end3 - start);
	std::cout << "onnxruntime單圖推理時間:" << (ms / 1000.0).count() << "ms" << std::endl;
}

int main(int argc, char** argv)
{
	// 模型初始化
	ModelInit(onnxPath, true, 0);

	// 讀取圖像
	std::vector<std::string> filenames;
	cv::glob(imagePath, filenames);

	// 單圖推理測試
	for (int i = 0; i < filenames.size(); i++)
	{
		// 每張圖重復(fù)運行100次,計算平均時間
		auto start = chrono::high_resolution_clock::now();
		cv::Mat srcImage = imread(filenames[i]);
		std::string className;
		float confidence;
		for (int n = 0; n < 101; n++) {
			if (n == 1)
				start = chrono::high_resolution_clock::now();
			ModelInference(srcImage, className, confidence);
		}

		// 顯示
		cv::putText(srcImage, className + ":" + std::to_string(confidence),
			cv::Point(10, 20), FONT_HERSHEY_SIMPLEX, 0.6, cv::Scalar(0, 0, 255), 1, 1);

		auto end = chrono::high_resolution_clock::now();
		auto ms = chrono::duration_cast<std::chrono::microseconds>(end - start) / 100;
		std::cout << "onnxruntime 平均推理時間:---------------------" << (ms / 1000.0).count() << "ms" << std::endl;
	}

	// 批量推理測試
	std::vector<cv::Mat> srcImages;
	for (int i = 0; i < filenames.size(); i++)
	{
		cv::Mat image = imread(filenames[i]);
		srcImages.push_back(image);
		if ((i + 1) % batchSize == 0 || i == filenames.size() - 1)
		{
			// 重復(fù)100次,計算平均時間
			auto start = chrono::high_resolution_clock::now();
			for (int n = 0; n < 101; n++) {
				if (n == 1)
					start = chrono::high_resolution_clock::now();	// 首次推理耗時很久
				std::vector<std::string> classNames;
				std::vector<float> confidences;
				ModelInference_Batch(srcImages, classNames, confidences);
			}
			srcImages.clear();
			auto end = chrono::high_resolution_clock::now();
			auto ms = chrono::duration_cast<std::chrono::microseconds>(end - start) / 100;
			std::cout << "onnxruntime batch" << batchSize << " 平均推理時間:---------------------" << (ms / 1000.0).count() << "ms" << std::endl;
		}
	}

	return 0;
}

注意:ORT支持多圖并行推理,但是要求轉(zhuǎn)出onnx的時候batch就要使用固定數(shù)值。動態(tài)batch(即batch=-1)的onnx文件是不支持推理的。

4.2 選擇CPU/GPU

使用GPU推理,只需要添加一行代碼:

if (useCuda) {
	// 開啟CUDA加速
	OrtSessionOptionsAppendExecutionProvider_CUDA(*sessionOptions, deviceId);
} 

4.3 多輸入多輸出模型推理

推理步驟和單圖推理基本一致,需要在輸入tensor中依次添加所有的輸入。假設(shè)模型有兩個輸入和兩個輸出:

// 創(chuàng)建session
session2 = new Ort::Session(env1, onnxPath, sessionOptions1);

// 獲取模型輸入輸出信息
inputCount2 = session2->GetInputCount();
outputCount2 = session2->GetOutputCount();

// 輸入和輸出各有兩個
Ort::AllocatorWithDefaultOptions allocator;
const char* inputName1 = session2->GetInputName(0, allocator);
const char* inputName2 = session2->GetInputName(1, allocator);
const char* outputName1 = session2->GetOutputName(0, allocator);
const char* outputName2 = session2->GetOutputName(1, allocator);
intputNames2 = { inputName1, inputName2 };
outputNames2 = { outputName1, outputName2 };

// 獲取輸入輸出維度信息,返回類型std::vector<int64_t>
inputShape2_1 = session2->GetInputTypeInfo(0).GetTensorTypeAndShapeInfo().GetShape();
inputShape2_2 = session2->GetInputTypeInfo(1).GetTensorTypeAndShapeInfo().GetShape();
outputShape2_1 = session2->GetOutputTypeInfo(0).GetTensorTypeAndShapeInfo().GetShape();
outputShape2_2 = session2->GetOutputTypeInfo(1).GetTensorTypeAndShapeInfo().GetShape();

...

// 創(chuàng)建輸入tensor
auto memoryInfo = Ort::MemoryInfo::CreateCpu(OrtAllocatorType::OrtArenaAllocator, OrtMemType::OrtMemTypeDefault);
std::vector<Ort::Value> inputTensors;
inputTensors.emplace_back(Ort::Value::CreateTensor<float>(memoryInfo,
	blob1.ptr<float>(), blob1.total(), inputShape2_1.data(), inputShape2_1.size()));
inputTensors.emplace_back(Ort::Value::CreateTensor<float>(memoryInfo,
	blob2.ptr<float>(), blob2.total(), inputShape2_2.data(), inputShape2_2.size()));
	
// 推理
auto outputTensors = session2->Run(Ort::RunOptions{ nullptr },
	intputNames2.data(), inputTensors.data(), inputCount2, outputNames2.data(), outputCount2);

// 獲取輸出
float* preds1 = outputTensors[0].GetTensorMutableData<float>();
float* preds2 = outputTensors[1].GetTensorMutableData<float>();

5. TensorRT部署GoogLeNet

TRT推理有兩種常見的方式:

  1. 通過官方安裝包里邊的提供的trtexec.exe工具,從onnx文件轉(zhuǎn)換得到trt文件,然后執(zhí)行推理;
  2. 由onnx文件轉(zhuǎn)化得到engine文件,再執(zhí)行推理。

兩種方式原理一樣,這里我們只介紹第二種方式。推理過程可分為兩階段:使用onnx構(gòu)建推理engine和加載engine執(zhí)行推理。

5.1 構(gòu)建推理引擎(engine文件)?

engine的構(gòu)建是TensorRT推理至關(guān)重要的一步,它特定于所構(gòu)建的確切GPU模型,不能跨平臺或TensorRT版本移植。舉個簡單的例子,如果你在RTX3060上使用TensorRT 8.2.5構(gòu)建了engine,那么推理部署也必須要在RTX3060上進行,且要具備TensorRT 8.2.5環(huán)境。engine構(gòu)建的大致流程如下:

【模型部署 01】C++實現(xiàn)GoogLeNet在OpenCV DNN、ONNXRuntime、TensorRT、OpenVINO上的推理部署

engine的構(gòu)建有很多種方式,這里我們介紹常用的三種。我一般會選擇直接在Python中構(gòu)建,這樣模型的訓(xùn)練、轉(zhuǎn)onnx、轉(zhuǎn)engine都在Python端完成,方便且省事。

方法一:在Python中構(gòu)建

import os
import sys
import logging
import argparse
import tensorrt as trt

os.environ['CUDA_VISIBLE_DEVICES'] = '0'
# 延遲加載模式,cuda11.7新功能,設(shè)置為LAZY有可能會極大的降低內(nèi)存和顯存的占用
os.environ['CUDA_MODULE_LOADING'] = 'LAZY'

logging.basicConfig(level=logging.INFO)
logging.getLogger("EngineBuilder").setLevel(logging.INFO)
log = logging.getLogger("EngineBuilder")


class EngineBuilder:
    """
    Parses an ONNX graph and builds a TensorRT engine from it.
    """

    def __init__(self, batch_size=1, verbose=False, workspace=8):
        """
        :param verbose: If enabled, a higher verbosity level will be set on the TensorRT logger.
        :param workspace: Max memory workspace to allow, in Gb.
        """
        # 1. 構(gòu)建builder
        self.trt_logger = trt.Logger(trt.Logger.INFO)
        if verbose:
            self.trt_logger.min_severity = trt.Logger.Severity.VERBOSE

        trt.init_libnvinfer_plugins(self.trt_logger, namespace="")

        self.builder = trt.Builder(self.trt_logger)
        self.config = self.builder.create_builder_config()  # 構(gòu)造builder.config
        self.config.max_workspace_size = workspace * (2 ** 30)  # workspace分配

        self.batch_size = batch_size
        self.network = None
        self.parser = None

    def create_network(self, onnx_path):
        """
        Parse the ONNX graph and create the corresponding TensorRT network definition.
        :param onnx_path: The path to the ONNX graph to load.
        """
        network_flags = (1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))

        self.network = self.builder.create_network(network_flags)
        self.parser = trt.OnnxParser(self.network, self.trt_logger)

        onnx_path = os.path.realpath(onnx_path)
        with open(onnx_path, "rb") as f:
            if not self.parser.parse(f.read()):
                log.error("Failed to load ONNX file: {}".format(onnx_path))
                for error in range(self.parser.num_errors):
                    log.error(self.parser.get_error(error))
                sys.exit(1)

        # 獲取網(wǎng)絡(luò)輸入輸出
        inputs = [self.network.get_input(i) for i in range(self.network.num_inputs)]
        outputs = [self.network.get_output(i) for i in range(self.network.num_outputs)]

        log.info("Network Description")
        for input in inputs:
            self.batch_size = input.shape[0]
            log.info("Input '{}' with shape {} and dtype {}".format(input.name, input.shape, input.dtype))
        for output in outputs:
            log.info("Output '{}' with shape {} and dtype {}".format(output.name, output.shape, output.dtype))
        assert self.batch_size > 0
        self.builder.max_batch_size = self.batch_size

    def create_engine(self, engine_path, precision):
        """
        Build the TensorRT engine and serialize it to disk.
        :param engine_path: The path where to serialize the engine to.
        :param precision: The datatype to use for the engine, either 'fp32', 'fp16' or 'int8'.
        """
        engine_path = os.path.realpath(engine_path)
        engine_dir = os.path.dirname(engine_path)
        os.makedirs(engine_dir, exist_ok=True)
        log.info("Building {} Engine in {}".format(precision, engine_path))

        inputs = [self.network.get_input(i) for i in range(self.network.num_inputs)]

        if precision == "fp16":
            if not self.builder.platform_has_fast_fp16:
                log.warning("FP16 is not supported natively on this platform/device")
            else:
                self.config.set_flag(trt.BuilderFlag.FP16)

        with self.builder.build_engine(self.network, self.config) as engine, open(engine_path, "wb") as f:
            log.info("Serializing engine to file: {:}".format(engine_path))
            f.write(engine.serialize())


def main(args):
    builder = EngineBuilder(args.batch_size, args.verbose, args.workspace)
    builder.create_network(args.onnx)
    builder.create_engine(args.engine, args.precision)


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("-o", "--onnx", default=r'googlenet-pretrained_batch8.onnx', help="The input ONNX model file to load")
    parser.add_argument("-e", "--engine", default=r'googlenet-pretrained_batch8_from_py_3080_FP16.engine', help="The output path for the TRT engine")
    parser.add_argument("-p", "--precision", default="fp16", choices=["fp32", "fp16", "int8"],
                        help="The precision mode to build in, either 'fp32', 'fp16' or 'int8', default: 'fp16'")
    parser.add_argument("-b", "--batch_size", default=8, type=int, help="batch number of input")
    parser.add_argument("-v", "--verbose", action="store_true", help="Enable more verbose log output")
    parser.add_argument("-w", "--workspace", default=8, type=int, help="The max memory workspace size to allow in Gb, "
                                                                       "default: 8")

    args = parser.parse_args()
    main(args)

生成fp16模型:參數(shù)precision設(shè)置為fp16即可。int8模型生成過程比較復(fù)雜,且對模型精度影響較大,用的不多,這里暫不介紹。

parser.add_argument("-p", "--precision", default="fp16", choices=["fp32", "fp16", "int8"],
                        help="The precision mode to build in, either 'fp32', 'fp16' or 'int8', default: 'fp16'")

方法二:在C++中構(gòu)建

#include "NvInfer.h"
#include "NvOnnxParser.h"
#include "cuda_runtime_api.h"
#include "logging.h"
#include <fstream>
#include <map>
#include <chrono>
#include <cmath>
#include <opencv2/opencv.hpp>
#include <fstream>

using namespace nvinfer1;
using namespace nvonnxparser;
using namespace std;
using namespace cv;

std::string onnxPath = "E:/inference-master/models/engine/googlenet-pretrained_batch.onnx";
std::string enginePath = "E:/inference-master/models/engine/googlenet-pretrained_batch_from_cpp.engine";  // 通過C++構(gòu)建

static const int INPUT_H = 224;
static const int INPUT_W = 224;
static const int OUTPUT_SIZE = 1000;

static const int BATCH_SIZE = 25;

const char* INPUT_BLOB_NAME = "input";
const char* OUTPUT_BLOB_NAME = "output";

static Logger gLogger;

// onnx轉(zhuǎn)engine
void onnx_to_engine(std::string onnx_file_path, std::string engine_file_path, int type) {

    // 創(chuàng)建builder實例,獲取cuda內(nèi)核目錄以獲取最快的實現(xiàn),用于創(chuàng)建config、network、engine的其他對象的核心類
    nvinfer1::IBuilder* builder = nvinfer1::createInferBuilder(gLogger);
    const auto explicitBatch = 1U << static_cast<uint32_t>(nvinfer1::NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);

    // 創(chuàng)建網(wǎng)絡(luò)定義
    nvinfer1::INetworkDefinition* network = builder->createNetworkV2(explicitBatch);

    // 創(chuàng)建onnx解析器來填充網(wǎng)絡(luò)
    nvonnxparser::IParser* parser = nvonnxparser::createParser(*network, gLogger);

    // 讀取onnx模型文件
    parser->parseFromFile(onnx_file_path.c_str(), 2);
    for (int i = 0; i < parser->getNbErrors(); ++i) {
        std::cout << "load error: " << parser->getError(i)->desc() << std::endl;
    }
    printf("tensorRT load mask onnx model successfully!!!...\n");

    // 創(chuàng)建生成器配置對象
    nvinfer1::IBuilderConfig* config = builder->createBuilderConfig();
   
    builder->setMaxBatchSize(BATCH_SIZE);           // 設(shè)置最大batch 
    config->setMaxWorkspaceSize(16 * (1 << 20));    // 設(shè)置最大工作空間大小

    // 設(shè)置模型輸出精度,0代表FP32,1代表FP16
    if (type == 1) {
        config->setFlag(nvinfer1::BuilderFlag::kFP16);
    }

    // 創(chuàng)建推理引擎
    nvinfer1::ICudaEngine* engine = builder->buildEngineWithConfig(*network, *config);

    // 將推理引擎保存到本地
    std::cout << "try to save engine file now~~~" << std::endl;
    std::ofstream file_ptr(engine_file_path, std::ios::binary);
    if (!file_ptr) {
        std::cerr << "could not open plan output file" << std::endl;
        return;
    }
    // 將模型轉(zhuǎn)化為文件流數(shù)據(jù)
    nvinfer1::IHostMemory* model_stream = engine->serialize();
    // 將文件保存到本地
    file_ptr.write(reinterpret_cast<const char*>(model_stream->data()), model_stream->size());
    // 銷毀創(chuàng)建的對象
    model_stream->destroy();
    engine->destroy();
    network->destroy();
    parser->destroy();
    std::cout << "convert onnx model to TensorRT engine model successfully!" << std::endl;
}

int main(int argc, char** argv)
{
    // onnx轉(zhuǎn)engine
    onnx_to_engine(onnxPath, enginePath, 0);
    
    return 0;
}

方法三:使用官方安裝包bin目錄下的trtexec.exe工具構(gòu)建

trtexec.exe --onnx=googlenet-pretrained_batch.onnx --saveEngine=googlenet-pretrained_batch_from_trt_trt853.engine --shapes=input:25x3x224x224

fp16模型:在后邊加--fp16即可

trtexec.exe --onnx=googlenet-pretrained_batch.onnx --saveEngine=googlenet-pretrained_batch_from_trt_trt853.engine --shapes=input:25x3x224x224 --fp16 

5.2 讀取engine文件并部署模型

【模型部署 01】C++實現(xiàn)GoogLeNet在OpenCV DNN、ONNXRuntime、TensorRT、OpenVINO上的推理部署

推理代碼:

#include "NvInfer.h"
#include "NvOnnxParser.h"
#include "cuda_runtime_api.h"
#include "logging.h"
#include <fstream>
#include <map>
#include <chrono>
#include <cmath>
#include <opencv2/opencv.hpp>

#include "cuda.h"
#include "assert.h"
#include "iostream"

using namespace nvinfer1;
using namespace nvonnxparser;
using namespace std;
using namespace cv;

#define CHECK(status) \
    do\
    {\
        auto ret = (status);\
        if (ret != 0)\
        {\
            std::cerr << "Cuda failure: " << ret << std::endl;\
            abort();\
        }\
    } while (0)

std::string enginePath = "E:/inference-master/models/GoogLeNet/googlenet-pretrained_batch1_from_py_3080_FP32.engine";
std::string imagePath = "E:/inference-master/images/catdog";
std::string classNamesPath = "E:/inference-master/imagenet-classes.txt";			// 標簽名稱列表(類名)

std::vector<std::string> classNameList;		// 標簽名列表

static const int INPUT_H = 224;
static const int INPUT_W = 224;
static const int CHANNEL = 3;
static const int OUTPUT_SIZE = 1000;

static const int BATCH_SIZE = 1;

const char* INPUT_BLOB_NAME = "input";
const char* OUTPUT_BLOB_NAME = "output";

static Logger gLogger;

IRuntime* runtime;
ICudaEngine* engine;
IExecutionContext* context;
void* gpu_buffers[2];
cudaStream_t stream;
const int inputIndex = 0;
const int outputIndex = 1;

// 提前申請內(nèi)存,可節(jié)省推理時間
static float mydata[BATCH_SIZE * CHANNEL * INPUT_H * INPUT_W];
static float prob[BATCH_SIZE * OUTPUT_SIZE];

// 逐行求softmax
int softmax(const cv::Mat & src, cv::Mat & dst)
{
    float max = 0.0;
    float sum = 0.0;

    cv::Mat tmpdst = cv::Mat::zeros(src.size(), src.type());

    std::vector<cv::Mat> srcRows;
    // 逐行求softmax
    for (size_t i = 0; i < src.rows; i++)
    {
        cv::Mat tmpRow;
        cv::Mat dataRow = src.row(i).clone();
        max = *std::max_element(dataRow.begin<float>(), dataRow.end<float>());
        cv::exp((dataRow - max), tmpRow);
        sum = cv::sum(tmpRow)[0];
        tmpRow /= sum;

        srcRows.push_back(tmpRow);
        cv::vconcat(srcRows, tmpdst);
    }

    dst = tmpdst.clone();
    return 0;
}

// onnx轉(zhuǎn)engine
void onnx_to_engine(std::string onnx_file_path, std::string engine_file_path, int type) {

    // 創(chuàng)建builder實例,獲取cuda內(nèi)核目錄以獲取最快的實現(xiàn),用于創(chuàng)建config、network、engine的其他對象的核心類
    nvinfer1::IBuilder* builder = nvinfer1::createInferBuilder(gLogger);
    const auto explicitBatch = 1U << static_cast<uint32_t>(nvinfer1::NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);

    // 創(chuàng)建網(wǎng)絡(luò)定義
    nvinfer1::INetworkDefinition* network = builder->createNetworkV2(explicitBatch);

    // 創(chuàng)建onnx解析器來填充網(wǎng)絡(luò)
    nvonnxparser::IParser* parser = nvonnxparser::createParser(*network, gLogger);

    // 讀取onnx模型文件
    parser->parseFromFile(onnx_file_path.c_str(), 2);
    for (int i = 0; i < parser->getNbErrors(); ++i) {
        std::cout << "load error: " << parser->getError(i)->desc() << std::endl;
    }
    printf("tensorRT load mask onnx model successfully!!!...\n");

    // 創(chuàng)建生成器配置對象
    nvinfer1::IBuilderConfig* config = builder->createBuilderConfig();
   
    builder->setMaxBatchSize(BATCH_SIZE);           // 設(shè)置最大batch 
    config->setMaxWorkspaceSize(16 * (1 << 20));    // 設(shè)置最大工作空間大小

    // 設(shè)置模型輸出精度
    if (type == 1) {
        config->setFlag(nvinfer1::BuilderFlag::kFP16);
    }
    if (type == 2) {
        config->setFlag(nvinfer1::BuilderFlag::kINT8);
    }
    // 創(chuàng)建推理引擎
    nvinfer1::ICudaEngine* engine = builder->buildEngineWithConfig(*network, *config);

    // 將推理引擎保存到本地
    std::cout << "try to save engine file now~~~" << std::endl;
    std::ofstream file_ptr(engine_file_path, std::ios::binary);
    if (!file_ptr) {
        std::cerr << "could not open plan output file" << std::endl;
        return;
    }
    // 將模型轉(zhuǎn)化為文件流數(shù)據(jù)
    nvinfer1::IHostMemory* model_stream = engine->serialize();
    // 將文件保存到本地
    file_ptr.write(reinterpret_cast<const char*>(model_stream->data()), model_stream->size());
    // 銷毀創(chuàng)建的對象
    model_stream->destroy();
    engine->destroy();
    network->destroy();
    parser->destroy();
    std::cout << "convert onnx model to TensorRT engine model successfully!" << std::endl;
}

// 模型推理:包括創(chuàng)建GPU顯存緩沖區(qū)、配置模型輸入及模型推理
void doInference(IExecutionContext& context, const void* input, float* output, int batchSize)
{
    //auto start = chrono::high_resolution_clock::now();

    // DMA input batch data to device, infer on the batch asynchronously, and DMA output back to host
    CHECK(cudaMemcpyAsync(gpu_buffers[inputIndex], input, batchSize * 3 * INPUT_H * INPUT_W * sizeof(float), cudaMemcpyHostToDevice, stream));

    // context.enqueue(batchSize, buffers, stream, nullptr);
    context.enqueueV2(gpu_buffers, stream, nullptr);

    //auto end1 = std::chrono::high_resolution_clock::now();
    //auto ms1 = std::chrono::duration_cast<std::chrono::microseconds>(end1 - start);
    //std::cout << "推理: " << (ms1 / 1000.0).count() << "ms" << std::endl;

    CHECK(cudaMemcpyAsync(output, gpu_buffers[outputIndex], batchSize * OUTPUT_SIZE * sizeof(float), cudaMemcpyDeviceToHost, stream));
    //size_t dest_pitch = 0;
    //CHECK(cudaMemcpy2D(output, dest_pitch, buffers[outputIndex], batchSize * sizeof(float), batchSize, OUTPUT_SIZE, cudaMemcpyDeviceToHost));
    cudaStreamSynchronize(stream);

    
    //auto end2 = std::chrono::high_resolution_clock::now();
    //auto ms2 = std::chrono::duration_cast<std::chrono::microseconds>(end2 - start)/100.0;
    //std::cout << "cuda-host: " << (ms2 / 1000.0).count() << "ms" << std::endl;

}

// 結(jié)束推理,釋放資源
void GpuMemoryRelease()
{
    // Release stream and buffers
    cudaStreamDestroy(stream);
    CHECK(cudaFree(gpu_buffers[0]));
    CHECK(cudaFree(gpu_buffers[1]));

    // Destroy the engine
    context->destroy();
    engine->destroy();
    runtime->destroy();
}

// GoogLeNet模型初始化
void ModelInit(std::string enginePath, int deviceId)
{
    // 設(shè)置GPU
    cudaSetDevice(deviceId);

    // 從本地讀取engine模型文件
    char* trtModelStream{ nullptr };
    size_t size{ 0 };
    std::ifstream file(enginePath, std::ios::binary);
    if (file.good()) {
        file.seekg(0, file.end);    // 將讀指針從文件末尾開始移動0個字節(jié)
        size = file.tellg();    // 返回讀指針的位置,此時讀指針的位置就是文件的字節(jié)數(shù)
        file.seekg(0, file.beg);    // 將讀指針從文件開頭開始移動0個字節(jié)
        trtModelStream = new char[size];
        assert(trtModelStream);
        file.read(trtModelStream, size);
        file.close();
    }

    // 創(chuàng)建推理運行環(huán)境實例
    runtime = createInferRuntime(gLogger);
    assert(runtime != nullptr);
    // 反序列化模型
    engine = runtime->deserializeCudaEngine(trtModelStream, size, nullptr);
    assert(engine != nullptr);
    // 創(chuàng)建推理上下文
    context = engine->createExecutionContext();
    assert(context != nullptr);
    delete[] trtModelStream;

    // Create stream
    CHECK(cudaStreamCreate(&stream));

    // Pointers to input and output device buffers to pass to engine.
    // Engine requires exactly IEngine::getNbBindings() number of buffers.
    assert(engine.getNbBindings() == 2);

    // In order to bind the buffers, we need to know the names of the input and output tensors.
    // Note that indices are guaranteed to be less than IEngine::getNbBindings()
    const int inputIndex = engine->getBindingIndex(INPUT_BLOB_NAME);
    const int outputIndex = engine->getBindingIndex(OUTPUT_BLOB_NAME);

    // Create GPU buffers on device
    CHECK(cudaMalloc(&gpu_buffers[inputIndex], BATCH_SIZE * 3 * INPUT_H * INPUT_W * sizeof(float)));
    CHECK(cudaMalloc(&gpu_buffers[outputIndex], BATCH_SIZE * OUTPUT_SIZE * sizeof(float)));

    // 讀取標簽名稱
    ifstream fin(classNamesPath.c_str());
    string strLine;
    classNameList.clear();
    while (getline(fin, strLine))
        classNameList.push_back(strLine);
    fin.close();
}

// 單圖推理
bool ModelInference(cv::Mat srcImage, std::string& className, float& confidence)
{
    auto start = chrono::high_resolution_clock::now();

    cv::Mat image = srcImage.clone();

    // 預(yù)處理(尺寸變換、通道變換、歸一化)
    cv::cvtColor(image, image, cv::COLOR_BGR2RGB);
    cv::resize(image, image, cv::Size(224, 224));
    image.convertTo(image, CV_32FC3, 1.0 / 255.0);
    cv::Scalar mean(0.485, 0.456, 0.406);
    cv::Scalar std(0.229, 0.224, 0.225);
    cv::subtract(image, mean, image);
    cv::divide(image, std, image);

    // cv::Mat blob = cv::dnn::blobFromImage(image);

    // 下邊代碼比上邊blobFromImages速度更快
    for (int r = 0; r < INPUT_H; r++)
    {
        float* rowData = image.ptr<float>(r);
        for (int c = 0; c < INPUT_W; c++)
        {
            mydata[0 * INPUT_H * INPUT_W + r * INPUT_W + c] = rowData[CHANNEL * c];
            mydata[1 * INPUT_H * INPUT_W + r * INPUT_W + c] = rowData[CHANNEL * c + 1];
            mydata[2 * INPUT_H * INPUT_W + r * INPUT_W + c] = rowData[CHANNEL * c + 2];
        }
    }

    // 模型推理
    // doInference(*context, blob.data, prob, BATCH_SIZE);
    doInference(*context, mydata, prob, BATCH_SIZE);

    // 推理結(jié)果后處理
    cv::Mat preds = cv::Mat(BATCH_SIZE, OUTPUT_SIZE, CV_32FC1, (float*)prob);
 
    softmax(preds, preds);

    Point minLoc, maxLoc;
    double minValue = 0, maxValue = 0;
    cv::minMaxLoc(preds, &minValue, &maxValue, &minLoc, &maxLoc);
    int labelIndex = maxLoc.x;
    double probability = maxValue;

    className = classNameList[labelIndex];
    confidence = probability;

    std::cout << "class:" << className << endl << "confidence:" << confidence << endl;
    auto end = chrono::high_resolution_clock::now();
    auto ms = chrono::duration_cast<std::chrono::microseconds>(end - start);
    std::cout << "Inference time by TensorRT:" << (ms / 1000.0).count() << "ms" << std::endl;
   
    return 0;
}

// GoogLeNet模型推理
bool ModelInference_Batch(std::vector<cv::Mat> srcImages, std::vector<std::string>& classNames, std::vector<float>& confidences)
{
    auto start = std::chrono::high_resolution_clock::now();

    if (srcImages.size() != BATCH_SIZE) return false;

    // 預(yù)處理(尺寸變換、通道變換、歸一化)
    std::vector<cv::Mat> images;
    for (size_t i = 0; i < srcImages.size(); i++)
    {
        cv::Mat image = srcImages[i].clone();
        cv::cvtColor(image, image, cv::COLOR_BGR2RGB);
        cv::resize(image, image, cv::Size(224, 224));
        image.convertTo(image, CV_32FC3, 1.0 / 255.0);
        cv::Scalar mean(0.485, 0.456, 0.406);
        cv::Scalar std(0.229, 0.224, 0.225);
        cv::subtract(image, mean, image);
        cv::divide(image, std, image);

        images.push_back(image);
    }

    // 圖像轉(zhuǎn)blob格式
    // cv::Mat blob = cv::dnn::blobFromImages(images);

    // 下邊代碼比上邊blobFromImages速度更快
    for (int b = 0; b < BATCH_SIZE; b++)
    { 
        cv::Mat image = images[b];
        for (int r = 0; r < INPUT_H; r++)
        {
            float* rowData = image.ptr<float>(r);
            for (int c = 0; c < INPUT_W; c++)
            {
                mydata[b * CHANNEL * INPUT_H * INPUT_W + 0 * INPUT_H * INPUT_W + r * INPUT_W + c] = rowData[CHANNEL * c];
                mydata[b * CHANNEL * INPUT_H * INPUT_W + 1 * INPUT_H * INPUT_W + r * INPUT_W + c] = rowData[CHANNEL * c + 1];
                mydata[b * CHANNEL * INPUT_H * INPUT_W + 2 * INPUT_H * INPUT_W + r * INPUT_W + c] = rowData[CHANNEL * c + 2];
            }   
        }
    }
     
    auto end1 = std::chrono::high_resolution_clock::now();
    auto ms1 = std::chrono::duration_cast<std::chrono::microseconds>(end1 - start);
    std::cout << "PreProcess time: " << (ms1 / 1000.0).count() << "ms" << std::endl;

    // 執(zhí)行推理
    doInference(*context, mydata, prob, BATCH_SIZE);

    auto end2 = std::chrono::high_resolution_clock::now();
    auto ms2 = std::chrono::duration_cast<std::chrono::microseconds>(end2 - end1);
    std::cout << "Inference time: " << (ms2 / 1000.0).count() << "ms" << std::endl;

    // 推理結(jié)果后處理
    cv::Mat result = cv::Mat(BATCH_SIZE, OUTPUT_SIZE, CV_32FC1, (float*)prob);
    softmax(result, result);

    for (int r = 0; r < BATCH_SIZE; r++)
    {
        cv::Mat scores = result.row(r).clone();

        cv::Point minLoc, maxLoc;
        double minValue = 0, maxValue = 0;
        cv::minMaxLoc(scores, &minValue, &maxValue, &minLoc, &maxLoc);
        int labelIndex = maxLoc.x;
        double probability = maxValue;

        classNames.push_back(classNameList[labelIndex]);
        confidences.push_back(probability);
    }

    auto end3 = std::chrono::high_resolution_clock::now();
    auto ms3 = std::chrono::duration_cast<std::chrono::microseconds>(end3 - end2);
    std::cout << "PostProcess time: " << (ms3 / 1000.0).count() << "ms" << std::endl;

    auto ms = std::chrono::duration_cast<std::chrono::microseconds>(end3 - start);
    std::cout << "TensorRT batch" << BATCH_SIZE << " 推理時間:" << (ms / 1000.0).count() << "ms" << std::endl;

    return true;
}

int main(int argc, char** argv)
{
    // onnx轉(zhuǎn)engine
    // onnx_to_engine(onnxPath, enginePath, 0);

    // 模型初始化
    ModelInit(enginePath, 0);

    // 讀取圖像
    vector<string> filenames;
    cv::glob(imagePath, filenames);

    // 單圖推理測試
    for (int n = 0; n < filenames.size(); n++)
    {
        // 重復(fù)100次,計算平均時間
        auto start = chrono::high_resolution_clock::now();
        cv::Mat src = imread(filenames[n]);
        std::string className;
        float confidence;
        for (int i = 0; i < 101; i++) {
            if (i == 1)
                start = chrono::high_resolution_clock::now();
            ModelInference(src, className, confidence);
        }
        auto end = chrono::high_resolution_clock::now();
        auto ms = chrono::duration_cast<std::chrono::microseconds>(end - start) / 100;
        std::cout << "TensorRT 平均推理時間:---------------------" << (ms / 1000.0).count() << "ms" << std::endl;
    }

    // 批量(動態(tài)batch)推理測試
    std::vector<cv::Mat> srcImages;
    int okNum = 0, ngNum = 0;
    for (int n = 0; n < filenames.size(); n++)
    {
        cv::Mat image = imread(filenames[n]);
        srcImages.push_back(image);
        if ((n + 1) % BATCH_SIZE == 0 || n == filenames.size() - 1)
        {
            // 重復(fù)100次,計算平均時間
            auto start = chrono::high_resolution_clock::now();
            for (int i = 0; i < 101; i++) {
                if (i == 1)
                    start = chrono::high_resolution_clock::now();
                std::vector<std::string> classNames;
                std::vector<float> confidences;
                ModelInference_Batch(srcImages, classNames, confidences);

                for (int j = 0; j < classNames.size(); j++)
                {
                    if (classNames[j] == "0")
                        okNum++;
                    else
                        ngNum++;
                }

            }
            srcImages.clear();
            auto end = chrono::high_resolution_clock::now();
            auto ms = chrono::duration_cast<std::chrono::microseconds>(end - start) / 100;
            std::cout << "TensorRT " << BATCH_SIZE << " 平均推理時間:---------------------" << (ms / 1000.0).count() << "ms" << std::endl;
        }        
    }

    GpuMemoryRelease();

    std::cout << "all_num = " << filenames.size() << endl << "okNum = " << okNum << endl << "ngNum = " << ngNum << endl;

    return 0;
} 

5.3 fp32、fp16模型對比測試

fp16模型推理結(jié)果幾乎和fp32一致,但是卻較大的節(jié)約了顯存和內(nèi)存占用,同時推理速度也有明顯的提升。

6. OpenVINO部署GoogLeNet

6.1 推理過程及代碼

【模型部署 01】C++實現(xiàn)GoogLeNet在OpenCV DNN、ONNXRuntime、TensorRT、OpenVINO上的推理部署

代碼:

/* 推理過程
* 1. Create OpenVINO-Runtime Core
* 2. Compile Model
* 3. Create Inference Request
* 4. Set Inputs
* 5. Start Inference
* 6. Process inference Results
*/

#include <opencv2/opencv.hpp>
#include <openvino/openvino.hpp>
#include <inference_engine.hpp>
#include <chrono>
#include <fstream>

using namespace std;
using namespace InferenceEngine;
using namespace cv;

std::string onnxPath = "E:/inference-master/models/GoogLeNet/googlenet-pretrained_batch1.onnx";
std::string imagePath = "E:/inference-master/images/catdog";
std::string classNamesPath = "E:/inference-master/imagenet-classes.txt";			// 標簽名稱列表(類名)

ov::InferRequest inferRequest;
std::vector<std::string> classNameList;		// 標簽名,可以從文件讀取
int batchSize = 1;

// softmax,輸入輸出為數(shù)組
std::vector<float> softmax(std::vector<float> input)
{
	float total = 0;
	for (auto x : input)
		total += exp(x);
	std::vector<float> result;
	for (auto x : input)
		result.push_back(exp(x) / total);

	return result;
}

// softmax,輸入輸出為Mat
int softmax(const cv::Mat& src, cv::Mat& dst)
{
	float max = 0.0;
	float sum = 0.0;

	max = *max_element(src.begin<float>(), src.end<float>());
	cv::exp((src - max), dst);
	sum = cv::sum(dst)[0];
	dst /= sum;

	return 0;
}

// 模型初始化
void ModelInit(string onnxPath)
{
	// Step 1: 創(chuàng)建一個Core對象
	ov::Core core;

	// 打印當前設(shè)備
	std::vector<std::string> availableDevices = core.get_available_devices();
	for (int i = 0; i < availableDevices.size(); i++)
		printf("supported device name: %s\n", availableDevices[i].c_str());

	// Step 2: 讀取模型
	std::shared_ptr<ov::Model> model = core.read_model(onnxPath);

	// Step 3: 加載模型到CPU
	ov::CompiledModel compiled_model = core.compile_model(model, "CPU");

	// 設(shè)置推理實例并發(fā)數(shù)為5個
	//core.set_property("CPU", ov::streams::num(10));
	// 設(shè)置推理實例數(shù)為自動分配
	//core.set_property("CPU", ov::streams::num(ov::streams::AUTO));
	// 推理實例數(shù)按計算資源平均分配
	//core.set_property("CPU", ov::streams::num(ov::streams::NUMA));

	// 設(shè)置推理實例的線程并發(fā)數(shù)為10
	// core.set_property("CPU", ov::inference_num_threads(20));

	// Step 4: 創(chuàng)建推理請求
	inferRequest = compiled_model.create_infer_request();

	// 讀取標簽名稱
	ifstream fin(classNamesPath.c_str());
	string strLine;
	classNameList.clear();
	while (getline(fin, strLine))
		classNameList.push_back(strLine);
	fin.close();
}

// 單圖推理
void ModelInference(cv::Mat srcImage, std::string& className, float& confidence )
{
	auto start = chrono::high_resolution_clock::now();

	// Step 5: 將輸入數(shù)據(jù)填充到輸入tensor
	// 通過索引獲取輸入tensor
	ov::Tensor input_tensor = inferRequest.get_input_tensor(0);
	// 通過名稱獲取輸入tensor
	// ov::Tensor input_tensor = infer_request.get_tensor("input");

	// 預(yù)處理
	cv::Mat image = srcImage.clone();
	cv::cvtColor(image, image, cv::COLOR_BGR2RGB);
	resize(image, image, Size(224, 224));
	image.convertTo(image, CV_32FC3, 1.0 / 255.0);
	Scalar mean(0.485, 0.456, 0.406);
	Scalar std(0.229, 0.224, 0.225);
	subtract(image, mean, image);
	divide(image, std, image);

	// HWC -> NCHW
	ov::Shape tensor_shape = input_tensor.get_shape();
	const size_t channels = tensor_shape[1];
	const size_t height = tensor_shape[2];
	const size_t width = tensor_shape[3];
	float* image_data = input_tensor.data<float>();
	for (size_t r = 0; r < height; r++) {
		for (size_t c = 0; c < width * channels; c++) {
			int w = (r * width * channels + c) / channels;
			int mod = (r * width * channels + c) % channels;  // 0,1,2
			image_data[mod * width * height + w] = image.at<float>(r, c);
		}
	}

	// --------------- Step 6: Start inference ---------------
	inferRequest.infer();

	// --------------- Step 7: Process the inference results ---------------
	// model has only one output
	auto output_tensor = inferRequest.get_output_tensor();
	float* detection = (float*)output_tensor.data();
	ov::Shape out_shape = output_tensor.get_shape();
	int batch = output_tensor.get_shape()[0];
	int num_classes = output_tensor.get_shape()[1];

	cv::Mat result(batch, num_classes, CV_32F, detection);

	softmax(result, result);

	Point minLoc, maxLoc;
	double minValue = 0, maxValue = 0;
	cv::minMaxLoc(result, &minValue, &maxValue, &minLoc, &maxLoc);
	int labelIndex = maxLoc.x;
	double probability = maxValue;

	auto end = chrono::high_resolution_clock::now();
	auto ms = chrono::duration_cast<std::chrono::milliseconds>(end - start);
	std::cout << "openvino單張推理時間:" << ms.count() << "ms" << std::endl;
}

// 多圖并行推理(動態(tài)batch)
void ModelInference_Batch(std::vector<cv::Mat> srcImages, std::vector<string>& classNames, std::vector<float>& confidences)
{
	auto start = chrono::high_resolution_clock::now();

	// Step 5: 將輸入數(shù)據(jù)填充到輸入tensor
	// 通過索引獲取輸入tensor
	ov::Tensor input_tensor = inferRequest.get_input_tensor(0);
	// 通過名稱獲取輸入tensor
	// ov::Tensor input_tensor = infer_request.get_tensor("input");

	// 預(yù)處理(尺寸變換、通道變換、歸一化)
	std::vector<cv::Mat> images;
	for (size_t i = 0; i < srcImages.size(); i++)
	{
		cv::Mat image = srcImages[i].clone();
		cv::cvtColor(image, image, cv::COLOR_BGR2RGB);
		cv::resize(image, image, cv::Size(224, 224));
		image.convertTo(image, CV_32FC3, 1.0 / 255.0);
		cv::Scalar mean(0.485, 0.456, 0.406);
		cv::Scalar std(0.229, 0.224, 0.225);
		cv::subtract(image, mean, image);
		cv::divide(image, std, image);

		images.push_back(image);
	}

	ov::Shape tensor_shape = input_tensor.get_shape();
	const size_t batch = tensor_shape[0];
	const size_t channels = tensor_shape[1];
	const size_t height = tensor_shape[2];
	const size_t width = tensor_shape[3];
	float* image_data = input_tensor.data<float>();

	// 圖像轉(zhuǎn)blob格式(速度比下邊像素操作方式更快)
	cv::Mat blob = cv::dnn::blobFromImages(images);
	memcpy(image_data, blob.data, batch * 3 * height * width * sizeof(float));

	// NHWC -> NCHW
	//for (size_t b = 0; b < batch; b++){
	//	for (size_t r = 0; r < height; r++) {
	//		for (size_t c = 0; c < width * channels; c++) {
	//			int w = (r * width * channels + c) / channels;
	//			int mod = (r * width * channels + c) % channels;  // 0,1,2
	//			image_data[b * 3 * width * height + mod * width * height + w] = images[b].at<float>(r, c);
	//		}
	//	}
	//}

	auto end1 = std::chrono::high_resolution_clock::now();
	auto ms1 = std::chrono::duration_cast<std::chrono::microseconds>(end1 - start);
	std::cout << "PreProcess time: " << (ms1 / 1000.0).count() << "ms" << std::endl;
	
	// --------------- Step 6: Start inference ---------------
	inferRequest.infer();

	auto end2 = std::chrono::high_resolution_clock::now();
	auto ms2 = std::chrono::duration_cast<std::chrono::microseconds>(end2 - end1)/100;
	std::cout << "Inference time: " << (ms2 / 1000.0).count() << "ms" << std::endl;

	// --------------- Step 7: Process the inference results ---------------
	// model has only one output
	auto output_tensor = inferRequest.get_output_tensor();
	float* detection = (float*)output_tensor.data();
	ov::Shape out_shape = output_tensor.get_shape();
	int num_classes = output_tensor.get_shape()[1];

	cv::Mat output(batch, num_classes, CV_32F, detection);

	int rows = output.size[0];	// batch
	int cols = output.size[1];	// 類別數(shù)(每一個類別的得分)
	for (int row = 0; row < rows; row++)
	{
		cv::Mat scores(1, cols, CV_32FC1, output.ptr<float>(row));
		softmax(scores, scores);	// 結(jié)果歸一化

		Point minLoc, maxLoc;
		double minValue = 0, maxValue = 0;
		cv::minMaxLoc(scores, &minValue, &maxValue, &minLoc, &maxLoc);
		int labelIndex = maxLoc.x;
		double probability = maxValue;

		classNames.push_back(classNameList[labelIndex]);
		confidences.push_back(probability);
	}

	auto end3 = std::chrono::high_resolution_clock::now();
	auto ms3 = std::chrono::duration_cast<std::chrono::microseconds>(end3 - end2);
	std::cout << "PostProcess time: " << (ms3 / 1000.0).count() << "ms" << std::endl;

	auto ms = chrono::duration_cast<std::chrono::milliseconds>(end3 - start);
	std::cout << "openvino單張推理時間:" << ms.count() << "ms" << std::endl;
}

int main(int argc, char** argv)
{
	// 模型初始化
	ModelInit(onnxPath);

	// 讀取圖像
	vector<string> filenames;
	glob(imagePath, filenames);

	// 單圖推理測試
	for (int n = 0; n < filenames.size(); n++)
	{
		// 重復(fù)100次,計算平均時間
		auto start = chrono::high_resolution_clock::now();
		for (int i = 0; i < 101; i++) {
			if (i == 1)
				start = chrono::high_resolution_clock::now();
			cv::Mat src = imread(filenames[n]);
			std::string className;
			float confidence;
			ModelInference(src, className, confidence);
		}
		auto end = chrono::high_resolution_clock::now();
		auto ms = chrono::duration_cast<std::chrono::microseconds>(end - start) / 100.0;
		std::cout << "opencv_dnn 單圖平均推理時間:---------------------" << (ms / 1000.0).count() << "ms" << std::endl;
	}

	std::vector<cv::Mat> srcImages;
	for (int i = 0; i < filenames.size(); i++)
	{
		cv::Mat image = imread(filenames[i]);
		srcImages.push_back(image);
		if ((i + 1) % batchSize == 0 || i == filenames.size() - 1)
		{
			// 重復(fù)100次,計算平均時間
			auto start = chrono::high_resolution_clock::now();
			for (int i = 0; i < 101; i++) {
				if (i == 1)
					start = chrono::high_resolution_clock::now();
				std::vector<std::string> classNames;
				std::vector<float> confidences;
				ModelInference_Batch(srcImages, classNames, confidences);
			}
			srcImages.clear();
			auto end = chrono::high_resolution_clock::now();
			auto ms = chrono::duration_cast<std::chrono::microseconds>(end - start) / 100;
			std::cout << "openvino batch" << batchSize << " 平均推理時間:---------------------" << (ms / 1000.0).count() << "ms" << std::endl;
		}
	}
	return 0;
}

注意:OV支持多圖并行推理,但是要求轉(zhuǎn)出onnx的時候batch就要使用固定數(shù)值。動態(tài)batch(即batch=-1)的onnx文件會報錯。

6.2 遇到的問題

理論:OpenVINO是基于CPU推理最佳的方式。

實測:在測試OpenVINO的過程中,我們發(fā)現(xiàn)OpenVINO推理對于CPU的利用率遠沒有OpenCV DNN和ONNXRuntime高,這也是隨著batch數(shù)量增加,OV在CPU上的推理速度反而不如DNN和ORT的主要原因。嘗試過網(wǎng)上的多種優(yōu)化方式,比如設(shè)置線程數(shù)并發(fā)數(shù)等等,未取得任何改善。如下圖,在OpenVINO推理過程中,始終只有一半的CPU處于活躍狀態(tài);而OnnxRuntime或者OpenCV DNN推理時,所有的CPU均處于活躍狀態(tài)。

【模型部署 01】C++實現(xiàn)GoogLeNet在OpenCV DNN、ONNXRuntime、TensorRT、OpenVINO上的推理部署?【模型部署 01】C++實現(xiàn)GoogLeNet在OpenCV DNN、ONNXRuntime、TensorRT、OpenVINO上的推理部署

7. 四種推理方式對比測試

深度學習領(lǐng)域常用的基于CPU/GPU的推理方式有OpenCV DNN、ONNXRuntime、TensorRT以及OpenVINO。這幾種方式的推理過程可以統(tǒng)一用下圖來概述。整體可分為模型初始化部分和推理部分,后者包括步驟2-5。

【模型部署 01】C++實現(xiàn)GoogLeNet在OpenCV DNN、ONNXRuntime、TensorRT、OpenVINO上的推理部署

以GoogLeNet模型為例,測得幾種推理方式在推理部分的耗時如下:

【模型部署 01】C++實現(xiàn)GoogLeNet在OpenCV DNN、ONNXRuntime、TensorRT、OpenVINO上的推理部署

基于CPU推理:

【模型部署 01】C++實現(xiàn)GoogLeNet在OpenCV DNN、ONNXRuntime、TensorRT、OpenVINO上的推理部署

基于GPU推理:

【模型部署 01】C++實現(xiàn)GoogLeNet在OpenCV DNN、ONNXRuntime、TensorRT、OpenVINO上的推理部署

不論采用何種推理方式,同一網(wǎng)絡(luò)的前處理和后處理過程基本都是一致的。所以,為了更直觀的對比幾種推理方式的速度,我們拋去前后處理,只統(tǒng)計圖中實際推理部分,即3、4、5這三個過程的執(zhí)行時間。

同樣是GoogLeNet網(wǎng)絡(luò),步驟3-5的執(zhí)行時間對比如下:

【模型部署 01】C++實現(xiàn)GoogLeNet在OpenCV DNN、ONNXRuntime、TensorRT、OpenVINO上的推理部署

注:OpenVINO-CPU測試中始終只使用了一半數(shù)量的內(nèi)核,各種優(yōu)化設(shè)置都沒有改善。

最終結(jié)論:

  1. GPU加速首選TensorRT;
  2. CPU加速,單圖推理首選OpenVINO,多圖并行推理可選擇ONNXRuntime;
  3. 如果需要兼具CPU和GPU推理功能,可選擇ONNXRuntime。

參考資料

1. openvino2022版安裝配置與C++SDK開發(fā)詳解

2.?https://github.com/NVIDIA/TensorRT

3.?https://github.com/wang-xinyu/tensorrtx

4. 【TensorRT】TensorRT 部署Yolov5模型(C++)文章來源地址http://www.zghlxwxcb.cn/news/detail-460632.html

到了這里,關(guān)于【模型部署 01】C++實現(xiàn)GoogLeNet在OpenCV DNN、ONNXRuntime、TensorRT、OpenVINO上的推理部署的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔相關(guān)法律責任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費用

相關(guān)文章

  • OnnxRuntime TensorRT OpenCV::DNN性能對比(YoloV8)實測

    OnnxRuntime TensorRT OpenCV::DNN性能對比(YoloV8)實測

    之前把ORT的一套推理環(huán)境框架搭好了,在項目中也運行得非常愉快,實現(xiàn)了cpu/gpu,fp32/fp16的推理運算,同onnx通用模型在不同推理框架下的性能差異對比貼一下,記錄一下自己對各種推理框架的學習狀況 YoloV8模型大小 模型名稱 參數(shù)量 NANO 3.2M ... ... CPU推理框架性能比較 框架 推理耗時

    2024年02月14日
    瀏覽(32)
  • OpenCV DNN C++ 使用 YOLO 模型推理

    YOLO(You Only Look Once)是一種流行的目標檢測算法,因其速度快和準確度高而被廣泛應(yīng)用。OpenCV 的 DNN(Deep Neural Networks)模塊為我們提供了一個簡單易用的 API,用于加載和運行預(yù)先訓(xùn)練的深度學習模型。本文將詳細介紹如何使用 OpenCV 的 DNN 模塊來進行 YOLOv5 的目標檢測。 確保

    2024年02月08日
    瀏覽(17)
  • yolov8 opencv dnn部署自己的模型

    yolov8 opencv dnn部署自己的模型

    源碼地址 本人使用的opencv c++ github代碼,代碼作者非本人 使用github源碼結(jié)合自己導(dǎo)出的onnx模型推理自己的視頻 推理條件 windows 10 Visual Studio 2019 Nvidia GeForce GTX 1070 opencv4.7.0 (opencv4.5.5在別的地方看到不支持yolov8的推理,所以只使用opencv4.7.0) 導(dǎo)出yolov8模型 yolov8版本: version = ‘8.

    2024年01月24日
    瀏覽(55)
  • yolov5 opencv dnn部署自己的模型

    yolov5 opencv dnn部署自己的模型

    github開源代碼地址 yolov5官網(wǎng)還提供的dnn、tensorrt推理鏈接 本人使用的opencv c++ github代碼,代碼作者非本人,也是上面作者推薦的鏈接之一 如果想要嘗試直接運行源碼中的yolo.cpp文件和yolov5s.pt推理sample.mp4,請參考這個鏈接的介紹 使用github源碼結(jié)合自己導(dǎo)出的onnx模型推理自己的

    2024年01月23日
    瀏覽(48)
  • C++使用onnxruntime/opencv對onnx模型進行推理(附代碼)

    C++使用onnxruntime/opencv對onnx模型進行推理(附代碼)

    結(jié)果: current image classification : French bulldog, possible : 16.17 對兩張圖片同時進行推理 current image classification : French bulldog, possible : 16.17 current image class ification : hare, possible : 8.47 https://download.csdn.net/download/qq_44747572/87810859 https://blog.csdn.net/qq_44747572/article/details/131631153

    2024年02月05日
    瀏覽(28)
  • YOLOv5 實例分割 用 OPenCV DNN C++ 部署

    YOLOv5 實例分割 用 OPenCV DNN C++ 部署

    如果之前從沒接觸過實例分割,建議先了解一下實例分割的輸出是什么。 實例分割兩個關(guān)鍵輸出是:mask系數(shù)、mask原型 本文參考自該項目(這么優(yōu)秀的代碼當然要給star!):GitHub - UNeedCryDear/yolov5-seg-opencv-onnxruntime-cpp: yolov5 segmentation with onnxruntime and opencv 目錄 Pre: 一、代碼總結(jié)

    2024年02月12日
    瀏覽(26)
  • 使用opencv-dnn+C++部署onnx肺區(qū)分割模型

    使用opencv-dnn+C++部署onnx肺區(qū)分割模型

    1.環(huán)境: windows + vscdoe + opencv3.4.15 2.流程: ①通過python將訓(xùn)練得到的模型轉(zhuǎn)化為onnx。 ②通過C++調(diào)用onnx模型實現(xiàn)推理。 3.代碼: ① python代碼 ResUnet.py export.py ② C++代碼: 4.結(jié)果: 5.文件下載路徑: 鏈接:https://pan.baidu.com/s/1DDweuwcpSubLotU79c-jFw 提取碼:ZDWD 注:剛接觸深度學習完

    2024年02月13日
    瀏覽(21)
  • VS c++ onnxruntime 環(huán)境配置、onnx教程、部署推理模型、sklearn pkl模型轉(zhuǎn)onnx、問題匯總

    VS c++ onnxruntime 環(huán)境配置、onnx教程、部署推理模型、sklearn pkl模型轉(zhuǎn)onnx、問題匯總

    目錄 一、初步認識ONNX 二、pkl轉(zhuǎn)ONNX+可視化模型 三、ONNX Runtime運行時 3.1 相關(guān)介紹(了解此運行時): 3.2 VS、c++部署onnxruntime 3.3 頭文件引用的一些問題 四、問題匯總: 1. 類沒有成員 2. 版本兼容問題 3. 3.“GetInputName“: 不是 “Ort::Session“ 的成員 官網(wǎng): ONNX Runtime | Home GitHub

    2024年04月09日
    瀏覽(32)
  • YOLOV5-LITE實時目標檢測(onnxruntime部署+opencv獲取攝像頭+NCNN部署)python版本和C++版本

    使用yolov5-lite自帶的export.py導(dǎo)出onnx格式,圖像大小設(shè)置320,batch 1 之后可以使用 onnxsim對模型進一步簡化 onnxsim參考鏈接:onnxsim-讓導(dǎo)出的onnx模型更精簡_alex1801的博客-CSDN博客 這個版本的推理FPS能有11+FPS 這兩處換成自己的模型和訓(xùn)練的類別即可: ??? parser.add_argument(\\\'--modelpa

    2024年02月04日
    瀏覽(30)
  • 【OpenCV DNN】Flask 視頻監(jiān)控目標檢測教程 01

    【OpenCV DNN】Flask 視頻監(jiān)控目標檢測教程 01

    歡迎關(guān)注『OpenCV DNN @ Youcans』系列,持續(xù)更新中 【OpenCV DNN】Flask 視頻監(jiān)控目標檢測教程 01 本系列從零開始,詳細講解使用 Flask 框架構(gòu)建 OpenCV DNN 模型的 Web 應(yīng)用程序。 將OpenCV DNN模型部署到Web端,不需要安裝任何依賴,只需要訪問Web地址就可以訪問和運行應(yīng)用程序。 面向P

    2024年02月07日
    瀏覽(26)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包