【MediaPipe】Multi Hand Trackingのプログラムを解析してみた

2020.05.27

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

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

前回は、MediaPipeのHelloWorldのプログラムの動作を解析しました。

【MediaPipe】HelloWorldのプログラム動作/処理を解析してみた

今回は、Multi Hand TrackingのプログラムとGraphを解析していきます。

MediaPipeに関連する記事はこちらにまとめてあります。

【MediaPipe】投稿記事まとめ

Multi Hand Tracking のプログラム

まずはMulti Hand Trackingのビルドコマンドから見ていきましょう。mediapipe/examples/desktop/multi_hand_tracking内のmulti_hand_tracking_cpuを指定してビルドしていることがわかります。

bazel build -c opt --define MEDIAPIPE_DISABLE_GPU=1 mediapipe/examples/desktop/multi_hand_tracking:multi_hand_tracking_cpu

mediapipe/examples/desktop/multi_hand_tracking/BUILDを見てみると、multi_hand_tracking_cpuはmediapipe/examples/desktop内のdemo_run_graph_mainを参照しており、そこから実行している(いそうな)ことがわかります。(28、30行目)

mediapipe/examples/desktop/multi_hand_tracking/BUILD

cc_binary(
    name = "multi_hand_tracking_cpu",
    deps = [
        "//mediapipe/examples/desktop:demo_run_graph_main",
        "//mediapipe/graphs/hand_tracking:multi_hand_desktop_tflite_calculators",
    ],
)

mediapipe/examples/desktop/BUILDを見てみると、demo_run_graph_mainが同フォルダ内のdemo_run_graph_main.ccを参照していることがわかります。(38、39行目)

mediapipe/examples/desktop/BUILD

cc_library(
    name = "demo_run_graph_main",
    srcs = ["demo_run_graph_main.cc"],
    deps = [
        "//mediapipe/framework:calculator_framework",
        "//mediapipe/framework/formats:image_frame",
        "//mediapipe/framework/formats:image_frame_opencv",
        "//mediapipe/framework/port:commandlineflags",
        "//mediapipe/framework/port:file_helpers",
        "//mediapipe/framework/port:opencv_highgui",
        "//mediapipe/framework/port:opencv_imgproc",
        "//mediapipe/framework/port:opencv_video",
        "//mediapipe/framework/port:parse_text_proto",
        "//mediapipe/framework/port:status",
    ],
)

mediapipe/examples/desktop/demo_run_graph_main.ccを見てみると、以下のことがわかります。

  • 引数を3つ受け取っている。それぞれ、Graph設定ファイルへのパス、入力動画ファイルへのパス、出力動画ファイルへのパスであり、実行コマンドで渡しているもの。(33~35、36~38、39~41行目)
  • main関数があり、ここからプログラムが開始されている。RunMPPGraph()を実行している。(141、144行目)
  • Graph設定ファイルを読み込み、パースしている(45~46、49~51行目)
  • 以下の処理を繰り返す
    • OpenCVでファイル(or カメラ)から画像(frame)をキャプチャする(87~94行目)
    • 画像データ(Mat型)をImageFrame型に変換する(97~101行目)
    • Graphに画像データを入力する(104~108行目)
      • 入力先のStream名はkInputStream(="input_video")(29行目)
    • Graphの処理結果の出力を受け取る(111~113行目)
      • 出力元のStream名はkOutputStream(="output_video")(30行目)
    • ImageFrameをMat型に変換する(116~117行目)
    • ファイル(or ウィンドウ)に出力する(118~132行目)

Graphの入出力としては、画像を連続して入力し、検出結果が描画された画像が連続して出力されていることがわかります。

mediapipe/examples/desktop/demo_run_graph_main.cc

constexpr char kInputStream[] = "input_video";
constexpr char kOutputStream[] = "output_video";
constexpr char kWindowName[] = "MediaPipe";

DEFINE_string(
    calculator_graph_config_file, "",
    "Name of file containing text format CalculatorGraphConfig proto.");
DEFINE_string(input_video_path, "",
              "Full path of video to load. "
              "If not provided, attempt to use a webcam.");
DEFINE_string(output_video_path, "",
              "Full path of where to save result (.mp4 only). "
              "If not provided, show result in a window.");

::mediapipe::Status RunMPPGraph() {
  std::string calculator_graph_config_contents;
  MP_RETURN_IF_ERROR(mediapipe::file::GetContents(
      FLAGS_calculator_graph_config_file, &calculator_graph_config_contents));
  LOG(INFO) << "Get calculator graph config contents: "
            << calculator_graph_config_contents;
  mediapipe::CalculatorGraphConfig config =
      mediapipe::ParseTextProtoOrDie<mediapipe::CalculatorGraphConfig>(
          calculator_graph_config_contents);

  LOG(INFO) << "Initialize the calculator graph.";
  mediapipe::CalculatorGraph graph;
  MP_RETURN_IF_ERROR(graph.Initialize(config));

  LOG(INFO) << "Initialize the camera or load the video.";
  cv::VideoCapture capture;
  const bool load_video = !FLAGS_input_video_path.empty();
  if (load_video) {
    capture.open(FLAGS_input_video_path);
  } else {
    capture.open(0);
  }
  RET_CHECK(capture.isOpened());

  cv::VideoWriter writer;
  const bool save_video = !FLAGS_output_video_path.empty();
  if (!save_video) {
    cv::namedWindow(kWindowName, /*flags=WINDOW_AUTOSIZE*/ 1);
#if (CV_MAJOR_VERSION >= 3) && (CV_MINOR_VERSION >= 2)
    capture.set(cv::CAP_PROP_FRAME_WIDTH, 640);
    capture.set(cv::CAP_PROP_FRAME_HEIGHT, 480);
    capture.set(cv::CAP_PROP_FPS, 30);
#endif
  }

  LOG(INFO) << "Start running the calculator graph.";
  ASSIGN_OR_RETURN(mediapipe::OutputStreamPoller poller,
                   graph.AddOutputStreamPoller(kOutputStream));
  MP_RETURN_IF_ERROR(graph.StartRun({}));

  LOG(INFO) << "Start grabbing and processing frames.";
  bool grab_frames = true;
  while (grab_frames) {
    // Capture opencv camera or video frame.
    cv::Mat camera_frame_raw;
    capture >> camera_frame_raw;
    if (camera_frame_raw.empty()) break;  // End of video.
    cv::Mat camera_frame;
    cv::cvtColor(camera_frame_raw, camera_frame, cv::COLOR_BGR2RGB);
    if (!load_video) {
      cv::flip(camera_frame, camera_frame, /*flipcode=HORIZONTAL*/ 1);
    }

    // Wrap Mat into an ImageFrame.
    auto input_frame = absl::make_unique<mediapipe::ImageFrame>(
        mediapipe::ImageFormat::SRGB, camera_frame.cols, camera_frame.rows,
        mediapipe::ImageFrame::kDefaultAlignmentBoundary);
    cv::Mat input_frame_mat = mediapipe::formats::MatView(input_frame.get());
    camera_frame.copyTo(input_frame_mat);

    // Send image packet into the graph.
    size_t frame_timestamp_us =
        (double)cv::getTickCount() / (double)cv::getTickFrequency() * 1e6;
    MP_RETURN_IF_ERROR(graph.AddPacketToInputStream(
        kInputStream, mediapipe::Adopt(input_frame.release())
                          .At(mediapipe::Timestamp(frame_timestamp_us))));

    // Get the graph result packet, or stop if that fails.
    mediapipe::Packet packet;
    if (!poller.Next(&packet)) break;
    auto& output_frame = packet.Get<mediapipe::ImageFrame>();

    // Convert back to opencv for display or saving.
    cv::Mat output_frame_mat = mediapipe::formats::MatView(&output_frame);
    cv::cvtColor(output_frame_mat, output_frame_mat, cv::COLOR_RGB2BGR);
    if (save_video) {
      if (!writer.isOpened()) {
        LOG(INFO) << "Prepare video writer.";
        writer.open(FLAGS_output_video_path,
                    mediapipe::fourcc('a', 'v', 'c', '1'),  // .mp4
                    capture.get(cv::CAP_PROP_FPS), output_frame_mat.size());
        RET_CHECK(writer.isOpened());
      }
      writer.write(output_frame_mat);
    } else {
      cv::imshow(kWindowName, output_frame_mat);
      // Press any key to exit.
      const int pressed_key = cv::waitKey(5);
      if (pressed_key >= 0 && pressed_key != 255) grab_frames = false;
    }
  }

  LOG(INFO) << "Shutting down.";
  if (writer.isOpened()) writer.release();
  MP_RETURN_IF_ERROR(graph.CloseInputStream(kInputStream));
  return graph.WaitUntilDone();
}

int main(int argc, char** argv) {
  google::InitGoogleLogging(argv[0]);
  gflags::ParseCommandLineFlags(&argc, &argv, true);
  ::mediapipe::Status run_status = RunMPPGraph();
  if (!run_status.ok()) {
    LOG(ERROR) << "Failed to run the graph: " << run_status.message();
    return EXIT_FAILURE;
  } else {
    LOG(INFO) << "Success!";
  }
  return EXIT_SUCCESS;
}

実行コマンドをみると、読み込んでいるGraphのファイルパスは、mediapipe/graphs/hand_tracking/multi_hand_tracking_desktop_live.pbtxtであることがわかります。

GLOG_logtostderr=1 bazel-bin/mediapipe/examples/desktop/multi_hand_tracking/multi_hand_tracking_cpu \
--calculator_graph_config_file=mediapipe/graphs/hand_tracking/multi_hand_tracking_desktop_live.pbtxt \
--input_video_path="/mnt/c/Users/yamamoto.hiroki/Desktop/in.mp4" \
--output_video_path="//mnt/c/Users/yamamoto.hiroki/Desktop/out.mp4"

まとめ

Multi Hand Trackingのプログラムの動作を見て、mediapipe/graphs/hand_tracking/multi_hand_tracking_desktop_live.pbtxtで定義されたGraphに対して、画像を入出力していることがわかりました。

次回は、Graphの内部をみていきます。

次↓

【MediaPipe】Multi Hand TrackingのGraphを解析してみた