NVIDIA CV-CUDAを動かしてみた - 情報まとめ -

情報(と頭の)整理を兼ねて、先日GTC Spring 2023のkeynoteで触れられたCV-CUDAについて興味があったので周辺情報も含めて個人の備忘録として簡易的に纏めてみたいと思います。このライブラリ、同じくkeynoteで触れられた[VPF](https://github.com/NVIDIA/VideoProcessingFramework)との相性も良さそうなので、余力があればそちらも後日まとめられたらなと思います。(願望)

# はじめに
本ブログの元情報は主に以下の4つになります。もし本まとめ記事で気になった部分があればぜひ以下のリンクから詳細をご確認下さい。
1. https://github.com/CVCUDA/CV-CUDA
2. https://cvcuda.github.io/
3. https://blogs.nvidia.com/blog/2022/09/20/computer-vision-cloud/
4.  https://developer.nvidia.com/zh-cn/blog/cv-cuda-high-performance-image-processing/

# CV-CUDAとは ?
"CV"と名前についている通り、オープンソースのCV(Computer Vision)処理GPUプリミティブです。2023年3月に公開され、2023年5月現在、v0.3.0-betaとして公開されています。(ライセンスはApache2.0です)
インターネットのトラフィックの大半は動画像であり、それらに対してAIベースの画像処理パイプラインを構築するケースが増えています。CVベースの前処理や後処理が必要であるケースも多く、CV-CUDAはそれらのパイプラインをGPUで高速化する事を目的としています。

CVCUDAはC/C++/Pythonの3つの言語から呼び出す事が可能であり、画像処理結果、オペレータなどOpenCVとできるだけ揃えるようにデザインし、OpenCVからCVCUDAへの移行がしやすいように設計されているとの事です。

以下がCVCUDAの全体アーキテクチャ図になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125731/d36a708a-4460-84c2-4f97-c175f5f9de12.png)
<font color="Green">図1: CVCUDA Architecture Overview
※[Tech Blog:CV-CUDA high-performance image processing acceleration library](https://developer.nvidia.com/zh-cn/blog/cv-cuda-high-performance-image-processing/)より引用</font>

CV-CUDAにはメインのモジュールとして以下の3つが存在します。

- __NVCV__
    - 画像抽象化クラス、バッチクラス、メモリマネージメントクラスなどCVCUDAのコア機能が実装されている
- __Operators__
    - リサイズ、各種フィルタ処理などCV演算が実装されている
- __CV-CUDA Tools__
    - オペレータ開発時に必要な様々な機能が実装されている

CVCUDAはnumpyやpytorchなどAIワークロード開発に使われるライブラリとも簡単に連携可能です。またスループットが出るようにバッチ処理にも対応しています。以下はCVCUDA各種オペレータの性能ベンチマークになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125731/1d81fe6b-4ef1-6f9f-5f6e-354a5f685681.png)
<font color="Green">図2: CV-CUDA performance comparison ( CPU: Intel(R) Core(TM) i9-7900X CPU @ 3.30GHz ）
※[Tech Blog:CV-CUDA high-performance image processing acceleration library](https://developer.nvidia.com/zh-cn/blog/cv-cuda-high-performance-image-processing/)より引用</font>

# CV-CUDA オペレータ
"CV"とつくとてOpenCVのAPIカバレッジが気になる所。v0.3.0-beta時点でのCV-CUDAオペレータ一覧は[Developer Guide](https://github.com/CVCUDA/CV-CUDA/blob/release_v0.3.x/DEVELOPER_GUIDE.md)によると以下の通りです。

|オペレータ名|概要|
|---|---|
|[Adaptive Thresholding](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1AdaptiveThreshold.html#exhale-class-classcvcuda-1-1adaptivethreshold)|適応2値化|
|[AverageBlur](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1AverageBlur.html#exhale-class-classcvcuda-1-1averageblur)|平均ぼかし|
|[BilateralFilter](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1BilateralFilter.html#exhale-class-classcvcuda-1-1bilateralfilter)|バイラテラルフィルタ|
|[Bounding Box](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1BndBox.html#exhale-class-classcvcuda-1-1bndbox)|バウンディングボックスの描画|
|[Bounding Box Blur](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1BoxBlur.html#exhale-class-classcvcuda-1-1BoxBlur)|矩形選択部分へのぼかし|
|[CenterCrop](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1CenterCrop.html#exhale-class-classcvcuda-1-1CenterCrop)|クリッピング(トリミング)|
|[ChannelReorder](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1ChannelReorder.html#exhale-class-classcvcuda-1-1ChannelReorder)|画像チャンネルの順番をシャッフル|
|[Composite](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1Composite.html#exhale-class-classcvcuda-1-1Composite)|マスクに従って2枚の画像を合成|
|[Conv2D](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1Conv2D.html#exhale-class-classcvcuda-1-1Conv2D)|二次元畳み込み|
|[CopyMakeBorder](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1CopyMakeBorder.html#exhale-class-classcvcuda-1-1copymakeborder)|画像のパディング|
|[CvtColor](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1CvtColor.html#exhale-class-classcvcuda-1-1cvtcolor)|色空間変換|
|DataTypeConvert|画像データの型変換|
|[Erase](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1Erase.html#exhale-class-classcvcuda-1-1erase)|指定した領域を削除|
|[Flip](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1Flip.html#exhale-class-classcvcuda-1-1flip)|反転|
|[GammaContrast](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1GammaContrast.html#exhale-class-classcvcuda-1-1gammacontrast)|ガンマ補正|
|[Gaussian](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1Gaussian.html#exhale-class-classcvcuda-1-1gaussian)|ガウスぼかし|
|[JointBilateralFilter](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1JointBilateralFilter.html#exhale-class-classcvcuda-1-1jointbilateralfilter)|ジョイントバイラテラルフィルタ|
|[Laplacian](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1Laplacian.html#exhale-class-classcvcuda-1-1laplacian)|ラプラシアンフィルタ|
|[MedianBlur](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1MedianBlur.html#exhale-class-classcvcuda-1-1medianblur)|メディアンフィルタ|
|[Morphology](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1Morphology.html#exhale-class-classcvcuda-1-1morphology)|モルフォロジー変換|
|[Non-Max Suppression](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1NonMaximumSuppression.html#exhale-class-classcvcuda-1-1nonmaximumsuppression)|IOUと信頼度の閾値に基づき、重なり合う箱の集合から最適なバウンディングボックスを選択|
|[Normalize](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1Normalize.html#exhale-class-classcvcuda-1-1normalize)|正規化|
|[PadStack](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1PadAndStack.html#exhale-class-classcvcuda-1-1padandstack)|複数画像をパディングしてスタックする|
|[PillowResize](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1PillowResize.html#exhale-class-classcvcuda-1-1pillowresize)| python-pillow のアルゴリズムを使用したリサイズ|
|[Reformat](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1Reformat.html#exhale-class-classcvcuda-1-1reformat)|平面画像←→非平面画像|
|[Remap](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1Remap.html#exhale-class-classcvcuda-1-1remap)|幾何学変換|
|[Resize](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1Resize.html#exhale-class-classcvcuda-1-1resize)|リサイズ|
|[Rotate](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1Rotate.html#exhale-class-classcvcuda-1-1rotate)|回転|
|[Thresholding](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1Threshold.html#exhale-class-classcvcuda-1-1threshold)|2値化|
|[WarpAffine](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1WarpAffine.html#exhale-class-classcvcuda-1-1WarpAffine)|アフィン変換|
|[WarpPerspective](https://cvcuda.github.io/_exhale_api/classcvcuda_1_1WarpPerspective.html#exhale-class-classcvcuda-1-1WarpPerspective)|射影変換|

サポートしているオペレータ数はOpenCVに比べるとまだ少ないですが、ある程度CV界隈の人には直感的どのような処理が分かる命名になっていそうです。ブログによるとサポートするオペレータは今後も増える予定のようです。
(注: OpenCVライクな引数になっていますが引数など完全互換な訳ではなく癖はありそうです)

# CV-CUDAのインストール
[インストールガイド](https://cvcuda.github.io/installation.html)に従って[ここから](https://github.com/CVCUDA/CV-CUDA/releases)debianパッケージをダウンロードし、インストールします。
今回は、他のサーバでもCV-CUDAを試してみたかったので、実験環境を移動させやすいようにdocker container化する事にしましたが、勿論ホストに直接環境構築しても大丈夫です。CV-CUDAはC/C++から呼び出す事も可能ですが、今回はCV-CUDAのpython環境を構築する事にしました。

:::note warn
注:
github上のガイドにはPython wheelからのインストール方法の記載がありますが、v0.3.0-beta現在、wheelのインストールだけではエラーが出るような気がしますので(一部の必要モジュールが含まれない)、debianパッケージからのインストールがおススメです
:::

まずはCV-CUDAの実験用Dockerコンテナを作成します。
```ruby:CV-CUDA dockerコンテナの作成
$ wget https://gist.githubusercontent.com/acc-mu3n/afd50123316e43b6d49e8ecf8193908a/raw/ad5158680adab72c0d6afd9e39997cc666024c20/Dockerfile
$ mv Dockerfile ./Docker
$ docker build -t cvcuda-test:0.3.0-beta ./Docker 
```
今回はCuPyやPyTorch、OpenCVなど実験に使いそうなモジュールも一緒にインストールしていますが必須ではありません。
次に作成したコンテナを起動します。

```ruby:CV-CUDA dockerコンテナの起動
$ docker run --rm -it --network=host --gpus all -e DISPLAY=$DISPLAY -v $HOME/.Xauthority:/root/.Xauthority cvcuda-test:0.3.0-beta bash
```
cvcudaモジュールがimport出来るかテストします。
```ruby:cvcuda import テスト
$ python3 -c "import cvcuda"
```
これでCV-CUDAのインストールが完了しました。

# CV-CUDAのpythonインターフェースを触ってみる
## シンプルなコードでテスト

CV-CUDAの環境構築が出来たら、早速コードを実行してみようと思います。
以下は、画像を読み込み、いくつかのCVCUDAの関数を適用するというシンプルなコードのサンプルです。(フィルタは適当に選んだものです)
```python:cvcuda-simpe-test.py
import cvcuda
import cv2
import numpy as np
import cupy as cp

# load image
img = cvcuda.as_image(cp.asarray(cv2.imread("./tabby_tiger_cat.png")))
img_tensor = cvcuda.as_tensor(img)

print("Src image shape:",img_tensor.shape)

# resize image
resized_img = cvcuda.resize(img_tensor,(1,244,244,3),cvcuda.Interp.CUBIC)
print("Resized image shape:",resized_img.shape)

# cropping
cropped_img = cvcuda.center_crop(resized_img,[200,200])
print("Cropped image shape:",cropped_img.shape,"\n")

# apply filter to image
gauss_img = cvcuda.gaussian(cropped_img,[9,9],[0.5,0.5],cvcuda.Border.REFLECT)
bilat_img = cvcuda.bilateral_filter(gauss_img,40,20,20,cvcuda.Border.REFLECT)

# export to image
result = cp.asarray(bilat_img.cuda())
cv2.imwrite("result.png",cp.asnumpy(result[0]))

```
OpenCVの関数で同等の処理を実行した場合との比較をしたかった為、最終的には[このようなコード](https://gist.githubusercontent.com/acc-mu3n/afd50123316e43b6d49e8ecf8193908a/raw/1dcc8d5adde320f291b49c7fa11361ba41f05582/opencv-cvcuda-test.py)を作成し、実行しました。
```ruby:opencv-cvcuda-test.pyの実行
$ python3 opencv-cvcuda-test.py
cvcuda exec...
Src image shape: (1, 720, 720, 3)
Resized image shape: (1, 244, 244, 3)
Cropped image shape: (1, 200, 200, 3)

opencv exec...
Src image shape: (720, 720, 3)
Resized image shape: (244, 244, 3)
Cropped image shape: (200, 200, 3)
$ ls |grep .png
result.png
result_opencv.png
tabby_tiger_cat.png
```
result.pngがCVCUDA、result_opencv.pngはOpenCVを用いた画像処理の結果です。
|cvcuda(result.png)|opencv(result_opencv.png)|
|:-:|:-:|
|![result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125731/5f548861-0d89-06d3-4aa7-aebbb91edc56.png)|![result_opencv.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125731/82d05a96-1063-752d-f4a4-0cb664114aaf.png)|

OpenCVと違って境界処理を明示的に指定する必要があったり違いは感じたので、慣れるのに少し時間が必要かもしれませんが、推論の前処理後処理に便利そうです。ベータ版なのでこれからの進化にも期待したい所です。

# まとめ
今回は、AIベースの画像処理パイプラインの前処理、後処理に便利なCV-CUDAの概要、インストール方法、簡単な使い方を纏めました。
次回があれば、バッチ処理、動画の読み込みから推論までの一連のパイプラインの組み方、性能計測の為のプロファイリング等々してみようかなと思います。つづく(かも)