LoginSignup
7
6

More than 3 years have passed since last update.

C#で実行するOpenVINOによる手書き文字認識(MNIST)

Last updated at Posted at 2021-02-23

はじめに

この記事は、OpenVINOをC#で使うための記事です。
内容としては、過去に書いたC++で実行するOepnVinoによる手書き文字認識(MNIST)の内容を、最新バージョン(2021.2)向けに更新したものになっています。
また、それだけだとただの焼き直しになるので、C++とPythonのインターフェースしか用意されていないOpenVINOをC#から使えるようにしてみました。
説明はしていませんが、dllをC++から使うアプリも一緒に公開していますので、必要があればそちらも見てください。

構成

この記事は以下5つのパートに分かれます。

  1. 学習済みモデル(識別器)の生成
    Kerasというフレームワークを使って学習済みモデルを生成します。
    内容としてはC++で実行するOepnVinoによる手書き文字認識(MNIST)に沿っていますが、フレームワークをChainerからKerasに変更しています。

  2. 学習済みモデルをOpenVINOが扱える形式に変換
    Kerasで作成した学習済みモデルをOpenNINO付属のModel Optimizerで変換します。
    内容としてはC++で実行するOepnVinoによる手書き文字認識(MNIST)に沿っています。

  3. OpenVINOを使って推論するdll(C++)を作成
    OpenVINOを扱うクラスを実装します。
    内容としてはC++で作成したDLLでAPIではなくクラスを提供する方法に沿っています。

  4. SwigでC#向けのインターフェースを追加
    3.で作ったdllにSwigを使いC#向けのインターフェースを追加します。
    内容としてはC++で作成したdllにSwigを使ってC#向けのインターフェースを追加してC#から呼び出す方法に沿っています。

  5. SwigでC#向けインターフェースを追加したdllを使って推論するアプリ(C#)を作成
    C#で作ったdllを使うアプリを作ります。
    内容としてはC++で作成したdllにSwigを使ってC#向けのインターフェースを追加してC#から呼び出す方法に沿っています。

環境

このエントリを書いている私の環境は以下です。

  • OS : Windows10 Pro x64 20H2
  • CPU : Intel
  • GPU : あり(NVIDIA)
    • CUDA : 10.1
    • cuDNN : 7.6.5
  • VPU : あり(Movidius Neural Compute Stick 2)
  • Visual Studio Pro: 2019
  • OpenVino : 2021.2
  • Python3 : 3.7.9
    • Keras : 2.4.3
    • onnx : 1.8.0
    • onnx2keras : 0.0.24
    • opencv-python : 4.4.0.46
    • opencv-contrib-python : 4.4.0.46
    • tendorflow : 2.3.0(GPUがない場合のみ)
    • tensorflow-gpu : 2.3.0(GPUがある場合のみ)
  • Swig : 4.0.2

CUDAとcuDNNはお使いのtensorflow-GPUのバージョンで変えてください。
ここに対応表があります。

OpenVINOを使う理由について

近年、KerasやPyTorchなどのDeep Learningフレームワークが登場したことにより、手軽に個人でDeep Learning(学習フェーズと推論フェーズ)を実行できるようになりつつありますが、個人では以下の理由により製品適用レベルの学習済みモデルを作成することはまだハードルが高いです。

  • 十分な量・質の学習データを用意する必要がある
  • 専門的な知識がないとどのようなネットワークにすれば良いのか判断できない
  • 高性能な外付けGPU(dGPU)がないと学習に時間がかかる

また、学習済みモデルさえ用意できれば推論フェーズは個人で十分に実行可能ではあるのですが、推論自体も処理負荷が軽いわけではないために非力なデバイスで実行すると思った通りの速度がでない、もしくはCPU負荷が100%付近に張り付きユーザービリティの低下を招くという状況になってしまいます。
そのため、解決策の一つして今回はIntel社のOpenVINOを使用します。
OpenVINOを使用することで、以下が可能になります。
- CPU以外のデバイス(iGPU, VPU, FPGA)での推論
- CPUで実施する推論処理の高速化

具体的には、CPUが非力で推論を実行するとCPU負荷が100%になってしまう場合、VPU(Movidius Neural Compute Stick)を接続してVPUで推論することによりCPUには負荷をかけずに推論というようなことが可能になります。

なお、OpenVINOはIntelが出しているため、上で記載しているCPU、iGPU、VPU、FPGAとはIntel製のものがターゲットになります。

1. 学習済みモデル(識別器)の生成

問題設定

今回はMNISTという手書き数字(0~9)を学習させて、入力された数字が0~9のどれなのか当てるという分類問題とします。
mnist.png

データ自体はAPI一発でダウンロードできるため、簡単に利用することが可能です。
学習・検証には公開されているデータを使い、テストには私がペイントで作ったデータを使うこととします。

ネットワーク

ニューラルネットワークは、入力層と隠れ層(中間層)、出力層を決める必要がありますが、その前にNCHWの説明をします。
NCHWとはそれぞれ以下を示します。

  • N : バッチサイズ
  • C : チャネル数(カラー画像では3、グレースケールでは1)
  • H : 画像の高さ
  • W : 画像の幅

NCHW:1x3x224x224という感じで使われる用語なので覚えておいてください。
さて入力層ですがMNISTは手書き数字のデータセットで、1枚は28x28のグレースケールですので素直に作るとNx1x28x28となります。
が、今回はわざわざカラー画像として入力したいと思いますのでNx3x28x28となります。
Nは実際に学習させる時にメモリに載るサイズまで増やしていきましょう。
隠れ層は、各自自由に作ってみて精度が出なければ層を深くするなり、畳み込みをするなり、工夫してみてください。
最後の出力層ですが、最終的に入力した画像が0~9のどれなのかを識別したいので、Nx10となります。

実装

今回はJupyter Notebook上で試していますのでそれぞれのセルごとに説明していきます。
なおMNIST-Keras.ipynbというファイル名でここに格納しています。

import

from keras.datasets import mnist
from keras.utils import to_categorical
from keras.layers import Dense, Convolution2D, MaxPooling2D, Input, BatchNormalization, GlobalAveragePooling2D
from keras.layers import ReLU
from keras import Model
from keras.callbacks import EarlyStopping
from keras.optimizers import Adam
import keras2onnx
from keras2onnx import convert_keras
from onnx import save_model
import numpy as np
from tensorflow.python.keras import backend as K

Kerasなど必要なものをimportしています。
ここでエラーになる場合は必要なパッケージが足りていませんので、エラーに従って適宜入れてください。

GPUが接続されているか確認

from tensorflow.python.client import device_lib
deviceStr = str(device_lib.list_local_devices())
isGPU = False
if "GPU" in deviceStr:
    isGPU = True

print("*** isGPU == {} ***".format(isGPU))
print(deviceStr)

GPUが接続されているかチェックしています。
GPUの接続が確認できればisGPUをTrueと、確認できなければFalseとしています。

学習・検証用データの作成

# データ取得
(x_train, y_train), (x_test, y_test) = mnist.load_data()

x_train_shape = x_train.shape
x_test_shape = x_test.shape

# reshape([?, 28, 28] -> [?, 1, 28, 28])
x_train = x_train.reshape(x_train_shape[0], 1, x_train_shape[1], x_train_shape[2])
x_test = x_test.reshape(x_test_shape[0], 1, x_test_shape[1], x_test_shape[2])

# データの色情報を1次元から3次元に拡張([?, 1, 28, 28] -> [?, 3, 28, 28])
# グレースケールからカラーに拡張することになるが、色情報は同じ値としている
x_train = np.tile(x_train, (1,3,1,1))
x_test = np.tile(x_test, (1,3,1,1))

if isGPU:
    data_format = "channels_first"
    input_shape = (3, 28, 28)

else:
    data_format = "channels_last"
    input_shape = (28, 28, 3)
    x_train = np.transpose(x_train, [0,2,3,1])
    x_test  = np.transpose(x_test,  [0,2,3,1])

x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255

num_classes = 10
# convert class vectors to binary class matrices
y_train = to_categorical(y_train, num_classes)
y_test = to_categorical(y_test, num_classes)

ここでは3つのポイントがあります。
- チャネルを1チャネルから3チャネルへ
  これはテスト用画像をペイントで作る時にグレースケール画像(1チャネルの意味)を作れないからです。
- isGPU==FalseならNCHWからNHWCへ
  CPUではNCHWに対応していないため。
  なお、GPUはNHWCにも対応していますが、速度が遅くなります。
- 画像の取りうる値を[0,255]から[0.0, 1.0]へ
  精度を上げるため。

ネットワーク定義

def BN(x, gpu):
    if gpu:
        return BatchNormalization(axis=1)(x)
    else:
        return BatchNormalization(axis=-1)(x)   

input_layer = Input(shape=input_shape)
x = Convolution2D(32, kernel_size=(3, 3), padding="same", input_shape=input_shape, data_format=data_format)(input_layer)
x = BN(x, isGPU)
x = ReLU()(x)
x = Convolution2D(32, kernel_size=(3, 3), padding="same", data_format=data_format)(x)
x = BN(x, isGPU)
x = ReLU()(x)
x = MaxPooling2D(pool_size=(2, 2), padding="same", data_format=data_format)(x)

x = Convolution2D(128, kernel_size=(3, 3), padding="same", data_format=data_format)(x)
x = BN(x, isGPU)
x = ReLU()(x)
x = Convolution2D(128, kernel_size=(3, 3), padding="same", data_format=data_format)(x)
x = BN(x, isGPU)
x = ReLU()(x)
x = MaxPooling2D(pool_size=(2, 2), data_format=data_format)(x)

x = Convolution2D(512, kernel_size=(3, 3), padding="same", data_format=data_format)(x)
x = BN(x, isGPU)
x = ReLU()(x)
x = Convolution2D(512, kernel_size=(3, 3), padding="same", data_format=data_format)(x)
x = BN(x, isGPU)
x = ReLU()(x)
x = MaxPooling2D(pool_size=(2, 2), data_format=data_format)(x)

x = GlobalAveragePooling2D(data_format=data_format)(x)
x = Dense(128, activation='relu')(x)
output = Dense(num_classes, activation='softmax')(x)

model = Model(input_layer, output)

model.compile(loss='categorical_crossentropy',
              optimizer=Adam(),
              metrics=['accuracy'])

ネットワーク全体は上記としました。
上記ネットワークはCPUで実行すると時間がかかりますので、GPUがない方はGoogle Colabを使うなり、時間がかからないようにネットワークを変更するなりしてください。
最適化関数はAdamとしています。

学習

history = model.fit(x_train, y_train,
                    batch_size=1024,
                    epochs=30,
                    verbose=1,
                    callbacks=[EarlyStopping(monitor='val_loss', patience=5, verbose=0, mode='auto')],
                    validation_data=(x_test, y_test), )

Epochを進めて検証用データでのlossとaccuracyが改善していることを確認してください。

テスト

import  glob
import cv2
imageFilePaths = glob.glob("""OpenVINOApp\OpenVINOAppCpp\Data\Image\*.png""")

for imageFilePath in imageFilePaths:
    image = cv2.imread(imageFilePath)

    if isGPU:
        image = np.transpose(image, [2,0,1]) 
        image = image.reshape(1,3,28,28)
    else:
        image = image.reshape(1,28,28,3)

    image = image / 255.0
    outputs = model.predict(image)
    print("-------   {0}  -----------".format(imageFilePath))
    for i, output in enumerate(outputs[0]):
        print("[{0}] = {1:.4f}".format(i, output))

実際にペイントで作った3x28x28の画像を入力し、それぞれどの数字と予測したのかを表示しています。
今学習したモデルで正解しているかを確認してください。

学習済みモデルの保存

# .h5で保存
model.save("mnist.h5", save_format="h5")

if isGPU:
    # .onnxで保存
    onnx_model = convert_keras(model, model.name, channel_first_inputs=[1,3,28,28])
    save_model(onnx_model, "mnist.onnx")

else:
    # .pbで保存
    import tensorflow as tf
    from tensorflow import keras
    from tensorflow.python.framework.convert_to_constants import convert_variables_to_constants_v2

    #path of the directory where you want to save your model
    frozen_out_path = ''

    # Convert Keras model to ConcreteFunction
    full_model = tf.function(lambda x: model(x))
    full_model = full_model.get_concrete_function(
        tf.TensorSpec(model.inputs[0].shape, model.inputs[0].dtype))
    # Get frozen ConcreteFunction
    frozen_func = convert_variables_to_constants_v2(full_model)
    frozen_func.graph.as_graph_def()
    layers = [op.name for op in frozen_func.graph.get_operations()]

    # Save frozen graph to disk
    tf.io.write_graph(graph_or_graph_def=frozen_func.graph,
                      logdir=frozen_out_path,
                      name="mnist.pb",
                      as_text=False)
    # Save its text representation
    tf.io.write_graph(graph_or_graph_def=frozen_func.graph,
                      logdir=frozen_out_path,
                      name="mnist.pbtxt",
                      as_text=True)

まず、普通にKerasの機能で学習済みモデルを保存(mnist.h5)しています。
ただしh5形式の学習済みモデルはOpenVINOでは使えないため、ONNX形式かpb形式に一旦した後にIR形式というOpenVINO専用の形式にする必要があります。
なお、NHWCをONNX形式で保存してもうまくIR形式に変換できなかったので、NHWCの場合はpb形式で保存するようにしています。

これで学習済みモデルを作成する部分は完了です。

学習済みモデルをOpenVINOが扱える形式に変換

OpenVinoはONNX形式の学習済みモデルを動かすことはできず、IR形式と呼ばれる専用の形式で記述された学習済みモデルでしか動作させることができません。そのため、ONNX形式をIR形式に変換する必要があります。ここが皆さんが遭遇する最初の山かもしれません。

公式ページに沿ってやっていきましょう。

OpenVinoのインストール

公式ページにそのものずばりがありますので割愛します。
公式ページ以外ですと、以下2つを紹介しておきます。
- OpenVINO 2021.1 環境構築(Windows 10編)
- OpenVINO (2019.R1) Windows10版のインストールとサンプルのテスト

環境設定

公式ページによると、"setupvars.bat"を実行する必要があると記載されいるので、さっそく実行します。

cd /d "C:\Program Files (x86)\Intel\openvino_2021\bin"
setupvars.bat

次に変換に必要なパッケージをインストールしますが、ONNX形式をIR形式にするのか、それともpb形式をIR形式にするのかで必要となるパッケージが異なります。

ONNX形式の場合は以下を実行してください。

cd /d "C:\Program Files (x86)\Intel\openvino_2021\deployment_tools\model_optimizer\install_prerequisites"
install_prerequisites_onnx.bat

pb形式の場合ですが、今回はKeras(Tensorflow2)でpb形式の学習済みモデルを作ったので、install_prerequisites_tf2.batを実行することになります。

変換

ようやくIR形式に変換していきます。
このページを参照しながら変換していきましょう。

上記ページをみると、以下のコマンドで変換ができると記載があります。

python3 mo.py --input_model INPUT_MODEL

また必要なオプションも記載してありますので各自試してみてください。
最終的に私が点けたオプションは以下の通りです。

  • --framework onnx (pb形式の時はtf)
  • --output_dir xxx (出力先は適宜指定してください)
  • --b 1
  • --data_type FP16
  • --scale_values [255,255,255]

--data_type FP16は1つのモデルでCPU、GPU、VPUでの推論を可能にするためです。
各デバイスで対応しているdata_typeは以下ページ内のSupported Model Formatsセクションに記載されています。
Supported Devices

--scale_values [255,255,255]は入力された画像の値をコード上で255で割る手間を省くためです。

ONNX形式時に実行したコマンドは以下の通りです。

python mo.py --framework onnx --input_model mnist.onnx --output_dir "C:\Work\" --b 1 --data_type FP16 --scale_values [255,255,255]

上記を実行するとC:\Work\に以下のファイルが作成されていることが確認できます。

  • mnist.xml
  • mnist.bin
  • mnist.mapping

これでModel Optimaizerによる変換は完了です。
今までのコマンドを自動で実行するバッチをconvertModel.batというファイル名で置いていますので面倒な人はそっちを使っても良いです。

3. OpenVINOを使って推論するdll(C++)を作成

ようやくIR形式の学習済みモデルが作成できましたので、推論をする部分を作っていきます。
推論用のコードはC++かPythonで書くことができます。
Pythonで説明しているQiitaのエントリは我ら(また勝手に"我らが"とか言ってすみません)がPINTOさんを筆頭に良質なエントリがありますし、私は最終的にC#を使って推論をしたいため、今回はC++推論用のdllを作り、それをC#で使うことを目指します。

なにはともあれ、Inference Engineの説明ページを見てみます。
上記ページによると以下の流れで推論を実施することになります。

  1. Initialize Core
  2. Read Model IR
  3. Configure Input & Output
  4. Load Model
  5. Create Infer Request
  6. Prepare Input
  7. Do Inference

順にコードに落としていきます。
今回はMyOpenVINOImplというクラスを定義して、そこに推論機能を実装してGetInstance()というAPIでMyOpenVINOImplクラスのインスタンスを取得して使うことを想定しています。
dll自体の作り方はC++で作成したDLLでAPIではなくクラスを提供する方法に沿っていますのでそちらを参照してください。
ここではOpenVINOのAPIを使う部分にフォーカスして説明します。
なお、不明な個所があったら、Inference Engineの説明ページを参照してください。

実装

1. Initialize Core

    InferenceEngine::Core core;
    InferenceEngine::CNNNetwork network;
    InferenceEngine::ExecutableNetwork executableNetwork;

ただの宣言ですね。

2. Read Model IR

ここではモデルの読み込みを実施しています。

bool MyOpenVINOImpl::ReadNetwork(const std::string &modelName)
{
    bool ret = true;

    try
    {
        if (modelName.find(".onnx") == std::string::npos)
        {
            // IR
            std::string binName = modelName.substr(0, modelName.length() - 4) + ".bin";
            network = core.ReadNetwork(modelName, binName);
        }
        else
        {
            // onnx
            network = core.ReadNetwork(modelName);
        }
    }
    catch (InferenceEngine::details::InferenceEngineException e)
    {
        printf("Error ReadNetwork() : %s\r\n", e.what());
        ret = false;
    }
    catch (...)
    {
        printf("Error ReadNetwork() \r\n");
        ret = false;
    }

    return ret;
}

引数のmodelNameには.xmlファイルのパスを渡します。
そのため、学習済みモデルとして必要なものは.xmlファイルと.binファイルの2つです。
IR形式に変換する際に.mappingファイルが作成されていますが、不要です。
また、実はONNX形式もそのまま読み込めるようなので実装したんですが、未試行なので動くか不明です。

3. Configure Input & Output

ここでは入力と出力の形状(NCHWとか)とデータ型(FP32とか)を設定します。

bool MyOpenVINOImpl::SetNetworkConfiguration(const Layout&  iLayout, const Precision& iPrecision, const Layout& oLayout, const Precision& oPrecision)
{
    bool ret = true;
    try
    {
        auto inputLayerData = network.getInputsInfo().begin()->second;
        InferenceEngine::Layout il = ConvertInferenceEngineLayout(iLayout);
        InferenceEngine::Precision ip = ConvertInferenceEnginePrecision(iPrecision);
        inputLayerData->setLayout(il);
        inputLayerData->setPrecision(ip);
        inputLayerName = std::string( network.getInputsInfo().begin()->first.c_str() );
        inputLayout = iLayout;
        inputPresicion = iPrecision;

        auto outputLayerData = network.getOutputsInfo().begin()->second;
        InferenceEngine::Layout ol = ConvertInferenceEngineLayout(oLayout);
        InferenceEngine::Precision op = ConvertInferenceEnginePrecision(oPrecision);
        outputLayerData->setLayout(ol);
        outputLayerData->setPrecision(op);
        outputLayerName = std::string ( network.getOutputsInfo().begin()->first.c_str() );
    }
    catch (...)
    {
        printf("Error SetNetworkConfiguration()\r\n");
        ret = false;
    }

    return ret;
}

Pythonで学習させた時に形状とデータ型をどうしていたのか思い出して、それを指定するだけです。

4. Load Model

実行可能なネットワークを作成します。
特に何も考えずにこのまま実行しましょう。

bool MyOpenVINOImpl::LoadNetwork(const std::vector<Device>& devices, const bool &isMulti )
{
    bool ret = true;

    try
    {
        std::string deviceStr = ConvertDevices2String(devices, isMulti);
        executableNetwork = core.LoadNetwork(network, deviceStr);
    }
    catch (...)
    {
        ret = false;
        printf("Error : LoadNetwork()\r\n");
    }

    return ret;
}

5. Create Infer Request

ここでは推論を実行するオブジェクトを作成します。
ここも特に何も考えずにこのまま実行しましょう。

InferenceEngine::InferRequest MyOpenVINOImpl::CreateInferRequest()
{
    bool ret = true;
    InferenceEngine::InferRequest inferRequest;
    try
    {
        inferRequest = executableNetwork.CreateInferRequest();
    }
    catch (...)
    {
        ;
    }

    return inferRequest;
}

6. Prepare Input

matU8ToBlob()でデータをセットしています。

bool MyOpenVINOImpl::SetInputData(InferenceEngine::InferRequest  &inferRequest,const std::string &imageName)
{
    bool ret = true;
    cv::Mat imageData = cv::imread(imageName);

    InferenceEngine::Blob::Ptr inputBlob = inferRequest.GetBlob(inputLayerName);
    matU8ToBlob<uint8_t>(imageData, inputBlob);

    return ret;
}

7. Do Inference

推論を実行します。
同期の場合はInferSync()を呼び出すだけで、推論が終了するまで待たされて結果が返ってきます。
非同期の場合はInferASync()を呼び出すとすぐ制御が返りますが、推論の実態はInferASyncLocal()でされて結果はコールバックで渡されます。
なお、事前に結果を返すコールバックを登録しておく必要があります。

// 推論(同期)
std::vector<float> MyOpenVINOImpl::InferSync(const std::string& imageName)
{
    InferenceEngine::InferRequest inferRequest = CreateInferRequest();

    SetInputData(inferRequest, imageName);
    inferRequest.Infer();
    std::vector<float> outputVect;
    InferenceEngine::SizeVector dims = inferRequest.GetBlob(outputLayerName)->getTensorDesc().getDims();
    const float* oneHotVector = (inferRequest.GetBlob(outputLayerName))->buffer().as<float*>();
    int dim = dims[1];

    for (int i = 0; i < dim; i++)
    {
        outputVect.push_back(oneHotVector[i]);
    }

    return outputVect;
}

// 推論(非同期)
int MyOpenVINOImpl::InferASync(const std::string& imageName)
{
    int inferID = inferCounter;
    inferCounter += 1;

    InferenceEngine::InferRequest inferRequest = CreateInferRequest();

    InferInfo inferInfo(inferRequest, imageName);
    inferMap[inferID] = inferInfo;
    std::thread* th = NULL;

    // 現状はInferASync()が実行されるたびにスレッドを作ってその中でStartAsync()をしている。
    // スレッドを作るコストは高いので、本当はthreadNumだけWorkerスレッド作って、ということをするべき。
    th =new std::thread(&MyOpenVINOImpl::InferASyncLocal, this, inferID);
    threadVector.push_back(std::move(th));
    return inferID;
}

void  MyOpenVINOImpl::InferASyncLocal(int inferID)
{
    bool successed = true;
    std::vector<float> outputVect;
    std::string inputImage;
    try
    {
        InferInfo inferInfo = inferMap[inferID];
        inputImage = inferInfo.inputImage;
        SetInputData(inferInfo.inferRequest, inputImage);
        inferInfo.inferRequest.StartAsync();        
        inferInfo.inferRequest.Wait(InferenceEngine::IInferRequest::WaitMode::RESULT_READY);
        InferenceEngine::SizeVector dims = inferInfo.inferRequest.GetBlob(outputLayerName)->getTensorDesc().getDims();
        const float* oneHotVector = (inferInfo.inferRequest.GetBlob(outputLayerName))->buffer().as<float*>();
        int dim = dims[1];

        for (int i = 0; i < dim; i++)
        {
            outputVect.push_back(oneHotVector[i]);
        }
    }
    catch (...)
    {
        successed = false;
    }

    if (pCallbackHandler != NULL)
    {
        pCallbackHandler->InferCallBack(inferID, inputImage  , successed,  outputVect);
    }

    return ;
}

ビルド

Visual Studioでビルドするのですが、そのままではビルド出来ないため以下を設定します。

項目 内容
アーキテクチャ x64
追加のインクルードディレクトリ - C:\Program Files (x86)\Intel\openvino_2021\deployment_tools\inference_engine\include\
- C:\Program Files (x86)\Intel\openvino_2021\opencv\include
追加の依存ファイル Debug
- C:\Program Files (x86)\Intel\openvino_2021\deployment_tools\inference_engine\lib\intel64\Debug\inference_engined.lib
- C:\Program Files (x86)\Intel\openvino_2021\opencv\lib\opencv_imgproc451d.lib
- C:\Program Files (x86)\Intel\openvino_2021\opencv\lib\opencv_core451d.lib
- C:\Program Files (x86)\Intel\openvino_2021\opencv\lib\opencv_imgcodecs451d.lib

Release
- C:\Program Files (x86)\Intel\openvino_2021\deployment_tools\inference_engine\lib\intel64\Release\inference_engine.lib
- C:\Program Files (x86)\Intel\openvino_2021\opencv\lib\opencv_imgproc451.lib
- C:\Program Files (x86)\Intel\openvino_2021\opencv\lib\opencv_core451.lib
- C:\Program Files (x86)\Intel\openvino_2021\opencv\lib\opencv_imgcodecs451.lib

また、出力ディレクトリを以下に変更しています。

$(ProjectDir)$(Platform)\$(Configuration)\

C#側がdllを使う際に作成したdll(MyOpenVINO.dll)をコピーするのですが、dllが置かれている場所が異なるとコピーできませんの注意してください。

dumpbin

ビルドに成功したのでdllが作成されましたが、どのようなAPIが使えるのか確認してみましょう。
Visual Studioに付属しているDeveloper Command Prompt for VS2019を起動してみます。
(お使いの環境によっては開発者コマンドプロンプトという名称かもしれません)

Developer Command Prompt for VS2019を起動して、さきほど作成したdllが置いてある場所に移動してください。
今回はC:\Workに置いてあるとしています。
dumpbinというコマンドでdllが公開しているAPIを確認することが出来ます。
swig_before.png

赤枠部分が公開しているAPIですが、GetInstanceという名前のAPIだけが公開されていることが分かります。

4. SwigでC#向けのインターフェースを追加

Swigを使ってC#向けのインターフェースを追加する方法はC++で作成したdllにSwigを使ってC#向けのインターフェースを追加してC#から呼び出す方法を確認してください。
最終的なインターフェースファイル(.i)は以下となります。

/* File : Swig.i */
%include <windows.i> 
%include <std_string.i>
%include <std_vector.i>
%include <arrays_csharp.i>

%module (directors="1") MyOpenVINO

%{
#include "MyOpenVINO.h"
%}

%feature("director") CallbackHandlerBase;

/* Let's just grab the original header file here */
%include "MyOpenVINO.h"

%template(DeviceVector) std::vector<Device>;
%template(floatVector) std::vector<float>;

次に以下のコマンドを実行します。
なお、作ったインターフェースファイル、先ほどdllを作る際に作成したヘッダファイル(MyOpenVINO.h)が同じ場所にある前提です。

swig.exe -c++ -csharp -cppext cpp Swig.i

これで同じ場所に以下のファイルが作成されます。

  • Swig_wrap.cpp
  • Swig_wrap.h
  • CallbackHandlerBase.cs
  • Device.cs
  • DeviceVector.cs
  • floatVector.cs
  • IMyOpenVINO.cs
  • Layout.cs
  • MyOpenVINO.cs
  • MyOpenVINOPINVOKE.cs
  • NetworkInfo.cs
  • Precision.cs

作成された上記ファイルのうち、Swig_wrap.cppをMyOpenVINOプロジェクトに加えてビルドします。
なお、Swig_wrap.hがSwig_wrap.cppと同じ場所にあるのであれば、Swig_wrap.hを加える必要はありませんが、同じ場所にないのであればSwig_wrap.hも加えてください。
ちなみにexecSwig.batというファイル名でSwigを実行するバッチを作っていますので、それを実行しても良いです。

dumpbin

Swigで作成したファイルを加えてビルドしましたので、dllが公開しているAPIがどう変わったのか確認してみましょう。
swig_after.png

赤枠部分が公開しているAPIですが、ものすごくAPIが追加されていることが分かりますね。

5. SwigでC#向けインターフェースを追加したdllを使って推論するアプリ(C#)を作成

SwigでC#向けインターフェースを追加したdllを使って推論するアプリ(C#)を作成する方法はC++で作成したdllにSwigを使ってC#向けのインターフェースを追加してC#から呼び出す方法を確認してください。

やることを簡単に書くと以下になります。

  1. Swigが作成した.csファイルをプロジェクトに追加
  2. CallbackHandlerBaseクラスを継承したクラスを作成
  3. MyOpenVINOクラスを使って推論
  4. dllを動かすためのファイルをコピー

なお今回作成するアプリはWPF App(.NET)としていますが、コンソールでも良いので必要あれば適宜変更してください。

1. Swigが作成した.csファイルをプロジェクトに追加

プロジェクトに以下のファイルを追加してください。

  • CallbackHandlerBase.cs
  • Device.cs
  • DeviceVector.cs
  • floatVector.cs
  • IMyOpenVINO.cs
  • Layout.cs
  • MyOpenVINO.cs
  • MyOpenVINOPINVOKE.cs
  • NetworkInfo.cs
  • Precision.cs

2. CallbackHandlerBaseクラスを継承したクラスを作成

以下となります。

public  class InferCallBackHandler : CallbackHandlerBase
{
    private TextBox textBox;

    public InferCallBackHandler(TextBox tbx)
    {
        textBox = tbx;
    }

    // dll側から呼ばれる
    public override void InferCallBack(int inferID, string inputImage, bool isSuccessed, floatVector results)
    {
        textBox.Dispatcher.Invoke((Action)(() =>
        {
            int i = 0;
            textBox.Text += $"CallBack   inferID[{inferID}] : [inputImage] = {inputImage}" + System.Environment.NewLine;
            foreach (float result in results)
            {
                textBox.Text += $"CallBack   inferID[{inferID}] : [{i}] = {Math.Round(result, 4, MidpointRounding.AwayFromZero)}" + System.Environment.NewLine;
                i += 1;
            }
            textBox.Text += $"---------------------" + System.Environment.NewLine;
        }));
    }
}

InferCallBack()でUI上に配置してあるTextBoxに推論結果を表示しています。

3. MyOpenVINOクラスを使って推論

private void Click_InferSync(object sender, RoutedEventArgs e)
{
    string deviceStr = GetCheckedRadioButtonString();
    NetworkInfo networkInfo = new NetworkInfo();
    networkInfo.modelName = @"Model\mnist.xml";
    networkInfo.inputLayout = Layout.NCHW;
    networkInfo.inputPrecision = Precision.U8;
    networkInfo.outputLayout = Layout.NC;
    networkInfo.outputPrecision = Precision.FP32;
    networkInfo.isMultiDevices = false;
    networkInfo.devices.Add(ConvertString2Device(deviceStr));

    string[] images = System.IO.Directory.GetFiles(inputImageDir.Text);
    List<string> inputImageFiles = new List<string>();
    inputImageFiles.AddRange(images);

    // instance.GetAvailableDevices();
    instance.Initialize(networkInfo);

    foreach (string inputImage in inputImageFiles)
    {             
        instance.InferSync(inputImage); 

        floatVector outputVec = instance.InferSync(inputImage);
        textBox.Text += $"inputImageFile : {System.IO.Path.GetFileName(inputImage)}" + System.Environment.NewLine;
        int i = 0;
        foreach(float output in outputVec)
        {
        //textBox.Text += string.Format("{0} : {1:f4}", i, outputVec[i]) + System.Environment.NewLine;
        textBox.Text +=  $"[{i}] :  {Math.Round(output, 4, MidpointRounding.AwayFromZero)}" + System.Environment.NewLine;
        i++;
        }
    }
}

private void Click_InferASync(object sender, RoutedEventArgs e)
{
    InferCallBackHandler obj = new InferCallBackHandler(textBox);
    string deviceStr = GetCheckedRadioButtonString();
    NetworkInfo networkInfo = new NetworkInfo();
    networkInfo.modelName = @"Model\mnist.xml";
    networkInfo.inputLayout = Layout.NCHW;
    networkInfo.inputPrecision = Precision.U8;
    networkInfo.outputLayout = Layout.NC;
    networkInfo.outputPrecision = Precision.FP32;
    networkInfo.threadNum = 0;
    networkInfo.isMultiDevices = false;
    networkInfo.devices.Add(ConvertString2Device(deviceStr));

    string[] images = System.IO.Directory.GetFiles(inputImageDir.Text);
    List<string> inputImageFiles = new List<string>();
    inputImageFiles.AddRange(images);

    // instance.GetAvailableDevices();
    Task.Run(() =>
    {
        instance.Initialize(networkInfo);
        instance.SetInferCallBack(obj);

        foreach (string inputImage in inputImageFiles)
        {
            instance.InferASync(inputImage);
        }
    });           
}

Click_InferSync()はInferSyncボタンクリック時に実行されるAPIで、Click_InferASync()はInferASyncボタンクリック時に実行されるAPIです。

あとは、UI周りをちょちょっと作成してください。

4. dllを動かすためのファイルをコピー

コード自体は完成し、ビルドも出来るようになったのですがdllを動かすために必要なファイルがありますので、ビルド後のイベントを使ってコピーします。
以下のコマンドをビルド後イベントのコマンドラインに記載してください。

if "$(ConfigurationName)" == "Debug" (
xcopy "C:\Program Files (x86)\Intel\openvino_2021\deployment_tools\inference_engine\external\tbb\bin\tbb_debug.dll" "$(TargetDir)" /D /S /R /Y /I /K
xcopy "C:\Program Files (x86)\Intel\openvino_2021\deployment_tools\ngraph\lib\ngraphd.dll" "$(TargetDir)" /D /S /R /Y /I /K
xcopy "C:\Program Files (x86)\Intel\openvino_2021\deployment_tools\ngraph\lib\onnx_importerd.dll" "$(TargetDir)" /D /S /R /Y /I /K
xcopy "C:\Program Files (x86)\Intel\openvino_2021\opencv\bin\opencv_imgproc451d.dll" "$(TargetDir)" /D /S /R /Y /I /K
xcopy "C:\Program Files (x86)\Intel\openvino_2021\opencv\bin\opencv_core451d.dll" "$(TargetDir)" /D /S /R /Y /I /K
xcopy "C:\Program Files (x86)\Intel\openvino_2021\opencv\bin\opencv_imgcodecs451d.dll" "$(TargetDir)" /D /S /R /Y /I /K
xcopy "C:\Program Files (x86)\Intel\openvino_2021\inference_engine\bin\intel64\$(Configuration)" "$(TargetDir)" /D /S /R /Y /I /K
)

if "$(ConfigurationName)" == "Release" (
xcopy "C:\Program Files (x86)\Intel\openvino_2021\deployment_tools\inference_engine\external\tbb\bin\tbb.dll" "$(TargetDir)" /D /S /R /Y /I /K
xcopy "C:\Program Files (x86)\Intel\openvino_2021\deployment_tools\ngraph\lib\ngraph.dll" "$(TargetDir)" /D /S /R /Y /I /K
xcopy "C:\Program Files (x86)\Intel\openvino_2021\deployment_tools\ngraph\lib\onnx_importer.dll" "$(TargetDir)" /D /S /R /Y /I /K
xcopy "C:\Program Files (x86)\Intel\openvino_2021\opencv\bin\opencv_imgproc451.dll" "$(TargetDir)" /D /S /R /Y /I /K
xcopy "C:\Program Files (x86)\Intel\openvino_2021\opencv\bin\opencv_core451.dll" "$(TargetDir)" /D /S /R /Y /I /K
xcopy "C:\Program Files (x86)\Intel\openvino_2021\opencv\bin\opencv_imgcodecs451.dll" "$(TargetDir)" /D /S /R /Y /I /K
xcopy "C:\Program Files (x86)\Intel\openvino_2021\inference_engine\bin\intel64\$(Configuration)" "$(TargetDir)" /D /S /R /Y /I /K
)
xcopy "$(SolutionDir)MyOpenVINO\x64\$(Configuration)\MyOpenVINO.dll" "$(TargetDir)" /D /S /R /Y /I /K
xcopy "$(SolutionDir)OpenVINOAppCpp\Data\" "$(TargetDir)" /D /S /R /Y /I /K

これで動くようになったはずなので、実行してみてください。
以下のような画面が表示されます。
OpenVINOAppNet.png

UI上にラジオボタンが表示されていますが、MYRIADはMovidius Neural Compute Stick 2が接続されていると選択できるようになります。

終わりに

OpenVinoを使って推論を実行してみました。
OpenVINOをC#から使っている記事はなさそうですので、同じことをしたい人の助けになれば幸いです。
誤記や認識間違いあればご指摘をお願いします。

参考リンク

7
6
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6