LoginSignup
23

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-08-03

今回、とみーさんと一緒にピクトグラムさんになれるアプリを開発したので、そのことについて書きます。

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

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

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

機能紹介

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

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

ピクトグラム化する部分でTensorFlow.jsのpose-detectionというモデルを使っています。

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

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

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

TensorFlowを使って画像にマスクをかけるアプリバーチャル背景をつけるアプリを作ったときにも書いたのですが、

$ 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)を読み込みます。
必要に応じて、アーキテクチャなどを指定することができます。

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

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

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

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

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

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

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

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

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

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

  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()
    // ここに必要な処理
  }

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

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

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

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

      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だと言われるエラーに悩まされました。笑

      webcam.width = webcam.videoWidth
      webcam.height = webcam.videoHeight

カメラの切り替えを行う

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

  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です。

参考

こちらは高橋さんのアイデアをもとにしています。

あとがき

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

===

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

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23