Satomiのアイコン画像

目覚ましマウスパッド

Satomi 2021年05月15日に作成  (2021年05月16日に更新)

目覚ましマウスパッド

概要

ブラウザ上でWebカメラを使用し、Face-api.jsを用いて眠気検知を行う。
寝ていることを検知したらWi-Fi経由でマウスパッドに搭載されたObnizに指令が送られ、目覚ましアクションが発生する。

目覚ましアクションは、眠気レベルによって変化する。

  • レベル1 空調の温度を下げる
  • レベル2 マウスパッドを振動させる
  • レベル3 ブザーを鳴らす
  • レベル4 ライトを消す(疲れているため、目覚ましを諦める)

デモ動画

ここに動画が表示されます

構成図と部品

ハードウェア構成図

ソフトウェア構成図

部品

  • obnizBoard 1個
  • Buzzer 1個
  • 赤外線受信モジュール 1個
  • 赤外線LED 1個
  • DCモーター 1個
  • 抵抗(10Ω)  1個
  • LEGOブロック
    LEGO ブロック 部品

配線とピン配置

配線

ピン配置

PIN No 用途
0 赤外線受信モジュール Output
1 赤外線受信モジュール Gnd
2 赤外線受信モジュール Vcc
3 赤外線LED Cathode
4 赤外線LED Anode(抵抗を介して接続。より遠くの機器を操作する場合は低めを推奨。)
5 ブザー Signal
6 ブザー Gnd
10 DCモーター forward
11 DCモーター back

機能説明

機能概要

目覚ましマウスパッドには、大きく分けて以下3つの機能が搭載されている。
各機能の詳細は、次節にまとめる。

  • 家電操作登録
     目覚ましに使う家電の赤外線データの登録
  • 眠気検知
     瞼の動きから眠気レベルを検知
  • 目覚ましアクション
     眠気レベルに応じたアクションの実行

機能詳細

[家電操作登録] について

ブラウザ上の登録画面から、赤外線受信モジュールに向けてリモコンを操作することで家電の各操作の赤外線データを登録できる。
赤外線データ登録画面

登録状態遷移

obniz:赤外線受信モジュール、赤外線LED、ブザー

[眠気検知] について

WebカメラとFace-api.jsで実現。
動画 : https://youtu.be/gzsYHE1Xhwg
キャプションを入力できます

眠気検知方法はFace-api.jsを用いて目の特徴点を抽出。
【参考】https://github.com/justadudewhohacks/face-api.js/
目の開閉判定は以下記事を参考にした。
【参考】https://www.pyimagesearch.com/2017/04/24/eye-blink-detection-opencv-python-dlib/

目の閉じている時間から、眠っているか否かの判定をする。
眠っていると判定された回数が多くなるにつれ、眠気レベルを上げていく。

[目覚ましアクション] について

  • レベル1 空調の温度を下げる
     登録済みの「エアコン ON」と「温度 下げる」アクションを実行する

  • レベル2 マウスパッドを振動させる
     DCモーターを正逆交互に回転させ、レゴブロックで作ったラック・アンド・ピニオンを動作させる。
     動画:https://youtu.be/2pcjLCafXPY
    LEGO 組み立て手順
    LEGO 振動機構

  • レベル3 ブザーを鳴らす
     100Hz指定で、ブザーを再生

  • レベル4 ライトを消す
     登録済みの「ライト OFF」アクションを実行する

ソースコード

Main(GUI+眠気検知+Obniz操作)

<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" /> <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script> <script src="https://unpkg.com/obniz@3.x/obniz.js" crossorigin="anonymous" ></script> <script src="https://unpkg.com/obniz-parts-kits@0.16.0/ui/index.js"></script> <script src="https://obniz.com/users/5143/repo/storage.json"></script> <script src="https://obniz.com/users/5143/repo/register_data.js"></script> <script src="https://obniz.com/users/5143/repo/face-api.min.js"></script> </head> <body> <!-- webカメラ画面表示 --> <div style="position: relative"> <video id="video" onloadedmetadata="onPlay(this)" muted autoplay width="640" height="480"></video> <canvas id="facecanvas" style=" position: absolute; top: 0; left: 0;" width="640" height="480"></canvas> </div> <div id="obniz-debug"></div> <div class="container"> <div class="text-center"> <h3>Sleepiness Detection</h3> <label>電源 ON : </label> <button class="btn btn-link" id="power_on_reg">確認中</button> <button class="btn btn-info" id="power_on_send">実行</button> <label id="power_on_sts"></label> <br> <label>電源 OFF : </label> <button class="btn btn-link" id="power_off_reg">確認中</button> <button class="btn btn-info" id="power_off_send">実行</button> <label id="power_off_sts"></label> <br> <label>ライト ON : </label> <button class="btn btn-link" id="light_on_reg">確認中</button> <button class="btn btn-info" id="light_on_send">実行</button> <label id="light_on_sts"></label> <br> <label>ライト OFF : </label> <button class="btn btn-link" id="light_off_reg">確認中</button> <button class="btn btn-info" id="light_off_send">実行</button> <label id="light_off_sts"></label> <br> <label>エアコン ON : </label> <button class="btn btn-link" id="aircon_on_reg">確認中</button> <button class="btn btn-info" id="aircon_on_send">実行</button> <label id="aircon_on_sts"></label> <br> <label>エアコン OFF : </label> <button class="btn btn-link" id="aircon_off_reg">確認中</button> <button class="btn btn-info" id="aircon_off_send">実行</button> <label id="aircon_off_sts"></label> <br> <label>温度 上げる : </label> <button class="btn btn-link" id="thm_up_reg">確認中</button> <button class="btn btn-info" id="thm_up_send">実行</button> <label id="thm_up_sts"></label> <br> <label>温度 下げる : </label> <button class="btn btn-link" id="thm_down_reg">確認中</button> <button class="btn btn-info" id="thm_down_send">実行</button> <label id="thm_down_sts"></label> <br> </div> </div> <script> var obniz = new Obniz("OBNIZ_ID_HERE"); var ObnizId = document.getElementById('ObnizId'); var power_on_reg = document.getElementById('power_on_reg'); var power_on_send = document.getElementById('power_on_send'); var power_on_sts = document.getElementById('power_on_sts'); var power_off_reg = document.getElementById('power_off_reg'); var power_off_send = document.getElementById('power_off_send'); var power_off_sts = document.getElementById('power_off_sts'); var light_on_reg = document.getElementById('light_on_reg'); var light_on_send = document.getElementById('light_on_send'); var light_on_sts = document.getElementById('light_on_sts'); var light_off_reg = document.getElementById('light_off_reg'); var light_off_send = document.getElementById('light_off_send'); var light_off_sts = document.getElementById('light_off_sts'); var aircon_on_reg = document.getElementById('aircon_on_reg'); var aircon_on_send = document.getElementById('aircon_on_send'); var aircon_on_sts = document.getElementById('aircon_on_sts'); var aircon_off_reg = document.getElementById('aircon_off_reg'); var aircon_off_send = document.getElementById('aircon_off_send'); var aircon_off_sts = document.getElementById('aircon_off_sts'); var thm_up_reg = document.getElementById('thm_up_reg'); var thm_up_send = document.getElementById('thm_up_send'); var thm_up_sts = document.getElementById('thm_up_sts'); var thm_down_reg = document.getElementById('thm_down_reg'); var thm_down_send = document.getElementById('thm_down_send'); var thm_down_sts = document.getElementById('thm_down_sts'); var res_promise; const canvas = document.getElementById( 'facecanvas' ); const videoEl = document.getElementById( 'video' ); const ctx = canvas.getContext("2d"); ctx.font = "32px serif"; ctx.fillStyle = "Red"; const inputSize = 224; const scoreThreshold = 0.5; const options = new faceapi.TinyFaceDetectorOptions({ inputSize, scoreThreshold }); var PassSec = 0;   // 秒数カウント用変数 var WakeupCount = 0;   // 秒数カウント用変数 var flg = true; var sleepinessCount = 0; var sleepiness_level = 0; var power_off_flag = false; var thm_down_flag = false; var PassSeccount_flag = false; timerID = setInterval('countup()',1000); //1秒毎にcountup()を呼び出し const xml = new XMLHttpRequest(); xml.open("POST", "https://maker.ifttt.com/trigger/WakeupAlarm/with/key/d5nIKdxHkXiL3bjtNe6_t_", false); xml.setRequestHeader("content-type", "application/x-www-form-urlencoded;charset=UTF-8","Access-Control-Allow-Origin", "https://maker.ifttt.com/trigger/WakeupAlarm/with/key/d5nIKdxHkXiL3bjtNe6_t_"); function countup() { PassSec++; PassSeccount_flag = true; } async function onPlay() { if(videoEl.paused || videoEl.ended || !faceapi.nets.tinyFaceDetector.params) return setTimeout(() => onPlay()) // face-apiを使って, 両目の特徴点を取得 const result = await faceapi.detectSingleFace(videoEl, options).withFaceLandmarks() if (result) { const dims = faceapi.matchDimensions(canvas, videoEl, true) const resizedResult = faceapi.resizeResults(result, dims) const leftEye = result.landmarks.getLeftEye() const rightEye = result.landmarks.getRightEye() // EAR(Eye Aspect Ratio)を算出 var a_l = Math.sqrt( Math.pow( leftEye[1].x-leftEye[5].x, 2 ) + Math.pow( leftEye[1].y-leftEye[5].y, 2 ) ) ; var b_l = Math.sqrt( Math.pow( leftEye[2].x-leftEye[4].x, 2 ) + Math.pow( leftEye[2].y-leftEye[4].y, 2 ) ) ; var c_l = Math.sqrt( Math.pow( leftEye[0].x-leftEye[3].x, 2 ) + Math.pow( leftEye[0].y-leftEye[3].y, 2 ) ) ; var EAR_L = ( a_l + b_l ) / ( 2 * c_l ) ; var a_r = Math.sqrt( Math.pow( rightEye[1].x-rightEye[5].x, 2 ) + Math.pow( rightEye[1].y-rightEye[5].y, 2 ) ) ; var b_r = Math.sqrt( Math.pow( rightEye[2].x-rightEye[4].x, 2 ) + Math.pow( rightEye[2].y-rightEye[4].y, 2 ) ) ; var c_r = Math.sqrt( Math.pow( rightEye[0].x-rightEye[3].x, 2 ) + Math.pow( rightEye[0].y-rightEye[3].y, 2 ) ) ; var EAR_R = ( a_r + b_r ) / ( 2 * c_r ) ; var EAR = ( EAR_R + EAR_L ) / 2; ctx.fillText("眠気検知回数:" + sleepinessCount, 20, 20); ctx.fillText("レベル:" + sleepiness_level, 20, 40); ctx.fillText(flg, 20, 100); //目があいているフレーム数をカウント if(EAR > 0.33){ // 使用者によりチューニングが必要!! WakeupCount++; } //目があいているフレーム数をが5フレーム以上は起きている if(WakeupCount > 5){ PassSec = 0; WakeupCount = 0; } if(PassSec == 5){ WakeupCount = 0; if(PassSeccount_flag == true){ sleepinessCount ++ ; if(sleepinessCount < 2){ sleepiness_level = 1 ; }else if(sleepinessCount < 4){ sleepiness_level = 2 ; }else if(sleepinessCount < 6){ sleepiness_level = 3 ; }else if(sleepinessCount < 7){ sleepiness_level = 4 ; }else{ sleepiness_level = 5 ; } } ctx.fillText("SLEEP", 20, 120); flg = false; }else if(PassSec > 5){ ctx.fillText("SLEEP", 20, 120); }else{ ctx.fillText("AWAKE", 20, 120); flg = true; } } PassSeccount_flag = false; setTimeout(() => onPlay()) }; async function run(){ await faceapi.nets.tinyFaceDetector.load('https://obniz.com/users/5143/repo/') await faceapi.loadFaceLandmarkModel('https://obniz.com/users/5143/repo/') const stream = await navigator.mediaDevices.getUserMedia({ video: {} }) videoEl.srcObject = stream; } $(document).ready(function() { run(); }); // called on online obniz.onconnect = async function() { var sensor = obniz.wired('IRSensor', {vcc:2, gnd:1, output: 0}); var led = obniz.wired('InfraredLED', {anode: 4, cathode: 3}); var speaker = obniz.wired("Speaker", {signal:5 , gnd:6 }); var dcmotor = obniz.wired("DCMotor",{forward:10, back:11}); var power_on = []; var power_off = []; var light_on = []; var light_off = []; // Load console.log('Load Start'); var power_on_reg_data = new Register(obniz, "PowerOn" , sensor, led, power_on_reg , power_on_sts); var power_off_reg_data = new Register(obniz, "PowerOff" , sensor, led, power_off_reg , power_off_sts); var light_on_reg_data = new Register(obniz, "light_on" , sensor, led, light_on_reg , light_on_sts); var light_off_reg_data = new Register(obniz, "light_off" , sensor, led, light_off_reg , light_off_sts); var aircon_on_reg_data = new Register(obniz, "aircon_on" , sensor, led, aircon_on_reg , aircon_on_sts); var aircon_off_reg_data = new Register(obniz, "aircon_off" , sensor, led, aircon_off_reg , aircon_off_sts); var thm_up_reg_data = new Register(obniz, "thm_up" , sensor, led, thm_up_reg , thm_up_sts); var thm_down_reg_data = new Register(obniz, "thm_down" , sensor, led, thm_down_reg , thm_down_sts); power_on_reg_data.LoadData(); power_off_reg_data.LoadData(); light_on_reg_data.LoadData(); light_off_reg_data.LoadData(); aircon_on_reg_data.LoadData(); aircon_off_reg_data.LoadData(); thm_up_reg_data.LoadData(); thm_down_reg_data.LoadData(); $("#power_on_reg").click(function() { power_on_reg_data.RegisterData(); sensor.ondetect = function(arr) { power_on_reg_data.RegisterComp(arr); } }); $("#power_off_reg").click(function() { power_off_reg_data.RegisterData(); sensor.ondetect = function(arr) { power_off_reg_data.RegisterComp(arr); } }); $("#light_on_reg").click(function() { light_on_reg_data.RegisterData(); sensor.ondetect = function(arr) { light_on_reg_data.RegisterComp(arr); } }); $("#light_off_reg").click(function() { light_off_reg_data.RegisterData(); sensor.ondetect = function(arr) { light_off_reg_data.RegisterComp(arr); } }); $("#aircon_on_reg").click(function() { aircon_on_reg_data.RegisterData(); sensor.ondetect = function(arr) { aircon_on_reg_data.RegisterComp(arr); } }); $("#aircon_off_reg").click(function() { aircon_off_reg_data.RegisterData(); sensor.ondetect = function(arr) { aircon_off_reg_data.RegisterComp(arr); } }); $("#thm_up_reg").click(function() { thm_up_reg_data.RegisterData(); sensor.ondetect = function(arr) { thm_up_reg_data.RegisterComp(arr); } }); $("#thm_down_reg").click(function() { thm_down_reg_data.RegisterData(); sensor.ondetect = function(arr) { thm_down_reg_data.RegisterComp(arr); } }); // Send $("#power_on_send").click(async function() { power_on_reg_data.SendData(); }); $("#power_off_send").click(async function() { power_off_reg_data.SendData(); }); $("#light_on_send").click(async function() { light_on_reg_data.SendData(); }); $("#light_off_send").click(async function() { light_off_reg_data.SendData(); }); $("#aircon_on_send").click(async function() { aircon_on_reg_data.SendData(); }); $("#aircon_off_send").click(async function() { aircon_off_reg_data.SendData(); }); $("#thm_up_send").click(async function() { thm_up_reg_data.SendData(); }); $("#thm_down_send").click(async function() { thm_down_reg_data.SendData(); }); // called while online. obniz.onloop = async function() { const _sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); switch (sleepiness_level){ case 1: //空調の温度を下げる if(flg == false){ if( thm_down_flag == false){ aircon_on_reg_data.SendData(); await _sleep(500); thm_down_reg_data.SendData(); thm_down_flag = true; } } break; case 2: //マウスパットを振動させる if(flg == false){ dcmotor.power(70); for (let i = 0; i < 3; i++) { dcmotor.move(false); await _sleep(300); dcmotor.stop(); await _sleep(250); dcmotor.move(true); await _sleep(300); dcmotor.stop(); await _sleep(250); } } break; case 3: //ブザーを鳴らす if(flg == false){ speaker.play(1000); } else{ speaker.stop(); } break; case 4: //ライトを消す speaker.stop(); if( power_off_flag == false){ light_off_reg_data .SendData(); power_off_flag = true; } break; default: break; } }; }; // called on offline obniz.onclose = async function() { }; </script> </body> </html>

赤外線データ登録

var Register = function(obniz, key, sensor, led, reg_status, load_status) { this.obniz = obniz; this.key = key; this.sensor = sensor; this.led = led; this.reg_status = reg_status; this.load_status = load_status; this.data = []; this.LoadData = async function(){ var res_promise; res_promise = (ObnizUI.Util.loadFromStorage(this.key)); res_promise.then( response => { if (Array.isArray(response)){ this.reg_status.innerText = "登録済み"; this.data = response.concat(); } else{ this.reg_status.innerText = "未登録"; } }, error => { this.reg_status.innerText = "読み出し失敗"; } ); return 0; } this.RegisterData = async function(){ var res_data = []; if(this.reg_status.innerText == "登録中") { this.LoadData (); } else { this.reg_status.innerText = "登録中"; this.obniz.display.print("detecting..."); this.sensor.start(); } } this.RegisterComp = async function(arr){ var res_data = []; if (this.reg_status.innerText == "登録中"){ this.reg_status.innerText = "登録完了"; this.obniz.display.print("detecte success"); res_data = arr.concat(); ObnizUI.Util.saveToStorage(this.key, res_data); this.LoadData (); } } this.SendData = function(){ if ((this.reg_status.innerText == "登録済み") || (this.reg_status.innerText == "登録完了")) { this.led.send(this.data); } else { alert("未登録です"); } } };
ログインしてコメントを投稿する