バーチャルブーブークッションを作りたかった

どうも、フリーランスエンジニアの黒神（[@kokushing](https://twitter.com/kokushing)）と申します。
[クソアプリ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/kuso-app) 22日目の投稿です。

## 作ったもの

顔を認識すると屁を放つクソアプリです。

[booboo-app](https://kokushin.github.io/booboo-app/)

## 動作デモ

※Mac版Chromeにて動作確認

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">顔認識するとオナラが放たれるクソアプリ作った <a href="https://t.co/RVJTzCPqhX">pic.twitter.com/RVJTzCPqhX</a></p>&mdash; 黒神 / Yusuke Ishiguro (@kokushing) <a href="https://twitter.com/kokushing/status/1208351140468805633?ref_src=twsrc%5Etfw">2019年12月21日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## 経緯

どんなクソアプリを作ってやろうかなとトイレで考えてたとき、ふとひらめいたんですよね。
__「バーチャルブーブークッションを作ろう」__と。

ブーブークッションとは、空気の入った袋のようなものの上に座ることで、「ﾌﾞｩｰｯ」っと妙にリアルな屁の音が鳴るあのジョークグッズです。

実装に関して、IoT的に物体を用意するのは難易度高そうだし、なにか手軽に実装できそうな手段は無いか考えた結果、__「PCの前に着席したときに顔認識して音出せば良いんじゃね？」__という天才的アイデアを思いつきました。

## 実装

顔認識には[face-api.js](https://github.com/justadudewhohacks/face-api.js/)という[tensorflow.js](https://www.tensorflow.org/js?hl=ja)を用いたJavaScriptライブラリを利用しました。

顔認識といえばPythonとかOpenCVとかのイメージが強いんですが、なんとこのJavaScriptライブラリ、単体でリアルタイムな顔認識を行うことができるんですねー。
精度も高いしAPIも豊富なのでかなり使い勝手の良いライブラリだと思います。

ただ、モデルデータが20MBくらいあるので軽量とは言えませんけどね。
（同系統のJSライブラリ「[pico.js](https://github.com/tehnokv/picojs)」は200行程度の超軽量ですが精度がイマイチでした）

フロントのフレームワークにはReact.jsを使用しました。
最近Hooks APIで書くことが多いので、その練習も兼ねています。
（クソアプリアドベントカレンダーは新しい技術を試すのにもってこいの神イベントですね）

ソースコードはこんな感じです。

```jsx
import React, { useState, useEffect, useRef } from "react";
import * as faceapi from "face-api.js";
import "./App.css";

function App() {
  const [audio, setAudio] = useState(null);
  const [isCompleted, setIsCompleted] = useState(false);
  const videoEl = useRef(null);

  const onPlay = () => {
    const options = new faceapi.TinyFaceDetectorOptions();
    const loop = setInterval(async () => {
      const result = await faceapi
        .detectSingleFace(videoEl.current, options)
        .withFaceLandmarks()
        .withFaceExpressions();

      if (result) {
        audio.play();
        setIsCompleted(true);
        clearInterval(loop);
      }
    }, 500);
  };

  useEffect(() => {
    (async () => {
      await faceapi.nets.tinyFaceDetector.loadFromUri(
        "https://kokushin.github.io/booboo-app/weights"
      );
      await faceapi.nets.faceLandmark68Net.loadFromUri(
        "https://kokushin.github.io/booboo-app/weights"
      );
      await faceapi.nets.faceExpressionNet.loadFromUri(
        "https://kokushin.github.io/booboo-app/weights"
      );

      navigator.mediaDevices
        .getUserMedia({
          audio: false,
          video: { width: 320 }
        })
        .then(stream => {
          videoEl.current.srcObject = stream;
          videoEl.current.play();

          setAudio(
            new Audio("https://kokushin.github.io/booboo-app/assets/audio.mp3")
          );
        });
    })();
  }, []);

  return (
    <div className="App">
      <h1>booboo-app</h1>
      <p>カメラに写った顔を認識すると音が出ます</p>
      {isCompleted ? (
        <>
          <p>放屁完了</p>
          <p>
            <img
              src="https://kokushin.github.io/booboo-app/assets/image.png"
              width="320"
              alt=""
            />
          </p>
          <button onClick={() => window.location.reload()}>Retry</button>
        </>
      ) : (
        <p>認識中...</p>
      )}
      <video ref={videoEl} onPlay={onPlay} style={{ display: "none" }} />
    </div>
  );
}

export default App;
```

最初にface-api.jsのモデルデータを読み込み初期化、その後Webカメラを起動、受け取ったvideoのオブジェクトをface-api.jsに渡し、setIntervalで500ms毎にvideo内に顔が検知されるまで監視を続けます。

顔が検知できたらresultがtrueになるので屁を放ち監視を終了します。

至ってシンプルな流れですね。

## 残念な点

Webカメラの起動から顔の認識まで若干時間がかかってしまうので、椅子に着席したタイミングの1秒後くらいに音が鳴ってしまいます。本当は着席と同時に出したかったんですけど己の技術力不足で厳しそうでした😇

## クソースコード

コード自体はMITライセンスです。

[kokushin/booboo-app](https://github.com/kokushin/booboo-app)

使用されている画像に関しては [いらすとや](https://www.irasutoya.com/2015/08/blog-post_149.html) 様
妙にリアルなオナラの音に関しては [Onara Sound MP3](https://onara-mp3.com/) 様 を利用しています。

実を出したい勢の方々、ご拡張ください。

<blockquote class="twitter-tweet" data-conversation="none" data-lang="ja"><p lang="ja" dir="ltr">オーバーライドして実も出させたい</p>&mdash; だーいー@フロントエンドエンジニア (@u_u_u_1_1_0_1) <a href="https://twitter.com/u_u_u_1_1_0_1/status/1208356741609476097?ref_src=twsrc%5Etfw">2019年12月21日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## まとめ

face-api.js、初めて使ったんですが手軽でいい感じでした。
次はもっと多くのAPIを用いてクソじゃないアプリを開発したいですね。

## 参考リンク

- [フック API リファレンス – React](https://ja.reactjs.org/docs/hooks-reference.html)
- [face-api.js docs](https://justadudewhohacks.github.io/face-api.js/docs/index.html)
- [ブラウザで顔、表情、パーツ位置の検出、誰かを当ててみる（face-api.js）](https://blog.capilano-fw.com/?p=4063)