OpenCV for UnityのDnnモジュールをCUDAバックエンドを利用して高速化する

#はじめに
Unityでマルチプラットフォーム対応でディープラーニングの推論をしたいときに便利なアセットに[OpenCV for Unity](https://assetstore.unity.com/packages/tools/integration/opencv-for-unity-21088)があります。[Deep Learning with OpenCV](https://www.embedded-vision.com/academy/Embedded_Vision_Alliance_Meetup_March_2019_OpenCV.pdf)によると、かなり最適化されていてCPUの推論処理でもそこそこの速度がでているようです。

しかし、最近になってOpenCVのリポジトリに[YashasSamaga](https://github.com/YashasSamaga)さんの[コミット](https://github.com/opencv/opencv/pull/14827)がマージされました。このコミットによってDnnモジュールの推論処理を**NVIDIA社製のGPUを利用するしくみ（CUDA）**を使用してより高速に行うことができるようになりました。

この記事ではCUDAを有効にしたOpenCVライブラリを自前でビルドしてOpenCV for Unityで利用すると、はたしてDNNモジュールの推論処理がどのくらい高速化されるのかを比較してみたいと思います。

#テスト環境
- OpenCV 4.2.0
- Unity 2017.5f1
- OpenCV for Unity 2.3.8
- Cmake 3.15.5
- Visual Studio 2017
- OS : Windows10
- CPU : Intel Core i7-7700K 4.2GHz
- GPU : NVIDIA GeForce GTX 1050

#Windows用OpenCVダイナミックライブラリをCUDAバックエンドを有効にしてビルドする手順

OpenCV for Unityに同梱されている[ReadMe.pdf](https://github.com/EnoxSoftware/OpenCVForUnity/blob/master/Assets/OpenCVForUnity/ReadMe.pdf)の**How to use OpenCV Dynamic Link Library with customized build settings**の項目を参考にして、CUDAを有効にした**OpenCVのダイナミックライブラリ**をビルドしていきます。

ディフォルトで**Assets/OpenCVForUnity/Plugins/Windows/x86_64/フォルダ**に入っている**opencvforunity.dll**（おそらくC#側のコードとC++側のOpenCVライブラリを繋ぐ役割のファイルだと思います。）はOpenCVライブラリが**スタティックライブラリ**として埋め込まれて一つのファイルとしてビルドされています。
この記事では自分でビルドしたOpenCVの**ダイナミックライブラリ**を**Assets/OpenCVForUnity/Extra/dll_version/Windows/x86_64/フォルダ**に入っているOpenCVライブラリが埋め込まれていないopencvforunity.dllから呼び出す形で利用します。

##CUDA関連のセットアップ

[この記事](https://www.kkaneko.jp/tools/win/windows_opencv.html)を参考に以下のCUDA関連のプログラムをインストールし、環境変数にパスを設定したりしてセットアップを完了してください。

- NVIDIA グラフィックスボード・ドライバ
- NVIDIA CUDA ツールキット
- NVIDIA cuDNN
- （任意）Intel MKL

##OpenCVをビルド

1. OpenCVのリポジトリからOpenCV for Unityのバージョンに対応したコミット時点のコードをダウンロードする。コミットへのリンクは[ReadMe.pdf](https://github.com/EnoxSoftware/OpenCVForUnity/blob/master/Assets/OpenCVForUnity/ReadMe.pdf)に書いてあります。例えばOpenCV for Unity2.3.8の場合は**OpenCV 4.2.0 repository ( git: ​ [opencv](https://github.com/opencv/opencv/tree/89d3f95a8eea50acbfb4b8db380d5a4dc8a98173)​ , ​ [opencv-contrib​](https://github.com/opencv/opencv_contrib/tree/65abc7090dedc84bbedec4dfd143f0340e52114f) )**です。  
2. cmakeでビルドプロジェクトを出力する。
 1. VisualStudio 15 2017 x64bitでプロジェクトを生成する。
![cmake.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/7c3b8f88-7763-d9ae-ab68-d4615b0d2f20.png)
 2. 以下の画像のようにビルド設定を変更する。**CUDA_ARCH_BIN**は[このページ](https://www.myzhar.com/blog/tutorials/tutorial-nvidia-gpu-cuda-compute-capability/)を参考に適切な数値を指定してください。MKL関連は任意。
![cmake2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/4e27429f-27e7-db12-b20e-105807c94c49.png)
![cmake3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/337a9697-a899-8d91-c94f-227fa88bbe50.png)
![cmake4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/5324c176-f3eb-083e-b5b6-134f15ca6c27.png)

 3. Generateボタンを押して、VidualStudioのプロジェクトを出力する。
3. VisualStudioのプロジェクトを開いて、ビルドする。
 1. ビルド構成をReleaseに変更する。
![visualstudio_release.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/0c505bec-9c8f-7c59-16af-e500ee36fe5e.png)
 2. INSTALLを右クリックしてビルド開始する。
![visualstudio_install.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/6559e47c-e31c-30ff-9e22-373da6fa5f86.png)
 3. installフォルダ以下にビルドされたライブラリが出力されます。
![copy_dll.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/f2592ea8-62cb-3993-8b8a-5c83aa5d9cb1.png)

##ビルドしたダイナミックライブラリをUnityプロジェクトにコピー
1. 空プロジェクトに[OpenCV for Unity](https://assetstore.unity.com/packages/tools/integration/opencv-for-unity-21088)をImportする。
1. ディフォルトのWindows 64bit用のライブラリファイル(**opencvforunity.dll**)が入っている**Assets/OpenCVForUnity/Plugins/Windows/x86_64/フォルダ**を削除する。
![delete_static_dll.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/a99c3034-86d8-fcce-04b0-112517a16e2f.png)

1. **Extra.package**をインポートして、**Assets/OpenCVForUnity/Extra/dll_version/Windows/x86_64/フォルダ**を**Assets/OpenCVForUnity/Plugins/Windows/フォルダ**以下に移動させる。
![import_extra0.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/1992868e-5f6d-4041-b2ba-a071f0de73fc.png)
![move_dynamic_dll.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/c0db9c83-d0cd-8279-894f-513b0833845a.png)

1. 先ほどビルドしたOpenCVのライブラリを**Assets/OpenCVForUnity/Plugins/Windows/フォルダ**以下にコビーする。
![move_opencv_dll.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/8c377b42-89b7-dd6d-b125-912b546fd3d4.png)

1. ライブラリのImportSettingsを以下の画像のように設定する。すべてのdllファイルを設定してください。
![copy_dll_unity.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/e9aecf59-1bab-3a7b-0bbf-bf4c7fb51bd0.png)



#速度比較

実際にコードでCUDAバックエンドを有効にして推論処理をするためには、以下のように設定を変更するだけでOKです。

                net = Dnn.readNetFromCaffe(prototxt_filepath, caffemodel_filepath);

                //net.setPreferableBackend(Dnn.DNN_BACKEND_OPENCV);
                //net.setPreferableTarget(Dnn.DNN_TARGET_CPU);
                net.setPreferableBackend(Dnn.DNN_BACKEND_CUDA);
                net.setPreferableTarget(Dnn.DNN_TARGET_CUDA);

テストしたソースコードは[github](https://github.com/utibenkei/OpenCVForUnityDnnCudaBackendExample)にアップしてあります。元々同梱されていたExampleをVideoCaptureクラスを使用して好きな動画を読み込めるように改変しました。WebCamTextureバージョンも入ってます。もし、シーンを実行したときにdllの読み込みエラーが発生した場合は、dllファイルの依存関係を調べる[ツール](https://github.com/lucasg/Dependencies)を使用してきちんとパスが通っているかなどを確認してみて下さい。

**追記**
下記のテストコードを参考に推論処理部分のみを計測した結果を追加しました。最初の3回の結果を捨てて、そのあとの10回の平均値を出しました。FpsMonitor.csで表示されるFPSではなく純粋な推論処理にかかる時間で比較したいと思います。
Test Code: https://gist.github.com/YashasSamaga/71157cf0c3768c497e5e70fb95435596

                    TickMeter tm = new TickMeter();
                    tm.start();

                    net.setInput(blob);
                    net.forward(outs, outBlobNames);

                    tm.stop();
                    Debug.Log ("Inference time, ms: " + tm.getTimeMilli ());

では、推論処理がどのくらい速くなったかを見てきたいと思います。

##Yolo v3
[ソースコード](https://github.com/utibenkei/OpenCVForUnityDnnCudaBackendExample/blob/master/CUDABackendExample/DnnObjectDetectionVideoCaptureExample.cs)

CPUバックエンド **33.6401ms**
![yolov3_cpu_360.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/f644d872-ca02-7aff-3ae2-f365e2a35b66.gif)
CUDAバックエンド **7.4248ms**
![yolov3_cuda_360.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/7de78691-2732-d628-8765-c25529170e0c.gif)
約**4.5**倍くらい速くなりました。

##LibFaceDetection
[ソースコード](https://github.com/utibenkei/OpenCVForUnityDnnCudaBackendExample/blob/master/CUDABackendExample/DnnObjectDetectionVideoCaptureExample.cs)

CPUバックエンド **7.1025ms**
![facedetection_cpu_360.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/4099eada-b8bb-99b5-2412-b22397d8fce7.gif)
CUDAバックエンド **4.1526ms**
![facedetection_cuda_360_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/e7e9c747-985c-f05d-58a9-d395c740bb4f.gif)
~~CPUバックエンドでも十分な速度が出ていますが、CUDAバックエンドにしてもなぜかFPSはまったく変わらず。CUDAバックエンドで現在[対応しているLayer](https://gist.github.com/YashasSamaga/a84cf2826ab2dc755005321fe17cd15d#file-d1_supportmatrix-md)も関係しているのかもしれません。~~
約**1.7**倍くらい速くなりました。

##Mask-RCNN
[ソースコード](https://github.com/utibenkei/OpenCVForUnityDnnCudaBackendExample/blob/master/CUDABackendExample/MaskRCNNVideoCaptureExample.cs)

CPUバックエンド **1311.3804ms**
![maskrcnn_cpu_360.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/09cf2e97-77b2-f6fc-9b5d-3e81e1ab46d7.gif)
CUDAバックエンド **172.2432ms**
![maskrcnn_cuda_360_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/2a9ecfc9-c63a-e98c-d43a-b7dd4ed3d219.gif)|
約**7.6**倍くらい速くなりました。

##OpenPose
[ソースコード](https://github.com/utibenkei/OpenCVForUnityDnnCudaBackendExample/blob/master/CUDABackendExample/OpenPoseVideoCaptureExample.cs)

CPUバックエンド **785.3027ms**
![opencv_dnn_cpu_360.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/1300e31f-5ea6-49b0-895b-ffd1cb31cb76.gif)

CUDAバックエンド **152.0677ms**
![opencv_dnn_cuda_360.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/ec43f4ec-b764-7d72-8621-bf6c4f0a0442.gif)
約**5.2**倍くらい速くなりました。

##ThreeDPoseUnityForiOS
ついでに@yukihiko_aさんが公開してくださっている[ThreeDPoseUnityForiOS](https://github.com/digital-standard/ThreeDPoseUnityForiOS)も試してみました。

CPUバックエンド **42.0681ms**
![threedpose_cpu_360.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/70cb933a-5c2b-b1d4-6110-a7ab622faafb.gif)

CUDAバックエンド **210.3789ms**
![threedpose_cuda_360.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28558/affa8e21-cfe2-970a-1046-c2e5d8be8897.gif)

約**5.0**倍くらい遅くなりました。CPUバックエンドでは20FPSも出ているのに、CUDAバックエンドにするとなぜか4.5FPSくらいまで下がってしまいました。謎です。モデルによっては必ずしもCUDAにすれば速くなるということではないようです。

#まとめ
ということで、CUDAバックエンドの力を使うと推論速度は多くのモデルで向上するようです。中には7倍以上も向上するモデルもありました。今回の結果を[このページ](https://gist.github.com/YashasSamaga/a84cf2826ab2dc755005321fe17cd15d#file-d2_backend_comparision-md)のベンチマークを比べてみると、OpenPoseモデルはだいたい近い結果[^1]になっているのでこの記事で紹介したやり方は間違ってはいないようです。たぶん。
[^1]: NVIDIA GTX 1050でOpenPoseモデルが160.561msとあるので、計算するとだいたい6.25FPSとなります。ざっくりですみません。:sweat_smile:

また、DnnモジュールのCUDAバックエンドに関するコードは続々と新しいコミットがマージされているので、今後さらなる推論速度向上が期待できそうです。

#おまけ
- この記事で紹介したように、OpenCV for Unityには自分でビルド設定をカスタマイズしたダイナミックライブラリを使用できる方法があります。**[VideoCaptureクラスのGStreamerバックエンド](https://qiita.com/nakasuke_/items/84ec6cb3ddc9dee003c2)**や**[Feature2DモジュールのNON_FREEアルゴリズム（SIFT、SURF）](https://qiita.com/shu223/items/fa3cf693296e5641f771)**を有効にしてビルドすれば、ディフォルトビルドでは無効になっているいろいろな機能を使用することができるようになると思います。
- 今回いろいろ試しているなかで、Unityのバージョンを**2019.3.0f3**にしたところ、シーン実行時にdllファイル群が読み込めないというエラーメッセージがコンソールに表示され、なぜかdllファイルの読み込みに失敗してしまいます。<details><summary>コンソールに表示されたエラーメッセージ</summary><div>

>Plugins: Failed to load 'Assets/OpenCVForUnity/Plugins/Windows/x86_64/opencvforunity.dll' with error '指定されたプロシージャが見つかりません。
>'.
>DllNotFoundException: opencvforunity
</div></details>Unityのバージョンアップに伴いライブラリのロード方法が変わったのかもしれません。**どなたか解決方法を知っている方がいたら教えてください。**