• 22
    いいね
  • 0
    コメント

この記事はOpenCV Advent Calendar 2015の20日目の記事です.

はじめに

OpenCV 3.0よりCPU/GPU実装のカプセル化を行うための仕組みであるT-API(transparent API)が導入されました.そのため,OpenCVユーザーは,T-APIで提供されるUMatと呼ばれるデータ構造を用いて実装することで,CPU/GPUどちらでも動作する処理を同一コードで記述できます.

UMatについては,筆者の連載記事スライドだけでなく,以下のURLの記事を参照ください.

UMatを使ったサンプルコード

UMatを使ったサンプルコードは2015年12月現在でWeb上であまり公開されていないため,
OpenCVにある以下のディレクトリにあるものが参考になるかもしれません.

  • samples/tapi
  • modules/core/perf/opencl
  • modules/core/test/ocl

前準備

UMatでOpenCLを使用するには以下のCMakeオプションをONにしてOpenCVをビルドする必要があります.

  • WITH_OPENCL

また,コマンドラインでCMakeを実行する場合は-D WITH_OPENCL=ONと指定してください.

UMatの内部処理

以下のサンプルコードにある処理を例に挙げてUMatの内部処理を紹介します.

#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>

#include <iostream>

int main(int argc, char *argv[])
{
    cv::Mat img = cv::imread("lena.jpg", cv::IMREAD_COLOR);
    if (img.empty())
    {
        std::cerr << "Failed to open image file." << std::endl;
        return -1;
    }

    cv::UMat u_img, u_gray;

    // (1)MatのデータをUMatにコピーする
    img.copyTo(u_img);

    // (2)UMatクラスのインスタンスを用いてグレースケール化を行う
    cv::cvtColor(u_img, u_gray, cv::COLOR_BGR2GRAY);

    // (3)UMatのデータをウィンドウ表示する
    cv::namedWindow("u_gray");
    cv::imshow("u_gray", u_gray);
    cv::waitKey(0);
    cv::destroyAllWindows();

    return 0;
}

Mat::copyToメソッドの振る舞い

前述のサンプルコードではimg.copyTo(u_img);という記述により,
MatのデータをUMatにコピーしています.

以下にcopyToメソッドの実装の一部を引用します.

    if( _dst.isUMat() )
    {
        _dst.create( dims, size.p, type() );
        UMat dst = _dst.getUMat();

        size_t i, sz[CV_MAX_DIM], dstofs[CV_MAX_DIM], esz = elemSize();
        for( i = 0; i < (size_t)dims; i++ )
            sz[i] = size.p[i];
        sz[dims-1] *= esz;
        dst.ndoffset(dstofs);
        dstofs[dims-1] *= esz;
        dst.u->currAllocator->upload(dst.u, data, dims, sz, dstofs, dst.step.p, step.p);
        return;
    }

ここではcopyToメソッドの引数がUMatクラスのインスタンスだった場合,
UMat dst = _dst.getUMat();にて,コピー先のインスタンスを生成しています.

そして,UMat dst = _dst.getUMat();
にて,MatのデータをUMatにコピーしています.

その後,オフセットの調整を行ってから
dst.u->currAllocator->upload(dst.u, data, dims, sz, dstofs, dst.step.p, step.p);
にて,OpenCLデバイスにデータをアップロードします.

cv::cvtColorメソッドの振る舞い

以下にcv::cvtColorメソッドの実装の一部を引用します.

void cv::cvtColor( InputArray _src, OutputArray _dst, int code, int dcn )
{
    int stype = _src.type();
    int scn = CV_MAT_CN(stype), depth = CV_MAT_DEPTH(stype), bidx;

    CV_OCL_RUN( _src.dims() <= 2 && _dst.isUMat() && !(depth == CV_8U && (code == CV_Luv2BGR || code == CV_Luv2RGB)),
                ocl_cvtColor(_src, _dst, code, dcn) )

この例では,CV_OCL_RUNマクロにある条件(src.dims() <= 2 && dst.isUMat() && !(depth == CV_8U && (code == CV_Luv2BGR || code == CVLuv2RGB)))を満たす場合に,OpenCL実装であるoclcvtColorがコールされます.

以下にocl_cvtColorの実装の一部を引用します.

    case COLOR_RGB2GRAY: case COLOR_RGBA2GRAY:
    {
        CV_Assert(scn == 3 || scn == 4);
        bidx = code == COLOR_BGR2GRAY || code == COLOR_BGRA2GRAY ? 0 : 2;
        dcn = 1;
        k.create("RGB2Gray", ocl::imgproc::cvtcolor_oclsrc,
                 opts + format("-D dcn=1 -D bidx=%d -D STRIPE_SIZE=%d",
                               bidx, stripeSize));
        globalsize[0] = (src.cols + stripeSize-1)/stripeSize;
        break;
    }

また,ocl_cvtColorのk.createにて目的の処理に応じたOpenCLカーネルをコンパイルし,OpenCLプログラムを生成します.

このとき生成したOpenCLプログラムは,OpenCLカーネルのビルドオプション文字列から生成したハッシュ値をキーとしてmap形式でメモリ上に保存しています,そのため,同じ関数を再度コールした場合は,OpenCLカーネルをコンパイルは行わずにハッシュキーでヒットしたOpenCLプログラムを使って処理を行います.

OpenCV 2.4系(oclMat)との違い

OpenCV 2.4系(厳密に言うとOpenCV 2.4.7以降のOpenCV 2.4系)のoclMatは生成したOpenCLプログラムを外部ファイルとして出力しますが,3.0からはmap形式でメモリ上に保存するように変更になりました.

oclMatとはなんぞやという方は私のスライドを参照ください.

cv::imshowメソッドの振る舞い

結論から述べるとcv::imshowメソッドに対してUMatクラスのインスタンスをそのまま渡して問題ありません.
以下にcv::imshowメソッドの実装の一部を引用します.

void cv::imshow( const String& winname, InputArray _img )
{
    const Size size = _img.size();
#ifndef HAVE_OPENGL
    CV_Assert(size.width>0 && size.height>0);
    {
        Mat img = _img.getMat();
        CvMat c_img = img;
        cvShowImage(winname.c_str(), &c_img);
    }

cv::imshowメソッド内のMat img = _img.getMat();でMatのデータとして変換し,そのデータをcvShowImage関数に渡しているため,ユーザーはOpenCLデバイスからのダウンロード処理を記述する必要はありません.

UMatでハマったこと

UMatを使うことでOpenCL利用による高速化の恩恵を受けられる場合もあるのですが,劇的に遅くなるケースにも遭遇しました.もしかしたら他のUMatユーザーも参考になるかもしれないのでご紹介します.

事の発端

@WL_Amigoさんがwaifu2x-converter-cppを開発されているときの以下のツイートがきっかけです.

※現在のwaifu2x-converter-cppは,この問題が解決しています.

サンプルコード

調査するために現象を再現させるサンプルコードを作りました.

#include <opencv2/core.hpp>
#include <opencv2/core/ocl.hpp>
#include <opencv2/imgproc.hpp>

#include <iostream>
#include <random>

int main(int argc, const char* argv[])
{
    const int loopNum = 10;
    cv::Mat src(cv::Size(512, 512), CV_8UC3, cv::Scalar(0, 0, 255));
    cv::Mat dst;
    cv::Mat kernel[loopNum];
    cv::UMat u_kernel[loopNum];

    std::random_device rd;
    std::srand(rd());
    for (int i = 0; i < loopNum; i++)
    {
        kernel[i] =
            (
                cv::Mat_<float>(3, 3) <<
                std::rand(), 1.0, 1.0,
                1.0, 1.0, 1.0,
                1.0, 1.0, 1.0
            );
        u_kernel[i] = kernel[i].getUMat(cv::ACCESS_READ);
    }
    cv::UMat u_src, u_dst;
    src.copyTo(u_src);
    double f = 1000.0f / cv::getTickFrequency();

    int64 start = cv::getTickCount();
    // ここからがMatの処理
    for (int i = 0; i < loopNum; i++)
    {
        cv::filter2D(src, dst, -1, kernel[i]);
    }
    int64 end = cv::getTickCount();

    start = cv::getTickCount();
    // ここからがUMatの処理
    for (int i = 0; i < loopNum; i++)
    {
        cv::filter2D(u_src, u_dst, -1, u_kernel[i]);
    }
    end = cv::getTickCount();

    std::cout << "[Mat]  " << (end - start) * f << "[ms]" << std::endl;
    std::cout << "[UMat] " << (end - start) * f << "[ms]" << std::endl;

    return 0;
}

実行結果

私の環境で前述のサンプルコードを実行した時の結果は以下の通りです.


[Mat]  35.0787[ms]
[UMat] 1257.96[ms]

UMatの方がめちゃくちゃ遅い・・・!!

原因がわからないと気持ち悪いので調査してみることにしました.

調査

ということで,Visual Studio 2013のプロファイラーを使ってどの部分が遅いか確認します.
プロファイラーを使って調べたホットパスは以下の通りです.

hotpath.png

この結果からもわかるように,OpenCLカーネルコンパイル時間がほとんどの時間を占めていることがわかります.具体的には,forループ中でcv::filter2Dを呼んでいるところが今回の問題のトリガです.

    for (int i = 0; i < loomNum; i++)
    {
        cv::filter2D(src, dst, -1, kernel[i]);
    }

UMatでOpenCLが有効な場合,cv::filter2Dではフィルタのカーネル毎にOpenCLカーネルをコンパイルするのですが,フィルタのカーネルkernel[i]が異なると使いまわせないのでOpenCLカーネルコンパイル処理が大量に走り,パフォーマンスが著しく劣化してしまいます.

回避方法

知らない方からするとおまじないみたいなのでアレですが,UMatの関数を実行する前に
cv::ocl::setUseOpenCL(false);
と実行することで,UMat内部で常にCPU動作の実装が動くようになり,この問題を回避することができます.

公式の見解

このUMatの振る舞いは想定されているものなのか気になったので公式メンバに質問しました
以下に公式メンバの回答を引用します.


In filter2D OpenCL kernel is compiled with exact filter kernel values (to reach maximum speed).
In your example you change filter kernel values on every call, so OpenCL compiler invoked many times.
So this behaviour is expected.

結論から述べると「想定された振る舞い」とのことです.
というわけで,UMatを使うときにはこの辺の振る舞いにも気を付ける必要がありそうです.
(OpenCL詳しくない人がこの現象踏んだら詰みそうではありますが・・・)

おわりに

この記事では,UMatの内部処理およびUMatでハマったことをまとめました.
これをきっかけに,UMatユーザーが増えてUMatに関する日本語情報が増えると嬉しいです.
2015年12月現在,UMatの情報は現状Web上でほとんど見付けることができないので・・・

備考

筆者は以下の環境で動作確認しました.

  • OpenCV 3.0.0
  • Windows 8.1 Pro(64bit)
  • Visual Studio 2013 Update5
  • NVIDIA CUDA Toolkit 7.5
この投稿は OpenCV Advent Calendar 201520日目の記事です。