AE2100 OpenVINO API2.0移行ガイド

# 要約
- この記事ではOpenVINO2022.1から新たに導入されたOpenVINO API2.0について紹介します。
- OpenVINO API2.0を使ったC++のシンプルなプログラムを作成し、OKI AIエッジコンピューターAE2100で実行してみます。 

※本記事は、AE2100向けのOpenVINO2022.1コンテナを対象としています

# はじめに
OpenVINO2022.1ではAPIが”OpenVINO API2.0"にバージョンアップされました。
暫くは従来のAPIも使用できるようですが将来的に削除される予定のため、
これから新規にOpenVINOを使って開発をされる方はOpenVINO API2.0への移行をお勧めします。

# API2.0の変更点
主な変更は以下の通りです。
-  関数名などが学習フレームワーク寄りの名前に変更され、PytorchやTensorFlowから移行しやすくなりました。

-  前処理や後処理をOpenVINOのAPIを使ってユーザーが定義できるようになりました。
   これまでOpenCVやNumpyなどで記述していた前処理や後処理をモデル構造に組み込めるようになることで、CPUの負荷を減らすことができます。

-  モデルにDynamic Shapesがサポートされました。
   この機能によりNLP(自然言語処理）等で入力データのパディングなしに推論を実行できるようになります。しかし、私が試したところOpenVINO2022.1時点ではCPUのみのサポートのようです。

# API対応表（簡易）
OpenVINO API1.0とAPI2.0の対応表を掲載します。
過去に作成されたプログラムをAPI2.0へ移行される際にお役立てください。

【C++】
| API 1.0 | API 2.0 |備考|
|:---|:---|:------------------------| 
|InferenceEngine::Core ie;|core = Core()|Coreオブジェクトの生成| 
|network = ie.ReadNetwork(model_path);|network = ie.read_model(model_path);|IRモデル読み込み| 
|なし（OpenCV,Numpy,PIL等で記述）| ov::preprocess::PrePostProcessor ppp(network);|前処理をモデルに組み込み| 
|executable_network = ie.LoadNetwork(network, "CPU");|compiled_model = ie.compile_model(network, "CPU");|モデルをデバイス（プラグイン）に送信してコンパイル| 
|infer_request = executable_network.CreateInferRequest();|infer_request = compiled_model.create_infer_request();|推論要求を送るためのinfer_requestオブジェクトの生成| 
|infer_request.SetBlob(input_name,InferenceEngine::make_shared_blob<uint8_t>(tDesc,image.data));|infer_request.set_input_tensor(input_tensor);|推論データをモデルの入力にセット| 
|infer_request.Infer();|infer_request.infer();|推論実行| 
|float* output = infer_request.GetBlob(output_name)->buffer();|const ov::Tensor& output_tensor = infer_request.get_output_tensor();|推論結果の取り出し| 

【Python】
| API 1.0 | API 2.0 |備考|
|:---|:---|:---| 
| ie = IECore | core = Core() |Coreオブジェクトの生成| 
| net = ie.read_network() | model = core.read_model() |IRモデル読み込み| 
| なし（OpenCV,Numpy,PIL等で記述） | ppp = PrePostProcessor(model) |前処理をモデルに組み込み| 
| exenet = ie.load_network() | compiled_model = core.compile_model() |モデルをデバイス（プラグイン）に送信してコンパイル| 
| result = exenet.infer() | result = compiled_model.infer_new_request() |推論実行| 
| infer_result = result['outBlobName'] | infer_result = next(iter(result.values()))  |推論結果の取り出し| 


# OpenVINO API2.0を使ったプログラミング
C++の画像分類プログラムの実装を例を説明していきます。
ここで紹介するサンプルコードではOpenCVを使って画像ファイルを読み込み、OpenVINO API2.0で推論処理をおこないます。
ソースのビルドと推論実行のために準備するものは以下のものです。
- C++ソースファイル
- Makefile
- IRファイル
- 画像ファイル

IRファイルと画像ファイルの準備は、AE2100のSDKマニュアル「AE2100_SDKManual(DeepLearning)_v1.5.pdf」の2.2章および2.3章を参照してください。
以下ではC++ソースコードとMakefileについて説明します。

## C++ソースコード
下記をクリックしてC++ソースコードをコピーし、sample.cppという名前のファイルを作成して書き込んでください。

<details>
<summary><b>ここをクリックしてsample.cppを表示</b></summary>
<div>

``` sample.cpp
#include <iostream>
#include <string>
#include <vector>
#include <opencv2/opencv.hpp>
#include <openvino/openvino.hpp>

const std::string labels_path = "inception_v3_2016_08_28_frozen.labels";
const std::string model_path = "inception_v3_2016_08_28_frozen.xml";
const std::string input_path = "car_1.bmp";
const std::string device_name = "CPU";

int main(int argc, char* argv[]) {

	// ラベルリストを読込み。
	std::vector<std::string> labels;
	FILE* fp = fopen(labels_path.c_str(), "r");
	if (fp == NULL) {
		std::cout << "labels is not exist" << std::endl;
		return -1;
	}
	char buf[256];

	while (fgets(buf, 256, fp) != NULL) {
		if (buf[strlen(buf) - 1] == '\n') {
			buf[strlen(buf) - 1] = '\0';
		}
		labels.push_back(buf);
	}

	//画像ファイルの読み込み
	cv::Mat image = cv::imread(input_path);
	if (image.empty()) {
		std::cout << "cv::imread() failed\n" << std::endl;
		return -1;
	}

	//Coreオブジェクト生成
	ov::Core ie;

	//IRモデル読み込み
	std::shared_ptr<ov::Model> network;
	try{
		network = ie.read_model(model_path);
	}
	catch(...){
		std::cout << "ie.read_model() failed\n" << std::endl;
		return -1;
	}

	//前処理をモデルに組み込み
	ov::preprocess::PrePostProcessor ppp(network);
	ppp.input().preprocess().resize(ov::preprocess::ResizeAlgorithm::RESIZE_LINEAR);
	ppp.input().tensor().set_shape({ 1, (unsigned long long)image.rows, (unsigned long long)image.cols, (unsigned long long)image.channels() });
	ppp.input().tensor().set_layout("NHWC");
	ppp.input().tensor().set_element_type(ov::element::u8);
	network = ppp.build();

	//モデルをデバイスに送信してコンパイル
	ov::CompiledModel compiled_model = ie.compile_model(network, device_name);

	//推論要求を送るため infer_requestオブジェクトの生成
	ov::InferRequest infer_request = compiled_model.create_infer_request();

	//推論データをモデルの入力にセット
	ov::Tensor input_tensor = ov::Tensor(ov::element::u8, { 1, (unsigned long long)image.rows, (unsigned long long)image.cols, (unsigned long long)image.channels() }, image.data);
	infer_request.set_input_tensor(input_tensor);

	//推論実行
	infer_request.infer();
	
	//推論結果の取り出し
	const ov::Tensor& output_tensor = infer_request.get_output_tensor();
	float* output = reinterpret_cast<float*>(output_tensor.data());
	ov::Shape output_shappe = output_tensor.get_shape();

	//推論結果の表示
	std::cout << "classid probability label\n------ ------ ------" << std::endl;
	std::vector<int> idx;	
	for (int i = 0; i < output_shappe[1]; i++) {
		idx.push_back(i);
	}

	std::sort(idx.begin(), idx.end(), [output](const int& left, const int& right) { return output[left] > output[right]; });

	for (size_t id = 0; id < 5; ++id) {
		std::cout << idx[id] << ": " << output[idx[id]] * 100 << "% " << ": " << labels[idx[id]] <<  std::endl;
	}
	return 0;
}
```

</div>
</details>

ソースコードについて解説していきます。
OpenCVを使って画像ファイルから画像データを読み込みます。

``` C
cv::Mat image = cv::imread(input_path);
```

Coreオブジェクトを生成し、IRモデルを読み込みます。

``` C
ov::Core ie;
network = ie.read_model(model_path);
```

前処理をモデルに組み込みます。
ここではModel Optimizerで指定したモデルの入力サイズ（1,299,299,3）と画像サイズ（縦749画素、横637画素）が異なるため、前処理にリサイズを設定しています。
また入力データとなるTensorにはOpenCVで読み込んだ画像のサイズ、入力データのレイアウトに”NHWC”、データ型に”u8”を指定しています。
”NHWC”と"u8"を指定する理由は、Tensor作成時にMat型のデータポインタを指定できるようにするためです。

``` C
ov::preprocess::PrePostProcessor ppp(network);
ppp.input().preprocess().resize(ov::preprocess::ResizeAlgorithm::RESIZE_LINEAR);
ppp.input().tensor().set_shape({ 1, (unsigned long long)image.rows, (unsigned long long)image.cols, (unsigned long long)image.channels() });
ppp.input().tensor().set_layout("NHWC");
ppp.input().tensor().set_element_type(ov::element::u8);
ppp.output().tensor().set_element_type(ov::element::f32);
network = ppp.build();
```

モデルをデバイスに送信してコンパイルをおこないます。

``` C
ov::CompiledModel compiled_model = ie.compile_model(network, device_name);
```

推論要求を送るため infer_requestオブジェクトの生成をおこないます。
``` C
ov::InferRequest infer_request = compiled_model.create_infer_request();
```

推論データをモデルの入力にセットします。
ここではOpenCVのMat型のデータの先頭アドレスを指定することで、ゼロコピーでTensor型に変換しています。

``` C
ov::Tensor input_tensor = ov::Tensor(ov::element::u8, { 1, (unsigned long long)image.rows, (unsigned long long)image.cols, (unsigned long long)image.channels() }, image.data);
infer_request.set_input_tensor(input_tensor);
```

推論を実行します。
``` C
infer_request.infer();
```

最後に推論結果を取得します。
``` C
const ov::Tensor& output_tensor = infer_request.get_output_tensor();
float* output = reinterpret_cast<float*>(output_tensor.data());
```

## Makefileの準備
下記をクリックしてコードをコピーし、Makefileという名前のファイルを作成して書き込んでください。
このMakefileでは、OpenVINOとOpenCVのヘッダーおよびライブラリをリンクしています。

<details>
<summary><b>ここをクリックしてMakefileを表示</b></summary>
<div>

``` Makefile
CC		=	g++
CFLAGS	=	-std=c++11 
CXFLAGS	=
LDFLAGS	=       -L/opt/intel/openvino/extras/opencv/lib \
                -lopencv_core -lopencv_imgcodecs -lopencv_highgui -lopencv_imgproc -lopencv_videoio -lopencv_video \
		-L/opt/intel/openvino/runtime/lib/intel64 \
		-lopenvino

LIBS	=
INCLUDE	=	-I /opt/intel/openvino/extras/opencv/include \
		-I /opt/intel/openvino/runtime/include/ie \
		-I /opt/intel/openvino/runtime/include

SRC_DIR	=	./
OBJ_DIR	=	./build
SOURCES	=	$(shell ls $(SRC_DIR)/*.cpp)
OBJS	=	$(subst $(SRC_DIR),$(OBJ_DIR), $(SOURCES:.cpp=.o))
TARGET	=	sample
DEPENDS	=	$(OBJS:.o=.d)

all: $(TARGET)

$(TARGET): $(OBJS) $(LIBS)
		$(CC) -o $@ $(OBJS) $(LIBS) $(LDFLAGS)

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
		@if [ ! -d $(OBJ_DIR) ]; \
			then echo "mkdir -p $(OBJ_DIR)"; mkdir -p $(OBJ_DIR); \
		fi
		$(CC) $(CFLAGS) $(INCLUDE) -O2 -o $@ -c $<

clean:
		$(RM) $(OBJS) $(TARGET) $(DEPENDS)

-include $(DEPENDS)

.PHONY: all clean

``` 
</div>
</details>



# ビルドと実行
gccを導入した開発環境でc++コードのビルドをおこないます。
開発環境の構築は、AE2100のSDKマニュアル「AE2100_SDKManual(DeepLearning)_v1.5.pdf」の2.1章を参照してください。

開発環境のカレントディレクトリに先ほどのsample.cppとMakefileを置きmakeコマンドを実行します。

```
# make
```
makeが成功すると、”sample”という実行ファイルが出力されます。

次に、AE2100のSDKマニュアル「AE2100_SDKManual(DeepLearning)_v1.5.pdf」の2.2章および2.3章を参照し、画像分類モデルと画像ファイル用意します。
なお、ここで用意するモデル（.xml、.bin）はImageNetデータを事前学習した1000種類の画像分類をおこなうモデルです。
また”inception_v3_2016_08_28_frozen.labels” は inception_v3_2016_08_28_frozen.pb.tar に含まれるimagenet_slim_labels.txt のファイル名を置換したものです。
以下のファイルをAE2100のOpenVINOコンテナ内にコピーします。
- sample
- inception_v3_2016_08_28_frozen.xml
- inception_v3_2016_08_28_frozen.bin
- inception_v3_2016_08_28_frozen.labels
- car_1.bmp

AE2100のOpenVINOコンテナ内でプログラムの実行をおこないます。
```
# source /opt/intel/openvino/setupvars.sh
# ./sample
```
実行が成功すると以下の表示が出力されます。

```
classid probability label
------ ------ ------
657: 53.7629% : minivan
437: 3.15917% : beach wagon
818: 2.32508% : sports car
469: 1.48971% : cab
705: 0.848346% : parking meter
```
表示は左から”ラベル番号”、”スコア”、”ラベル名”　の順です。1000種類のラベルの内、上位のスコアの5ラベルを出力しています。
トップスコアのラベル名は”minivan”を示しており、車両が写っている画像を入力しているため妥当な結果と言えます。


# まとめ
今回はOpenVINO API2.0を使ったC++のシンプルなプログラムを作成し実行してみました。
OpenVINO API2.0は、これまでのAPIより使いやすくなっていますので、皆さんぜひご活用ください。

# 参考
OpenVINO公式ドキュメント
- [openvino_2_0_transition_guide](https://docs.openvino.ai/latest/openvino_2_0_transition_guide.html)