AIを始めよう！PythonでOpenVINOの仕組みを理解する

Pythonで簡単なコードを書いて、OpenVINOの使い方を理解しましょう。
ただし、Python版のOpenVINOはベータ版ですので、今後仕様が変わる可能性があります。

※Tool kit R4の変更点
　IENetworkオブジェクトで入力Blobの形状を取得する際は、プロパティ'shape'を明示する。

#OpenVINOの構成
OpenVINOは、推論エンジンと、入力画像を格納するインプットBlob、推論結果を格納するアウトプットBlobから構成されます。また、推論エンジンには、IR（中間表現）形式の学習済みモデルをあらかじめロードしておきます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/137772/c0af32b8-2e54-9ed7-ac7c-08146e63b663.png)
推論エンジンを実行するハードウェアは、CPU、GPU、FPGA、MYRIAD(Movidius)、HETERO(複数の組み合わせ)から選ぶことができます。それぞれのハードウェア向けに最適化された推論エンジンのプラグインが用意されています。なお、プログラムから推論に使うハードウェアの違いを意識する必要はありません。

# 最小コードで仕組みを知る
OpenVINOは複雑な処理を、非常に単純なAPIの中に隠ぺいしていることから、簡潔にプログラムを記述することができます。Python APIを使用して、顔の検出を行うプログラムを書いてみましょう。プログラムで使うAPIは以下の３つのクラスです。

IENetworkクラス：IRを読み込み、学習済みネットワークモデルの情報を保持する。
IEPluginクラス：プラグインの初期化と設定を行う。
ExecutableNetworkクラス：IRを展開し、プラグインで指定したハードウェアにより推論を実行する。

クラスの詳細は以下のURLで確認できます。
[Overview of Inference Engine Python API](https://software.intel.com/en-us/articles/OpenVINO-InferEngine#overview-of-inference-engine-python-api)

では、実際にコードの中身を見ていきましょう。
このコードは、ツールキットに含まれるサンプルプログラムを参考に、OpenVINOの動作をできるだけ簡単に理解できるよう、簡潔に書き直しています。

プログラムは以下のステップから構成されます。
＜前半＞
- STEP-1 モジュールのインポート
- STEP-2 IRの読み込み
- STEP-3 プラグインの初期化
- STEP-4 モデルの入出力パラメータを取得
＜後半＞
- STEP-5 USBカメラの準備とキャプチャ
- STEP-6 キャプチャ画像を推論エンジンのフォーマットに変換
- STEP-7 推論の実行
- STEP-8 推論結果からキャプチャ画像に顔の枠を描画
- STEP-9 画像の表示
- STEP-10 終了処理

以下のソースコードを確認して、プログラム全体を把握しましょう。前半は推論エンジンの初期化などを行っています。後半のwhileループは、USBカメラから画像を入力し、顔検出の推論を行い、画像を加工し、画像を表示します。

```simple.py
#!/usr/bin/env python
#STEP-1
import sys
import cv2
from openvino.inference_engine import IENetwork, IEPlugin

#STEP-2
model_xml='/opt/intel/computer_vision_sdk/deployment_tools/intel_models/face-detection-adas-0001/FP32/face-detection-adas-0001.xml'
model_bin='/opt/intel/computer_vision_sdk/deployment_tools/intel_models/face-detection-adas-0001/FP32/face-detection-adas-0001.bin'
net = IENetwork.from_ir(model=model_xml, weights=model_bin)

#STEP-3
plugin = IEPlugin(device='GPU', plugin_dirs=None)
exec_net = plugin.load(network=net, num_requests=1)

#STEP-4
input_blob = next(iter(net.inputs))  #input_blob = 'data'
out_blob   = next(iter(net.outputs)) #out_blob   = 'detection_out'
model_n, model_c, model_h, model_w = net.inputs[input_blob].sharp #Tool kit R4
#model_n, model_c, model_h, model_w = net.inputs[input_blob] #Tool kit R3 
#model_n, model_c, model_h, model_w = 1, 3, 384, 672

del net

#STEP-5
cap = cv2.VideoCapture(0)
while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break

    #STEP-6
    cap_w = cap.get(3)
    cap_h = cap.get(4)
    in_frame = cv2.resize(frame, (model_w, model_h))
    in_frame = in_frame.transpose((2, 0, 1))  # Change data layout from HWC to CHW
    in_frame = in_frame.reshape((model_n, model_c, model_h, model_w))

    #STEP-7
    exec_net.start_async(request_id=0, inputs={input_blob: in_frame})

    if exec_net.requests[0].wait(-1) == 0:
        res = exec_net.requests[0].outputs[out_blob]

        #STEP-8
        for obj in res[0][0]:
            if obj[2] > 0.5:
                xmin = int(obj[3] * cap_w)
                ymin = int(obj[4] * cap_h)
                xmax = int(obj[5] * cap_w)
                ymax = int(obj[6] * cap_h)
                class_id = int(obj[1])
                # Draw box and label\class_id
                color = (255, 0, 0)
                cv2.rectangle(frame, (xmin, ymin), (xmax, ymax), color, 2)
                cv2.putText(frame, class_id + ' ' + str(round(obj[2] * 100, 1)) + ' %', (xmin, ymin - 7), cv2.FONT_HERSHEY_COMPLEX, 0.6, color, 1)

    #STEP-9
    cv2.imshow("Detection Results", frame)
    key = cv2.waitKey(1)
    if key == 27:
        break

#STEP-10
cv2.destroyAllWindows()
del exec_net
del plugin
```
全体を俯瞰したところで、ステップごとに何を行っているのか、詳細にみていきましょう。
##STEP-1 モジュールのインポート
OpenCVとOpenVINOのモジュールを読み込みます。
IENetworkとIEPluginの２つのクラスを明示的に使いますが、IEPluginによりExecutableNetworkクラスのインスタンスが生成されますので、合計３つのクラスを使うことになります。

```simple.py
#STEP-1
import sys
import cv2
from openvino.inference_engine import IENetwork, IEPlugin
```

##STEP-2 IRの読み込み
IRはネットワークモデルの構成を定義したXMLファイルと、学習済みネットワークモデルが保存されているbinファイルに分かれています。２つのファイルを読み込みIENetworkクラスのインスタンスnetを生成します。

```simple.py
#STEP-2
model_xml='/opt/intel/computer_vision_sdk/deployment_tools/intel_models/face-detection-adas-0001/FP32/face-detection-adas-0001.xml'
model_bin='/opt/intel/computer_vision_sdk/deployment_tools/intel_models/face-detection-adas-0001/FP32/face-detection-adas-0001.bin'
net = IENetwork.from_ir(model=model_xml, weights=model_bin)
```

##STEP-3 プラグインを指定して推論エンジンを初期化
IEPluginクラスに、デバイスとしてGPUを指定し、推論エンジンプラグインのインスタンスを生成します。生成したプラグインに、先ほどIRを展開したnetインスタンスをロードします。ロードすることでexec_netには、ExecutableNetworkクラスのインスタンスが格納され、推論の準備が完了します。

```simple.py
#STEP-3
plugin = IEPlugin(device='GPU', plugin_dirs=None)
exec_net = plugin.load(network=net, num_requests=1)
```

##STEP-4 モデルの入出力パラメータを取得
インプットBlobとアウトプットBlobには、IRのXMLファイルによって名前が定義されています。ここで使う顔検出モデルでは、インプットBlobは"data"、アウトプットBlobは"detection_out"という名前がつけられています。
直接名前を指定してもよいのですが、モデルが変わるとBlobの名前も変わるので、netインスタンスから取り出して設定すると便利です。イテレーターを使って取り出すので、少しわかりずらい表記です。
次に、インプットBlobのフォーマットを読み込みます。インプットBlobには画像を格納するので、そのフォーマットを取り出すことになります。こちらも、IRのXMLファイルに定義されていますが、netインスタンスから取り出すことができます。今回使用する顔検出モデルでは、以下のフォーマットとなります。

|インプットBlob|変数|値|
|:--:|:--:|:--:|
|バッチ数（画像数）|model_n|1|
|色数（チャンネル）|model_c|3|
|画像の高さ|model_h|384|
|画像の幅|model_w|762|

つまり、入力は１枚の画像で、B,G,Rそれぞれの画像を用意し、画像のサイズは高さ３８４ピクセル、幅７６２ピクセルということになります。入力画像は、この構成に従わなければなりません。視覚化すると以下のようになります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/137772/d87c4375-3cb2-4c59-1f6b-1f3a6c029a27.png)

また、netインスタンスは今後使用しないのでdelを使い削除しています。

```simple.py
#STEP-4
input_blob = next(iter(net.inputs))  #input_blob = 'data'
out_blob   = next(iter(net.outputs)) #out_blob   = 'detection_out'
model_n, model_c, model_h, model_w = net.inputs[input_blob].shape  #model_n, model_c, model_h, model_w = 1, 3, 384, 672

del net
```

※仕様変更により、Tool KitのR4ではインプットBlobの形状を取り出すプロパティを明示する必要があります。

##STEP-5 USBカメラの準備とキャプチャ
OpenCVを使い、USBカメラから画像をキャプチャします。キャプチャに失敗した場合はwhileループを終了します。正常にキャプチャできたなら、画像の幅と、高さを取り出します。

```simple.py
#STEP-5
cap = cv2.VideoCapture(0)
while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break
    cap_w = cap.get(3)
    cap_h = cap.get(4)
```

##STEP-6 キャプチャ画像の前処理
画像がキャプチャできましたので、インプットBlobのフォーマットに変換します。
cv2.resizeを使いキャプチャした画像のサイズをインプットBlobの幅と高さに変換します。
次に、画像データの格納フォーマットの変換を行います。今回使用するモデルでは、青成分の画像、緑成分の画像、赤成分の画像に分けて入力する必要があります。キャプチャした画像のフォーマットは、１ピクセルごとにRGBそれぞれの値が順番に並んでいます。つまり、h x w x cの３次元配列です。この配列をc x h x wに変換します。変換はtranspose((2, 0, 1))で行います。つまり(h, w, c)を(0, 1, 2)とした場合、(2, 0, 1)と並び変えるので(c, h, w)への変換となります。並び変えたら、reshapeでチャンネル数（色数）と高さと幅を与えます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/137772/1ce51a71-1592-57f5-d9bc-aad0892ff3b5.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/137772/c5ba460f-ef38-80ad-8db5-207d3d2a8ecb.png)

```simple.py
    #STEP-6
    in_frame = cv2.resize(frame, (model_w, model_h))
    in_frame = in_frame.transpose((2, 0, 1))  # Change data layout from HWC to CHW
    in_frame = in_frame.reshape((model_n, model_c, model_h, model_w))
```

##STEP-7 推論の実行
さて、インプットBlobのフォーマットに合わせて画像が変換できましたので、推論を行います。コードではExecutableNetworkクラスのstart_asyncメソッドを使って、非同期で顔検出の推論を実行しています。
waitで終了を待ち、推論結果が格納されているアウトプットBlobをresに格納します。

```simple.py
    #STEP-7
    exec_net.start_async(request_id=0, inputs={input_blob: in_frame})

    if exec_net.requests[0].wait(-1) == 0:
        res = exec_net.requests[0].outputs[out_blob]
```

##STEP-8 推論結果からキャプチャ画像に顔の枠を描画
推論が完了すると、アウトプットBlobには、顔の領域の座標と検出精度が格納されます。一度に複数の顔を検出しますので、イテレートを使って一人ずつ情報を取り出します。検出した顔にはIDがつけられ、検出精度と顔を覆う矩形領域の座標が格納されています。コードでは、検出精度が50%以上の場合のみ、描画処理を実行しています。
また、アウトプットBlobから得られた座標は比率であらわされているため、元の画像のサイズを掛けて実際の座標に変換します。コードでは顔の領域の右上の座標(xmin, ymin)と左下の座標(xmax, ymax)を計算しています。その後、顔を囲うように矩形を描画し、IDと検出精度を描画するコードが続きます。

```simple.py
        #STEP-8
        for obj in res[0][0]:
            if obj[2] > 0.5:
                xmin = int(obj[3] * cap_w)
                ymin = int(obj[4] * cap_h)
                xmax = int(obj[5] * cap_w)
                ymax = int(obj[6] * cap_h)
                class_id = int(obj[1])
                # Draw box and label\class_id
                color = (255, 0, 0)
                cv2.rectangle(frame, (xmin, ymin), (xmax, ymax), color, 2)
                cv2.putText(frame, class_id + ' ' + str(round(obj[2] * 100, 1)) + ' %', (xmin, ymin - 7), cv2.FONT_HERSHEY_COMPLEX, 0.6, color, 1)
```

##STEP-9 画像の表示
さて、画像が加工できたので、imshowで表示します。その後、エスケープキー(アスキーコード27)を押すことでループを終了するためのコードが続きます。

```simple.py
    #STEP-9
    cv2.imshow("Detection Results", frame)
    key = cv2.waitKey(1)
    if key == 27:
        break
```

##STEP-10 終了処理
ループが終了したところで、終了処理として、キャプチャ画像のウィンドウを閉じて、インスタンスを削除しています。

```simple.py
#STEP-10
cv2.destroyAllWindows()
del exec_net
del plugin
```

以上で、コードの説明は終わりです。いかがでしたか。OpenVINOを使うと推論処理が非常に簡潔に記述できることが理解できたと思います。