【MediaPipe】Multi Hand Trackingで3つ以上の手を骨格検出する(解決編1)

2020.05.31

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

前回は、(複数人がいても使えるようにするために)3つ以上の手をトラッキングしようとして設定を変更しました。しかし、実行した結果、手自体の検出は3つ以上検出できるものの、手の骨格が2つまでしか検出せず3つ以上できないという問題がありました。

【MediaPipe】Multi Hand Trackingで3つ以上の手を骨格検出できなかった話

今回は、仕様を深く見直した結果を踏まえ、問題の原因を調べ、解決していきたいと思います。なお今回は解決に至るまでの1つ目の内容(途中でPacketが損失する問題)になり、2つ目の内容(並列処理で出力結果が上書きされる)は別記事としました。

結論として、設定を変更して実行したところ、”手そのものの検出”は3つ以上できましたが、手の骨格の検出は2つまでしかできませんでした。

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

【MediaPipe】投稿記事まとめ

問題箇所の特定

まずは、Graph中の各NodeにおけるProcess関数の呼ばれるタイミングと、受け取っているPacketのタイムスタンプを調べます。WSL上で動かしていることもあり、良いデバッグ方法が思いつかなかったため、今回はひたすらコンソール出力をしてみました。

利用するコードとしては、以下のようになります。Processメソッドの引数であるCalculatorContextのもつ、InputTimestampメソッドによってPacketのタイムスタンプを取得し、それをコンソールに出力することで、実行されたタイミング(順序)がわかります。(実際には、MediaPipeのプログラムはマルチスレッドで動作しているので、以下のコードだと、表示処理の順序が入れ替わり、表示が乱れることがあります)

::mediapipe::Status Process(CalculatorContext* cc) override {
    std::cout << "**" << " " << " Time:" << cc->InputTimestamp() << std::endl;
    ...
  }

(以下の結果は、4つ手が映っているフレームを処理するループの内容です)

Multi Hand Tracking Graph

Graphの様子は以下の図のとおりです。前回検出データを取り出した結果では、MultiHandDetectionSubgraphの出力は4つ分の結果が格納されていますが、MultiHandLandmarkSubgraphの出力は2つ分しかなかったので、MultiHandLandmarkSubgraphの内部が問題になっているようでした。

MultiHandLandmarkSubgraph

MultiHandLandmarkSubgraphの構造は以下のようです(詳しい動作についてはこちらを見てください)。

BeginLoopNormalizedRectCalculatorとEndLoopNormalizedLandmarksVectorCalculatorの、Processの呼び出しと出力をを見てみると、以下のようになっていました。

  • BeginLoopNormalizedRectCalculator
    • ITERABLEの入力Packetは1つ(=Process関数の呼び出しも1回)。タイムスタンプは、Graph全体を通して共通のもの(例:2092754982)。
    • ITEMの出力Packetは4つ(=Process関数の呼び出しも4回)。タイムスタンプは、このCalculator内で新たにつけ直された連番(例:4,5,6,7)。
  • EndLoopNormalizedLandmarksVectorCalculator
    • ITEMの入力Packetは2つ。タイムスタンプは、BeginLoopが出力したもののうち、最初と最後のみ(例:4,7)。

ここから、HandLandmarkSubgraphの中でいくつかのPacketが無視されていそうなことがわかりました。

HandLandmarkSubgraph

MultiHandLandmarkSubgraphの構造は以下のようです(詳しい動作についてはこちらを見てください)。

Processの呼び出しと出力をを見てみると、以下のようになっていました。

  • ImageCroppingCalculator、ImageTransformationCalculator、TfLiteConverterCalculator
    • 入力Packetは4つ(=Process関数の呼び出しも4回)。タイムスタンプは、BeginLoopNormalizedRectのものと同じ(例:4,5,6,7)。
    • 出力Packetは4つ。入力Packetに対応するタイムスタンプで出力されている(例:4,5,6,7)
  • TfLiteInferenceCalculator
    • 入力Packetは2つ(=Process関数の呼び出しも2回)。タイムスタンプは、BeginLoopが出力したもののうち、最初と最後のみ(例:4,7)。
    • 出力Packetは2つ。入力Packetに対応するタイムスタンプで出力されている(例:4,7)
  • Processメソッドの呼び出し順序(カッコ内は処理したPacketのタイムスタンプ)
    • ImageCropping *4 (4,5,6,7)
    • ImageTransformation *4(4,5,6,7)
    • TfLiteConverterCalculator * 1(4)
    • TfLiteInferenceCalculator * 1(4)
    • TfLiteConverterCalculator *3(5,6,7)
    • TfLiteInferenceCalculator * 1(7)
    • (何回か試してる中で若干前後する場合がありましたが、下4つの順序関係は変わりませんでした)

ここから、TfLiteInferenceCalculatorの入力段階でいくつかのPacketが無視されていそうなことがわかりました。

原因の特定 → 修正

以前MediaPipeの仕様を調べた結果、以下のことがわかっていました。

  • デフォルトのInput Policyを利用している限り、Packetは失われないことが保証される
  • 処理のリアルタイム性を上げるために、Input Policyを変更したり、フロー制御用のNodeを追加し、Packetの一部を捨てるように変更することができる。

これらのことから、どうやらInput Policyが影響していそうです。ということで、TfLiteInferenceCalculatorのInput Policyの定義箇所を見てみると、FixedInputStreamHandlerと書かれています(DefaultInputStreamHandlerではありませんでした)。

mediapipe/calculator/tflite/tflite_inference_calculator.cc

cc->SetInputStreamHandler("FixedSizeInputStreamHandler");

FixedSizeInputStreamHandlerの動作

FixedSizeInputStreamHandlerのccファイルを見てみると、以下のように書かれています。target_queue_sizeで指定された数のPacketのみを保持し、それよりも多くのPacketがキューされたら、古いものから捨てる、と書かれています。

mediapipe/framework/stream_handler/fixed_size_input_stream_handler.cc

// Input stream handler that limits each input queue to a maximum of
// target_queue_size packets, discarding older packets as needed.  When a
// timestamp is dropped from a stream, it is dropped from all others as well.

FixedSizeInputStreamHandlerのprotoファイルを見てみると、target_queue_sizeのデフォルト値が1になっています(target_queue_size = 2となっているのは、Protocol Bufferの仕様で、message内での変数のIDであり、値ではありません)。ccファイルの方でこの値は変更しないため、FixedSizeInputStreamHandlerは1つのPacketしか保持しないということになりそうです。

mediapipe/framework/stream_handler/fixed_size_input_stream_handler.proto

syntax = "proto2";

package mediapipe;

import "mediapipe/framework/mediapipe_options.proto";

// See FixedSizeInputStreamHandler for documentation.
message FixedSizeInputStreamHandlerOptions {
  extend MediaPipeOptions {
    optional FixedSizeInputStreamHandlerOptions ext = 125744319;
  }
  // The queue size at which input queues are truncated.
  optional int32 trigger_queue_size = 1 [default = 2];
  // The queue size to which input queues are truncated.
  optional int32 target_queue_size = 2 [default = 1];
  // If false, input queues are truncated to at most trigger_queue_size.
  // If true, input queues are truncated to at least trigger_queue_size.
  optional bool fixed_min_size = 3 [default = false];
}

問題の原因

上記を踏まえると、原因は以下のことが起きたためでした。

  • (TfLiteInferenceCalculatorが入力(Packet 4)処理を開始するが、処理に時間がかかる)
  • TfLiteInferenceCalculatorのStateがreadyになる前に、別のCalculatorのStateがReadyになり、並列して先に実行される(Packet 5,6,7)
  • TfLiteInferenceCalculatorの入力に前段の処理結果(Packet 5,6,7)がキューされるが、FixedSizeInputStreamHandlerによって古い結果(Packet 5,6)は捨てられ、最新の1つのみ(Packet 7)が保持される
  • (TfLiteInferenceCalculatorの処理が終わる)
  • TfLiteInferenceCalculatorのStateがreadyになり、FixedSizeInputStreamHandlerが入力を実行する。しかし、保持されているのは最新の結果(Packet 7)のみになっている

処理のリアルタイム性を上げるために、こうした処理が挟まれているようです。これが"バグ"であるかどうかは微妙なところだと思います。TfLiteInferenceCalculator自体は、リアルタイム性を上げるためにこうした処理を挟むのは、ありうることだと思います。特に、今回CPUで動かしたのですが、もともとはGPUで動かす想定をされていたために、処理結果に違いがでたのかもしれません。

むしろ、MultiHandLandmarkSubgraphのBeginLoop-Endloop側がかなりトリッキーな構成をしているのが原因とも捉えることもできます。HandLandmarkSubgraph内を修正しないとした場合の解決方法は、Endloop→BeginLoopにループフィードバックをかけ、HandLandmarkSubgraph複数の入力が同時に行かないようにすることが考えられます。(その場合、パイプライン化による並列化ができず、すこしスループットが下がるかと思われます)

修正方法

今回は、簡単に修正するために、以下のように、FixedSizeInputStreamHandlerからDefaultInputStreamHandlerに変更しました。これによって、Packetをロスせずに処理してくれることが期待できます。

mediapipe/calculator/tflite/tflite_inference_calculator.cc

cc->SetInputStreamHandler("DefaultInputStreamHandler");

修正した結果:動作がおかしい

修正したプログラムで、手を検出させた結果が以下の動画です。4つの手に対してLandmarkが描画されており、一見目的どおり正しく動作しているように見えます。Inferenceにも、Packtが4つ(4,5,6,7)入力されており、Packetのロスはなくなりました。

しかし、動画を止めてみると、2つの手に関してはうまくLandmarkが検出できているものの、残りの2つに関しては少しずれています。よくよく見てみると、ずれている2つに関しては、検出できた2つのうちの片方と同じのLandmarkが描画(下の図でいうと、左から3番目の手のLandmarkが、1番目のと4番目に)されています。

どうやらまだ修正する必要がありそうです。

まとめ

今回は、2つしかLandmarkが検出されないという問題に対して、InputStreamHandler(Input Policy)を変更することで対処しました。これにより、Packetのロスはなくなったものの、描画結果がおかしいという問題がおきました。

次回は、この問題を解決していきます。

次↓

【MediaPipe】Multi Hand Trackingで3つ以上の手を骨格検出する(解決編2)