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

G-API カーネルAPI

G-API の根底にある中心的な考え方は移植性である。G-API で構築されたパイプラインは移植可能(少なくとも移植可能になり得る)でなければならない。これは、新しいプラットフォーム向けにコンパイルした際に追加作業なしで動作するか、あるいは G-API がアルゴリズム自体にほとんど変更を加えることなくそこで動作させるために必要なツールを提供することを意味する。

この考え方は、カーネルインタフェースをその実装から分離することで実現できる。いったんパイプラインがカーネルインタフェースを用いて構築されると、それは実装に依存しないものとなる。すなわち、実装の詳細(どのカーネルを使うか)は別の段階(グラフのコンパイル)で渡される。

カーネルと実装の階層は次のようになる場合がある:

Kernel API/implementation hierarchy example

パイプライン自体は、AB などの観点だけで表現でき、実行時にどの実装を使用するかは外部の引数となる。

カーネルの定義

G-API は新しいカーネルインタフェースを定義するためのマクロ G_TYPED_KERNEL() を提供する:

#include <opencv2/gapi.hpp>
G_TYPED_KERNEL(GFilter2D,
"org.opencv.imgproc.filters.filter2D")
{
static cv::GMatDesc // outMeta's return value type
outMeta(cv::GMatDesc in , // descriptor of input GMat
int ddepth , // depth parameter
cv::Mat /* coeffs */, // (unused)
cv::Point /* anchor */, // (unused)
double /* scale */, // (unused)
int /* border */, // (unused)
cv::Scalar /* bvalue */ ) // (unused)
{
return in.withDepth(ddepth);
}
};

このマクロは新しい型定義のための簡略記法である。新しい型を登録するために3つの引数を取り、型本体が存在することを要求する(下記を参照)。マクロの引数は次のとおりである:

  1. カーネルインタフェース名 — このマクロで定義される新しい型の名前としても機能する。
  2. カーネルのシグネチャ — カーネルの API を定義する std::function<> ライクなシグネチャ。
  3. カーネルの一意な名前 — システム内で型情報が取り除かれた際にカーネルを識別するために用いられる。

カーネル宣言は関数宣言と見なせる。どちらの場合も、定義された方法に従って新しいエンティティを使用する必要がある。

カーネルのシグネチャは、グラフ構築時にどの引数を取るかというカーネルの利用構文を定義する。実装はこのシグネチャを用いて、バックエンド固有のコールバックシグネチャへと導出することもできる(次章を参照)。

カーネルは任意の型の値を受け取ることができ、G-API の動的型は特別な方法で扱われる。それ以外のすべての型は G-API にとって不透明であり、outMeta() や実行時コールバックにそのままカーネルへ渡される。

カーネルの戻り値は G-API の動的型 — cv::GMatcv::GScalar、または cv::GArray<T>のみとすることができる。演算が複数の出力を持つ場合は、std::tuple<>(前述の G-API 型のみを含むことができる)でラップする必要がある。出力数が任意の演算はサポートされていない。

カーネルが定義されると、G-API が提供する特別なメソッド "::on()" を用いてパイプライン内で使用できる。このメソッドはカーネルで定義されたものと同じシグネチャを持つため、次のコードは

cv::GMat out = GFilter2D::on(/* GMat */ in,
/* int */ -1,
/* Mat */ conv_kernel_mat,
/* Point */ cv::Point(-1,-1),
/* double */ 0.,
/* int */ cv::BORDER_DEFAULT,
/* Scalar */ cv::Scalar(0));

まったく正当な構文である。ただしこの例にはやや冗長な部分があるため、通常はカーネル宣言に、省略可能な引数やより簡潔な構文、Doxygen コメントなどを可能にする C++ 関数ラッパー(「ファクトリメソッド」)が付随する:

int ddepth,
cv::Point anchor = cv::Point(-1,-1),
double scale = 0.,
int border = cv::BORDER_DEFAULT,
{
return GFilter2D::on(in, ddepth, k, anchor, scale, border, bval);
}

そうすると、次のように使用できる:

cv::GMat out = filter2D(in, -1, conv_kernel_mat);

補足情報

現在のバージョンでは、カーネル宣言の本体(中括弧内のすべて)に静的関数 outMeta() を含める必要がある。この関数は、演算の入力メタデータと出力メタデータの間の関数的依存関係を確立する。

メタデータとは、カーネルが操作するデータに関する情報である。G-API でない型は G-API にとって不透明であるため、G-API は G* データ記述子(すなわち cv::GMat などの次元やフォーマット)のみを扱う。

outMeta() は、カーネルのシグネチャがどのように導出コールバックへ変換され得るかの一例でもある。この例では outMeta() のシグネチャはカーネルのシグネチャ(マクロ内で定義されたもの)に正確に従っているが、異なっている点に注意してほしい。カーネルが cv::GMat を期待する箇所で、outMeta()cv::GMatDesccv::GMat のための G-API のメタデータ構造体)を受け取り、返す。

outMeta() の目的は、計算内で入力から出力へとメタデータ情報を伝播させ、内部(中間・一時)データオブジェクトのメタデータを推論することである。この情報は、グラフのコンパイル時に G-API フレームワークが行うさらなるパイプライン最適化、メモリ割り当て、その他の処理に必要となる。

カーネルの実装

カーネルが宣言されると、そのインタフェースを用いて、異なるバックエンドにおけるこのカーネルのバージョンを実装できる。この概念はオブジェクト指向プログラミングの「インタフェース/実装」イディオムから自然に導かれる。すなわち、1つのインタフェースは複数回実装でき、あるカーネルの異なる実装は、アルゴリズム(パイプライン)のロジックを壊すことなく互いに置換可能でなければならない(リスコフの置換原則)。

すべてのバックエンドはカーネルインタフェースを実装する独自の方法を定義する。ただしその方法は規則的である。どのようなプラグインであっても、そのカーネル実装はカーネルインタフェース型から「派生」していなければならない。

カーネル実装はその後、カーネルパッケージとして整理される。カーネルパッケージは、適切なカーネルを選択する方法に関する G-API へのヒントとともに、コンパイル引数として cv::GComputation::compile() に渡される(これについては「ヘテロジニアス性 (Heterogeneity)」[TBD] でさらに詳しく述べる)。

例えば、前述の Filter2D は「リファレンス」CPU(OpenCV)プラグインにおいて次のように実装される( — これは境界処理が適切でない簡略化された形である):

#include <opencv2/gapi/cpu/gcpukernel.hpp> // GAPI_OCV_KERNEL()
#include <opencv2/imgproc.hpp> // cv::filter2D()
GAPI_OCV_KERNEL(GCPUFilter2D, GFilter2D)
{
static void
run(const cv::Mat &in, // in - derived from GMat
const int ddepth, // opaque (passed as-is)
const cv::Mat &k, // opaque (passed as-is)
const cv::Point &anchor, // opaque (passed as-is)
const double delta, // opaque (passed as-is)
const int border, // opaque (passed as-is)
const cv::Scalar &, // opaque (passed as-is)
cv::Mat &out) // out - derived from GMat (retval)
{
cv::filter2D(in, out, ddepth, k, anchor, delta, border);
}
};

CPU(OpenCV)プラグインが元のカーネルシグネチャをどのように変換したかに注目してほしい:

  • 入力のcv::GMatは、内部のOpenCV関数呼び出しのための実際の入力データを保持するcv::Matに置き換えられている。
  • 出力の cv::GMat は追加の出力引数へと変換された。そのため GCPUFilter2D::run() は元のカーネルシグネチャより引数を1つ多く取る。

ここでのカーネル開発者にとっての基本的な考え方は、それらのcv::Matオブジェクトが元のcv::GMatのどこから来たのかを気にしないこと、そしてプラグインが定義するシグネチャの規約に従うだけでよいということである。G-APIは実行時にこのメソッドを呼び出し、必要な情報をすべて供給する(そして元の不透明データをそのまま転送する)。

複合カーネル

カーネルが単一のものであるのは API レベルだけ、という場合がある。これはユーザにとって便利だが、特定の実装側では、その処理を行うために複数のカーネル(サブグラフ)を持つ方が望ましいことがある。例として goodFeaturesToTrack() が挙げられる。OpenCV バックエンドでは単一のカーネルのままでよいが、Fluid では複合カーネルとなる。Fluid は Harris レスポンスの計算は扱えるが、疎な非極大値抑制や STL ベクトルへの点抽出は行えないためである:

複合カーネルの実装は、汎用マクロ GAPI_COMPOUND_KERNEL() を用いて定義できる:

#include <opencv2/gapi/gcompoundkernel.hpp> // GAPI_COMPOUND_KERNEL()
using PointArray2f = cv::GArray<cv::Point2f>;
G_TYPED_KERNEL(HarrisCorners,
<PointArray2f(cv::GMat,int,double,double,int,double)>,
"org.opencv.imgproc.harris_corner")
{
static cv::GArrayDesc outMeta(const cv::GMatDesc &,
int,
double,
double,
int,
double)
{
// No special metadata for arrays in G-API (yet)
}
};
// Define Fluid-backend-local kernels which form GoodFeatures
G_TYPED_KERNEL(HarrisResponse,
<cv::GMat(cv::GMat,double,int,double)>,
"org.opencv.fluid.harris_response")
{
static cv::GMatDesc outMeta(const cv::GMatDesc &in,
double,
int,
double)
{
return in.withType(CV_32F, 1);
}
};
G_TYPED_KERNEL(ArrayNMS,
<PointArray2f(cv::GMat,int,double)>,
"org.opencv.cpu.nms_array")
{
static cv::GArrayDesc outMeta(const cv::GMatDesc &,
int,
double)
{
}
};
GAPI_COMPOUND_KERNEL(GFluidHarrisCorners, HarrisCorners)
{
static PointArray2f
expand(cv::GMat in,
int maxCorners,
double quality,
double minDist,
int blockSize,
double k)
{
cv::GMat response = HarrisResponse::on(in, quality, blockSize, k);
return ArrayNMS::on(response, maxCorners, minDist);
}
};
// Then implement HarrisResponse as Fluid kernel and NMSresponse
// as a generic (OpenCV) kernel

複合カーネルを G-API の高階関数、すなわちカーネルのように見えるが実際にはサブグラフを生成する C++ 関数と区別することが重要である。核心的な違いは、複合カーネルは実装の詳細であり、あるカーネルの実装は(バックエンドの能力に応じて)複合であってもなくてもよいのに対し、高階関数は G-API の用語でいう「マクロ」であり、したがってバックエンドによって実装される必要のあるインタフェースとしては機能できない点である。