OpenCVのcudaoptflowモジュールからNVIDIA Optical Flow SDKを使う

* この記事は[OpenCV Advent Calendar 2020](http://qiita.com/advent-calendar/2020/opencv)の8日目の記事です。
* 他の記事は[目次](https://qiita.com/advent-calendar/2020/opencv)にまとめられています。

## はじめに
<https://developer.nvidia.com/opticalflow-sdk>によるとTuring世代のGPUからオプティカルフローを計算するためのアクセラレータとしてOptical Flow Engineが搭載されており、開発者はオプティカルフロー算出処理をこのOptical Flow Engineにオフロードできるようになっています。また、OpenCVのcudaoptflowモジュールはNVIDIA Optical Flow SDK対応されており、OpenCV利用者は簡単に扱えるようになっています。本記事ではこの機能の使い方および使ってみた所感を述べることとします。

## NVIDIA Optical Flow SDKとは
NVIDIA Optical Flow SDKは、Turing世代のGPUに搭載されているOptical Flow Engineを使って算出したオプティカルフローを取得するためのSDKです。このSDKではDense flowを得ることができます。

Turing世代以降のGPUに搭載されているOptical Flow Engineにアクセスするための機能はNVIDIAグラフィックスドライバに含まれているようです（手元の環境だと`/usr/lib/x86_64-linux-gnu/libnvidia-opticalflow.so`にありました）。

Volta世代以前のGPUでもNVENCからMotion Vector（「動きベクトル」とも呼ばれる）を得ることができましたが、NVIDIA Optical Flow SDKで得られるフローと異なるようです。<https://devblogs.nvidia.com/an-introduction-to-the-nvidia-optical-flow-sdk/>に記載されているTuring世代とMaxwell/Pascal/Volta世代におけるNVENCのフローの違いを表した表を以下に引用します。

| | Turing Optical Flow | Maxwell/Pascal/Volta NVENC Motion Vectors |
|---|---|---|
|Granularity | Up to 4×4 | Up to 8×8 |
|Accuracy | Close to true motion Low average EPE (end-point error) | Optimized for video encoding; may differ from true motion Higher EPE|
|Intensity changes | Robust to intensity changes | Not robust to intensity changes|
|Software | Optical Flow SDK 1.0+, Q1 2019 | Video Codec SDK 7.0+, Available|

Volta世代以前のNVENCで得られるMotion Vectorだと動画エンコードに特化していたため、フローの精度がよくなかったり輝度変化に弱いという問題があったようですが、NVIDIA Optical Flow SDKだとその問題が改善されているようです。<https://devblogs.nvidia.com/an-introduction-to-the-nvidia-optical-flow-sdk/>のFigure.1を以下に引用します。

![fig1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31158/59d76210-df3f-f525-ed0a-ad461647f338.png)

このブログ記事には「NVIDIA Optical Flow SDKで得られるフローは、DNNベースのオプティカルフローアルゴリズムに比べるとフロー精度は一歩劣るものの処理時間においては大きなアドバンテージがある」という記載があります。

<https://developer.nvidia.com/blog/opencv-optical-flow-algorithms-with-nvidia-turing-gpus/>ではOpenCVで提供しているオプティカルフローアルゴリズムとフロー精度の比較が行われています。このブログのFigure.3を以下に引用します。

![fig3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31158/c7ab9242-2bba-75c1-8f55-640790e22a22.png)

このベンチマーク結果から、NVIDIA OpticalFlow SDKはOpenCVで提供しているオプティカルフローアルゴリズムと比較してAPE(Average Endpoint Error)が低い（＝フローの精度が高い）ことがわかります。

### サポートGPUに注意
余談ですが<https://developer.nvidia.com/opticalflow-sdk>には

> NVIDIA GeForce, Quadro and Tesla products with Turing(except TU117) and Ampere generation GPUs

とあり、NVIDIA Optical Flow SDKではTuring世代GPUのうち、TU 117をサポートしてないことが明記してあります。以前の公式ホームページでは「Turing世代のGPUだったら使える」という記載になっていましたが、手元でGeForce GTX 1650で実験していたところ、NVIDIA Optical Flow SDKのサポート外であることがわかり、フォーラムにて「誤解がないようにTU117では使えないと明記した方がよいのでは？」というフィードバックをしたため、この追記がなされたのだと思われます。
<https://forums.developer.nvidia.com/t/optical-flow-sdk-with-non-turing-gpu/71714/9>

ちょっとややこしいのですが、TU 117はTuring世代のGPUではあるもののENENCの機能自体はVolta世代相当という他のTuring世代のGPUと少し毛色の違うGPUであるためだと思われます。
<https://pc.watch.impress.co.jp/docs/column/hothot/1288472.html>

### OpenCV cudaoptflowモジュールのNVIDIA Optical Flow SDK対応
OpenCV対応のためにNVIDIAがヘッダファイルを<https://github.com/NVIDIA/NVIDIAOpticalFlowSDK/tree/nvof_1_0_bsd>で公開しており、<https://github.com/opencv/opencv_contrib/pull/2165>にてOpenCVのcudaoptflowモジュールにNVIDIA Optical Flow SDK対応が取り込まれています。

余談ですが、cudaoptflowモジュールのCMakeLists.txtで以下の記述がなされており、OpenCVのCMake実行時に前述のリポジトリからヘッダファイルを引っ張ってきていることがわかります。OpenCVのcudaoptflowモジュールからAPI呼び出し方法については後述します。

```cmake
set(NVIDIA_OPTICAL_FLOW_1_0_HEADERS_COMMIT "79c6cee80a2df9a196f20afd6b598a9810964c32")
set(NVIDIA_OPTICAL_FLOW_1_0_HEADERS_MD5 "ca5acedee6cb45d0ec610a6732de5c15")
set(NVIDIA_OPTICAL_FLOW_1_0_HEADERS_PATH "${OpenCV_BINARY_DIR}/3rdparty/NVIDIAOpticalFlowSDK_1_0_Headers")
ocv_download(FILENAME "${NVIDIA_OPTICAL_FLOW_1_0_HEADERS_COMMIT}.zip"
               HASH ${NVIDIA_OPTICAL_FLOW_1_0_HEADERS_MD5}
               URL
                 "https://github.com/NVIDIA/NVIDIAOpticalFlowSDK/archive/"
               DESTINATION_DIR "${NVIDIA_OPTICAL_FLOW_1_0_HEADERS_PATH}"
               STATUS NVIDIA_OPTICAL_FLOW_1_0_HEADERS_DOWNLOAD_SUCCESS
               ID "NVIDIA_OPTICAL_FLOW"
               UNPACK RELATIVE_URL)
```

<https://github.com/opencv/opencv_contrib/blob/4.5.0/modules/cudaoptflow/CMakeLists.txt#L11-L28>

## 動作確認環境
筆者は以下の環境で動作確認しました。

### ソフトウェア
* Ubuntu 20.04 64bit
* CMake 3.16.3
* gcc 9.3.0
* CUDA 11.1
* NVIDIA Optical Flow SDK 1.0
* nvidia-driver 455.32
* [OpenCV 4.5.0](https://github.com/opencv/opencv/releases/tag/4.5.0)

### ハードウェア
* CPU：Intel Core i7-9800X CPU @ 3.80GHz
* メモリ：32GB
* GPU：NVIDIA GeForce RTX 2080 Ti 11GB

## NVIDIA Optical Flow SDKをOpenCVから使ってみる
### 前準備
OpenCVのcudaoptflowモジュールからNVIDIA Optical Flow SDKを使うために以下の前準備が必要です。

* Turing世代以降のGPUを用意する
* <https://developer.nvidia.com/opticalflow-sdk>の動作要件を満たしたNVIDIA Driverをインストールする
* CUDA ToolKitをインストールする
* 以下のビルド設定でOpenCVをビルドする（opencv_contribあり）
  * [x] WITH_CUDA
  * [x] BUILD_opencv_cudaoptflow 

### 使い方
API仕様については基本的には<https://docs.opencv.org/4.5.0/dc/d9d/classcv_1_1cuda_1_1NvidiaOpticalFlow__1__0.html>を読めばよいです。また、サンプルコードは<https://github.com/opencv/opencv_contrib/blob/4.5.0/modules/cudaoptflow/samples/nvidia_optical_flow.cpp>にあります。

サンプルコードから要点だけ抜き出すと、まずは以下のようにAPIを呼び、インスタンスを生成します。

```cpp
Ptr<NvidiaOpticalFlow_1_0> nvof = NvidiaOpticalFlow_1_0::create(
            frameL.size().width, frameL.size().height, perfPreset,
            enableTemporalHints, enableExternalHints, enableCostBuffer, gpuId);
```

このインスタンス生成時にpresetが設定できます。フローの精度を優先するか、処理速度を優先するかがこのpresetによって切り替えられます。

* NV_OF_PERF_LEVEL_SLOW
* NV_OF_PERF_LEVEL_MEDIUM
* NV_OF_PERF_LEVEL_FAST

<https://docs.opencv.org/4.5.0/dc/d9d/classcv_1_1cuda_1_1NvidiaOpticalFlow__1__0.html#a6ff2c6689c0b236d6d18226b9698883b>

その後、calcメソッドでフローを計算し、元画像の解像度に合わせるためにupSamplerメソッドを呼ぶという感じです（upSamplerメソッド以外はOpenCVの既存のオプティカルフローのAPIと大体似たような感じですね）。最終的に`upsampledFlowXY`にdense flowの画像が格納されています。

```cpp
nvof->calc(frameL, frameR, flowxy);
nvof->upSampler(flowxy, frameL.size().width, frameL.size().height,
            nvof->getGridSize(), upsampledFlowXY);
```

<https://developer.nvidia.com/blog/opencv-optical-flow-algorithms-with-nvidia-turing-gpus/>によるとPython binding対応されているみたいです（筆者はPythonからこの機能を使ったことがないので現時点でこのコードが動くのかよくわかりません）。

```python
import numpy as np 
import cv2

frame1 = (cv2.imread('basketball1.png', cv2.IMREAD_GRAYSCALE))
frame2 = (cv2.imread('basketball2.png', cv2.IMREAD_GRAYSCALE))
nvof = cv2.cuda_NvidiaOpticalFlow_1_0.create(frame1.shape[1], frame1.shape[0], 5, False, False, False, 0)
flow = nvof.calc(frame1, frame2, None)
flowUpSampled = nvof.upSampler(flow[0], frame1.shape[1], frame1.shape[0], nvof.getGridSize(), None)
cv2.writeOpticalFlow('OpticalFlow.flo', flowUpSampled)
nvof.collectGarbage()
```

## フロー結果
OpenCVのサンプルデータを入力してDense flowを出力してみました。
※Qiitaに動画を直接アップできないのでgifアニメにしています。

### 1920x1080.avi

![1920x1080.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31158/dd759c2a-498a-4060-78f8-7b1e631041bd.gif)

<https://github.com/opencv/opencv_extra/blob/4.5.0/testdata/cv/video/1920x1080.avi>

### vtest.avi

![vtest.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31158/a3e7f395-d8fc-233c-6af1-20f38b8044cd.gif)

<https://github.com/opencv/opencv/blob/4.5.0/samples/data/vtest.avi>

## 処理速度
以下の条件で処理速度計測を行いました。

* 前述の「動作確認環境」に記載されているハードウェア、ソフトウェアを使用する
* テストデータは<https://github.com/opencv/opencv_extra/blob/4.5.0/testdata/cv/video/1920x1080.avi>を用いる
* presetとして`NV_OF_PERF_LEVEL_SLOW`を指定する
* 処理速度に関しては`calc`メソッドの処理時間を計測する
* 最初のフレームを除外し、それ以降のフレームでフロー計算に要した処理時間の平均値を取る

計測結果は以下の通りです。FPS換算だと約170fps出ていることがわかります。

```
time = 5.9258 [ms]
```

念のためこの処理時間が妥当なのか（＝OpenCV対応により遅くなっていないか）をチェックするためにNVIDIA Optical Flow SDK公式サンプルコードであるAppOFCudaを使ってFPSを計測してみます。

```bash
$ ./AppOFCuda/AppOFCuda --input=data/%06d.png -output=output/ --measureFPS=true --gridSize=4 --preset=slow
＜中略＞
Time = 0.0227777 s, NvOF FPS = 175.61
```

この結果からも約170fpsというのは妥当なようです。一方で[公式ページ](https://developer.nvidia.com/opticalflow-sdk)に書かれている

> Performance: Up to 150 fps at 4K resolution*
> 
> *Clock and preset dependent

と今回の計測結果とでかなり処理時間にギャップがあるのが気になってきます。そのため、試しにOpenCVサンプルコードでpresetとして`NV_OF_PERF_LEVEL_FAST`を指定したところ

```
time = 2.58953 [ms]
```

という結果（約386fps）になったことから、NVIDIA Optical Flow SDK公式ページのパフォーマンスは`NV_OF_PERF_LEVEL_FAST`の結果であると推察されます（公式ページでパフォーマンスを書いてくれるのはいいけど計測条件は明記してほしい・・・）。

## 所感
今回、OpenCVのcudaoptflow経由でNVIDIA Optical Flow SDKを軽く触ってみた所感は以下の通りです。

* dense flow計算がとても速い（小並感）
* <https://developer.nvidia.com/blog/opencv-optical-flow-algorithms-with-nvidia-turing-gpus/>によるとOpenCVの既存のフロー算出アルゴリズムより精度が高い
* ただ、内部でどんなアルゴリズムで動いているのかわからないのが気持ち悪い
  * <https://devtalk.nvidia.com/default/topic/1067514/video-codec-and-optical-flow-sdk/about-the-algorithm-of-turing-optical-flow/>で質問中・・・だが回答がない

上記のことから、内部でどんなアルゴリズムで動いているのかはあまり気にしなくて、そこそこの精度のフローを高速で計算できると嬉しい人には需要がありそうです。

## おわりに
本記事ではOpenCVのcudaoptflowモジュールからNVIDIA Optical Flow SDKを扱う方法、所感を書いてみました。Turing世代以降のGPUを用意するというのが人によってはハードル高いかもしれませんが、そこさえクリアできれば用途によっては使い道がありそうな感じがします。とはいえ、内部でどんなアルゴリズムが動いているのかわからないとホビーレベルの域から外れた用途で使いにくい気がするので、ざっくりとしたレベルでもいいからアルゴリズムを開示して欲しいなーという気持ちがあります。

明日は[kounoike](https://twitter.com/ko_noike)さんの記事です、楽しみにしましょう！

## Appendix
### 視差計算、トラッキング機能
<https://docs.nvidia.com/video-technologies/optical-flow-sdk/index.html>を読むとNVIDIA Optical Flow SDK 2.0には視差計算やトラッキング機能が追加されたようです（Optical Flow Engineの詳細は公開されていませんが、マッチングに関するハードウェアが入っているんでしょうか？）ただし、OpenCV 4.5.0時点ではOpenCV APIからこれらの機能を触ることができない点に注意が必要です。

### NVIDIA OpticalFlow SDK 2.0
実は<https://github.com/NVIDIA/NVIDIAOpticalFlowSDK/tree/nvof_2_0_bsd>でNVIDIA OpticalFlow SDK 2.0のヘッダファイルが公開されていたりするのですが、執筆時点ではOpenCV自体がNVIDIA OpticalFlow SDK 2.0に対応していません。手元で暫定的に対応したコードは作ってあるので整理してPRを投げる予定です。

また、公式ホームページによるとNVIDIA OpticalFlow SDK 2.0からの変更内容として以下のものがあるようです。

> * NEW to 2.0: - Support for Ampere generation GPUs, with improved optical flow hardware engine, independent of NVENC
> * NEW to 2.0: - Increased accuracy in flow vector’s cost, which indicates the confidence of the vector
> * NEW to 2.0: - Ability to get flow vectors for a specific region of interest
> * NEW to 2.0: - Hardware optical-flow-assisted object tracker library
> * NEW to 2.0: - Support for 1x1 and 2x2 grid sizes

公式ドキュメントから抜粋した表を引用します。

|Hardware Features|Turing|GA100 and above|
|:-:|:-:|:-:|
|Optical flow and stereo mode|Y|Y|
|Support for external hints|Y|Y|
|4x4 grid size|Y|Y|
|2x2 and 1x1 grid size|N|Y|
|Hardware cost|N|Y|
|Region of interest (ROI) optical flow calculation|N|Y|
|Maximum supported resolution|4096x4096|4096x4096|

個人的にはグリッドサイズを細かくしたり、フロー計算のROIを指定できる機能が気になったりするのですが、<https://docs.nvidia.com/video-technologies/optical-flow-sdk/nvofa-application-note/index.html#nvof-api>によると、NVIDIA OpticalFlow SDK 2.0から増えた機能はAmpere世代のGA100以降のGPUでないと使えないものが多く、手元にAmpere世代のGPUがないためこれらの機能は試せていません。残念・・・。

### オープンソース化
<https://developer.nvidia.com/opticalflow-sdk>に「Get Source Code」というリンクがあり、そこをクリックすると<https://developer.nvidia.com/optical-flow-sdk-source-code>に飛びます。そこには

> Request Access to Optical Flow SDK Source Code 

と書いてあり、リクエストを送るとNVIDIA Optical Flow SDKのソースコードを開示してもらえるようです（筆者はこのリクエストを送っていないのでどの部分まで開示されているのかわかりません・・・）。興味のある人はアクセスしてみると良いかもしれません。

## 参考URL
* <https://developer.nvidia.com/opticalflow-sdk>
* <https://docs.nvidia.com/video-technologies/optical-flow-sdk/index.html>
* <https://devblogs.nvidia.com/an-introduction-to-the-nvidia-optical-flow-sdk/>
* <https://developer.nvidia.com/blog/opencv-optical-flow-algorithms-with-nvidia-turing-gpus/>