記事の著者: Yan Guojin、インテル エッジ コンピューティング イノベーション アンバサダー
技術指導: Wu Zhuo、Li Yiwei
OpenVINO™ ツールキットは、ディープ ラーニング ビジョン アプリケーションの開発をスピードアップし、ユーザーがエッジからクラウドまでのさまざまなインテル プラットフォーム上で AI モデルをより便利かつ迅速に実稼働システムに導入できるように支援します。
C# は、C および C++ から派生した安全で安定したシンプルかつエレガントなオブジェクト指向プログラミング言語であり、VB のシンプルな視覚的操作と C++ の高い操作効率を兼ね備えており、.NET 開発言語の第一の選択肢となっています。人工知能開発者として、C# 側で OpenVINO™ を使用したい場合は、OpenVINOSharp が第一の選択肢になります。C# 側でのOpenVINO ™のインストールと使用をワンストップで実現するための NuGet パッケージが作成されました。
OpenVINOSharp GitHub: https://github.com/guojin-yan/OpenVinoSharp
OpenVINOSharp は OpenVINO™ C++ API を参照して作成しているため、これまで OpenVINO™ を使用したことのある人にとっては非常にフレンドリーです。次の表は、C# と C++ API の対応関係を示しています。
クラス |
C++ API |
C# API |
説明する |
コアクラス |
ov::コア |
芯 |
OpenVINO ランタイム コア エンティティ クラス |
モデルクラス |
ov::モデル |
モデル |
ユーザー定義モデルクラス |
CompiledModel クラス |
ov::コンパイル済みモデル |
コンパイルされたモデル |
コンパイルされたモデルクラス |
出力クラス |
ov:: 出力<ov::ノード> |
出力 |
ノード出力のハンドル クラス |
入力クラス |
ov:: 入力 <ov::ノード> |
入力 |
ノード入力のハンドル クラス |
InferRequest クラス |
ov::InferRequest |
ov::InferRequest |
推論リクエストを非同期または同期で実行するクラス |
テンソルクラス |
ov::テンソル |
テンソル |
テンソル |
形状クラス |
ov::シェイプ |
形 |
テンソル形状クラス |
この記事では、モデル展開の一般的な手順に基づいて、モデルの読み込みから推論までのメソッド関数の使用方法を示し、C++ API と比較します。
1.1 OpenVINOSharp をインストールする
OpenVINOSharp は、C++ でのインストール プロセスよりも簡単な NuGet パッケージのインストール方法をサポートしており、パッケージにはOpenVINO™ 2023.0 リリース バージョンが含まれており、NuGet によるインストール後に直接使用できます。
Visual Studio を使用してプロジェクトをコンパイルする場合は、NuGet パッケージ管理機能を通じてプロジェクトを直接インストールできます。
dotnet コマンドを使用してインストールする場合は、次のステートメントを使用してインストールできます。
dotnet add package OpenVinoSharp.win
1.2 アセンブリのインポート
すべての OpenVINOSharp アセンブリは OpenVinoSharp 名前空間の下にあるため、OpenVINOSharp を使用したい場合は、最初に名前空間を導入する必要があります。
using OpenVinoSharp;
1.3 OpenVINOランタイム カーネルを初期化する
Core クラスは、OpenVINO ランタイムのコア エンティティを表します。モデルの読み取りやモデルのロードなどの後続のメソッドは、Core クラスを通じて作成する必要があります。C# API をカプセル化する場合、C++ API に対応するために、Core クラスもカプセル化され、 encapsulated C++ API の対応するメソッド
C# での初期化方法:
Core core = new Core();
C++での初期化方法:
ov::Core core;
1.4 モデル情報をロードして取得する
1.4.1 負荷モデル
OpenVINO™ 2022.1 バージョンが更新されてロードされた後、使用される API メソッドは次のとおりです。
API |
効果 |
Core.read_model() |
モデルをハードディスクからメモリにロードし、Model オブジェクトを返します。 |
C# でモデルをロードする方法:
Model model = core.read_model(model_path);
C++での初期化方法:
std::shared_ptr<ov::Model> model = core.read_model(model_path);
1.4.2モデル情報を取得する
Core.read_model () メソッドで取得した Model オブジェクトと Core.compile_model () メソッドで取得した CompiledModel オブジェクトはどちらも、入力層と出力層の情報を取得するための属性への直接アクセスをサポートしています。モデル情報を取得する Model オブジェクトを例に挙げると、使用される API メソッドは次のとおりです。
API |
効果 |
Model.get_friend_name() |
モデルのフレンドリ名を取得します。 |
Model.input() |
モデルの入力レイヤーを取得し、Input オブジェクトを返します。 |
Model.output() |
モデルの出力層を取得し、Output オブジェクトを返します。 |
入出力は主にモデルのネットワーク層をカプセル化し、次の API を通じてモデルの詳細情報を取得できます。
API |
効果 |
Output.get_any_name() |
モデルのネットワーク層の名前を取得します。 |
Output.get_element_type() |
モデル ネットワーク層のデータ型を取得し、OvType オブジェクトを返します。OvType は主にネットワークの基本データ型をカプセル化します。 |
Output.get_shape() |
モデル ネットワーク層の形状を取得し、Shape オブジェクトを返します。Shape は、ネットワーク層の形状配列をカプセル化します。 |
C# では、次のコードを使用して、モデルの入力、入力レイヤー、およびフレンドリ名を直接取得できます。
string model_name = model.get_friendly_name();
Input input = model.input();
Output output = model.output();
次に、モデル固有の情報をコンソール ページに出力します。
Console.WriteLine("Model name: {0}", model_name);
Console.WriteLine("/------- [In] -------/");
Console.WriteLine("Input name: {0}", input.get_any_name());
Console.WriteLine("Input type: {0}", input.get_element_type().to_string());
Console.WriteLine("Input shape: {0}", input.get_shape().to_string());
Console.WriteLine("/------- [Out] -------/");
Console.WriteLine("Output name: {0}", output.get_any_name());
Console.WriteLine("Output type: {0}", output.get_element_type().to_string());
Console.WriteLine("Output shape: {0}", output.get_shape().to_string());
次のようにモデルのネットワーク層情報を取得します。
Model name: torch_jit
/------- [In] -------/
Input name: data
Input type: float
Input shape: [1,3,224,224]
/------- [Out] -------/
Output name: prob
Output type: float
Output shape: [1,1000]
C++ API を使用して、次のように同じ出力情報を実装します。
std::cout << "Model name: " << model->get_friendly_name() << std::endl;
ov::Output<ov::Node> input = model->input();
std::cout << "/------- [In] -------/" << std::endl;
std::cout << "Input name: " << input.get_any_name() << std::endl;
std::cout << "Input type: " << input.get_element_type().c_type_string() << std::endl;
std::cout << "Input shape: " << input.get_shape().to_string() << std::endl;
ov::Output<ov::Node> output = model->output();
std::cout << "/------- [Out] -------/" << std::endl;
std::cout << "Output name: " << output.get_any_name() << std::endl;
std::cout << "Output type: " << output.get_element_type().c_type_string() << std::endl;
std::cout << "Output shape: " << output.get_shape().to_string() << std::endl;
1.5モデルをコンパイルし、推論リクエストを作成する
ローカル モデルを読み取った後、モデル コンパイル メソッドを呼び出してモデルをターゲット デバイス上で実行できるcompile_model オブジェクトにコンパイルし、このオブジェクトを通じてコンパイルされたモデルを推論するための推論要求オブジェクトを作成します。使用される API メソッドは次のとおりです。
API |
効果 |
Core.compile_model() |
モデルを、ターゲット デバイス上で実行できるcompile_model オブジェクトにコンパイルします。 |
CompiledModel.create_infer_request() |
コンパイルされたモデルを推論するために使用される推論リクエスト オブジェクトを作成します。作成されたリクエストには、入力テンソルと出力テンソルが割り当てられています。 |
C# でモデルをコンパイルし、推論リクエストを作成する方法:
CompiledModel compiled_model = core.compile_model(model, "AUTO");
InferRequest infer_request = compiled_model.create_infer_request();
C++ API を使用してモデルをコンパイルし、推論リクエストを作成します。
CompiledModel compiled_model = core.compile_model(model, "AUTO");
InferRequest infer_request = compiled_model.create_infer_request();
1.6 TensorTensor _
1.6.1 テンソルの取得と設定
推論リクエストを作成した後、システムは入力および出力テンソルを自動的に作成して割り当てます。テンソルは InferRequest オブジェクトを通じて取得でき、テンソルをカスタマイズしてモデルの指定されたノードにロードできます。シーケンス番号と名前は、モデル ノード Node オブジェクトの取得と設定と同様に、主な C# API は次のとおりです。
API |
効果 |
InferRequest.set_tensor() |
推論する入出力テンソルを設定します。 |
InferRequest.set_input_tensor() |
推論する入力テンソルを設定します。 |
InferRequest.set_output_tensor() |
推論する出力テンソルを設定します |
InferRequest.get_tensor() |
推論のために入力/出力テンソルを取得します。 |
InferRequest.get_input_tensor() |
推論に使用される入力テンソルを取得します。 |
InferRequest.get_output_tensor() |
推論に使用される出力テンソルを取得します。 |
1.6.2 テンソル情報の取得と設定
テンソルに含まれる主な情報は、テンソルの形状 (Shape)、テンソルのデータ形式 (OvType-> element.Type)、およびテンソル内のメモリ データです。Tensor パラメーターは、次の API メソッドを通じて操作できます。
API |
効果 |
Tensor.set_shape () |
テンソルに新しい形状を与えます。 |
Tensor.get_shape() |
テンソルの形状を取得します。 |
Tensor.get_element_type() |
テンソルのデータ型を取得します。 |
Tensor.get_size() |
テンソルのデータ長を取得します。 |
Tensor.get_byte_size() |
テンソルのバイト サイズを取得します。 |
Tensor.data() |
テンソルのメモリアドレスを取得します。 |
Tensor.set_data<T>() |
指定された型のデータをテンソル メモリに読み込みます。 |
Tensor.get_data<T>() |
指定された型のデータをテンソルから読み取ります。 |
上記のメソッドはテンソルに対する基本的な操作の一部であり、OpenVINOSharp に固有の set_data と get_data を除き、他のインターフェイスは C++ API と一致しています。
1.7 推論データをロードする
1.7.1 获取输入张量
对于单输入的模型可以直接通过get_input_tensor()方法获得,并调用Tensor的相关方法获取Tensor的相关信息,C# 代码如下所示:
Tensor input_tensor = infer_request.get_input_tensor();
Console.WriteLine("/------- [Input tensor] -------/");
Console.WriteLine("Input tensor type: {0}", input_tensor.get_element_type().to_string());
Console.WriteLine("Input tensor shape: {0}", input_tensor.get_shape().to_string());
Console.WriteLine("Input tensor size: {0}", input_tensor.get_size());
获取输出结果为:
/------- [Input tensor] -------/
Input tensor type: f32
Input tensor shape: Shape : {1, 3, 224, 224}
Input tensor size: 150528
对于上述的同样输出内容,我们也可以通过C++ API 实现,C++ 代码如下:
ov::Tensor input_tensor = infer_request.get_input_tensor();
std::cout << "/------- [Input tensor] -------/" << std::endl;
std::cout << "Input tensor type: " << input_tensor.get_element_type().c_type_string() << std::endl;
std::cout << "Input tensor shape: " << input_tensor.get_shape().to_string() << std::endl;
std::cout << "Input tensor size: " << input_tensor.get_size() << std::endl;
1.7.2 添加推理数据
这一步主要是将处理好的图片数据加载到Tensor数据内存中,OpenVINO的API中提供了访问内存地址的接口,可以获取数据内存首地址,不过为了更好的加载推理数据,我们此处封装了set_data<T>()方法,可以实现将处理后的图片数据加载到数据内存上。在C#中的代码为:
Mat input_mat = new Mat();
Shape input_shape = input_tensor.get_shape();
long channels = input_shape[1];
long height = input_shape[2];
long width = input_shape[3];
float[] input_data = new float[channels * height * width];
Marshal.Copy(input_mat.Ptr(0), input_data, 0, input_data.Length);
input_tensor.set_data(input_data);
下面是在C++中实现上述功能的代码:
cv::Mat input_mat;
float* input_data = input_tensor.data<float>();
ov::Shape input_shape = input_tensor.get_shape();
size_t channels = input_shape[1];
size_t height = input_shape[2];
size_t width = input_shape[3];
for (size_t c = 0; c < channels; ++c) {
for (size_t h = 0; h < height; ++h) {
for (size_t w = 0; w < width; ++w) {
input_data[c * height * width + h * width + w] = input_mat.at<cv::Vec<float, 3>>(h, w)[c];
}
}
}
1.8 模型推理
在加载完推理数据后,就可以调用模型推理的API方法推理当前数据,主要使用到的API方法为:
API |
作用 |
InferRequest.infer() |
在同步模式下推断指定的输入。 |
调用该方法也较为简单,只需要调用该API接口即可,在C#中的代码为:
infer_request.infer();
C++中的代码与C++中一致。
1.9 获取推理结果
对于单输出的模型可以直接通过get_output_tensor()方法获得,并调用Tensor的相关方法获取Tensor的相关信息,C# 代码如下所示:
Tensor output_tensor = infer_request.get_output_tensor();
Console.WriteLine("/------- [Output tensor] -------/");
Console.WriteLine("Output tensor type: {0}", output_tensor.get_element_type().to_string());
Console.WriteLine("Output tensor shape: {0}", output_tensor.get_shape().to_string());
Console.WriteLine("Output tensor size: {0}", output_tensor.get_size());
获取输出output_tensor信息为:
/------- [Output tensor] -------/
Output tensor type: f32
Output tensor shape: Shape : {1, 1000}
Output tensor size: 1000
对于输出Tensor,我们只需要读取输出内存上的数据即可,此处我们封装了get_data<T>()方法,可以直接获取输出内存上的数据,在C#中的代码为:
float[] result = output_tensor.get_data<float>(1000);
同样获取推理结果,在C++中的代码为:
const float* output_data = output_tensor.data<const float>();
float result[1000];
for (int i = 0; i < 1000; ++i) {
result[i] = *output_data;
output_data++;
}
在获取结果后,后续的处理需要根据模型的输出类型做相应的处理。
1.10 释放分配的内存
由于C#在封装时采用的C API 接口实现的,因此在C#中会产生较多的 非托管内存,若该对象出现循环重复创建,会导致过多的内存未释放导致内存泄漏,因此对于临时创建的对象在使用后要即使销毁,销毁方式也较为简单,只需要调用对象的dispose()方法即可。
output_tensor.dispose();
input_shape.dispose();
infer_request.dispose();
compiled_model.dispose();
input.dispose();
output.dispose();
model.dispose();
core.dispose();
1.11 Yolov8分類モデルの例
次のコードは、OpenVINOSharp API メソッドを使用してモデルをデプロイする Yolov8 分類モデルの完全なコードを示しています。
using OpenCvSharp;
using OpenCvSharp.Dnn;
using OpenVinoSharp;
using System.Data;
using System.Runtime.InteropServices;
namespace test_openvinosharp_api
{
internal class Program
{
static void Main(string[] args)
{
string model_path = "E:\\GitSpace\\OpenVinoSharp\\model\\yolov8\\yolov8s-cls.xml";
Core core = new Core(); // 初始化推理核心
Model model = core.read_model(model_path); // 读取本地模型
CompiledModel compiled_model = core.compile_model(model, "AUTO"); // 便哟模型到指定设备
// 获取模型的输入输出信息
Console.WriteLine("Model name: {0}", model.get_friendly_name());
Input input = compiled_model.input();
Console.WriteLine("/------- [In] -------/");
Console.WriteLine("Input name: {0}", input.get_any_name());
Console.WriteLine("Input type: {0}", input.get_element_type().to_string());
Console.WriteLine("Input shape: {0}", input.get_shape().to_string());
Output output = compiled_model.output();
Console.WriteLine("/------- [Out] -------/");
Console.WriteLine("Output name: {0}", output.get_any_name());
Console.WriteLine("Output type: {0}", output.get_element_type().to_string());
Console.WriteLine("Output shape: {0}", output.get_shape().to_string());
// 创建推理请求
InferRequest infer_request = compiled_model.create_infer_request();
// 获取输入张量
Tensor input_tensor = infer_request.get_input_tensor();
Console.WriteLine("/------- [Input tensor] -------/");
Console.WriteLine("Input tensor type: {0}", input_tensor.get_element_type().to_string());
Console.WriteLine("Input tensor shape: {0}", input_tensor.get_shape().to_string());
Console.WriteLine("Input tensor size: {0}", input_tensor.get_size());
// 读取并处理输入数据
Mat image = Cv2.ImRead(@"E:\GitSpace\OpenVinoSharp\dataset\image\demo_7.jpg");
Mat input_mat = new Mat();
input_mat = CvDnn.BlobFromImage(image, 1.0 / 255.0, new Size(224, 224), 0, true, false);
// 加载推理数据
Shape input_shape = input_tensor.get_shape();
long channels = input_shape[1];
long height = input_shape[2];
long width = input_shape[3];
float[] input_data = new float[channels * height * width];
Marshal.Copy(input_mat.Ptr(0), input_data, 0, input_data.Length);
input_tensor.set_data(input_data);
// 模型推理
infer_request.infer();
// 获取输出张量
Tensor output_tensor = infer_request.get_output_tensor();
Console.WriteLine("/------- [Output tensor] -------/");
Console.WriteLine("Output tensor type: {0}", output_tensor.get_element_type().to_string());
Console.WriteLine("Output tensor shape: {0}", output_tensor.get_shape().to_string());
Console.WriteLine("Output tensor size: {0}", output_tensor.get_size());
// 获取输出数据
float[] result = output_tensor.get_data<float>(1000);
List<float[]> new_list = new List<float[]> { };
for (int i = 0; i < result.Length; i++)
{
new_list.Add(new float[] { (float)result[i], i });
}
new_list.Sort((a, b) => b[0].CompareTo(a[0]));
KeyValuePair<int, float>[] cls = new KeyValuePair<int, float>[10];
for (int i = 0; i < 10; ++i)
{
cls[i] = new KeyValuePair<int, float>((int)new_list[i][1], new_list[i][0]);
}
Console.WriteLine("\n Classification Top 10 result : \n");
Console.WriteLine("classid probability");
Console.WriteLine("------- -----------");
for (int i = 0; i < 10; ++i)
{
Console.WriteLine("{0} {1}", cls[i].Key.ToString("0"), cls[i].Value.ToString("0.000000"));
}
// 销毁非托管内存
output_tensor.dispose();
input_shape.dispose();
infer_request.dispose();
compiled_model.dispose();
input.dispose();
output.dispose();
model.dispose();
core.dispose();
}
}
}
1.12 概要
この記事では、モデル推論プロセスに基づいて、OpenVINOSharp API の使用法をデモし、OpenVINOSharp API と OpenVINO C++ API を比較して、OpenVINOSharp API と C++ API の違いを示し、C++ API の開発にも役立ちます。とてもフレンドリーなので、始めるのはとても簡単です。
この記事では、基本的なモデル推論処理のコードのみを示し、各 API のテストも行いますが、その他のより高度な API メソッドについては、今後もテストを続けて開発者に使用方法を示します。
一般的に言えば、OpenVINOSharp は Windows 環境でのインストールと使用を完全にサポートしています。開発者はインストールして使用することを歓迎します。関連する質問や最適化方法がある場合は、コメントやガイダンスを提供することも歓迎します。