【MediaPipe】Multi Hand Trackingで3つ以上の手を骨格検出する(解決編1)
カフェチームの山本です。
前回は、(複数人がいても使えるようにするために)3つ以上の手をトラッキングしようとして設定を変更しました。しかし、実行した結果、手自体の検出は3つ以上検出できるものの、手の骨格が2つまでしか検出せず3つ以上できないという問題がありました。
今回は、仕様を深く見直した結果を踏まえ、問題の原因を調べ、解決していきたいと思います。なお今回は解決に至るまでの1つ目の内容(途中でPacketが損失する問題)になり、2つ目の内容(並列処理で出力結果が上書きされる)は別記事としました。
結論として、設定を変更して実行したところ、”手そのものの検出”は3つ以上できましたが、手の骨格の検出は2つまでしかできませんでした。
(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ではありませんでした)。
cc->SetInputStreamHandler("FixedSizeInputStreamHandler");
FixedSizeInputStreamHandlerの動作
FixedSizeInputStreamHandlerのccファイルを見てみると、以下のように書かれています。target_queue_sizeで指定された数のPacketのみを保持し、それよりも多くのPacketがキューされたら、古いものから捨てる、と書かれています。
// 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しか保持しないということになりそうです。
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をロスせずに処理してくれることが期待できます。
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のロスはなくなったものの、描画結果がおかしいという問題がおきました。
次回は、この問題を解決していきます。
次↓