Handposeで手の形が精巧にとれて興奮したのでAR.jsに組み合わせてみた話

去年の８月くらいに公開されていたMediapipeで実現していた手の検出がTensorFlow.jsで行えるようになったということで、
触りたくてしょうがなくなったので動かしてみました。

試した内容としては、AR.jsとHandposeを組み合わせてAR表示された3Dモデルを手で操作してみました。
結果は下の動画のようになりました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">AR.jsとHandposeを組み合わせてみた。Handposeで人差し指と親指を認識して、3Dモデルを操作<br>手の認識ができるようになったのでいろいろ遊びたいことが出てきた・・・ <a href="https://t.co/GFYsbxJbhb">pic.twitter.com/GFYsbxJbhb</a></p>&mdash; s-haya@技術書典応援際 (@shaya59912305) <a href="https://twitter.com/shaya59912305/status/1238464660707270656?ref_src=twsrc%5Etfw">March 13, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

親指と人差し指の位置に四角のオブジェクトを表示しています。
Handposeではすべての指の位置がとれているのですが、すべて表示すると画面が煩わしくなってしまったので一部だけにしてます。
人指と親指の間に3Dオブジェクトが来たら、指の位置に追従して動く仕組みにしてます。

Handposeを試してみたところ、かなり精巧に手の位置が取れたと感じ、
Webで手を活用したアプリやゲームに使えるぞ！？と期待が膨らみました。

ぜひとも、手の活用したアプリやゲームが出てきて欲しいと思ったので、
Handposeの概要と自分が試した内容を共有したいと思います。

この内容は、[技術書典で出したTensorFlow.jsで解説したデモアプリ](https://techbookfest.org/product/5108125614997504)を元に変更したものとなっています。
技術書典では、手の検出にHandtrack.jsというTensorFlow.jsで構築されたライブラリを使って実装していました。
執筆してからすぐにHandposeが発表されたので、これはアプリの中身をHandposeにアップデートするしかないな・・・！って心境とこの内容で本のちょっとした宣伝にならないかな・・・？という心境で望んでます。

# 要約
Handposeを使う上で必要な情報をまとめておきます。

* Handposeの使い方は、load()でモデルの読み込み。estimateHands()で手の検出が可能  
 手の検出で取得できる値は指の関節の座標と手のひらの座標
* 入力値に使えるデータ：　tf.Tensor3D | ImageData | HTMLVideoElement | HTMLImageElement | HTMLCanvasElement  
画像かビデオのデータが入力値として使用可能
* 出力値：

``` js
{
    handInViewConfidence: 1,
    boundingBox: {
        topLeft: [162.91, -17.42], // [x, y]
        bottomRight: [548.56, 368.23],
    },
    landmarks: [
        [472.52, 298.59, 0.00], // [x, y, z]
        [412.80, 315.64, -6.18],
        ...
    ],
    annotations: {
        indexFinger: [
            [412.80, 315.64, -6.18], // [x, y, z]
            [350.02, 298.38, -7.14],
            ...
        ],
        ...
    }
}
```
指の座標はlandmarksとannotationsに格納されている。
annotationsは、landmarksの値にどの指の情報なのかわかりやすくるためにアノテーションを付与したもの。

* HandposeとAR.jsの組み合わせ方  
AR.jsで初期化処理したときに追加されるHTMLVidoElementをHandposeの入力値とすることで組み合わせることが可能

# Handposeについて
動画や画像から手の検出を行い、3次元の座標を算出できるパッケージです。
2019年の8月にMediapipeで実装されたものをTensorFlow.jsで動かしたものとなっています。
Mediapipeはパイプライン推論という複数のグラフを組み合わせてMLアプリを実現するためのフレームワークです。
Mediapipeについて紹介されているブログはすでにたくさんあるのでこの記事では割愛します（たとえば[この記事](https://qiita.com/otmb/items/3e43be59de87e9e5a146)など）。

# Handposeで取得できるデータの解説
データはestimateHandsを使うことで取得できます。
estimateHandsの入力値は以下の型となっています。
ここから、動画と画像のどちらでも使うことが可能と読み取れます。

```
tf.Tensor3D | ImageData | HTMLVideoElement | HTMLImageElement | HTMLCanvasElement
```

estimateHandsで取得した値は以下のようになってます。

``` js
{
    handInViewConfidence: 1,
    boundingBox: {
        topLeft: [162.91, -17.42], // [x, y]
        bottomRight: [548.56, 368.23],
    },
    landmarks: [
        [472.52, 298.59, 0.00], // [x, y, z]
        [412.80, 315.64, -6.18],
        ...
    ],
    annotations: {
        indexFinger: [
            [412.80, 315.64, -6.18], // [x, y, z]
            [350.02, 298.38, -7.14],
            ...
        ],
        ...
    }
}
```

取得した値が何を示しているのか、もう少し中身を見てみます。
estimateHandsの返り値の型定義を見ると以下となっています。

``` index.d.ts
import { Prediction } from './pipeline';
interface AnnotatedPrediction extends Prediction {
    annotations: {
        [key: string]: Array<[number, number, number]>;
    };
}
```

``` pipeline.d.ts
declare type Coords3D = Array<[number, number, number]>;
interface Prediction {
    handInViewConfidence: number;
    landmarks: Coords3D;
    boundingBox: {
        topLeft: [number, number];
        bottomRight: [number, number];
    };
}
```

pipelineのPredictionに`annotations`を足した形となってます。Predictionはpipelineで算出した値であり、指の座標がlandmarksに格納されています。
では、AnnotatedPredictionで定義しているannotationsが保持している値は何かというと、landmarksの配列がどの指に対応するのかをわかりやすくアノテーションを付与したものです。

annotationsは`keypoints.ts`で定義したMESH_ANNOTATIONSを元に作成をしています。`thumb`などのkeyがlandmarksの何番目の値を取得するのかを示してます。
たとえば、`thumb`はlandmarks[1]、 landmarks[2]、 landmarks[3]、 landmarks[4]から取得して、配列を作成しています。

``` keypoints.ts
export const MESH_ANNOTATIONS: {[key: string]: number[]} = {
    thumb: [1, 2, 3, 4],
    indexFinger: [5, 6, 7, 8],
    middleFinger: [9, 10, 11, 12],
    ringFinger: [13, 14, 15, 16],
    pinky: [17, 18, 19, 20],
    palmBase: [0]
};
```

取得した座標は左上が原点(x,y)=(0,0)、右下が(x,y)=(1,1)となっています。
また、各指の配列はindexが進むほど、指先の座標を示しています。

# AR.jsとの連携について
`document.getElementById("arjs-video")`でHTMLVideoElementを取得して、estimateHandsにわたすことでAR.jsと連携できます。
"arjs-video"は何を指しているかというと、AR.jsで初期化処理をした際に追加される要素です。カメラから取得した映像がこの要素に描画されます。
estimateHandsの入力値にHTMLVideoElementは入力可能なので、以下のコードで手の位置座標を取得できます。

``` ts
import * as handpose from '@tensorflow-models/handpose';
async function estimateHands() {
  const model = await handpose.load();
  const video = document.getElementById("arjs-video") as HTMLVideoElement;
  const estimatedHands = await model.estimateHands(video);
}
```

注意点として、estimateHandsを最初に実行した際には、長く時間がかかります。
これは、TensorFlow.jsでWebGLを使う際のウォームアップが起きている影響で遅くなっているのかな？と予測しています。

３Dモデルを手つまむ方法としては、[レイキャスト](https://ics.media/tutorial-three/raycast/)で手の座標と３Dモデルの座標が交差しているかを判定し、交差していたら３Dモデルの座標を手の位置に追従するようにしました。
リンク先ではマウスの座標を使っていたところを、ここではestimateHandsで算出した親指と人差し指の中間の座標を使っています。

# 今後やりたいこと
* ジェスチャー認識
すべての指の位置座標が取得できるので、手のジェスチャーを認識できるので試してみたいです。
ジェスチャーを認識することができれば、またやれることも増えそうでわくわくします。

* 顔認識 
Handposeのほかに、顔のメッシュを認識できるモデルも公開されていたので試してみたいです。

* AR.jsのImageTracking
TensorFlow.jsとは関係ないのですが、この記事を書いていた途中でAR.jsでImageTrackingできるようになってたので、これも試してみたいです。

宣伝になってしまうのですが、技術書典の応援祭に[TensorFlow.jsについて説明した本](https://techbookfest.org/product/5108125614997504)を出しています。
TensorFlow.jsを使う上でのTipsやAR.jsとの組み合わせについてもう少し詳しく説明していますので、チェックいただけると嬉しいです。