【忙しい現代人のために】表情で扇風機を操作するシステムを作ったよ


猛暑です。

机で仕事、勉強するのに、扇風機はかかせません。

しかしながら、扇風機のスイッチのオンオフには、仕事や勉強を一時中断しないといけないという問題があります。

そんな１分１秒でも惜しい現代人へ、
顔の表情で扇風機を操作するシステムを作りました。


##作ったもの


<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">表情で操作する扇風機を作りました。暑い顔で扇風機が回り、スマイルで止まります。 <a href="https://t.co/pmweAbNZus">pic.twitter.com/pmweAbNZus</a></p>&mdash; たつや @8/27 Developers Summit 2020 KANSAI (@tatsuya1970) <a href="https://twitter.com/tatsuya1970/status/1296081106328936455?ref_src=twsrc%5Etfw">August 19, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>



<br>
**暑くてつらそうな顔**をしてるときに、モーターが回ります。

なぜか**笑顔**で消えます。



<br>
##必要なもの

・[obniz board](http://akizukidenshi.com/catalog/g/gM-13685/)
・[DCモーター](http://akizukidenshi.com/catalog/g/gP-09169/)
・プロペラ


##使用技術

###システム構成図
![system.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291592/84d0b96e-067a-e2ab-e833-f9c4d489da86.jpeg)



###顔認証について
[face-api.js](https://github.com/justadudewhohacks/face-api.js/) というTensorFlow.jsで学習済みの顔認識の機械学習モデルが使えるライブラリを利用しました。
本件では、顔の検出、顔のランドマーク付、表情分析で活用しました。

なお、face-api.jsは、サービスアカウント不要、アクセストークン・シークレットキー不要、無料で手軽に利用できます。
<br>
**１．導入手順**
・ 以下のリンクからZIPファイルをダウンロード (Cloneでも可）<br>https://github.com/justadudewhohacks/face-api.js/
・ distフォルダにある face-api.js を自分のアプリと同じディレクトリに移動する
・ weightsフォルダの名前を modelsに変更し、自分のアプリと同じディレクトリに移動する

```
models
face-api.js
index.html
script.js
```
<br>

**２．判定方法**
映像から以下の７つの表情の度合を返してくれます。
angry（怒り）、disgusted（うんざり）、fearful（恐れ）、happy（幸せ）、neutral（ニュートラル）、sad（悲しみ）、surprised（驚き）

いろいろ試した結果、neutral（ニュートラル）の数値が0.9以下で判定するのが、今のところベターでした。
扇風機の電源を消すには、なんでもよかったのですが、ハッピーな顔（happy指数が0.3超）というところに落ち着きました。




###obniz

obniz公式のモーターを動かすサンプルを使いました。
https://obniz.io/ja/sdk/parts/DCMotor/README.md


##コード
###パソコン側（顔認証）
ほとんどこちらのチュートリアルどおりで、obnizへPOST通信する処理を付け加えました。
https://github.com/WebDevSimplified/Face-Detection-JavaScript


```HTML:index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>表情で扇風機をまわすシステム</title>
  <script defer src="face-api.js"></script>
  <script defer src="script.js"></script>
  <style>
    body {
      margin: 0;
      padding: 0;
      width: 100vw;
      height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    canvas {
      position: absolute;
    }
  </style>
</head>
<body>
  <video id="video" width="720" height="560" autoplay muted></video>
</body>
</html>
```


```JS:script.js
const video = document.getElementById('video')

Promise.all([
  faceapi.nets.tinyFaceDetector.loadFromUri('/models'),
  faceapi.nets.faceLandmark68Net.loadFromUri('/models'),
  faceapi.nets.faceRecognitionNet.loadFromUri('/models'),
  faceapi.nets.faceExpressionNet.loadFromUri('/models')
]).then(startVideo)

function startVideo() {
  navigator.getUserMedia(
    { video: {} },
    stream => video.srcObject = stream,
    err => console.error(err)
  )
}

video.addEventListener('play', () => {
  const canvas = faceapi.createCanvasFromMedia(video)
  document.body.append(canvas)
  const displaySize = { width: video.width, height: video.height }
  faceapi.matchDimensions(canvas, displaySize)

  fan_status = 0
  send_obniz = 0

  setInterval(async () => {
    //１つの顔だけなのでfaceapi.detectAllFacesではなくて detectSingleFaceでよいはずが、本件はdetectAllFacesを使った。
    const detections = await faceapi.detectAllFaces(video, new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks().withFaceExpressions()
    
    const resizedDetections = faceapi.resizeResults(detections, displaySize)
    canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height)
    faceapi.draw.drawDetections(canvas, resizedDetections)
    faceapi.draw.drawFaceExpressions(canvas, resizedDetections)

    console.debug("resizedDetections",resizedDetections)
    if (resizedDetections[0] != null) {
       let happy = resizedDetections[0].expressions.happy
       let neutral = resizedDetections[0].expressions.neutral
       
       console.debug("neutral",neutral,"happy",happy)

       //表情がニュートラルじゃないときに扇風機をつける
       if (fan_status == 0 && neutral < 0.9) {
         fan_status = 1
         send_obniz = 1
        }
        //表情がハッピーのときに扇風機を消す
        if (fan_status == 1 && happy > 0.3) {
          fan_status = 0
          send_obniz = 1
         }

         //obnizクラウドへPOST
         if (send_obniz == 1){
          send_obniz = 0
          let value=[{"value":fan_status}];
          const url="https://obniz.com/events/0000/XXXXXXXXXXXXXX/run"; //ここにobnizのURLを入力
 
          Promise.all(post(value,url))   
           .then((result) => {})
           .catch((result) => {})       
         }  
    }
  
  }, 3000)

})

//POST通信  ここのを丸写し　https://www.it-swarm.dev/ja/javascript/%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%81%AA%E3%81%97%E3%81%A7post%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E9%80%81%E4%BF%A1%E3%81%99%E3%82%8B%E7%B4%94%E7%B2%8B%E3%81%AAjavascript/972618857/
function post(value,url) {
  let xhr = new XMLHttpRequest();
  xhr.open("POST", url, true);
  xhr.setRequestHeader('Content-Type', 'application/json');
  xhr.send(JSON.stringify({
    value: value
  }));
}

```

###oznizクラウド側
```HTML:index.html
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
    />
    <link rel="stylesheet" href="/css/starter-sample.css" />
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script
      src="https://unpkg.com/obniz@3.7.1/obniz.js"
      crossorigin="anonymous"
    ></script>
  </head>
  <body>
    
    <script>
      const obniz = new Obniz("XXXX-YYYY");//obnizのID

      obniz.onconnect = async function() {

        let motor = obniz.wired("DCMotor", {forward:0, back:1});

        if (req.body.value[0].value=="1") {
          obniz.display.clear();
          obniz.display.print("ON");
          motor.forward();
        }
        if (req.body.value[0].value=="0") {
          obniz.display.clear();
          obniz.display.print("OFF");
          motor.stop();
        }

      }

    </script>
  </body>
</html>

```


<br>
##余談

当初は、以下の写真のように、実際の卓上扇風機を分解してobnizと接続しました。
obniz 5V・170mA、卓上扇風機5V・350mA、扇風機を動かすには電力不足なので、単３乾電池を２本つなげました。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">おお、動いた！ <a href="https://t.co/XMaQkf2LJR">pic.twitter.com/XMaQkf2LJR</a></p>&mdash; たつや @8/27 Developers Summit 2020 KANSAI (@tatsuya1970) <a href="https://twitter.com/tatsuya1970/status/1294271878979178497?ref_src=twsrc%5Etfw">August 14, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<br>
動いたからヨシ！と思ったら、
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291592/f9873846-1cbe-a843-0ce5-5fcb12bbdab3.png" width=50%>

Twitterで、ある方から
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">モーター用の電源をマイコン電源とは別に用意して、トランジスタで駆動するのがよくある回路です。<a href="https://twitter.com/tatsuya1970?ref_src=twsrc%5Etfw">@tatsuya1970</a> さんの回路はモーター用の電流が直接マイコンに流れるので、（Obnizならある程度耐えられるとは思いますが）マイコンが発熱したりする危険があります。 <a href="https://t.co/VR2DofkC7l">pic.twitter.com/VR2DofkC7l</a></p>&mdash; ぺんた (@plageoj) <a href="https://twitter.com/plageoj/status/1294276121714757632?ref_src=twsrc%5Etfw">August 14, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

と、ご指摘いただきました。
ご親切にありがとうございました。

動いたら ヨシ！ではなくて、
動いたからといって良くないことが分かりました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291592/3ecefabf-e27e-e616-682d-d5123730e58e.png" width=50%>


また、別の方からFacebookで、理屈とブレッドボードの実装方法について書かれているリンク先を教えていただきました。
- [FETによるモータ駆動]
(http://irobot.csse.muroran-it.ac.jp/html-corner/robotMaker/themes/motorDriveWithFET/index.html)
- [MOSFETとDCモーターの使い方 [Arduino]](https://www.petitmonte.com/robot/howto_mosfet_motor.html)


ありがとうございました。
いろいろな方が私のようなド素人に教えていただき、SNSって本当にいいものですね。

ただ、トランジスタが手元にないので、とりあえず今回はモーターと簡易なプロペラにしました。

卓上扇風機の制御については、トランジスタを入手して、もし成功できたら、記事にしたいと思います。

<br>