CUDAを使ってCPU vs GPUしたった

# Introduction
ゆる〜く自作中のwebサイト( https://featurepoints.jp/opencv_doc/ )を紹介します。
[OpenCV.jp : OpenCV逆引きリファレンス cookbook](http://opencv.jp/cookbook/)のパクリなんすけど、このページが制作されたのはOpenCV2系が出始めて数年経った2011年くらいとちょっと古いです。なおかつ、それ以降更新していないです。
基本的な画像処理の種類が掲載されておって、何か処理かけてみようと思った時はこのサイトを見ています。処理後の結果の画像もサンプルコードと一緒に掲載されているのでずっと重宝してみてます。
てか、[OpenCV2 プログラミングブック](https://book.mynavi.jp/ec/products/detail/id=22688)の付録ページなんすよねこれ。(一応書籍は買いました。)

cookbookが出てから今までの7,8年の間で、つい最近論文発表された画像処理技術のアルゴリズムが次々とpushされています。今作っているサイトは、OpenCV3系以降で登場した画像処理手法をもくもくと書き込んでいたところです。

で、いま着手していたのは `cudaimgproc` `cudafilters` で、GPUを使った画像処理をしていたとこでした。

# Motive
ここでは、 CPUとGPUの両方ある画像処理関数・クラスを使ってどちらが高速で処理されるかを試してみようと思います。

# Device

## hardware
- CPU AMD Ryzan 5 1400
- GPU NVIDIA GeForce GTX960 (CUDAコア数:1024、GPUメモリ:2GB)
- マザーボード  MSI B450 GAMING PLUS MAX B450
- メモリ DDR4 8G * 4枚 = 32GB

([中古パーツでDeepLearning専用の自作PCを組み立てみた](https://qiita.com/satsukiya/items/90dffb6c70cf6fa9d0fe) を参照)

## software
- OS ubuntu 18.04
- NVIDIA Driver 455.45.01
- CUDA 11.1
- cuDNN 8.0.5
- OpenCV 4.5.0

# Dataset
~~[The PASCAL Visual Object Classes Challenge 2007](http://host.robots.ox.ac.uk/pascal/VOC/voc2007/index.html)~~ にある訓練データとテストデータ9966枚を使っています。
あとはコード上でサイズを4倍にしており、通常サイズ幅高さ400〜500を幅高さ 1200〜1600にして処理をかけています。
なぜかリンク切れ。ダウンロードしたい場合はここから取得。[Pascal VOC Dataset Mirror](https://pjreddie.com/projects/pascal-voc-dataset-mirror/)

# Development

GPUで画像を処理するフローは下記の通りです。
1. CPUからGPUに画像データを転送
2. GPU内で画像処理
3. GPUからCPUに画像データを転送

コード上ではこんな感じ。

```C++
    cv::Mat mat = cv::imread("sample.png"), dst;
    cv::cuda::GpuMat gpu_src, gpu_bigmat, gpu_dst;
    
    //1. CPUからGPUに画像データを転送
    gpu_src.upload(mat);

    //2. GPU内で画像処理
    //[TODO] ここで画像処理の内容を書く
    cv::cuda::resize(gpu_src, gpu_bigmat, cv::Size(), __MAG__, __MAG__);

    //3. GPUからCPUに画像データを転送
    gpu_dst.download(dst);
    
    cv::imwrite("output.png", dst);
```

GPU内の画像データはそのまま表示や保存ができないので、 `gpu_dst.download(dst);` でCPUに一度転送する必要があります。

それで画像処理のコードを書くことになるのですが、関数のみとクラスを使った実装の2パターンあります。
クラスを使う場合は、画像処理用のオブジェクトを生成して `apply` `detect` のメソッドを呼び出して処理をかけます。

```C++

#関数のみ
cv::cuda::bilateralFilter(gpu_bigmat, gpu_dst, 20, 90, 40);

#クラスあり
cv::Ptr<cv::cuda::Filter> sobelobj = cv::cuda::createSobelFilter(gpu_gray.type(), gpu_dst.type(), 1, 1, 5);
sobelobj->apply(gpu_gray, gpu_dst);
```

詳しくは次節のコードカラムのリンクにて参照してください。


また余談ですが、ファイル操作と時間計測は `boost` を使っています。

```c++
//ファイル操作
#include <boost/filesystem.hpp>
#include <boost/foreach.hpp>
namespace fs = boost::filesystem;

//フォルダ内のファイルパスを取得
BOOST_FOREACH(const fs::path& p, std::make_pair(fs::directory_iterator(dir),
                                                fs::directory_iterator())) {
    if (!fs::is_directory(p)){
        std::string parent_path = p.parent_path().c_str();  
        std::string slash = "/";
        std::string filename = p.filename().c_str();
        std::string full_path = parent_path + slash + filename;
        std::cout << full_path << std::endl;
    }
}

// 時間計測
#include <boost/timer/timer.hpp>
boost::timer::cpu_timer timer;
std::string result = timer.format(); // 結果文字列を取得する
std::cout << result << std::endl;
```

# Consequence

| 関数名 | 説明 | CPU [sec] | GPU [sec] | CPU ? GPU (compare) | コード |備考|
|:---|:---|:---:|:---:|:---:|:---:|:---|
|cv::cuda::createLaplacianFilter|ボカシ処理|102.195|62.561| << GPU |[LINK](https://featurepoints.jp/opencv_doc/LaplacianFilter/)||
|cv::cuda::createGaussianFilter|ボカシ処理|85.658|97.908| CPU => |[LINK](https://featurepoints.jp/opencv_doc/GaussianFilter/)||
|cv::cuda::createSobelFilter|輪郭抽出|124.375|61.005|<<< GPU|[LINK](https://featurepoints.jp/opencv_doc/SobelFilter/)||
|cv::cuda::createScharrFilter|輪郭抽出|100.278|58.467| << GPU |[LINK](https://featurepoints.jp/opencv_doc/ScharrFilter/)||
|cv::cuda::createMorphologyFilter|膨張・収縮処理|66.938|199.362| CPU >> |[LINK](https://featurepoints.jp/opencv_doc/MorphologyFilter/)||
|cv::cuda::bilateralFilter|ボカシ処理|4510.572|494.816| <<<<< <<<<< GPU|[LINK](https://featurepoints.jp/opencv_doc/bilateralFilter/)||
|createCannyEdgeDetector|輪郭抽出|79.006|123.397|CPU >|[LINK](https://featurepoints.jp/opencv_doc/Canny/)||
|cv::cuda::createHoughLinesDetector|直線抽出|224.101|51.030| <<<< GPU| [LINK](https://featurepoints.jp/opencv_doc/HoughLine/) |resizeしないで、等倍で処理|
|cv::cuda::createHoughCirclesDetector|円抽出|1265.655|85.629|<<<<< <<<<< <<<<< GPU|[LINK](https://featurepoints.jp/opencv_doc/HoughCircle/)|resizeしないで、等倍で処理<br> 処理結果がCPUとGPUとで異なる。( https://github.com/opencv/opencv/issues/7830 を参照)|
|cv::cuda::createHarrisCorner|コーナー検出|146.705|140.672| ≒ |[LINK](https://featurepoints.jp/opencv_doc/CornerHarris/)|resizeしないで、等倍で処理|
|cv::cuda::createMinEigenValCorner|コーナー検出|53.074|47.270| <≒ GPU |[LINK](https://featurepoints.jp/opencv_doc/CornerMinEigenVal/)|resizeしないで、等倍で処理|


# Thoughts
重たい処理するとGPUの効力が発揮されます。特にbilateralFilterは10倍以上違います。
一方、処理がシンプルなものはGPU間の転送時間がネックとなってCPUのみで処理した方が速かったりします。


# Future
`cv::cuda::createTemplateMatching` を使ってCPUとどのくらい高速で判別するかを検証しようと思います。

# ☞ To Be Continued
次は [@kazuki-ma](https://qiita.com/kazuki-ma)さんです。
ちょっと早いですが、、、、良いお年を〜。

# Reference

- [GpuMatの内部を探検してみる](https://proc-cpuinfo.fixstars.com/2016/08/gpumat-html/)
- [GpuMatの画像データをhighguiのウィンドウに表示する](https://qiita.com/dandelion1124/items/9766a854b9085217fea5)