🤌

TensorFlow.js の Hand Pose Detection を使って「AR妖怪けむり」を作ってみる

2023/03/31に公開

数年前にちょっと話題になりましたが 「妖怪けむり」、製造終了してしまったのでもう売っていないそうですね。[1] あまり遊んだ記憶はないですが、もう無くなってしまったと聞くと少し寂しいものがあります。

TensorFlow.jsHand Pose Detection を使ったら、Web ブラウザ上で動作する AR妖怪けむり 的なものが作れるんじゃなかろうか...
とおもったのでちょっと試してみました

指先から煙が出る様
完成したものはこちら

そもそも「妖怪けむり」とは...?

妖怪けむり、馴染みがない方も居られるかとおもうので一応。

  • ニコニコ大百科 - ようかいけむりとは

    愛知県の堀商店が販売、小林商店が製造。
    主な販路は駄菓子屋で、価格は1枚20円。
    商品名は「ようかいけむり」「おばけけむり」「カードけむり」など。いずれも、力強い筆致で描かれたおどろおどろしい妖怪の絵が特徴。
    紙製のカードに、パラフィン紙で保護された「くすり」が塗られている。これを指に取り、親指と人差し指をつけたり離したりすることで、煙のような白い物体が空中に舞う。

  • YouTube 検索 - ようかいけむり
  • Google 画像検索 - ようかいけむり

指先から煙[2]を発生させて楽しむ、素朴なおもちゃのことですね。
駄菓子屋さんで手に入るシャボン玉とかポリバルーンの仲間?みたいな。

TensorFlow.js

TensorFlow logo

Hand Pose Detection で検出できる情報

Readme からどんな情報が取れるのか確認。

関節の番号

関節の番号
画像引用元: https://github.com/tensorflow/tfjs-models/tree/master/hand-pose-detection#keypoint-diagram

CMC ... carpometacarpal (手根中手根)
MCP ... metacarpophalangeal (中手指節)
IP ... interphalangeal (指節間)
PIP ... proximal interphalangeal (近位指節間)
DIP ... distal interphalangeal (遠位指節間)

JSON の構造

[
  {
    // 信頼度 (0 ~ 1)
    score: 0.8,
    // 右手 or 左手
    handedness: 'Right',
    // 2Dの座標
    keypoints: [
      {x: 105, y: 107, name: "wrist"},
      {x: 108, y: 160, name: "pinky_finger_tip"},
      ...
    ],
    // 3Dの座標
    keypoints3D: [
      {x: 0.00388, y: -0.0205, z: 0.0217, name: "wrist"},
      {x: -0.025138, y: -0.0255, z: -0.0051, name: "pinky_finger_tip"},
      ...
    ]
  }
]

Webカメラからの映像を解析して、毎フレームこちらの JSON が返ってくる感じ。
例えば keypoints[4] が親指の先端、keypoints[8] が人差し指の先端。

ローカル開発環境構築

公式が用意している Live Demo がとてもよく出来ているので、これを改造して目的を達成したいとおもいます。
まずは Readme に従って、Live Demo がローカル開発環境で動作するように。

yarn, git は事前にインストール済みの想定

# ソースコード取得
git clone git@github.com:tensorflow/tfjs-models.git

# live demo のフォルダへ移動
cd tfjs-models/hand-pose-detection/demos/live_video

# 古いキャッシュなどがあれば削除
rm -rf .cache dist node_modules

# 依存する package をビルド
yarn build-dep

# package のインストール
yarn

# 起動 (停止は Ctrl + C)
yarn watch

# ブラウザで下記URLにアクセス
http://localhost:1234/?model=mediapipe_hands

Live Demo のソースコード観察

hand-pose-detection/demos/live_video フォルダ以下が Live Demo のソースコード。こちらを追って、どこに手を入れたら良いか探ります。

src/camera.js ... Webカメラからの映像を解析して結果(2D/3Dの骨格など)を描画する class
src/option_panel.js ... 画面右上、動作設定用GUIの作成
src/shared/params.js ... GUIの選択項目など、設定値の定数
src/index.js ... エントリポイント

おおよそ上記のような構成となっているので camera.js の処理、Canvas に 2D で手の関節を描画しているところを改変するのがよさそう。(drawResults, drawResult, drawKeypoints などが描画処理)

また index.html やソースコード上の import を確認すると
右上に表示されている動作設定の GUI 部分は dat.gui を、
左上のFPS表示は Stats.js をCDNから持ってきて使用している模様。
https://github.com/dkawanabe/tfjs-models/blob/d596de273ccc5143fbb1f550bf30af1eb94a45b9/hand-pose-detection/demos/live_video/index.html#L60-L62

今回は GUI の設定項目も追加・変更するので dat.gui の API ドキュメントを確認しておく。

指先がくっついた、離れたを判定する

妖怪けむり は人差し指と親指の腹をくっつけたり離したりを繰り返すと、その間から煙状の物質が出る遊びなので、その指の動きを Hand Pose Detection が検出した指先の座標から「くっついた」「離れた」が行われたことを判定する必要が。
今回のケースでは

  1. 親指の先端人差し指の先端 の座標がある一定以上に接近したら指が「くっついた」👌
  2. その後、座標がある一定の距離を離れたら「離れた」✋

と判定するだけでよさそう。
今回はざっくりと 15px 以下に接近したら指がくっついた、その後 80px 以上離れたら指が離れたとしておきます。[3]

また、煙を発生させるタイミングは 指が離れた時 に、煙を発生させる位置は親指と人差し指の座標の 中間点 にすればそれらしくできそう。
JavaScript のコードで書くとおおよそ以下のような感じ。

// 抜粋

// 親指の先端の座標
const thumbTip = keyPoints[4];

// 人差し指の先端の座標
const indexTip = keyPoints[8];

// 2点間の距離を算出
const distance = Math.sqrt(Math.pow(thumbTip.x - indexTip.x, 2) +
                           Math.pow(thumbTip.y - indexTip.y, 2));

// 親指と人差し指がくっ付いた状態かどうか (※ stillPinched は前フレームでくっついていたかどうか)
const pinched = stillPinched ? distane < 80 : distance <= 15

// 2点間の中心座標を算出。この座標に煙を発生させる
const midwayPoint = {
  x: (thumbTip.x + indexTip.x) / 2,
  y: (thumbTip.y + indexTip.y) / 2,
}

2点間の距離の公式, 中点の座標

Smoke.js で指定した座標の位置に煙を発生させる

煙を発生させる位置が決まったので、あとはそこに煙を描画するだけ。
煙は Canvas 上に関節の位置を描画する処理に倣って Particle で描画すればいいのですが、自前で煙の Perticle をゼロから書くのはつらいものがあり😫
探してみたところ、Smoke.js という Canvas 上の任意の座標に煙のエフェクトを発生させる という今回のケースにぴったりの Package があったので、こちらを使って煙を描画します。

改造後のソースコード

改造後のソースコード全体は こちら を参照。

改造のポイントとしては以下のあたり

つまむ、はなすの検出部分

https://github.com/dkawanabe/tfjs-models/blob/d28f565a1f7e6ff60e4759f72d968674b810123b/hand-pose-detection/demos/live_video/src/camera.js#L300-L322

骨格を描画する Canvas の上に、煙を描画する Canvas を重ねる[4]

https://github.com/dkawanabe/tfjs-models/blob/8f07ccdc1f513fbab25e14628e6cd50e1167ffd3/hand-pose-detection/demos/live_video/index.html#L52
https://github.com/dkawanabe/tfjs-models/blob/8f07ccdc1f513fbab25e14628e6cd50e1167ffd3/hand-pose-detection/demos/live_video/index.html#L38-L43

GUI に Smoke.js の設定項目を追加

https://github.com/dkawanabe/tfjs-models/blob/8f07ccdc1f513fbab25e14628e6cd50e1167ffd3/hand-pose-detection/demos/live_video/src/option_panel.js#L84-L87
https://github.com/dkawanabe/tfjs-models/blob/8f07ccdc1f513fbab25e14628e6cd50e1167ffd3/hand-pose-detection/demos/live_video/src/option_panel.js#L139-L148

デフォルトのモデルを mediapipe_hands に設定。URLのパラメータが指定されていないくてもモデル未選択のエラーを出さない

https://github.com/dkawanabe/tfjs-models/blob/8f07ccdc1f513fbab25e14628e6cd50e1167ffd3/hand-pose-detection/demos/live_video/src/index.js#L170-L175

2D/3D の骨格描画はデフォルトでOFFにしておく

https://github.com/dkawanabe/tfjs-models/blob/8f07ccdc1f513fbab25e14628e6cd50e1167ffd3/hand-pose-detection/demos/shared/params.js#L34-L38

静的サイトとしてビルドする

yarn build を実行すれば、これまで作ってきたものを静的サイトとしてビルドすることができます。
Web サイトとして公開する際は、dist フォルダに生成された index.htmlsrc.xxxxxxxx.js[5] をアップロードするだけ。

完成したものを GitHub Pages に公開

今回作成したものは こちら に公開しました。
判定が甘いので時折おかしな動きをすることもありますが、
画面右上の GUI で煙の色(smokeColor)を変えたり、発生量(particles)を増やしたりして遊んでいただければとおもいます。
ヤバい色の煙が発生する様

おわりに

TensorFlow.js と Smoke.js を使うことで、容易にそれっぽいものを作ることができました。
これでいつでも指先から煙を無限に発生させられます。
実際の「ようかいけむり」は霧状の煙ではなく お線香 のような細い煙がゆっくり発生するので、
Smoke.js の描画処理をカスタマイズしたり自前で Particle を書いたらもっとリアルに近づけられるかもしれないですね。
ほかにも手がカメラの近くにある時は煙を大きく、遠くにあるは煙を小さく描画するなどしたらよさそうです[6]

Hand Pose Detection のリアルタイム検出にはまだまだ面白いことができそうな可能性を感じるので、機会があればいろいろ試していきたいところです。

脚注
  1. https://www.google.com/search?q=妖怪けむり+製造終了 ↩︎

  2. 煙というか繊維状のほこり? ↩︎

  3. 固定値だと手がカメラに近過ぎたり遠過ぎたりするとうまく判定できなくなるので、手の距離(画面内での大きさ)によって判定を変えたほうがよい ↩︎

  4. 同一の Canvas に煙を描画すると、背景が真っ白になる現象があるため。 ↩︎

  5. xxxxxxxx 部分はビルドのたびに変わる ↩︎

  6. 各関節間の距離をみたら、手がカメラの近くにあるか遠くにあるかはある程度判別できそう ↩︎

Discussion