Tensorflow.jsのhandposeをA-Frameに実装してみた

最近技術メモはGithubのプライベートに書き込んでいるのでBlogには何もアウトプットできていないすぎどんです。
すこしはアウトプットしていこうと一念発起。

GoogleがMediapipeで作成したFacemeshとHandposeをTensorflow.jsのライブラリとして公開していたので、それを試したことを記事にしようと思います。

Face and hand tracking in the browser with MediaPipe and TensorFlow.js

Tensorflow.jsでMediapipeのFacemeshおよびHandposeをjsライブラリ化して公開していた。 以下の記事です。

blog.tensorflow.org

Handposeに興味があってみていると・・・

returns twenty-one 3-dimensional landmarks locating features within each hand.

どうやら3D位置情報を返すようだ。それならA-Frameに実装してハンドトラッキングできるんじゃないかと思って実装してみた。

ライブラリの場所

Tensorflow.jsのHandposeは以下のGithubにて公開されている。

github.com

Readmeのソースおよび、デモソースを参考に実装を行う。

Mediapipeとは?

HandposeはMediapipeという仕組みを使って構築されている。 その前に、Mediapipeとは何なのかを少しご紹介します。

github.com

MediaPipe is a framework for building multimodal (eg. video, audio, any time series data), cross platform (i.e Android, iOS, web, edge devices) applied ML pipelines. With MediaPipe, a perception pipeline can be built as a graph of modular components, including, for instance, inference models (e.g., TensorFlow, TFLite) and media processing functions.

(Google翻訳)↓

MediaPipeは、マルチモーダル(ビデオ、オーディオ、任意の時系列データなど)、クロスプラットフォーム(つまり、AndroidiOS、Web、エッジデバイス)を適用したMLパイプラインを構築するためのフレームワークです。 MediaPipeを使用すると、知覚パイプラインを、たとえば推論モデル(TensorFlow、TFLiteなど)やメディア処理機能など​​のモジュールコンポーネントのグラフとして構築できます。

日本語で解説しているサイトをいくつか紹介します。

webbigdata.jp

note.com

www.techceed-inc.com

Handposeに関するMediapipeのGithubは以下

github.com

以前、Andorid アプリで試しましたが、かなりの精度でした。

デモサイト

A-Frameで実装したものは、glitch.com上で公開しています。

torpid-fanatical-nail.glitch.me

起動直後は重くて固まりますが、しばらくすると動き出すと思います。

ソース説明

Tensorflow.jsのHandposeは以下のGithubにて公開されています。

github.com

Readmeのソースおよび、デモソースを参考に実装を行っています。

以下、部分的にソースコードの説明をします。

Video Streamの取得

      window.onload = function() {
        navigator.mediaDevices = navigator.mediaDevices || ((navigator.mozGetUserMedia || navigator.webkitGetUserMedia) ? {
           getUserMedia: function(c) {
             return new Promise(function(y, n) {
               (navigator.mozGetUserMedia ||
                navigator.webkitGetUserMedia).call(navigator, c, y, n);
             });
           }
        } : null);

        if (!navigator.mediaDevices) {
          console.log("getUserMedia() not supported.");
          return;
        }

        // Prefer camera resolution nearest to 1280x720.

        var constraints = { audio: true, video: { width: 1280, height: 720 } };

        navigator.mediaDevices.getUserMedia(constraints)
        .then(function(stream) {
          var video = document.querySelector('video');
          video.srcObject = stream
          video.onloadedmetadata = function(e) {
            video.play();
            main();
          };
        })
        .catch(function(err) {
          console.log(err.name + ": " + err.message);
        });

      }

Handposeの推論で使用するカメラ動画Streamを取得する処理で、一般的な実装かと思います。

onload のタイミングで、video streamを取得しています。
video.onloadedmetadata のタイミングで main() を呼んでいて、この main() が次で説明するHandposeの推論およびA-Frameへ表示する関数となります。

Handposeの推論実施

      async function main() {
        // Load the MediaPipe handpose model assets.
        const model = await handpose.load();

        // Pass in a video stream to the model to obtain 
        // a prediction from the MediaPipe graph.
        const video = document.querySelector("video");
        // let hands = await model.estimateHands(video);

        // Each hand object contains a `landmarks` property,
        // which is an array of 21 3-D landmarks.
        async function handTracking() {
          let hands = await model.estimateHands(video);
          let handEntity = document.querySelector('#hand');
          let spList = document.querySelectorAll('.sp');
          spList.forEach(sp => sp.parentNode.removeChild(sp));
          
          if (hands.length > 0) {
            hands.forEach(hand => {
              hand.landmarks.forEach(landmark => {
                let sp = document.createElement("a-sphere");
                let landmarkX = landmark[0];
                let landmarkY = landmark[1];
                let landmarkZ = landmark[2] * 2.5;
                sp.setAttribute("position", `${landmarkX} ${landmarkY} ${landmarkZ}`);
                sp.className = "sp";
                sp.setAttribute("color", "black");
                sp.setAttribute("scale", "10 10 10");
                handEntity.appendChild(sp);
              })
            });
          }
          requestAnimationFrame(handTracking);
        };
        
        handTracking();
      }

await handpose.load(); でHandposeモデルを取得しています。
取得したモデルを使って async function handTracking 内の await model.estimateHands(video); で推論実行し、手の情報を取得します。

手の情報を取得した後は、A-Frameで表示する処理となります。毎回削除追加しているので無駄に負荷をかけちゃっています。

リアルタイム更新を行うため、requestAnimationFrame(handTracking); で関数を再帰的に呼び出して手の情報は更新しています。

ポイントは、let landmarkZ = landmark[2] * 2.5; で、縦横方向の位置情報に対して奥行き方向の位置情報が弱かったので増幅をかけています。

HTML の構成

  <body>
    <video id="video" style="-webkit-transform: scaleX(-1);"></video>
    <a-scene background="color: #FAFAFA" stats>
      <a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9" shadow></a-box>
      <a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E" shadow></a-sphere>
      <a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D" shadow></a-cylinder>
      <a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4" shadow></a-plane>
      <a-entity id="hand" position="5 5 -3" rotation="0 0 180" scale="0.01 0.01 0.01"></a-entity>
    </a-scene>
  </body>

videoタグにカメラ画像を出力しています。その際、style="-webkit-transform: scaleX(-1);で左右反転させています。左右反転はawait model.estimateHands(video);の引数でも実現できるようです。

id="hand" の a-entityがあります。async function handTracking() で取得した手の情報からa-sphereを作成していましたが、それはこのa-entityの子要素として配置されます。

手の調整として、async function handTracking() 内では手の形状調整を行い、<a-entity id="hand"~では、手そのものの位置、回転、サイズを調整しているイメージです。

まとめ

簡単ですがTensorflow.jsのライブラリとして公開された、MediapipeのHandposeをA-Frameへの実装を説明しました。こんなにも簡単にハンドトラッキングができるとは思いませんでした。

Mediapipeは、ほかにも様々なことができるようです。昨日公開されたBlogでは、3D Object Detectionができるようです。これもTensorflow.jsでライブラリ公開されたら試してみたいですね。

ai.googleblog.com

XR + AIは今後も目が離せないです。