OpenCV 5.0.0
Open Source Computer Vision
読み込み中...
検索中...
見つかりません
🤖 AIによる機械翻訳(非公式) — これは OpenCV 5.0.0 公式リファレンス(英語)を AI (Claude) で自動翻訳したものです。訳に誤りを含む場合があります。正確な情報は 公式英語版(原文) を参照してください。
異方性画像セグメンテーションのG-APIへの移植

前のチュートリアル: G-APIによる顔解析パイプライン
次のチュートリアル: G-APIによる顔の美化アルゴリズムの実装

はじめに

このチュートリアルでは以下を学ぶ:

  • 既存のアルゴリズムをG-APIの計算(グラフ)へ変換する方法;
  • G-APIグラフを調査しプロファイルする方法;
  • コードを変更せずにグラフの実行をカスタマイズする方法。

このチュートリアルは 勾配構造テンソルによる異方性画像セグメンテーション に基づいている。

クイックスタート: OpenCVバックエンドの使用

始める前に、元のアルゴリズムの実装を確認しておこう。

#include <iostream>
using namespace cv;
using namespace std;
void calcGST(const Mat& inputImg, Mat& imgCoherencyOut, Mat& imgOrientationOut, int w);
int main()
{
int W = 52; // window size is WxW
double C_Thr = 0.43; // threshold for coherency
int LowThr = 35; // threshold1 for orientation, it ranges from 0 to 180
int HighThr = 57; // threshold2 for orientation, it ranges from 0 to 180
samples::addSamplesDataSearchSubDirectory("doc/tutorials/imgproc/anisotropic_image_segmentation/images");
Mat imgIn = imread(samples::findFile("gst_input.jpg"), IMREAD_GRAYSCALE);
if (imgIn.empty()) //check whether the image is loaded or not
{
cout << "ERROR : Image cannot be loaded..!!" << endl;
return -1;
}
Mat imgCoherency, imgOrientation;
calcGST(imgIn, imgCoherency, imgOrientation, W);
Mat imgCoherencyBin;
imgCoherencyBin = imgCoherency > C_Thr;
Mat imgOrientationBin;
inRange(imgOrientation, Scalar(LowThr), Scalar(HighThr), imgOrientationBin);
Mat imgBin;
imgBin = imgCoherencyBin & imgOrientationBin;
normalize(imgCoherency, imgCoherency, 0, 255, NORM_MINMAX, CV_8U);
normalize(imgOrientation, imgOrientation, 0, 255, NORM_MINMAX, CV_8U);
imshow("Original", imgIn);
imshow("Result", 0.5 * (imgIn + imgBin));
imshow("Coherency", imgCoherency);
imshow("Orientation", imgOrientation);
imwrite("result.jpg", 0.5*(imgIn + imgBin));
imwrite("Coherency.jpg", imgCoherency);
imwrite("Orientation.jpg", imgOrientation);
waitKey(0);
return 0;
}
void calcGST(const Mat& inputImg, Mat& imgCoherencyOut, Mat& imgOrientationOut, int w)
{
Mat img;
inputImg.convertTo(img, CV_32F);
// GST components calculation (start)
// J = (J11 J12; J12 J22) - GST
Mat imgDiffX, imgDiffY, imgDiffXY;
Sobel(img, imgDiffX, CV_32F, 1, 0, 3);
Sobel(img, imgDiffY, CV_32F, 0, 1, 3);
multiply(imgDiffX, imgDiffY, imgDiffXY);
Mat imgDiffXX, imgDiffYY;
multiply(imgDiffX, imgDiffX, imgDiffXX);
multiply(imgDiffY, imgDiffY, imgDiffYY);
Mat J11, J22, J12; // J11, J22 and J12 are GST components
boxFilter(imgDiffXX, J11, CV_32F, Size(w, w));
boxFilter(imgDiffYY, J22, CV_32F, Size(w, w));
boxFilter(imgDiffXY, J12, CV_32F, Size(w, w));
// GST components calculation (stop)
// eigenvalue calculation (start)
// lambda1 = 0.5*(J11 + J22 + sqrt((J11-J22)^2 + 4*J12^2))
// lambda2 = 0.5*(J11 + J22 - sqrt((J11-J22)^2 + 4*J12^2))
Mat tmp1, tmp2, tmp3, tmp4;
tmp1 = J11 + J22;
tmp2 = J11 - J22;
multiply(tmp2, tmp2, tmp2);
multiply(J12, J12, tmp3);
sqrt(tmp2 + 4.0 * tmp3, tmp4);
Mat lambda1, lambda2;
lambda1 = tmp1 + tmp4;
lambda1 = 0.5*lambda1; // biggest eigenvalue
lambda2 = tmp1 - tmp4;
lambda2 = 0.5*lambda2; // smallest eigenvalue
// eigenvalue calculation (stop)
// Coherency calculation (start)
// Coherency = (lambda1 - lambda2)/(lambda1 + lambda2)) - measure of anisotropism
// Coherency is anisotropy degree (consistency of local orientation)
divide(lambda1 - lambda2, lambda1 + lambda2, imgCoherencyOut);
// Coherency calculation (stop)
// orientation angle calculation (start)
// tan(2*Alpha) = 2*J12/(J22 - J11)
// Alpha = 0.5 atan2(2*J12/(J22 - J11))
phase(J22 - J11, 2.0*J12, imgOrientationOut, true);
imgOrientationOut = 0.5*imgOrientationOut;
// orientation angle calculation (stop)
}
Comma-separated Matrix Initializer.
Definition mat.hpp:964
bool empty() const
Returns true if the array has no elements.
void convertTo(OutputArray m, int rtype, double alpha=1, double beta=0) const
Converts an array to another data type with optional scaling.
Template class for specifying the size of an image or rectangle.
Definition types.hpp:338
#define CV_8U
Definition interface.h:54
#define CV_32F
Definition interface.h:59
int main(int argc, char *argv[])
Definition highgui_qt.cpp:3
Definition core.hpp:107
STL namespace.

calcGST()の検討

関数calcGST()は明らかに画像処理パイプラインである。

  • これは単に多数の cv::Mat に対する一連の操作にすぎない;
  • コードにロジック(条件分岐)やループが含まれていない;
  • すべての関数は2D画像に対して動作する(cv::Sobel, cv::multiply, cv::boxFilter, cv::sqrt などのように)。

以上を考えると、calcGST()は出発点として最適な候補である。元のコードでは、そのプロトタイプは次のように定義されている。

void calcGST(const Mat& inputImg, Mat& imgCoherencyOut, Mat& imgOrientationOut, int w);

G-APIでは、次のように定義できる。

void calcGST(const cv::GMat& inputImg, cv::GMat& imgCoherencyOut, cv::GMat& imgOrientationOut, int w);

重要なのは、新しいG-APIベースのバージョンのcalcGST()は、実際に値を計算する元のバージョンとは対照的に、計算グラフを生成するだけである点を理解することだ。これは原理的な違いであり、このようなG-APIベースの関数は実際のデータを処理するためではなく、グラフを構築するために使用される。

calcGST()の実装を、\(J\) 行列の計算から始めよう。元のコードは次のようになっている。

void calcGST(const Mat& inputImg, Mat& imgCoherencyOut, Mat& imgOrientationOut, int w)
{
Mat img;
inputImg.convertTo(img, CV_32F);
// GST components calculation (start)
// J = (J11 J12; J12 J22) - GST
Mat imgDiffX, imgDiffY, imgDiffXY;
Sobel(img, imgDiffX, CV_32F, 1, 0, 3);
Sobel(img, imgDiffY, CV_32F, 0, 1, 3);
multiply(imgDiffX, imgDiffY, imgDiffXY);

ここでは、すべての新しい演算に対して出力オブジェクトを宣言する必要がある(cv::Mat::convertTo の結果としてのimg、cv::Sobel および cv::multiply の結果としてのimgDiffXなどを参照)。

G-APIでの対応コードを以下に示す。

void calcGST(const cv::GMat& inputImg, cv::GMat& imgCoherencyOut, cv::GMat& imgOrientationOut, int w)
{
auto img = cv::gapi::convertTo(inputImg, CV_32F);
auto imgDiffX = cv::gapi::Sobel(img, CV_32F, 1, 0, 3);
auto imgDiffY = cv::gapi::Sobel(img, CV_32F, 0, 1, 3);
auto imgDiffXY = cv::gapi::mul(imgDiffX, imgDiffY);

このスニペットは、G-APIと従来のOpenCVの間における次の構文上の違いを示している。

  • すべての標準G-API関数は、デフォルトで"cv::gapi"名前空間に配置される;
  • G-API演算は結果を返す – 関数に余分な"出力"パラメータを渡す必要はない。

注意 – このコードは auto も使用している – imgimgDiffX などの中間オブジェクトの型は、C++コンパイラによって自動的に推論される。この例では、型はG-API演算の戻り値によって決まり、それらはすべて cv::GMat である。

G-APIの標準カーネルは可能な限りOpenCV APIの慣例に従おうとする – そのためcv::gapi::sobelは cv::Sobel と同じ引数を取り、cv::gapi::mulcv::multiply に従う、といった具合である(戻り値を持つ点を除く)。

calcGST()関数の残りの部分も、同じやり方で簡単に実装できる。以下にその全ソースコードを示す。

void calcGST(const cv::GMat& inputImg, cv::GMat& imgCoherencyOut, cv::GMat& imgOrientationOut, int w)
{
auto img = cv::gapi::convertTo(inputImg, CV_32F);
auto imgDiffX = cv::gapi::Sobel(img, CV_32F, 1, 0, 3);
auto imgDiffY = cv::gapi::Sobel(img, CV_32F, 0, 1, 3);
auto imgDiffXY = cv::gapi::mul(imgDiffX, imgDiffY);
auto imgDiffXX = cv::gapi::mul(imgDiffX, imgDiffX);
auto imgDiffYY = cv::gapi::mul(imgDiffY, imgDiffY);
auto J11 = cv::gapi::boxFilter(imgDiffXX, CV_32F, cv::Size(w, w));
auto J22 = cv::gapi::boxFilter(imgDiffYY, CV_32F, cv::Size(w, w));
auto J12 = cv::gapi::boxFilter(imgDiffXY, CV_32F, cv::Size(w, w));
auto tmp1 = J11 + J22;
auto tmp2 = J11 - J22;
auto tmp22 = cv::gapi::mul(tmp2, tmp2);
auto tmp3 = cv::gapi::mul(J12, J12);
auto tmp4 = cv::gapi::sqrt(tmp22 + 4.0*tmp3);
auto lambda1 = tmp1 + tmp4;
auto lambda2 = tmp1 - tmp4;
imgCoherencyOut = (lambda1 - lambda2) / (lambda1 + lambda2);
imgOrientationOut = 0.5*cv::gapi::phase(J22 - J11, 2.0*J12, true);
}

G-APIグラフの実行

calcGST()がG-API言語で定義されたら、それに基づいてグラフを構築し、最終的に実行できる – 入力画像を渡して結果を得る。実行する前に、元のコードがどのようになっていたか見ておこう。

Mat imgCoherency, imgOrientation;
calcGST(imgIn, imgCoherency, imgOrientation, W);
Mat imgCoherencyBin;
imgCoherencyBin = imgCoherency > C_Thr;
Mat imgOrientationBin;
inRange(imgOrientation, Scalar(LowThr), Scalar(HighThr), imgOrientationBin);
Mat imgBin;
imgBin = imgCoherencyBin & imgOrientationBin;
normalize(imgCoherency, imgCoherency, 0, 255, NORM_MINMAX, CV_8U);
normalize(imgOrientation, imgOrientation, 0, 255, NORM_MINMAX, CV_8U);
imshow("Original", imgIn);
imshow("Result", 0.5 * (imgIn + imgBin));
imshow("Coherency", imgCoherency);
imshow("Orientation", imgOrientation);
imwrite("result.jpg", 0.5*(imgIn + imgBin));
imwrite("Coherency.jpg", imgCoherency);
imwrite("Orientation.jpg", imgOrientation);
waitKey(0);

calcGST()のようなG-APIベースの関数は、それが処理コードではなく構築コードであるため、入力データに直接適用することはできない。計算を実行するには、cv::GComputation クラスの特別なオブジェクトを作成する必要がある。このオブジェクトは、我々のG-APIコード(G-APIのデータと演算の合成)を、C++11の std::function<> に似た呼び出し可能なオブジェクトとしてラップする。

cv::GComputation クラスには、グラフを定義するために使用できる複数のコンストラクタがある。一般に、ユーザはグラフの境界 – GComputationが定義される入力オブジェクトと出力オブジェクト – を渡す必要がある。そうするとG-APIは出力から入力への呼び出しフローを解析し、指定された境界の間にある演算でグラフを再構築する。これは複雑に聞こえるかもしれないが、実際にはコードは次のようになる。

// Calculate Gradient Structure Tensor and post-process it for output with G-API
cv::GMat imgCoherency, imgOrientation;
calcGST(in, imgCoherency, imgOrientation, W);
cv::GMat imgCoherencyBin = imgCoherency > C_Thr;
cv::GMat imgOrientationBin = cv::gapi::inRange(imgOrientation, LowThr, HighThr);
cv::GMat imgBin = imgCoherencyBin & imgOrientationBin;
cv::GMat out = cv::gapi::addWeighted(in, 0.5, imgBin, 0.5, 0.0);
// Normalize extra outputs
cv::GMat imgCoherencyNorm = cv::gapi::normalize(imgCoherency, 0, 255, cv::NORM_MINMAX);
cv::GMat imgOrientationNorm = cv::gapi::normalize(imgOrientation, 0, 255, cv::NORM_MINMAX);
// Capture the graph into object segm
cv::GComputation segm(cv::GIn(in), cv::GOut(out, imgCoherencyNorm, imgOrientationNorm));
// Define cv::Mats for output data
cv::Mat imgOut, imgOutCoherency, imgOutOrientation;
// Run the graph
segm.apply(cv::gin(imgIn), cv::gout(imgOut, imgOutCoherency, imgOutOrientation));
cv::imwrite("result.jpg", imgOut);
cv::imwrite("Coherency.jpg", imgOutCoherency);
cv::imwrite("Orientation.jpg", imgOutOrientation);

このコードは元のものから少しだけ変わっている点に注意。結果画像の形成もパイプラインの一部になっている(cv::gapi::addWeighted で行われる)。

このG-APIパイプラインの結果は、(同じ入力画像を与えれば)元のものとビット単位で完全に一致する。

Segmentation result with G-API

G-API初期バージョン: 全リスト

以下は、G-APIへ移植した異方性画像セグメンテーションの初期バージョンの全リストである。

#include <iostream>
#include <utility>
#include "opencv2/gapi.hpp"
void calcGST(const cv::GMat& inputImg, cv::GMat& imgCoherencyOut, cv::GMat& imgOrientationOut, int w);
int main()
{
int W = 52; // window size is WxW
double C_Thr = 0.43; // threshold for coherency
int LowThr = 35; // threshold1 for orientation, it ranges from 0 to 180
int HighThr = 57; // threshold2 for orientation, it ranges from 0 to 180
cv::Mat imgIn = cv::imread("input.jpg", cv::IMREAD_GRAYSCALE);
if (imgIn.empty()) //check whether the image is loaded or not
{
std::cout << "ERROR : Image cannot be loaded..!!" << std::endl;
return -1;
}
// Calculate Gradient Structure Tensor and post-process it for output with G-API
cv::GMat imgCoherency, imgOrientation;
calcGST(in, imgCoherency, imgOrientation, W);
cv::GMat imgCoherencyBin = imgCoherency > C_Thr;
cv::GMat imgOrientationBin = cv::gapi::inRange(imgOrientation, LowThr, HighThr);
cv::GMat imgBin = imgCoherencyBin & imgOrientationBin;
cv::GMat out = cv::gapi::addWeighted(in, 0.5, imgBin, 0.5, 0.0);
// Normalize extra outputs
cv::GMat imgCoherencyNorm = cv::gapi::normalize(imgCoherency, 0, 255, cv::NORM_MINMAX);
cv::GMat imgOrientationNorm = cv::gapi::normalize(imgOrientation, 0, 255, cv::NORM_MINMAX);
// Capture the graph into object segm
cv::GComputation segm(cv::GIn(in), cv::GOut(out, imgCoherencyNorm, imgOrientationNorm));
// Define cv::Mats for output data
cv::Mat imgOut, imgOutCoherency, imgOutOrientation;
// Run the graph
segm.apply(cv::gin(imgIn), cv::gout(imgOut, imgOutCoherency, imgOutOrientation));
cv::imwrite("result.jpg", imgOut);
cv::imwrite("Coherency.jpg", imgOutCoherency);
cv::imwrite("Orientation.jpg", imgOutOrientation);
return 0;
}
void calcGST(const cv::GMat& inputImg, cv::GMat& imgCoherencyOut, cv::GMat& imgOrientationOut, int w)
{
auto img = cv::gapi::convertTo(inputImg, CV_32F);
auto imgDiffX = cv::gapi::Sobel(img, CV_32F, 1, 0, 3);
auto imgDiffY = cv::gapi::Sobel(img, CV_32F, 0, 1, 3);
auto imgDiffXY = cv::gapi::mul(imgDiffX, imgDiffY);
auto imgDiffXX = cv::gapi::mul(imgDiffX, imgDiffX);
auto imgDiffYY = cv::gapi::mul(imgDiffY, imgDiffY);
auto J11 = cv::gapi::boxFilter(imgDiffXX, CV_32F, cv::Size(w, w));
auto J22 = cv::gapi::boxFilter(imgDiffYY, CV_32F, cv::Size(w, w));
auto J12 = cv::gapi::boxFilter(imgDiffXY, CV_32F, cv::Size(w, w));
auto tmp1 = J11 + J22;
auto tmp2 = J11 - J22;
auto tmp22 = cv::gapi::mul(tmp2, tmp2);
auto tmp3 = cv::gapi::mul(J12, J12);
auto tmp4 = cv::gapi::sqrt(tmp22 + 4.0*tmp3);
auto lambda1 = tmp1 + tmp4;
auto lambda2 = tmp1 - tmp4;
imgCoherencyOut = (lambda1 - lambda2) / (lambda1 + lambda2);
imgOrientationOut = 0.5*cv::gapi::phase(J22 - J11, 2.0*J12, true);
}

初期バージョンの調査

G-APIで動作するアルゴリズムの最初の動くバージョンを手に入れたので、それを使ってG-APIの仕組みを調べて学ぶことができる。この章では2つの側面、すなわちグラフ構造の理解とメモリのプロファイリングを扱う。

グラフ構造の理解

G-APIは"Graph API"の略だが、上の例でグラフについて何か言及があっただろうか? これは初期の設計目標の1つであった – G-APIは、導入と移植の作業をより簡単にするために、式を念頭に置いて設計された。人々は通常、普通のコードを書くときにノードエッジという観点で考えることはまずない。そのため、G-APIはGraph APIでありながら、ユーザにそのような考え方を強制しない。

しかしながら、cv::GComputation オブジェクトが定義されると、グラフは依然として暗黙的に構築される。生成されたグラフがどのように見えるかを調べることは、それが正しく生成されているか、本当に我々のアルゴリズムを表現しているかを確認するうえで有用である。また、グラフに冗長性がないか確認するために、グラフの構造を学ぶことも有用である。

G-APIは、生成したグラフを .dot ファイルにダンプできる。これは人気のあるオープンなグラフ可視化ソフトウェアである Graphviz で可視化できる。

グラフを .dot ファイルにダンプするには、アプリケーションを実行する前に GRAPH_DUMP_PATH をファイル名に設定する。例えば次のようにする。

$ GRAPH_DUMP_PATH=segm.dot ./bin/example_tutorial_porting_anisotropic_image_segmentation_gapi

これで、このファイルを次のような dot コマンドで可視化できる。

$ dot segm.dot -Tpng -o segm.png

あるいは xdot でインタラクティブに表示する(これらのパッケージのインストール方法については、お使いのディストリビューション/OSのドキュメントを参照のこと)。

Anisotropic image segmentation graph

上の図は、G-APIの内部的なアルゴリズム表現に関する興味深い側面をいくつか示している。

  1. G-APIの基盤となるグラフは二部グラフである。これは演算(Operation)ノードとデータ(Data)ノードから構成され、データノードは演算ノードにのみ接続でき、演算ノードはデータノードにのみ接続でき、同じ種類のノード同士が直接接続されることはない。
  2. グラフは有向である - グラフ内のすべてのエッジは方向を持つ。
  3. グラフはデータ種のノードで"始まり"、"終わる"。
  4. データノードは1つのライタと複数のリーダのみを持つことができる。
  5. 演算ノードは複数の入力を持つことができるが、すべての入力は(入力の中で)一意なポート番号を持たなければならない。
  6. 演算ノードは複数の出力を持つことができ、すべての出力は(出力の中で)一意なポート番号を持たなければならない。

メモリ使用量の計測

アルゴリズムのメモリ使用量を、G-APIベースとOpenCVベースという2つのバージョンで計測し比較してみよう。現時点では、G-APIバージョンも内部でOpenCVの関数にフォールバックするため、OpenCVベースでもある。

GNU/Linuxでは、アプリケーションのメモリ使用量を Valgrind でプロファイルできる。Debian/Ubuntuシステムでは、次のようにインストールできる(管理者権限があると仮定)。

$ sudo apt-get install valgrind massif-visualizer

インストールが完了すれば、我々の2つのアルゴリズムバージョンについてメモリプロファイルを簡単に収集できる。

$ valgrind --tool=massif --massif-out-file=ocv.out ./bin/example_tutorial_anisotropic_image_segmentation
==6101== Massif, a heap profiler
==6101== Copyright (C) 2003-2015, and GNU GPL'd, by Nicholas Nethercote
==6101== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==6101== Command: ./bin/example_tutorial_anisotropic_image_segmentation
==6101==
==6101==
$ valgrind --tool=massif --massif-out-file=gapi.out ./bin/example_tutorial_porting_anisotropic_image_segmentation_gapi
==6117== Massif, a heap profiler
==6117== Copyright (C) 2003-2015, and GNU GPL'd, by Nicholas Nethercote
==6117== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==6117== Command: ./bin/example_tutorial_porting_anisotropic_image_segmentation_gapi
==6117==
==6117==

完了したら、収集したプロファイルを Massif Visualizer(上記の手順でインストール済み)で確認できる。

以下は、このアルゴリズムのオリジナルのOpenCV版のメモリプロファイルを可視化したものである:

Memory profile: original Anisotropic Image Segmentation sample

アプリケーションの実行に伴ってメモリが確保され、calcGST() 関数でピークに達することがわかる。その後、calcGST() の実行が完了し、すべての一時バッファが解放されると使用量が減少する。Massifはピーク時のメモリ消費量を7.6 MiBと報告している。

次に、G-API版のプロファイルを見てみよう:

Memory profile: G-API port of Anisotropic Image Segmentation sample

G-APIの計算が作成され実行が開始されると、G-APIは必要なメモリをすべて一度に確保し、その後はプログラムが終了するまでメモリプロファイルは平坦なまま保たれる。Massifはピーク時のメモリ消費量を11.4 MiBと報告している。

ここで読者は当然の疑問を持つかもしれない。G-APIはそれほど悪いのか? そもそも使う理由は何なのか?

幸いにも、そうではない。ここでメモリ消費量が増えているのは、このグラフの実行にデフォルトの素朴なOpenCVベースのバックエンドが使われているためである。このバックエンドは主に、オフロードやさらなる最適化を行う前にアルゴリズムを手早くプロトタイピングしデバッグするためのものである。

このバックエンドは、現時点ではそれが目的ではないため、複雑なメモリ管理戦略をまだ何も利用していない。次の章では、Fluidバックエンドについて学び、同じG-APIコードがまったく異なるモデルで実行され(使用量が数キロバイトにまで縮小する)様子を見ていく。

バックエンドとカーネル

この章では、G-APIの計算を特別な方法で実行する方法、たとえば別のデバイスへオフロードしたり、特別な知能を用いてスケジューリングしたりする方法を扱う。G-APIはそのグラフをポータブルにするよう設計されている。つまり、いったんG-APIの用語でグラフを定義すれば、それをCPUで実行する場合でも、GPUで実行する場合でも、あるいは両方のデバイスで同時に実行する場合でも、グラフ自体には何の変更も必要としないということである。それを可能にする技術的詳細については、G-API High-level overview および G-API Kernel API がさらに詳しく解説している。この章では、G-API Fluidバックエンドを利用して、グラフをCPU上でキャッシュ効率の良いものにする。

G-APIは バックエンド (backend) を、カーネルの実行方法を知っている下位レベルのエンティティとして定義する。バックエンドは、そのバックエンド向けにカーネルをプログラムし統合するために使われる、異なる Kernel API を持つことがある(実際に持っている)。この文脈において カーネル (kernel) とは、最上位のAPIレベルで定義される 演算 (operation) の実装である(G_TYPED_KERNEL() マクロを参照)。

バックエンドはデバイスやプラットフォーム固有の事情を把握しているものであり、その固有の事情を念頭に置きながらカーネルを実行する。たとえば、G-APIの演算をHalide言語で記述(実装)し、G-APIグラフのうちうまく対応づけられる部分について機能するHalideコードを生成できる Halide バックエンドが考えられる。

Fluidバックエンドでグラフを実行する

OpenCV 4.0には2つのG-APIバックエンドが同梱されている。先ほど使ったデフォルトの「OpenCV」と、特別な「Fluid」バックエンドである。

Fluidバックエンドは、いわゆる「ストリーミング」モデルの実行を実装することで、メモリを節約しほぼ完璧なキャッシュ局所性を達成するように実行を再編成する。

Fluidカーネルの使用を始めるには、まず適切なヘッダファイル(デフォルトではインクルードされない)をインクルードする必要がある:

#include "opencv2/gapi/fluid/core.hpp" // Fluid Core kernel library
#include "opencv2/gapi/fluid/imgproc.hpp" // Fluid ImgProc kernel library

これらのヘッダをインクルードしたら、新しい カーネルパッケージ (kernel package) を構成してG-APIに指定できる:

// Prepare the kernel package and run the graph
cv::GKernelPackage fluid_kernels = cv::gapi::combine // Define a custom kernel package:
(cv::gapi::core::fluid::kernels(), // ...with Fluid Core kernels
cv::gapi::imgproc::fluid::kernels()); // ...and Fluid ImgProc kernels

G-APIでは、カーネル(すなわち演算の実装)はオブジェクトである。カーネルはコレクション、すなわち カーネルパッケージ (kernel package) にまとめられ、これはクラス cv::GKernelPackage で表される。カーネルパッケージの主な目的は、グラフで使いたいカーネルを取りまとめ、それを グラフのコンパイルオプション (graph compilation option) として渡すことである:

segm.apply(cv::gin(imgIn), // Input data vector
cv::gout(imgOut, imgOutCoherency, imgOutOrientation), // Output data vector
cv::compile_args(fluid_kernels)); // Kernel package to use

従来のOpenCVは論理的にモジュールへ分割されており、各モジュールが一連の関数を提供する。G-APIにも「モジュール」が存在し、それは特定のバックエンドが提供するカーネルパッケージとして表される。この例では、グラフ内で適切なFluid関数を利用するために、FluidカーネルパッケージをG-APIに渡す。

カーネルパッケージは結合可能である。上記の例では、「Core」と「ImgProc」のFluidカーネルパッケージを取り、それらを1つに結合している。cv::gapi::combine のドキュメントを参照のこと。

オプションでカーネルパッケージを何も指定しなければ、G-APIはデフォルトのOpenCV実装からなる デフォルト (default) パッケージを使用するため、G-APIグラフはデフォルトでOpenCV関数を介して実行される。OpenCVバックエンドは、他のどのバックエンドよりも広範な機能カバレッジを提供する。この例のようにカーネルパッケージが指定された場合は、それが デフォルト (default) と結合される。つまり、競合が起きた場合にはユーザ指定の実装がデフォルト実装を置き換えるということである。

トラブルシューティングとカスタマイズ

上記の変更を行った後、(OpenCV 4.0では)アプリケーションは次のようなメッセージとともにクラッシュするはずである:

$ ./bin/example_tutorial_porting_anisotropic_image_segmentation_gapi_fluid
terminate called after throwing an instance of 'std::logic_error'
what(): .../modules/gapi/src/backends/fluid/gfluidimgproc.cpp:436: Assertion kernelSize.width == 3 && kernelSize.height == 3 in function run failed
Aborted (core dumped)

Fluidバックエンドには、OpenCV 4.0においていくつかの制限がある(より最新の状況についてはこの wikiページ を参照)。特に、このサンプルで使われているBoxフィルタは静的な3x3のカーネルサイズのみをサポートする。

この問題は、このサンプルでG-APIがBoxフィルタカーネルのFluid版を使わないようにすることで容易に回避できる。それは、先ほど作成したカーネルパッケージから該当するカーネルを取り除くことで実現できる:

fluid_kernels.remove<cv::gapi::imgproc::GBoxFilter>(); // Remove Fluid Box filter as unsuitable,
// G-API will fall-back to OpenCV there.

これでこのカーネルパッケージは、(テンプレート引数として指定された)Boxフィルタカーネルインタフェースの実装を 一切 持たなくなった。上で述べたように、G-APIはこのカーネルを実行するためにOpenCVへフォールバックする。この変更を加えた結果のコードは次のようになる:

// Prepare the kernel package and run the graph
cv::GKernelPackage fluid_kernels = cv::gapi::combine // Define a custom kernel package:
(cv::gapi::core::fluid::kernels(), // ...with Fluid Core kernels
cv::gapi::imgproc::fluid::kernels()); // ...and Fluid ImgProc kernels
fluid_kernels.remove<cv::gapi::imgproc::GBoxFilter>(); // Remove Fluid Box filter as unsuitable,
// G-API will fall-back to OpenCV there.
segm.apply(cv::gin(imgIn), // Input data vector
cv::gout(imgOut, imgOutCoherency, imgOutOrientation), // Output data vector
cv::compile_args(fluid_kernels)); // Kernel package to use

Fluidバックエンドに切り替えた後の、このサンプルのメモリプロファイルを調べてみよう。今度は次のようになる:

Memory profile: G-API/Fluid port of Anisotropic Image Segmentation sample

今度はツールが4.7MiBと報告している。しかもグラフ自体は変更せず、コードを数行変えただけである! これは以前のG-APIの結果に対して約2.4倍、オリジナルのOpenCV版に対して約1.6倍の改善である。

グラフの内部表現が今どうなっているかも調べてみよう。グラフを .dot にダンプすると、次のような可視化が得られる:

Anisotropic image segmentation graph with OpenCV & Fluid kernels

このグラフは(演算とデータオブジェクトという観点では)以前のバージョンと構造的に変わらないが、レイアウトが変わっている(ダンプの左側)ことが容易に見て取れる。

この可視化は、G-APIが混在グラフ、すなわち ヘテロジニアス (heterogeneous) グラフをどう扱うかを反映している。このグラフのほとんどの演算はFluidバックエンドで実装されているが、BoxフィルタはOpenCVバックエンドで実行される。グラフが(矩形によって)分割されていることが容易に見て取れる。G-APIは接続された演算をその親和性に基づいてグループ化し、サブグラフ (subgraph)(G-APIの用語では 島 (island))を形成する。こうして最上位のグラフは、複数のより小さなサブグラフの合成となる。各バックエンドはそのサブグラフ(島)をどう実行するかを決定するため、Fluidバックエンドは可能な限りメモリを最適化により省く一方、OpenCVのBoxフィルタがアクセスする6つの中間バッファは完全に確保され、最適化により省くことはできない。

まとめ

このチュートリアルでは、G-APIとは何か、その主要な設計概念は何か、アルゴリズムをどのようにG-APIへ移植できるか、そしてその後にグラフモデルの利点をどう活用するかを示した。

OpenCV 4.0では、G-APIはまだ初期段階にある。今後のすべての作業の基盤という側面が強いが、現時点でも使用可能な状態である。

さらに、このチュートリアルは今後、カスタムカーネルのプログラミング、並列化などに関する新しい章で拡張される予定である。