「がんばる人のための画像検査機 presented by shinmura0」をOpenVINOで異次元のスピードにパワーアップして異常検出 (CPUのみ 又は Intel HD Graphics 615) その1

Keras-OneClassAnomalyDetection GitHub stars


はじめに

@shinmura0 さんの 「がんばる人のための画像検査機」 を30倍以上にスピードアップしました。

なお、 Githubへの引用 は、ご本人様の承諾を得たうえで行っております。

shinmura0 さんは理論と実装、 私はパフォーマンスチューニング、 と割りきっております。

かくして、スピード狂のオジサンは更なる推論スピードの向上を求めて旅立つのであった。。。


  • デバイス


    • 【前】 RaspberryPi3 (armv7l) ---> 【後】 LattePanda Alpha (Core m3)



  • 推論用フレームワーク


    • 【前】 Keras (バックエンド:Tensorflow) ---> 【後】 OpenVINO




環境


  • LattePanda Alpha (シングルボードコンピュータ) + Ubuntu 16.04

  • USB Camera (Playstationeye)

  • OpenVINO R5 2018.5.445

  • Numpy 1.15.4

  • scikit-learn 0.19.2+


いきなり結果

もはや、カメラの撮影スピードが推論スピードに負けてしまいます。

320x240 (QVGA) の解像度では、カメラの撮影スピードの上限が 180 FPS です。

640x480 (VGA) の解像度では、カメラの撮影スピードの上限が 60 FPS です。

1240x780 (HD) 60 FPS 対応のカメラも所有していますが、撮影が面倒だったため今回はテストをしていません。

ただ、高解像度でもかなり高速に推論できることでしょう。

200 FPS 近い数字が見えます。

なお、残念ながら、RaspberryPi3 + NCS2 の組み合わせでは、APIのバグで正常に動作しませんでした。


320x240_CPU_Core_m3

$ python3 main_openvino.py -cfps 150 -cwd 320 -cht 240


Youtube: https://youtu.be/8GhYysCBhq4

01.gif


640x480_CPU_Core_m3

$ python3 main_openvino.py -cfps 60 -cwd 640 -cht 480


Youtube: https://youtu.be/FplYoZfHx3A

02.gif


320x240_GPU_Intel_HD_Graphics_615

$ python3 main_openvino.py -cfps 150 -cwd 320 -cht 240 -d GPU


Youtube: https://youtu.be/d5KTGv7K7II

03.gif


640x480_GPU_Intel_HD_Graphics_615

$ python3 main_openvino.py -cfps 60 -cwd 640 -cht 480 -d GPU


Youtube: https://youtu.be/hg1kmPA3TcQ

04.gif


導入手順

Github - Keras-OneClassAnomalyDetection - 10-2. How to use をご覧ください。


異常検出モデルのコンバージョン

OpenVINOのモデルへ変換する手順を少し工夫しました。

ストレートに Keras (.h5) から OpenVINO (.bin) へ変換するのではなく、一度、 Tensorflow (.pb) へ変換してから OpenVINO へ変換しています。

モデル変換フロー

Keras → Tensorflow → OpenVINO

詳しくは、 Github - Keras-OneClassAnomalyDetection - 13. Model Convert をご覧ください。


ロジック

下記のように、たったのこれだけです。 素晴らしくシンプル。

shinmura0 さんに感謝。


main_openvino.py

import cv2

import time
import os
import sys
import numpy as np
import argparse
import platform
from sklearn.neighbors import LocalOutlierFactor
from sklearn.preprocessing import MinMaxScaler
from sklearn.externals import joblib
from keras.models import model_from_json
from keras import backend as K
from openvino.inference_engine import IENetwork, IEPlugin

def main(camera_FPS, camera_width, camera_height, inference_scale, threshold, device):

path = "pictures/"
if not os.path.exists(path):
os.mkdir(path)

model_path = "OneClassAnomalyDetection-RaspberryPi3/DOC/model/"
if os.path.exists(model_path):
# LOF
print("LOF model building...")
x_train = np.loadtxt(model_path + "train.csv",delimiter=",")

ms = MinMaxScaler()
x_train = ms.fit_transform(x_train)

# fit the LOF model
clf = LocalOutlierFactor(n_neighbors=5)
clf.fit(x_train)

# DOC
print("DOC Model loading...")
if device == "MYRIAD":
model_xml="irmodels/tensorflow/FP16/weights.xml"
model_bin="irmodels/tensorflow/FP16/weights.bin"
else:
model_xml="irmodels/tensorflow/FP32/weights.xml"
model_bin="irmodels/tensorflow/FP32/weights.bin"
net = IENetwork(model=model_xml, weights=model_bin)
plugin = IEPlugin(device=device)
if device == "CPU":
if platform.processor() == "x86_64":
plugin.add_cpu_extension("lib/x86_64/libcpu_extension.so")
exec_net = plugin.load(network=net)
input_blob = next(iter(net.inputs))
print("loading finish")
else:
print("Nothing model folder")
sys.exit(0)

base_range = min(camera_width, camera_height)
stretch_ratio = inference_scale / base_range
resize_image_width = int(camera_width * stretch_ratio)
resize_image_height = int(camera_height * stretch_ratio)

if base_range == camera_height:
crop_start_x = (resize_image_width - inference_scale) // 2
crop_start_y = 0
else:
crop_start_x = 0
crop_start_y = (resize_image_height - inference_scale) // 2
crop_end_x = crop_start_x + inference_scale
crop_end_y = crop_start_y + inference_scale

fps = ""
message = "Push [p] to take a picture"
result = "Push [s] to start anomaly detection"
flag_score = False
picture_num = 1
elapsedTime = 0
score = 0
score_mean = np.zeros(10)
mean_NO = 0

cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FPS, camera_FPS)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, camera_width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, camera_height)

time.sleep(1)

while cap.isOpened():
t1 = time.time()

ret, image = cap.read()

if not ret:
break

image_copy = image.copy()

# prediction
if flag_score == True:
prepimg = cv2.resize(image, (resize_image_width, resize_image_height))
prepimg = prepimg[crop_start_y:crop_end_y, crop_start_x:crop_end_x]
prepimg = np.array(prepimg).reshape((1, inference_scale, inference_scale, 3))
prepimg = prepimg / 255
prepimg = prepimg.transpose((0, 3, 1, 2))

exec_net.start_async(request_id = 0, inputs={input_blob: prepimg})
exec_net.requests[0].wait(-1)
outputs = exec_net.requests[0].outputs["Reshape_"]
outputs = outputs.reshape((len(outputs), -1))
outputs = ms.transform(outputs)
score = -clf._decision_function(outputs)

# output score
if flag_score == False:
cv2.putText(image, result, (camera_width - 350, 100), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1, cv2.LINE_AA)
else:
score_mean[mean_NO] = score[0]
mean_NO += 1
if mean_NO == len(score_mean):
mean_NO = 0

if np.mean(score_mean) > threshold: #red if score is big
cv2.putText(image, "{:.1f} Score".format(np.mean(score_mean)),(camera_width - 230, 100), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 1, cv2.LINE_AA)
else: # blue if score is small
cv2.putText(image, "{:.1f} Score".format(np.mean(score_mean)),(camera_width - 230, 100), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 1, cv2.LINE_AA)

# message
cv2.putText(image, message, (camera_width - 285, 15), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA)
cv2.putText(image, fps, (camera_width - 164, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0 ,0), 1, cv2.LINE_AA)

cv2.imshow("Result", image)

# FPS
elapsedTime = time.time() - t1
fps = "{:.0f} FPS".format(1/elapsedTime)

# quit or calculate score or take a picture
key = cv2.waitKey(1)&0xFF
if key == ord("q"):
break
if key == ord("p"):
cv2.imwrite(path + str(picture_num) + ".jpg", image_copy)
picture_num += 1
if key == ord("s"):
flag_score = True

cv2.destroyAllWindows()

if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument("-cfps","--camera_FPS",dest="camera_FPS",type=int,default=30,help="USB Camera FPS. (Default=30)")
parser.add_argument("-cwd","--camera_width",dest="camera_width",type=int,default=320,help="USB Camera Width. (Default=320)")
parser.add_argument("-cht","--camera_height",dest="camera_height",type=int,default=240,help="USB Camera Height. (Default=240)")
parser.add_argument("-sc","--inference_scale",dest="inference_scale",type=int,default=96,help="Inference scale. (Default=96)")
parser.add_argument("-th","--threshold",dest="threshold",type=int,default=2.0,help="Threshold. (Default=2.0)")
parser.add_argument("-d","--device",dest="device",default="CPU",help="Device. CPU/GPU (Default=CPU) GPU=Intel HD Graphics.")
args = parser.parse_args()
camera_FPS = args.camera_FPS
camera_width = args.camera_width
camera_height = args.camera_height
inference_scale = args.inference_scale
threshold = args.threshold
device = args.device

main(camera_FPS, camera_width, camera_height, inference_scale, threshold, device)



おわりに

とてつもなく速いタクトの業種でも活用できそうなほど、CPUのみで無意味に速く異常検出できるようになりました。