OpenCV 2系でGPUで膨張処理をさせる方法

OpenCV 2系でGPUで膨張処理をさせる方法で詰まったので書いておく。最後に検証に使った全コードがあるのでどうぞ。

CPUで膨張処理をさせるとき、さくっと書けば次のようになる。

cv::Mat src, dst;
int width = 256;
int height = 256;
src = cv::Mat::eye(cv::Size(width, height), CV_8U);
cv::dilate(src, dst, cv::Mat::ones(3, 3, CV_8U));
std::cout << cv::countNonZero(dst) << " cpu dilate w/o rect" << std::endl;

この結果は、 256+255*2+254*2=1274 となる。 たとえば、周辺1ピクセルを膨張処理させなかった場合は、

cv::Rect rect = cv::Rect(1, 1, width - 2, height - 2);
cv::dilate(src(rect), dst(rect), cv::Mat::ones(3, 3, CV_8U));

この結果は、 256+255*2+254*2-5*2=1264 となる。

GPU版では、領域外チェックが甘いため、全ての領域を処理させたとき、領域外のメモリを含めて計算してしまうバグ(仕様)がある。 それを検証するために、 cv::gpu::dilate と、 cv::gpu::getMaxFilter_GPU と、cv::gpu::createBoxFilter_GPU を比較してみた。 それぞれ、全領域で処理させた場合 w/o rect と、 rect で周辺1ピクセルを無視した場合 with rect を行った。 結果は次のとおり。

処理方法 countNonZeroの結果
gpu dilate w/o rect 1264
gpu dilate with rect 1249
gpu max w/o rect 1278
gpu max with rect 1264
gpu box w/o rect 1259
gpu box with rect 1264

w/o rect の期待値は1274なので、いずれも正しい答えを返していない。一方で、 with rectcv::gpu::getMaxFilter_GPU と、cv::gpu::createBoxFilter_GPU で正しい答えを返している。 この2つ以外は、おそらく環境依存で値が変わりうると考えられる。ここで、BoxFilterを使うときは、下記の検証用コードでもそそしているとおり、要素を Expansion の二乗より大きい値にしておく必要がある。さらに、最後に適切な値に閾値処理する必要がある。これらの手間を考えると、 cv::gpu::getMaxFilter_GPUwith rect で使うのが最もシンプルな方法であろう。

なかなか難解なライブラリである。

さて、検証用全コード

#include <opencv2/opencv.hpp>
#include <opencv2/gpu/gpu.hpp>

using namespace cv;

int main() {

    cv::gpu::setDevice(0);

    int gpudeviceid = cv::gpu::getDevice();
    cv::gpu::DeviceInfo dev(gpudeviceid);
    string gpu_name = dev.name();

    cv::gpu::Stream stream;
    int Expansion = 3;

    Ptr<cv::gpu::BaseFilter_GPU> fb;
    fb = cv::gpu::getMaxFilter_GPU(CV_8UC1, CV_8UC1, Size(Expansion, Expansion));

    cv::Ptr<gpu::FilterEngine_GPU> filter;
    filter = gpu::createBoxFilter_GPU(CV_8U, CV_8U, cv::Size(Expansion, Expansion));

    cv::gpu::GpuMat gsrc, gdst;
    cv::Mat src, dst;

    int width = 256;
    int height = 256;
    src = cv::Mat::eye(cv::Size(width, height), CV_8U) * 9;
    gsrc.upload(src);

    cv::Rect rect = cv::Rect(1, 1, width - 2, height - 2);

    {
        cv::dilate(src, dst, cv::Mat::ones(3, 3, CV_8U));
        std::cout << cv::countNonZero(dst) << " cpu dilate w/o rect" << std::endl;
    }

    {
        dst = cv::Scalar(0);
        cv::dilate(src(rect), dst(rect), cv::Mat::ones(3, 3, CV_8U));
        std::cout << cv::countNonZero(dst) << " cpu dilate with rect" << std::endl;
    }

    src = cv::Scalar(0);
    {
        gdst.upload(src);
        cv::gpu::dilate(gsrc, gdst, cv::Mat::ones(3, 3, CV_8U));
        std::cout << cv::countNonZero(dst) << " gpu dilate w/o rect" << std::endl;
    }

    {
        gdst.upload(src);
        cv::gpu::dilate(gsrc(rect), gdst(rect), cv::Mat::ones(3, 3, CV_8U));
        std::cout << cv::gpu::countNonZero(gdst) << " gpu dilate with rect" << std::endl;
    }

    {
        gdst.upload(src);
        (*fb)(gsrc, gdst);
        std::cout << cv::gpu::countNonZero(gdst) << " gpu max w/o rect" << std::endl;
    }

    {
        gdst.upload(src);
        (*fb)(gsrc(rect), gdst(rect)); //おすすめの方法
        std::cout << cv::gpu::countNonZero(gdst) << " gpu max with rect" << std::endl;
    }

    {
        gdst.upload(src);
        filter->apply(gsrc, gdst);
        std::cout << cv::gpu::countNonZero(gdst) << " gpu box w/o rect" << std::endl;
    }

    {
        gdst.upload(src);
        filter->apply(gsrc, gdst, rect);
        std::cout << cv::gpu::countNonZero(gdst) << " gpu box with rect" << std::endl;
    }

    fb.release(); // no error

    stream.waitForCompletion();
    std::cout << "Free memory rate = " << dev.freeMemory()*100.0 / dev.totalMemory() << " [%] at Device = " << gpudeviceid << " at first" << std::endl;
    return 0;
}