猛暑です。
机で仕事、勉強するのに、扇風機はかかせません。
しかしながら、扇風機のスイッチのオンオフには、仕事や勉強を一時中断しないといけないという問題があります。
そんな1分1秒でも惜しい現代人へ、
顔の表情で扇風機を操作するシステムを作りました。
##作ったもの
表情で操作する扇風機を作りました。暑い顔で扇風機が回り、スマイルで止まります。 pic.twitter.com/pmweAbNZus
— たつや @8/27 Developers Summit 2020 KANSAI (@tatsuya1970) August 19, 2020
**暑くてつらそうな顔**をしてるときに、モーターが回ります。
なぜか笑顔で消えます。
##必要なもの
・obniz board
・DCモーター
・プロペラ
##使用技術
###顔認証について
face-api.js というTensorFlow.jsで学習済みの顔認識の機械学習モデルが使えるライブラリを利用しました。
本件では、顔の検出、顔のランドマーク付、表情分析で活用しました。
なお、face-api.jsは、サービスアカウント不要、アクセストークン・シークレットキー不要、無料で手軽に利用できます。
1.導入手順
・ 以下のリンクからZIPファイルをダウンロード (Cloneでも可)
https://github.com/justadudewhohacks/face-api.js/
・ distフォルダにある face-api.js を自分のアプリと同じディレクトリに移動する
・ weightsフォルダの名前を modelsに変更し、自分のアプリと同じディレクトリに移動する
models
face-api.js
index.html
script.js
2.判定方法
映像から以下の7つの表情の度合を返してくれます。
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
<!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>
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 () => {
//1つの顔だけなので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>
<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>
##余談
当初は、以下の写真のように、実際の卓上扇風機を分解してobnizと接続しました。
obniz 5V・170mA、卓上扇風機5V・350mA、扇風機を動かすには電力不足なので、単3乾電池を2本つなげました。
おお、動いた! pic.twitter.com/XMaQkf2LJR
— たつや @8/27 Developers Summit 2020 KANSAI (@tatsuya1970) August 14, 2020
動いたからヨシ!と思ったら、
Twitterで、ある方から
モーター用の電源をマイコン電源とは別に用意して、トランジスタで駆動するのがよくある回路です。@tatsuya1970 さんの回路はモーター用の電流が直接マイコンに流れるので、(Obnizならある程度耐えられるとは思いますが)マイコンが発熱したりする危険があります。 pic.twitter.com/VR2DofkC7l
— ぺんた (@plageoj) August 14, 2020
と、ご指摘いただきました。
ご親切にありがとうございました。
動いたら ヨシ!ではなくて、
動いたからといって良くないことが分かりました。
また、別の方からFacebookで、理屈とブレッドボードの実装方法について書かれているリンク先を教えていただきました。
- [FETによるモータ駆動]
(http://irobot.csse.muroran-it.ac.jp/html-corner/robotMaker/themes/motorDriveWithFET/index.html) - MOSFETとDCモーターの使い方 [Arduino]
ありがとうございました。
いろいろな方が私のようなド素人に教えていただき、SNSって本当にいいものですね。
ただ、トランジスタが手元にないので、とりあえず今回はモーターと簡易なプロペラにしました。
卓上扇風機の制御については、トランジスタを入手して、もし成功できたら、記事にしたいと思います。