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

Next Tutorial: 異方性画像セグメンテーションのG-APIへの移植

概要

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

  • ディープラーニングの推論をG-APIグラフに統合する方法。
  • G-APIグラフをビデオストリーム上で実行し、そこからデータを取得する方法。

前提条件

このサンプルには以下が必要である:

  • GNU/LinuxまたはMicrosoft WindowsのPC(Apple macOSもサポートされているが、テストはされていない);
  • Intel® Distribution of OpenVINO™ Toolkit でビルドされたOpenCV 4.2以降(Intel® TBB でビルドするとなお良い);
  • The following topologies from OpenVINO™ Toolkit Open Model Zoo:
    • face-detection-adas-0001;
    • age-gender-recognition-retail-0013;
    • emotions-recognition-retail-0003.

はじめに: なぜG-APIなのか

多くのコンピュータビジョンのアルゴリズムは、個々の画像ではなくビデオストリーム上で動作する。ストリーム処理は通常、デコード、前処理、検出、トラッキング、(検出された物体の)分類、可視化といった複数のステップから構成され、ビデオ処理パイプラインを形成する。さらに、こうしたパイプラインの多くのステップは並列に実行できる。現代のプラットフォームは同一チップ上にデコーダやGPUといった異なるハードウェアブロックを備えており、さらにディープラーニングのオフロード用にIntel® Movidius™ Neural Compute Stickのような追加のアクセラレータを拡張として接続することもできる。

このように多様な選択肢とビデオ解析アルゴリズムの多様性を考えると、こうしたパイプラインを効果的に管理することはすぐに問題となる。確かに手作業で行うことはできるが、この手法はスケールしない。アルゴリズムに変更が必要になった場合(たとえば新しいパイプラインステップが追加された場合)や、能力の異なる新しいプラットフォームに移植された場合、パイプライン全体を再最適化する必要がある。

バージョン4.2より、OpenCVはこの問題に対する解決策を提供する。OpenCV G-APIは、ディープラーニングの推論(現代の解析パイプラインの要となる要素)を、従来のコンピュータビジョン、さらにはビデオのキャプチャ・デコードと併せて、すべて単一のパイプライン内で管理できるようになった。G-APIはパイプライン処理そのものを引き受けるため、アルゴリズムやプラットフォームが変わっても、実行モデルが自動的にそれに適応する。

パイプラインの概要

このサンプルアプリケーションは、OpenVINO™ Toolkit Open Model Zoo の "Interactive Face Detection" デモをベースにしている。簡略化したパイプラインは次のステップから構成される:

  1. 画像の取得とデコード。
  2. 前処理を伴う検出。
  3. 2つのネットワークを用い、検出した各オブジェクトに前処理を施して分類する;
  4. 可視化。

パイプラインの構築

ビデオストリーミング向けのG-APIグラフの構築は、G-APIの通常の使い方と大きく変わらない。依然として、グラフのデータcv::GMat, cv::GScalar, cv::GArray)と、それに対する演算を定義することが中心となる。推論もまたグラフ内の演算となるが、少し異なる方法で定義される。

ディープラーニングのトポロジーの宣言

G-APIが関数ごとに個別の演算を宣言する従来のCV関数(coreimgproc を参照)とは対照的に、G-APIにおける推論は単一の汎用演算 cv::gapi::infer<> である。いつものように、これは単なるインターフェースであり、内部ではさまざまな方法で実装できる。OpenCV 4.2では、OpenVINO™ Inference Engineベースのバックエンドのみが利用可能であり、OpenCV独自のDNNモジュールベースのバックエンドは今後追加される予定である。

cv::gapi::infer<> は、実行しようとするトポロジーの詳細によってパラメータ化される。演算と同様に、G-APIのトポロジーは厳密に型付けされており、専用のマクロ G_API_NET() で定義される:

// Face detector: takes one Mat, returns another Mat
G_API_NET(Faces, <cv::GMat(cv::GMat)>, "face-detector");
// Age/Gender recognition - takes one Mat, returns two:
// one for Age and one for Gender. In G-API, multiple-return-value operations
// are defined using std::tuple<>.
using AGInfo = std::tuple<cv::GMat, cv::GMat>;
G_API_NET(AgeGender, <AGInfo(cv::GMat)>, "age-gender-recoginition");
// Emotion recognition - takes one Mat, returns another.
G_API_NET(Emotions, <cv::GMat(cv::GMat)>, "emotions-recognition");

演算が G_API_OP() で定義されるのと同様に、ネットワークの記述には3つのパラメータが必要である:

  1. 型名。定義された各トポロジーは個別のC++型として宣言され、プログラム内でさらに利用される — 後述を参照;
  2. std::function<> のようなAPIシグネチャ。G-APIはネットワークをデータを受け取って返す通常の「関数」として扱う。ここでネットワーク Faces(検出器)は cv::GMat を受け取り cv::GMat を返すのに対し、ネットワーク AgeGender は2つの出力(それぞれ年齢と性別のblob)を提供することが分かっている — そのため戻り値の型として std::tuple<> を持つ。
  3. トポロジー名 — 空でない任意の文字列でよく、G-APIは内部でネットワークを区別するためにこれらの名前を使用する。名前は単一のグラフのスコープ内で一意でなければならない。

GComputationの構築

上記のパイプラインをG-APIで表現すると次のようになる:

cv::GComputation pp([]() {
// Declare an empty GMat - the beginning of the pipeline.
// Run face detection on the input frame. Result is a single GMat,
// internally representing an 1x1x200x7 SSD output.
// This is a single-patch version of infer:
// - Inference is running on the whole input image;
// - Image is converted and resized to the network's expected format
// automatically.
// Parse SSD output to a list of ROI (rectangles) using
// a custom kernel. Note: parsing SSD may become a "standard" kernel.
cv::GArray<cv::Rect> faces = custom::PostProc::on(detections, in);
// Now run Age/Gender model on every detected face. This model has two
// outputs (for age and gender respectively).
// A special ROI-list-oriented form of infer<>() is used here:
// - First input argument is the list of rectangles to process,
// - Second one is the image where to take ROI from;
// - Crop/Resize/Layout conversion happens automatically for every image patch
// from the list
// - Inference results are also returned in form of list (GArray<>)
// - Since there're two outputs, infer<> return two arrays (via std::tuple).
std::tie(ages, genders) = cv::gapi::infer<custom::AgeGender>(faces, in);
// Recognize emotions on every face.
// ROI-list-oriented infer<>() is used here as well.
// Since custom::Emotions network produce a single output, only one
// GArray<> is returned here.
// Return the decoded frame as a result as well.
// Input matrix can't be specified as output one, so use copy() here
// (this copy will be optimized out in the future).
cv::GMat frame = cv::gapi::copy(in);
// Now specify the computation's boundaries - our pipeline consumes
// one images and produces five outputs.
cv::GOut(frame, faces, ages, genders, emotions));
});

すべてのパイプラインは、空のデータオブジェクトの宣言から始まる — これらはパイプラインへの入力として機能する。次に、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::GMatcv::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 である:

cmd.get<std::string>("fdm"), // read cmd args: path to topology IR
cmd.get<std::string>("fdw"), // read cmd args: path to weights
cmd.get<std::string>("fdd"), // read cmd args: device specifier
};
cmd.get<std::string>("agem"), // read cmd args: path to topology IR
cmd.get<std::string>("agew"), // read cmd args: path to weights
cmd.get<std::string>("aged"), // read cmd args: device specifier
}.cfgOutputLayers({ "age_conv3", "prob" });
cmd.get<std::string>("emom"), // read cmd args: path to topology IR
cmd.get<std::string>("emow"), // read cmd args: path to weights
cmd.get<std::string>("emod"), // read cmd args: device specifier
};

ここでは3つのパラメータオブジェクト、det_net, age_net, emo_net を定義する。各オブジェクトは、使用する個々のネットワークごとの cv::gapi::ie::Params 構造体のパラメータ化である。コンパイル段階で、G-APIはこの情報を用いて、グラフ内のネットワークパラメータをそれぞれの cv::gapi::infer<> 呼び出しに自動的に対応付ける。

トポロジーに関わらず、すべてのパラメータ構造体は、OpenVINO™ Inference Engine固有の3つの文字列引数で構築される:

  1. トポロジーの中間表現(.xml ファイル)へのパス;
  2. トポロジーのモデル重み(.bin ファイル)へのパス;
  3. 実行先のデバイス — お使いのOpenVINO™ Toolkitのインストール環境に応じて「CPU」「GPU」などを指定する。これらの引数はコマンドラインパーサから取得される。

ネットワークが定義され、カスタムカーネルが実装されたら、パイプラインをストリーミング用にコンパイルする:

// Form a kernel package (with a single OpenCV-based implementation of our
// post-processing) and a network package (holding our three networks).
auto networks = cv::gapi::networks(det_net, age_net, emo_net);
// Compile our pipeline and pass our kernels & networks as
// parameters. This is the place where G-API learns which
// networks & kernels we're actually operating with (the graph
// description itself known nothing about that).
auto cc = pp.compileStreaming(cv::compile_args(kernels, networks));

cv::GComputation::compileStreaming() は、G-APIがスループットの最適化を試みる特別なビデオ指向のグラフコンパイル形式を起動する。このコンパイルの結果は特別な型 cv::GStreamingCompiled のオブジェクトである — 従来の呼び出し可能な cv::GCompiled とは対照的に、これらのオブジェクトはその意味論においてメディアプレーヤーに近い。

覚え書き
cv::GComputation::compileStreaming() では、入力ビデオストリームのフォーマットを記述するメタデータ引数を渡す必要はない — G-APIは入力ベクトルのフォーマットを自動的に判別し、その場でパイプラインをこれらのフォーマットに合わせて調整する。ユーザーは、特定の入力フォーマットにパイプラインを固定するために、通常の cv::GComputation::compile() の場合と同様にメタデータをここに渡すこともできる。

パイプラインの実行

パイプライン化の最適化は、複数の入力ビデオフレームを同時に処理し、パイプラインの異なるステップを並列に実行することに基づいている。このため、フレームワークがビデオストリームを完全に制御する場合に最も効果を発揮する。

ストリーミング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を用いて、独自のストリーミングソースを実装できる。

サンプルアプリケーションでは、入力ソースを次のように指定する:

cc.setSource(cv::gin(in_src));

GComputationは cv::GMatcv::GScalarcv::GArray オブジェクトのように複数の入力を持つ場合があることに注意してほしい。ユーザーはそれぞれに対応するホスト側の型 (cv::Matcv::Scalar、std::vector<>) を入力ベクトルに渡すこともできるが、ストリーミングモードではこれらのオブジェクトは「無限に続く」定数ストリームを生成する。実際のビデオソースストリームと定数データストリームを混在させることは許可されている。

パイプラインの実行は簡単である — cv::GStreamingCompiled::start() を呼び出し、ブロッキングの cv::GStreamingCompiled::pull() または非ブロッキングの cv::GStreamingCompiled::try_pull() でデータを取得するだけでよい。ストリームが終了するまでこれを繰り返す:

// After data source is specified, start the execution
cc.start();
// Declare data objects we will be receiving from the pipeline.
cv::Mat frame; // The captured frame itself
std::vector<cv::Rect> faces; // Array of detected faces
std::vector<cv::Mat> out_ages; // Array of inferred ages (one blob per face)
std::vector<cv::Mat> out_genders; // Array of inferred genders (one blob per face)
std::vector<cv::Mat> out_emotions; // Array of classified emotions (one blob per face)
// Implement different execution policies depending on the display option
// for the best performance.
while (cc.running()) {
auto out_vector = cv::gout(frame, faces, out_ages, out_genders, out_emotions);
if (no_show) {
// This is purely a video processing. No need to balance
// with UI rendering. Use a blocking pull() to obtain
// data. Break the loop if the stream is over.
if (!cc.pull(std::move(out_vector)))
break;
} else if (!cc.try_pull(std::move(out_vector))) {
// Use a non-blocking try_pull() to obtain data.
// If there's no data, let UI refresh (and handle keypress)
if (cv::waitKey(1) >= 0) break;
else continue;
}
// At this point we have data for sure (obtained in either
// blocking or non-blocking way).
frames++;
labels::DrawResults(frame, faces, out_ages, out_genders, out_emotions);
labels::DrawFPS(frame, frames, avg.fps(frames));
if (!no_show) cv::imshow("Out", frame);
}

上記のコードは複雑に見えるかもしれないが、実際にはグラフィカルユーザーインターフェース(GUI)のありなしの2つのモードを処理している:

  • サンプルが「ヘッドレス」モードで実行される場合(--pure オプションが設定されている場合)、このコードは単にブロッキングの pull() でパイプラインからデータを終了するまで取得する。これは最も性能の高い実行モードである。
  • 結果を画面にも表示する場合、ウィンドウシステムはウィンドウの内容を更新しGUIイベントを処理するために多少の時間を必要とする。この場合、デモは非ブロッキングの try_pull() で利用可能なデータがなくなるまでデータを取得し(ただしこれはストリームの終わりを示すものではない — 単に新しいデータがまだ準備できていないことを意味する)、そのうえで最後に得られた結果を表示して画面を更新する。このトリックによりGUIに費やす時間を削減することで、全体の性能をわずかに向上させる。

シリアルモードとの比較

このサンプルは、参照とベンチマークの目的でシリアルモードでも実行できる。この場合、通常の cv::GComputation::compile() が使用され、通常の単一フレームの cv::GCompiled オブジェクトが生成される。パイプライン化の最適化はG-API内では適用されない。cv::VideoCapture オブジェクトから画像フレームを取得してG-APIに渡すのはユーザーの責任である。

cv::VideoCapture cap(input);
cv::Mat in_frame, frame; // The captured frame itself
std::vector<cv::Rect> faces; // Array of detected faces
std::vector<cv::Mat> out_ages; // Array of inferred ages (one blob per face)
std::vector<cv::Mat> out_genders; // Array of inferred genders (one blob per face)
std::vector<cv::Mat> out_emotions; // Array of classified emotions (one blob per face)
while (cap.read(in_frame)) {
pp.apply(cv::gin(in_frame),
cv::gout(frame, faces, out_ages, out_genders, out_emotions),
cv::compile_args(kernels, networks));
labels::DrawResults(frame, faces, out_ages, out_genders, out_emotions);
frames++;
if (frames == 1u) {
// Start timer only after 1st frame processed -- compilation
// happens on-the-fly here
avg.start();
} else {
// Measurfe & draw FPS for all other frames
labels::DrawFPS(frame, frames, avg.fps(frames-1));
}
if (!no_show) {
cv::imshow("Out", frame);
if (cv::waitKey(1) >= 0) break;
}
}

テストマシン(Intel® Core™ i5-6600)において、[Intel® TBB] サポートを有効にしてビルドしたOpenCVで、検出器ネットワークをCPUに、分類器をiGPUに割り当てた場合、パイプライン化したサンプルはシリアル版を1.36倍上回る性能を示す(すなわち全体のスループットで +36% の向上)。

まとめ

G-APIは、ハイブリッドパイプラインを構築し最適化するための技術的な手法を導入する。新しい実行モデルへの切り替えにあたって、G-APIで表現されたアルゴリズムのコードを変更する必要はない — グラフをどのように起動するかが異なるだけである。

リスト: 後処理カーネル

G-APIは、ストリーミングモードで実行されテンソルデータを処理している場合でも、カスタムコードをパイプラインに組み込む簡単な方法を提供する。推論結果は多次元の cv::Mat オブジェクトとして表現されるため、これらへのアクセスは通常のDNNモジュールと同じくらい簡単である。

OpenCVベースのSSD後処理カーネルは、このサンプルで次のように定義および実装されている:

// SSD Post-processing function - this is not a network but a kernel.
// The kernel body is declared separately, this is just an interface.
// This operation takes two Mats (detections and the source image),
// and returns a vector of ROI (filtered by a default threshold).
// Threshold (or a class to select) may become a parameter, but since
// this kernel is custom, it doesn't make a lot of sense.
G_API_OP(PostProc, <cv::GArray<cv::Rect>(cv::GMat, cv::GMat)>, "custom.fd_postproc") {
static cv::GArrayDesc outMeta(const cv::GMatDesc &, const cv::GMatDesc &) {
// This function is required for G-API engine to figure out
// what the output format is, given the input parameters.
// Since the output is an array (with a specific type),
// there's nothing to describe.
}
};
// OpenCV-based implementation of the above kernel.
GAPI_OCV_KERNEL(OCVPostProc, PostProc) {
static void run(const cv::Mat &in_ssd_result,
const cv::Mat &in_frame,
std::vector<cv::Rect> &out_faces) {
const int MAX_PROPOSALS = 200;
const int OBJECT_SIZE = 7;
const cv::Size upscale = in_frame.size();
const cv::Rect surface({0,0}, upscale);
out_faces.clear();
const float *data = in_ssd_result.ptr<float>();
for (int i = 0; i < MAX_PROPOSALS; i++) {
const float image_id = data[i * OBJECT_SIZE + 0]; // batch id
const float confidence = data[i * OBJECT_SIZE + 2];
const float rc_left = data[i * OBJECT_SIZE + 3];
const float rc_top = data[i * OBJECT_SIZE + 4];
const float rc_right = data[i * OBJECT_SIZE + 5];
const float rc_bottom = data[i * OBJECT_SIZE + 6];
if (image_id < 0.f) { // indicates end of detections
break;
}
if (confidence < 0.5f) { // a hard-coded snapshot
continue;
}
// Convert floating-point coordinates to the absolute image
// frame coordinates; clip by the source image boundaries.
rc.x = static_cast<int>(rc_left * upscale.width);
rc.y = static_cast<int>(rc_top * upscale.height);
rc.width = static_cast<int>(rc_right * upscale.width) - rc.x;
rc.height = static_cast<int>(rc_bottom * upscale.height) - rc.y;
out_faces.push_back(rc & surface);
}
}
};