この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
カフェチームの山本です。
今まで、いくつか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によって実行されていることがわかります。
HelloWorldを実行するコマンド
bazel run --define MEDIAPIPE_DISABLE_GPU=1 \
mediapipe/examples/desktop/hello_world:hello_world
mediapipe/examples/desktop/hello_worldのBUILDを見ると、hello_world.cc が実行されていることがわかります。
mediapipe/examples/desktop/hello_world/BUILD
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行目でグラフに対してデータの入出力をしている。次回のブログでとりあげます)
mediapipe/examples/desktop/hello_world/hello_world.cc
#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/