OpenCV GPUモジュールを試す

せっかくグラボがあるのに腐っていたので、有益なことに使ってみます。OpenCVのcv::gpu名前空間以下にあるGPUモジュールを使い、CUDAの力を確認します。OpenCV 2.4.8です。

準備

注意点は3点。

  • OpenCVは自分でビルド。CMakeではWITH_CUDAを有効にします。
  • ターゲットは VisualStudio 2010にしました。(手はあるようですが)普通にやると、現状では2012以降向けにするとビルドでこけるようです。
  • ビルドは数時間単位でかかりました。寝る前、出かける前などに。

CUDA付きのOpenCVになると、cv::gpu::getCudaEnabledDeviceCountが1以上を返すようになります。GPUが使えるかはこれで判定します。

int cudaDevices = cv::gpu::getCudaEnabledDeviceCount();
if (cudaDevices > 0)
{
    // GPUが使えるときの処理
}

デバイス情報

cv::gpu::DeviceInfo で取得できます。我が家のGPU情報を表示させます。

using namespace std;

cv::gpu::DeviceInfo info(0);
cout <<
    "[CUDA Device 0]" << endl <<
    "name: " << info.name() << endl <<
    "majorVersion: " << info.majorVersion() << endl <<
    "minorVersion: " << info.minorVersion() << endl <<
    "multiProcessorCount: " << info.multiProcessorCount() << endl <<
    "sharedMemPerBlock: " << info.sharedMemPerBlock() << endl <<
    "freeMemory: " << info.freeMemory() << endl <<
    "totalMemory: " << info.totalMemory() << endl <<
    "isCompatible: " << info.isCompatible() << endl <<
    "supports(FEATURE_SET_COMPUTE_10): " << info.supports(FEATURE_SET_COMPUTE_10) << endl <<
    "supports(FEATURE_SET_COMPUTE_11): " << info.supports(FEATURE_SET_COMPUTE_11) << endl <<
    "supports(FEATURE_SET_COMPUTE_12): " << info.supports(FEATURE_SET_COMPUTE_12) << endl <<
    "supports(FEATURE_SET_COMPUTE_13): " << info.supports(FEATURE_SET_COMPUTE_13) << endl <<
    "supports(FEATURE_SET_COMPUTE_20): " << info.supports(FEATURE_SET_COMPUTE_20) << endl <<
    "supports(FEATURE_SET_COMPUTE_21): " << info.supports(FEATURE_SET_COMPUTE_21) << endl <<
    "supports(FEATURE_SET_COMPUTE_30): " << info.supports(FEATURE_SET_COMPUTE_30) << endl <<
    "supports(FEATURE_SET_COMPUTE_35): " << info.supports(FEATURE_SET_COMPUTE_35) << endl;

f:id:Schima:20140406112411p:plain
GTX 650 Tiは、1スロットで済むお手頃な中ではまずまずの性能で、満足しています。

処理時間計測の注意

(CPUでも多少ありますが、)GPUは初回の処理で1秒ほど準備(?)にかかったりします。適当なダミー処理をさせてから計測しましょう。

thresholdの性能差

以降、私が興味があるいくつかの関数について、通常のCPUでの処理と、GPU(CUDA)での処理の処理速度を比べていきます。なお、TBBなどのオプションは付けずにビルドしているので、いくらかCPUが過小評価かもしれません。

GPUは先ほど示した通り、CPUは Core i7-2600 3.40GHz です。

まずは二値化です。5000回実行する時間を計ります。ターゲットはいつものようにレナさん。

using namespace std;
using namespace cv;
using namespace cv::gpu;

Mat src = imread("C:\\Lenna.png", cv::IMREAD_GRAYSCALE);
GpuMat srcg(src);

const int Count = 5000;

// CPU
TickMeter meter;
meter.start();
for (int i = 0; i < Count; i++) {
    Mat binary;
    threshold(src, binary, 128, 255, cv::THRESH_BINARY);
}
meter.stop();
cout << "Threshold (CPU): " << meter.getTimeMilli() << "ms" << endl;

// GPU
meter.reset();
meter.start();
for (int i = 0; i < Count; i++) {
    GpuMat binaryg;
    threshold(srcg, binaryg, 128, 255, cv::THRESH_BINARY);
}
meter.stop();
cout << "Threshold (GPU): " << meter.getTimeMilli() << "ms" << endl;
方法 処理時間[ms]
CPU 53
GPU 492

かえって低速になりました。万能ではないということですね。

※はじめはTHRESH_OTSUで実験しようと思ったのですが、GPUでは大津の手法が使えないようでした。

matchTemplateの性能差

次はテンプレートマッチングです。

対象画像は、Wikipediaにある100年前のNew York Timesにしました。大きさ 5988x7046 の巨大な画像です。
http://en.wikipedia.org/wiki/File:New_York_Times_Frontpage_1914-07-29.png

テンプレートは、ロゴの N の字です。
f:id:Schima:20140406122314p:plain

using namespace std;
using namespace cv;
using namespace cv::gpu;

Mat org = imread("C:\\New_York_Times_Frontpage_1914-07-29.png");
Mat src;
cvtColor(org, src, cv::COLOR_BGR2GRAY);

Mat tmpl = src(cv::Rect(2015, 125, 330, 360));

GpuMat srcg(src);
GpuMat tmplg(tmpl); 

// CPU
TickMeter meter;
meter.start();
{
    Mat result;
    matchTemplate(src, tmpl, result, cv::TM_CCOEFF_NORMED);
}
meter.stop(); 
cout << "MatchTemplate (CPU): " << meter.getTimeMilli() << "ms" << endl;

// GPU
meter.reset();
meter.start();
{
    GpuMat resultg;
    matchTemplate(srcg, tmplg, resultg, cv::TM_CCOEFF_NORMED);
}
meter.stop(); 
cout << "MatchTemplate (GPU): " << meter.getTimeMilli() << "ms" << endl;
方法 処理時間[ms]
CPU 4762
GPU 279

15倍以上、感動の速さです。

matchTemplateの性能差 (小さい画像)

New York Timesのような巨大な画像ではなく、小さな画像を何度もする場合はどうなるのかも試してみました。
ターゲットはレナさんで、テンプレートは100x100にしています。マッチング後のminMaxLocも込みで、100回行っています。

using namespace std;
using namespace cv;
using namespace cv::gpu;

Mat src = imread("C:\\Lenna.png", cv::IMREAD_GRAYSCALE);    
Mat tmpl = src(cv::Rect(200, 200, 100, 100));    
    
const int Count = 100;

// CPU
TickMeter meter;
meter.start();
for (int i = 0; i < Count; i++) {
    Mat result;
    matchTemplate(src, tmpl, result, cv::TM_CCOEFF_NORMED);

    double min, max;
    cv::Point minLoc, maxLoc;
    minMaxLoc(result, &min, &max, &minLoc, &maxLoc);
}
meter.stop();
cout << "MatchTemplate (CPU): " << meter.getTimeMilli() << "ms" << endl;

// GPU
meter.reset();
meter.start();
for (int i = 0; i < Count; i++) {
    GpuMat srcg(src);
    GpuMat tmplg(tmpl);
    GpuMat resultg;
    matchTemplate(srcg, tmplg, resultg, cv::TM_CCOEFF_NORMED);

    double min, max;
    cv::Point minLoc, maxLoc;
    minMaxLoc(resultg, &min, &max, &minLoc, &maxLoc);
}
meter.stop();
cout << "MatchTemplate (GPU): " << meter.getTimeMilli() << "ms" << endl;
方法 処理時間[ms]
CPU 1308
GPU 910

差は小さくなりますが、それでもまだ高速です。
今回テストしたいシーンを想定して、わざと入力のGpuMat変数定義をforの中に入れています。これを外に出しても、時間はほとんど変わりませんでした。GpuMatへのuploadは無視できるほど高速なのか、はたまたコンパイラで最適化がされてしまったのか。

fastNlMeansDenoisingの性能差

以前の記事で紹介した、Non-local means filterの性能差を見てみます。CPUでは負荷の大きい重い処理でしたが、これがどう変わるでしょうか。
NLMeansのパラメータが妥当なのか自信が無いですが、簡単に試しておきます。

ターゲットは以前の記事同様にレナさんにノイズを付加したものです。ノイズ付加の関数は省略します。

GPU版NLMeansのリファレンスはこちら。GPUAPIでは search_window, block_size の指定順が逆なのがややこしいです。3種類の関数があり、いずれも試しました。
http://docs.opencv.org/modules/gpu/doc/image_processing.html?highlight=simplemethod#void%20gpu::FastNonLocalMeansDenoising::simpleMethod%28const%20GpuMat&%20src,%20GpuMat&%20dst,%20float%20h,%20int%20search_window%20,%20int%20block_size%20,%20Stream&%20s%29

using namespace std;
using namespace cv;
using namespace cv::gpu;

Mat src = imread("C:\\Lenna.png", cv::IMREAD_COLOR);
    
const int NoiseSigma = 15;
cv::Mat noise;
addNoise(src, noise, NoiseSigma);

Mat result, resultPure, resultSimple, resultLab;
GpuMat resultg1, resultg2, resultg3;

// CPU
TickMeter meter;
meter.start();
{
    cv::fastNlMeansDenoising(noise, result, NoiseSigma, 3, 7);
}
meter.stop();
cout << "NLMeans (CPU): " << meter.getTimeMilli() << "ms";
cout << "   PSNR = " << PSNR(src, result) << endl;

// GPU - pure
meter.reset();
meter.start();
{
    GpuMat noiseg(noise);
    nonLocalMeans(noiseg, resultg1, NoiseSigma, 7, 3);
}
meter.stop();
resultg1.download(resultPure);
cout << "NLMeans (GPU - pure): " << meter.getTimeMilli() << "ms";
cout << "   PSNR = " << PSNR(src, resultPure) << endl;

// GPU - simpleMethod
meter.reset();
meter.start();
{
    GpuMat noiseg(noise);
    FastNonLocalMeansDenoising nlMeans;
    nlMeans.simpleMethod(noiseg, resultg2, NoiseSigma, 7, 3);
}
meter.stop();
resultg2.download(resultSimple);
cout << "NLMeans (GPU - simpleMethod): " << meter.getTimeMilli() << "ms";
cout << "   PSNR = " << PSNR(src, resultSimple) << endl;

// GPU - labMethod
meter.reset();
meter.start();
{
    GpuMat noiseg(noise);
    FastNonLocalMeansDenoising nlMeans;
    nlMeans.labMethod(noiseg, resultg3, NoiseSigma, NoiseSigma, 7, 3);
}
meter.stop();
resultg3.download(resultLab);
cout << "NLMeans (GPU - labMethod): " << meter.getTimeMilli() << "ms";
cout << "   PSNR = " << PSNR(src, resultLab) << endl;

処理時間はやはり速いです。GPUならリアルタイム動画処理でも使えそうな高速さです。PSNRもほとんど差がありません。

方法 処理時間[ms] PSNR[db]
CPU 175 31.88
GPU(pure) 32 31.70
GPU(simpleMethod) 47 31.89
GPU(labMethod) 73 32.44

以下はそれぞれの出力画像です。特に一番下から2つは近似して高速計算する手法ということで、結果が多少悪くなると思われましたが、見た限りではほとんどわかりません。

近似手法(simpleMethod)よりも普通の実装(pure)が速いのが納得いきませんが...

元画像(ノイズ付加)
f:id:Schima:20140406133236p:plain

cv::fastNlMeansDenoising
f:id:Schima:20140406133302p:plain

cv::gpu::nonLocalMeans
f:id:Schima:20140406133310p:plain

cv::gpu::FastNonLocalMeansDenoising::simpleMethod
f:id:Schima:20140406133316p:plain

cv::gpu::FastNonLocalMeansDenoising::labMethod
f:id:Schima:20140406133322p:plain