YOLOv3をWindowsでpythonから呼び出してみる（デスクトップアイコン識別）

#はじめに
YOLOv3をPythonから呼び出してみようと思います。

YOLOv3(darknet)をpythonから呼び出すサンプルであるdarknet.pyは、呼び出しにctypesを使っています。ctypesはpythonからCを呼び出す標準のライブラリです。

Windowsからdarknetを使用するには、`yolo_cpp_dll.dll`を準備しなければいけません。
ここでは、その準備と、実際にpythonから呼び出してみることをやってみます。

なぜ、Windowsを使っているかというのは、WindowsのデスクトップアイコンをYOLOv3で識別する実験をしているからです。


#前提
「[Yolo v3+Windows 10でデスクトップのアイコンを識別してみる その１](https://qiita.com/yasunari_matsuo/items/78695e9f53bc6b589daa)」などに従ってWindowsでYOLOv3をビルドすることができている（やり方を理解できている）ことを前提としています。

#環境
- Windows10 Pro
- Visual Studio 2017
- CUDA 10.0
- cuDNN 7.3.0.29
- OpenCV 4.1.1

#DLLをビルドする
最初に、VSのソリューションを開きます。
ソリューションは、下記にあります。
`[配置先]\darknet\build\darknet\yolo_cpp_dll.sln`

ソリューションプラットホームをReleaseとx64に変更して、前記事と同様に、CUDAとOpenCVのインクルードディレクトリを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/a6005e58-7c6c-8990-ed0c-32490d4b6296.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/f87233f1-14b0-163f-ffd3-4dd2cd87107d.png)

ビルドすると、`yolo_cpp_dll.dll`が出来上がります。

`[配置先]\darknet\build\darknet\x64`まで移動して、以下のコマンドを実行してみましょう。

```bash
python darknet.py
```

いつもの犬のやつが表示されたら成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/f6afef61-e90a-cf38-3c37-29adf0d6c617.png)

#解説
####ctypes
ctypesはC言語と互換性のあるデータ型を提供し、動的リンク/共有ライブラリをPythonでラップすることができるようにするPython標準の外部関数ライブラリです。
呼び出し方には、標準 cdecl 呼び出し規約を用いて関数をエクスポートしているライブラリをロードする`CDLL`と、stdcall 呼び出し規約を用いる関数を呼び出す`WinDLL`の2種類があります。
cdeclとstdcallの違いについては、ここでは詳しく言及しません。ざっくりいうとメモリの確保と解放を呼び出し元、呼び出し先どちらでするかの規約です。

darknet.pyでは以下のようにDLLを呼び出しています。

```python:darnknet.py
from ctypes import *
...
...
    winGPUdll = os.path.join(cwd, "yolo_cpp_dll.dll")
...
...
    lib = CDLL(winGPUdll, RTLD_GLOBAL)

```

`CDLL`を使ってるので、cdecl規約で呼び出しています。ソリューションで、そのような設定になっているかを確認してみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/6834fb23-2201-dd22-f00a-5a78c53d04de.png)
確かにcdeclになっていますね。
それから、`CDLL`で読み込んだ`lib`をつかって、Cを呼び出しています。

```python:darknet.py
lib.network_width.argtypes = [c_void_p]
lib.network_width.restype = c_int

def network_width(net):
    return lib.network_width(net)
```

ここで呼び出されている`network_width`は、Cでは、以下の通り定義されています。

```c:darknet.h
LIB_API int network_width(network *net);
```

これを見るとわかるように、Pythonからctypesを使う際は、まず、`argtypes`と`restype`でそれぞれ、引数の型と戻り値の型を指定し、それから、関数を呼び出す、という手順を踏みます。
引数の型に`c_void_p`を、戻り値に`c_int`を指定しています。

####darknet.py
下記の関数がまず呼ばれます。

```python:darknet.py
def performDetect(imagePath="data/dog.jpg", thresh= 0.25, configPath = "./cfg/yolov3.cfg", weightPath = "yolov3.weights", metaPath= "./cfg/coco.data", showImage= True, makeImageOnly = False, initOnly= False):
```
上のほうで動作させたのはすべてデフォルトのパラメータです。
この関数では最初にパラメータのチェックをしています。チェックに成功したら、次の関数を呼び出して認識を行ってます。

```python:darknet.py
    detections = detect(netMain, metaMain, imagePath.encode("ascii"), thresh)
```
この関数の中で何をやっているかというと、

```python:darknet.py
def detect(net, meta, image, thresh=.5, hier_thresh=.5, nms=.45, debug= False):
    im = load_image(image, 0, 0)
    if debug: print("Loaded image")
    ret = detect_image(net, meta, im, thresh, hier_thresh, nms, debug)
    free_image(im)
    if debug: print("freed image")
    return ret
```
画像を読み込んで、`detect_image`を呼び出しています。
この関数は何をしているかというと、

```python:darknet.py
predict_image(net, im)
```
`predict_image`を呼び出しています。ここでやっと、Cの関数が呼び出されます。`predict_image`は以下の通り定義されいます。

```python:darknet.py
predict_image = lib.network_predict_image
predict_image.argtypes = [c_void_p, IMAGE]
predict_image.restype = POINTER(c_float)
```
やっとこさ、ctypesでの呼び出しが出てきました。ついでにCの実装を見ておきましょう。

```c:network.c
float *network_predict_image(network *net, image im)
{
    //image imr = letterbox_image(im, net->w, net->h);
    float *p;
    if(net->batch != 1) set_batch_network(net, 1);
    if (im.w == net->w && im.h == net->h) {
        // Input image is the same size as our net, predict on that image
        p = network_predict(*net, im.data);
    }
    else {
        // Need to resize image to the desired size for the net
        image imr = resize_image(im, net->w, net->h);
        p = network_predict(*net, imr.data);
        free_image(imr);
    }
    return p;
}
```
確かに、引数がポインタとイメージで戻り値がfloatですね。
pythonでdarknetを呼びだすときは、この辺りを参考にすればよさそうです。

#デスクトップを識別してみる
[前回](https://qiita.com/yasunari_matsuo/items/78695e9f53bc6b589daa)訓練させたweightsを使用して、デスクトップの識別をpythonから呼び出してみましょう。

コマンドプロンプトから`python`を起動して、以下のように`performDetect`を呼び出します。フルパス指定しているので少し長いです・・。パスは適宜読み替えてください。

```python:
>>> from darknet import performDetect
>>> performDetect("D:/Projects/python/desktopyolov3/data/datasets/Desktop006.jpg", 0.25, "D:/Projects/python/desktopyolov3/cfg/yolov3-voc.cfg", "D:/Projects/python/desktopyolov3/data/backup/yolov3-voc_last.weights", "D:/Projects/python/desktopyolov3/data/desktop.data")
```
以下の通り識別が動作しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/43d2459b-4f76-4e26-8aab-6b081f91785f.png)
大成功です。
#つづく
次回は、ここでビルドしたDLLを使ってC++からYOLOv3を呼び出して、その場所のアイコンをゴミ箱に移動させるやつを作ろうと思います。

YOLOv3シリーズは以下から参照できます。

Yolo v3+Windows 10でデスクトップのアイコンを識別してみる その１
https://qiita.com/yasunari_matsuo/items/78695e9f53bc6b589daa

（おまけ）Yolo v3+Windows 10でデスクトップのアイコンを動的に識別してみる
https://qiita.com/yasunari_matsuo/items/f0989523849f6ae40483#_reference-8747cd277d7aeccfdef1

ＡＩでデスクトップアイコンを自動削除してくれるツールを作ったった（YOLOv3+Windows 10）
https://qiita.com/yasunari_matsuo/items/2ac2140e7ac250304d4a

#参考にしたサイト
https://docs.python.org/ja/3/library/ctypes.html