某アニメの悪役が使う「とっておきの手品」っぽい呪文が使えた気になる(かもしれない)Webアプリを作った話【技術概要】

7 min read読了の目安(約6800字

タイトルの件、どんなものかというと以下のようなものです。
手を閉じた状態から 1本ずつ指を伸ばしていくと指先に炎がやどり、最終的には 5本の指全てに以下のように炎がやどります(笑)

動いている様子は、以下のツイートの動画か YouTube の動画をご覧ください。

https://twitter.com/youtoy/status/1359824140086124544

相当に年代を選ぶネタだったはずのものですが(あらためて調べたら、原作の漫画の連載期間は「1989年から1996年まで」とのこと)、現在アニメで放送中&この技の使い手の敵がバトル中なタイミングだからか、けっこう反応をいただけたりしました。

せっかくなので、技術の部分の概要について、少し記事に書いてみようと思います。
⇒ 「Zenn のアカウントはけっこう前に作ったのに、投稿はしてなかった...」という考えが頭をよぎったのもあって初投稿。

手の認識の仕組み

指の認識というか、手を認識している部分の仕組みは「Googleさんの提供の MediaPipe Hands」の Web版で、HTML+JavaScript(+CSS)という構成のものがブラウザで動いています。

以前試作した仕組みの話

今回、片手のみを認識させていましたが、以下のように2つの手を同時に認識できたりもします(※ 複数人の3つ以上の手を認識したりもできるようです)。

●Googleさんの MediaPipe Hands を使った仕組みのテスト - YouTube

なお、上記は冒頭の動画と異なり、MediaPipe Hands のサンプルプログラムで元々入っていた、認識結果を可視化する仕組みを残したままにしてます。見た目に分かりやすいように残していたのですが、そのため両手の上に緑・赤の線が重畳されているのが分かるかと思います。

描画されているもののうち、透過の矩形は Canvas上に描画するよう、自分がプログラムで実装したものです。余談ですが、これらの矩形は人差し指の先を起点に描画されるようにしていて、そのサイズは「親指の先端と人差し指の先端の距離」に連動して拡大/縮小されるように作っています。

手を認識した結果の取得方法

両手の認識の仕組みに関して、公式ページの「Hand Landmark Model」の項目の部分に、手のどの位置がどんな配列版号で取得できるか画像で示させています。

プログラム的には公式ページの「JavaScript Solution API」の部分に書かれた以下のコード内だと、 results.multiHandLandmarks の部分で手の各部の位置座標を取得できます。

function onResults(results) {
  canvasCtx.save();
  canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
  canvasCtx.drawImage(
      results.image, 0, 0, canvasElement.width, canvasElement.height);
  if (results.multiHandLandmarks) {
    for (const landmarks of results.multiHandLandmarks) {
      drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS,
                     {color: '#00FF00', lineWidth: 5});
      drawLandmarks(canvasCtx, landmarks, {color: '#FF0000', lineWidth: 2});
    }
  }
  canvasCtx.restore();
}

例えば、 results.multiHandLandmarks[4] が親指の先、 results.multiHandLandmarks[8] が人差し指の先、という形です。さらに、 results.multiHandLandmarks[4].x で x座標の値がとれたりします。

実際に実装した処理

記事の冒頭の動画では、そういった情報を使い、以下のような処理を実装しています。

  • 5本の指の先の座標 (x, y) を取得
  • 指の根元の座標(今回のものは、中指の根元と小指の根元)の座標を取得
  • 5本の指の先と指の根元との距離を計算し、その距離が事前に設定した閾値を超えると、指先を基点に炎の画像を表示

上記の 3つ目の処理を入れているため、曲げている指を伸ばすと、指先に炎の画像が重畳されるような動作をします。

指の曲げ伸ばしの判定

指の曲げ伸ばしの判定について、簡易に行うなら指の先と第二関節の上下・左右の位置関係を見る方法もあります。過去に Scratch で実装した以下のデモ(※ 手の認識の部分は、独自の拡張が行われた Scratch の独自拡張機能である Handposeベースの機能(Handpose2Scratch)を利用)は、その方法を使っています。

https://twitter.com/youtoy/status/1313327059196735489

親指は左右の位置関係、それ以外の指は上下の位置関係で判定しています。

このとき親指は、右手か左手か/裏向きか表向きかで、指先と第二関節の左右の位置関係が逆向きになってしまいます。それらに対応するために、親指と薬指の関節の左右の位置関係を合わせて条件判定に使い、親指の曲げ伸ばしを判定する際の指先と第二関節の位置関係の判定条件を変化させています。
なお人差し指から小指までは「手を逆さにする使い方はしないだろう」という前提で、指先が根元より画面上で上にあれば、指を伸ばしているという判定を常に行っています。

画像を描画している部分の仕組み

公式ページの「JavaScript Solution API」の部分に書かれた以下のあたりのコードを見ると分かりますが、サンプルそのままだと描画まわりは Canvas を使う形です。

<body>
  <div class="container">
    <video class="input_video"></video>
    <canvas class="output_canvas" width="1280px" height="720px"></canvas>
  </div>
</body>
const videoElement = document.getElementsByClassName('input_video')[0];
const canvasElement = document.getElementsByClassName('output_canvas')[0];
const canvasCtx = canvasElement.getContext('2d');

記事の途中で掲載していた YouTube の動画のプログラムでは、半透明の矩形の描画は Canvas の描画処理を直に書いていました。

しかし、描画周りでいろいろ複雑なことをやろうとすると、何らかの描画ライブラリを使うほうが後々便利になりそうです。

p5.js を利用している話

そこで、それに関するライブラリの情報を色々と見て、p5.js を選びました。
選んだ理由は、p5.js がアート系でもよく使われる Processing の JavaScript実装であるためです。また、ずっと興味があったけれど実際に利用したことがなく、利用する機会を見つけたいと思っていたのもありました。

1つ目の理由として書いた内容について補足すると、自分は描画周りを作るノウハウがあまりなく、そのあたりのサンプル・ノウハウが Processing だとたくさんありそうで、それを利用できそうだと思ったためです(実際の利用は少なかったですが、Processing に関する情報はけっこう前から定期的に追いかけtいました)。
実際に、「Processing|p5.js クリエイティブコーディング」や「Processing|p5.js ジェネラティブデザイン」といったキーワードで検索すると、いろいろと情報が出てきます。

p5.js を MediaPipe Hands のサンプルに組み込む

今回は概要を書く記事というのもあり、詳細は長くなるのもあって省きますが、p5.js関連では以下のような実装をしました。
記事の途中で書いていたとおり、p5.js は手をつけはじめたばかりだったため、実装を進めて問題が出るたびに対策をググったり、簡単なサンプルプログラムを別に書いて挙動を確認したり、ということを繰り返して、うまく動作するように実装できました。

  • p5.js単体での実装に関わる話
    • 描画用の画像は preload() で事前読み込み
    • createGraphics() を使って描画用の複数のレイヤーを作る
    • 画像表示に透過処理を加える
  • MediaPipe Hands のサンプルが絡む部分の話
    • MediaPipe Hands側との衝突(グローバル汚染)を避けるために Instance Container を利用
    • 通常は setup()draw()(ループ処理)のセットで動くところを、noLoop() を使ってループ処理を止め、MediaPipe Hands側の描画処理タイミングに合わせた再描画を行うようにする
    • 5本の指の先と指の根元との距離を計算して閾値判定をしていると上で書いていた部分に関連し、閾値を超えた場合に事前読み込みをした画像を指先の座標に合わせて表示
    • 指先への画像表示について、5本全ての指先に画像が表示された後は、全ての指の先に表示させた画像は非表示にならないようにする

なお、指先の部分に表示している炎の画像は、「いらすとやさんの火・炎のイラスト」を使わせていただきました。

今後の話

今回作ったものは、元々は「p5.js の MediaPipe Hands のサンプルへの組み込み」を進めていた中で、その作業の副産物としてできたものです。

記事の途中で掲載していた YouTube の動画の事例のように、MediaPipe Hands のサンプルは両手を同時に認識できるため、両手を認識する仕組み&p5.js での描画を組み合わせていこうと思っています。

MediaPipe Holistic に関する内容

また、MediaPipe について、他に気になっていて少し試したもの(p5.js との連携は未着手)に、MediaPipe Holisticがあります。

●Googleさんの MediaPipe Holistic を使った仕組みのテスト - YouTube

MediaPipe Holistic を使うと、手・顔・姿勢の認識結果を同時に取得できます。MediaPipe の中の「Hands」に加えて「Face Mesh」と「Pose の顔と手を除いた部分」が同時に使えるようなものでした。

上記の動画では、MediaPipe Hands で自分が試していたような仕組みに加えて、以下のような仕組みを試しに加えてみています。

  • 「口が開いているかどうか」で描画される図形の形が変わる
  • 「両肩が左右どちらに傾いているか」によって図形の色が変わる

公式ページの冒頭「Overview」を見ると、以下のように良い感じに認識がされています(以下は静止画ですが、公式ページの冒頭はアニメーションする画像です)。

この記事を書いた時点で、YouTube動画で掲載していた自分が手を加えたサンプルを再度動かそうとすると、なぜかうまく動いていない状態で(公式サンプルも自分の環境だと同時点では動かない...)、この記事で書いていた p5.js との組み合わせの実装に着手できない状態なのですが、しばらく経ってから様子を見てみようと思っています。

追記

WebGL による 3D表現

p5.js を組み込んだからこそ手軽に使えるようになった描画を試したくて、WebGL による 3D表現ができるものを試しました。

https://twitter.com/youtoy/status/1362095940950061057