TensorFlow.js version of PoseNetで手旗信号読取りサイト

#はじめに
初記事です。
以前Coral USB Acceleratorを購入したので、Raspberry Pi 4で**PoseNet**を動かして**手旗信号**を読み取るようなものを作って自己満足してました。
ボーイスカウトの指導者などをやっているので、スカウトたちにもやらせてみたいな、などと思ってたら、
[Real-time Human Pose Estimation in the Browser with TensorFlow.js](https://medium.com/tensorflow/real-time-human-pose-estimation-in-the-browser-with-tensorflow-js-7dd0bc881cd5)
ってのがあるんですね。すごい。

というわけでタイトルの通り、読取りサイトを作ってみたので、その備忘録です。ベンチャー/ローバースカウトが興味を持ってくれるの期待して…
![snap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1031494/4f6c0013-47eb-0324-173f-3db2ba3624be.png)
#参考にしたもの
とはいえWebサイトやJavaScriptなどはほぼ触ったことないので各所を参考に。
##PoseNet
何はともあれ姿勢推定してくれるPoseNetを、ということで、

 [Github - tensorflow/tfjs-models](https://github.com/tensorflow/tfjs-models/tree/master/posenet)

で公開されているdemosからcamera.jsを丸パク…

実装の詳細説明は割愛しますが、poseDetectionFrame()内でPoseNetから17関節の座標を取得して骨人間とかを描画しているところに手旗信号の姿勢を判定するロジックを入れます。
##ポーズを判定する
判定といってもどうすれば良いのか、ということでCoral Acceleratorで実装したときに参考にさせていただいた

[カメラモジュールから動画撮影してポーズを判定する](https://kihara0223.hatenablog.com/entry/2020/01/12/184711)

から、肘と肩の内角を計算して、その角度の状態からポーズを判定します。

##手旗信号
オールドスカウトなので手旗信号は一応マスターしているつもりですが、再確認＆参考に下記サイト。

[ポーラスター ～ボーイスカウト指導者応援サイト](https://polar-stars.com/?tag=signal-flag)

ボーイスカウト関連のかわいいイラストなどを提供してくれているサイトなのですが、手旗信号関連の参考イラストもたくさん。

基本的には数字の1～14までを表すポーズ（原画）があり、それを組み合わせてカタカナを表現するのが手旗信号です。
#手旗信号の姿勢を判定する
さて本題の手旗信号認識です。
##各関節の内角を計算する
まず1~14の数字を表す原画を判定するのですが、関節の角度を求めてその状態からどういうポーズをしているかを判定します。
ので3つの関節の座標から真ん中の関節の内角を求める関数。

```Javascript
function calculateInternalAngle(keypoints, point0, point1, point2)
{
    var a = {x:keypoints[point1].position.x-keypoints[point0].position.x, y:keypoints[point1].position.y-keypoints[point0].position.y};
    var b = {x:keypoints[point2].position.x-keypoints[point0].position.x, y:keypoints[point2].position.y-keypoints[point0].position.y};

    var dot = a.x * b.x + a.y * b.y;

    var absA = Math.sqrt(a.x*a.x + a.y*a.y);
    var absB = Math.sqrt(b.x*b.x + b.y*b.y);

    var cosTheta = dot/(absA*absB);
    var theta = Math.acos(cosTheta) * 180 / Math.PI;

    return theta;
}
```
手旗信号はほぼ腕の状態で判定するので、両肩と両肘の角度を求めておきます。

```Javascript
function getAngles(keypoints, minConfidence) {
    var angles = [];
  
    // left elbow
    deg = calculateInternalAngle(keypoints, LEFTELBOW, LEFTWRIST, LEFTSHOULDER, minConfidence);
    angles.push(deg);

    :
    // 省略
}
```

内角なので腕を上げている場合の角度か、下げている場合の角度かを求めておきます。

```Javascript
function get_positions(keypoints, minConfidence) {
    positions = [];
    if((keypoints[LEFTELBOW].score > minConfidence) || (keypoints[LEFTWRIST].score > minConfidence)){
        if((keypoints[LEFTELBOW].score > minConfidence) && (keypoints[LEFTELBOW].position.y < keypoints[LEFTSHOULDER].position.y) ||
        (keypoints[LEFTWRIST].score > minConfidence) && (keypoints[LEFTWRIST].position.y < keypoints[LEFTSHOULDER].position.y)){
            positions.push(UP);
        }else{
            positions.push(DOWN);
        }

    :
    // 省略
}
```

##腕の状態から原画を判定
原画の一覧は[ポーラスターさん](https://polar-stars.com/?p=2099)を参考に。
「2」は右上げと左上げの2種類、「11」は降り下げるジェスチャーなので上の状態と下の状態の2種類。
基本姿勢（原姿）を「0」として全17姿勢を判定。

```Javascript
function judge_genkaku(keypoints, minConfidence){
    angles = getAngles(keypoints, minConfidence);
    positions = get_positions(keypoints, minConfidence);

    if (150 < angles[ANG_LELBOW] &&
        150 < angles[ANG_RELBOW] &&
        (80 < angles[ANG_LSHOULDER] && angles[ANG_LSHOULDER] < 130) &&
        (80 < angles[ANG_RSHOULDER] && angles[ANG_RSHOULDER] < 130) &&
        positions[LEFTHAND_UPDOWN] == DOWN &&
        positions[RIGHTHAND_UPDOWN] == DOWN)
        return 0;

    :
    // 省略
}
```
角度は微妙な調整が必要だったので、コテコテのコードですが💦
判定した数値を配列にとっておきます。

##カナ（五十音）を判定
手旗信号は原画を続けて打って、基本姿勢（原姿）に戻るとカナ1つを打ち終えたと判断します。原画の形を組み合わせるとカタカナの形になります。
こちらも[ポーラスターさんの一覧](https://polar-stars.com/?p=33149)を参考に。

```Javascript
function judge_kana(text, genkakus) {
    strGen = genkakus.toString();

    if (strGen == [9, 3])
        return text + 'あ';
    else if (strGen == [3, 2])
        return text + 'い';

    :
    // 省略
}
```
という具合で原画の組み合わせをカナにします。

以上、手旗信号判定の簡単な流れでした。

#最後に
ソースコードはこちらです。
[Github](https://github.com/Hiroshi32Yoshida/posenet_flag_sign_js)

読取りサイトはこちら
https://hiroshi32yoshida.github.io/posenet_flag_sign_js/

うまく読み取るためのコツは注意点としてサイトに書いてありますが、
・背景がゴチャゴチャしていないこと
・服装と背景のコントラストがあること
・上半身の関節が隠れていないこと（手旗を持ってると判定できないことがあります）
などです。6とか9とか11が読取り渋いですが、条件良ければ上手く読み取ってくれます。
練習程度にはなると思います。

ただ角度からの判定が微妙なことが多いので、ml5.jsの機械学習で原画をトレーニングして判定するVerも作成してみました。機械学習とかよくわかっていませんが。
次回記事で紹介します。

以上です。