CustomVisionのモデルをOpenCVで推論(ラズパイ4+Windows10)

# はじめに

ラズパイやJetsonでCustomVision推論するときに、時間かかるわエラーが出るわでなぜかonnxruntimeが入らないのでOpenCVでやってみようと思いました。その予行演習としてまずはWindowsノートPCで動かしてラズパイ＋Windowsで動かそうと思います。ちなみに、ノートPCで推論するなら普通にonnxruntime入れるほうが手っ取り早いです。

# ノートPCで推論

**環境**

- Python3.6(Anaconda)
- OpenCV>=4.0.0


必要なライブラリは、OpenCVとpillowとnumpyです。

```
pip install pillow numpy opencv-python
```

OpenCVはバージョン4以降じゃないとだめかもです。
CustomVisionのモデルは、ONNXでエクスポートしてダウンロードしておいてください。Compactドメインで学習して、パフォーマンスタブからエクスポートできます([参考](https://docs.microsoft.com/ja-jp/azure/cognitive-services/custom-vision-service/export-your-model))。
展開したら以下のような構成になっていると思います。C#とPythonのサンプルコードがついてます。

```
$ tree /f
E:.
│  cvexport.manifest
│  labels.txt
│  LICENSE
│  model.onnx
│
├─CSharp
│      ObjectDetection.cs
│
└─python
    │  cntk_predict.py
    │  object_detection.py
    │  onnxruntime_predict.py
    │
    └─__pycache__
            object_detection.cpython-36.pyc
```

pythonというフォルダに移動して以下のコードを保存します。

```python:inference.py
import cv2
import numpy as np
from object_detection import ObjectDetection
from PIL import Image, ImageDraw

filename = 'test1.png'        # 推論したい画像のパス
modelname = '../model.onnx'   # 推論に使うモデルのパス
labelpath = '../labels.txt'   # エクスポートした中にあったlabels.txtへのパス

class ODbyCV2(ObjectDetection):
    def __init__(self, model_filename, labels):
        super(ODbyCV2, self).__init__(labels)
        self.model = cv2.dnn.readNetFromONNX(modelname)
    def predict(self, img):
        img = self.pil2cv(img)
        h,w,c = img.shape
        self.model.setInput(cv2.dnn.blobFromImage(img,size=(w,h),swapRB=True,crop=False))
        results = self.model.forward()
        return np.squeeze(results).transpose((1,2,0)).astype(np.float32)
    def pil2cv(self, image):
        new_image = np.array(image, dtype=np.uint8)
        if new_image.ndim == 2:
            pass
        elif new_image.shape[2] == 3:
            new_image = cv2.cvtColor(new_image, cv2.COLOR_RGB2BGR)
        elif new_image.shape[2] == 4:
            new_image = cv2.cvtColor(new_image, cv2.COLOR_RGBA2BGRA)
        return new_image

# ラベル読み込み
with open(labelpath, 'r') as f:
    labels = [l.strip() for l in f.readlines()]

model = ODbyCV2(modelname,labels)
img = Image.open(filename)

# 推論
results = model.predict_image(img)
print(results)

# 以下推論結果の表示
img = cv2.imread(filename)
height,width,channel = img.shape

font_face = cv2.FONT_HERSHEY_DUPLEX
font_scale = 0.6
font_thickness = 1
for result in [results[0]]:
    # if result['probability'] >= 0.3:
    text_str = str(int(result['tagName']))
    text_w, text_h = cv2.getTextSize(text_str, font_face, font_scale, font_thickness)[0]
    score_w, score_h = cv2.getTextSize(str(round(result['probability'],2)), font_face, font_scale, font_thickness)[0]
    x1 = int(width*result['boundingBox']['left'])
    y1 = int(height*result['boundingBox']['top'])
    x2 = int(width*result['boundingBox']['width']+width*result['boundingBox']['left'])
    y2 = int(height*result['boundingBox']['height']+height*result['boundingBox']['top'])
    text_pt = (x1, y1 - 3)
    text_color = [255,255,255]
    rgb = [255,0,0]
    cv2.rectangle(img, (x1, y1),(x2,y2),rgb,2)
    cv2.rectangle(img, (x1, y1),(x1+text_w,y1-text_h-4),rgb,-1)
    cv2.rectangle(img, (x1, y1),(x1+score_w,y1+score_h+4),rgb,-1)
    cv2.putText(img, text_str, text_pt, font_face, font_scale, text_color, font_thickness, cv2.LINE_AA)
    cv2.putText(img, str(round(result['probability'],2)), (x1, y1 +score_h), font_face, font_scale, text_color, font_thickness, cv2.LINE_AA)
        
cv2.imshow('test',img)
cv2.waitKey(0)
cv2.destroyAllWindows()
```

エクスポートしてきたときについているサンプルコードを参考にしました。object_detection.pyのObjectDetectionクラスを継承してpredict関数をオーバーライドしています。このコードを実行すると以下のような結果が得られます。8がtagName、0.72がprobabilityです。

![result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685335/763bd8b5-676e-bfbc-9e20-14ac65d0121c.png)

# ラズパイ(Windows10)で推論

**環境**

- Raspberry Pi4 Model B 4GB RAM
- Windows10

ラズパイでWindows10を動かす方法は、[こちら](https://qiita.com/mkht/items/9d173334dc5b26bfef46)を参考にしてください。

結論からいうと上記と全く同じ流れでできます。しかし、NumpyがWindows＋Armに対応してないのか、import時に以下のようなwarningが出て、推論時にもたくさんwarningが出てきます。

```
C:\Users\pi\anaconda3\envs\test\lib\site-packages\numpy\core\getlimits.py:172: RuntimeWarning: divide by zero encountered in exp2
  eps=exp2(ld(-112)),
C:\Users\pi\anaconda3\envs\test\lib\site-packages\numpy\core\getlimits.py:51: RuntimeWarning: divide by zero encountered in log10
  self.precision = int(-log10(self.eps))
C:\Users\pi\anaconda3\envs\test\lib\site-packages\numpy\core\getlimits.py:52: RuntimeWarning: divide by zero encountered in power
  self.resolution = float_to_float(float_conv(10) ** (-self.precision))
C:\Users\pi\anaconda3\envs\test\lib\site-packages\numpy\core\getlimits.py:184: RuntimeWarning: divide by zero encountered in exp2
  epsneg_f80 = exp2(ld(-64))
C:\Users\pi\anaconda3\envs\test\lib\site-packages\numpy\core\getlimits.py:185: RuntimeWarning: divide by zero encountered in exp2
  tiny_f80 = exp2(ld(-16382))
C:\Users\pi\anaconda3\envs\test\lib\site-packages\numpy\core\getlimits.py:199: RuntimeWarning: divide by zero encountered in exp2
  eps=exp2(ld(-63)),
C:\Users\pi\anaconda3\envs\test\lib\site-packages\numpy\core\getlimits.py:212: RuntimeWarning: divide by zero encountered in nextafter
  if hasattr(umath, 'nextafter')  # Missing on some platforms?
C:\Users\pi\anaconda3\envs\test\lib\site-packages\numpy\core\getlimits.py:224: RuntimeWarning: divide by zero encountered in exp2
  eps=exp2(ld(-105)),
C:\Users\pi\anaconda3\envs\test\lib\site-packages\numpy\core\getlimits.py:225: RuntimeWarning: divide by zero encountered in exp2
  epsneg= exp2(ld(-106)),
C:\Users\pi\anaconda3\envs\test\lib\site-packages\numpy\core\getlimits.py:227: RuntimeWarning: divide by zero encountered in exp2
  tiny=exp2(ld(-1022)))
C:\Users\pi\anaconda3\envs\test\lib\site-packages\numpy\__init__.py:316: RuntimeWarning: divide by zero encountered in subtract
  if not abs(x.dot(x) - 2.0) < 1e-5:
C:\Users\pi\anaconda3\envs\test\lib\site-packages\numpy\__init__.py:316: RuntimeWarning: divide by zero encountered in absolute
  if not abs(x.dot(x) - 2.0) < 1e-5:
```

[ここ](https://github.com/numpy/numpy/issues/17747)に同じwarningが出るという報告がありますが解決策はわからず...。import時はともかく推論実行時に出るのでうまく推論できているのかもわからないです(同じような結果が得られるのでただしそうではありますが...)。解決策があれば教えてください。


# おわりに
お疲れさまでした。opencvでCustomVisionのモデルの推論をしました。cv2.dnn.forward()の出力が何を表しているかもわかっていないので知っている方は教えていただければ幸いです。

間違い等ありましたらご指摘よろしくお願いします。

# 参考

- [ラズパイ4にWindows 10 on ARM64をインストールする](https://qiita.com/mkht/items/9d173334dc5b26bfef46)
- [【Python】Pillow ↔ OpenCV 変換](https://qiita.com/derodero24/items/f22c22b22451609908ee)