PythonでIntel NCStick2

PythonでIntel NCStick2を操作する

事前準備

何はなくともPythonが必要。
実際には3.7.4を使用して動作確認。

Pythonはpyenvでインストールしておくのが便利。インストール方法は pyenvのインストール を参照。

[!NOTE] pyenvとの相性が悪いらしく、pyenvで3.7.xがインストールされていると (python3.7コマンド自体は存在するので;実行自体はエラーで返ってくるが)、 setupvars.shでPYTHONPATHにpython3.7用pathが設定されてしまう。 プログラム中でsys.path.appendでpathを追加しても、sys.pathの末尾に追加されてしまい、 先に設定された3.7用モジュールがインポートされてしまう。 もし、他のバージョンのpythonで実行する場合はsys.path.appendではなく、sys.path.insertで pathの先頭に追加するようにしないといけないようだ(試してないけど)。

sys.path.append(path)
    
sys.path.insert(0, path)

ということで、下のプログラムソースのsys.path.appendの行は不要。

モジュールのインストール

必要なモジュール類をインストールする。

sudo apt install libatlas-base-dev
sudo apt install libgfortran-6-dev
pip install numpy
pip install opencv-python

ワークディレクトリ

色々共通で使いたいサイズのでっかいファイルがあるので、ワークディレクトリを以下の構成で作成しておく。
以降のopenVINO関連の投稿もこのディレクトリ構成を使うようにする。

# C++プログラム湯尾
mkdir -p /work/NCS2/c++
# pythonプログラム用
mkdir -p /work/NCS2/python
# モデルデータ用
mkdir -p /work/NCS2/openvino_models/FP16
mkdir -p /work/NCS2/openvino_models/FP32

参考

パクった 参考にしたのは以下のあたり
解説や実行方法などはこっちを参照してちょ(相変わらずの他力本願ぶり…))

顔検出+感情分類の参照元はカメラキャプチャした画像を処理しているけど、手元に適当なカメラがなかったので、JPEGファイルで実行するようにしてある(こっちの方が余計な処理なくて分かりやすいかな、と思ったのもある)。

感情分類

感情分類のプログラムを試す。

準備

モデルデータをダウンロードしておく。

models_dir=/work/NCS2/openvino_models/FP16
DL_URL1=https://download.01.org/opencv/2019/open_model_zoo/R1/models_bin/emotions-recognition-retail-0003/FP16/emotions-recognition-retail-0003
wget ${DL_URL1}.bin -P ${models_dir}
wget ${DL_URL1}.xml -P ${models_dir}

使用する顔画像(顔部分のみ切り出し)をPHOTO/face.jpgとして保存しておく。

プログラム

試したソースはこちら。
PCのubuntuで実行するための処理も追加済み。

# 感情分析

# モジュール読み込み 
import sys
import os
import platform
from argparse import ArgumentParser, SUPPRESS
import cv2
import numpy as np
# sys.path.append('/opt/intel/openvino/python/python3.7')
from openvino.inference_engine import IENetwork, IEPlugin

# =======================================================
# 使用する画像ファイル名
image_filename = 'PHOTO/face.jpg'

# 使用するモデルファイルのディレクトリ
models_dir = '/work/NCS2/openvino_models/FP16/'

# 各感情の文字列をリスト化 
list_emotion    = ['neutral', 'happy', 'sad',    'surprise', 'anger']
list_emotion_jp = ['無表情',  '幸福',  '悲しみ', '驚き',     '怒り']

# =======================================================
def build_argparser():
    parser = ArgumentParser(add_help=False)
    args = parser.add_argument_group('Options')
    args.add_argument('-h', '--help', action='help', default=SUPPRESS, help='Show this help message and exit.')
    args.add_argument("-i", "--input", 
                        help=f"Optional. Path to a image/video file. default is {image_filename}", 
                        required=False, 
                        default=image_filename, 
                        type=str)
    args.add_argument("-m", "--model_dir", 
                        help=f"Optional. Path to an .xml file with a trained model.default is {image_filename}", 
                        required=False, 
                        default=models_dir, 
                        type=str)
    args.add_argument("-d", "--device",
                        help="Optional. Specify the target device to infer on; CPU, GPU, FPGA, HDDL or MYRIAD is"
                           " acceptable. The sample will look for a suitable plugin for device specified. "
                           "Default value is CPU", 
                        default="CPU", 
                        type=str)
    return parser

# =======================================================
# オプション指定値取得
args = build_argparser().parse_args()

image_filename = args.input
models_dir = args.model_dir
print(f' image_filename = {image_filename}\n models_dir={models_dir}')

# ターゲットデバイスの指定 
plugin = IEPlugin(device=args.device)
if args.device == "CPU" and  platform.processor() == 'x86_64' :
    # avx2対応extensionの追加
    plugin.add_cpu_extension("libcpu_extension_avx2.so")

# モデルの読み込み(感情分類) 
net_emotion = IENetwork(model=models_dir+'emotions-recognition-retail-0003.xml', weights=models_dir+'emotions-recognition-retail-0003.bin')
exec_net_emotion = plugin.load(network=net_emotion)

# 入力画像読み込み 
frame = cv2.imread(image_filename)

# 入力データフォーマットへ変換 
img = cv2.resize(frame, (64, 64)) # サイズ変更 
img = img.transpose((2, 0, 1))    # HWC > CHW 
img = np.expand_dims(img, axis=0) # 次元合せ 

# 推論実行 
out = exec_net_emotion.infer(inputs={'data': img})

# 結果取り出し
out = out['prob_emotion']
out = np.squeeze(out) #不要な次元の削減 

# 結果の最大値を持つインデックスを取得
index_max = np.argmax(out)

# 結果表示
print('\n---------------------\n結果')
print(f'{index_max}   {list_emotion[index_max]}({list_emotion_jp[index_max]}): {out[index_max]}')
# 結果の詳細表示
print('\n---------------------\n詳細')
print(f'スコア:{out}')
for i in range(out.size) :
    print(f'{i}   {list_emotion[i]}({list_emotion_jp[i]}): {out[i]}')

プログラムの実行

# ヘルプの表示
python emotion.py -h

# PCのCPUを使用した場合(RasPiのCPU使用だとエラーになる)
python emotion.py [-i jpegファイル]

# NCS2を使用した場合
python emotion.py -d MYRIAD [-i jpegファイル]

顔検出

顔検出のプログラムを試す。
結果画像を表示するので、X環境での実行必須。

準備

モデルデータをダウンロードしておく。

models_dir=/work/NCS2/openvino_models/FP16
DL_URL1=https://download.01.org/opencv/2019/open_model_zoo/R1/models_bin/face-detection-retail-0004/FP16/face-detection-retail-0004
wget ${DL_URL1}.bin -P ${models_dir}
wget ${DL_URL1}.xml -P ${models_dir}

使用する画像をPHOTO/photo.jpgとして保存しておく。
または実行時にコマンドラインパラメータとしてファイル名を指定。

プログラム

試したソースはこちら。

# 顔検出

# モジュール読み込み 
import sys
import os
import platform
from argparse import ArgumentParser, SUPPRESS
import cv2
import numpy as np
# sys.path.append('/opt/intel/openvino/python/python3.7')
from openvino.inference_engine import IENetwork, IEPlugin

# =======================================================
# 使用する画像ファイル名
image_filename = 'PHOTO/photo.jpg'

# 使用するモデルファイルのディレクトリ
models_dir = '/work/NCS2/openvino_models/FP16/'

# 各感情の文字列をリスト化 
list_emotion    = ['neutral', 'happy', 'sad',    'surprise', 'anger']
list_emotion_jp = ['無表情',  '幸福',  '悲しみ', '驚き',     '怒り']

# =======================================================
def build_argparser():
    parser = ArgumentParser(add_help=False)
    args = parser.add_argument_group('Options')
    args.add_argument('-h', '--help', action='help', default=SUPPRESS, help='Show this help message and exit.')
    args.add_argument("-i", "--input", 
                        help=f"Optional. Path to a image/video file. default is {image_filename}", 
                        required=False, 
                        default=image_filename, 
                        type=str)
    args.add_argument("-m", "--model_dir", 
                        help=f"Optional. Path to an .xml file with a trained model.default is {image_filename}", 
                        required=False, 
                        default=models_dir, 
                        type=str)
    args.add_argument("-d", "--device",
                        help="Optional. Specify the target device to infer on; CPU, GPU, FPGA, HDDL or MYRIAD is"
                           " acceptable. The sample will look for a suitable plugin for device specified. "
                           "Default value is CPU", 
                        default="CPU", 
                        type=str)
    return parser

# =======================================================
# アスペクト比固定でリサイズする関数
def scale_to_width(img, width):
    scale = width / img.shape[1]
    return cv2.resize(img, dsize=None, fx=scale, fy=scale)

# =======================================================
# オプション指定値取得
args = build_argparser().parse_args()

image_filename = args.input
models_dir = args.model_dir
print(f' image_filename = {image_filename}\n models_dir={models_dir}')

# ターゲットデバイスの指定 
plugin = IEPlugin(device=args.device)
if args.device == "CPU" and  platform.processor() == 'x86_64' :
    # avx2対応extensionの追加
    plugin.add_cpu_extension("libcpu_extension_avx2.so")

# モデルの読み込み(顔検出) 
net_detect = IENetwork(model=models_dir+'face-detection-retail-0004.xml', weights=models_dir+'face-detection-retail-0004.bin')
exec_net_detect = plugin.load(network=net_detect)

# 入力画像読み込み 
frame = cv2.imread(image_filename)

# 入力データフォーマットへ変換 
img = cv2.resize(frame, (300, 300)) # サイズ変更 
img = img.transpose((2, 0, 1))      # HWC > CHW 
img = np.expand_dims(img, axis=0)   # 次元合せ 

# 推論実行 
out = exec_net_detect.infer(inputs={'data': img})

# 出力から必要なデータのみ取り出し 
out = out['detection_out']
out = np.squeeze(out) #サイズ1の次元を全て削除 

# アスペクト比固定で表示用画像をリサイズ
frame = scale_to_width(frame, 640)

# 検出されたすべての顔領域に対して1つずつ処理 
for detection in out:
    # conf値の取得 
    confidence = float(detection[2])

    # conf値が0.5より小さい場合はスキップ
    if confidence < 0.5:
        continue

    # バウンディングボックス座標を入力画像のスケールに変換 
    xmin = int(detection[3] * frame.shape[1])
    ymin = int(detection[4] * frame.shape[0])
    xmax = int(detection[5] * frame.shape[1])
    ymax = int(detection[6] * frame.shape[0])

    # 顔検出領域はカメラ範囲内に補正する。
    if xmin < 0:
        xmin = 0
    if ymin < 0:
        ymin = 0
    if xmax > frame.shape[1]:
        xmax = frame.shape[1]
    if ymax > frame.shape[0]:
        ymax = frame.shape[0]

    # 結果表示
    cv2.rectangle(frame, (xmin, ymin), (xmax, ymax), color = (120, 90, 0), thickness = 2)

# 画像表示 
cv2.imshow('frame', frame)

# 何らかのキーが押されたら終了 
cv2.waitKey(0)
cv2.destroyAllWindows()

プログラムの実行

# ヘルプの表示
python face_detect.py -h

# PCのCPUを使用した場合(RasPiのCPU使用だとエラーになる)
python face_detect.py [-i jpegファイル]

# NCS2を使用した場合
python face_detect.py -d MYRIAD [-i jpegファイル]

顔検出+感情分類

上の2つを合体させ、顔検出した結果に感情分類を実行する。
結果画像を表示するので、X環境での実行必須。

準備

モデルデータは上の2つをそのまま使用。

使用する画像をPHOTO/photo.jpgとして保存しておく。
または実行時にコマンドラインパラメータとしてファイル名を指定。

プログラム

試したソースはこちら。

# 顔検出 + 感情分析

# モジュール読み込み 
import sys
import os
import platform
from argparse import ArgumentParser, SUPPRESS
import cv2
import numpy as np
# sys.path.append('/opt/intel/openvino/python/python3.7')
from openvino.inference_engine import IENetwork, IEPlugin

# =======================================================
# 使用する画像ファイル名
image_filename = 'PHOTO/photo.jpg'

# 使用するモデルファイルのディレクトリ
models_dir = '/work/NCS2/openvino_models/FP16/'

# 各感情の文字列をリスト化 
# 日本語表示にはPillow使うとかしないといけないので、英語で。
list_emotion    = ['neutral', 'happy', 'sad',    'surprise', 'anger']
list_emotion_jp = ['無表情',  '幸福',  '悲しみ', '驚き',     '怒り']

# =======================================================
def build_argparser():
    parser = ArgumentParser(add_help=False)
    args = parser.add_argument_group('Options')
    args.add_argument('-h', '--help', action='help', default=SUPPRESS, help='Show this help message and exit.')
    args.add_argument("-i", "--input", 
                        help=f"Optional. Path to a image/video file. default is {image_filename}", 
                        required=False, 
                        default=image_filename, 
                        type=str)
    args.add_argument("-m", "--model_dir", 
                        help=f"Optional. Path to an .xml file with a trained model.default is {image_filename}", 
                        required=False, 
                        default=models_dir, 
                        type=str)
    args.add_argument("-d", "--device",
                        help="Optional. Specify the target device to infer on; CPU, GPU, FPGA, HDDL or MYRIAD is"
                           " acceptable. The sample will look for a suitable plugin for device specified. "
                           "Default value is CPU", 
                        default="CPU", 
                        type=str)
    return parser

# =======================================================
# アスペクト比固定でリサイズする関数
def scale_to_width(img, width):
    scale = width / img.shape[1]
    return cv2.resize(img, dsize=None, fx=scale, fy=scale)

# =======================================================
# バウンティングボックスとラベルを表示する関数
def disp_Bounting_box(img, xmin, ymin, xmax, ymax, text, color) :
    # ラベル領域サイズ
    LABEL_W = len(text) * 12    # 等幅フォントじゃないので正確ではないけど、大体こんな感じ
    LABEL_H = 14                # こっちもトライアンドエラーで微調整
    # 画像サイズ
    IMAGE_W = img.shape[1]
    IMAGE_H = img.shape[0]
    
    # バウンディングボックス表示 
    cv2.rectangle(img, (xmin, ymin), (xmax, ymax), color, thickness = 2)
    
    # ラベル表示位置(X)
    if (xmin + LABEL_W) > IMAGE_W :
        # 画面からはみ出さないように
        label_x = IMAGE_W - LABEL_W
    else :
        label_x = xmin
    
    # ラベル表示位置(Y)
    if (ymin - LABEL_H) < 0 :
        # 画面からはみ出さないように
        label_y = 0
    else :
        label_y = ymin - LABEL_H
    
    # 文字列背景描画(塗りつぶし)
    cv2.rectangle(img, (label_x, label_y), (label_x + LABEL_W, label_y + LABEL_H), color, thickness = -1)
    
    # 文字列描画(座標は文字の左下)
    cv2.putText(img, text, (label_x, label_y + LABEL_H), cv2.FONT_HERSHEY_COMPLEX_SMALL, 0.8, (255, 255, 255), 1)

# =======================================================
# オプション指定値取得
args = build_argparser().parse_args()

image_filename = args.input
models_dir = args.model_dir
print(f' image_filename = {image_filename}\n models_dir={models_dir}')

# ターゲットデバイスの指定 
plugin = IEPlugin(device=args.device)
if args.device == "CPU" and  platform.processor() == 'x86_64' :
    # avx2対応extensionの追加
    plugin.add_cpu_extension("libcpu_extension_avx2.so")

# モデルの読み込み(顔検出) 
net_detect = IENetwork(model=models_dir+'face-detection-retail-0004.xml', weights=models_dir+'face-detection-retail-0004.bin')
exec_net_detect = plugin.load(network=net_detect)

# モデルの読み込み(感情分類) 
net_emotion = IENetwork(model=models_dir+'emotions-recognition-retail-0003.xml', weights=models_dir+'emotions-recognition-retail-0003.bin')
exec_net_emotion = plugin.load(network=net_emotion)

# 入力画像読み込み 
frame = cv2.imread(image_filename)

# 入力データフォーマットへ変換 
img = cv2.resize(frame, (300, 300)) # サイズ変更 
img = img.transpose((2, 0, 1))      # HWC > CHW 
img = np.expand_dims(img, axis=0)   # 次元合せ 

# 推論実行 
out = exec_net_detect.infer(inputs={'data': img})

# 出力から必要なデータのみ取り出し 
out = out['detection_out']
out = np.squeeze(out) #サイズ1の次元を全て削除 

# アスペクト比固定で表示用画像をリサイズ
frame = scale_to_width(frame, 640)

# 検出されたすべての顔領域に対して1つずつ処理 
for detection in out:
    # conf値の取得 
    confidence = float(detection[2])
    
    # conf値が0.5より小さい場合はスキップ
    if confidence < 0.5:
        continue
    
    # バウンディングボックス座標を入力画像のスケールに変換 
    xmin = int(detection[3] * frame.shape[1])
    ymin = int(detection[4] * frame.shape[0])
    xmax = int(detection[5] * frame.shape[1])
    ymax = int(detection[6] * frame.shape[0])
    
    # 顔検出領域はカメラ範囲内に補正する。特にminは補正しないとエラーになる
    if xmin < 0:
        xmin = 0
    if ymin < 0:
        ymin = 0
    if xmax > frame.shape[1]:
        xmax = frame.shape[1]
    if ymax > frame.shape[0]:
        ymax = frame.shape[0]
    
    # 顔領域のみ切り出し 
    frame_face = frame[ ymin:ymax, xmin:xmax ]
    
    # 入力データフォーマットへ変換 
    img_face = cv2.resize(frame_face, (64, 64))   # サイズ変更 
    img_face = img_face.transpose((2, 0, 1))    # HWC > CHW 
    img_face = np.expand_dims(img_face, axis=0) # 次元合せ 
    
    # 推論実行 
    out = exec_net_emotion.infer(inputs={'data': img_face})
    
    # 出力から必要なデータのみ取り出し 
    out = out['prob_emotion']
    out = np.squeeze(out) #不要な次元の削減 
    
    # 出力値が最大のインデックスを得る 
    index_max = np.argmax(out)
    
    # 結果表示
    # print(list_emotion[index_max])
    disp_Bounting_box(frame, xmin, ymin, xmax, ymax, list_emotion[index_max], (120, 90, 0))

# 画像表示 
cv2.imshow('frame', frame)

# 何らかのキーが押されたら終了 
cv2.waitKey(0)
cv2.destroyAllWindows()

プログラムの実行

# ヘルプの表示
python face_emotion.py -h

# PCのCPUを使用した場合(RasPiのCPU使用だとエラーになる)
python face_emotion.py [-i jpegファイル]

# NCS2を使用した場合
python face_emotion.py -d MYRIAD [-i jpegファイル]

考察

基本パターンはこんな感じ。

# モジュールのロード
from openvino.inference_engine import IENetwork, IEPlugin

# 初期化
plugin = IEPlugin(device="MYRIAD")

# モデルの読み込み
net = IENetwork(model='xmlファイル', weights='binファイル')
exec_net = plugin.load(network=net)

# 推論実行 
# imgは入力データ、Numpy配列 ndarray型
out = exec_net.infer(inputs={'data': img})

# outが出力データ、Numpy配列 ndarray型

入出力データの並びは読み込んだモデルに依存する。
入力データのサイズやRGB並び順などを合わせる。
(モデルに入力する際にリサイズすれば、元の画像ファイルのサイズは任意で良い。)
出力データはそのままでは「何のこっちゃ?」なので、きちんと整形してやる必要がある。

ここに学習済みデータが色々登録されているらしい。
https://docs.openvinotoolkit.org/2019_R1/_docs_Pre_Trained_Models.html

TensorflowやCaffeで作成した学習済みデータを変換することもできるらしいが、やり方はこれから調査。

参考情報

openVINO toolkit リリースノート
openVINO toolkit リポジトリ