Mediapipeで顔と手の同時トラッキング


今回作ったもの

顔と手を検出するグラフを作成する

複数の手を検出するサンプルと、顔のキーポイントを検出するサンプルの2つは用意されています。本記事では顔と手を同時に検出するグラフを作成しました。グラフは次の通りです。(ソースコードは記事最後)

Streamに関してですが、
SidePacket(定数定義相当)右から出力して、左から入力。Pcketはノードの上から入力して、下から出力されます。オレンジ色で塗りつぶされているノードはグラフの入出力です。

SobleEdgesはSobelフィルタで画像のエッジを検出するフィルタです。
ある程度元データを伝えつつ、プライバシーに考慮した出力動画を作るためにつけています。今回の処理の本質ではありません。普通の画像を表示したい場合はこのノードを外してください。

描画処理はFaceRendererGpu、MultiHandRendererと直列の多段にしてあり、リアルタイム性を少しでも高くするために、手の検出とキーポイント推定、顔の検出とキーポイント推定は並列にしてあります。

FlowLimitterは処理を待つノードで、この場合、あるフレームの手のランドマークが検出されるまではビデオ入力を捨て続けるというノードで処理速度を保つために主にMobileように用意されています。PCでは合ってもなくてもあまり速度は変わりません。

AssociationNormRectは前フレームで検知された手と限フレームで検知された手の関連付けをしてくれるもので、手のBoundingBoxのIoUがある値(今回は0.5)以上の場合は前フレームと同じ手、低い場合は新しい手が検出されたとみなすようになっています。これにより、手の簡単なトラッキングを実現しています。(ただ、手のID付与がなされていなかったり、改良の価値はあります。)

可視化ツールについて

先程出したグラフの描画はMediapipeの可視化ツールを使って描画しています。ドキュメントはこちらです。

複数のグラフを表示する機能もあり、サブグラフと一緒に追加すると、サブグラフの表示もできたりします。

ソースコード

# MediaPipe graph that performs multi-hand tracking with TensorFlow Lite on GPU.
# Used in the examples in
# mediapipe/examples/android/src/java/com/mediapipe/apps/multihandtrackinggpu.

# Images coming into and out of the graph.
input_stream: "input_video"
output_stream: "output_video"

# Collection of detected/processed faces, each represented as a list of
# landmarks. (std::vector<NormalizedLandmarkList>)
output_stream: "multi_face_landmarks"


# Throttles the images flowing downstream for flow control. It passes through
# the very first incoming image unaltered, and waits for downstream nodes
# (calculators and subgraphs) in the graph to finish their tasks before it
# passes through another image. All images that come in while waiting are
# dropped, limiting the number of in-flight images in most part of the graph to
# 1. This prevents the downstream nodes from queuing up incoming images and data
# excessively, which leads to increased latency and memory usage, unwanted in
# real-time mobile applications. It also eliminates unnecessarily computation,
# e.g., the output produced by a node may get dropped downstream if the
# subsequent nodes are still busy processing previous inputs.
node {
  calculator: "FlowLimiterCalculator"
  input_stream: "input_video"
  input_stream: "FINISHED:multi_hand_rects"
  input_stream_info: {
    tag_index: "FINISHED"
    back_edge: true
  }
  output_stream: "throttled_input_video"
}

# Subgraph that detections hands (see multi_hand_detection_gpu.pbtxt).
node {
  calculator: "MultiHandDetectionSubgraph"
  input_stream: "throttled_input_video"
  output_stream: "DETECTIONS:multi_palm_detections"
  output_stream: "NORM_RECTS:multi_palm_rects"
}

# Subgraph that localizes hand landmarks for multiple hands (see
# multi_hand_landmark.pbtxt).
node {
  calculator: "MultiHandLandmarkSubgraph"
  input_stream: "IMAGE:throttled_input_video"
  input_stream: "NORM_RECTS:multi_hand_rects"
  output_stream: "LANDMARKS:multi_hand_landmarks"
  output_stream: "NORM_RECTS:multi_hand_rects_from_landmarks"
}

# Caches a hand rectangle fed back from MultiHandLandmarkSubgraph, and upon the
# arrival of the next input image sends out the cached rectangle with the
# timestamp replaced by that of the input image, essentially generating a packet
# that carries the previous hand rectangle. Note that upon the arrival of the
# very first input image, an empty packet is sent out to jump start the
# feedback loop.
node {
  calculator: "PreviousLoopbackCalculator"
  input_stream: "MAIN:throttled_input_video"
  input_stream: "LOOP:multi_hand_rects_from_landmarks"
  input_stream_info: {
    tag_index: "LOOP"
    back_edge: true
  }
  output_stream: "PREV_LOOP:prev_multi_hand_rects_from_landmarks"
}

# Performs association between NormalizedRect vector elements from previous
# frame and those from the current frame if MultiHandDetectionSubgraph runs.
# This calculator ensures that the output multi_hand_rects vector doesn't
# contain overlapping regions based on the specified min_similarity_threshold.
node {
  calculator: "AssociationNormRectCalculator"
  input_stream: "prev_multi_hand_rects_from_landmarks"
  input_stream: "multi_palm_rects"
  output_stream: "multi_hand_rects"
  node_options: {
    [type.googleapis.com/mediapipe.AssociationCalculatorOptions] {
      min_similarity_threshold: 0.5
    }
  }
}

node {
  calculator: "ZmqCalculator"
  input_stream: "FACE_LANDMARKS:multi_face_landmarks"
  input_stream: "LANDMARKS:multi_hand_landmarks"
  input_stream: "NORM_RECTS:multi_hand_rects"
}

# Defines side packets for further use in the graph.
node {
  calculator: "ConstantSidePacketCalculator"
  output_side_packet: "PACKET:num_faces"
  node_options: {
    [type.googleapis.com/mediapipe.ConstantSidePacketCalculatorOptions]: {
      packet { int_value: 1 }
    }
  }
}

# Subgraph that detects faces and corresponding landmarks.
node {
  calculator: "FaceLandmarkFrontGpu"
  input_stream: "IMAGE:throttled_input_video"
  input_side_packet: "NUM_FACES:num_faces"
  output_stream: "LANDMARKS:multi_face_landmarks"
  output_stream: "ROIS_FROM_LANDMARKS:face_rects_from_landmarks"
  output_stream: "DETECTIONS:face_detections"
  output_stream: "ROIS_FROM_DETECTIONS:face_rects_from_detections"
}

node: {
  calculator: "SobelEdgesCalculator"
  input_stream: "input_video"
  output_stream: "fix_video"
}

# Subgraph that renders face-landmark annotation onto the input image.
node {
   calculator: "FaceRendererGpu"
   input_stream: "IMAGE:fix_video"
   input_stream: "LANDMARKS:multi_face_landmarks"
   input_stream: "NORM_RECTS:face_rects_from_landmarks"
   input_stream: "DETECTIONS:face_detections"
   output_stream: "IMAGE:output_video_1"
}

# Subgraph that renders annotations and overlays them on top of the input
# images (see multi_hand_renderer_gpu.pbtxt).
node {
  calculator: "MultiHandRendererSubgraph"
  input_stream: "IMAGE:output_video_1"
  input_stream: "DETECTIONS:multi_palm_detections"
  input_stream: "LANDMARKS:multi_hand_landmarks"
  input_stream: "NORM_RECTS:0:multi_palm_rects"
  input_stream: "NORM_RECTS:1:multi_hand_rects"
  output_stream: "IMAGE:output_video"
}