OpenCV(CUDA)によるtemplate matching

# 動機
- CUDA有効化OpenCV(Python)をインストールしたはいいものの、(日/英)ドキュメント、サンプルコードが見つからなかったので、c++版を参考に書いてみた
- CUDA万歳がしてみたかった
- Qiita記事を書いてみたかった

# まとめ
- UbuntuにCUDAが有効化されたOpenCVをインストールした
- Python上のOpenCVで、リサイズ及びテンプレートマッチングを行った
- 結論
    - CUDA万歳
    - 処理や画像サイズの大きさによってはCPUの方が速いこともある(今の環境だと尚更かもしれない)
    - Template MatchingはCUDAが圧倒的に速い
    - 転送が意外とかかる


# 環境  
- 2*Xeon E5-2667 v3 @3.20 GHz 
- DDR4-2133 4*8 GB RAM
- ubuntu LTS 20.04 @ Samsung Evo Plus 970 (500GB)
- GTX 1080 (RTX 30XXがほしい)
- OpenCV 4.5.0 pre (preなことに特に意味はない。気が向いたら直したい[環境構築](https://qiita.com/Fal318/items/ef1bfe6e69622a563615))
- cuda 10.2, cnDNN 7.6.5

#コード
##前処理
まず、諸々を読み込んでから200*200のmaharo.jpgを取得します。
これはサンシャイン水族館にいるカワウソの[マハロくん](https://wikiwiki.jp/kawausotter/%E3%83%9E%E3%83%8F%E3%83%AD%EF%BC%88%E3%82%B5%E3%83%B3%E3%82%B7%E3%83%A3%E3%82%A4%E3%83%B3%E6%B0%B4%E6%97%8F%E9%A4%A8%EF%BC%89)の画像です。

```python
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt

src = cv.imread("maharo.jpg", cv.IMREAD_GRAYSCALE)
h, w = src.shape[::-1] # w=200, h=200

plt.subplot(111),plt.imshow(src,cmap = 'gray')
plt.title('otter image'), plt.xticks([]), plt.yticks([])
plt.show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726758/ac8a59ec-0a13-845e-86e7-fbae8c118731.png)


次にGPU周りの変数を設定します。
GPUが有効になっているかどうかは、`cv2.cuda.getCudaEnabledDeviceCount()`で1以上の数字が返ってくるかどうかで確認できます。
基本的には、CUDA有効化されたOpenCVは、`cv2.function`を`cv2.cuda.function`と書き換えることで用いることができます。(例外がいくつかあり、今回のtemplate matchingなどがそうです。詳しくは`dir(cv2.cuda)`参照)
また、変数とGPUのメモリを`cv2.cuda_GpuMat()`により確保し、`.upload()/.download()`によりメモリ間の情報のやり取りを行います。

```python
print(cv.cuda.getCudaEnabledDeviceCount())
g_src = cv.cuda_GpuMat()
g_dst = cv.cuda_GpuMat()
g_src.upload(src)
```
とりあえずマハロくんの画像をGPUにも移しました。
## Resize
情報の読み出し/書き出しをループに含めないただのResize (512 -> 2K)で、GPUの素材の味を実感してみようと思います。
ちなみに、CUDAの方はLanczos補間に対応していません。(結構全体的にこういうのある)
### CPU
```python
%%timeit
cpu_dst = cv.resize(src, (h*10, w*10), interpolation=cv.INTER_CUBIC)
# 777 µs ± 8.06 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
```
### CUDA
```python
%%timeit
g_dst = cv.cuda.resize(g_src, (h*4, w*4), interpolation=cv.INTER_CUBIC)
# 611 µs ± 6.34 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
```
__素材の味が遅い__
これはちょっとGPUの恩恵を受けていない結果になってしまいました。ちなみにCPUからの読み込み書き出しを考慮した実行速度はもっと遅いです。

```python
%%timeit
g_src.upload(src)
g_dst = cv.cuda.resize(g_src, (h*4, w*4), interpolation=cv.INTER_CUBIC)
gpu_dst = g_dst.download()
# 1.51 ms ± 16.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
```

苦労してCUDA入れたのにこんなんだと嫌になっちゃいますね。まあそんな並列じゃなさそうだし難しいのかな？
読み込み書き出しは800 μs程度かかるようです。これ何依存なんだろう。PCIe 3.0がボトルネックになってそうな気がする(根拠なし)

##Template Matching
気を取り直して並列が活きそうなtemplate matchingをやります。

```python
ex_src = cv.resize(src, (h*10, w*10), interpolation=cv.INTER_CUBIC)
tmpl = ex_src[1000:1200, 1000:1200]
th, tw = tmpl.shape[::-1]
```
### CPU
```python
%%timeit
result = cv.matchTemplate(ex_src, tmpl, cv.TM_CCOEFF_NORMED)
# 138 ms ± 5.03 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
```
思ったより遅くない… (追記: 多分32 threadsで処理が動いているからです。)

```python
result = cv.matchTemplate(ex_src, tmpl, cv.TM_CCOEFF_NORMED)
min_val, max_val, min_loc, max_loc = cv.minMaxLoc(result)
top_left = max_loc
bottom_right = (top_left[0] + th, top_left[1] + tw)
cv.rectangle(ex_src,top_left, bottom_right, 255, 2)
plt.subplot(121),plt.imshow(result,cmap = 'gray')
plt.title('Matching Result'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(ex_src,cmap = 'gray')
plt.title('Detected Point'), plt.xticks([]), plt.yticks([])
plt.show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726758/0645afa6-f663-bf55-8716-83f401391376.png)
### CUDA
CUDA版のtemplate maching は(Pythonとしては)若干特殊ですが、`createTemplateMatching(precision, METHOD)`のように定義します。

```python
gsrc = cv.cuda_GpuMat()
gtmpl = cv.cuda_GpuMat()
gresult = cv.cuda_GpuMat()
gsrc.upload(ex_src)
gtmpl.upload(tmpl)
matcher = cv.cuda.createTemplateMatching(cv.CV_8UC1, cv.TM_CCOEFF_NORMED)
```

```python
%%timeit
gresult = matcher.match(gsrc, gtmpl)
# 10.5 ms ± 406 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
```

__素材の味が良い__
CPU: 138 msに比べて、CUDAは10.5 msと10倍程度の高速化が達成できています。

```python
%%timeit
gsrc.upload(ex_src)
gtmpl.upload(tmpl)
matcher = cv.cuda.createTemplateMatching(cv.CV_8UC1, cv.TM_CCOEFF_NORMED)
gresult = matcher.match(gsrc, gtmpl)
resultg = gresult.download()
# 16.6 ms ± 197 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
```
読み込み書き出しを計算しても、やはり8倍程度高速になります。GTX 1080でこうなので、最近話題のAmpere世代GPUなら更に高速に…?
自分はmax_locgだけしか要らないんですが、gresultの段階でmax_locgを取り出す方法ってないのでしょうか。

```python
gresult = matcher.match(gsrc, gtmpl)
resultg = gresult.download()
min_valg, max_valg, min_locg, max_locg = cv.minMaxLoc(resultg)
top_leftg = max_locg
bottom_rightg = (top_leftg[0] + tw, top_leftg[1] + th)
cv.rectangle(src,top_leftg, bottom_rightg, 255, 2)
plt.subplot(121),plt.imshow(resultg,cmap = 'gray')
plt.title('Matching Result'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(ex_src,cmap = 'gray')
plt.title('Detected Point'), plt.xticks([]), plt.yticks([])
plt.show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726758/8e99fd45-d4b3-1110-2ac3-daebf921e58f.png)


```python
print(max_loc,max_locg)
# (1000, 1000) (1000, 1000)
```
同じ結果が得られています。

今回使用したコード(Jupyter Notebook)は[Github](https://github.com/Ikwus/qiita_post/tree/master/opencv_CUDA)に公開しています。

# 参考
[テストコード](https://github.com/opencv/opencv/blob/4.0.1/modules/python/test/test_cuda.py)
[UbuntuでCUDAを有効にしたOpenCVをビルド](https://qiita.com/Fal318/items/ef1bfe6e69622a563615)