OpenCV(CUDA)によるaffine transformation

# 動機
[過去記事](https://qiita.com/Ikwus/items/0a978edcd24148a188cb)参照

# まとめ
- Python上のOpenCVで、affine変換を行った
- 結論
    - CUDA万歳。
    - OpenCVのmultithreadingすごい。


# 環境  
- 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 (4.5.0 preなことに特に意味はない。気が向いたら4.4.0あたりに直したい。[環境構築](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)の画像です。
このあたりの詳細は[過去記事](https://qiita.com/Ikwus/items/0a978edcd24148a188cb)参照。

```python
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
print('Enabled CUDA devices:',cv.cuda.getCudaEnabledDeviceCount()) # 1

src = cv.cvtColor(cv.imread("maharo.jpg"),cv.COLOR_BGR2RGB)
h, w, c = src.shape #200, 200, 3
plt.subplot(111),plt.imshow(src)
plt.title('otter image'), plt.xticks([]), plt.yticks([])
plt.show()

g_src = cv.cuda_GpuMat()
g_dst = cv.cuda_GpuMat()
g_src.upload(src)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726758/1335ad5a-e2eb-1ec6-cdb6-f6fe4cd76bdb.png)
先にaffine変換の行列を定義しておきます。中心を軸にした回転と並行移動を組み合わせて、scale倍rot度の中心軸回転を行います。

```python
def get_rot_affine(src, rot, scale):
    h, w = src.shape[::-1]
    rot_affine = cv.getRotationMatrix2D((h/2, w/2), rot, scale)
    rot_affine[:2,2] -= [h/2, w/2]
    rot_affine[:2,2] += [h/2*scale, w/2*scale]
    return rot_affine

```

## 等倍20度回転
等倍20度回転についてはaffine行列を以下のように定義します。
ちなみに、CUDAの方はLanczos補間に対応していません。

```python
rot_affine = get_rot_affine(src, 20, 1)
```

### CPU
まずはCPUで試してみます。

```python
%%timeit
img_dst = cv.warpAffine(src, rot_affine, (w*1, h*1), flags=cv.INTER_CUBIC)
# 1.08 ms ± 7.24 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726758/4ebc4ae3-5978-fc36-d51a-4b294122f8f7.png)


### CUDA
情報の読み出し/書き出しをループに含めない処理オンリーで、GPUの素材の味を実感してみようと思います。
例によって`cv2.cuda.wrapAffine`を使うのですが、引数はsource画像`src`とoutput画像`dst`だけGPU上に置いて、affine行列やdstサイズは通常のcv.wrapAffineと共通のものを使うようです。(GPU上に置くと怒られる)。

```python
%%timeit
g_dst = cv.cuda.warpAffine(g_src, rot_affine, (w*1, h*1), flags=cv.INTER_CUBIC)
# 383 µs ± 28.6 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
```

まあ、こんなもんでしょう…やはり画像が小さいと2~3倍程度しか効果はないようです。

```python
%%timeit
g_src.upload(src)
g_dst = cv.cuda.warpAffine(g_src, rot_affine, (w*1, h*1), flags=cv.INTER_CUBIC)
gpu_dst = g_dst.download()
# 450 µs ± 5.11 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726758/62f420c5-0b1e-74db-30af-1d060e924e4b.png)

読み込み/書き出しを考慮してもそこまで速度が変わりません。(画像が軽いから)

## 10倍20度回転
マハロくんを10倍(2K)にして、20度回転してみます。
affine行列は以下のように呼んでおきました。

```python
rot_affine = get_rot_affine(src, 20, 10)
```

### CPU
まずはCPUで試してみます。

```python
%%timeit
img_dst = cv.warpAffine(src, rot_affine, (w*10, h*10), flags=cv.INTER_CUBIC)
# 7.29 ms ± 36.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
```
10倍拡大したにしては結構速いです。
これは環境依存で、200 px画像の処理時は並列化が十分に働かず、CPUが1 threadで動いていたのですが、2K画像を処理する際には32 threadsの並列化がきちんと動いて高速化されていることによります。
(普通のCore i5/7等でやるともっと遅くなります。ちなみに検証環境の2*Xeon E5-2667 v3 @3.20 GHzはCinebench曰く1*Ryzen 9 3900XT程度だそうです。時代だ…)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726758/4ebc4ae3-5978-fc36-d51a-4b294122f8f7.png)

### CUDA

```python
%%timeit
g_dst = cv.cuda.warpAffine(g_src, rot_affine, (w*10, h*10), flags=cv.INTER_CUBIC)
# 1.42 ms ± 16.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
```

CPUと比較すると5倍程度速いです。まあ、相手がXeonじゃないとかなりわかりやすく速いんですが、GTX1080ではちょっと分が悪いみたいです。

```python
%%timeit
g_src.upload(src)
g_dst = cv.cuda.warpAffine(g_src, rot_affine, (w*10, h*10), flags=cv.INTER_CUBIC)
gpu_dst = g_dst.download()
# 2.64 ms ± 163 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
```

読み込み/書き出しをすると、画像が大きい分少し時間がかかっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726758/3a4bf6aa-593e-0b70-0309-1b104d02fbc3.png)

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

# 参考
[完全に理解するアフィン変換](https://qiita.com/koshian2/items/c133e2e10c261b8646bf)