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