【MediaPipe】HelloWorldからMediaPipeの動作/構成を学ぶ
カフェチームの山本です。
今まで、いくつかMediaPipeに関する記事を挙げてきました。
今回は、これから内部を変更していくにあたり、MediaPipeそのものを学んでおこうと思います。処理パイプラインのグラフを見るまでにとどめ、データの入力・出力のプログラムは、今回は対象外としています。
学習しただけであるため、特に結論はありません。ご参考になれば幸いです。
MediaPipeに関連する記事はこちらにまとめてあります。
MediaPipeの概要
概要
公式ドキュメントを見てみると、次のように書かれています
MediaPipe is a graph-based framework for building multimodal (video, audio, and sensor) applied machine learning pipelines. MediaPipe is cross-platform running on mobile devices, workstations and servers, and supports mobile GPU acceleration. With MediaPipe, an applied machine learning pipeline can be built as a graph of modular components, including, for instance, inference models and media processing functions. Sensory data such as audio and video streams enter the graph, and perceived descriptions such as object-localization and face-landmark streams exit the graph. An example graph that performs real-time hand tracking on mobile GPU is shown below.
要点を挙げると、こんな感じでしょうか
- 映像・音声・センサデータに利用できる、機械学習パイプラインを作成するためのフレームワーク
- クロスプラットフォームで動かすことができる(モバイル端末・デスクトップ環境・サーバなど)。GPU処理をサポート
- コンポーネント同士を接続するようにして構築する
構成要素
Conceptのページをみてみると、以下の要素があるようです。わかりにくい方は、先にページ下部のHelloWorldサンプルのグラフを見ると良いと思います。
- Packet
- データをやり取りする単位
- タイムスタンプを持っている
- 内部的にはimmutableなデータへのポインタを持っていて、コピーを参照カウントで管理
- Graph
- 処理を行うNodeとパスの接続関係を定義
- Nodes
- Packetを受け取り処理したり新たに生成する、メインの箇所
- 入出力としてPortを持ち、パケットのタグ/インデックスで識別
- クラス名は「~Calculator」と表記される
- Streams
- ノード間の接続
- 一連のPacketがやりとりされる。その際、タイムスタンプの値は単調増加しなければいけない
- Side packets
- ノード間の接続
- Streamsと異なり、単一のパケットを送信する
- (?。若干、理解不足です、、、)
- Packet Ports
- Nodeの入出力を行う
- 出力されたポートに対して、複数の入力ポートを接続可能
ノードのライフサイクル
各ノードは以下のライフサイクルをもつ
- Open
- 処理開始前(グラフ作成時)に1回呼ばれる
- Calculatorクラスのコンストラクタとは別
- Process
- 新しいデータ(パケット)が来たら毎回呼ばれる
- (内部でどのパケットか判定して、処理を分岐させる)
- Graph
- 処理終了後(グラフ削除時)に1回呼ばれる
- Calculatorクラスのデストラクタとは別
HelloWorldのプログラム
MediaPipeに用意されているHelloWorldの実行コマンドは以下のようで、mediapipe/examples/desktop/hello_worldのhello_worldがbazelによって実行されていることがわかります。
bazel run --define MEDIAPIPE_DISABLE_GPU=1 \ mediapipe/examples/desktop/hello_world:hello_world
mediapipe/examples/desktop/hello_worldのBUILDを見ると、hello_world.cc が実行されていることがわかります。
cc_binary( name = "hello_world", srcs = ["hello_world.cc"], visibility = ["//visibility:public"], deps = [ "//mediapipe/calculators/core:pass_through_calculator", "//mediapipe/framework:calculator_graph", "//mediapipe/framework/port:logging", "//mediapipe/framework/port:parse_text_proto", "//mediapipe/framework/port:status", ], )
mediapipe/examples/desktop/hello_world/hello_world.ccを見ると以下のことがわかります。
- main関数があり、mediapipe::PrintHelloWorld()を実行し、結果をチェックしている(62, 64行目)
- PrintHelloWorld()の内部でGraphがテキストで定義されている。それがパースされGraphを構成している(26~39行目)
- (43行目~57行目でグラフに対してデータの入出力をしている。次回のブログでとりあげます)
#include "mediapipe/framework/calculator_graph.h" #include "mediapipe/framework/port/logging.h" #include "mediapipe/framework/port/parse_text_proto.h" #include "mediapipe/framework/port/status.h" namespace mediapipe { ::mediapipe::Status PrintHelloWorld() { // Configures a simple graph, which concatenates 2 PassThroughCalculators. CalculatorGraphConfig config = ParseTextProtoOrDie<CalculatorGraphConfig>(R"( input_stream: "in" output_stream: "out" node { calculator: "PassThroughCalculator" input_stream: "in" output_stream: "out1" } node { calculator: "PassThroughCalculator" input_stream: "out1" output_stream: "out" } )"); CalculatorGraph graph; MP_RETURN_IF_ERROR(graph.Initialize(config)); ASSIGN_OR_RETURN(OutputStreamPoller poller, graph.AddOutputStreamPoller("out")); MP_RETURN_IF_ERROR(graph.StartRun({})); // Give 10 input packets that contains the same std::string "Hello World!". for (int i = 0; i < 10; ++i) { MP_RETURN_IF_ERROR(graph.AddPacketToInputStream( "in", MakePacket<std::string>("Hello World!").At(Timestamp(i)))); } // Close the input stream "in". MP_RETURN_IF_ERROR(graph.CloseInputStream("in")); mediapipe::Packet packet; // Get the output packets std::string. while (poller.Next(&packet)) { LOG(INFO) << packet.Get<std::string>(); } return graph.WaitUntilDone(); } } // namespace mediapipe int main(int argc, char** argv) { google::InitGoogleLogging(argv[0]); CHECK(mediapipe::PrintHelloWorld().ok()); return 0; }
HelloWorldのグラフ構成
MediaPipe Visualizerというものが用意されているため、これ使ってグラフを図示してみます(一部、追記しています)。
input_stream: "in" output_stream: "out" node { calculator: "PassThroughCalculator" input_stream: "in" output_stream: "out1" } node { calculator: "PassThroughCalculator" input_stream: "out1" output_stream: "out" }
- Graph全体の入出力として、"in", "out"が定義されている(1,2行目)
- ノード(PassThroughCalculator)が2つあり、表記は「PassThrough」「PassThrough_2」になっている。それぞれのノードでPort(入出)が定義されている。(3~7、8~12行目)
- 名前によって接続関係が定義される(1、2、5~6、10~11行目)
- 入力"in"からPacketが渡され、StreamとNodeを通って、出力"out"からでてくる
PassThroughCalculatorは、入力されたPacketをそのまま出力するので、Graph全体では、入力されたPacketがそのまま出力されます。
まとめ
今回はMediaPipeの概要を把握し、HelloWorldのグラフの構造をみてみました。NodeをStreamで接続していくことでGraphを作成すること、それを動かすプログラムからGraphをパースし、データを入出力すること、がわかりました。
次回はグラフからデータを取り出すために、プログラムの中を見ていきます。
次↓
参考にさせていただいたページ
MediaPipe Concepts https://mediapipe.readthedocs.io/en/latest/concepts.html
MediaPipe Visualizer https://viz.mediapipe.dev/