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

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

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

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

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

# 動作例
![IMB_IoIUAd.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/948501/90169d9e-daf9-3ad3-b45d-bf7982b2e0b4.gif)


# 使用技術
使用技術についてフロントエンド、バックエンド、インフラで分けると以下のとおりとなります。
### フロントエンド
HTML
CSS
Bootstrap
javascript
face-api.js
SmtpJS
### バックエンド
無し
### インフラ
VScode
Heroku
github

# アプリ作成時の感想
初めは、Djangoとpython、openｃｖを使って、アプリを作成しようとしていたが、faceapi.jsを利用しても顔認識ができることがわかった。
フロントエンドのみで、アプリが完結してしまった。

# コード
```html: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>
```

```javascript: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();
});
```