はじめに
Camera Sleepというアプリを作成しました。
パソコンの前で作業をしているときに、つい寝てしまった時に、起こしてくれるアプリです。
使用場面としては、会議中につい寝てしまうような時に、起こしてくれるよう想定しました。
ただし、会議にはパソコンを持ち込む必要があります。
アプリのURLはこちらです。
https://camera-sleep.herokuapp.com/index.html
githubのソースコードはこちらです。
https://github.com/shosuke1989/camera-sleep
アプリの説明
- 目を閉じているかwebカメラで判定し、寝ていると判定した場合音を鳴らします。
- 寝ている場合にメールを送信することもでき、スマホのバイブレーションで眠気を覚まさせます。
- メール記入欄に、メールアドレスを記入すると寝ている場合に、メールが送信されます。
- メールが届かない場合は、アドレスが間違っているか、迷惑フィルタがかかっている可能性があります。
- 目を閉じているか判定する閾値を調整することで、精度を上げることができます。
- 寝ていると判定するまでの時間を調整できます。
動作例
使用技術
使用技術についてフロントエンド、バックエンド、インフラで分けると以下のとおりとなります。
フロントエンド
HTML
CSS
Bootstrap
javascript
face-api.js
SmtpJS
バックエンド
無し
インフラ
VScode
Heroku
github
アプリ作成時の感想
初めは、Djangoとpython、opencvを使って、アプリを作成しようとしていたが、faceapi.jsを利用しても顔認識ができることがわかった。
フロントエンドのみで、アプリが完結してしまった。
コード
index.html
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<title>Camera Sleep</title>
<script src="./js/face-api.min.js"></script>
<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@emailjs/browser@3/dist/email.min.js"></script>
<script src="https://smtpjs.com/v3/smtp.js"></script>
<link rel="stylesheet" href="css/main.css">
</head>
<body>
<div class="container">
<h1>Camera Sleep</h1>
<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>
<h2>
<table>
<tr id="color" class="awake">
<td id="sleepcheck" >Awake</td>
<td id="sleepcount"></td>
</tr>
</table>
</h2>
<table>
<tr>
<td>アスペクト比</td>
<td id="sleepaspect"> 0.30 > 0.30</td>
</tr>
</table>
<div class="menus">
<div class="menu">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="soundOn" checked>
<label class="form-check-label" for="soundOn">Sound</label>
</div>
</div>
<div class="menu">
<label for="customRange1" class="form-label">判定値:</label>
<span id="aspectvalue">0.3</span>
<input type="range" class="form-range" id="aspectrange" min="0.1" max="1" step="0.01" value="0.3">
<p>アスペクト比が判定値を下回ると、目を閉じていると判定されます。</p>
</div>
<div class="menu">
<label for="customRange1" class="form-label">Count:</label>
<span id="countvalue" >5</span>
<span>秒</span>
<input type="range" class="form-range" id="countrange" min="1" max="10" step="1" value="5">
<p>目を閉じている時間が設定値を超えると、眠っていると判定されます。</p>
</div>
<div class="menu">
<form>
<div class="mb-3">
<label for="exampleInputEmail1" class="form-label">メール</label>
<input type="text" class="form-control" id="email" aria-describedby="emailHelp">
<div id="emailHelp" class="form-text"></div>
<p>眠っている場合、指定したアドレスにメールを送信します。</p>
<p>メールが届かない場合は、アドレスが間違っているか、</p>
<p>迷惑メールに振り分けられている可能性があります。</p>
</div>
</form>
</div>
</div>
</div>
<script src="./js/main.js"></script>
<!-- Optional JavaScript; choose one of the two! -->
<!-- Option 1: Bootstrap Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<!-- Option 2: Separate Popper and Bootstrap JS -->
<!--
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js" integrity="sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF" crossorigin="anonymous"></script>
-->
</body>
</html>
main.js
const canvas = document.getElementById( 'facecanvas' );
const videoEl = document.getElementById( 'video' );
const inputSize = 224;
const scoreThreshold = 0.5;
const options = new faceapi.TinyFaceDetectorOptions({ inputSize, scoreThreshold });
let sleepcheck=document.getElementById( 'sleepcheck' );// 後で消す
let sleepcount=document.getElementById( 'sleepcount' );// 後で消す
let sleepaspect=document.getElementById( 'sleepaspect' );// 後で消す
let color=document.getElementById( 'color' );// 後で消す
let context;
context = canvas.getContext("2d");
context.fillRect(0, 0, canvas.width, canvas.height);
const soundOn = document.getElementById("soundOn");
let count;
count=0
//ここからはrangeの設定
//aspect
const aspect = document.getElementById('aspectrange'); // input要素
const currentValueElem = document.getElementById('aspectvalue'); // 埋め込む先のspan要素
// 現在の値をspanに埋め込む関数
const setCurrentValue = (val) => {
currentValueElem.innerText = val;
}
// inputイベント時に値をセットする関数
const rangeOnChange = (e) =>{
setCurrentValue(e.target.value);
}
//count
const time = document.getElementById('countrange'); // input要素
const currentValueElem2 = document.getElementById('countvalue'); // 埋め込む先のspan要素
// 現在の値をspanに埋め込む関数
const setCurrentValue2 = (val) => {
currentValueElem2.innerText = val;
}
// inputイベント時に値をセットする関数
const rangeOnChange2 = (e) =>{
setCurrentValue2(e.target.value);
}
window.onload = () => {
time.addEventListener('input', rangeOnChange2); // スライダー変化時にイベントを発火
setCurrentValue2(time.value); // ページ読み込み時に値をセット
aspect.addEventListener('input', rangeOnChange); // スライダー変化時にイベントを発火
setCurrentValue(aspect.value); // ページ読み込み時に値をセット
}
//rangeの設定終わり
//エンターキー無効
document.onkeypress = function(e) {
// エンターキーだったら無効にする
if (e.key === 'Enter') {
return false;
}
}
function sound(type, sec) {
const ctx = new AudioContext()
const osc = ctx.createOscillator()
osc.type = type
osc.connect(ctx.destination)
osc.start()
osc.stop(sec)
}
function sleep(){
if(soundOn.checked==true){
sound('sine', 0.5)
}
//メール送信設定
const email=document.getElementById('email');
if (email.value!=""){
Email.send({
SecureToken : "7ec0102f-ed83-4584-b1d5-5337d26c99a1",
To : email.value,
From : "camera.sleep.app@gmail.com",
Subject : "Wake Up!",
Body : "Wake Up!"
}).then(function(message) {
console.log(message);
window.alert("メールを送信しました");
})
};
//メール送信設定終わり
}
async function onPlay(){
if(videoEl.paused || videoEl.ended || !faceapi.nets.tinyFaceDetector.params)
return setTimeout(() => onPlay())
const result = await faceapi.detectSingleFace(videoEl, options).withFaceLandmarks()
if (result) {
const dims = faceapi.matchDimensions(canvas, videoEl, true)
const resizedResult = faceapi.resizeResults(result, dims)
const mrks = resizedResult.landmarks.positions
faceapi.draw.drawFaceLandmarks(canvas, resizedResult)
aspectcheck(mrks);
}
setTimeout(() => onPlay(),1000)
};
function aspectcheck(mrks){
var a_l = Math.sqrt( Math.pow( mrks[37].x-mrks[41].x, 2 ) + Math.pow( mrks[37].y-mrks[41].y, 2 ) ) ;
var b_l = Math.sqrt( Math.pow( mrks[38].x-mrks[40].x, 2 ) + Math.pow( mrks[38].y-mrks[40].y, 2 ) ) ;
var c_l = Math.sqrt( Math.pow( mrks[36].x-mrks[39].x, 2 ) + Math.pow( mrks[36].y-mrks[39].y, 2 ) ) ;
var EAR_L = ( a_l + b_l ) / ( 2 * c_l ) ;
var a_r = Math.sqrt( Math.pow( mrks[43].x-mrks[47].x, 2 ) + Math.pow( mrks[43].y-mrks[47].y, 2 ) ) ;
var b_r = Math.sqrt( Math.pow( mrks[44].x-mrks[46].x, 2 ) + Math.pow( mrks[44].y-mrks[46].y, 2 ) ) ;
var c_r = Math.sqrt( Math.pow( mrks[42].x-mrks[45].x, 2 ) + Math.pow( mrks[42].y-mrks[45].y, 2 ) ) ;
var EAR_R = ( a_r + b_r ) / ( 2 * c_r ) ;
var EAR = ( EAR_R + EAR_L ) / 2;
if(EAR<aspect.value){
sleepaspect.textContent=String(Math.round(String(EAR)*1000)/1000)+" < "+String(aspect.value);// 後で消す
sleepcheck.textContent="Close";// 後で消す
color.className="close"
count+=1
sleepcount.textContent=":"+String(count)+"秒";// 後で消す
if(count>time.value){
sleepcheck.textContent="Sleep";// 後で消す
color.className="sleep"
sleep()
count=0
sleepcount.textContent="";// 後で消す
}
}
else{
sleepaspect.textContent=String(Math.round(String(EAR)*1000)/1000)+" > "+String(aspect.value);// 後で消す
sleepcheck.textContent="Awake";// 後で消す
count=0
sleepcount.textContent="";// 後で消す
color.className="awake"
}
}
async function run(){
await faceapi.nets.tinyFaceDetector.load("models/")
await faceapi.loadFaceLandmarkModel("models/")
const stream = await navigator.mediaDevices.getUserMedia({ video: {} })
videoEl.srcObject = stream;
}
$(document).ready(function() {
run();
});