はじめに
朝苦手でいつも出勤ギリギリまで寝てしまうので、絶対に起きれる環境をソフトウェアで作ろうと思い、カメラから目覚めを検知するまでアラームが鳴り続ける目覚まし時計をRaspberryPiで作成しました。
目覚め検知には以下の記事で作成した眠気検知プログラムを使用しています。
ラズパイカメラによる眠気検知とWebストリーミング
本記事のソースコードは以下に置いています。
https://github.com/AkitoArai709/SmartAlarm
環境
- Raspberry Pi 4
- Camera Module V2 For raspberry pi
- ROUNDS portable speaker
- Python : 3.7.3
- opencv : 4.5.1.48
- dlib : 19.22.0
- imutils : 0.5.4
- scipy : 1.6.2
システム構成
設定されたアラーム時間になるとスピーカーからアラームを鳴らし、カメラを起動します。カメラから人物を検知し、顔の目の大きさから眠気状態を検知し、目覚め状態になるまでアラームを鳴らし続けます。
ファイル構成
.
├─ models
│ ├─ dlib
│ │ └─ shape_predictor_68_face_landmarks.dat
│ └─ opencv
│ └─ haarcascade_frontalface_alt2.xml
├─ settings
│ └─ alarmSettings.json
├─ sounds
│ └─ <Alarm sound file>.wav
└─ src
├─ alarmSetting.py
├─ alarmSpeaker.py
├─ buffer.py
├─ detectionSleepiness.py
├─ main.py
└─ smartAlarm.py
スマートアラーム
main.py
ではsmartAlarm.py
で実装されたスマートアラームアプリケーション(SmartAlarm
クラス)を読み込んで実行します。
#!/usr/bin/env python
""" main.py
Summay:
RaspberryPi smart alarm application.
"""
from smartAlarm import SmartAlarm
def main():
alarm = SmartAlarm()
if __name__ == '__main__':
main()
""" smartAlarm.py
Summay:
Smart Alarm application.
This alarm will continue to sound until wake up.
"""
import json
import time
import threading
from alarmSetting import AlarmSetting
from alarmSpeaker import AlarmSpeaker
from datetime import datetime
class SmartAlarm:
""" SmartAlram. """
__SETTINGS_FILE = "./settings/alarmSettings.json"
def __init__(self):
""" Init constructor.
"""
self.__isRunning = False
self.__threadObj = None
self.__alarmSettings = []
self.alarmSpeaker = AlarmSpeaker()
self.readJson()
self.startAlarmThread()
def __del__(self):
""" Destructor.
"""
self.stopAlarmThread()
self.writeJson()
def addAlarmSetting(self, time: time, days: list):
""" Add Alarm setting.
(Example)
addAlarmSetting(datetime.strptime("07:00", '%H:%M').time(),['Sun','Mon','Tue','Web','Thu','Fri'])
Args:
time (time): Alarm time.
days (list): Days of the weeks.
"""
setting = AlarmSetting(time.replace(second=0, microsecond=0),days)
self.__alarmSettings.append(setting)
def startAlarmThread(self):
""" Start AlarmThread.
"""
self.__isRunning = True
if self.__threadObj is None:
self.__threadObj = threading.Thread(target=self.__alarmThread)
self.__threadObj.start()
def stopAlarmThread(self):
""" Stop AlarmThread.
"""
self.__isRunning = False
if self.__threadObj is not None:
self.__threadObj.join()
self.__threadObj = None
def writeJson(self):
""" Write the alarm settings to json file.
"""
# Serialize the AlarmSetting object.
list = []
for alarm in self.__alarmSettings:
if isinstance(alarm, AlarmSetting):
alarm: AlarmSetting = alarm
list.append(alarm.serialize())
# Convert to json, write file.
with open(self.__SETTINGS_FILE, mode='w', encoding='utf-8') as file:
json.dump(list, file, indent = 2)
def readJson(self):
""" Read the alarm settings to json file.
"""
with open(self.__SETTINGS_FILE, mode='r') as file:
list = json.load(file)
for setting in list:
alarmSetting = AlarmSetting.deserialize(setting)
if alarmSetting is not None:
self.__alarmSettings.append(alarmSetting)
def __alarmThread(self):
""" Main thread of SmartAlarm class.
Check the alarm settings.
If it matches the setting time, go off the alarm.
"""
while self.__isRunning:
if self.__checkAlram():
self.alarmSpeaker.goOff()
# Sleep until next time.
time.sleep(60 - datetime.now().second)
def __checkAlram(self) -> bool:
""" Check the alarm settings
Returns:
(bool): Judgment value of check the alarm settings.
"""
retVal = False
now = datetime.now()
for alarm in self.__alarmSettings:
if isinstance(alarm, AlarmSetting):
alarm: AlarmSetting = alarm
# Check the alarm setting
if alarm.isSetTime(now):
retVal = True
alarm.setEnabled(False)
return retVal
SmartAlarm
クラスはインスタンス生成時にアラーム再生クラス(AlarmSpeaker
)のインスタンス生成、アラーム設定ファイル読み込み、アラームスレッドの起動を行い、アプリケーションの動作を開始します。__alarmThread
スレッドではアラーム設定のチェック及び、アラームの再生処理の呼び出しを行います。また、アラーム設定時間は分単位としているため、アラーム時間のチェック処理実行完了後、次の分単位の時間になるまでSleepさせて、CPU使用率が無駄に増加しないように抑制しています。
アラーム設定時間はAlarmSetting
クラスで定義され、__alarmSettings
のリストで管理します。リストで管理することで、複数個のアラームを設定可能としています。
アラーム設定クラス
AlarmSetting
クラスではアラーム設定として、アラーム時間と曜日を定義します。
""" alarmSetting.py
Summay:
Alarm setting.
"""
from datetime import datetime, time
class AlarmSetting:
""" AlarmSetting. """
# Define properties.
__PROP_TIME = "time"
__PROP_DAYS = "days"
__PROPERTIES = [__PROP_TIME, __PROP_DAYS]
# Reset time of alarm setting.
__RESET_TIME = datetime.strptime("00:00", '%H:%M').time()
def __init__(self, time: datetime, days: list):
""" Init constructor.
"""
# self.time = time
# self.days = days
self.__dict__[self.__PROP_TIME] = time
self.__dict__[self.__PROP_DAYS] = days
self.__isEnabled = True
self.__isAlreadyReset = False
@property
def time(self) -> time:
""" time property.
Returns:
(time): datetime.time
"""
return self.__dict__[self.__PROP_TIME]
@property
def days(self) -> list:
""" days property.
Returns:
(list): days of week.
"""
return self.__dict__[self.__PROP_DAYS]
def isSetTime(self, now: datetime) -> bool:
""" Check the setting time.
Args:
now (datetime): Current time.
Returns:
(bool): Judgment value of check the alarm setting.
"""
self.__resetEnabledSetting(now.time())
return self.__isSettingTime(now.time()) \
and self.__isSettingDay(now.strftime('%a')) \
and self.__isEnabled
def setEnabled(self, enabled: bool):
""" Set the enabled value.
Args:
enabled (bool): Setting value.
"""
self.__isEnabled = enabled
def serialize(self) -> dict:
""" Serialize the AlarmSetting object.
Return the dict type of the defined property.
Returns:
retVal (dict): Serialized value.
"""
retVal = {}
for key, value in self.__dict__.items():
# Only defined properties.
if self.__PROPERTIES in key:
# The time type does not support json.
# Convert time to str.
if isinstance(value, time):
retVal[key] = value.strftime('%H:%M')
else:
retVal[key] = value
return retVal
@classmethod
def deserialize(cls, dict: dict):
""" Deserialize the AlarmSetting object.
Create the myself class from the dict data.
Args:
dict (dict): Setting data.
Returns:
(AlarmSetting): myself object.
"""
# Return None if there is no defined property in the dict data.
for prop in cls.__PROPERTIES:
if prop not in dict:
return None
return cls(datetime.strptime(dict[cls.__PROP_TIME], '%H:%M').time(), dict[cls.__PROP_DAYS])
def __isSettingTime(self, time: time) -> bool:
""" Check the setting time.
Args:
time (time): Current time.
Returns:
(bool): Judgment value of check the setting time.
"""
return time.replace(second=0, microsecond=0) == self.time
def __isSettingDay(self, day: str) -> bool:
""" Check the setting day of the week.
Args:
day (str): Current day of the week.
Returns:
(bool): Judgment value of check the setting day of the week.
"""
return day in self.days
def __resetEnabledSetting(self, time: time):
""" Reset the enabled value.
Args:
time (time): Current time.
"""
time = time.replace(second=0, microsecond=0)
# Matche the reset time and not yet reset.
# Execute the reset process only during the reset time.
if time == self.__RESET_TIME \
and self.__isAlreadyReset == False:
# Reset process.
# Set enabled.
self.setEnabled(True)
self.__isAlreadyReset = True
elif time != self.__RESET_TIME \
and self.__isAlreadyReset == True:
# Set false isAlreadyReset flg.
self.__isAlreadyReset = False
このクラスではserialize
とdeserialize
を用意してjson形式の変換を行えるようにしています。json形式の変換にはクラスの辞書型変数である__dict__
を用い、変換対象のプロパティを__PROPERTIES
で定義を行っています。こうすることで、json変換したいプロパティに変更があった場合でも、定数定義を変数するだけでプロパティを増減できる設計としています。
isSetTime
関数でアラーム有効度リセット(__resetEnabledSetting
)と、現在日時・曜日のチェックを行い、全て一致時にTrue
を返します。
__resetEnabledSetting
関数ではリセット時間(00:00
)になると、アラームの有効度をTrue
にして、アラーム実行で無効(Flase
)なった設定を日付の変わるタイミングで有効(True
)に戻します。また、True
を設定する処理だけでは、アラーム時間がリセット時間と重なっている場合に、関数の使い方によっては、
「アラーム実行(Flase
を設定)」→「有効度リセット(True
を設定)」→「有効度がTrue
のため、アラーム実行」...
と、同じ時間に複数回アラームが再生される可能性があるため、リセット時間に一度だけTrue
を設定するようにしています。
アラーム設定ファイル
アラーム設定として、アラーム時間(time
)と曜日(days
)をjson形式で定義します。
[
{
"time": "07:00",
"days": [
"Sun",
"Mon",
"Tue",
"Web",
"Thu",
"Fri",
"Sat"
]
}
]
アラーム再生クラス
AlarmSpeaker
クラスではアラーム再生処理と、眠気検知チェック処理を実行します。
""" alarmSleaker.py
Summay:
Alarm sleaker.
"""
import os
import cv2
import wave
import pyaudio
import random
import threading
from detectionSleepiness import DetectionSleepiness
class AlarmSpeaker:
""" AlarmSpeaker. """
# Sound path.
__SOUNDS_DIR = "./sounds"
def __init__(self):
""" Init constructor.
"""
self.__isRunning = False
self.__speakerThreadObj = None
self.__checkSleepinessThreadObj = None
def __del__(self):
""" Destructor.
"""
self.stopThread()
def goOff(self):
""" Go off the alarm.
"""
self.stopThread()
self.startThread()
def startThread(self):
""" Start SpeakerThread and CheckSleepinessThread.
"""
self.__isRunning = True
if self.__speakerThreadObj is None:
self.__speakerThreadObj = threading.Thread(target=self.__speakerThread)
self.__speakerThreadObj.start()
if self.__checkSleepinessThreadObj is None:
self.__checkSleepinessThreadObj = threading.Thread(target=self.__checkSleepinessThread)
self.__checkSleepinessThreadObj.start()
def stopThread(self):
""" Stop SpeakerThread and CheckSleepinessThread.
"""
self.__isRunning = False
if self.__speakerThreadObj is not None:
self.__speakerThreadObj.join()
self.__speakerThreadObj = None
if self.__checkSleepinessThreadObj is not None:
self.__checkSleepinessThreadObj.join()
self.__checkSleepinessThreadObj = None
def __checkSleepinessThread(self):
""" Check sleepiness form the camera.
"""
infApp = DetectionSleepiness()
camera = cv2.VideoCapture(0)
while self.__isRunning:
_, frame = camera.read()
if infApp.isSleepy(frame) == False:
self.__isRunning = False
def __speakerThread(self):
""" Continue to sound music until stopped status.
"""
sound = self.__SOUNDS_DIR + "/" + random.choice(os.listdir(self.__SOUNDS_DIR))
while self.__isRunning:
wf = wave.open(sound, "r")
audio = pyaudio.PyAudio()
stream = audio.open(format=audio.get_format_from_width(wf.getsampwidth()),
channels=wf.getnchannels(),
rate=wf.getframerate(),
output=True)
data = wf.readframes(1024)
while data != b'' and self.__isRunning:
stream.write(data)
data = wf.readframes(1024)
stream.stop_stream()
stream.close()
audio.terminate()
wf.close()
このクラスではアラーム再生スレッド(__speakerThread
)と眠気チェックスレッド(__checkSleepinessThread
)の2つのスレッドを動作します。__speakerThread
スレッドでは__SOUNDS_DIR
ディレクトリに格納されたアラームファイルの再生を__isRunning
がFalse
になるまで繰り返し再生を行います。__isRunning
は__checkSleepinessThread
スレッドで目覚め状態を検知すると、False
を設定します。つまり、目覚め状態になるまで、アラームを繰り返し再生続ける動作となります。
最後に
今回はカメラから目覚めを検知する目覚まし時計を作成しました。普通の目覚まし時計とは違い、物理スイッチでアラームを止めることが出来ないので、より起きやすい環境を作ることはできたと感じています。課題としては、現状は目だけで判定しているため、目を無理やり開けると目覚め状態として検知してしまうので、顔全体で判定するアルゴリズムを考える必要があります。
また、アラーム設定をjson形式のファイルに設定するようにしていますが、ファイルを編集するのは面倒なのでWebページから設定できるように改良しようと思います。