Firebase 入門 と PoseNet #ヒーローズリーグ

[MAヒーローズ・リーグ アドベントカレンダー](https://qiita.com/advent-calendar/2019/heroesleague) 5日目として書いた記事です。

先日行われた [学生リーグ2019決勝審査会](https://mashupawards.connpass.com/event/153903/) にお手伝いで参加したのですが、 作品として 機械学習 をうまく使った作品がいくつかあり、とても刺激をうけたので参加した学生に教えてもらった [PoseNet](https://github.com/tensorflow/tfjs-models/tree/master/posenet) と [Firebase](https://firebase.google.com/?hl=ja) で 簡単な 厨二病 作品を作ってみました!

# Firebase とは
[Firebase](https://firebase.google.com/?hl=ja)
Google が提供する モバイルおよびWebアプリケーション開発プラットフォームです。
無料枠もあり、BaaS なので構築も楽なのでおすすめです。

# PoseNet とは
[PoseNet](https://github.com/tensorflow/tfjs-models/tree/master/posenet)
Webブラウザでリアルタイムに人間の姿勢推定を可能にする機械学習モデル
TensorFlow.jsバーションを利用しました。
貧弱なPCやAndroidでも ChromeブラウザとWebカメラだけで 姿勢推定が可能です。 

# 作るもの
厨二病 ポーズ をしたら PC 画面に 文字列が表示される ような作品をサクッとつくりたいと思います。Firebaseは Hosting と 動画の保存先に Storage を利用します。
![kids_chuunibyou_girl.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100147/92c604ce-b293-6edb-6572-1aa62a63126c.png)

ヒーローズリーグ 応募作品 [CR - 厨二病現実感](https://youtu.be/zgAvGCmFDtI?t=1559) を再現。

# ソースコード一式
[tfuru/walpurgisnacht](https://github.com/tfuru/walpurgisnacht)

# 設置先
[サンプル](https://walpurgisnacht-f96b1.firebaseapp.com)

左肘近くに右手をもっていくとエフェクト再生させる。

# Firebase 環境を作る
BaaS なので コマンドラインから デプロイなどができます。まずは [Firebase CLI](https://firebase.google.com/docs/cli?hl=ja) を
自分のPCにインストールして `firebase` コマンドが動作するようにしてみます。
*環境によっては 事前に npm や git のインストールも必要です。

```
npm install -g firebase-tools
firebase login

```

# Firebase プロジェクトを作成
[Firebase console](https://console.firebase.google.com/u/0/?hl=ja) にアクセスして新規プロジェクトを作る
<img width="380" alt="スクリーンショット 2019-12-02 22.47.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100147/e7ca1e61-5210-1477-925c-51e243d7cfef.png">

<img width="343" alt="スクリーンショット 2019-12-02 22.50.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100147/c728c2e1-54eb-735e-0879-dee2973f2944.png">

ここでは `walpurgisnacht` と名付けてみました。

## Firebase Storage 設定を変更する
ロケーション未設定の場合 次の手順で失敗するので必ず確認する必要があります。
Firebase Console 左メニューの Storage を選択して はじめる ボタンを押して初期誠意手をおこなってください。
<img width="426" alt="スクリーンショット 2019-12-02 22.53.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100147/7aa2768c-46d3-fca5-a6ed-fcaf6734d4d3.png">

# Firebase CLI で プロジェクト雛形を生成する。
Firebase CLI を使ってプロジェクの雛形を作成しましょう。 `Hosting` と `Storage` だけ有効にして `Use an existing project` で先程作った `walpurgisnacht` を指定してその他は適当に設定する。 

```
mkdir walpurgisnacht
cd walpurgisnacht
firebase init
```
<img width="533" alt="スクリーンショット 2019-12-02 22.56.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100147/84a9604f-1ef1-00f2-3b30-4fdb4c120da6.png">

# PoseNet を 組み込む
index.html に [PoseNet](https://github.com/tensorflow/tfjs-models/tree/master/posenet) サンプルを参考に組み込む。

```(public/index.html)
...
      function posenetOnLoad() {
        const img = new Image();
        img.crossOrigin = "anonymous";
        img.src = 'https://placekitten.com/300/300';
        console.log("img", img);
        posenet.load().then(function(net) {
          console.log("net", net);
          const pose = net.estimateSinglePose(img, {flipHorizontal: true});
          return pose;
        }).then(function(pose){
          console.log('pose', pose);
        })
      }

      document.addEventListener('DOMContentLoaded', function() {
        posenetOnLoad();
....
```

# Firebase 開発サーバを起動してブラウザで確認する
javascript コンソールに スコア などが表示される事を確認する。

```
firebase serve
```
`http://localhost:5000/` 

<img width="522" alt="スクリーンショット 2019-12-02 23.50.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100147/d2830c33-b29b-f03b-6177-4fdec38d9f91.png">

# PoseNet で姿勢推定
PoseNetが組み込めたので カメラの画像を元に姿勢推定するコードに書き換えてみます。

## ボタンと vide と canvas タグを追加する。

```(public/index.html)
<style>
#container {
 position: relative;
}
#video,#overlay {
 position: absolute;       
}
</style>
...
<button id='btn' onclick="clickBtn()">カメラ</button>
<div id='container'>
 <video id='video'></video>
 <canvas id='overlay'></canvas>
</div>
```

## ボタンで カメラを開始して、video タグに表示させるコードを追加する。

```(public/index.html)
...
const SIZE = {width:640,height:480};
var _stream = null;
var video = null;
function clickBtn(){
  console.log('clickBtn');
  if (_stream){
    _stream.getVideoTracks()[0].stop();
    _stream = null;
  }
  else{
    const container = document.getElementById('video');
    video = document.getElementById('video');
    overlay = document.getElementById('overlay');

    // 必須 width, height を設定する        
    container.width = SIZE.width;
    container.height = SIZE.height;
    video.width = SIZE.width;
    video.height = SIZE.height;
    overlay.width = SIZE.width;
    overlay.height = SIZE.height;
    navigator.mediaDevices.getUserMedia({audio:false,video:SIZE})
      .then((stream) => {            
        _stream = stream;
        video = document.getElementById('video');
        video.srcObject = stream;
        video.play();
        video.onloadedmetadata = () => {
          // onloadedmetadata が発生後に 描画 処理をする
          drawLoop();
        }
    });
  }
}

function drawLoop() {
  requestAnimationFrame(drawLoop);
}
...
```

## 実行してカメラ表示がでるか確認する。

```
firebase serve
```
`http://localhost:5000/`
<img width="359" alt="スクリーンショット 2019-12-03 0.53.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100147/2e53b837-3874-7fa8-16ae-6e22442cbdb1.png">

## PoseNet に video タグ データを設定して初期化する

```(public/index.html)
var _stream = null;
var _net = null;
...
function dwawPose(pose) {
  console.log('pose', pose);
}

function drawLoop() {
  if (_stream === null) return;
  const imageScaleFactor = 0.2;
  const flipHorizontal = true;
  const outputStride = 16;
  const pose = await _net.estimateSinglePose(video, imageScaleFactor, flipHorizontal, outputStride);
  dwawPose(pose);
  requestAnimationFrame(drawLoop);
}

function posenetOnLoad() {
  posenet.load().then((net) => {
    _net = net;
  });
}

document.addEventListener('DOMContentLoaded', function() {
  posenetOnLoad();
...
```
<img width="394" alt="スクリーンショット 2019-12-03 1.08.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100147/deb8f82a-e66a-3242-f6c1-6916dba08fe2.png">


## ポイントを オーバーレイ 表示させてみる。

`dwawPose` に変更をして両腕に印をオーバーレイ描画してみる

```
function drawWristPoint(wrist, ctx, color){
  // console.log('wrist', wrist.position);
  ctx.beginPath();
  ctx.arc(wrist.position.x , wrist.position.y, 6, 0, 2 * Math.PI);
  ctx.fillStyle = color;
  ctx.fill();
}

function dwawPose(pose) {
  console.log('pose', pose);
  ctx.clearRect(0, 0, 640, 480);
  ctx.drawImage(video, 0, 0, 640, 480);

  [pose].forEach(({ score, keypoints }) => {
      // keypoints[9]には左手、keypoints[10]には右手の予測結果が格納されている 
      drawWristPoint(keypoints[9],ctx, 'red');
      drawWristPoint(keypoints[10],ctx, 'blue');
  });
}
```

```
TODO
```

# Firebase Store に動画をアップロード & 画面に配置

## Store のルールを変更する
アップロードしたファイルにアクセスできるようにする為 Store ルールを変更します。

<img width="314" alt="スクリーンショット 2019-12-05 12.13.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100147/16661bf5-2c4b-d0a7-40c4-e4740987f4e5.png">

```
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read;
      allow write: if request.auth != null;
    }
  }
}
```
## 動画をアプロードして URLを取得する
ファイルをアップロードすると 画像のような 青いリンクが表示されるのでそのURLをコピーする。

<img width="511" alt="スクリーンショット 2019-12-05 12.18.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100147/926f3f7f-000d-bee0-1a17-beb6f91439e2.png">
　

## アップロード と URL 取得手順
## 画面へ配置
```
<video id='effect' src='https://firebasestorage.googleapis.com/v0/b/walpurgisnacht-f96b1.appspot.com/o/nc195768.mp4?alt=media&token=5873c747-a5f7-4d29-a1ae-7fa8797b3450' autoplay></video>
```

# 特定の位置に腕を置いた時 エフェクトを再生する
左肘近くに右手をもっていくとエフェクト再生させる。

```
// エフェクト再生 ポーズ判定
function effectPose(rightWrist, leftElbow, size) {
  var result = false;
  if((leftElbow.position.x - size)  <= rightWrist.position.x && rightWrist.position.x <= (leftElbow.position.x + size) &&
      (leftElbow.position.y - size) <= rightWrist.position.y && rightWrist.position.y <= (leftElbow.position.y + size)){
      result = true;
  }
  // console.log('effectPose x', rightWrist.position.x, leftElbow.position.x);
  // console.log('effectPose y', rightWrist.position.y, leftElbow.positiony);
  return result;
}
....
[pose].forEach(({ score, keypoints }) => {
    // keypoints[9] 左手首、keypoints[10] 右手首
    drawWristPoint(keypoints[9],ctx, 'red');
    drawWristPoint(keypoints[10],ctx, 'blue');
    // 左肘 keypoints[7]
    drawWristPoint(keypoints[7],ctx, '#0ff');

    // 右手首,左肘 が近くにあるか確認する
    if (effectPose(keypoints[10],keypoints[7], 20)) {
      effect.style.display = 'block';
      effect.play();
    }
});
....
```

# まとめ
腕の重なり等で ターゲット外れるようで 厨二病 にはなれませんでした…。
カメラからの距離や位置をうまく調整すると、ジェスチャーゲーム作れる気がします。

簡単に動かせたので、ライブラリ動かす事から初めて機械学習, TensorFlowを学んでいくのはいいのかもしれないと感じました。

# 参考
[PoseNetを使ってジェスチャーゲームを作る](https://developers.gnavi.co.jp/entry/posenet/hasegawa)

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