OpenCVの顔の識別機能とカレンダー読み上げの組み合わせ

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 ではなくインストールタイプにしたり
マシンスペックを上げれば短縮できるかもしれない