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

**Keras-OneClassAnomalyDetection**　[![GitHub stars](https://img.shields.io/github/stars/PINTO0309/Keras-OneClassAnomalyDetection.svg?style=social&label=Stars)](https://github.com/PINTO0309/Keras-OneClassAnomalyDetection)
<a href="https://github.com/PINTO0309/Keras-OneClassAnomalyDetection"><img src="https://qiita-image-store.s3.amazonaws.com/0/194769/e17f2ca6-f26f-36bc-c88c-edf21d608d54.png" width=60%></a>

# はじめに
**@shinmura0** さんの **[「がんばる人のための画像検査機」](https://qiita.com/shinmura0/items/7f4298b75d6b788bba80)** を３０倍以上にスピードアップしました。
なお、 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のバグで正常に動作しませんでした。

```bash:320x240_CPU_Core_m3
$ python3 main_openvino.py -cfps 150 -cwd 320 -cht 240
```
**Youtube： https://youtu.be/8GhYysCBhq4**
![01.gif](https://qiita-image-store.s3.amazonaws.com/0/194769/fad792a5-550c-c9fd-2479-b985843799bf.gif)

```bash:640x480_CPU_Core_m3
$ python3 main_openvino.py -cfps 60 -cwd 640 -cht 480
```
**Youtube： https://youtu.be/FplYoZfHx3A**
![02.gif](https://qiita-image-store.s3.amazonaws.com/0/194769/cc0a2254-ce6c-6cce-fac7-c218be103750.gif)

```bash: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](https://qiita-image-store.s3.amazonaws.com/0/194769/32df67b1-a19c-bfca-be56-c4644acefa20.gif)

```bash: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](https://qiita-image-store.s3.amazonaws.com/0/194769/6d2fb9a5-52a2-3f0a-f3f0-403ff8019326.gif)

# 導入手順
**[Github - Keras-OneClassAnomalyDetection - 10-2. How to use](https://github.com/PINTO0309/Keras-OneClassAnomalyDetection#10-2-how-to-use)** をご覧ください。

# 異常検出モデルのコンバージョン
OpenVINOのモデルへ変換する手順を少し工夫しました。
ストレートに Keras (.h5) から OpenVINO (.bin) へ変換するのではなく、一度、 Tensorflow (.pb) へ変換してから OpenVINO へ変換しています。

**モデル変換フロー**
**`Keras → Tensorflow → OpenVINO`**

詳しくは、 **[Github - Keras-OneClassAnomalyDetection - 13. Model Convert](https://github.com/PINTO0309/Keras-OneClassAnomalyDetection#13-model-convert)** をご覧ください。

# ロジック
下記のように、たったのこれだけです。 素晴らしくシンプル。
shinmura0 さんに感謝。

```python: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のみで無意味に速く異常検出できるようになりました。