1.1 項目概述
1.1.1 項目介紹
C#調(diào)用OpenVINO工具套件部署Al模型項目開發(fā)項目,簡稱OpenVinoSharp,這是一個示例項目,該項目實現(xiàn)在C#編程語言下調(diào)用Intel推出的 OpenVINO 工具套件,進行深度學習等Al項目在C#框架下的部署。該項目由C++語言編寫OpenVINO dll庫,并在C#語言下實現(xiàn)調(diào)用。
項目可以實現(xiàn)在C#編程語言下調(diào)用Intel推出的 OpenVINO 工具套件,進行深度學習等Al項目在C#框架下的部署,目前可以支持的Al模型格式:
■ Paddlepaddle 飛槳模型 (.pdmodel)
■ ONNX 開放式神經(jīng)網(wǎng)絡交換模型 (.onnx)
■ IR 模型 (.xml, .bin)
目前該項目針對 Paddlepaddle 飛槳現(xiàn)有模型進行了測試,主要有:
■ PaddleClas 飛槳圖像識別套件
1.1.2 OpenVINO
OpenVINO 工具套件是英特爾基于自身現(xiàn)有的硬件平臺開發(fā)的一種可以加快高性能計算機視覺和深度學習視覺應用開發(fā)速度工具套件,支持各種英特爾平臺的硬件加速器上進行深度學習,并且允許直接異構執(zhí)行。支持在Windows與Linux系統(tǒng),官方支持編程語言為Python與C++語言,但不直接支持C#。
OpenVINO 工具套件2022.1版于2022年3月22日正式發(fā)布,根據(jù)官宣《OpenVINO 迎來迄今為止最重大更新,2022.1新特性搶先看》,OpenVINO 2022.1將是迄今為止最大變化的版本。從開發(fā)者的角度來看,對于提升開發(fā)效率或運行效率有用的特性有:
■ 提供預處理API函數(shù)
■ ONNX前端API
■ AUTO設備插件
■ 支持直接讀入飛槳模型
該項目開發(fā)環(huán)境為OpenVINO 2022.1最新版本,因此使用者需在使用時將自己電腦上的OpenVINO 版本升級到2022.1版,不然會有較多的問題。
1.1.3 項目方案
該項目主要通過調(diào)用dll文件方式實現(xiàn)。通過C++調(diào)用OpenVINO ,編寫模型推理接口,將我們所用到的推理方法在C++中實現(xiàn),并將其生成dll文件,在C#調(diào)用dll文件,重寫dll文件接口,并重新組建Core類,用于在C#中進行模型的推理,其方案如圖1- 1所示。
圖1- 1 項目解決方案
1.1.4 安裝方式
該項目所有文件已經(jīng)上傳到Github和Gitee遠程代碼倉,大家可以通過Gi在本地進行克隆。
● 系統(tǒng)平臺:
Windows
● 軟件要求:
Visual Studio 2022 / 2019 / 2017
OpenCV 4.5.5
OpenVINO 2022.1
● 安裝方式
在Github上克隆下載:
git clone https://github.com/guojin-yan/OpenVinoSharp.git
在Gitee上克隆下載:
git clone https://gitee.com/guojin-yan/OpenVinoSharp.git
1.2 軟件安裝
1.2.1 Microsoft Visual Studio 2022安裝
Microsoft Visual Studio(簡稱VS)是美國微軟公司的開發(fā)工具套件系列產(chǎn)品。VS是一個基本完整的開發(fā)工具集,它包括了整個軟件生命周期中所需要的大部分工具,如UML工具、代碼管控工具、集成開發(fā)環(huán)境(IDE)等等。其支持C、C++、C#、F#、J#等多門編程語言。
本次項目所使用的編程語言為C++與C#兩門編程語言,在VS中完全可以實現(xiàn),可選擇安裝版本VS2017、VS2019或VS2022版本。對于VS不同版本的選擇,該項目不做較多要求,就筆者使用來說,VS2017版本推出時間較久,不建議使用,其一些編程語言規(guī)范有一些變動,對于該項目所提供的范例可能會有部分不兼容;VS2019和VS2022版本相對更新,是由起來差異不大,建議選擇這兩個版本,并且新版OpenVINO 支持VS2022版本Cmake。
筆者電腦安裝的為Microsoft Visual Studio Community 2022 版本,其安裝包可由VS官網(wǎng)直接下載,下載時選擇社區(qū)版,按照一般安裝步驟進行安裝即可。在安裝中,工作負荷的選擇圖1- 2所示。
圖1- 2 Visual Studio 2022安裝負荷
安裝完成后,可以參照網(wǎng)上相關教程,進行學習VS的使用。
1.2.2 OpenVINO 安裝
該項目所使用的OpenVINO 版本為2022.1版本,是Intel公司在2022年第一季度發(fā)布的最新版本。該版本基于之前版本有了較大變動,不在默認包含OpenCV工具;其次,對代碼做了更進一步的優(yōu)化,使得代碼在使用時更加靈活。其具體安裝方式,參考
https://www.intel.com/content/www/us/en/developer/tools/openvino-toolkit/download.html
1.2.3 OpenCV安裝
由于最新版的OpenVINO 2022.1 版本不在默認附帶OpenCV工具,所以我們需要額外安裝OpenCV工具。
01
下載并安裝OpenCV
訪問OpenCV
圖1- 3 OpenCV-4.5.5 版本頁面
根據(jù)負載使用情況,選擇Windows版本,如圖1- 3所示,跳轉(zhuǎn)頁面后,下載文件名為:opencv-4.5.5-vc14_vc15.exe。下載完成后,直接雙擊打開安裝文件,安裝完成后,打開安裝文件夾,該文件夾下 build、sources文件夾以及LICENSE相關文件,我們所使用的文件在build文件夾中。
02
配置Path環(huán)境變量
右擊我的電腦,進入屬性設置,選擇高級系統(tǒng)設置進入系統(tǒng)屬性,點擊環(huán)境變量,進入到環(huán)境變量設置,編輯系統(tǒng)變量下的Path變量,增加以下地址變量:
E:OpenCV Sourceopencv-4.5.5uildx64vc15in
E:OpenCV Sourceopencv-4.5.5uildx64vc15lib
E:OpenCV Sourceopencv-4.5.5uildinclude
E:OpenCV Sourceopencv-4.5.5uildincludeopencv2
其中
1.3 OpenVINO 推理模型
與測試數(shù)據(jù)集
1.3.1 模型種類與下載方式
為了測試該項目,我們提供并整合了訓練好的 Paddlepaddle 模型,主要針對 PaddleClas 以及 PaddleDetection 現(xiàn)有的模型,提供了 PaddleClas 下的花卉分類模型以及 PaddleDetection 中的 Vehicle Detection 模型,并針對該模型,提供了pdmodel、onnx以及IR格式。
該項目所使用的測試模型以及數(shù)據(jù)集,均可以在本文下的gitee上下載,下載鏈接為:
https://gitee.com/guojin-yan/OpenVinoSharp
1.3.2 PaddleDetection 模型
PaddleDetection 為飛槳 PaddlePaddle 的端到端目標檢測套件,提供多種主流目標檢測、實例分割、跟蹤、關鍵點檢測算法,配置化的網(wǎng)絡模塊組件、數(shù)據(jù)增強策略、損失函數(shù)等。該項目在此處主要使用的為PaddleDetection應用中的目標檢測功能,使用的網(wǎng)絡為YOLOv3網(wǎng)絡,表1- 1 給出了YOLOv3網(wǎng)絡輸出與輸入的相關信息。
表1- 1 YOLOv3模型輸入與輸出節(jié)點信息
注:None表示batch維度,H、W 分別為圖片的高和寬,Num表示識別結果的數(shù)量。
本次測試使用的為 PaddleDetection 中 Vehicle Detection 模型,我們可以在 PaddleDetection gitee 上下載。該模型輸入圖片要求為3×608×608大小,輸出為預測框信息,其信息組成為[class_id, score, x1, y1, x2, y2],分別代表分類編號、分類得分以及預測框?qū)琼旤c坐標。
1.3.3 PaddleClas 模型
飛槳圖像識別套件 PaddleClas 是飛槳為工業(yè)界和學術界提供的的一個圖像識別任務的工具集,該模型經(jīng)過數(shù)據(jù)集訓練,可以識別多種物品。在該項目中,我們使用flower數(shù)據(jù)集,使用ResNet50網(wǎng)絡訓練識別102種花卉,關于該模型的輸入與輸出節(jié)點信息如所示
表1- 2 ResNet50模型輸入與輸出節(jié)點信息
注:None表示batch維度,H、W 分別為圖片的高和寬,Class表示分類數(shù)量。
花卉訓練模型要求圖片輸入為3×224×224大小,輸出結果為102中預測結果概率。
1.4 創(chuàng)建OpenVINO 方法
C++動態(tài)鏈接庫
1.4.1 新建解決方案以及項目文件
打開vs2022,首先新建一個C++空項目文件,并將同時新建一個解決方案命名為:OpenVinoSharp,用于存放后續(xù)其他項目文件。將C++項目命名為:CppOpenVinoAPI。
進入項目后,右擊源文件,選擇添加→新建項→C++文件(cpp),進行的文件的添加。具體操作如圖1- 4所示。
圖1- 4 新建項目解決方案及C++項目
本次我們需要添加OpenVinoAPIcpp以及Source.def兩個文件,如圖1- 5所示。
圖1- 5 CppOpenVinoAPIl函數(shù)方法所需文件
1.4.2 配置C++項目屬性
右擊項目,點擊屬性,進入到屬性設置,此處需要設置項目的配置類型包含目錄、庫目錄以及附加依賴項,本次項目選擇Release模式下運行,因此以Release情況進行配置。
01
(1)設置配置與平臺
進入屬性設置后,在最上面,將配置改為Release,平臺改為x64。具體操作如圖1- 6所示。
圖1- 6 C++項目屬性配置與平臺設置
02
設置常規(guī)屬性
常規(guī)設置下,點擊輸出目錄,將輸出位置設置為,即將生成文件放置在項目文件夾下的dll文件夾下;其次將目標文件名修改為:OpenVinoSharp;最后將配置類型改為:動態(tài)庫(.dll),讓其生成dll文件。具體操作如圖1- 7所示。
圖1- 7 C++項目常規(guī)屬性設置
03
設置包含目錄
點擊VC++目錄,然后點擊包含目錄,進行編輯,在彈出的新頁面中,添加以下路徑:
E:OpenCV Sourceopencv-4.5.5uildinclude
E:OpenCV Sourceopencv-4.5.5uildincludeopencv2
C:Program Files (x86)Intelopenvino_2022.1.0.643 untimeinclude
C:Program Files (x86)Intelopenvino_2022.1.0.643 untimeincludeie
其中路徑
圖1- 8 C++項目屬性庫目錄設置
04
設置庫目錄
同樣的方式,在VC++目錄下,點擊下方的庫目錄,點擊編輯,在彈出來的頁面中增加以下路徑:
E:OpenCV Sourceopencv-4.5.5uildx64vc15lib
C:Program Files (x86)Intelopenvino_2022.1.0.643 untimelibintel64Release
如果時配置Debug模式,則需要將Release文件路徑改為Debug即可。
05
設置附加依賴項
點擊展開鏈接器,點擊輸入,在附加依賴項中點擊編輯,在彈出來的新的頁面,添加以下文件名:
opencv_world455.lib
openvino.lib
具體操作步驟參考如圖1-9所示。新版OpenCV與OpenVINO 都將依賴庫文件合成到了一個文件中,這極大地簡化了使用,如果使用老版本的,需要將所有的.lib文件放置在此處即可。
圖1- 9 C++項目屬性附加依賴項設置
1.4.3 編寫C++代碼
01
推理引擎結構體
Core是OpenVINO 工具套件里的推理核心類,該類下包含多個方法,可用于創(chuàng)建推理中所使用的其他類。在此處,需要在各個方法中傳遞的僅僅是所使用的幾個變量,因此選擇構建一個推理引擎結構體,用于存放各個變量。
// @brief 推理核心結構體
typedef struct openvino_core {
ov::Core core; // core對象
std::shared_ptr model_ptr; // 讀取模型指針
ov::CompiledModel compiled_model; // 模型加載到設備對象
ov::InferRequest infer_request; // 推理請求對象
} CoreStruct;
其中Core是OpenVINO 工具套件里的推理機核心,該模塊只需要初始化;shared_ptr
02
接口方法規(guī)劃
經(jīng)典的OpenVINO 進行模型推理,一般需要八個步驟,主要是:初始化Core對象、讀取本地推理模型、配置模型輸入&輸出、載入模型到執(zhí)行硬件、創(chuàng)建推理請求、準備輸入數(shù)據(jù)、執(zhí)行推理計算以及處理推理計算結果。我們根據(jù)原有的八個步驟,對步驟進行重新整合,并根據(jù)推理步驟,調(diào)整方法接口。
對于方法接口,主要設置為:推理初始化、配置輸入數(shù)據(jù)形狀、配置輸入數(shù)據(jù)、模型推理、讀取推理結果數(shù)據(jù)以及刪除內(nèi)存地址六個大類,其中配置輸入數(shù)據(jù)形狀要細分為配置圖片數(shù)據(jù)形狀以及普通數(shù)據(jù)形狀,配置輸入數(shù)據(jù)要細分為配置圖片輸入數(shù)據(jù)與配置普通數(shù)據(jù)輸入,讀取推理結果數(shù)據(jù)細分為讀取float數(shù)據(jù)和int數(shù)據(jù),因此,總共有6類方法接口,9個方法接口。
03
初始化推理模型
OpenVINO 推理引擎結構體是聯(lián)系各個方法的橋梁,后續(xù)所有操作都是在推理引擎結構體中的變量上操作的,為了實現(xiàn)數(shù)據(jù)在各個方法之間的傳輸,因此在創(chuàng)建推理引擎結構體時,采用的是創(chuàng)建結構體指針,并將創(chuàng)建的結構體地址作為函數(shù)返回值返回。推理初始化接口主要整合了原有推理的初始化Core對象、讀取本地推理模型、載入模型到執(zhí)行硬件和創(chuàng)建推理請求步驟,并將這些步驟所創(chuàng)建的變量放在推理引擎結構體中。
初始化推理模型接口方法為:
extern "C" __declspec(dllexport) void* __stdcall core_init(const wchar_t* model_file_wchar, const wchar_t* device_name_wchar);
該方法返回值為CoreStruct結構體指針,其中model_file_wchar為推理模型本地地址字符串指針,device_name_wchar為模型運行設備名指針,在后面使用上述變量時,需要將其轉(zhuǎn)換為string字符串,利用wchar_to_string()方法可以實現(xiàn)將其轉(zhuǎn)換為字符串格式:
std::string model_file_path = wchar_to_string(model_file_wchar);
std::string device_name = wchar_to_string(device_name_wchar);
模型初始化功能主要包括:初始化推理引擎結構體和對結構體里面定義的其他變量進行賦值操作,其主要是利用InferEngineStruct中創(chuàng)建的Core類中的方法,對各個變量進行初始化操作:
CoreStruct* p = new CoreStruct(); // 創(chuàng)建推理引擎指針
p->model_ptr = p->core.read_model(model_file_path); // 讀取推理模型
p->compiled_model = p->core.compile_model(p->model_ptr, ?CPU“); // 將模型加載到設備
p->infer_request = p->compiled_model.create_infer_request(); // 創(chuàng)建推理請求
04
配置輸入數(shù)據(jù)形狀
在新版OpenVINO 2022.1 中,新增加了對Paddlepaddle 模型以及onnx模型的支持,Paddlepaddle 模型不支持指定指定默認bath通道數(shù)量,因此需要在模型使用時指定其輸入;其次,對于onnx模型,也可以在轉(zhuǎn)化時不指定固定形狀,因此在配置輸入數(shù)據(jù)前,需要配置輸入節(jié)點數(shù)據(jù)形狀。其方法接口為:
extern "C" __declspec(dllexport) void* __stdcall set_input_image_sharp(void* core_ptr, const wchar_t* input_node_name_wchar, size_t * input_size);
extern "C" __declspec(dllexport) void* __stdcall set_input_data_sharp(void* core_ptr, const wchar_t* input_node_name_wchar, size_t * input_size);
由于需要配置圖片數(shù)據(jù)輸入形狀與普通數(shù)據(jù)的輸入形狀,在此處設置了兩個接口,分別設置兩種不同輸入的形狀。該方法返回值是CoreStruct結構體指針,但該指針所對應的數(shù)據(jù)中已經(jīng)包含了對輸入形狀的設置。第一個輸入參數(shù)core_ptr是CoreStruct指針,在當前方法中,我們要讀取該指針,并將其轉(zhuǎn)換為CoreStruct類型:
CoreStruct* p = (CoreStruct*)core_ptr;
input_node_name_wchar 為待設置網(wǎng)絡節(jié)點名,input_size 為形狀數(shù)據(jù)數(shù)組,對圖片數(shù)據(jù),需要設置 [batch, dim, height, width] 四個維度大小,所以input_size數(shù)組傳入4個數(shù)據(jù),其設置在形狀主要使用Tensor類下的set_shape()方法:
std::string input_node_name = wchar_to_string(input_node_name_wchar); // 將節(jié)點名轉(zhuǎn)為string類型
ov::Tensor input_image_tensor = p->infer_request.get_tensor(input_node_name); // 讀取指定節(jié)點Tensor
input_image_tensor.set_shape({ input_size[0],input_size[1],input_size[2],input_size[3] }); // 設置節(jié)點數(shù)據(jù)形狀
05
配置輸入數(shù)據(jù)
在新版OpenVINO 中,Tensor類的T* data()方法,其返回值為當前節(jié)點Tensor的數(shù)據(jù)內(nèi)存地址,通過填充Tensor的數(shù)據(jù)內(nèi)存,實現(xiàn)推理數(shù)據(jù)的輸入。對于圖片數(shù)據(jù),其最終也是將其轉(zhuǎn)為一維數(shù)據(jù)進行輸入,不過為方便使用,此處提供了配置圖片數(shù)據(jù)和普通數(shù)據(jù)的接口,對于輸入為圖片的方法接口:
extern "C" __declspec(dllexport) void* __stdcall load_image_input_data(void* core_ptr, const wchar_t* input_node_name_wchar, uchar * image_data, size_t image_size);
該方法返回值是CoreStruct結構體指針,但該指針所對應的數(shù)據(jù)中已經(jīng)包含了加載的圖片數(shù)據(jù)。第一個輸入?yún)?shù)core_ptr是CoreStruct指針,在當前方法中,我們要讀取該指針,并將其轉(zhuǎn)換為CoreStruct類型;第二個輸入?yún)?shù)input_node_name_wchar為待填充節(jié)點名,先將其轉(zhuǎn)為string字符串:
std::string input_node_name = wchar_to_string(input_node_name_wchar);
在該項目中,我們主要使用的是以圖片作為模型輸入的推理網(wǎng)絡,模型主要的輸入為圖片的輸入。其圖片數(shù)據(jù)主要存儲在矩陣image_data和矩陣長度image_size兩個變量中。需要對圖片數(shù)據(jù)進行整合處理,利用創(chuàng)建的data_to_mat () 方法,將圖片數(shù)據(jù)讀取到OpenCV中:
cv::Mat input_image = data_to_mat(image_data, image_size);
接下來就是配置網(wǎng)絡圖片數(shù)據(jù)輸入,對于節(jié)點輸入是圖片數(shù)據(jù)的網(wǎng)絡節(jié)點,其配置網(wǎng)絡輸入主要分為以下幾步:
首先,獲取網(wǎng)絡輸入圖片大小。
使用InferRequest類中的get_tensor ()方法,獲取指定網(wǎng)絡節(jié)點的Tensor,其節(jié)點要求輸入大小在Shape容器中,通過獲取該容器,得到圖片的長寬信息:
ov::Tensor input_image_tensor = p->infer_request.get_tensor(input_node_name);
int input_H = input_image_tensor.get_shape()[2]; //獲得“image“節(jié)點的Height
int input_W = input_image_tensor.get_shape()[3]; //獲得“image“節(jié)點的Width
其次,按照輸入要求,處理輸入圖片。
在這一步,我們除了要按照輸入大小對圖片進行放縮之外,還要根據(jù) PaddlePaddle 對模型輸入的要求進行處理。因此處理圖片其主要分為交換RGB通道、放縮圖片以及對圖片進行歸一化處理。在此處我們借助OpenCV來實現(xiàn)。
OpenCV讀取圖片數(shù)據(jù)并將其放在Mat類中,其讀取的圖片數(shù)據(jù)是BGR通道格式,PaddlePaddle 要求輸入格式為RGB通道格式,其通道轉(zhuǎn)換主要靠一下方式實現(xiàn):
cv::cvtColor(input_image, blob_image, cv::COLOR_BGR2RGB);
接下來就是根據(jù)網(wǎng)絡輸入要求,對圖片進行壓縮處理:
cv::resize(blob_image, blob_image, cv::Size(input_H, input_W), 0, 0, cv::INTER_LINEAR);
最后就是對圖片進行歸一化處理,其主要處理步驟就是減去圖像數(shù)值均值,并除以方差。查詢PaddlePaddle模型對圖片的處理,其均值mean = [0.485, 0.456, 0.406],方差std = [0.229, 0.224, 0.225],利用OpenCV中現(xiàn)有函數(shù),對數(shù)據(jù)進行歸一化處理:
std::vectormean_values{ 0.485 * 255, 0.456 * 255, 0.406 * 255 };
std::vectorstd_values{ 0.229 * 255, 0.224 * 255, 0.225 * 255 };
std::vectorrgb_channels(3);
cv::split(blob_image, rgb_channels); // 分離圖片數(shù)據(jù)通道
for (auto i = 0; i < rgb_channels.size(); i++){
//分通道依此對每一個通道數(shù)據(jù)進行歸一化處理
rgb_channels[i].convertTo(rgb_channels[i], CV_32FC1, 1.0 / std_values[i], (0.0 – mean_values[i]) / std_values[i]);
}
cv::merge(rgb_channels, blob_image); // 合并圖片數(shù)據(jù)通道
最后,將圖片數(shù)據(jù)輸入到模型中。
在此處,我們重寫了網(wǎng)絡賦值方法,并將其封裝到 fill_tensor_data_image(ov::Tensor& input_tensor, const cv::Mat& input_image)方法中,input_tensor為模型輸入節(jié)點Tensor類,input_image為處理過的圖片Mat數(shù)據(jù)。因此節(jié)點賦值只需要調(diào)用該方法即可:
fill_tensor_data_image(input_image_tensor, blob_image);
對于普通數(shù)據(jù)的輸入,其方法接口如下:
extern "C" __declspec(dllexport) void* __stdcall load_input_data(void* core_ptr, const wchar_t* input_node_name_wchar, float* input_data);
與配置圖片數(shù)據(jù)不同點,在于輸入數(shù)據(jù)只需要輸入input_data數(shù)組即可。其數(shù)據(jù)處理哦在外部實現(xiàn),只需要將處理后的數(shù)據(jù)填充到輸入節(jié)點的數(shù)據(jù)內(nèi)存中即可,通過調(diào)用自定義的fill_tensor_data_float(ov::Tensor& input_tensor, float* input_data, int data_size) 方法即可實現(xiàn):
std::string input_node_name = wchar_to_string(input_node_name_wchar);
ov::Tensor input_image_tensor = p->infer_request.get_tensor(input_node_name); // 讀取指定節(jié)點tensor
int input_size = input_image_tensor.get_shape()[1]; //獲得輸入節(jié)點的長度
fill_tensor_data_float(input_image_tensor,input_data, input_size); // 將數(shù)據(jù)填充到tensor數(shù)據(jù)內(nèi)存上
06
模型推理
上一步中我們將推理內(nèi)容的數(shù)據(jù)輸入到了網(wǎng)絡中,在這一步中,我們需要進行數(shù)據(jù)推理,這一步中我們留有一個推理接口:
extern "C" __declspec(dllexport) void* __stdcall core_infer(void* core_ptr)
進行模型推理,只需要調(diào)用CoreStruct結構體中的infer_request對象中的infer()方法即可:
CoreStruct* p = (CoreStruct*)core_ptr;
p->infer_request.infer();
07
讀取推理數(shù)據(jù)
上一步我們對數(shù)據(jù)進行了推理,這一步就需要查詢上一步推理的結果。對于我們所使用的模型輸出,主要有float數(shù)據(jù)和int數(shù)據(jù),對此,留有了兩種數(shù)據(jù)的查詢接口,其方法為:
extern "C" __declspec(dllexport) void __stdcall read_infer_result_F32(void* core_ptr, const wchar_t* output_node_name_wchar, int data_size, float* infer_result);
extern "C" __declspec(dllexport) void __stdcall read_infer_result_I32(void* core_ptr, const wchar_t* output_node_name_wchar, int data_size, int* infer_result);
其中data_size為讀取數(shù)據(jù)長度,infer_result 為輸出數(shù)組指針。讀取推理結果數(shù)據(jù)與加載推理數(shù)據(jù)方式相似,依舊是讀取輸出節(jié)點處數(shù)據(jù)內(nèi)存的地址:
const ov::Tensor& output_tensor = p->infer_request.get_tensor(output_node_name);
const float* results = output_tensor.data();
針對讀取整形數(shù)據(jù),其方法一樣,只是在轉(zhuǎn)換類型時,需要將其轉(zhuǎn)換為整形數(shù)據(jù)即可。我們讀取的初始數(shù)據(jù)為二進制數(shù)據(jù),因此要根據(jù)指定類型轉(zhuǎn)換,否則數(shù)據(jù)會出現(xiàn)錯誤。將數(shù)據(jù)讀取出來后,將其放在數(shù)據(jù)結果指針中,并將所有結果賦值到輸出數(shù)組中:
for (int i = 0; i < data_size; i++) {
*inference_result = results[i];
inference_result++;
}
08
刪除推理核心結構體指針
推理完成后,我們需要將在內(nèi)存中創(chuàng)建的推理核心結構地址刪除,防止造成內(nèi)存泄露,影響電腦性能,其接口該方法為:
extern "C" __declspec(dllexport) void __stdcall core_delet(void* core_ptr);
在該方法中,我們只需要調(diào)用delete命令,將結構體指針刪除即可。
1.4.4 編寫模塊定義文件
我們在定義接口方法時,在原有方法的基礎上,增加了extern "C" 、 __declspec(dllexport) 以及__stdcall 三個標識,其主要原因是為了讓編譯器識別我們的輸出方法。其中,extern ?C“是指示編譯器這部分代碼按C語言(而不是C++)的方式進行編譯;__declspec(dllexport)用于聲明導出函數(shù)、類、對象等供外面調(diào)用;__stdcall是一種函數(shù)調(diào)用約定。通過上面三個標識,我們在C++種所寫的接口方法,會在dll文件中暴露出來,并且可以實現(xiàn)在C#中的調(diào)用。
不過上面所說內(nèi)容,我們在編輯器中可以通過模塊定義文件(.def)所實現(xiàn),在模塊定義文件中,添加以下代碼:
LIBRARY
"OpenVinoSharp"
EXPORTS
core_init
set_input_image_sharp
set_input_data_sharp
load_image_input_data
load_input_data
core_infer
read_infer_result_F32
read_infer_result_I32
core_delet
LIBRARY后所跟的為輸出文件名,EXPORTS后所跟的為輸出方法名。僅需要以上語句便可以替代extern "C" 、 __declspec(dllexport) 以及__stdcall的使用。
1.4.5 生成dll文件
前面我們將項目配置輸出設置為了生成dll文件,因此該項目不是可以執(zhí)行的exe文件,只能生成不能運行。右鍵項目,選擇重新生成/生成。在沒有錯誤的情況下,會看到項目成功的提示??梢钥吹絛ll文件在解決方案同級目錄下x64Release文件夾下。
使用dll文件查看器打開dll文件,如圖1- 10所示;可以看到,我們創(chuàng)建的四個方法接口已經(jīng)暴露在dll文件中。
圖1- 10 dll文件方法輸出目錄
1.5 C#構建Core類
1.5.1 新建C#類庫
右擊解決方案,添加->新建項目,選擇添加C#類庫,項目名命名為OpenVinoSharp,項目框架根據(jù)電腦中的框架選擇,此處使用的是.NET 5.0。新建完成后,然后右擊項目,選擇添加->新建項,選擇類文件,添加Core.cs和NativeMethods.cs兩個類文件。
1.5.2 引入dll文件中的方法
在NativeMethods.cs文件下,我們通過[DllImport()]方法,將dll文件中所有的方法讀取到C#中。讀取方式如下:
[DllImport(openvino_dll_path, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
public extern static IntPtr core_init(string model_file, string device_name);
其中openvino_dll_path為dll文件路徑,CharSet = CharSet.Unicode代表支持中文編碼格式字符串,CallingConvention = CallingConvention.Cdecl指示入口點的調(diào)用約定為調(diào)用方清理堆棧。
上述所列出的為初始化推理模型,dlii文件接口在匹配時,是通過方法名字匹配的,因此,方法名要保證與dll文件中一致。其次就是方法的參數(shù)類型要進行對應,在上述方法中,函數(shù)的返回值在C++中為void* ,在C#中對應的為IntPtr類型,輸入?yún)?shù)中,在C++中為wchar_t* 字符指針,在C#中對應的為string字符串。通過方法名與參數(shù)類型一一對應,在C#可以實現(xiàn)對方法的調(diào)用。其他方法的引用類似,在此處不在一一贅述,具體可以參照項目提供的源代碼。
1.5.3 創(chuàng)建Core類
為了更方便地調(diào)用我們通過dll引入的OpenVINO 方法,減少使用時的函數(shù)方法接口,我們在C#中重新組建我們自己的推理類,命名為Class Core,其主要成員變量和方法如圖1- 11所示。
圖1- 11 Core類圖
在Core.類中,我們只需要創(chuàng)建一個地址變量,作為Core類的成員變量,用于接收接口函數(shù)返回的推理核心指針,該成員變量我們只需要在當前類下訪問,因此將其設置為私有變量:
private IntPtr ptr = new IntPtr();
接下來,構建類的構造函數(shù),在類的初始化時,我們需要輸入模型地址以及設備類型,通過掉用dll文件中引入的方法,獲取初始化指針,對成員變量進行賦值,實現(xiàn)類的初始化:
public Core(string model_file, string device_name) {
ptr = NativeMethods.core_init(model_file, device_name);
}
然后構其中的方法,在構建設置數(shù)據(jù)輸入形狀時,我們需要提供的為節(jié)點名以及形狀數(shù)據(jù),為了簡化該方法,我們合并了圖片形狀設置與普通數(shù)據(jù)形狀設置接口,通過判斷輸入數(shù)組的長度,來確定是對那一個形狀的設置:
public void set_input_sharp(string input_node_name, ulong[] input_size);
對于該類中方法的構建,可以參考源碼文件,在此處不做詳述。并且其他方法構建方式基本相似,此處不在一一贅述,具體可以參考源碼文檔。
1.5.4 編譯Core類庫
右擊項目,點擊生成/重新生成,出現(xiàn)如下圖1- 12所示,表示編譯成功。
圖1- 12 Core類編譯輸出
1.6 C#實現(xiàn)OpenVINO
方法的調(diào)用
1.6.1 新建C#項目
右擊解決方案,添加->新建項目,選擇添加C#控制臺項目,項目框架根據(jù)電腦中的框架選擇,此處使用的是.NET 5.0。
圖1- 13 C#項目設置
1.6.2 添加OpenCVsharp
右擊項目,選擇管理NuGet程序包,在新頁面中選擇瀏覽,在搜索框中輸入opencvsharp3,在搜索結果中,找到OpenCvSharp3-AnyCPU,然后右側(cè)點擊安裝,具體操作步驟如圖1- 14所示。
圖1- 14 NuGet程序包安裝
1.6.3添加項目引用
上一步中我們將dll文件中的方法引入到C#中,并組建了Core類,在這一步中,我們主要通過調(diào)用Core類,進行Al模型的部署,所以需要引入上一步的項目。
右擊當前項目,選擇添加,選擇項目引用,在出現(xiàn)的窗體中,選擇上一步中創(chuàng)建的項目OpenVinoSharp,點擊確定;然后在當前項目下,添加using OpenVinoSharp命名空間。具體操作如圖1- 15所示。
圖1- 15 添加項目引用
1.6.4 編寫代碼測試花卉分類模型
在該項目中,我們提供了兩種推理模型,此處我們以花卉分類模型為例,簡介如何通過C#調(diào)用OpenVINO 進行Al模型的部署。
01
引入相關變量
string device_name = "CPU";
string model_file = "E:/Text_Model/flowerclas/flower_rec.onnx";
string image_file = "E:/Text_dataset/flowers102/jpg/image_00001.jpg";
string input_node_name = "x";
string output_node_name = "softmax_1.tmp_0";
為了讓大家更加清晰的看懂后續(xù)代碼,在此處對引入的相關變量進行解釋:
device_name:設備類型名稱,可為CPU、GPU以及AUTO(均可);
model_file:模型地址,可以為onnx、pdmodel或者xml格式;
image_file:測試圖片地址;
input_node_name:輸入模型節(jié)點名,當多輸入時,可以為數(shù)組;
output_node_name:輸出模型節(jié)點名,當多輸出時,可以為數(shù)組。
02
初始化Core類
在此處我們直接調(diào)用Core類的構造函數(shù),進行初始化:
Core ie = new Core(model_file_paddle, device_name);
03
配置模型輸入
花卉分類模型輸入只有一個,即待分類花卉圖片。如果我們調(diào)用的模型未指定輸入大小,需要在輸入數(shù)據(jù)前,調(diào)用模型輸入數(shù)據(jù)形狀設置方法,設置節(jié)點輸入數(shù)據(jù)形狀。圖片數(shù)據(jù)為三維數(shù)組,再加一個batchsize,最終為四維數(shù)據(jù),將形狀數(shù)據(jù)放在數(shù)組中,調(diào)用set_input_sharp()方法:
ulong[] image_sharp = new ulong[] { 1, 3, 224, 224 };
ie.set_input_sharp(input_node_name, image_sharp);
對于圖片數(shù)據(jù),需要將其轉(zhuǎn)為轉(zhuǎn)為矩陣數(shù)據(jù),在此處,我們可以直接使用opencvsharp中的編解碼方法,將圖片數(shù)據(jù)放置在byte數(shù)組中:
Mat image = new Mat(image_file);
byte[] image_data = new byte[2048 * 2048 * 3];
ulong image_size = new ulong();
image_data = image.ImEncode(".bmp");
image_size = Convert.ToUInt64(image_data.Length);
就最后調(diào)用Core類中的load _input_data()方法,將數(shù)據(jù)加載到推理網(wǎng)絡中:
ie.load_input_data(input_node_name, image_data, image_size);
在配置完輸入數(shù)據(jù)后,調(diào)用模型推理方法,對輸入數(shù)據(jù)進行推理:
ie.infer();
接下來就是讀取推理結果,對于模型的推理結果輸出一般為數(shù)組數(shù)據(jù),可以通過調(diào)用Core類中讀取推理數(shù)據(jù)結果的方法,對與花卉分類模型的輸出,其結果為長度為102的浮點型數(shù)據(jù),所以直接調(diào)用read_inference_result
float[] result = new float[102];
result = ie.read_infer_result(output_node_name, 102);
在讀取推理數(shù)據(jù)時,我們一定要根據(jù)模型的書名讀取正確的結果數(shù)據(jù),因為如果超出實際輸出長度,其結果數(shù)據(jù)會摻雜其他干擾數(shù)據(jù)。
最后一步就是處理輸出數(shù)據(jù)。對于不同的推理模型,其結果處理方式是不同的,對于花卉分類模型,其輸出為102種分類情況打分,因此,在處理數(shù)據(jù)時,需要找出得分最高的哪一類即可。在此處,我們提供了一個方法,該方法可以實現(xiàn)提取數(shù)組中前N個max數(shù)據(jù)的位置,通過調(diào)用該方法,我們可以獲取分類結果中分數(shù)最高的幾個結果,并將結果打印輸出:
int[] index = find_array_max(result,5);
for (int i = 0; i < 5; i++){
Console.WriteLine("the index is {0} , the score is {1} ", index[i], result[index[i]]);
}
最終輸出結果如圖1- 16所示,該頁面打印出來了推理結果預測分數(shù)最大的前五個分數(shù)和其對應的索引值,最后可以通過索引值查詢flowers102_label_list.txt文件中對應的花卉名稱。
圖1- 16 花卉分類結果
在程序最后,我們該需要將前面在內(nèi)存上創(chuàng)建推理引擎結構體進行刪除,只需要調(diào)用Core類下的delet()即可。
1.6.5 編寫代碼測試車輛識別模型
對于車輛識別模型此處不再進行詳細講解,具體實現(xiàn)可以參考源碼文件,此處只對一些不同點進行分析。
在配置輸入時,除了需要配置圖片數(shù)據(jù)輸入,還需要配置圖片長寬數(shù)據(jù)以及長寬縮放比例數(shù)據(jù),在配置時,只需要將數(shù)據(jù)放置在數(shù)組中,通過調(diào)用load_input_data()方法實現(xiàn),對于設置縮放比例數(shù)據(jù)輸入,如下所示:
float scale_h = 608.00f / image.Height;
float scale_w = 608.00f / image.Width;
float[] scale_factor = new float[] { scale_h, scale_w };
ie.load_input_data(input_node_name[1], scale_factor);
對于該模型推理結果數(shù)據(jù),總共有兩個節(jié)點輸出,一個是識別結果數(shù)量,一個為識別結果信息。對于識別結果數(shù)量,其數(shù)據(jù)類型為整形數(shù)據(jù),對于單圖片輸入,只需要讀取一位即可,利用該數(shù)據(jù),確定識別結果信息長度。識別結果信息為6列N行數(shù)據(jù),在數(shù)據(jù)讀取時,我們將其轉(zhuǎn)化為一維數(shù)據(jù),所以在處理數(shù)據(jù)時,以6位數(shù)據(jù)為一組,進行處理。
識別結果信息數(shù)據(jù)中,第1位為識別標簽,第2位為識別得分,第3位到第6位四個數(shù)據(jù)為位置矩形框?qū)屈c坐標,通過每6位讀取一次數(shù)據(jù),獲取識別結果。在此處,我們提供了專門的結果處理方法,通過該方法們可以實現(xiàn)直接將結果繪制在原圖片上:
image = draw_image_resule(image, resule_num[0], result, lable, 0.2f);
其中image為原圖片,resule_num[0]為識別結果數(shù)量,result為識別結果數(shù)組,lable為結果標簽,0.2f為評價得分下限。通過結果處理,將識別結果標注在圖片中,并把識別結果以及得分情況打印在圖片中,最終識別結果如圖1- 17所示。
圖1- 17 車輛類型識別結果輸出
1.7 程序時間分析
為了對比C++、C#以及Python這三個平臺下調(diào)用OpenVINO 所使用的時間,我們通過測試flower_clas以及vehicle_yolov3_darknet模型運行時間進行對比,在同一臺電腦相同運行環(huán)境之下,以及對模型的處理方式在不同編程語言下盡量做到相同,在程序測試100次之后,得到結果表1- 3如所示。
表1- 3 程序運行時間
在本次檢測中,我們通過C++、C#以及Python分別調(diào)用OpenVINO 進行模型的部署與推理,通過上述表格,一方面可以看出,C#通過調(diào)用C++的dll,實現(xiàn)模型的部署與推理,并沒有太大的影響程序的運行速度;另一方面,C++與C#部署模型推理,在總時間上來看,運行速度是優(yōu)于Python的。
測試模型運行時間所使用的測試代碼,已同步到遠程成代碼托管倉庫gitee與github中,具體在https://gitee.com/guojin-yan/OpenVinoSharp/tree/master/openvino_run_time文件夾下,使用人員可以根據(jù)自己的設備對C++、C#和Python三個平臺進行測試。
1.8 項目總結
該項目通過C++調(diào)用OpenVINO ,創(chuàng)建推理方法接口,并通過調(diào)用dll文件的方式,在C#中進行重新構建Core模型推理類,并測試了花卉分類模型以及車輛識別模型,在預測結果精度以及預測時間上,和C++相比,并沒有較大的差異。
該項目所提供的方法,證實了C#平臺調(diào)用OpenVINO 的可行性,為后續(xù)在C#部署OpenVINO 模型提供了一個技術途徑。本文所有源代碼參見:
https://gitee.com/guojin-yan/OpenVinoSharp
審核編輯 :李倩
-
C++
+關注
關注
21文章
2102瀏覽量
73453 -
深度學習
+關注
關注
73文章
5463瀏覽量
120891
原文標題:在C#中調(diào)用OpenVINO? 模型 | 開發(fā)者實戰(zhàn)
文章出處:【微信號:英特爾物聯(lián)網(wǎng),微信公眾號:英特爾物聯(lián)網(wǎng)】歡迎添加關注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關推薦
評論