【MediaPipe】HelloWorldからMediaPipeの動作/構成を学ぶ

2020.05.22

カフェチームの山本です。

今まで、いくつか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によって実行されていることがわかります。

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】HelloWorldのプログラム動作/処理を解析してみた

参考にさせていただいたページ

MediaPipe Concepts https://mediapipe.readthedocs.io/en/latest/concepts.html

MediaPipe Visualizer https://viz.mediapipe.dev/