ml5.jsでPoseNetで手旗信号読取りWebを作る

![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1031494/c1be7b16-87f1-8c89-88da-fa69cfcbc77a.gif)
#はじめに
前回 [TensorFlow.js version of PoseNetで手旗信号読取りサイト](https://qiita.com/hiro32/items/9c7fa684c22caa148760) という**PoseNet**（機械学習によって姿勢を推定できる技術）を使った**手旗信号**読取りWebサイトを作った話を書きました。
記事中にもありますが、推定された肩や肘の角度から判定しているのですが、実測値を見ながら手動で判定式を書いた感じなので精度に納得がいっていないんです。

せっかくTensorFlowとか使ってるなら手旗信号のポーズ自体も学習させた方が良いな…とか何となしには思っていたのですが、そんなスキルがあるわけでもなく。

と半ば諦めつつもググっていると、

https://thecodingtrain.com/learning/ml5/7.2-pose-classifier.html

いや、まさにコレじゃないすか！

というわけでこのml5.jsとPoseNetを使った手旗信号読取りWeb 機械学習Verの作成を紹介します。
結果から言うと読取り精度のイマイチさはあまり変わっていません💦ただ学習方法の最適化など、まだ精度向上の余地はあるな、という感じなので前向きです。
#参考にしたもの
ちなみにわたくしのバックグラウンドはフロントエンドエンジニアとか機械学習専門とかではないので、全て勉強しながらの備忘録という感じです。
##PoseNet
前回と同じです。前回の記事参考にしていただければと思います。
##ml5.js
https://ml5js.org/

TensorFlow.jsをベースにした機械学習用のライブラリです。アーティストやクリエイティブ、学生向け、とありますね。
このライブラリを使って手旗信号を学習させて、得られたモデルを使って判定させる、といった具合です。
##p5.js
はじめは前回のコードの判定部分のみを上記ml5.jsによる判定に置き換えようと思ったのですが、描画の部分が上手く動かないため、先の[Coding Train](https://thecodingtrain.com/learning/ml5/7.2-pose-classifier.html)のダニエル先生が使っている

https://p5js.org/

というライブラリをそのまま使うことにしました。
Processingという映像や音楽に使われているライブラリをJavaScript用のライブラリにしたものだそうです。
#実装！
ぶっちゃけ手旗信号の原画の判定は、条件付きであれば[Coding Trainサイト](https://thecodingtrain.com/learning/ml5/7.2-pose-classifier.html)のサンプルのみで実現できます。
条件というのは、画面の大きさが固定だったり画面内の位置に縛りがあったりとか、その辺の課題への対応を中心に作業の流れを書きます。
##姿勢データの収集
まず手旗信号の各原画1～14の17姿勢（詳しくは前記事参照）のデータを収集します。

[Coding Train](https://thecodingtrain.com/learning/ml5/7.2-pose-classifier.html)の「Data Collection」というサンプルがこれにあたるのですが、こちらではPoseNetを使って、判定したい姿勢の17ポイントの座標を収集して、jsonファイルで出力されます。
操作方法としては、「poseNet ready」という表示後、アルファベットのキー（"s"と"t"以外）を押下すと5秒後「collecting」となって収集を開始します。10秒後「not collecting」に表示が変わると押したキーの文字に割り当てられるデータの収集終了です。これを必要な姿勢分実行し、最後に"s"を押すと結果がjsonファイルに出力されます。

ですがこれで得られる座標が絶対座標というか、収集する時の画面の座標なので、これが画面を変えたときに判定が渋い原因なのかなと。
ですので下図の通り、肩幅の距離（緑線）を単位距離とし、鼻と両肩のそれぞれのポイントから各関節の距離（黄線）を正規化した値を収集することにしました。
**追記** 精度向上のため前回記事で行った肩と肘の内角も学習の要素に入れてみました。
![kids_boyscouts_man_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1031494/be72dac1-663e-d0ac-6068-21e250235a87.png)

```Javascript:flag_sem_util.js
const distance = (x0, y0, x1, y1) => Math.hypot(x1 - x0, y1 - y0);
function pose_normalize(keypoints){
  inputs = [];

  // 肩幅を単位距離とする
  let x0 = keypoints[LEFTSHOULDER].position.x, y0 = keypoints[LEFTSHOULDER].position.y;
  let x1 = keypoints[RIGHTSHOULDER].position.x, y1 = keypoints[RIGHTSHOULDER].position.y;
  if(x0 == undefined || y0 == undefined || x1 == undefined || y1 == undefined){
    return null;
  }
  let basedist = distance(x0, y0, x1, y1);
  if(basedist == float.NaN || basedist == 0){
    return null;
  }

  // 上下判定で渋いことがあるので鼻からの距離も
  let x2 = keypoints[NOSE].position.x, y2 = keypoints[NOSE].position.y;
  
  // 両肩と鼻を除く8ポイントの正規化距離を登録
  for (let i = 1; i < LEFTHIP; i++) {
    if(i != LEFTSHOULDER && i != RIGHTSHOULDER){
        let l0 = distance(x0, y0, keypoints[i].position.x, keypoints[i].position.y) / basedist;
        let l1 = distance(x1, y1, keypoints[i].position.x, keypoints[i].position.y) / basedist;
        let l2 = distance(x2, y2, keypoints[i].position.x, keypoints[i].position.y) / basedist;
        inputs.push(l0);
        inputs.push(l1);
        inputs.push(l2);
    }
  }

  // 肩と肘の内角を登録
  // left elbow
  deg = calculateInternalAngle(keypoints, LEFTELBOW, LEFTWRIST, LEFTSHOULDER);
  inputs.push(deg);

  // right elbow
  deg = calculateInternalAngle(keypoints, RIGHTELBOW, RIGHTWRIST, RIGHTSHOULDER);
  inputs.push(deg);
    
  // left shoulder
  deg = calculateInternalAngle(keypoints, LEFTSHOULDER, LEFTELBOW, LEFTHIP);
  inputs.push(deg);
    
  // right shoulder
  deg = calculateInternalAngle(keypoints, RIGHTSHOULDER, RIGHTELBOW, RIGHTHIP);
  inputs.push(deg);

  return inputs;
}
```

[Github](https://github.com/Hiroshi32Yoshida/flag_semaphore_reading/tree/main/data_collection)

上記のソースコードで手旗原画の17姿勢を収集しjsonファイルが得られます。
##姿勢データを学習させる
これは簡単です。上記収集で得られたjsonファイルをml5のニューラルネットワークオブジェクトに食わせるだけです。
[Coding Train](https://thecodingtrain.com/learning/ml5/7.2-pose-classifier.html)のサンプルをほぼそのまま使えるのですが、読み込ませるポイント数と出力数を変更する必要があります。

今回は3つの距離を正規化したものと、上半身の判定に使う関節分8ポイント、それと両肩両肘の内角4つなのでinputsを28、17姿勢なのでoutputsを17にしました。
**訂正** 姿勢の数は17にするとなぜか17姿勢目を判定してくれないので、18にしてます。

```Javascript:loading_data/sketchs.js
let options = {
    inputs: 28,
    outputs: 18,
    task: 'classification',
    debug: true
}
```

これを実行し学習が終了するとモデルファイルが出力されます。

* model.json: //モデルのデータ
* model_meta.json: //メタデータ（モデルの付随情報）
* model.weights.bin: //読み取った姿勢と正解の結びつきの重みバイナリデータ（多分）

##姿勢判定
学習して得られた3つのモデルファイルをml5に読み込ませます。

```Javascript:flag_semaphore_reading.js
  brain = ml5.neuralNetwork(options);
  const modelInfo = {
    model: 'model/model.json',
    metadata: 'model/model_meta.json',
    weights: 'model/model.weights.bin',
  };
  brain.load(modelInfo, brainLoaded);
```

あとはclassifyPose()をコールバックしてリアルタイムに読み取り距離で正規化したデータで分類判定させます。
判定された原画でカナを判定するのは前回のロジックをそのまま使いました。
#まとめ
##ソースコード
[Github](https://github.com/Hiroshi32Yoshida/flag_semaphore_reading)
##読取りサイト
https://hiroshi32yoshida.github.io/flag_semaphore_reading/
##課題
心なしか前回版より精度は上がっている気がしないでもないですが…うーーん…気のせいだなぁ😢
###スマホで使えない
ml5.jsの影響かな？
私の環境iPhone8+Safariでは動きませんでした。
そもそもAppleはブラウザからカメラの使用は推奨しないという噂もどこかで見ましたが…前回版は動くんだよなぁ
**追記** PoseNetの姿勢推定は問題ないのですが、どうもml5.classify()を呼ぶと固まってしまいました。Code Trainサイトのサンプルも同じなので、やはりml5.jsの影響かな…
###手旗を持っていると読み取れない
**手旗信号**なのに！
関節が手旗に隠れてしまっているとPoseNetの姿勢推定が渋くなるみたいです。
ですので原画「6」とか手旗を持っていなくても場合によって読取りが渋いです。「11」ももちろんですが、「9」とかも。
###画面の端に近いと読取りが上手くいかない
このための対策をしているはずなんだけどな…
###誤判定
似ている原画や体格の違いやカメラのアングルによってまだまだ不安定です。
学習させるパラメータを増やすとか、子供のデータを学習させるとか何か方法はあると思います。勉強します。
#さいごに
##（日本の）手旗信号を考えた人に言いたいこと
何で「11」を*ジェスチャー*にしたの！？
##全国のボーイスカウト指導者のみなさん
練習程度には使えると思います。おうちスカウティングなどに是非！