オリンピックを盛り上げるためにピクトグラムさんになれるアプリを作った【Next.js+TypeScriptでTensorFlow.jsを使って姿勢検出】

今回、[とみーさん](https://twitter.com/tommy19970714)と一緒にピクトグラムさんになれるアプリを開発したので、そのことについて書きます。

頑張って作ったのでぜひぜひ利用してみてください！一緒にオリンピックを盛り上げましょう :thumbsup: 

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">東京五輪のピクトグラムさんになれるアプリをAIを使って作りました🎉<br>↓のURLですぐに遊べます！<a href="https://t.co/U3rdsKRaRp">https://t.co/U3rdsKRaRp</a><br><br>ぜひピクトグラムさんになって東京五輪を応援しましょう！頑張って作ったのでいいねやRTで広めてください😂<br><br>Everyone can be Pictogram-san!<a href="https://twitter.com/hashtag/Tokyo2020?src=hash&amp;ref_src=twsrc%5Etfw">#Tokyo2020</a><a href="https://twitter.com/hashtag/pictogram_challenge?src=hash&amp;ref_src=twsrc%5Etfw">#pictogram_challenge</a> <a href="https://t.co/4fhb0StjaB">pic.twitter.com/4fhb0StjaB</a></p>&mdash; Toshiki Tomihira (@tommy19970714) <a href="https://twitter.com/tommy19970714/status/1422010924882296834?ref_src=twsrc%5Etfw">August 2, 2021</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

アプリはこちら→https://pictogram-san.com/
github→https://github.com/tommy19970714/pictogram-san

面白いなと思ったらLGTMしてもらえると励みになります :pray: 

## 機能紹介

今回、実装した機能は以下です。

- webcamで取得した映像をピクトグラム化する
- 画面の縦幅を1/2にして、それぞれ上をピクトグラム、下を映像にして流す
- 再生ボタンを押すと音楽を流す
- 音楽が終了したタイミングでその画面のスクリーンショットを撮る

ピクトグラム化する部分で[TensorFlow.jsのpose-detection](https://github.com/tensorflow/tfjs-models/tree/master/pose-detection)というモデルを使っています。

全体の部分はNext.js + TypeScriptで組んでいます。

技術的なことはすべて書ききれないので、ポイントとなる部分だけ書くことにします。

## Next.js + TypeScriptでTensorFlow.jsを使って姿勢検出する

[TensorFlowを使って画像にマスクをかけるアプリ](https://qiita.com/yuikoito/items/b8062b9f89f72c6865ed)や[バーチャル背景をつけるアプリ](https://qiita.com/yuikoito/items/bb9c40bbb673e4f71abe)を作ったときにも書いたのですが、

```
$ yarn add @tensorflow/tfjs
```

とするとTypeScriptでは型エラーが出てしまうので、以下のように必要なものだけ読み込む必要があります。

```
$ yarn add @tensorflow-models/pose-detection @tensorflow/tfjs-core @tensorflow/tfjs-converter @tensorflow/tfjs-backend-webgl
```

使うモデルがwasmベースの場合は、``@tensorflow/tfjs-backend-webgl``の部分が``@tensorflow/tfjs-backend-wasm``になります。

そして今回使うモデル（PoseNet）を読み込みます。
必要に応じて、アーキテクチャなどを指定することができます。

```react
    const modelName = SupportedModels.PoseNet
    const net = await createDetector(modelName, {
      quantBytes: 2,
      architecture: 'MobileNetV1'
      outputStride: 16,
      inputResolution: resolution,
    })
```

アーキテクチャはMobileNetV1とResNet50の2種類があるのですが、ResNet50にすると精度は高くなるものの、かなり重たてスマホでは特に重たくなるので、今回はMobileNetV1を使っています。

モデルが読み込めたら、estimatePosesの引数に画像か動画データを入れます。
今回だとwebcamのデータをそのまま入れています。

```react
      const predictions = await net.estimatePoses(webcam, {
        maxPoses: 1,
        flipHorizontal: false,
      })
```

後は検出したpredictionsを利用して、好きなように描画すればOKです。

## 画面を起動すると同時にカメラを起動して姿勢検出を開始する

webcamの情報を使うためにはvideoタグが読み込まれるまで待つ必要があります。
そのため、以下のように書く必要があります。

```react
if (webcamRef.current.video.readyState === 4) {
 // ここにTensorFlow.jsを使う処理
}
```

ただ、ページが読み込まれる際にはreadyStateが4でないため、この条件文を付けただけだと、この処理部分がパスされてしまい、永遠に姿勢検出ができません。

そのため、以下のコードでreadyStateが4になるまで待つようにしました。

```react
  const handleLoadWaiting = async () => {
    return new Promise((resolve) => {
      const timer = setInterval(() => {
        if (webcamRef.current?.video?.readyState === 4) {
          resolve(true)
          clearInterval(timer)
        }
      }, 500)
    })
  }

  const handleStartDrawing = async () => {
    await handleLoadWaiting()
    // ここに必要な処理
  }

```

ちなみにこの部分は前に私が作った[マスクをつけるアプリ](https://mask-app-one.vercel.app/)の場合はuseEffectで対応してました。（ローディングがないので最初マスクが表示されなくて戸惑うかと思いますが、待ってれば数秒後にマスクがつきます。）

```react
  useEffect(() => {
    runFaceDetect();
  }, [webcamRef.current?.video?.readyState])
```

今回はhandleLoadWaitingで読み込みが完了するまで待って次に進むという書き方の方が使いやすかったのでそうしてます。

また、この際に、入力のサイズと出力のサイズを指定する必要があるので、その部分だけ注意が必要です。

```react
      const webcam = webcamRef.current.video as HTMLVideoElement
      const canvas = canvasRef.current
      webcam.width = webcam.videoWidth
      webcam.height = webcam.videoHeight
      canvas.width = webcam.videoWidth
      // 今回は描画用canvasはビデオ部分の2倍の高さにする必要があるので2倍にしてる
      canvas.height = webcam.videoHeight * 2
```

最初、入力値の以下部分の幅を指定してなかった関係で、入力値の幅が0だと認識されてしまい、roiが0だと言われるエラーに悩まされました。笑

```react
      webcam.width = webcam.videoWidth
      webcam.height = webcam.videoHeight
```


## カメラの切り替えを行う

インカメラとリアカメラの切り替えはwebcamに渡している情報のfacingModeを'user'と'environment'で切り替えるだけです。

```react
  const [facingMode, setFacingMode] = useState<'user' | 'environment'>('user')
  const videoConstraints = {
    width: // 幅を指定,
    height: // 高さを指定,
    facingMode: facingMode,
  }

// webcamでは以下のように渡す

          <Webcam
            audio={false}
            mirrored={true}
            videoConstraints={videoConstraints}
            ref={webcamRef}
          />

```

後はボタンクリックなどでfacingModeの値を切り替えたらOKです。

## 参考

こちらは[高橋さん](https://twitter.com/KzhtTkhs)のアイデアをもとにしています。

https://github.com/Kazuhito00/Tokyo2020-Pictogram-using-MediaPipe


## あとがき

今回のアプリは[とみー](https://twitter.com/tommy19970714)さんを中心として、[わせりん](https://twitter.com/_waserin)さん、[西川](https://twitter.com/nishikawa_kohei)さん、[mikkame](https://twitter.com/mikkameee)さんと一緒に作り上げました。
2日間ずっとdiscordつなぎながらハッカソンのように開発しましたが、なかなか楽しかったので、またやりたいなと思ってます。

===

これで11週目の週イチ発信となりました。
今回ちょうどとみーさんからお誘いをもらって一緒に作ることになりました。
仕事以外で誰かと一緒に開発するというのがなかったこの2ヶ月半だったので、非常に楽しかったです。

良ければこれまでの週イチ発信も見て下さい！
ではでは〜。

- [【React + Typescriptで顔認識】tensorflowを使って画像にマスクをかけるアプリを作った](https://qiita.com/Yui_active/items/b8062b9f89f72c6865ed)
- [【React + Typescript】ボタン一つでコンポーネントのscssをコピーできるサイトを作った](https://qiita.com/Yui_active/items/1288fed0dcc24c71e159)
- [【アップデート】ui-componentsに18個のコンポーネントを追加した](https://qiita.com/yuiko_ito/items/c1d0599e1e7da7fa42e0)
- [【Nuxt.js × Tailwind CSS】ボタン一つで有名絵画風の画像にできるサービスをリリースした！](https://qiita.com/yuikoito/items/126b0e2d36a707bfd36e)
- [【GASでLINE Bot作成】現在地の近くのおすすめのごはん屋さんを教えてくれるLINE Botを作った](https://qiita.com/yuikoito/items/c1aef147cef4458f9d2e)
- [【動的OGP】Next.js + TypeScript + Vercelデプロイで動的OGPを実現する](https://qiita.com/yuikoito/items/619120c592d99f9d3053)
- [【LambdaでOpenCVを利用】AWSとOpenCVを利用してポケモン画像でアスキーアート風に変換するAPIを作った](https://qiita.com/yuikoito/items/7dcf1deeed46f46b2f2e)
- [ポケモン画像でアスキーアート風に変換するwebアプリを作った](https://qiita.com/yuikoito/items/da5954a5d78da9de6a4e)
- [ボタン一つで漫画風の画像にできるサービスを作った](https://qiita.com/yuikoito/items/a76ea5bf05f04a44df8d)
- [【Next.js + TensorFlowでweb cameraにバーチャル背景をつける】バーチャル旅行を体験できるアプリを作った](https://qiita.com/yuikoito/items/bb9c40bbb673e4f71abe)