【MediaPipe】Multi Hand Trackingから検出データを抽出した

2020.05.27

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

※ 5/28:必要手順に抜けがありましたので、追記いたしました。 ※ 5/29:本記事の内容はMediaPipeのバージョンはv0.7.4で実行した内容です。5/22にバージョンがv0.7.5に変更になったため、以下の方法では動かない可能性がありますので、ご注意ください。

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

前々回前回で、Multi Hand Trackingのプログラムとグラフを解析しました。

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

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

今回は、Multi Hand TrackingのGraphを変更し、検出結果のデータを抽出します。Graphにデータ出力用のストリームを追加し、受け取る側のプログラムを変更します。また、抽出したデータはProtocol Buffer形式で少し扱いにくいため、C++からシリアライズ化したデータを出力し、PythonでJSON形式に変換します。

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

【MediaPipe】投稿記事まとめ

Graphに出力ストリームを追加する

Graphからデータを取り出すため、ほしいデータが通っているストリームを、出力ストリームに接続します。今回、取り出す対象のストリームは、結果を描画するMultiHandRendererに入力されているストリーム4つと、MultiHandLandmarkから出力されているストリーム1つです。

今回、出力ストリームに検出結果のストリームを直接接続する方法がわからなかったため、(HelloWorldで利用されていた)PassThroughCalculatorを介して出力するようにしました。各Calculatorで、対象の検出結果ストリームをInputとして指定し、追加した出力ストリームをOutputとして指定します。(もしかしたら、別の方法があるかもしれません)

GraphからPassThroughCalculatorを参照できるようにする

mediapipe/graphs/hand_tracking/hand_tracking_desktop_live.pbtxtから参照できるように、BUILDファイルににPassThroughCalculatorへのパスを追加します。

mediapipe/graphs/hand_tracking/BUILD

cc_library(
    name = "multi_hand_desktop_tflite_calculators",
    deps = [
        ":desktop_offline_calculators",
        "//mediapipe/calculators/util:association_norm_rect_calculator",
        "//mediapipe/calculators/util:collection_has_min_size_calculator",
        "//mediapipe/graphs/hand_tracking/subgraphs:multi_hand_detection_cpu",
        "//mediapipe/graphs/hand_tracking/subgraphs:multi_hand_landmark_cpu",
        "//mediapipe/graphs/hand_tracking/subgraphs:multi_hand_renderer_cpu",
        "//mediapipe/calculators/core:pass_through_calculator",
    ],
)

Graphに出力ストリームを接続する

mediapipe/graphs/hand_tracking/hand_tracking_desktop_live.pbtxtを以下のように変更します。

  • それぞれの検出結果を取り出すための出力ストリームを追加する(9~13行目)
  • 出力ストリームに検出結果のストリームを接続する(110~140行目)。

mediapipe/graphs/hand_tracking/hand_tracking_desktop_live.pbtxt

# MediaPipe graph that performs multi-hand tracking on desktop with TensorFlow
# Lite on CPU.
# Used in the example in
# mediapipie/examples/desktop/hand_tracking:multi_hand_tracking_cpu.

# Images coming into and out of the graph.
input_stream: "input_video"
output_stream: "output_video"
output_stream: "output_detections"
output_stream: "output_landmarks"
output_stream: "output_palm_rects"
output_stream: "output_hand_rects"
output_stream: "output_hand_rects_from_landmarks"

# Determines if an input vector of NormalizedRect has a size greater than or
# equal to the provided min_size.
node {
  calculator: "NormalizedRectVectorHasMinSizeCalculator"
  input_stream: "ITERABLE:prev_multi_hand_rects_from_landmarks"
  output_stream: "prev_has_enough_hands"
  node_options: {
    [type.googleapis.com/mediapipe.CollectionHasMinSizeCalculatorOptions] {
      # This value can be changed to support tracking arbitrary number of hands.
      # Please also remember to modify max_vec_size in
      # ClipVectorSizeCalculatorOptions in
      # mediapipe/graphs/hand_tracking/subgraphs/multi_hand_detection_gpu.pbtxt
      min_size: 8
    }
  }
}

# Drops the incoming image if the previous frame had at least N hands.
# Otherwise, passes the incoming image through to trigger a new round of hand
# detection in MultiHandDetectionSubgraph.
node {
  calculator: "GateCalculator"
  input_stream: "input_video"
  input_stream: "DISALLOW:prev_has_enough_hands"
  output_stream: "multi_hand_detection_input_video"
  node_options: {
    [type.googleapis.com/mediapipe.GateCalculatorOptions] {
      empty_packets_as_allow: true
    }
  }
}

# Subgraph that detections hands (see multi_hand_detection_cpu.pbtxt).
node {
  calculator: "MultiHandDetectionSubgraph"
  input_stream: "multi_hand_detection_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: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: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
    }
  }
}

# Subgraph that renders annotations and overlays them on top of the input
# images (see multi_hand_renderer_cpu.pbtxt).
node {
  calculator: "MultiHandRendererSubgraph"
  input_stream: "IMAGE:input_video"
  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"
}

# Streams to get results of detection and landmark
node {
  calculator: "PassThroughCalculator"
  input_stream: "multi_palm_detections"
  output_stream: "output_detections"
}

node {
  calculator: "PassThroughCalculator"
  input_stream: "multi_hand_landmarks"
  output_stream: "output_landmarks"
}


node {
  calculator: "PassThroughCalculator"
  input_stream: "multi_palm_rects"
  output_stream: "output_palm_rects"
}

node {
  calculator: "PassThroughCalculator"
  input_stream: "multi_hand_rects"
  output_stream: "output_hand_rects"
}

node {
  calculator: "PassThroughCalculator"
  input_stream: "multi_hand_rects_from_landmarks"
  output_stream: "output_hand_rects_from_landmarks"
}

可視化すると、次のようになります。図下側の5つの PassThrough_* と output_* が今回追加した箇所です。(図だけだとわかりにくいと思いますので、Visualizer に上のコードを貼り付けて、マウスオーバなどしながら確認するのがオススメです)

変更前

変更後

プログラムを変更する

Graphを変更したことによって、データを取り出す準備ができました。次にプログラム側を変更します。取り出す検出データの型情報が必要であるため、対応するヘッダファイルを利用します。今回取り出すデータは、Protocol Bufferで定義されているもの(.protoファイル)を、bazelでビルドしてヘッダファイルとします。詳しくはこちらなどをご参照ください。

プログラムからヘッダファイルを参照できるようにする

mediapipe/examples/desktop/demo_run_graph_main.ccから参照するため、同じフォルダのBUILDファイルに以下のパスを追加します。

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/framework/formats:detection_cc_proto",
        "//mediapipe/framework/formats:landmark_cc_proto",
        "//mediapipe/framework/formats:rect_cc_proto",
    ],
)

データを取り出して出力する

  • 取り出すデータのクラスを定義したヘッダファイルをインクルードする(今回取り出す結果は、Protocol Bufferで定義されているため、bazelがビルドして生成するヘッダファイル(*.pb.h)をインクルードする必要があります)(34~36行目)
  • 出力結果を描画した画像を取り出す処理と同様に、出力ストリームに対するポーラをGraphに接続し(99~108行目)、Packetを通して結果を得る(160~179行目)。
  • (今回はファイル出力するために)SerializeToString()でバイナリデータをstring形式で取得し、ファイル出力する(182~272行目)

 

今回のプログラムでは、以下のようなファイルが出力されます。出力されるファイル数は検出された結果によって異なります。

  • iLoop=0_detection_j=0.txt
  • iLoop=0_detection_j=1.txt
  • iLoop=0_handRect_j=0.txt
  • iLoop=0_handRect_j=1.txt
  • iLoop=0_handRectFromLandmarks_j=0.txt
  • iLoop=0_handRectFromLandmarks_j=1.txt
  • iLoop=0_inputFrame.jpg
  • iLoop=0_landmark_j=0.txt
  • iLoop=0_landmark_j=1.txt
  • iLoop=0_outputFrame.jpg
  • iLoop=0_palmRect_j=0.txt
  • iLoop=1_detection_j=0.txt
  • ...

検出データを変換する

出力したデータを利用しやすくするために、Pythonで読み込みJSON形式に変換します。まず、Protocol Buffer形式で定義されたクラスを、Pythonで使えるようにするため、Python用にコンパイルします。その後、

Protobuf定義ファイルをPython用にコンパイルする

protocをインストールするため、以下のコマンドを実行します。コードはこちらを参考にしました。

sudo apt-get install autoconf automake libtool curl make g++ unzip -y
git clone https://github.com/google/protobuf.git
cd protobuf
git submodule update --init --recursive
./autogen.sh
./configure
make
make check
sudo make install
sudo ldconfig

インストールできてるか確認します。

protoc --version

今回利用するProtobufファイルをコンパイルします。コマンドはこちらを参考にしました。

protoc --python_out=./ ./mediapipe/framework/formats/*.proto
protoc --python_out=./ ./mediapipe/framework/formats/annotation/*.proto

./temp/mediapipe/framework/formats内に、Python用にコンパイルされたファイル(*_pb2.py)が生成されます。

JSON形式に変換する

protobufのライブラリが必要であるため、以下のコマンドをWSLで実行します。

pip3 install protobuf

今回使用したコードは以下のようです(雑なプログラムですが、、、)。

  • 先程コンパイルしたライブラリをインポートする(1~3行目)
  • それぞれの出力の種類ごとに、ファイル名のリストを取得する(今回の主眼ではありません)(20~43行目)
  • 各データ形式に対応するクラスでパースし(ParseFromString)、JSON形式に変換し(MessageToJson)、ファイル出力する(46~107行目)

converter.py

from mediapipe.framework.formats.detection_pb2 import Detection
from mediapipe.framework.formats.landmark_pb2 import LandmarkList
from mediapipe.framework.formats.rect_pb2 import NormalizedRect
from google.protobuf.json_format import MessageToJson

import glob
import re
from pprint import pprint
import json
import sys

if len(sys.argv) > 1:
    targetDir = sys.argv[1]
else:
    targetDir = "./result/iPhoneXR_overshelf_scene1_short_hands8.mp4/"

outputFiles = glob.glob(targetDir + "/" + "*.txt")


detectionFiles = [(re.findall(
    r"iLoop=(\d+)_detection_j=(\d+).txt", outputFile), outputFile) for outputFile in outputFiles]
detectionFilesFiltered = [
    (detectionFile[0], detectionFile[1].replace("\\", "/")) for detectionFile in detectionFiles if detectionFile[0]]

landmarkFiles = [(re.findall(
    r"iLoop=(\d+)_landmark_j=(\d+).txt", outputFile), outputFile) for outputFile in outputFiles]
landmarkFilesFiltered = [
    (landmarkFile[0], landmarkFile[1].replace("\\", "/")) for landmarkFile in landmarkFiles if landmarkFile[0]]

handRectFiles = [(re.findall(
    r"iLoop=(\d+)_handRect_j=(\d+).txt", outputFile), outputFile) for outputFile in outputFiles]
handRectFilesFiltered = [
    (handRectFile[0], handRectFile[1].replace("\\", "/")) for handRectFile in handRectFiles if handRectFile[0]]

palmRectFiles = [(re.findall(
    r"iLoop=(\d+)_palmRect_j=(\d+).txt", outputFile), outputFile) for outputFile in outputFiles]
palmRectFilesFiltered = [
    (palmRectFile[0], palmRectFile[1].replace("\\", "/")) for palmRectFile in palmRectFiles if palmRectFile[0]]

handRectFromLandmarksFiles = [(re.findall(
    r"iLoop=(\d+)_handRectFromLandmarks_j=(\d+).txt", outputFile), outputFile) for outputFile in outputFiles]
handRectFromLandmarksFilesFiltered = [
    (handRectFromLandmarksFile[0], handRectFromLandmarksFile[1].replace("\\", "/")) for handRectFromLandmarksFile in handRectFromLandmarksFiles if handRectFromLandmarksFile[0]]


for detectionFile in detectionFilesFiltered:
    with open(detectionFile[1], "rb") as f:
        content = f.read()

    detection = Detection()
    detection.ParseFromString(content)
    jsonObj = MessageToJson(detection)

    detectionFileOutput = detectionFile[1].replace("txt", "json")
    with open(detectionFileOutput, "w") as f:
        f.write(jsonObj) 


for landmarkFile in landmarkFilesFiltered:
    with open(landmarkFile[1], "rb") as f:
        content = f.read()

    landmark = LandmarkList()
    landmark.ParseFromString(content)
    jsonObj = MessageToJson(landmark)

    landmarkFileOutput = landmarkFile[1].replace("txt", "json")
    with open(landmarkFileOutput, "w") as f:
        f.write(jsonObj) 


for handRectFile in handRectFilesFiltered:
    with open(handRectFile[1], "rb") as f:
        content = f.read()

    handRect = NormalizedRect()
    handRect.ParseFromString(content)
    jsonObj = MessageToJson(handRect)

    handRectFileOutput = handRectFile[1].replace("txt", "json")
    with open(handRectFileOutput, "w") as f:
        f.write(jsonObj) 


for palmRectFile in palmRectFilesFiltered:
    with open(palmRectFile[1], "rb") as f:
        content = f.read()

    palmRect = NormalizedRect()
    palmRect.ParseFromString(content)
    jsonObj = MessageToJson(palmRect)

    palmRectFileOutput = palmRectFile[1].replace("txt", "json")
    with open(palmRectFileOutput, "w") as f:
        f.write(jsonObj) 

for handRectFromLandmarksFile in handRectFromLandmarksFilesFiltered:
    with open(handRectFromLandmarksFile[1], "rb") as f:
        content = f.read()

    handRectFromLandmarks = NormalizedRect()
    handRectFromLandmarks.ParseFromString(content)
    jsonObj = MessageToJson(handRectFromLandmarks)

    handRectFromLandmarksFileOutput = handRectFromLandmarksFile[1].replace("txt", "json")
    with open(handRectFromLandmarksFileOutput, "w") as f:
        f.write(jsonObj)

Pythonで実行します。

python3 converter.py

結果

得られるデータは以下のようなものになります。

detection(Detection型)

検出した物体(=手、正確には手のひら/手の甲)のラベルやスコア、バウンディングボックス、手のひら内のキーポイントがわかります。

{
  "label": [
    "Palm"
  ],
  "score": [
    0.9865578
  ],
  "locationData": {
    "format": "RELATIVE_BOUNDING_BOX",
    "relativeBoundingBox": {
      "xmin": 0.39248314,
      "ymin": 0.26998103,
      "width": 0.048939228,
      "height": 0.08700302
    },
    "relativeKeypoints": [
      {
        "x": 0.40684253,
        "y": 0.2803954
      },
      {
        "x": 0.4169947,
        "y": 0.33548334
      },
      {
        "x": 0.4065402,
        "y": 0.34309858
      },
      {
        "x": 0.40017793,
        "y": 0.34611765
      },
      {
        "x": 0.3962021,
        "y": 0.34618986
      },
      {
        "x": 0.425763,
        "y": 0.27499962
      },
      {
        "x": 0.43927395,
        "y": 0.30067408
      }
    ]
  }
}

palmRect(NormalizedRect型)

palmRectという名前ですが、囲っているのは手全体になっています。

{
  "xCenter": 0.416743,
  "yCenter": 0.35698244,
  "height": 0.22620799,
  "width": 0.12724198,
  "rotation": -3.1330206
}

handRect(NormalizedRect型)

{
  "xCenter": 0.7210063,
  "yCenter": 0.1579529,
  "height": 0.1772464,
  "width": 0.0997011,
  "rotation": -2.8904288
}

landmark(Landmark型)

21点あります。手首で1点、各指で4点ずつあります。こちらのブログで画像を作成されている方がいらっしゃいます。

{
  "landmark": [
    {
      "x": 0.40840688,
      "y": 0.28062445,
      "z": 0.0
    },
    {
      "x": 0.42267725,
      "y": 0.276408,
      "z": 0.0
    },
    {
      "x": 0.43161613,
      "y": 0.29168004,
      "z": 0.0
    },
    {
      "x": 0.4361234,
      "y": 0.3206246,
      "z": 0.0
    },
    {
      "x": 0.44291618,
      "y": 0.35354337,
      "z": 0.0
    },
    {
      "x": 0.4092951,
      "y": 0.32624507,
      "z": 0.0
    },
    {
      "x": 0.4172063,
      "y": 0.3700544,
      "z": 0.0
    },
    {
      "x": 0.4325033,
      "y": 0.39400557,
      "z": 0.0
    },
    {
      "x": 0.44500858,
      "y": 0.4125882,
      "z": 0.0
    },
    {
      "x": 0.4057869,
      "y": 0.3467034,
      "z": 0.0
    },
    {
      "x": 0.42252448,
      "y": 0.38882673,
      "z": 0.0
    },
    {
      "x": 0.43710503,
      "y": 0.38357568,
      "z": 0.0
    },
    {
      "x": 0.4447874,
      "y": 0.37505886,
      "z": 0.0
    },
    {
      "x": 0.40684992,
      "y": 0.359745,
      "z": 0.0
    },
    {
      "x": 0.42386237,
      "y": 0.3922723,
      "z": 0.0
    },
    {
      "x": 0.43604085,
      "y": 0.38865924,
      "z": 0.0
    },
    {
      "x": 0.4427886,
      "y": 0.38379118,
      "z": 0.0
    },
    {
      "x": 0.41125774,
      "y": 0.36923084,
      "z": 0.0
    },
    {
      "x": 0.4223674,
      "y": 0.38892198,
      "z": 0.0
    },
    {
      "x": 0.43208086,
      "y": 0.38867816,
      "z": 0.0
    },
    {
      "x": 0.43905717,
      "y": 0.3841055,
      "z": 0.0
    }
  ]
}

handRectFromLandmark(NormalizedRect型)

{
  "xCenter": 0.42539775,
  "yCenter": 0.3444981,
  "height": 0.21788836,
  "width": 0.12256221,
  "rotation": -3.071221
}

inputFrame, outputFrame(jpg形式ファイル)

注:Pythonで変換したものではなく、C++のプログラムで得られるものです。

まとめ

今回は、Graphとプログラムを変更し、Graph内の検出データを抽出し、ファイル出力したデータをPythonでJSON形式に変換しました。これによって、検出データを簡単に扱えるようになりました。

次回は、複数人に対応できるよう、3つ以上の手を検出してみたいと思います。

次↓

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

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

Protocol Buffer Basics: C++ https://developers.google.com/protocol-buffers/docs/cpptutorial

How to Install Latest Protobuf on Ubuntu 18.04 https://gist.github.com/diegopacheco/cd795d36e6ebcd2537cd18174865887b#file-latest-protobuf-ubuntu-18-04-md

PythonでProtocol Buffersの文字列と数値の単純なメッセージを操作する https://nansystem.com/protocol-buffers-in-python/

MediaPipe の Hand Tracking を mac で動かす https://qiita.com/mml/items/9128de32103f88b9927c

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