眠気を覚ましたい!
私は仕事中や勉強中に眠いときに、眠気を覚ましたいなと思うことがよくあります。
今はコーヒーを飲んでいますが、学生の頃、授業で居眠り中、先生に声をかけられたときが1番目が覚めたので、今回はその体験をアプリで再現したいなと思います。
作成したもの
PCのインカメで自分の顔を映し、目を閉じたら音声が流れて起こされるアプリを作成しました。入力した名前が〇〇の場合、「そこの寝ている〇〇さん、起きなさい」という音声が流れます。
目を閉じた判定はこちらの記事を参考にしました。
実装
構成とコードまとめ
project_root/
├── app.py # Flaskアプリケーションのメインファイル
├── shape_predictor_68_face_landmarks.dat # Dlibのランドマークモデルファイル
└── templates/
└── index.html # 名前入力フォームとカメラ映像のHTMLテンプレート
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のコードまとめ
<!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秒ごとにチェック
}
});
}
まとめ
作成してみたものの、正直これでは起きないなと思いました笑
特に音声は機械音なので、もう少し人間の声に近づけることができれば、全然違う感覚なのではないかと思います。