OpenCVの顔の識別機能とカレンダー読み上げの組み合わせ
まず顔の識別で自分の顔だった時に動作するように
カレンダー読み上げ機能をモジュールにする
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | from calendar_utils import authenticate, get_upcoming_events, synthesize_speech, format_date_with_weekday from playsound import playsound def main(): creds = authenticate() audio_files = [] # 音声ファイルのリスト if creds: events = get_upcoming_events(creds) if not events: print( '今週の残りの予定はありません。' ) # 音声ファイルは再生しない else : print( '今週の残りの予定:' ) for event in events: start = event[ 'start' ].get( 'dateTime' , event[ 'start' ].get( 'date' )) summary = event.get( 'summary' , '(タイトルなし)' ) formatted_date = format_date_with_weekday(start) event_text = f "{formatted_date} - {summary}" print(event_text) filename = synthesize_speech(event_text) if filename: audio_files.append(filename) # 生成されたファイル名をリストに追加 # 音声ファイルが存在する場合のみ notice.wav と各予定の音声を再生 if audio_files: # notice.wavを最初に再生 print( "再生中: notice.wav" ) playsound( "notice.wav" ) # 各予定の音声ファイルを再生 for audio_file in audio_files: print(f "再生中: {audio_file}" ) playsound(audio_file) if __name__ == '__main__' : main() |
をモジュールにして他のプログラムから呼び出せるようにしたい
1 | touch calendar_audio_utils.py |
でファイルを作成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | from calendar_utils import authenticate, get_upcoming_events, synthesize_speech, format_date_with_weekday from playsound import playsound def get_weekly_schedule_with_audio(play_audio=False): "" " 今週の残りの予定を取得し、音声ファイルを生成する関数。 :param play_audio: 予定を音声で再生するかどうか(デフォルトは再生しない) : return : 今週の予定をテキスト形式で返すリスト "" " creds = authenticate() audio_files = [] # 音声ファイルのリスト event_texts = [] # 予定のテキストリスト if creds: events = get_upcoming_events(creds) if not events: print( '今週の残りの予定はありません。' ) else : print( '今週の残りの予定:' ) for event in events: start = event[ 'start' ].get( 'dateTime' , event[ 'start' ].get( 'date' )) summary = event.get( 'summary' , '(タイトルなし)' ) formatted_date = format_date_with_weekday(start) event_text = f "{formatted_date} - {summary}" event_texts.append(event_text) # テキストをリストに追加 print(event_text) # 音声ファイルを生成 filename = synthesize_speech(event_text) if filename: audio_files.append(filename) # 生成されたファイル名をリストに追加 # 音声を再生するオプションがTrueの場合にのみ、音声ファイルを再生 if play_audio and audio_files: # notice.wavを最初に再生 print( "再生中: notice.wav" ) playsound( "notice.wav" ) # 各予定の音声ファイルを再生 for audio_file in audio_files: print(f "再生中: {audio_file}" ) playsound(audio_file) return event_texts |
として保存
念のため動作するかチェック
1 | vim testvoice.py |
でファイル作成
1 2 3 4 5 6 7 8 | from calendar_audio_utils import get_weekly_schedule_with_audio # 音声再生なしで予定を取得 schedule = get_weekly_schedule_with_audio(play_audio=False) print(schedule) # 音声再生ありで予定を取得 schedule = get_weekly_schedule_with_audio(play_audio=True) |
保存したら
1 | python testvoice.py |
で実行
これで動作するのが確認できたので
次に顔の識別
以前使ったものを再利用する
Pixcel8で撮影したスマホの写真で顔データを作る場合には
元画像の1/4にする必要があるため
変換のため
resize_save.py
を作成したのでこれを使う
コードは
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | import cv2 import os import argparse def main(): # コマンドライン引数を解析するパーサーを作成 parser = argparse.ArgumentParser(description= "Resize and save an image" ) parser.add_argument( "image_path" , help= "Path to the image file" ) args = parser.parse_args() # 画像を読み込む image = cv2.imread(args.image_path) if image is None: print( "画像が読み込めませんでした。" ) return # 画像の元の高さ、幅を取得 height, width = image.shape[:2] # 新しい寸法を計算(元のサイズの1/4) new_width = width // 4 new_height = height // 4 # 画像をリサイズ resized_image = cv2.resize(image, (new_width, new_height)) # 新しいファイル名を設定 new_file_path = os.path.splitext(args.image_path)[0] + "_quarter.jpg" # リサイズした画像を保存 cv2.imwrite(new_file_path, resized_image) print(f "リサイズされた画像が保存されました: {new_file_path}" ) if __name__ == '__main__' : main() |
使用する時にはターミナルでコマンドで実行する
1 | python resize_save.py PXL_20240612_091410912.jpg |
というようにファイルを指定すれば
実行後1/4サイズにした画像が作成される
ファイルサイズを調べるスクリプトも作成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import cv2 import os import argparse def main(): # コマンドライン引数を解析するパーサーを作成 parser = argparse.ArgumentParser(description= "Display image properties" ) parser.add_argument( "image_path" , help= "Path to the image file" ) args = parser.parse_args() # 画像を読み込む image = cv2.imread(args.image_path) if image is None: print( "画像が読み込めませんでした。" ) return # 画像の高さ、幅、チャンネル数を取得 height, width, channels = image.shape print(f "画像の幅: {width} ピクセル" ) print(f "画像の高さ: {height} ピクセル" ) print(f "色チャネル数: {channels}" ) # ファイルサイズを取得 file_size = os.path.getsize(args.image_path) print(f "ファイルサイズ: {file_size} バイト" ) if __name__ == '__main__' : main() |
これを
1 | python file_info.py PXL_20240612_091410912_resized_resized.jpg |
というように実行すればサイズが表示される
1 2 3 4 | 画像の幅: 684 ピクセル 画像の高さ: 912 ピクセル 色チャネル数: 3 ファイルサイズ: 228769 バイト |
この2つは自分以外の写真から登録画像を作るのに使うので
1 2 | cp .. /face_recog/file_info .py . cp .. /face_recog/resize_save .py . |
でコピーしておく
次に
入力した写真から人の顔の部分を切り出して保存するプログラム
1 | generate_aligned_faces.py |
に写真のファイルを引数にして実行すれば個人ごとの顔写真ができる
これは
入力した写真から人の顔の部分を切り出して保存するプログラム
複数の人物が写っている場合は全員を切り出して face001.jpg , face002.jpg ・・・ と名前を付けて保存する
出力されたファイル名を 人の名前に変更しておくと後々便利です。
face001.jpg → taro.jpg
例
1 | python generate_aligned_faces.py image.jpg |
とすれば
写真に写っている人の分だけファイルができる
そのファイル名を人の名前に変更する
つまり全て
face001.jpg
という感じで
Face00x.jpg
となっているので
写真ごとに名前を変える
これもコピーしておく
1 | cp .. /face_recog/generate_aligned_faces .py . |
次に
1 | generate_feature_dictionary.py |
で
切り出した顔のjpgファイルを読み込んで、顔の特徴量に変換する
例えば 顔写真 taro.jpg を入力すると 顔の特徴量 taro.npy が出力される
このnumpyファイルに各個人の顔の特徴量が128次元ベクトルに変換されて入っている
例
1 2 | python generate_feature_dictionary.py face001.jpg python generate_feature_dictionary.py face002.jpg |
つまり
写真の人の分だけ実行すればOK
これもコピーしておく
1 | cp .. /face_recog/generate_feature_dictionary .py . |
次に顔の得微量が近い人を検出するにはモデルが必要なのでコピー
1 2 | cp .. /face_recog/face_recognizer_fast .onnx . cp .. /face_recog/face_detection_yunet_2023mar .onnx . |
そして作成した自分の顔の得微量ファイルもコピーしておく
1 | cp .. /face_recog/satoru .* . |
Webカメラから映った時に顔の識別をするので
1 | cp .. /face_recog/webcam_face_recognizer .py . |
でコピー
念の為動作確認
1 | python webcam_face_recognizer.py |
で自分の顔を識別しているのがわかる
次にこの中で
顔認識した時に
1 2 | from calendar_module import get_weekly_schedule_with_audio # 音声再生なしで予定を取得 schedule = get_weekly_schedule_with_audio(play_audio=False) print(schedule) # 音声再生ありで予定を取得 schedule = get_weekly_schedule_with_audio(play_audio=True) |
を実行
また
毎回読み上げでは負荷がかかるため、次の呼び出しは12時間後になるように設定
顔を識別できたときに特定の関数を呼び出し、
呼び出しが12時間に1回のみになるように制限するためには、識別が成功した時間を記録し、
次に呼び出すタイミングを管理する
1. call_function_when_recognized: この関数が顔認識時に呼び出され、最後の呼び出し時間を記録します。
次に呼び出すまでの間隔が12時間経過していない場合は、新たに処理を実行しないようにしています。
2. THROTTLE_TIME: 12時間を秒単位(12 * 60 * 60)で設定しています。
3. last_called_time: この変数は最後に呼び出された時間を記録し、
次の呼び出しが12時間以内であれば、新たな処理をスキップします。
.wavファイルを毎回生成していると容量を圧迫するため
1 2 3 4 5 6 7 8 | from calendar_audio_utils import get_weekly_schedule_with_audio # 音声再生なしで予定を取得 schedule = get_weekly_schedule_with_audio(play_audio=False) print(schedule) # 音声再生ありで予定を取得 schedule = get_weekly_schedule_with_audio(play_audio=True) |
の処理の後に notice.wavファイル以外の .wavファイルをすべて削除する
1 | touch webcam_face_calendar.py |
でファイルを作成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | import os import glob import numpy as np import cv2 import time from calendar_module import get_weekly_schedule_with_audio # カレンダーから予定を取得するためのインポート COSINE_THRESHOLD = 0.363 NORML2_THRESHOLD = 1.128 # 12時間(秒単位) THROTTLE_TIME = 12 * 60 * 60 last_called_time = 0 # 最後に呼び出した時間を初期化 def match(recognizer, feature1, dictionary): for element in dictionary: user_id, feature2 = element score = recognizer.match(feature1, feature2, cv2.FaceRecognizerSF_FR_COSINE) if score > COSINE_THRESHOLD: return True, (user_id, score) return False, ( "" , 0.0) def call_function_when_recognized(user_id): global last_called_time current_time = time . time () # 最後に呼び出してから12時間経過しているかを確認 if current_time - last_called_time >= THROTTLE_TIME: print(f "認識されました: {user_id}" ) # 予定を音声再生なしで取得 schedule = get_weekly_schedule_with_audio(play_audio=False) print( "予定:" , schedule) # 予定を音声再生ありで取得 schedule = get_weekly_schedule_with_audio(play_audio=True) print( "音声で再生される予定:" , schedule) # notice.wavファイル以外の.wavファイルを削除 cleanup_audio_files(exclude_file= "notice.wav" ) # 最後に呼び出した時間を更新 last_called_time = current_time else : print( "まだ12時間経過していないため、次の呼び出しは行われません。" ) def cleanup_audio_files(exclude_file): "" "指定された.wavファイル以外の.wavファイルを削除する関数" "" directory = os.getcwd() # 現在のディレクトリを取得 wav_files = glob.glob(os.path. join (directory, "*.wav" )) # すべての.wavファイルを取得 for wav_file in wav_files: if os.path. basename (wav_file) != exclude_file: try: os.remove(wav_file) # 指定されたファイル以外を削除 print(f "削除しました: {wav_file}" ) except OSError as e: print(f "ファイル削除エラー: {wav_file}, {e}" ) def main(): directory = os.path. dirname (__file__) capture = cv2.VideoCapture(0) # Use the default camera if not capture.isOpened(): print( "Error: The webcam could not be opened." ) return dictionary = [] files = glob.glob(os.path. join (directory, "*.npy" )) for file in files: feature = np.load( file ) user_id = os.path.splitext(os.path. basename ( file ))[0] dictionary.append((user_id, feature)) weights = os.path. join (directory, "face_detection_yunet_2023mar.onnx" ) face_detector = cv2.FaceDetectorYN_create(weights, "" , (0, 0)) weights = os.path. join (directory, "face_recognizer_fast.onnx" ) face_recognizer = cv2.FaceRecognizerSF_create(weights, "" ) while True: result, image = capture. read () if not result: print( "Error: No image from webcam." ) break image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # Ensure image is in RGB height, width, _ = image.shape face_detector.setInputSize((width, height)) result, faces = face_detector.detect(image) faces = faces if faces is not None else [] for face in faces: aligned_face = face_recognizer.alignCrop(image, face) feature = face_recognizer.feature(aligned_face) result, user = match(face_recognizer, feature, dictionary) box = list(map(int, face[:4])) color = (0, 255, 0) if result else (0, 0, 255) thickness = 2 cv2.rectangle(image, box, color, thickness, cv2.LINE_AA) id , score = user if result else ( "unknown" , 0.0) text = "{} ({:.2f})" . format ( id , score) position = (box[0], box[1] - 10) font = cv2.FONT_HERSHEY_SIMPLEX scale = 0.6 cv2.putText(image, text, position, font, scale, color, thickness, cv2.LINE_AA) if result and id != "unknown" : call_function_when_recognized( id ) # 顔が認識された時にカレンダーの予定取得を実行 # 画像を表示する前にRGBからBGRに再変換 cv2.imshow( "face recognition" , cv2.cvtColor(image, cv2.COLOR_RGB2BGR)) key = cv2.waitKey(1) if key == ord( 'q' ): break capture.release() cv2.destroyAllWindows() if __name__ == '__main__' : main() |
で実行
1 2 3 4 | Traceback (most recent call last): File "/Users/snowpool/aw10s/week_calendar_voice/webcam_face_calendar.py" , line 6, in <module> from calendar_module import get_weekly_schedule_with_audio # カレンダーから予定を取得するためのインポート ImportError: cannot import name 'get_weekly_schedule_with_audio' from 'calendar_module' ( /Users/snowpool/aw10s/week_calendar_voice/calendar_module .py) |
となる
これはChatGPTで作成した時のモジュールのエラー
結構あることでライブラリのインポートを間違えたり削除下入りしている
1 | from calendar_module import get_weekly_schedule_with_audio # カレンダーから予定を取得するためのインポート |
に変更すれば解決
起動はしたけど、このままだとOpenCVで画面描画するので
これは不要なので非表示にする
これをしないとリモート環境などで動作しない
v2.imshow()やキーボードの操作に関する部分を削除し、
無限ループで顔認識を行うコードに修正
cv2.VideoCapture の映像確認が不要な場合は、その部分を省略しても動作する
ということで
画面表示とキー入力待機を削除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | import os import glob import numpy as np import cv2 import time from calendar_audio_utils import get_weekly_schedule_with_audio # カレンダーから予定を取得するためのインポート COSINE_THRESHOLD = 0.363 NORML2_THRESHOLD = 1.128 # 12時間(秒単位) THROTTLE_TIME = 12 * 60 * 60 last_called_time = 0 # 最後に呼び出した時間を初期化 def match(recognizer, feature1, dictionary): for element in dictionary: user_id, feature2 = element score = recognizer.match(feature1, feature2, cv2.FaceRecognizerSF_FR_COSINE) if score > COSINE_THRESHOLD: return True, (user_id, score) return False, ( "" , 0.0) def call_function_when_recognized(user_id): global last_called_time current_time = time . time () # 最後に呼び出してから12時間経過しているかを確認 if current_time - last_called_time >= THROTTLE_TIME: print(f "認識されました: {user_id}" ) # 予定を音声再生なしで取得 schedule = get_weekly_schedule_with_audio(play_audio=False) print( "予定:" , schedule) # 予定を音声再生ありで取得 schedule = get_weekly_schedule_with_audio(play_audio=True) print( "音声で再生される予定:" , schedule) # notice.wavファイル以外の.wavファイルを削除 cleanup_audio_files(exclude_file= "notice.wav" ) # 最後に呼び出した時間を更新 last_called_time = current_time else : print( "まだ12時間経過していないため、次の呼び出しは行われません。" ) def cleanup_audio_files(exclude_file): "" "指定された.wavファイル以外の.wavファイルを削除する関数" "" directory = os.getcwd() # 現在のディレクトリを取得 wav_files = glob.glob(os.path. join (directory, "*.wav" )) # すべての.wavファイルを取得 for wav_file in wav_files: if os.path. basename (wav_file) != exclude_file: try: os.remove(wav_file) # 指定されたファイル以外を削除 print(f "削除しました: {wav_file}" ) except OSError as e: print(f "ファイル削除エラー: {wav_file}, {e}" ) def main(): directory = os.path. dirname (__file__) capture = cv2.VideoCapture(0) # Use the default camera if not capture.isOpened(): print( "Error: The webcam could not be opened." ) return dictionary = [] files = glob.glob(os.path. join (directory, "*.npy" )) for file in files: feature = np.load( file ) user_id = os.path.splitext(os.path. basename ( file ))[0] dictionary.append((user_id, feature)) weights = os.path. join (directory, "face_detection_yunet_2023mar.onnx" ) face_detector = cv2.FaceDetectorYN_create(weights, "" , (0, 0)) weights = os.path. join (directory, "face_recognizer_fast.onnx" ) face_recognizer = cv2.FaceRecognizerSF_create(weights, "" ) while True: result, image = capture. read () if not result: print( "Error: No image from webcam." ) break image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # Ensure image is in RGB height, width, _ = image.shape face_detector.setInputSize((width, height)) result, faces = face_detector.detect(image) faces = faces if faces is not None else [] for face in faces: aligned_face = face_recognizer.alignCrop(image, face) feature = face_recognizer.feature(aligned_face) result, user = match(face_recognizer, feature, dictionary) if result and user[0] != "unknown" : call_function_when_recognized(user[0]) # 顔が認識された時にカレンダーの予定取得を実行 # 適当な待機時間を設けてリソースの使用を抑える time . sleep (1) capture.release() if __name__ == '__main__' : main() |
というコードに変更
これでwebカメラの画面描画はなくなり
停止手段は ctrl + c で停止となる
実際に動かしたけど
M1macbookAir 16GB で
顔認識してからGoogle カレンダーを読み込み
Voicevox で音声ファイルを生成し、予定を読み上げるまで一分かかる
Docker ではなくインストールタイプにしたり
マシンスペックを上げれば短縮できるかもしれない