OpenCVとNPPの連携

この記事は[OpenCV Advent Calendar 2016](http://qiita.com/advent-calendar/2016/opencv)の11日目の記事です．

# はじめに
この記事ではOpenCVと[NVIDIA Performance Primitives library (NPP)](https://developer.nvidia.com/npp)の連携方法について紹介します．

# NPPとは
[NVIDIA Performance Primitives library (NPP)](https://developer.nvidia.com/npp)とは画像処理，信号処理の各種アルゴリズムをCUDA実装したライブラリです．雑に紹介するとIPPのCUDA版みたいなものです．[CUDAオンラインドキュメント](http://docs.nvidia.com/cuda/npp/index.html)にNPPドキュメント（PDFファイル）へのリンク一覧があるので興味がある方は読んでみるとよいでしょう．
※対応するデータ型，チャンネル数などによってAPIが分かれているのでページ数がそれなりにあります．

また，NPPは機能毎にモジュール化されており，NPPで用意されているモジュールは以下の通りです．

* **nppc**：core
* **nppi**：image processing
* **npps**：signal processing

そのため，使いたい機能が入ったモジュールに対応したライブラリをリンクしてください（※**nppc**は必須）．

## NPPの機能
NPPではどういったことができるのか気になる方もいらっしゃると思いますが，[NPP公式サイト](https://developer.nvidia.com/npp)には以下のように紹介されています．

* Eliminates unnecessary copying of data to/from CPU memory
  * Process data that is already in GPU memory
  * Leave results in GPU memory so they are ready for subsequent processing
* Data Exchange and Initialization
  * Set, Convert, Copy, CopyConstBorder, Transpose, SwapChannels
* Arithmetic and Logical Operations
  * Add, Sub, Mul, Div, AbsDiff, Threshold, Compare
* Color Conversion
  * GBToYCbCr, YcbCrToRGB, YCbCrToYCbCr, ColorTwist, LUT_Linear
* Filter Functions
  * FilterBox, Filter, FilterRow, FilterColumn, FilterMax, FilterMin, Dilate, Erode, SumWindowColumn, SumWindowRow
* JPEG
  * DCTQuantInv, DCTQuantFwd, QuantizationTableJPEG
* Geometry Transforms
  * Mirror, WarpAffine, WarpAffineBack, WarpAffineQuad, WarpPerspective, WarpPerspectiveBack, WarpPerspectiveQuad, Resize
* Statistics Functions
  * Mean_StdDev, NormDiff, Sum, MinMax, HistogramEven, RectStdDev

この説明はあくまでざっくりとしたものなので，詳細を知りたい方は[CUDAオンラインドキュメント](http://docs.nvidia.com/cuda/npp/index.html)にあるPDFドキュメントを読むことをオススメします．

# OpenCVとNPPの関係性
OpenCV cudaモジュールの一部関数は内部的にNPP関数を呼んでいます．
以下にいくつかその例を紹介します．

* [BoxFilter](https://github.com/opencv/opencv/blob/3.1.0/modules/cudafilters/src/filtering.cpp#L129)
* [Image Rotation](https://github.com/opencv/opencv/blob/3.1.0/modules/cudawarping/src/warp.cpp#L475)
* [warpAffine](https://github.com/opencv/opencv/blob/3.1.0/modules/cudawarping/src/warp.cpp#L243)

ただし，アルゴリズムは同じでもAPIに与えられるパラメータ，実装がOpenCVと異なるものはNPPを使わず，OpenCV同梱のCUDAカーネルを呼んでいます．特にwarpAffineなどは，

```cpp:
bool useNpp = borderMode == BORDER_CONSTANT && ofs.x == 0 && ofs.y == 0 && useNppTab[src.depth()][src.channels() - 1][interpolation];
```

というようにNPPで実装されているものかどうかを判定して，NPPとOpenCV内のCUDAカーネルを呼び分けています．

# NPPを直接使うメリット
OpenCVのcudaモジュールに同梱されるCUDAカーネルは基本的に特定アーキテクチャに対する最適化が行われているわけではありません．一方でNPPはアーキテクチャ更新に対応して定期的にバージョンアップされていることからNPPを直接叩いたほうがよいケースもあります．

# OpenCVと連携するための前準備
OpenCVとNPP連携して使うためには前準備，事前知識が必要なので簡単に説明していきます．

## CMakeLists.txtを書く
OpenCVとNPPを使ったプログラムをコンパイルするにはCMakeLists.txtで書いちゃうのが簡単です．
大まかな流れはこんな感じです．

1. <code>find_package</code>でCUDAを探す ※詳細は[CMakeドキュメント（FindCUDA）](https://cmake.org/cmake/help/v3.7/module/FindCUDA.html)を参照ください
1. NPPのヘッダファイルがあるパス（<code>${CUDA_INCLUDE_DIRS}</code>）をインクルードパスに設定する
1. NPPライブラリをリンクする

この流れに従ったCMakeLists.txtのサンプルを以下に示します．

```cmake:
cmake_minimum_required(VERSION 2.8)
add_executable(npp_test main.cpp)

# OpenCVを見つける
find_package(OpenCV REQUIRED)

# CUDAを見つける
find_package(CUDA REQUIRED)

if(OpenCV_FOUND)
  if(CUDA_FOUND)
    include_directories(${CUDA_INCLUDE_DIRS} ${OpenCV_INCLUDE_DIRS})

    # NPPライブラリをリンクする
    target_link_libraries(
      npp_test 
      ${CUDA_npp_LIBRARY} 
      ${CUDA_nppc_LIBRARY} 
      ${CUDA_nppi_LIBRARY}
      ${CUDA_npps_LIBRARY} 
      ${OpenCV_LIBS})
  endif(CUDA_FOUND)
endif(OpenCV_FOUND)
```

## よく使うNPPのデータ型を知る
NPPを使った実装を行う前によく使うNPPのデータ型について紹介します．

### Basic NPP Data Types

NPPで定義される基本型は以下の通りです．
以下の表からもわかるように<code>Npp<ビット数><データ型></code>という命名規則になっています．

| NPPで定義されるデータ型 | 実際のデータ型 |
|:---------------------|:------------|
| Npp8u  | 8-bit unsigned char |
| Npp8s  | 8-bit signed char |
| Npp16u  | 16-bit unsigned integer |
| Npp16s  | 16-bit signed integer |
| Npp32u  | 32-bit unsigned integer |
| Npp32s  | 32-bit signed integer |
| Npp64u  | 64-bit unsigned integer |
| Npp64s  | 64-bit signed integer |
| Npp32f  | 32-bit (IEEE) floating-point number |
| Npp64f  | 64-bit floating-point number |

### NppiPoint構造体
2次元座標を扱う構造体でデータフィールドは以下の通りです．

* int x
* int y

以下にNppiPoint構造体の初期化を行う例を示します．

```cpp:
int x        = 0;
int y        = 0;
NppiPoint pt = {x, y};
```

### NppiRect構造体
矩形領域の情報を扱う構造体でデータフィールドは以下の通りです．

* int x
* int y
* int width
* int height

以下にNppiRect構造体の初期化を行う例を示します．

```cpp:
int x         = 0;
int y         = 0;
int width     = 320;
int height    = 240;
NppiRect rect = {x, y, width, height};
```


### NppiSize構造体
サイズ情報を扱う構造体でデータフィールドは以下の通りです．

* int width
* int height

以下にNppiSize構造体の初期化を行う例を示します．

```cpp:
int width    = 320;
int height   = 240;
NppiSize roi = {width, height};
```

### NppStatus
ステータスコードのenum値です．NPP関数は戻り値としてステータスコードを返すようになっています．
<code>NPP_NO_ERROR</code>，<code>NPP_SUCCESS</code>（意味としては<code>NPP_NO_ERROR</code>と等価）は正常に関数が実行されたことを意味しています．
そのため，実際に使うときにはNPP関数の戻り値が<code>NPP_SUCCESS</code>なのかチェックしておくとよいでしょう．

## 画像データのアクセス方法
<code>cv::cuda::GpuMat</code>に格納されている画像データをNPPの関数に渡すために（個人的に）よく使う方法は以下の2通りです．

* <code>cv::cuda::GpuMat</code>クラスの**ptr**メソッド（[公式ドキュメント](http://docs.opencv.org/3.1.0/d0/d60/classcv_1_1cuda_1_1GpuMat.html#af0389962003eec644007c7ca85a04f6d)）にて画像データのポインタを取得する
* <code>cv::cuda::GpuMat</code>クラスのメンバ変数**datastart**（[公式ドキュメント](http://docs.opencv.org/3.1.0/d0/d60/classcv_1_1cuda_1_1GpuMat.html#ade4a4dfc61facd5f18143b4c9d56dbae)）を渡す

前者だとこんな感じです．

```cpp:
nppiFilterMedian_8u_C1R(d_src.ptr<Npp8u>(0), nSrcStep, d_dst.ptr<Npp8u>(0), nDstStep, roi, mask, anchor, d_median_filter_buffer);
```

一方後者だとこんな感じです．

```cpp:
nppiFilterMedian_8u_C1R(d_src.datastart, nSrcStep, d_dst.datastart, nDstStep, roi, mask, anchor, d_median_filter_buffer);
```

# OpenCVとNPPを連携したコードを実装してみる
というわけでこれまで長々（？）と前提となる話をして準備が整ったので，OpenCVとNPPを連携したサンプルコードを以下に示します．

```cpp:
// OpenCV header file
#include <opencv2/core.hpp>
#include <opencv2/core/cuda.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>

// NPP header file
#include <npp.h>

#include <iostream>

int main(int argc, char *argv[])
{
    const NppLibraryVersion *libVer = nppGetLibVersion();
    std::cout << "NPP Library Version: " << libVer->major << "." << libVer->minor << "." << libVer->build << std::endl;

    // GpuMat
    cv::Mat src = cv::imread("lena.jpg", cv::IMREAD_GRAYSCALE);
    cv::Mat dst = cv::Mat(src.size(), src.type());
    cv::cuda::GpuMat d_src(src);
    cv::cuda::GpuMat d_dst(dst);

    // NPP
    unsigned int width  = d_src.cols;
    unsigned int height = d_src.rows;
    NppiSize roi        = {width, height};
    NppiSize mask       = {7, 7};
    NppiPoint anchor    = {0, 0};

    // create temporary buffer
    Npp32u nBufferSize            = 0;
    Npp8u *d_median_filter_buffer = NULL;
    NppStatus status = nppiFilterMedianGetBufferSize_8u_C1R(roi, mask, &nBufferSize);
    if(status != NPP_SUCCESS)
    {
        std::cout << "[NPP ERROR] status = " << status << std::endl;
        return -1;
    }
    cudaMalloc((void **)(&d_median_filter_buffer), nBufferSize);

    Npp32s nSrcStep = d_src.step;
    Npp32s nDstStep = d_dst.step;
    status = nppiFilterMedian_8u_C1R(d_src.datastart, nSrcStep, d_dst.datastart, nDstStep, roi, mask, anchor, d_median_filter_buffer);
    if(status != NPP_SUCCESS)
    {
        std::cout << "[NPP ERROR] status = " << status << std::endl;
    }

    // free temporary buffer
    cudaFree(d_median_filter_buffer);

    // display result image
    d_dst.download(dst);
    cv::imshow("dst", dst);
    cv::waitKey(0);
    cv::destroyAllWindows();

    return 0;
}
```

このプログラムの実行結果は以下の通りです．

| ![入力画像](https://qiita-image-store.s3.amazonaws.com/0/31158/eb28b2f9-dc23-01b2-e4ca-2388c63d8e2a.png "入力画像") |
|:--:|
| 入力画像 |

| ![出力画像](https://qiita-image-store.s3.amazonaws.com/0/31158/435a4a27-bd5f-2ecc-5cad-e845dd3fa469.png "出力画像") |
|:--:|
| 出力画像 |

# おわりに
この記事ではOpenCVと[NVIDIA Performance Primitives library (NPP)](https://developer.nvidia.com/npp)の連携方法について紹介しました．
この記事をきっかけにNPP使ってみようかなと思ってくれる方が増えると幸いです．

**そして，まだOpenCVのNPP統合できていないところを対応してぜひPull Requestを送りましょう！（これが言いたかった）**

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

* CPU：Intel Core i7-6700HQ
* メモリ：64GB
* GPU：NVIDIA GeForce GTX 1060 / 6GB
* Ubuntu 16.04 LTS(64bit)
* [OpenCV 3.1.0](https://github.com/Itseez/opencv/releases/tag/3.1.0)
* CUDA 8.0
* gcc 5.4.0