4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

眠気を検知!現実にカムバック、居眠り防止アプリを作成!

Last updated at Posted at 2024-10-31

眠気を覚ましたい!

私は仕事中や勉強中に眠いときに、眠気を覚ましたいなと思うことがよくあります。

今はコーヒーを飲んでいますが、学生の頃、授業で居眠り中、先生に声をかけられたときが1番目が覚めたので、今回はその体験をアプリで再現したいなと思います。

スクリーンショット 2024-10-31 21.25.39.png

作成したもの

PCのインカメで自分の顔を映し、目を閉じたら音声が流れて起こされるアプリを作成しました。入力した名前が〇〇の場合、「そこの寝ている〇〇さん、起きなさい」という音声が流れます。

目を閉じた判定はこちらの記事を参考にしました。

実装

構成とコードまとめ

project_root/
├── app.py                                  # Flaskアプリケーションのメインファイル
├── shape_predictor_68_face_landmarks.dat   # Dlibのランドマークモデルファイル
└── templates/
    └── index.html                          # 名前入力フォームとカメラ映像のHTMLテンプレート
app.pyのコードまとめ
app.py
from flask import Flask, render_template, request, Response, jsonify
import cv2
import dlib
from scipy.spatial import distance
from gtts import gTTS
import pygame
import os

app = Flask(__name__)

# 読み上げフラグ
alert_triggered = False

# 目のアスペクト比を計算する関数
def eye_aspect_ratio(eye):
    A = distance.euclidean(eye[1], eye[5])
    B = distance.euclidean(eye[2], eye[4])
    C = distance.euclidean(eye[0], eye[3])
    ear = (A + B) / (2.0 * C)
    return ear

# 閾値とフレームカウントの設定
EYE_AR_THRESH = 0.25
EYE_AR_CONSEC_FRAMES = 3

# Dlibの顔検出器とランドマーク予測器を初期化
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")
(lStart, lEnd) = (42, 48)
(rStart, rEnd) = (36, 42)

# pygameの初期化
pygame.mixer.init()

def play_warning(name):
    # 音声メッセージを生成
    tts = gTTS(text=f"そこの寝ている{name}さん、起きなさい", lang='ja')
    tts.save("alert.mp3")  # 一時的に音声ファイルとして保存
    
    # 音声ファイルを再生
    pygame.mixer.music.load("alert.mp3")
    pygame.mixer.music.play()
    while pygame.mixer.music.get_busy():  # 再生中は待機
        continue

    # 一時ファイルを削除
    os.remove("alert.mp3")

@app.route("/", methods=["GET", "POST"])
def index():
    global alert_triggered
    alert_triggered = False  # ページをリロードするとフラグをリセット
    return render_template("index.html")

@app.route("/check_alert")
def check_alert():
    global alert_triggered
    return jsonify({"alert_triggered": alert_triggered})

def gen_frames(name):
    global alert_triggered
    cap = cv2.VideoCapture(0)
    cap.set(cv2.CAP_PROP_FPS, 15)  # フレームレートを15FPSに設定
    frame_counter = 0
    frame_skip_count = 2  # 2フレームごとに表示
    current_frame = 0

    while True:
        success, frame = cap.read()
        if not success or alert_triggered:  # 読み上げが終わったらカメラを停止
            break

        if current_frame % frame_skip_count == 0:
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            faces = detector(gray, 0)

            for face in faces:
                shape = predictor(gray, face)
                shape = [(shape.part(i).x, shape.part(i).y) for i in range(68)]
                leftEye = shape[lStart:lEnd]
                rightEye = shape[rStart:rEnd]

                leftEAR = eye_aspect_ratio(leftEye)
                rightEAR = eye_aspect_ratio(rightEye)
                ear = (leftEAR + rightEAR) / 2.0
                print(ear)

                if ear < EYE_AR_THRESH:
                    frame_counter += 1
                    if frame_counter >= EYE_AR_CONSEC_FRAMES and not alert_triggered:
                        play_warning(name)  # gTTSで音声を再生
                        alert_triggered = True  # 読み上げ完了後にフラグをセット
                else:
                    frame_counter = 0

            # フレームサイズを縮小
            frame = cv2.resize(frame, (640, 480))
            ret, buffer = cv2.imencode('.jpg', frame)
            frame = buffer.tobytes()

            yield (b'--frame\r\n'
                   b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')

        current_frame += 1

    cap.release()

@app.route('/video_feed')
def video_feed():
    name = request.args.get("name", "誰か")  # URLパラメータから名前を取得
    return Response(gen_frames(name), mimetype='multipart/x-mixed-replace; boundary=frame')

if __name__ == "__main__":
    app.run(debug=True, threaded=False)

index.htmlのコードまとめ
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>スリープモニタリング</title>
    <style>
        /* 全体のレイアウトを中央寄せ */
        body {
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
            font-family: Arial, sans-serif;
            background-color: #f3f4f6;
        }

        /* コンテナ全体のスタイル */
        .container {
            text-align: center;
            padding: 20px;
            background-color: #ffffff;
            border-radius: 10px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
            max-width: 400px;
            width: 100%;
        }

        /* ヘッダーとボタンのスタイル */
        h2 {
            color: #333;
            margin-bottom: 20px;
        }

        #nameForm label {
            font-weight: bold;
            color: #555;
        }

        #nameForm input[type="text"] {
            padding: 8px;
            margin: 10px 0;
            width: 80%;
            border: 1px solid #ddd;
            border-radius: 5px;
            text-align: center;
            font-size: 16px;
        }

        #nameForm button {
            padding: 10px 20px;
            background-color: #4CAF50;
            color: #fff;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            cursor: pointer;
            transition: background-color 0.3s ease;
        }

        #nameForm button:hover {
            background-color: #45a049;
        }

        /* カメラ映像のスタイル */
        #videoStream {
            margin-top: 20px;
            width: 100%;
            height: auto;
            border-radius: 10px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
        }

        /* アラートメッセージのスタイル */
        #alertMessage {
            display: none;
            margin-top: 30px;
            color: #e74c3c;
            font-size: 2em;
            font-weight: bold;
            text-align: center;
            animation: blink 1s infinite;
        }

        /* アニメーション: 点滅効果 */
        @keyframes blink {
            0%, 100% { opacity: 1; }
            50% { opacity: 0; }
        }
    </style>
</head>
<body>
    <div class="container">
        <h2>スリープモニタリング</h2>
        <form id="nameForm">
            <label for="name">名前を入力してください</label>
            <input type="text" id="name" name="name" required>
            <button type="submit">開始</button>
        </form>

        <div id="alertMessage"><h1>起きろ!</h1></div>
        <img id="videoStream" alt="カメラ映像">
    </div>

    <script>
        document.getElementById('nameForm').onsubmit = function(event) {
            event.preventDefault();  // フォーム送信を防ぐ
            const name = document.getElementById('name').value;
            const videoStream = document.getElementById('videoStream');
            videoStream.src = `/video_feed?name=${encodeURIComponent(name)}`;
            videoStream.style.display = "block";  // 映像の表示
            checkAlert();
        }

        // 定期的にalert_triggeredの状態を確認
        function checkAlert() {
            fetch("/check_alert")
                .then(response => response.json())
                .then(data => {
                    if (data.alert_triggered) {
                        document.getElementById("alertMessage").style.display = "block";
                        document.getElementById("videoStream").style.display = "none";
                    } else {
                        setTimeout(checkAlert, 1000);  // 1秒ごとにチェック
                    }
                });
        }
    </script>
</body>
</html>

shape_predictor_68_face_landmarks.datのリンク

app.py

リアルタイムで目の動きを監視し、閾値を下回ると音声で警告を発するシステムです。

コードの説明

インポートと初期設定

FlaskでWebサーバーを構築し、OpenCVとdlibで画像処理を行い、gTTSとpygameで音声を再生するためのライブラリをインポートしています。

from flask import Flask, render_template, request, Response, jsonify
import cv2
import dlib
from scipy.spatial import distance
from gtts import gTTS
import pygame
import os

app = Flask(__name__)

目のアスペクト比(EAR)の計算関数

目のランドマークの位置を使用して、目の開閉状態を示す「目のアスペクト比(EAR)」を計算します。この比率が低くなると、まばたきや目を閉じている状態を示します。

def eye_aspect_ratio(eye):
    A = distance.euclidean(eye[1], eye[5])
    B = distance.euclidean(eye[2], eye[4])
    C = distance.euclidean(eye[0], eye[3])
    ear = (A + B) / (2.0 * C)
    return ear

閾値と顔検出の初期化

EARの閾値を設定し、目を閉じていると判定するフレーム数を指定します。
また、dlibの顔検出器とランドマーク予測器を初期化し、目のランドマークの範囲も設定します。

EYE_AR_THRESH = 0.25
EYE_AR_CONSEC_FRAMES = 3
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")
(lStart, lEnd) = (42, 48)
(rStart, rEnd) = (36, 42)

警告音声の生成と再生

gTTSでの音声生成と再生:指定の名前を含む日本語の警告メッセージを生成し、pygameで再生します。再生後は一時的に作成した音声ファイルを削除します。

def play_warning(name):
    tts = gTTS(text=f"そこの寝ている{name}さん、起きなさい", lang='ja')
    tts.save("alert.mp3")
    pygame.mixer.music.load("alert.mp3")
    pygame.mixer.music.play()
    while pygame.mixer.music.get_busy():
        continue
    os.remove("alert.mp3")

ルートとエンドポイントの設定

index.htmlを表示し、ページがリロードされたときに音声の再生フラグをリセットします。

@app.route("/", methods=["GET", "POST"])
def index():
    global alert_triggered
    alert_triggered = False
    return render_template("index.html")

音声が再生されたかどうかの状態 (alert_triggered) をJSON形式で返します。

@app.route("/check_alert")
def check_alert():
    global alert_triggered
    return jsonify({"alert_triggered": alert_triggered})

フレーム生成と目の監視

指定したカメラからフレームを取得し、リアルタイムで目のEARを計算します。EARが閾値以下で連続した場合にplay_warningを呼び出し、警告音声を再生します。

def gen_frames(name):
    global alert_triggered
    cap = cv2.VideoCapture(0)
    cap.set(cv2.CAP_PROP_FPS, 15)
    frame_counter = 0
    frame_skip_count = 2
    current_frame = 0
    # ループ内の目の状態監視...

動画ストリームエンドポイント

gen_framesを呼び出して、指定された名前で動画ストリームを生成し、クライアントにリアルタイムで送信します。

@app.route('/video_feed')
def video_feed():
    name = request.args.get("name", "誰か")
    return Response(gen_frames(name), mimetype='multipart/x-mixed-replace; boundary=frame')

アプリ起動

Flaskアプリをデバッグモードで起動します。

if __name__ == "__main__":
    app.run(debug=True, threaded=False)

index.html

app.pyで設定されたカメラ映像を表示し、アラートを確認し表示する仕組みになっています。

コードの説明

HTMLの基本設定とCSSスタイル

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>スリープモニタリング</title>
    <style>
        /* スタイル定義 */
    </style>
</head>

コンテナ要素

  • nameFormで名前を入力し、モニタリングを開始するためのボタンが表示されています。
  • alertMessageはアラートが発動されたときに、「起きろ!」のメッセージを点滅させるように設定されています。
  • videoStreamというIDの画像要素を使用して、サーバーから受信するカメラ映像を表示します。
<body>
    <div class="container">
        <h2>スリープモニタリング</h2>
        <form id="nameForm">
            <label for="name">名前を入力してください</label>
            <input type="text" id="name" name="name" required>
            <button type="submit">開始</button>
        </form>

        <div id="alertMessage"><h1>起きろ!</h1></div>
        <img id="videoStream" alt="カメラ映像">
    </div>
</body>

JavaScriptの機能

  • onsubmitイベントでデフォルトのフォーム送信を無効にし、名前を取得してvideo_feedエンドポイントを呼び出します。その後取得した映像をvideoStream要素に表示します。
  • encodeURIComponent(name)で、特殊文字が含まれていてもURLとして扱えるようにエンコードしています。
<script>
    document.getElementById('nameForm').onsubmit = function(event) {
        event.preventDefault();  // フォーム送信を防ぐ
        const name = document.getElementById('name').value;
        const videoStream = document.getElementById('videoStream');
        videoStream.src = `/video_feed?name=${encodeURIComponent(name)}`;
        videoStream.style.display = "block";  // 映像の表示
        checkAlert();
    }

アラートチェック機能

  • エンドポイントに定期的にリクエストを送信し、alert_triggeredの状態をチェックします。
  • alert_triggeredがtrueの場合、アラートメッセージを表示し、カメラ映像を非表示にします。アラートが発生していない場合は、1秒後に再度checkAlertを呼び出し、確認を繰り返します。
function checkAlert() {
    fetch("/check_alert")
        .then(response => response.json())
        .then(data => {
            if (data.alert_triggered) {
                document.getElementById("alertMessage").style.display = "block";
                document.getElementById("videoStream").style.display = "none";
            } else {
                setTimeout(checkAlert, 1000);  // 1秒ごとにチェック
            }
        });
}

まとめ

作成してみたものの、正直これでは起きないなと思いました笑
特に音声は機械音なので、もう少し人間の声に近づけることができれば、全然違う感覚なのではないかと思います。

4
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?