![]() |
OpenCV 4.13.0
Open Source Computer Vision
|
G-API の根底にある中心的な考え方は移植性である。G-API で構築されたパイプラインは移植可能(少なくとも移植可能になり得る)でなければならない。これは、新しいプラットフォーム向けにコンパイルした際に追加作業なしで動作するか、あるいは G-API がアルゴリズム自体にほとんど変更を加えることなくそこで動作させるために必要なツールを提供することを意味する。
この考え方は、カーネルインタフェースをその実装から分離することで実現できる。いったんパイプラインがカーネルインタフェースを用いて構築されると、それは実装に依存しないものとなる。すなわち、実装の詳細(どのカーネルを使うか)は別の段階(グラフのコンパイル)で渡される。
カーネルと実装の階層は次のようになる場合がある:
パイプライン自体は、A、B などの観点だけで表現でき、実行時にどの実装を使用するかは外部の引数となる。
G-API は新しいカーネルインタフェースを定義するためのマクロ G_TYPED_KERNEL() を提供する:
このマクロは新しい型定義のための簡略記法である。新しい型を登録するために3つの引数を取り、型本体が存在することを要求する(下記を参照)。マクロの引数は次のとおりである:
std::function<> ライクなシグネチャ。カーネル宣言は関数宣言と見なせる。どちらの場合も、定義された方法に従って新しいエンティティを使用する必要がある。
カーネルのシグネチャは、グラフ構築時にどの引数を取るかというカーネルの利用構文を定義する。実装はこのシグネチャを用いて、バックエンド固有のコールバックシグネチャへと導出することもできる(次章を参照)。
カーネルは任意の型の値を受け取ることができ、G-API の動的型は特別な方法で扱われる。それ以外のすべての型は G-API にとって不透明であり、outMeta() や実行時コールバックにそのままカーネルへ渡される。
カーネルの戻り値は G-API の動的型 — cv::GMat、cv::GScalar、または cv::GArray<T> — のみとすることができる。演算が複数の出力を持つ場合は、std::tuple<>(前述の G-API 型のみを含むことができる)でラップする必要がある。出力数が任意の演算はサポートされていない。
カーネルが定義されると、G-API が提供する特別なメソッド "::on()" を用いてパイプライン内で使用できる。このメソッドはカーネルで定義されたものと同じシグネチャを持つため、次のコードは
まったく正当な構文である。ただしこの例にはやや冗長な部分があるため、通常はカーネル宣言に、省略可能な引数やより簡潔な構文、Doxygen コメントなどを可能にする C++ 関数ラッパー(「ファクトリメソッド」)が付随する:
そうすると、次のように使用できる:
現在のバージョンでは、カーネル宣言の本体(中括弧内のすべて)に静的関数 outMeta() を含める必要がある。この関数は、演算の入力メタデータと出力メタデータの間の関数的依存関係を確立する。
メタデータとは、カーネルが操作するデータに関する情報である。G-API でない型は G-API にとって不透明であるため、G-API は G* データ記述子(すなわち cv::GMat などの次元やフォーマット)のみを扱う。
outMeta() は、カーネルのシグネチャがどのように導出コールバックへ変換され得るかの一例でもある。この例では outMeta() のシグネチャはカーネルのシグネチャ(マクロ内で定義されたもの)に正確に従っているが、異なっている点に注意してほしい。カーネルが cv::GMat を期待する箇所で、outMeta() は cv::GMatDesc(cv::GMat のための G-API のメタデータ構造体)を受け取り、返す。
outMeta() の目的は、計算内で入力から出力へとメタデータ情報を伝播させ、内部(中間・一時)データオブジェクトのメタデータを推論することである。この情報は、グラフのコンパイル時に G-API フレームワークが行うさらなるパイプライン最適化、メモリ割り当て、その他の処理に必要となる。
カーネルが宣言されると、そのインタフェースを用いて、異なるバックエンドにおけるこのカーネルのバージョンを実装できる。この概念はオブジェクト指向プログラミングの「インタフェース/実装」イディオムから自然に導かれる。すなわち、1つのインタフェースは複数回実装でき、あるカーネルの異なる実装は、アルゴリズム(パイプライン)のロジックを壊すことなく互いに置換可能でなければならない(リスコフの置換原則)。
すべてのバックエンドはカーネルインタフェースを実装する独自の方法を定義する。ただしその方法は規則的である。どのようなプラグインであっても、そのカーネル実装はカーネルインタフェース型から「派生」していなければならない。
カーネル実装はその後、カーネルパッケージとして整理される。カーネルパッケージは、適切なカーネルを選択する方法に関する G-API へのヒントとともに、コンパイル引数として cv::GComputation::compile() に渡される(これについては「ヘテロジニアス性 (Heterogeneity)」[TBD] でさらに詳しく述べる)。
例えば、前述の Filter2D は「リファレンス」CPU(OpenCV)プラグインにおいて次のように実装される(注 — これは境界処理が適切でない簡略化された形である):
CPU(OpenCV)プラグインが元のカーネルシグネチャをどのように変換したかに注目してほしい:
GCPUFilter2D::run() は元のカーネルシグネチャより引数を1つ多く取る。ここでのカーネル開発者にとっての基本的な直観は、その cv::Mat オブジェクトが元の cv::GMat の代わりにどこから来るのかを気にしないこと、そしてプラグインによって定義されたシグネチャの規約に従うだけでよいということである。G-API は実行時にこのメソッドを呼び出し、必要なすべての情報を提供する(そして元の不透明なデータをそのまま転送する)。
カーネルが単一のものであるのは API レベルだけ、という場合がある。これはユーザにとって便利だが、特定の実装側では、その処理を行うために複数のカーネル(サブグラフ)を持つ方が望ましいことがある。例として goodFeaturesToTrack() が挙げられる。OpenCV バックエンドでは単一のカーネルのままでよいが、Fluid では複合カーネルとなる。Fluid は Harris レスポンスの計算は扱えるが、疎な非極大値抑制や STL ベクトルへの点抽出は行えないためである:
複合カーネルの実装は、汎用マクロ GAPI_COMPOUND_KERNEL() を用いて定義できる:
複合カーネルを G-API の高階関数、すなわちカーネルのように見えるが実際にはサブグラフを生成する C++ 関数と区別することが重要である。核心的な違いは、複合カーネルは実装の詳細であり、あるカーネルの実装は(バックエンドの能力に応じて)複合であってもなくてもよいのに対し、高階関数は G-API の用語でいう「マクロ」であり、したがってバックエンドによって実装される必要のあるインタフェースとしては機能できない点である。