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


Introduction

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

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を組み立てみた を参照)

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 にある訓練データとテストデータ9966枚を使っています。
あとはコード上でサイズを4倍にしており、通常サイズ幅高さ400〜500を幅高さ 1200〜1600にして処理をかけています。
なぜかリンク切れ。ダウンロードしたい場合はここから取得。Pascal VOC Dataset Mirror

Development

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

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

    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 のメソッドを呼び出して処理をかけます。


#関数のみ
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 を使っています。

//ファイル操作
#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
cv::cuda::createGaussianFilter ボカシ処理 85.658 97.908 CPU => LINK
cv::cuda::createSobelFilter 輪郭抽出 124.375 61.005 <<< GPU LINK
cv::cuda::createScharrFilter 輪郭抽出 100.278 58.467 << GPU LINK
cv::cuda::createMorphologyFilter 膨張・収縮処理 66.938 199.362 CPU >> LINK
cv::cuda::bilateralFilter ボカシ処理 4510.572 494.816 <<<<< <<<<< GPU LINK
createCannyEdgeDetector 輪郭抽出 79.006 123.397 CPU > LINK
cv::cuda::createHoughLinesDetector 直線抽出 224.101 51.030 <<<< GPU LINK resizeしないで、等倍で処理
cv::cuda::createHoughCirclesDetector 円抽出 1265.655 85.629 <<<<< <<<<< <<<<< GPU LINK resizeしないで、等倍で処理
処理結果がCPUとGPUとで異なる。( https://github.com/opencv/opencv/issues/7830 を参照)
cv::cuda::createHarrisCorner コーナー検出 146.705 140.672 LINK resizeしないで、等倍で処理
cv::cuda::createMinEigenValCorner コーナー検出 53.074 47.270 <≒ GPU LINK resizeしないで、等倍で処理

Thoughts

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

Future

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

☞ To Be Continued

次は @kazuki-maさんです。
ちょっと早いですが、、、、良いお年を〜。

Reference