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

2020.05.31

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

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

前回は、3つ以上の手のLandmarkを検出できないという問題に対して、プログラムを動作させて原因を調べ、原因を修正しました。4つのLandmarkが出力され、一見正しく動作したかのように見えたのですが、正しく検出できているのは2つまでで、もう2つのLandmarkは、正しく検出できているうちの片方と同じになってしまう、という問題が起きることを新たに確認しました。

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

今回は、この問題を解決するために、原因を調べ、解決方法を考えます。

注:今回の解決方法は一時しのぎの面があります。簡単に動かすのには利用できますが、実利用などにはあまりオススメできません。ご了承ください。

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

【MediaPipe】投稿記事まとめ

原因箇所を特定する

前回、TfLiteInferenceCalculatorのInputStreamHandlerをDefaultInputStreamHandlerに変更しました。変更後の各CalculatorのProcessメソッドの呼び出し順序は以下のようになりました。

  • ImageCropping *4 (4,5,6,7)
  • ImageTransformation *4(4,5,6,7)
  • TfLiteConverterCalculator * 1(4)
  • TfLiteInferenceCalculator * 1(4)
  • TfLiteConverterCalculator *3(5,6,7)
  • TfLiteInferenceCalculator * 3(5,6,7)

また、推論結果をLandmarkの数値に変換している、TfLiteTensorsToLandmarksCalculatorの入力・出力を見ると、Packet4とPacket5の出力結果は異なりますが、Packet6,7はPacket5の出力結果は同じでした。

MediaPipeが各タスクを並列処理をしていることと合わせて考えると、ConverterからInferenceに参照渡しをしており、Converterが先に3回実行されることで、出力結果を上書きしていることが原因と思われます。

原因を特定する

実際にコードを見てみると、以下のようになっていました。コードの下から見ていきます。

  • 最終的には、output_tensors を出力している(375~377行目)
  • output_tensors は tensor へのポインタを保持している(373~374行目)
  • tensor は tensor_buffer をもち、そこに画像データを変換結果の画像をコピーされている(360~370行目)(use_quantized_tensors_はfalseのため、342~358行目は実行されません)
  • tensor は、TfLiteConverterCalculatorのメンバである interpreter_ から取得されている。これは使いまわされている(336行目)

mediapipe/calculators/tflite/tflite_converter_calculator.cc

    const int tensor_idx = interpreter_->inputs()[0];
    TfLiteTensor* tensor = interpreter_->tensor(tensor_idx);
    interpreter_->ResizeInputTensor(tensor_idx,
                                    {height, width, channels_preserved});
    interpreter_->AllocateTensors();

    // Copy image data into tensor.
    if (use_quantized_tensors_) {
      const int width_padding =
          image_frame.WidthStep() / image_frame.ByteDepth() - width * channels;
      const uint8* image_buffer =
          reinterpret_cast<const uint8*>(image_frame.PixelData());
      uint8* tensor_buffer = tensor->data.uint8;
      RET_CHECK(tensor_buffer);
      for (int row = 0; row < height; ++row) {
        for (int col = 0; col < width; ++col) {
          for (int channel = 0; channel < channels_preserved; ++channel) {
            *tensor_buffer++ = image_buffer[channel];
          }
          image_buffer += channels;
        }
        image_buffer += width_padding;
      }
    } else {
      float* tensor_buffer = tensor->data.f;
      RET_CHECK(tensor_buffer);
      if (image_frame.ByteDepth() == 1) {
        MP_RETURN_IF_ERROR(NormalizeImage<uint8>(
            image_frame, zero_center_, flip_vertically_, tensor_buffer));
      } else if (image_frame.ByteDepth() == 4) {
        MP_RETURN_IF_ERROR(NormalizeImage<float>(
            image_frame, zero_center_, flip_vertically_, tensor_buffer));
      } else {
        return ::mediapipe::InternalError(
            "Only byte-based (8 bit) and float (32 bit) images supported.");
      }
    }

    auto output_tensors = absl::make_unique<std::vector<TfLiteTensor>>();
    output_tensors->emplace_back(*tensor);
    cc->Outputs()
        .Tag(kTensorsTag)
        .Add(output_tensors.release(), cc->InputTimestamp());

結果として、Converterから出力を受け取ったInferenceは、常にinterpreter_ から取得された、同じtensorを参照しているため、同じアドレスを参照していることになります。Inferenceでは下のコードのように入力を受け取った後、参照先の画像データをコピーしてはいるのですが、先にConverterが実行され同じアドレスのデータを上書きするため、2回目以降は、Converterが4回処理した後の結果を毎回コピーすることになります。これにより、描画された4つのLandmarkのうち、3つのが同じになっていました。

mediapipe/calculators/tflite/tflite_inference_calculator.cc

    // Read CPU input into tensors.
    const auto& input_tensors =
        cc->Inputs().Tag(kTensorsTag).Get<std::vector<TfLiteTensor>>();
    RET_CHECK_GT(input_tensors.size(), 0);
    for (int i = 0; i < input_tensors.size(); ++i) {
      const TfLiteTensor* input_tensor = &input_tensors[i];
      RET_CHECK(input_tensor->data.raw);
      if (use_quantized_tensors_) {
        const uint8* input_tensor_buffer = input_tensor->data.uint8;
        uint8* local_tensor_buffer = interpreter_->typed_input_tensor<uint8>(i);
        std::memcpy(local_tensor_buffer, input_tensor_buffer,
                    input_tensor->bytes);
      } else {
        const float* input_tensor_buffer = input_tensor->data.f;
        float* local_tensor_buffer = interpreter_->typed_input_tensor<float>(i);
        std::memcpy(local_tensor_buffer, input_tensor_buffer,
                    input_tensor->bytes);
      }
    }

コードを修正する

同じアドレスを利用していたことが原因でした。これに対する解決方法としては、Converter側で別のメモリアドレスを確保し、そこにコピーしてから渡せば良いことになります。今回は、以下のコードのように、mallocとmemcpyで割り当てし直すように修正しました。

(これは、とりあえずのコード修正です。メモリを割り当てているので、どこかで解放する必要があります。今回はそのコードは実装していませんので、ご了承ください。短い動画を入力する程度であれば問題ありませんが、長い動画に対して利用する場合にはどこかで解放処理を記述してください。解放する箇所としては、Inferenceの入力後あたりで行うのが良いと思います)

mediapipe/calculators/tflite/tflite_converter_calculator.cc

    output_tensors->emplace_back(*tensor);
    for(auto& output_tensor : *output_tensors){
      auto ptr = malloc(output_tensor.bytes);
      std::memcpy(ptr, (void*)output_tensor.data.uint8, output_tensor.bytes);
      output_tensor.data.uint8 = (uint8*) ptr;
    }
    cc->Outputs()
        .Tag(kTensorsTag)
        .Add(output_tensors.release(), cc->InputTimestamp());

修正した結果 → 動いた

修正したプログラムで動かした結果、以下の動画のようになりました。すべての手に対してLandmarkを検出できていることを確認でき、目的を達成できました。

(先程も記載したようにメモリ解放処理を入れていないので、処理を実行していると利用メモリが少しずつ増えて行きますのでご注意ください)

まとめ

今回は、3つ以上の検出結果がおかしいという問題に対して、並列処理で処理結果を上書きしてしまうことが原因であると解析しました。メモリを新たに割り当て直すことで解決しました。これにより、当初の目的であった、複数人が動画に映っている状態での、すべての手の検出を達成できました。

今後はこれをカフェのウォークスルーに適用していきます。