![]() |
OpenCV 4.13.0
Open Source Computer Vision
|
Next Tutorial: 異方性画像セグメンテーションのG-APIへの移植
このチュートリアルでは以下を学ぶ:
このサンプルには以下が必要である:
face-detection-adas-0001;age-gender-recognition-retail-0013;emotions-recognition-retail-0003.多くのコンピュータビジョンのアルゴリズムは、個々の画像ではなくビデオストリーム上で動作する。ストリーム処理は通常、デコード、前処理、検出、トラッキング、(検出された物体の)分類、可視化といった複数のステップから構成され、ビデオ処理パイプラインを形成する。さらに、こうしたパイプラインの多くのステップは並列に実行できる。現代のプラットフォームは同一チップ上にデコーダやGPUといった異なるハードウェアブロックを備えており、さらにディープラーニングのオフロード用にIntel® Movidius™ Neural Compute Stickのような追加のアクセラレータを拡張として接続することもできる。
このように多様な選択肢とビデオ解析アルゴリズムの多様性を考えると、こうしたパイプラインを効果的に管理することはすぐに問題となる。確かに手作業で行うことはできるが、この手法はスケールしない。アルゴリズムに変更が必要になった場合(たとえば新しいパイプラインステップが追加された場合)や、能力の異なる新しいプラットフォームに移植された場合、パイプライン全体を再最適化する必要がある。
バージョン4.2より、OpenCVはこの問題に対する解決策を提供する。OpenCV G-APIは、ディープラーニングの推論(現代の解析パイプラインの要となる要素)を、従来のコンピュータビジョン、さらにはビデオのキャプチャ・デコードと併せて、すべて単一のパイプライン内で管理できるようになった。G-APIはパイプライン処理そのものを引き受けるため、アルゴリズムやプラットフォームが変わっても、実行モデルが自動的にそれに適応する。
このサンプルアプリケーションは、OpenVINO™ Toolkit Open Model Zoo の "Interactive Face Detection" デモをベースにしている。簡略化したパイプラインは次のステップから構成される:
ビデオストリーミング向けのG-APIグラフの構築は、G-APIの通常の使い方と大きく変わらない。依然として、グラフのデータ(cv::GMat, cv::GScalar, cv::GArray)と、それに対する演算を定義することが中心となる。推論もまたグラフ内の演算となるが、少し異なる方法で定義される。
G-APIが関数ごとに個別の演算を宣言する従来のCV関数(core や imgproc を参照)とは対照的に、G-APIにおける推論は単一の汎用演算 cv::gapi::infer<> である。いつものように、これは単なるインターフェースであり、内部ではさまざまな方法で実装できる。OpenCV 4.2では、OpenVINO™ Inference Engineベースのバックエンドのみが利用可能であり、OpenCV独自のDNNモジュールベースのバックエンドは今後追加される予定である。
cv::gapi::infer<> は、実行しようとするトポロジーの詳細によってパラメータ化される。演算と同様に、G-APIのトポロジーは厳密に型付けされており、専用のマクロ G_API_NET() で定義される:
演算が G_API_OP() で定義されるのと同様に、ネットワークの記述には3つのパラメータが必要である:
std::function<> のようなAPIシグネチャ。G-APIはネットワークをデータを受け取って返す通常の「関数」として扱う。ここでネットワーク Faces(検出器)は cv::GMat を受け取り cv::GMat を返すのに対し、ネットワーク AgeGender は2つの出力(それぞれ年齢と性別のblob)を提供することが分かっている — そのため戻り値の型として std::tuple<> を持つ。上記のパイプラインをG-APIで表現すると次のようになる:
すべてのパイプラインは、空のデータオブジェクトの宣言から始まる — これらはパイプラインへの入力として機能する。次に、Faces 検出ネットワーク向けに特殊化された汎用の cv::gapi::infer<> を呼び出す。cv::gapi::infer<> はそのシグネチャをテンプレート引数から継承する — この場合、1つの入力 cv::GMat を期待し、1つの出力 cv::GMat を生成する。
このサンプルでは、学習済みのSSDベースのネットワークを使用しており、その出力は検出結果(オブジェクトの関心領域、ROI)の配列に解析する必要がある。これはカスタム演算 custom::PostProc によって行われ、矩形の配列(型 cv::GArray<cv::Rect>)をパイプラインに返す。この演算は信頼度のしきい値によって結果のフィルタリングも行う — これらの詳細はカーネル自体の中に隠蔽されている。それでも、グラフ構築の時点ではインターフェースのみを扱い、パイプラインを表現するために実際のカーネルは必要ない — そのため、この後処理の実装は後で示す。
検出結果の出力がオブジェクトの配列に解析された後、それらのいずれに対しても分類を実行できる。G-APIはまだ for_each() のようなグラフ内ループの構文をサポートしていないが、その代わりに cv::gapi::infer<> にはリスト指向の特別なオーバーロードが用意されている。
ユーザーは cv::gapi::infer<> を第1引数に cv::GArray を指定して呼び出せる。そうするとG-APIは、与えられたフレーム(第2引数)の与えられたリスト内のすべての矩形に対して、関連付けられたネットワークを実行する必要があると判断する。このような演算の結果もリストとなる — cv::GMat の cv::GArray である。
AgeGender ネットワーク自体は2つの出力を生成するため、cv::gapi::infer のリストベース版での出力型は配列のタプルとなる。この入力を2つの個別のオブジェクトに分解するために std::tie() を使用する。
Emotions ネットワークは単一の出力を生成するため、そのリストベースの推論の戻り値の型は cv::GArray<cv::GMat> である。
G-APIは構築と構成を厳密に分離する — アルゴリズムのコード自体をプラットフォーム中立に保つという考えに基づく。上記のリストでは、演算を宣言して全体のデータフローを表現しただけで、OpenVINO™ を使用することにすら言及していない。何を行うかを記述しただけで、どのように行うかは記述していない。これら2つの側面を明確に分離して保つことが、G-APIの設計目標である。
プラットフォーム固有の詳細は、パイプラインがコンパイルされるとき、すなわち宣言的な形式から実行可能な形式に変換されるときに生じる。処理をどのように実行するかはコンパイル引数によって指定され、新しい推論/ストリーミング機能もこのルールの例外ではない。
G-APIはインターフェースを実装するバックエンドの上に構築されている(詳細は Architecture および Kernels を参照)— したがって cv::gapi::infer<> はさまざまなバックエンドで実装できる関数である。OpenCV 4.2では、推論用にOpenVINO™ Inference Engineバックエンドのみが利用可能である。G-APIのすべての推論バックエンドは、バックエンド固有のニューラルネットワークパラメータを表現するための特別なパラメータ化可能な構造体を提供しなければならない — この場合、それは cv::gapi::ie::Params である:
ここでは3つのパラメータオブジェクト、det_net, age_net, emo_net を定義する。各オブジェクトは、使用する個々のネットワークごとの cv::gapi::ie::Params 構造体のパラメータ化である。コンパイル段階で、G-APIはこの情報を用いて、グラフ内のネットワークパラメータをそれぞれの cv::gapi::infer<> 呼び出しに自動的に対応付ける。
トポロジーに関わらず、すべてのパラメータ構造体は、OpenVINO™ Inference Engine固有の3つの文字列引数で構築される:
ネットワークが定義され、カスタムカーネルが実装されたら、パイプラインをストリーミング用にコンパイルする:
cv::GComputation::compileStreaming() は、G-APIがスループットの最適化を試みる特別なビデオ指向のグラフコンパイル形式を起動する。このコンパイルの結果は特別な型 cv::GStreamingCompiled のオブジェクトである — 従来の呼び出し可能な cv::GCompiled とは対照的に、これらのオブジェクトはその意味論においてメディアプレーヤーに近い。
パイプライン化の最適化は、複数の入力ビデオフレームを同時に処理し、パイプラインの異なるステップを並列に実行することに基づいている。このため、フレームワークがビデオストリームを完全に制御する場合に最も効果を発揮する。
ストリーミングAPIの背後にある考え方は、ユーザーがパイプラインへの入力ソースを指定し、その後G-APIがソースが終了するかユーザーが実行を中断するまで自動的にその実行を管理するというものである。G-APIはソースから新しい画像データを取得し、それを処理のためにパイプラインに渡す。
ストリーミングソースはインターフェース cv::gapi::wip::IStreamSource で表現される。このインターフェースを実装するオブジェクトは、ヘルパー関数 cv::gin() を介して通常の入力として GStreamingCompiled に渡せる。OpenCV 4.2では、パイプラインごとに1つのストリーミングソースのみが許可される — この制約は将来緩和される予定である。
OpenCVには優れたクラス cv::VideoCapture が付属しており、デフォルトでG-APIはそれに基づくストリームソースクラス cv::gapi::wip::GCaptureSource を同梱している。ユーザーは、たとえば VAAPI やその他のメディアまたはネットワークAPIを用いて、独自のストリーミングソースを実装できる。
サンプルアプリケーションでは、入力ソースを次のように指定する:
GComputationには依然として cv::GMat, cv::GScalar, cv::GArray のような複数の入力を持てる点に注意してほしい。ユーザーは、対応するホスト側の型(cv::Mat, cv::Scalar, std::vector<>)を入力ベクトルに渡すこともできるが、ストリーミングモードではこれらのオブジェクトは「無限の」定数ストリームを生成する。実際のビデオソースストリームと定数データストリームの混在は許可されている。
パイプラインの実行は簡単である — cv::GStreamingCompiled::start() を呼び出し、ブロッキングの cv::GStreamingCompiled::pull() または非ブロッキングの cv::GStreamingCompiled::try_pull() でデータを取得するだけでよい。ストリームが終了するまでこれを繰り返す:
上記のコードは複雑に見えるかもしれないが、実際にはグラフィカルユーザーインターフェース(GUI)のありなしの2つのモードを処理している:
--pure オプションが設定されている場合)、このコードは単にブロッキングの pull() でパイプラインからデータを終了するまで取得する。これは最も性能の高い実行モードである。try_pull() で利用可能なデータがなくなるまでデータを取得し(ただしこれはストリームの終わりを示すものではない — 単に新しいデータがまだ準備できていないことを意味する)、そのうえで最後に得られた結果を表示して画面を更新する。このトリックによりGUIに費やす時間を削減することで、全体の性能をわずかに向上させる。このサンプルは、参照とベンチマークの目的でシリアルモードでも実行できる。この場合、通常の cv::GComputation::compile() が使用され、通常の単一フレームの cv::GCompiled オブジェクトが生成される。パイプライン化の最適化はG-API内では適用されない。cv::VideoCapture オブジェクトから画像フレームを取得してG-APIに渡すのはユーザーの責任である。
テストマシン(Intel® Core™ i5-6600)において、[Intel® TBB] サポートを有効にしてビルドしたOpenCVで、検出器ネットワークをCPUに、分類器をiGPUに割り当てた場合、パイプライン化したサンプルはシリアル版を1.36倍上回る性能を示す(すなわち全体のスループットで +36% の向上)。
G-APIは、ハイブリッドパイプラインを構築し最適化するための技術的な手法を導入する。新しい実行モデルへの切り替えにあたって、G-APIで表現されたアルゴリズムのコードを変更する必要はない — グラフをどのように起動するかが異なるだけである。
G-APIは、ストリーミングモードで動作しテンソルデータを処理している場合でも、カスタムコードをパイプラインに組み込む簡単な方法を提供する。推論結果は多次元の cv::Mat オブジェクトで表現されるため、それらへのアクセスは通常のDNNモジュールと同じくらい簡単である。
OpenCVベースのSSD後処理カーネルは、このサンプルで次のように定義および実装されている: