LoginSignup
4
2

More than 1 year has passed since last update.

ポートフォリオとして、寝ていたら起こしてくれるCamera SleepというWebアプリを作成しました。

Posted at

はじめに

Camera Sleepというアプリを作成しました。
パソコンの前で作業をしているときに、つい寝てしまった時に、起こしてくれるアプリです。
使用場面としては、会議中につい寝てしまうような時に、起こしてくれるよう想定しました。
ただし、会議にはパソコンを持ち込む必要があります。

アプリのURLはこちらです。
https://camera-sleep.herokuapp.com/index.html

githubのソースコードはこちらです。
https://github.com/shosuke1989/camera-sleep

アプリの説明

  • 目を閉じているかwebカメラで判定し、寝ていると判定した場合音を鳴らします。
  • 寝ている場合にメールを送信することもでき、スマホのバイブレーションで眠気を覚まさせます。
  • メール記入欄に、メールアドレスを記入すると寝ている場合に、メールが送信されます。
  • メールが届かない場合は、アドレスが間違っているか、迷惑フィルタがかかっている可能性があります。
  • 目を閉じているか判定する閾値を調整することで、精度を上げることができます。
  • 寝ていると判定するまでの時間を調整できます。

動作例

IMB_IoIUAd.gif

使用技術

使用技術についてフロントエンド、バックエンド、インフラで分けると以下のとおりとなります。

フロントエンド

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();
});
4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2