ml5.jsでカメラ画像から姿勢推定を行う時にハマったこと

## はじめに
この記事では**JavaScript**のライブラリの1つ，**ml5.js**を利用した際のハマったことについて紹介したいと思います．

## ml5.jsとは
**ml5.js**を簡単に説明すると，簡単に機械学習に触れることが出来るライブラリと捉えています．
基本的な画像分類・文章生成から姿勢推定等といった様々な機械学習に触れることが出来ます．

> ml5.js aims to make machine learning approachable for a broad audience of artists, creative coders, and students. The library provides access to machine learning algorithms and models in the browser, building on top of TensorFlow.js with no other external dependencies.

今回はこの**ml5.js**を使って**姿勢推定**を利用した際のことについて記載しています．

（もっと詳細にml5.jsについて知りたい方に関しては公式ドキュメントやQiita等の記事を見ることをお勧めします）

https://ml5js.org/

https://qiita.com/ta9ma_0k/items/fff18b072fd75fd7aaf0

## 前提
### そもそも姿勢推定とは
姿勢推定とは人の画像から**関節点**を識別し，その情報からどのような姿勢・状態になっているかを推定する技術です．

![pose_estimation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/949737/8e91141b-a8ed-4b68-c5b1-27f40172343c.gif)
> 画像引用先：TensorFlow 「ポーズ推定  |  TensorFlow Lite」
> https://www.tensorflow.org/lite/examples/pose_estimation/overview

### 行いたいこと
今回**Webカメラ**を通じリアルタイムに姿勢を推定したく思っています．
また**どのようなカメラ形式（カメラ解像度）**でも正常に姿勢を推定することも必要だと考えています．

## 問題
**どのようなカメラ形式（カメラ解像度）**でも下記のようなコードにて姿勢推定を行いました．
今回のコードの特徴として，このような感じとなっています．
1. カメラの解像度に合わせてvideoタグの縦横を調整したいため**width・height**の値を**100%**にしている．
2. videoタグの縦横に合わせてcanvasタグの縦横を指定したいためにresizeForAll関数にてリサイズを行っている．

```javascript:react
let classifier;
let poses;

const App = () => {
    // useRef
    const videoRef = useRef();
    const canvasRef = useRef();

    // poseNet起動関数
    const poseNetPlay = () => {
      classifier = ml5.poseNet(videoRef.current, () => {
          console.log("PoseNet Loaded!");
          classifier.on('pose', result => {
              poses = result;
          });
      });

      resizeForAll();
    };

    // camara&canvasリサイズ関数
    const resizeForAll = () => {
        // videoの大きさにcanvasを合わせる
        canvasRef.current.width = videoRef.current.width;
        canvasRef.current.height = videoRef.current.height;
    };

    // 描画関数
    const CanvasOutput = (poses) => {
        const context = canvasRef.current.getContext('2d');

        // 初期化
        context.clearRect(0, 0, context.canvas.clientWidth, context.canvas.clientHeight);

        // この後に描画する処理を書いていますが略します
    };


    useEffect(() => {
        navigator.mediaDevices
            .getUserMedia({video: true, audio: false})
            .then((stream) => {
              videoRef.current.srcObject = stream;
              videoRef.current.play();
            });
    }, []);

    useInterval(() => {
        // canvasに描画する関数
        CanvasOutput(poses);
    }, 20);

    return(
        <div className="App">
              <div className="wrapper">
                <video id="video" ref={videoRef} height="100%" width="100%" />
                <canvas className="canvas" ref={canvasRef}  />
              </div>
              <button onClick={poseNetPlay} >aaa</button>
        </div>
    )
}

export default App;
```

出力結果として下記のように出力されました．

![qiita_pose_base.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/949737/7d6e4a1c-224a-f4e3-7730-27a1528ab3b4.png)

> フリー素材ぱくたそ（www.pakutaso.com）

姿勢推定が出来ていない…というよりは**なぜか上に小さく姿勢推定**が行われているように見えます．
ただこれだと本来の目的を満たしていないため，この問題を解決しなければいけません．


## 原因
### 原因究明
今回canvasの大きさをvideoに合わせるために下記のようにリサイズを行っています．

```javascript
    // camara&canvasリサイズ関数
    const resizeForAll = () => {
        // videoの大きさにcanvasを合わせる
        canvasRef.current.height = videoRef.current.height;
        canvasRef.current.width = videoRef.current.width;
    };
```

しかし，videoタグのwidth・heightに**カメラの解像度**は入っているのでしょうか。。。
```console.log（"videoRef.current.width, videoRef.current.height"）```
を見てみます．

![2021-10-10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/949737/9131a2b5-30e8-36ad-a310-ffcce2762669.png)

なんとvideoタグのwidth・heightを**100%**指定にしているのにもかかわらず，**カメラの解像度とは全く別の値**が入っていることが分かります．
また**width・height**の値を利用して姿勢推定の座標を推定しているため（**ここに関してはもしかしたら違うかもしれません**）width・heightの値がかなり重要となっています．
それではなぜ**width・height**にカメラの解像度が入っていないのでしょうか？

### 原因特定
要素には縦横（width・height）がありますが，**複数のwidth・height**がhtmlには存在することが理由です．

https://developer.mozilla.org/ja/docs/Web/HTML/Element/video

そしてカメラの解像度は**width・height**には入っておらず，全く別の値に入っています．そのためその値を**width・height**に代入する必要があります．

## 解決策
様々なアプローチを試した結果，videoタグの**ClientWidth・ClientHeight**にカメラの解像度が入ってることが分かりました．
そのため**ClientWidth・ClientHeight**の値をwidth・heightに代入することで正常に表示することが出来ます！

```javascript
    // camara&canvasリサイズ関数
    const resizeForAll = () => {
        // 追加した部分
        videoRef.current.width = videoRef.current.clientWidth;
        videoRef.current.height = videoRef.current.clientHeight;
        
        // videoの大きさにcanvasを合わせる
        canvasRef.current.width = videoRef.current.width;
        canvasRef.current.height = videoRef.current.height;
    }
```

書き換えた後の出力結果として下記のようになります．

![qiita_pose_success.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/949737/f27929b3-da9e-6d29-44e8-1011f74a0d62.png)

> フリー素材ぱくたそ（www.pakutaso.com）

きちんと**体の姿勢**（輪郭）に合わせて推定されていることが分かります！

## まとめ
今回はml5.jsでカメラ画像から姿勢推定を行う時にハマったこと，Webカメラを取り扱った際に起こったことについて記載しました！