FaceRecognizerSFによる顔の認識の実践

FaceRecognizerSFによる顔の認識の実践

スマホの写真のサイズを1/4にすることで

generate_aligned_faces.py

による顔の切り出しが成功した

取り出した顔画像は
Face00x.jpg
となっているので、個人ごとの名前ファイルに改名する

次に
顔画像から特徴を抽出、特徴辞書として保存する

python generate_feature_dictionary.py snowpool.jpg

これで
snowpool.npy
が作成される

同様に家族分も実行する

python resize_save.py PXL_20240218_063620749.jpg

で画像ファイルを1/4にして

python generate_aligned_faces.py PXL_20240218_063620749_quarter.jpg 

で写真から顔を抽出

mv face001.jpg child.jpg

python generate_feature_dictionary.py child.jpg

これでそれぞれのnpyファイルができる

次に識別
モデルが変更になっているので

     weights = os.path.join(directory, "yunet.onnx")

    weights = os.path.join(directory, "face_detection_yunet_2023mar.onnx")

というように
指定するモデルを
変更する

編集するファイルは

face_recognizer.py

これで

python face_recognizer.py PXL_20240218_063620749.jpg 

を実行したら

OpenCV: Couldn't read video stream from file "/Users/snowpool/aw10s/face_recog/image.jpg"

となる

https://sites.google.com/iot-com.net/home/ホーム/実験室/jetson-nano/jetson-nanoのopen-cvで顔認証
によれば

#            return True, (user_id, cos_score)   ←オリジナルのtypo
            return True, (user_id, score)

とあるので

            # return True, (user_id, cos_score)
            return True, (user_id, score)

というように修正

そして
そのままだとファイル名が指定されているので
コマンドラインからファイル名を指定して実行できるように
ソースを変更する

Mainの部分を

# main関数の引数を追加 def main(image_path): # captureの初期化を変更 capture = cv2.VideoCapture(image_path)  # コマンドラインから指定された画像ファイル

として

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="Face Recognition")
    parser.add_argument('image', help="Path to the image file")
    args = parser.parse_args()
    main(args.image)

と書き換える

これで再度実行してみる

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/face_recog/face_recognizer.py", line 108, in <module>
    main(args.image)
  File "/Users/snowpool/aw10s/face_recog/face_recognizer.py", line 37, in main
    files = glob.glob(os.path.join(directory, "*.npy"))
NameError: name 'directory' is not defined

となった

原因は

    directory = os.path.dirname(__file__)

を削除したことでディレクトリの指定ができなくなっていた

import os
import sys
import glob
import numpy as np
import cv2
import argparse

COSINE_THRESHOLD = 0.363
NORML2_THRESHOLD = 1.128

# 特徴を辞書と比較してマッチしたユーザーとスコアを返す関数
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, cos_score)
            return True, (user_id, score)

    return False, ("", 0.0)

# def main():
#     # キャプチャを開く
#     directory = os.path.dirname(__file__)
#     capture = cv2.VideoCapture(os.path.join(directory, "image.jpg")) # 画像ファイル
# main関数の引数を追加
def main(image_path):
    # captureの初期化を変更
    directory = os.path.dirname(__file__)
    
    capture = cv2.VideoCapture(image_path)  # コマンドラインから指定された画像ファイル

    #capture = cv2.VideoCapture(0) # カメラ
    if not capture.isOpened():
        exit()

    # 特徴を読み込む
    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, "yunet.onnx")
    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 result is False:
            cv2.waitKey(0)
            break

        # 画像が3チャンネル以外の場合は3チャンネルに変換する
        channels = 1 if len(image.shape) == 2 else image.shape[2]
        if channels == 1:
            image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
        if channels == 4:
            image = cv2.cvtColor(image, cv2.COLOR_BGRA2BGR)

        # 入力サイズを指定する
        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 = "{0} ({1:.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)

        # 画像を表示する
        cv2.imshow("face recognition", image)
        key = cv2.waitKey(1)
        if key == ord('q'):
            break
    
    cv2.destroyAllWindows()

# if __name__ == '__main__':
#     main()
if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="Face Recognition")
    parser.add_argument('image', help="Path to the image file")
    args = parser.parse_args()
    main(args.image)


再度

python face_recognizer.py PXL_20240218_063620749.jpg 


実行したら顔の認識ができた

yolov8 を Google Colab で実行

yolov8のテスト

自動ラベルで作成したものが間違っているのか
それとも変換したのが問題なのかを知りたいので
一度試す

Colabで実験する

# Install ultralytics
!pip install ultralytics

でyolov8インストール

from google.colab import drive
drive.mount('/content/drive')

でマウント

!yolo obb train data=/content/drive/MyDrive/InventoryControl/daily_necessities_label/data.yaml pretrained=yolov8n-obb.pt epochs=100 imgsz=640 exist_ok=True

で学習

このコマンドは、YOLO (You Only Look Once) モデルを用いて物体検出の学習を行うためのものです。特に、YOLOv8n-obbモデルを用いて、向き付き境界ボックス(Oriented Bounding Boxes, OBB)を使用して物体を検出する訓練を行います。以下は各パラメータの詳細です:
* train: このオプションは、モデルを訓練モードに設定します。
* data=/content/drive/MyDrive/InventoryControl/daily_necessities_label/data.yaml: 訓練に使用するデータセットの設定ファイルのパスです。このYAMLファイルには、訓練データ、検証データのパスや、クラス名が含まれています。
* pretrained=yolov8n-obb.pt: 事前訓練済みのモデルファイル。このファイルを初期の重みとして使用して、訓練を開始します。
* epochs=100: モデルが訓練データを何回繰り返して学習するかを指定します。この場合、100回繰り返します。
* imgsz=640: 入力画像のサイズを640ピクセルにリサイズします。
* exist_ok=True: 既に訓練結果のフォルダが存在しても、エラーを出さずに上書きまたは新たに訓練を開始することを許可します。
このコマンドを実行することで、指定されたパラメータでYOLOモデルの訓練が行われ、物体検出の精度を向上させることができます。

とりあえず、バスクリンだけでなく
バスロマンも学習させる

そして肌おもいも学習させて、その状態から実行してみる

とりあえずバスロマンと肌おもいの画像からは
バスロマンをバスクリンと誤認識してるけど
カウントはできた

バスクリンの在庫を使い切ったため
バスロマンと肌おもいの写真で識別してみました

バスクリンとバスロマンを誤認識してますが
数は合っていますので
在庫管理には使えるとは思います

いっそバスロマンも学習すれば誤認識はなくなるかもしれません

以下ログと使用したテストの画像です

# Install ultralytics
!pip install ultralytics

でyolov8インストール

from google.colab import drive
drive.mount('/content/drive')

でgoogle driveマウント

!yolo obb train data=/content/drive/MyDrive/InventoryControl/daily_necessities_label/data.yaml pretrained=yolov8n-obb.pt epochs=400 exist_ok=True

で前回370程度のエポックで停止したので
今回は400にしてA100で実行

import os
import subprocess

source_file = '/content/drive/MyDrive/PXL_20240617_182349485.jpg'
# テキストファイルのパスを構築(画像ファイル名と同じ)
file_name, file_extension = os.path.splitext(source_file)
label_file_path = '/content/runs/obb/predict/labels/' + os.path.basename(file_name) + '.txt'
# ファイルの存在を確認し、存在する場合は削除
if os.path.exists(label_file_path):
    os.remove(label_file_path)
# YOLOを使用して予測を実行
!yolo obb predict model=/content/runs/obb/train/weights/best.pt source='{source_file}' save=True save_txt=True exist_ok=True
# ファイルが存在する場合のみ、テキストファイルの行数を取得して表示
if os.path.exists(label_file_path):
    num_lines = subprocess.check_output(["wc", "-l", label_file_path]).decode().split()[0]
    print("バスクリンの数は", num_lines)
else:
    print("ファイルが見つかりませんでした。")

実行結果は

Ultralytics YOLOv8.2.35 :rocket: Python-3.10.12 torch-2.3.0+cu121 CUDA:0 (NVIDIA A100-SXM4-40GB, 40514MiB)
YOLOv8n-obb summary (fused): 187 layers, 3077804 parameters, 0 gradients, 8.3 GFLOPs

image 1/1 /content/drive/MyDrive/PXL_20240617_182349485.jpg: 1024x800 143.7ms
Speed: 12.4ms preprocess, 143.7ms inference, 197.3ms postprocess per image at shape (1, 3, 1024, 800)
Results saved to runs/obb/predict
1 label saved to runs/obb/predict/labels
:bulb: Learn more at https://docs.ultralytics.com/modes/predict
バスクリンの数は 1

日用品の買い物の時に少しずつ写真を撮影し
学習データに使っていこうと思います

 

Yolov8 を webカメラで使う

Yolov8 を webカメラで使う

import cv2
from yolov8.utils.webcam import Webcam

def main():
    webcam = Webcam(source=0)  # 通常、0はデフォルトのWebカメラを示します
    while True:
        frame = webcam.get_frame()
        if frame is None:
            break
        # YOLOv8を使って画像上で物体検出を実行
        results = webcam.model(frame)
        # 検出結果の表示
        results.show()
        # 'q'キーが押されたら終了
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

if __name__ == '__main__':
    main()


chatgptの答えだが

実行すると

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/inventory/webcam_yv8.py", line 2, in <module>
    from yolov8.utils.webcam import Webcam
ModuleNotFoundError: No module named 'yolov8'

となるので
yolov8 webカメラ
で検索し調べる

【やってみた】YOLOv8の機能試す&Webカメラでリアルタイム推論

によれば
Webカメラの場合はカメラ番号を入れれば実行可能とのこと

from ultralytics import YOLO

model = YOLO("yolov8n.pt")
results = model(0 , show=True) 
for i in enumerate(results):
    print(i)

を実行したら
M1macbookair のカメラからyolov8が起動し
該当するものが判定された

なおこのコードの場合
Ctrl + c で止めるまでずっと動きます

OpenCVのSFaceで顔認証の準備

OpenCVのSFaceで顔認証

https://sites.google.com/iot-com.net/home/ホーム/実験室/jetson-nano/jetson-nanoのopen-cvで顔認証
を参考に
OpenCVにDNNを使用した顔認識SFaceが実装され、誰の顔かを認識できる様になったのとの記事を見つけて試してみました。
記事 : OpenCVの新しい顔認識を試してみる https://qiita.com/UnaNancyOwen/items/8c65a976b0da2a558f06
Github : OpenCV ObjDetect Module Face Recognition (SFace) Sample https://gist.github.com/UnaNancyOwen/49df508ad8b6d9520024354df0e3e740

顔認識は OpenCV 4.5.4 以上からの導入になる

基本的にはGithub のPython をコピペすればそのまま動作する

generate_aligned_faces.py
入力した写真から人の顔の部分を切り出して保存するプログラム
複数の人物が写っている場合は全員を切り出して
face001.jpg , face002.jpg ・・・ と名前を付けて保存する
出力されたファイル名を 人の名前に変更しておくと後々便利です。 
face001.jpg → taro.jpg
というようにリネームする

generate_feature_dictionary.py
切り出した顔のjpgファイルを読み込んで、顔の特徴量に変換するプログラム
顔写真 taro.jpg を入力すると 顔の特徴量 taro.npy が出力される
このnumpyファイルに各個人の顔の特徴量が128次元ベクトルに変換されて入る

face_recognizer.py
入力された写真に上記で作成した顔の特徴量が近い人が写っているかを判別するプログラム
特徴量の npyファイルは同じフォルダに入っているものが全て自動で読み込まれる
表示される名前は特徴量ファイル名となるので人物名をファイル名にした方がわかりやすい
類似した顔が無い場合には Unknown と表示

これらを元に実践する

generate_aligned_faces.py
で写真のファイルを引数にして実行

python generate_aligned_faces.py image.jpg

とすれば
写真に写っている人の分だけファイルができる
そのファイル名を人の名前に変更する

つまり全て
face001.jpg
という感じで
Face00x.jpg
となっているので写真ごとに名前を変える

次に
generate_feature_dictionary.py

切り出した顔のjpgファイルを読み込んで、顔の特徴量に変換するプログラムです。
例えば 顔写真 taro.jpg を入力すると 顔の特徴量 taro.npy が出力されます。
このnumpyファイルに各個人の顔の特徴量が128次元ベクトルに変換されて入ってます。

python generate_feature_dictionary.py face001.jpg
python generate_feature_dictionary.py face002.jpg

写真の人の分だけ実行すればOK
人物名なら

python generate_feature_dictionary.py tarojpg
python generate_feature_dictionary.py jiro.jpg
python generate_feature_dictionary.py hoge.jpg
python generate_feature_dictionary.py hogehoge.jpg

これで
顔の特徴量 taro.npy
というようなnpyファイルが作成される
実際には画像ファイル名.npyファイルになる

実行するにあたり
写真を用意する必要がある
横も認識したいのなら、横の写真も必要になる

とりあえず写真を探すこと
まずは自分の写真を撮影し
GooglePhotoからダウンロード

 cp ~/Downloads/PXL_20240612_091410912.jpg .

でコピー

作業ディレクトリは

/Users/snowpool/aw10s/face_recog

で行う

https://gist.github.com/UnaNancyOwen/49df508ad8b6d9520024354df0e3e740#file-face_recognizer-pyのコードをそのまま使う

Download Zip
でダウンロードし展開
中身を

 cp ~/Downloads/49df508ad8b6d9520024354df0e3e740-54e7dbd2f15b6137dc2b6d4ef6ce3143528c3978/* .

でコピー

ソースだけでなくモデルのダウンロードが必要

https://github.com/ShiqiYu/libfacedetection.train/blob/master/tasks/task1/onnx/yunet.onnx
をクリックしたら

404 - page not found
The 
master
 branch of 
libfacedetection.train
 does not contain the path 

https://drive.google.com/file/d/1ClK9WiB492c5OZFKveF3XiHCejoOxINW/view
については
プレビューできません

となる

とりあえずモデルのダウンロードを調べることにする

https://www.eranger.co.jp/blog/news/face-detection-recognition-by-opencv
の記事を参考に

face_detection_yunet_2023mar.onnx
で検索

https://github.com/opencv/opencv_zoo/blob/main/models/face_detection_yunet/face_detection_yunet_2023mar.onnx
にデータがあったので

https://github.com/opencv/opencv_zoo/tree/main
のreadmeを見てから

git clone https://github.com/opencv/opencv_zoo.git

でリポジトリのclone

cp opencv_zoo/models/face_detection_yunet/face_detection_yunet_2023mar.onnx .

で作業ディレクトリにコピー

python generate_aligned_faces.py PXL_20240612_091410912.jpg 

を実行したが

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/face_recog/generate_aligned_faces.py", line 60, in <module>
    main()
  File "/Users/snowpool/aw10s/face_recog/generate_aligned_faces.py", line 33, in main
    face_detector = cv2.FaceDetectorYN_create(weights, "", (0, 0))
cv2.error: OpenCV(4.10.0) /Users/xperience/GHA-Actions-OpenCV/_work/opencv-python/opencv-python/opencv/modules/dnn/src/onnx/onnx_importer.cpp:277: error: (-5:Bad argument) Can't read ONNX file: /Users/snowpool/aw10s/face_recog/yunet.onnx in function 'ONNXImporter'

となる

python

でPythonインタープリターを使用し

import cv2
print(cv2.__version__)

でバージョンを確認すると
4.10.0
となった

pip show opencv-python

Name: opencv-python
Version: 4.8.0.74
Summary: Wrapper package for OpenCV python bindings.
Home-page: https://github.com/opencv/opencv-python
Author: 
Author-email: 
License: Apache 2.0
Location: /Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages
Requires: numpy, numpy, numpy, numpy, numpy
Required-by: ultralytics

だとバージョンが違う

Pythonインタープリターで
現在のインポートされているOpenCVの場所を確認

import cv2
print(cv2.__file__)

の結果は

/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/cv2/__init__.py

システムパスの確認

import sys
print(sys.path)

の結果は

['', '/Users/snowpool/.pyenv/versions/3.10.6/lib/python310.zip', '/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10', '/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/lib-dynload', '/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages']

その前によくREADMEを読んで再度実行

rm face_detection_yunet_2023mar.onnx

で一度削除し

cd opencv_zoo

git lfs install

これで
Git LFSが有効化され、大きなファイルを扱う準備が整う

git lfs pull
Git LFSを用いて管理されているファイル(大容量のファイルなど)をダウンロードします。git clone はリポジトリのメタデータと小さなファイルのみをダウンロードするため、git lfs pull を使用してLFSを介して管理されている大きなファイルを取得する必要があります
とのこと

cp opencv_zoo/models/face_detection_yunet/face_detection_yunet_2023mar.onnx .

でファイルをコピー

次に
* face_recognizer_fast.onnx
これもダウンロードできなかったので
githubで検索

https://github.com/MYJLAB-2022-HackThon/FaceServer/blob/27ce7099eb3ec46bb07d988b9681e9cc2a6b291c/app/face_recognizer_fast.onnx
にあったので

git clone https://github.com/MYJLAB-2022-HackThon/FaceServer.git

でリポジトリをclone

cp FaceServer/app/face_recognizer_fast.onnx .

でファイルをコピー

これで再度

 python generate_aligned_faces.py PXL_20240612_091410912.jpg

を実行したら

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/face_recog/generate_aligned_faces.py", line 60, in <module>
    main()
  File "/Users/snowpool/aw10s/face_recog/generate_aligned_faces.py", line 33, in main
    face_detector = cv2.FaceDetectorYN_create(weights, "", (0, 0))
cv2.error: OpenCV(4.10.0) /Users/xperience/GHA-Actions-OpenCV/_work/opencv-python/opencv-python/opencv/modules/dnn/src/onnx/onnx_importer.cpp:277: error: (-5:Bad argument) Can't read ONNX file: /Users/snowpool/aw10s/face_recog/yunet.onnx in function 'ONNXImporter'

となる

多分

    # モデルを読み込む
    weights = os.path.join(directory, "yunet.onnx")
    face_detector = cv2.FaceDetectorYN_create(weights, "", (0, 0))
    weights = os.path.join(directory, "face_recognizer_fast.onnx")

のonnxファイルの指定を変えればいけるはず

generate_aligned_faces.py
の中の

    # weights = os.path.join(directory, "yunet.onnx")

    weights = os.path.join(directory, "face_detection_yunet_2023mar.onnx")

にして実行

とりあえず

import os
import argparse
import numpy as np
import cv2

def main():
    # 引数をパースする
    parser = argparse.ArgumentParser("generate aligned face images from an image")
    parser.add_argument("image", help="input image file path (./image.jpg)")
    args = parser.parse_args()

    # 引数から画像ファイルのパスを取得
    path = args.image
    directory = os.path.dirname(args.image)
    if not directory:
        directory = os.path.dirname(__file__)
        path = os.path.join(directory, args.image)

    # 画像を開く
    image = cv2.imread(path)
    if image is None:
        exit()

    # 画像が3チャンネル以外の場合は3チャンネルに変換する
    channels = 1 if len(image.shape) == 2 else image.shape[2]
    if channels == 1:
        image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
    if channels == 4:
        image = cv2.cvtColor(image, cv2.COLOR_BGRA2BGR)

    # モデルを読み込む
    # weights = os.path.join(directory, "yunet.onnx")
    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, "")

    # 入力サイズを指定する
    height, width, _ = image.shape
    face_detector.setInputSize((width, height))

    # 顔を検出する
    _, faces = face_detector.detect(image)

    # 検出された顔を切り抜く
    aligned_faces = []
    if faces is not None:
        for face in faces:
            aligned_face = face_recognizer.alignCrop(image, face)
            aligned_faces.append(aligned_face)

    # 画像を表示、保存する
    for i, aligned_face in enumerate(aligned_faces):
        cv2.imshow("aligned_face {:03}".format(i + 1), aligned_face)
        cv2.imwrite(os.path.join(directory, "face{:03}.jpg".format(i + 1)), aligned_face)

    cv2.waitKey(0)
    cv2.destroyAllWindows()

if __name__ == '__main__':
    main()

として

サンプル画像をダウンロード後

python generate_aligned_faces.py 136777535-36d6bce1-91bf-446c-9377-645cc60b9c65.jpg

とすると
face001.jpg
face002.jpg
が作成されますが

スマホで撮影した画像で

python generate_aligned_faces.py PXL_20240612_091410912.jpg

とすると処理が終わりません

ファイルサイズなども関連しているかもしれないため一度ファイルサイズなども調べてみます

ファイル情報を調べたいので

 vim file_info.py

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()

で
python file_info.py PXL_20240612_091410912.jpg

結果は

画像の幅: 2736 ピクセル
画像の高さ: 3648 ピクセル
色チャネル数: 3
ファイルサイズ: 2319152 バイト
python file_info.py 136777535-36d6bce1-91bf-446c-9377-645cc60b9c65.jpg

だと

画像の幅: 450 ピクセル
画像の高さ: 312 ピクセル
色チャネル数: 3
ファイルサイズ: 26914 バイト

リサイズして半分にしてから実行したいので

vim resize_save.py

中身は

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]

    # 新しい寸法を計算(元のサイズの半分)
    new_width = width // 2
    new_height = height // 2

    # 画像をリサイズ
    resized_image = cv2.resize(image, (new_width, new_height))

    # 新しいファイル名を設定
    new_file_path = os.path.splitext(args.image_path)[0] + "_resized.jpg"

    # リサイズした画像を保存
    cv2.imwrite(new_file_path, resized_image)
    print(f"リサイズされた画像が保存されました: {new_file_path}")

if __name__ == '__main__':
    main()

これを実行し

python resize_save.py PXL_20240612_091410912.jpg    

でファイルサイズを半分にしたが結果は同じのため

python file_info.py PXL_20240612_091410912_resized.jpg

でさらに半分にすることでようやく

Face001.jpg

が作成できた

これにより
Pixcel8で撮影したスマホの写真で顔データを作る場合には
元画像の1/4にする必要があることが判明

ちなみにファイルの比較は

python file_info.py PXL_20240612_091410912_resized_resized.jpg 
画像の幅: 684 ピクセル
画像の高さ: 912 ピクセル
色チャネル数: 3
ファイルサイズ: 228769 バイト
python file_info.py PXL_20240612_091410912.jpg 
画像の幅: 2736 ピクセル
画像の高さ: 3648 ピクセル
色チャネル数: 3
ファイルサイズ: 2319152 バイト

画像データの水増し

画像データの水増し

Yolov8で使うファイルの水増しをする

回転以外で行うことは

1. 反転(Flipping): 画像を水平または垂直に反転させることで、新たな画像を生成します。物体が画像中で異なる方向に存在する場合の学習が可能になります。
2. スケーリング(Scaling): 画像のサイズを変更します。これにより、モデルが異なる解像度の画像に対しても頑健になります。
3. クロッピング(Cropping): 画像から一部を切り取ることで、モデルが部分的な視覚情報からも物体を識別できるようになります。ランダムクロッピングは特に有効です。
4. 色調の変更(Color Modification): 色の明度、コントラスト、彩度を変化させることで、異なる照明条件下での物体の見え方に対する耐性を高めます。
5. ノイズの追加: 画像にランダムなノイズを加えることで、モデルがノイズに対して頑健になります。例えばガウシアンノイズなどがあります。
6. ぼかし(Blurring): ガウシアンぼかし、平均ぼかしなどを画像に適用し、モデルが解像度の低い画像や細部がぼやけた画像にも対応できるようにします。
7. 画像のエラスティック変形(Elastic Transformations): 画像を局所的に引き伸ばしたり圧縮したりすることで、自然界で発生するさまざまな変形に対応します。
8. ランダムイラジ(Jittering): 色彩や輝度などの小さなランダムな変更を加えて、画像の見た目を微妙に変化させます。

なのでこれらを行うpythonスクリプトを作成する

pip install Pillow numpy

で必要なライブラリのインストール

import os
import numpy as np
from PIL import Image, ImageEnhance, ImageOps
import random

def rotate_image(image, degrees):
    return image.rotate(degrees, expand=True)

def color_change(image):
    enhancer = ImageEnhance.Color(image)
    factor = random.uniform(0.5, 1.5)  # 色の強さを変更
    return enhancer.enhance(factor)

def flip_image(image):
    if random.choice([True, False]):
        return image.transpose(Image.FLIP_LEFT_RIGHT)
    else:
        return image

def scale_image(image):
    scale_factor = random.uniform(0.75, 1.25)
    width, height = image.size
    new_width = int(width * scale_factor)
    new_height = int(height * scale_factor)
    return image.resize((new_width, new_height), Image.ANTIALIAS)

def add_noise(image):
    # PIL Imageをnumpy arrayに変換
    array = np.asarray(image)
    noise = np.random.randint(0, 50, array.shape, dtype='uint8')
    # ノイズを加えた後、再びPIL Imageに変換
    image = Image.fromarray(np.clip(array + noise, 0, 255).astype('uint8'), 'RGB')
    return image

def process_images(directory):
    for filename in os.listdir(directory):
        filepath = os.path.join(directory, filename)
        if filepath.lower().endswith(('.png', '.jpg', '.jpeg')):
            with Image.open(filepath) as img:
                for angle in range(0, 360, 45):
                    new_img = rotate_image(img, angle)
                    new_img = color_change(new_img)
                    new_img = flip_image(new_img)
                    new_img = scale_image(new_img)
                    new_img = add_noise(new_img)
                    new_img.save(os.path.join(directory, f'{filename[:-4]}_rot{angle}_mod.png'))

# 画像が保存されているディレクトリを指定してください
directory_path = 'path_to_your_images'
process_images(directory_path)


これらを行うコード

これを

vim augment_images.py

で作成して保存

ちなみに

#ドライブをマウント
from google.colab import drive
drive.mount('/content/drive')

の後に

cd /content/drive/MyDrive/InventoryControl/daily_necessities/baskulin
ls

でバスクリン画像の一覧が表示された

ということはおそらく各種ファイルを転送し
その後にスクリプトで増やせばいけるはず

コードを修正しimageフォルダ内を対象としたが
これだと回転以外の要素も全て行っている
そうではなく
それぞれの処理をしたファイルを生成して欲しいので
コードを変更する

rm -f image/*

で一度ファイルを削除し

cp data_bak/Baskulin1.jpg image

でバックアップから復元

pip install opencv-python-headless scipy

そしてコードを

import os
import numpy as np
from PIL import Image, ImageEnhance, ImageOps, ImageFilter
import random
import cv2  # OpenCVを使用

def rotate_image(image, degrees, directory, filename):
    new_img = image.rotate(degrees, expand=True)
    new_img.save(os.path.join(directory, f'{filename[:-4]}_rot{degrees}.png'))

def flip_image(image, directory, filename):
    # 水平反転
    new_img = image.transpose(Image.FLIP_LEFT_RIGHT)
    new_img.save(os.path.join(directory, f'{filename[:-4]}_fliph.png'))
    # 垂直反転
    new_img = image.transpose(Image.FLIP_TOP_BOTTOM)
    new_img.save(os.path.join(directory, f'{filename[:-4]}_flipv.png'))

def scale_image(image, directory, filename):
    scale_factor = random.uniform(0.75, 1.25)
    new_width = int(image.width * scale_factor)
    new_height = int(image.height * scale_transform)
    new_img = image.resize((new_width, new_height), Image.ANTIALIAS)
    new_img.save(os.path.join(directory, f'{filename[:-4]}_scaled.png'))

def crop_image(image, directory, filename):
    start_x = random.randint(0, int(image.width * 0.1))
    start_y = random.randint(0, int(image.height * 0.1))
    end_x = start_x + int(image.width * 0.8)
    end_y = start_y + int(image.height * 0.8)
    new_img = image.crop((start_x, start_y, end_x, end_y))
    new_img.save(os.path.join(directory, f'{filename[:-4]}_cropped.png'))

def color_change(image, directory, filename):
    factor = random.uniform(0.5, 1.5)
    enhancer = ImageEnhance.Color(image)
    new_img = enhancer.enhance(factor)
    new_img.save(os.path.join(directory, f'{filename[:-4]}_colorchg.png'))

def add_noise(image, directory, filename):
    array = np.array(image)
    noise = np.random.normal(0, 25, image.size)
    new_img = Image.fromarray(np.clip(array + noise[:, :, None], 0, 255).astype('uint8'), 'RGB')
    new_img.save(os.path.join(directory, f'{filename[:-4]}_noise.png'))

def blur_image(image, directory, filename):
    new_img = image.filter(ImageFilter.GaussianBlur(radius=5))
    new_img.save(os.path.join(directory, f'{filename[:-4]}_blurred.png'))

def elastic_transform(image, directory, filename):
    alpha = image.width * 2
    sigma = image.width * 0.08
    random_state = np.random.RandomState(None)
    shape = image.size[::-1]
    dx = gaussian_filter((random_state.rand(*shape) * 2 - 1), sigma, mode="constant", cval=0) * alpha
    dy = gaussian_filter((random_state.rand(*shape) * 2 - 1), sigma, mode="constant", cval=0) * alpha
    x, y = np.meshgrid(np.arange(shape[0]), np.arange(shape[1]), indexing="ij")
    indices = np.reshape(x + dx, (-1, 1)), np.reshape(y + dy, (-1, 1))
    distored_image = map_coordinates(np.array(image), indices, order=1, mode='reflect')
    new_img = Image.fromarray(distored_image.astype('uint8'), 'RGB')
    new_img.save(os.tmp.join(directory, f'{filename[:-4]}_elastic.png'))

def jitter_image(image, directory, filename):
    new_img = ImageEnhance.Brightness(image).enhance(random.uniform(0.5, 1.5))
    new_img = ImageEnhance.Contrast(new_img).enhance(random.uniform(0.5, 1.5))
    new_img.save(os.path.join(directory, f'{filename[:-4]}_jittered.png'))

def process_images(directory):
    for filename in os.listdir(directory):
        filepath = os.path.join(directory, filename)
        if filepath.lower().endswith(('.png', '.jpg', '.jpeg')):
            with Image.open(filepath) as img:
                for angle in [0, 45, 90, 135, 180, 225, 270, 315]:
                    rotate_image(img, angle, directory, filename)
                flip_image(img, directory, filename)
                scale_image(img, directory, filename)
                crop_image(img, directory, filename)
                color_change(img, directory, filename)
                add_noise(img, directory, filename)
                blur_image(img, directory, filename)
                elastic_transform(img, directory, filename)
                jitter_image(img, directory, filename)

directory_path = 'images'
process_images(directory_path)

へ変更し

python augment_images.py

で実行

しかし

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 85, in <module>
    process_images(directory_path)
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 76, in process_images
    scale_image(img, directory, filename)
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 22, in scale_image
    new_height = int(image.height * scale_transform)
NameError: name 'scale_transform' is not defined

となる

となるため

def scale_image(image, directory, filename):
    scale_factor = random.uniform(0.75, 1.25)  # スケーリングファクターをランダムに選択
    new_width = int(image.width * scale_factor)  # 正しい変数名で幅を計算
    new_height = int(image.height * scale_factor)  # 正しい変数名で高さを計算
    new_img = image.resize((new_width, new_height), Image.ANTIALIAS)  # 画像をリサイズ
    new_img.save(os.path.join(directory, f'{filename[:-4]}_scaled.png'))  # 保存

にコードを変更

今度は

/Users/snowpool/aw10s/inventory/augment_images.py:23: DeprecationWarning: ANTIALIAS is deprecated and will be removed in Pillow 10 (2023-07-01). Use LANCZOS or Resampling.LANCZOS instead.
  new_img = image.resize((new_width, new_height), Image.ANTIALIAS)  # 画像をリサイズ
Traceback (most recent call last):
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 86, in <module>
    process_images(directory_path)
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 82, in process_images
    elastic_transform(img, directory, filename)
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 56, in elastic_transform
    dx = gaussian_filter((random_state.rand(*shape) * 2 - 1), sigma, mode="constant", cval=0) * alpha
NameError: name 'gaussian_filter' is not defined

となる

再度コードを変更

1. ANTIALIAS を LANCZOS に修正する
Pillowライブラリで ANTIALIAS が非推奨になったため、これを LANCZOS に置き換えることで解決します。以下が修正後の scale_image 関数です:


def scale_image(image, directory, filename):
    scale_factor = random.uniform(0.75, 1.25)
    new_width = int(image.width * scale_factor)
    new_height = int(image.height * scale_factor)
    new_img = image.resize((new_width, new_height), Image.LANCZOS)  # LANCZOSを使用
    new_img.save(os.path.join(directory, f'{filename[:-4]}_scaled.png'))


gaussian_filter の未定義問題を解決する
gaussian_filter 関数が未定義というエラーを修正するために、scipy.ndimage から gaussian_filter をインポートする必要があります。また、map_coordinates 関数も同様にインポートする必要があります。以下が修正後の elastic_transform 関数と、必要なインポート文です

がGPTの答えだったので

from scipy.ndimage import gaussian_filter, map_coordinates

def elastic_transform(image, directory, filename):
    alpha = image.width * 2
    sigma = image.width * 0.08
    random_state = np.random.RandomState(None)
    shape = image.size[::-1]
    dx = gaussian_filter((random_state.rand(*shape) * 2 - 1), sigma, mode="constant", cval=0) * alpha
    dy = gaussian_filter((random_state.rand(*shape) * 2 - 1), sigma, mode="constant", cval=0) * alpha
    x, y = np.meshgrid(np.arange(shape[0]), np.arange(shape[1]), indexing="ij")
    indices = (np.reshape(x + dx, (-p, 1)), np.reshape(y + dy, (-1, 1)))
    distorted_image = map_coordinates(np.array(image), indices, order=1, mode='reflect')
    new_img = Image.fromarray(distorted_image.astype('uint8'), 'RGB')
    new_img.save(os.path.join(directory, f'{filename[:-4]}_elastic.png'))

これを実行したら

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 89, in <module>
    process_images(directory_path)
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 85, in process_images
    elastic_transform(img, directory, filename)
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 62, in elastic_transform
    indices = (np.reshape(x + dx, (-p, 1)), np.reshape(y + dy, (-1, 1)))
NameError: name 'p' is not defined. Did you mean: 'np'?

となるのでこれを修正

このログでGPTで調べる

lastic_transform 関数内で未定義の変数 p を使用していることがわかります。これはおそらく -1 を意図していたものと思われます。さらに、indices の設定に関するエラーも修正する必要があります。これを修正して、適切なインデックスの形状に合わせましょう。
以下は修正後の elastic_transform 関数
from scipy.ndimage import gaussian_filter, map_coordinates

def elastic_transform(image, directory, filename):
    alpha = image.width * 2
    sigma = image.width * 0.08
    random_state = np.random.RandomState(None)
    shape = image.size[::-1]
    dx = gaussian_filter((random_state.rand(*shape) * 2 - 1), sigma, mode="constant", cval=0) * alpha
    dy = gaussian_filter((random_yes.rand(*shape) * 2 - 1), sigma, mode="constant", cval=0) * alpha
    x, y = np.meshgrid(np.arange(shape[0]), np.arange(shape[1]), indexing="ij")
    indices = np.reshape(np.array([x + dx, y + dy]), (2, -1))
    distorted_image = map_coordinates(np.array(image), indices, order=1, mode='reflect')
    new_img = Image.fromarray(distorted_image.astype('uint8'), 'RGB')
    new_img.save(os.path.join(directory, f'{filename[:-4]}_elastic.png'))

今度は

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 90, in <module>
    process_images(directory_path)
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 84, in process_images
    add_noise(img, directory, filename)
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 44, in add_noise
    new_img = Image.fromarray(np.clip(array + noise[:, :, None], 0, 255).astype('uint8'), 'RGB')
ValueError: operands could not be broadcast together with shapes (4080,3072,3) (3072,4080,1) 

というエラーになるため
コードを

def add_noise(image, directory, filename):
    array = np.array(image)
    # 正しい形状のノイズを生成
    noise = np.random.normal(0, 25, array.shape)
    # ノイズを加えた画像を作成
    noisy_array = np.clip(array + noise, 0, 255).astype('uint8')
    new_img = Image.fromarray(noisy_array, 'RGB')
    new_img.save(os.path.join(directory, f'{filename[:-4]}_noise.png'))

へ変更

このエラーは、add_noise 関数内で、ノイズを加えようとした際に配列の形状が一致しないため発生しています。具体的には、画像データ array と生成したノイズ noise の形状が異なっています

再度実行すると

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 94, in <module>
    process_images(directory_path)
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 90, in process_images
    elastic_transform(img, directory, filename)
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 64, in elastic_transform
    dy = gaussian_filter((random_yes.rand(*shape) * 2 - 1), sigma, mode="constant", cval=0) * alpha
NameError: name 'random_yes' is not defined

となる

これは単純にタイプミスで
random_yes を random_state に修正

今度は

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 94, in <module>
    process_images(directory_path)
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 90, in process_images
    elastic_transform(img, directory, filename)
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 67, in elastic_transform
    distorted_image = map_coordinates(np.array(image), indices, order=1, mode='reflect')
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/scipy/ndimage/_interpolation.py", line 440, in map_coordinates
    raise RuntimeError('invalid shape for coordinate array')
RuntimeError: invalid shape for coordinate array

となる

from scipy.ndimage import gaussian_filter, map_coordinates

def elastic_transform(image, directory, filename):
    alpha = image.width * 2
    sigma = image.width * 0.08
    random_state = np.random.RandomState(None)
    shape = image.size[::-1]  # width, height

    # dx, dy を生成
    dx = gaussian_filter((random_state.rand(*shape) * 2 - 1), sigma, mode="constant", cval=0) * alpha
    dy = gaussian_filter((random_state.rand(*shape) * 2 - 1), sigma, mode="constant", cval=0) * alpha

    x, y = np.meshgrid(np.arange(shape[0]), np.arange(shape[1]), indexing="ij")
    
    # 正しい形状でインデックスを作成
    indices = np.vstack((x.ravel() + dx.ravel(), y.ravel() + dy.ravel()))  # 2xN の形状

    # 座標変換を適用
    distorted_image = map_coordinates(np.array(image), indices, order=1, mode='reflect')

    # 新しい画像を生成
    new_img = Image.fromarray(distorted_image.reshape(image.size).astype('uint8'), 'RGB')
    new_img.save(os.path.join(directory, f'{filename[:-4]}_elastic.png'))

というように修正

indices の配列を2行(x座標、y座標)の形状に整形しています。np.vstack を使用して2つの1次元配列を縦に積み重ね、期待される形状を作成しています。
distorted_image の生成時に reshape を適用して画像の元の形状を復元しています。

なお画像認識で文字の反転は不要なので削除する

# flip_image(img, directory, filename) # この行を削除
として、flip_image 関数自体を削除するのではなく、その呼び出しをコメントアウトまたは削除しています。将来的に反転が必要になった場合に簡単に再導入できるようにするため

今度は

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 104, in <module>
    process_images(directory_path)
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 100, in process_images
    elastic_transform(img, directory, filename)
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 75, in elastic_transform
    distorted_image = map_coordinates(np.array(image), indices, order=1, mode='reflect')
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/scipy/ndimage/_interpolation.py", line 440, in map_coordinates
    raise RuntimeError('invalid shape for coordinate array')
RuntimeError: invalid shape for coordinate array

となる

なので変更

重要な修正点
* np.meshgrid を使用して x, y の座標を生成する際、indexing='ij' の指定を取り除き、順序を逆にして np.arange(shape[1]), np.arange(shape[0]) としています。これにより、x と y の座標を正しく取得します。
* indices の形状を (2, M) に正しく設定しています。vstack で縦に積み重ね、ピクセルの y 座標と x 座標を合わせています。

とのこと

from scipy.ndimage import gaussian_filter, map_coordinates

def elastic_transform(image, directory, filename):
    alpha = image.width * 2
    sigma = image.width * 0.08
    random_state = np.random.RandomState(None)
    shape = image.size[::-1]  # (width, height)

    # dx, dy を生成
    dx = gaussian_filter((random_state.rand(*shape) * 2 - 1), sigma, mode="constant", cval=0) * alpha
    dy = gaussian_filter((random_state.rand(*shape) * 2 - 1), sigma, mode="constant", cval=0) * alpha

    x, y = np.meshgrid(np.arange(shape[1]), np.arange(shape[0]))  # (height, width) 形式で生成されたグリッド
    indices = np.vstack((y.ravel() + dy.ravel(), x.ravel() + dx.ravel()))  # y座標、x座標の順に並べる

    # 座標変換を適用
    distorted_image = map_coordinates(np.array(image), indices, order=1, mode='reflect')
    new_img = Image.fromarray(distorted_image.astype('uint8'), 'RGB')
    new_img.save(os.path.join(directory, f'{filename[:-4]}_elastic.png'))

へ変更し保存

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 98, in <module>
    process_images(directory_path)
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 94, in process_images
    elastic_transform(img, directory, filename)
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 71, in elastic_transform
    distorted_image = map_coordinates(np.array(image), indices, order=1, mode='reflect')
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/scipy/ndimage/_interpolation.py", line 440, in map_coordinates
    raise RuntimeError('invalid shape for coordinate array')
RuntimeError: invalid shape for coordinate array

となる

from scipy.ndimage import gaussian_filter, map_coordinates
import numpy as np
from PIL import Image

def elastic_transform(image, directory, filename):
    alpha = image.width * 2  # 変形の強さを設定
    sigma = image.width * 0.08  # ガウスフィルタの標準偏差を設定
    random_state = np.random.RandomState(None)
    shape = image.size  # PIL Imageのsizeは(width, height)

    # ランダム変位を生成
    dx = gaussian_filter((random_state.rand(*shape[::-1]) * 2 - 1), sigma, mode="constant", cval=0) * alpha
    dy = gaussian_filter((random_state.rand(*shape[::-1]) * 2 - 1), sigma, mode="constant", cval=0) * alpha

    # グリッドを生成
    x, y = np.meshgrid(np.arange(shape[0]), np.arange(shape[1]), indexing='ij')
    indices = np.vstack((y.flatten() + dy.flatten(), x.flatten() + dx.flatten()))  # 2xNの形状

    # 座標変換を適用
    distorted_image = map_coordinates(np.array(image), indices, order=1, mode='reflect')

    # 新しい画像を作成
    new_img = Image.fromarray(distorted_image.reshape(shape[::-1]).astype('uint8'), 'RGB')
    new_img.save(os.path.join(directory, f'{filename[:-4]}_elastic.png'))

へコード変更

shape = image.sizeで取得したサイズ(width, height)は、numpyの操作に合わせてshape[::-1]((height, width))を使っています。
indices配列を生成する際、np.vstackを使ってy座標とx座標を適切に並べて(2, M)形状にしています。
map_coordinatesの引数に渡すindicesが(2, M)形状であることを確認してください。この形状が要求されるため、それに従っています。

今度は

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 103, in <module>
    process_images(directory_path)
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 99, in process_images
    elastic_transform(img, directory, filename)
  File "/Users/snowpool/aw10s/inventory/augment_images.py", line 74, in elastic_transform
    distorted_image = map_coordinates(np.array(image), indices, order=1, mode='reflect')
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/scipy/ndimage/_interpolation.py", line 440, in map_coordinates
    raise RuntimeError('invalid shape for coordinate array')
RuntimeError: invalid shape for coordinate array

今度は

from scipy.ndimage import gaussian_filter, map_coordinates
import numpy as np
from PIL import Image

def elastic_transform(image, directory, filename):
    alpha = image.width * 2  # 変形の強さを設定
    sigma = image.width * 0.08  # ガウスフィルタの標準偏差を設定
    random_state = np.random.RandomState(None)
    shape = image.size[::-1]  # PIL Imageのsizeは(width, height)、numpyでのshapeは(height, width)

    # ランダム変位を生成
    dx = gaussian_filter((random_state.rand(*shape) * 2 - 1), sigma, mode="constant", cval=0) * alpha
    dy = gaussian_filter((random_state.rand(*shape) * 2 - 1), sigma, mode="constant", cval=0) * alpha

    # グリッドを生成
    x, y = np.meshgrid(np.arange(shape[1]), np.arange(shape[0]))  # (height, width) 形式で生成されたグリッド
    indices = np.vstack((x.flatten() + dx.flatten(), y.flatten() + dy.flatten()))  # x座標、y座標の順に並べる

    # 座標変換を適用
    distorted_array = map_coordinates(np.array(image), indices, order=1, mode='reflect')
    distorted_image = Image.fromarray(distorted_array.reshape(shape).astype('uint8'), 'RGB')
    distorted_image.save(os.path.join(directory, f'{filename[:-4]}_elastic.png'))

indicesの生成を再確認し、正しい形状(2, M)が生成されていることを保証します。
map_coordinatesへの入力としてindicesが(2, M)形状であることを再確認します。これにはx.flatten()とy.flatten()を正しく配置しています。

何度やっても画像のエラスティック変形でエラーになるので
機能を削除

import os
import numpy as np
from PIL import Image, ImageEnhance, ImageOps, ImageFilter
import random

def rotate_image(image, degrees, directory, filename):
    new_img = image.rotate(degrees, expand=True)
    new_img.save(os.path.join(directory, f'{filename[:-4]}_rot{degrees}.png'))

def scale_image(image, directory, filename):
    scale_factor = random.uniform(0.75, 1.25)
    new_width = int(image.width * scale_factor)
    new_height = int(image.height * scale_factor)
    new_img = image.resize((new_width, new_height), Image.LANCZOS)
    new_img.save(os.path.join(directory, f'{filename[:-4]}_scaled.png'))

def crop_image(image, directory, filename):
    start_x = random.randint(0, int(image.width * 0.1))
    start_y = random.randint(0, int(image.height * 0.1))
    end_x = start_x + int(image.width * 0.8)
    end_y = start_y + int(image.height * 0.8)
    new_img = image.crop((start_x, start_y, end_x, end_y))
    new_img.save(os.path.join(directory, f'{filename[:-4]}_cropped.png'))

def color_change(image, directory, filename):
    factor = random.uniform(0.5, 1.5)
    enhancer = ImageEnhance.Color(image)
    new_img = enhancer.enhance(factor)
    new_img.save(os.path.join(directory, f'{filename[:-4]}_colorchg.png'))

def add_noise(image, directory, filename):
    img_array = np.array(image)
    noise = np.random.normal(0, 25, img_array.shape)
    noisy_array = np.clip(img_array + noise, 0, 255).astype('uint8')
    new_img = Image.fromarray(noisy_array, 'RGB')
    new_img.save(os.path.join(directory, f'{filename[:-4]}_noise.png'))

def blur_image(image, directory, filename):
    new_img = image.filter(ImageFilter.GaussianBlur(radius=5))
    new_img.save(os.path.join(directory, f'{filename[:-4]}_blurred.png'))

def process_images(directory):
    for filename in os.listdir(directory):
        filepath = os.path.join(directory, filename)
        if filepath.lower().endswith(('.png', '.jpg', '.jpeg')):
            with Image.open(filepath) as img:
                for angle in [0, 45, 90, 135, 180, 225, 270, 315]:
                    rotate_image(img, angle, directory, filename)
                scale_image(img, directory, filename)
                crop_image(img, directory, filename)
                color_change(img, directory, filename)
                add_noise(img, directory, filename)
                blur_image(img, directory, filename)
                # elastic_transform 呼び出しを削除しました

# ディレクトリパスを指定
directory_path = 'images'
process_images(directory_path)

これでようやく成功
元画像に対し12の画像が作成される

とりあえず成功したので

cp data_bak/Baskulin*.jpg image 

でバックアップから画像を戻し

python augment_images.py

で画像を増やす

roboflow Workspaceでアノテーションしたデータを Google Colabで使う

roboflow Workspaceでアノテーションしたデータを Google Colabで使う

ダウンロードして解凍したファイル
anotation_test.v1i.yolov8-obb

Google Drive へアップロード

これをGoogleColabで学習する

使用するのはT4GPU

まずは

# Install ultralytics
!pip install ultralytics


ultralyticsをインストール

from google.colab import drive
drive.mount('/content/drive')

でGoogle Driveのデータを使うためドライブをマウント

次にYamlファイルを探す

/content/drive/MyDrive/InventoryControl/DailyNecessities/anotation_test.v1i.yolov8-obb/data.yaml

がパスだったので

!yolo obb train data=/content/drive/MyDrive/InventoryControl/DailyNecessities/anotation_test.v1i.yolov8-obb/data.yaml pretrained=yolov8n-obb.pt epochs=100 imgsz=640 exist_ok=True

で実行

エポック100回で学習
モデルは小型のyolov8n-obb.ptにしましたが、
(n)の部分を(s),(l),(x)など変更可能

なお実行結果は

Runs/obb/train/

の中に格納されている

学習したモデルで推論するので
Testフォルダにある画像で推論するので
対象ファイルを右クリックしコピー

best.pt
を右クリックしパスをコピーする

今回なら
/content/runs/obb/train/weights/best.pt
となる

あとは
model= にモデルのパス と source=’ ‘ にテスト画像パス を設定して推論

とりあえず

/content/drive/MyDrive/InventoryControl/DailyNecessities/anotation_test.v1i.yolov8-obb/test/images/Baskulin3_rot270_006_jpg.rf.8e9ca7b3fac80e0bb5b1a45dedae21d1.jpg

の画像を使うことにする

!yolo obb predict model=/content/runs/obb/train/weights/best.pt source='/content/drive/MyDrive/InventoryControl/DailyNecessities/anotation_test.v1i.yolov8-obb/test/images/Baskulin3_rot270_006_jpg.rf.8e9ca7b3fac80e0bb5b1a45dedae21d1.jpg' save=True save_txt=True exist_ok=True

を実行

とりあえず1つだけの画像でテストしたら

/content/runs/obb/predict/labels/Baskulin3_rot270_006_jpg.rf.8e9ca7b3fac80e0bb5b1a45dedae21d1.txt

にラベルがあり

/content/runs/obb/predict/Baskulin3_rot270_006_jpg.rf.8e9ca7b3fac80e0bb5b1a45dedae21d1.jpg

に検出がされた画像が出ていた

0 0.0758179 0.237794 0.0758906 0.693102 0.643104 0.693011 0.643031 0.237704

最初の0は
クラスだが
今回は1つしかクラスがないので0になる

アノテーション(ラベル付け)をしていない画像を推論して、推論結果が正しかった場合、検出ラベルを学習用のデータとすることもできます。OBBのアノテーションは多少手間がかかります。そこで、最初に少量のアノテーションを行い、次にアノテーションしていない画像で推論を行い、その後推論に成功したラベルを学習データに加えて画集データを充実していくこともできます。
とのこと

次は対象物のカウント

カウントの仕方は色々ありますが、今回は、クラス設定が1つということもあり、簡単なカウントの1つとしてこのラベルの行数をカウントする方法にしました。検出したクラスのチェックもしていません。
先ほどのノートブックに以下のコードを追加し実行して、検出物のカウントします。テスト画像の変更は、source_file = の設定で変更できます。

せっかくなので
ドラッグストアでバスクリンの種類の確認のため撮影した画像でカウントの実験をする
(以前バスクリンの種類を間違えたら家族に不評だったので)

パスは

/content/drive/MyDrive/PXL_20240529_101238381.jpg

なのでこれを指定する

対象の画像を変更するには
source_fileのパスを変更する

以下変更コード

import os
import subprocess

source_file = '/content/drive/MyDrive/PXL_20240529_101238381.jpg'

# テキストファイルのパスを構築(画像ファイル名と同じ)
file_name, file_extension = os.path.splitext(source_file)
label_file_path = '/content/runs/obb/predict/labels/' + os.path.basename(file_name) + '.txt'

# ファイルの存在を確認し、存在する場合は削除
if os.path.exists(label_file_path):
    os.remove(label_file_path)

# YOLOを使用して予測を実行
!yolo obb predict model=/content/runs/obb/train/weights/best.pt source='{source_file}' save=True save_txt=True exist_ok=True

# ファイルが存在する場合のみ、テキストファイルの行数を取得して表示
if os.path.exists(label_file_path):
    num_lines = subprocess.check_output(["wc", "-l", label_file_path]).decode().split()[0]
    print("バスクリンの数は", num_lines)
else:
    print("ファイルが見つかりませんでした。")

これを実行するとバスクリンの数をカウントしているのがわかる

実行結果は

Ultralytics YOLOv8.2.25 🚀 Python-3.10.12 torch-2.3.0+cu121 CUDA:0 (Tesla T4, 15102MiB)
YOLOv8n-obb summary (fused): 187 layers, 3077414 parameters, 0 gradients, 8.3 GFLOPs

/usr/local/lib/python3.10/dist-packages/torch/nn/modules/conv.py:456: UserWarning: Plan failed with a cudnnException: CUDNN_BACKEND_EXECUTION_PLAN_DESCRIPTOR: cudnnFinalize Descriptor Failed cudnn_status: CUDNN_STATUS_NOT_SUPPORTED (Triggered internally at ../aten/src/ATen/native/cudnn/Conv_v8.cpp:919.)
  return F.conv2d(input, weight, bias, self.stride,
image 1/1 /content/drive/MyDrive/PXL_20240529_101238381.jpg: 640x512 75.5ms
Speed: 4.4ms preprocess, 75.5ms inference, 983.9ms postprocess per image at shape (1, 3, 640, 512)
Results saved to runs/obb/predict
2 labels saved to runs/obb/predict/labels
💡 Learn more at https://docs.ultralytics.com/modes/predict
バスクリンの数は 4

となっているので
同じバスクリンでも違う種類は識別できているのがわかる

引き続き
https://axross-recipe.com/recipes/1302#requiredSkills
を参考に学習の後に
PCやラズパイでの推論を試す

roboflow Workspaceでアノテーションその1

roboflow Workspaceでアノテーション

https://axross-recipe.com/recipes/1469
を参考に行う

https://blog.roboflow.com/train-yolov8-obb-model/
のチュートリアルも参考にする

https://blog.roboflow.com
へアクセスし
GoogleIDなどで、Sign InすればOK

その場合
Continue with Google をクリック

ユーザ名を入力すると
プランが2つ出る
Free の無料か
Starter traialの月額249$
のどっちかになるので

とりあえずはFree にする

これでCreate workspace をクリック

次に
Invite teammates.
Add collaborators to help with labeling, upload data, train models, and more.
チームメイトを招待します。
ラベル付け、データのアップロード、モデルのトレーニングなどを支援するコラボレーターを追加します。

とあるけど
とりあえずSkipでOK

これでプロジェクトの作成画面になるが
デフォルトだとライセンスが
CC BY 4.0
になっている

Project Name は
anotation_test
とした

Annotation Group
注釈グループ
が識別のため必要らしい

とりあえずバスクリンなので
Baskulin

Project Typeには
Object Detection
を選択

これで
Create Project をクリック

次に画像ファイル
もしくは画像フォルダを選択する

ここでバスクリンを撮影したフォルダを指定するが
せっかくなので
画像の水増しをする

まず
Pixcel 8 で撮影したバスクリンの画像を
Google Photo からダウンロード

4枚の写真になっているので
これを水増しする

なお複数の写真をダウンロードすると
圧縮ファイルになっているのでこれを解凍する

vim generate_images.py

で内容を

import sys
import os
from PIL import Image

# コマンドライン引数から画像ファイル名を取得
if len(sys.argv) != 2:
    print("Usage: python generate_images.py imagefile.png")
    sys.exit(1)

image_file = sys.argv[1]

# 画像を読み込む
try:
    image = Image.open(image_file)
except IOError:
    print(f"Could not open the image file {image_file}")
    sys.exit(1)

# ファイル名と拡張子を分離し、ディレクトリ名を決定
file_name, file_extension = os.path.splitext(os.path.basename(image_file))
directory_name = file_name

# ディレクトリが存在しない場合は作成
if not os.path.exists(directory_name):
    os.makedirs(directory_name)

# 画像の変形と保存を行う関数
def save_images(image, prefix, transform, count=100):
    for i in range(count):
        filename = f'{prefix}{i+1:03}{file_extension}'
        filepath = os.path.join(directory_name, filename)
        transformed_image = image.transpose(transform)
        transformed_image.save(filepath)

# 各変換を適用して画像を保存
save_images(image, f'{file_name}_', Image.FLIP_TOP_BOTTOM, 100)
save_images(image, f'{file_name}_rot90_', Image.ROTATE_90, 100)
save_images(image, f'{file_name}_rot270_', Image.ROTATE_270, 100)

という画像を回転させてコピーするスクリプトを作成

次に写真をコピーしておく

cp ~/Downloads/Photos-001\ \(1\)/PXL_20240504_1732* .

ファイル名を変えた方が楽なので
ターミナルで

i=1
for file in PXL_20240504_173232354.jpg PXL_20240504_173242547.jpg PXL_20240504_173237123.jpg PXL_20240504_173253414.jpg; do
  mv "$file" "basclin$i.jpg"
  i=$((i + 1))
done

を実行

すると

basclin1.jpg
basclin2.jpg
basclin3.jpg
basclin4.jpg

というようにファイル名が変わる

よくみたらスペルミスなので

i=1
for file in basclin*.jpg; do
  mv "$file" "Baskulin$i.jpg"
  i=$((i + 1))
done

で修正

これでファイル名が

Baskulin1.jpg
Baskulin2.jpg
Baskulin3.jpg
Baskulin4.jpg

となったので

python generate_images.py Baskulin1.jpg
python generate_images.py Baskulin2.jpg
python generate_images.py Baskulin3.jpg
python generate_images.py Baskulin4.jpg

でファイルを量産

これで各ファイルごとのフォルダができたので
これを1つのフォルダにまとめる

ls -d */ | grep Bas


Basと書かれたディレクトリのみ表示できるので

move_files.sh

というスクリプトを作成

#!/bin/bash

# 移動先のディレクトリを作成(存在しない場合)
mkdir -p baskulin

# Baskulinで始まる全ディレクトリのファイルを baskulin ディレクトリに移動
for dir in Baskulin*/; do
    # ディレクトリ内のファイルを baskulin に移動
    mv "$dir"* baskulin/
done

echo "All files have been moved to the 'baskulin' directory."

として保存

chmod +x move_files.sh

で実行権限付与

./move_files.sh

で実行すれば全てのファイルが
1つのフォルダに移動される

そしてこのフォルダをアップロード

これでsave and continueをクリック

なお100枚以上に増やしたけど
同じ画像の場合は全て1つのものとしてみなすため
意味がなかった

回転させたりした場合は
異なる画像としてカウントされる

これでデータがアップできたので
次はアノテーション

テキストファイルからvoicevoxで音声を作成するスクリプト

テキストファイルからvoicevoxで音声を作成するスクリプト

Dockerマシンの voicevox で音声を作成するときにコマンドを毎回実行は面倒
ということで
Python スクリプトにして
テキストファイルを引数にして
音声を作成するようにする

元は

import subprocess
import pygame
import time

def generate_and_play_audio_from_text(file_path):
    # テキストファイルからテキストを読み込む
    with open(file_path, 'r') as file:
        text = file.read()

    # JSONファイルを作成するためのcurlコマンド
    command_json = [
        "curl", "-s", "-X", "POST",
        "http://{server_ip}/audio_query?speaker=1",
        "--get", "--data-urlencode", f"text={text}"
    ]

    # 音声ファイルを作成するためのcurlコマンド
    command_audio = [
        "curl", "-s", "-H", "Content-Type: application/json", "-X", "POST",
        "-d", "@query.json", "http://{server_ip}/synthesis?speaker=1"
    ]

    # JSONファイルと音声ファイルを作成
    with open('query.json', 'w') as file:
        subprocess.run(command_json, stdout=file)
    with open('audio_output.wav', 'wb') as file:
        subprocess.run(command_audio, stdout=file)

    # Pygameで音声ファイルを再生
    pygame.init()
    pygame.mixer.init()
    sound = pygame.mixer.Sound("audio_output.wav")
    sound.play()
    while pygame.mixer.get_busy():
        time.sleep(0.1)

# email_body.txtから音声を生成して再生
generate_and_play_audio_from_text('email_body.txt')

というgmail本文を抽出したテキストファイルを音声にするコードがあったので
これを改造する

create_voice.py

として

import subprocess
import sys

def generate_and_play_audio_from_text(file_path, server_ip):
    # テキストファイルからテキストを読み込む
    with open(file_path, 'r') as file:
        text = file.read()

    # JSONファイルを作成するためのcurlコマンド
    command_json = [
        "curl", "-s", "-X", "POST",
        f"http://{server_ip}/audio_query?speaker=1",
        "--get", "--data-urlencode", f"text={text}"
    ]

    # 音声ファイルを作成するためのcurlコマンド
    command_audio = [
        "curl", "-s", "-H", "Content-Type: application/json", "-X", "POST",
        "-d", "@query.json", f"http://{server_ip}/synthesis?speaker=1"
    ]

    # JSONファイルと音声ファイルを作成
    with open('query.json', 'w') as file:
        subprocess.run(command_json, stdout=file)
    with open('audio_output.wav', 'wb') as file:
        subprocess.run(command_audio, stdout=file)

    # 音声ファイルを再生
    subprocess.run(["afplay", "audio_output.wav"])

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print("Usage: python script.py <file_path> <server_ip>")
        sys.exit(1)
    
    file_path = sys.argv[1]
    server_ip = sys.argv[2]
    generate_and_play_audio_from_text(file_path, server_ip)

として

python script.py example.txt 192.168.1.5

というように実行すればいけるはず

python create_voice.py voice.txt 192.168.1.69:50021

で実行したら
音声が流れて、音声ファイルも作成された

現在の天気が雨でない場合に次の1時間の天気予報をチェックする

現在の天気が雨でない場合に次の1時間の天気予報をチェックするように変更

# weather_check.py
import location_utils2
import requests

def get_weather_data(api_key, latitude, longitude):
    # OpenWeather One Call API の URL
    url = f"https://api.openweathermap.org/data/3.0/onecall?lat={latitude}&lon={longitude}&exclude=minutely,daily&appid={api_key}&units=metric"
    response = requests.get(url)
    return response.json()

def check_for_rain(weather_data):
    hourly_forecast = weather_data.get('hourly', [])[:1]  # 次の1時間の予報だけをチェック
    for hour in hourly_forecast:
        for weather in hour['weather']:
            if weather['main'] == 'Rain':
                return True
    return False

# APIキーを設定
api_key = 'APIキー'


# location_utils2から緯度と経度と天気をを取得
latitude, longitude, _ , weather_description = location_utils2.get_current_location_and_address()

# 天気データを取得
weather_data = get_weather_data(api_key, latitude, longitude)

# 雨が予報されているかどうかをチェック
if check_for_rain(weather_data):
    print("Alert: Rain is expected within the next hour!")
else:
    print("No rain expected in the next hour.")

のコードだと
雨が降っていても一時間後に雨なのかを調べてしまう

なので現在地の天気が雨でないのなら実行するようにする

なおこの判定は

# location_utils2.py
import requests
from geopy.geocoders import Nominatim

def get_current_location_and_address():
    # APIキーとZIPコードは予め設定しておく必要があります。
    API_key = "APIキー"
    zip_place = "郵便番号,JP"
    
    # OpenWeatherMap API URL
    url = f"https://api.openweathermap.org/data/2.5/weather?zip={zip_place}&units=metric&lang=ja&appid={API_key}"
    
    # データの取得
    response = requests.get(url)
    jsondata = response.json()
    
    # 緯度と経度の取得
    latitude = jsondata['coord']['lat']
    longitude = jsondata['coord']['lon']
    
    #天気の取得
    # weather_description = jsondata['weather'][0]['description']
    weather_description = jsondata['weather'][0]['main']

    
    # 住所の取得(オプショナル)
    geolocator = Nominatim(user_agent="geoapiExercises")
    location = geolocator.reverse((latitude, longitude), language='ja')
    address = location.address if location else None
    
    return latitude, longitude, address, weather_description

で郵便番号をもとに
現在地の緯度経度と天気を取得している

この時に
Mainだと英語で取得になるが
Descriptionだと日本語で詳細の天気になる

ただ詳細な天気だと小雨とか判定が面倒なので
とにかく雨であれば
main==rainの判定にできるので
Mainを判定基準にした

変更後のコードは

import location_utils2
import requests

def get_weather_data(api_key, latitude, longitude):
    url = f"https://api.openweathermap.org/data/3.0/onecall?lat={latitude}&lon={longitude}&exclude=minutely,daily&appid={api_key}&units=metric"
    response = requests.get(url)
    return response.json()

def check_for_rain(weather_data):
    hourly_forecast = weather_data.get('hourly', [])[:1]
    for hour in hourly_forecast:
        for weather in hour['weather']:
            if weather['main'] == 'Rain':
                return True
    return False

api_key = 'APIキー'

latitude, longitude, _, weather_description = location_utils2.get_current_location_and_address()

# 現在の天気が雨でない場合にのみ次の1時間の雨の予報をチェック
if weather_description != 'Rain':
    weather_data = get_weather_data(api_key, latitude, longitude)
    if check_for_rain(weather_data):
        print("Alert: Rain is expected within the next hour!")
    else:
        print("No rain expected in the next hour.")
else:
    print("It is currently raining.")

次に音声を流れるように変更

今回も音声再生はpygameを使う

pygameライブラリを使って音声ファイルを再生しています。まず、pygame.mixerを初期化し、音声ファイルを読み込んで再生します。pygame.mixer.music.play()関数で音声を再生し、while pygame.mixer.music.get_busy()ループを使って音声が再生されている間は待機します。このスクリプトは、雨が予報されていない場合にのみ、次の1時間の雨の予報をチェックし、予報されていた場合に指定された音声ファイルを再生します

コードは

import location_utils2
import requests
import pygame  # Import pygame for audio playback

def get_weather_data(api_key, latitude, longitude):
    url = f"https://api.openweathermap.org/data/3.0/onecall?lat={latitude}&lon={longitude}&exclude=minutely,daily&appid={api_key}&units=metric"
    response = requests.get(url)
    return response.json()

def check_for_rain(weather_data):
    hourly_forecast = weather_data.get('hourly', [])[:1]
    for hour in hourly_forecast:
        for weather in hour['weather']:
            if weather['main'] == 'Rain':
                return True
    return False

api_key = 'APIキー'

latitude, longitude, _, weather_description = location_utils2.get_current_location_and_address()

# 現在の天気が雨でない場合にのみ次の1時間の雨の予報をチェック
if weather_description != 'Rain':
    weather_data = get_weather_data(api_key, latitude, longitude)
    if check_for_rain(weather_data):
        print("Alert: Rain is expected within the next hour!")
        pygame.mixer.init()  # Initialize the mixer module
        pygame.mixer.music.load('voice.wav')  # Load your sound file
        pygame.mixer.music.play()  # Play the sound
        while pygame.mixer.music.get_busy():  # Wait for music to finish playing
            pygame.time.Clock().tick(10)
    else:
        print("No rain expected in the next hour.")
else:
    print("It is currently raining.")

でOK

音声の部分は

python create_voice.py voice.txt 192.168.1.69:50021

で作成した音声を使用

次は顔認識したら実行するようにする

以前の
Weatheer や mail_voice などで使ったkao.pyとxmlファイルを使えばできるはず

ということでモジュール化する

weather_alert.py

として

# weather_alert.py

import location_utils2
import requests
import pygame

def get_weather_data(api_key, latitude, longitude):
    url = f"https://api.openweathermap.org/data/3.0/onecall?lat={latitude}&lon={longitude}&exclude=minutely,daily&appid={api_key}&units=metric"
    response = requests.get(url)
    return response.json()

def check_for_rain(weather_data):
    hourly_forecast = weather_data.get('hourly', [])[:1]
    for hour in hourly_forecast:
        for weather in hour['weather']:
            if weather['main'] == 'Rain':
                return True
    return False

def check_and_alert(api_key):
    latitude, longitude, _, weather_description = location_utils2.get_current_location_and_address()

    if weather_description != 'Rain':
        weather_data = get_weather_data(api_key, latitude, longitude)
        if check_for_rain(weather_data):
            print("Alert: Rain is expected within the next hour!")
            pygame.mixer.init()
            pygame.mixer.music.load('voice.wav')
            pygame.mixer.music.play()
            while pygame.mixer.music.get_busy():
                pygame.time.Clock().tick(10)
        else:
            print("No rain expected in the next hour.")
    else:
        print("It is currently raining.")

として保存

他で呼び出す時には

    api_key = 'APIキー'
    check_and_alert(api_key)

というように加えればいい

なので
以前作成した kao.pyの中での処理を変更すればOK

その前に
config.iniを作成し
待機時間とAPIキーを記述しておく

とりあえず mail_voiceのものを使うので

cp ../mail_voice/*.xml .
cp ../mail_voice/config.ini .

でコピー

config.iniを編集する

[Settings]
detection_interval = 1800 #30分

[API_KEYS]
OPENWEATHER_API_KEY = APIキー

としておく

次に kao.pyの編集

 cp ../mail_voice/kao.py .

の後に編集する

import weather_alert

でインポート

# 設定を変数に格納
api_key = config['API_KEYS']['OPENWEATHER_API_KEY']
detection_interval = int(config['Settings']['detection_interval'])

そして天気の取得処理を追加

        if lastTime is None or time.perf_counter() - lastTime > detection_interval:
            # 検出時刻更新
            lastTime = time.perf_counter()
            weather_alert.check_and_alert(api_key)

とする

これで実行したら

  File "/Users/snowpool/aw10s/rain_alert/kao.py", line 12, in <module>
    detection_interval = int(config['Settings']['detection_interval'])
ValueError: invalid literal for int() with base 10: '1800 #30分'

となった

原因はコメント
これが原因でonfig[‘Settings’][‘detection_interval’] の値が 1800 #30分 という文字列であり、これを整数に変換しようとしてエラーが発生していることです。int() 関数は整数に変換できない文字列を受け取るとエラーを発生させます。
問題の原因は、config[‘Settings’][‘detection_interval’] の値が正しい整数値ではなく、その後にコメントが付いているためです。Pythonでは、int() 関数は文字列中の数字のみを整数として解釈します。
とのこと

interval_string = config['Settings']['detection_interval']
interval_without_comment = interval_string.split('#')[0].strip()
detection_interval = int(interval_without_comment)

でコメントを消すこともできるらしいが
とりあえず余計なコメントは削除した

これで実行したら

Hello from the pygame community. https://www.pygame.org/contribute.html
Traceback (most recent call last):
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/geopy/geocoders/base.py", line 368, in _call_geocoder
    result = self.adapter.get_json(url, timeout=timeout, headers=req_headers)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/geopy/adapters.py", line 472, in get_json
    resp = self._request(url, timeout=timeout, headers=headers)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/geopy/adapters.py", line 500, in _request
    raise AdapterHTTPError(
geopy.adapters.AdapterHTTPError: Non-successful status code 403

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/rain_alert/kao.py", line 41, in <module>
    weather_alert.check_and_alert(api_key)
  File "/Users/snowpool/aw10s/rain_alert/weather_alert.py", line 21, in check_and_alert
    latitude, longitude, _, weather_description = location_utils2.get_current_location_and_address()
  File "/Users/snowpool/aw10s/rain_alert/location_utils2.py", line 28, in get_current_location_and_address
    location = geolocator.reverse((latitude, longitude), language='ja')
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/geopy/geocoders/nominatim.py", line 372, in reverse
    return self._call_geocoder(url, callback, timeout=timeout)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/geopy/geocoders/base.py", line 388, in _call_geocoder
    res = self._adapter_error_handler(error)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/geopy/geocoders/base.py", line 411, in _adapter_error_handler
    raise exc_cls(str(error)) from error
geopy.exc.GeocoderInsufficientPrivileges: Non-successful status code 403

となる

geopy ライブラリがジオコーディングサービスへのアクセスに失敗
が原因とのこと

住所の表示は不要で
緯度経度があれば問題ないため
住所表示機能を削除したら無事に動いた

とりあえず今回もコードを公開するのと同時に
Voicevoxのスクリプトも一緒にリポジトリに入れて公開する

緯度経度、天気を取得

緯度経度、天気を取得するように

# location_utils2.py
import requests
from geopy.geocoders import Nominatim

def get_current_location_and_address():
    # APIキーとZIPコードは予め設定しておく必要があります。
    API_key = "APIキー"
    zip_place = "437-郵便番号,JP"
    
    # OpenWeatherMap API URL
    url = f"https://api.openweathermap.org/data/2.5/weather?zip={zip_place}&units=metric&lang=ja&appid={API_key}"
    
    # データの取得
    response = requests.get(url)
    jsondata = response.json()
    
    # 緯度と経度の取得
    latitude = jsondata['coord']['lat']
    longitude = jsondata['coord']['lon']
    
    #天気の取得
    weather_description = jsondata['weather'][0]['description']
    
    # 住所の取得(オプショナル)
    geolocator = Nominatim(user_agent="geoapiExercises")
    location = geolocator.reverse((latitude, longitude), language='ja')
    address = location.address if location else None
    
    return latitude, longitude, address, weather_description

へコード変更

location_utils2.py

を変更したので

それに伴い

# weather_check.py
import location_utils2
import requests

def get_weather_data(api_key, latitude, longitude):
    # OpenWeather One Call API の URL
    url = f"https://api.openweathermap.org/data/3.0/onecall?lat={latitude}&lon={longitude}&exclude=minutely,daily&appid={api_key}&units=metric"
    response = requests.get(url)
    return response.json()

def check_for_rain(weather_data):
    hourly_forecast = weather_data.get('hourly', [])[:1]  # 次の1時間の予報だけをチェック
    for hour in hourly_forecast:
        for weather in hour['weather']:
            if weather['main'] == 'Rain':
                return True
    return False

# APIキーを設定
api_key = 'APIキー'


# location_utils2から緯度と経度を取得
latitude, longitude, _ , weather_description = location_utils2.get_current_location_and_address()

# 天気データを取得
weather_data = get_weather_data(api_key, latitude, longitude)

# 雨が予報されているかどうかをチェック
if check_for_rain(weather_data):
    print("Alert: Rain is expected within the next hour!")
else:
    print("No rain expected in the next hour.")

としたが
雨が降りそうだが降らないと出る

No rain expected in the next hour.
import location_utils2

# 緯度、経度、住所(無視)、天気を取得
latitude, longitude, _, weather_description = location_utils2.get_current_location_and_address()

# 緯度、経度、天気を表示
print(f"Latitude: {latitude}, Longitude: {longitude}")
print("Weather:", weather_description)

で一度
緯度経度と天気を出したら
Latitude: 緯度, Longitude: 経度
Weather: 小雨

となっている

import requests
import json
from pprint import pprint
# url = "https://api.openweathermap.org/data/2.5/weather?zip={zip_place}&units=metric&appid={API_key}"
url = "https://api.openweathermap.org/data/2.5/weather?zip={zip_place}&units=metric&lang=ja&appid={API_key}"

# xxxxx
url = url.format(zip_place = "437-郵便番号,JP", API_key = "APIキー")

jsondata = requests.get(url).json()

pprint(jsondata)
# 緯度経度のデータを抽出
latitude = jsondata['coord']['lat']
longitude = jsondata['coord']['lon']

# 緯度経度を表示
print("緯度:", latitude)
print("経度:", longitude)

で詳細のJSONを出しても
日本語になっている

{'base': 'stations',
 'clouds': {'all': 99},
 'cod': 200,
 'coord': {'lat': 緯度, 'lon': 経度},
 'dt': 1714511699,
 'id': 0,
 'main': {'feels_like': 18.27,
          'grnd_level': 1001,
          'humidity': 80,
          'pressure': 1002,
          'sea_level': 1002,
          'temp': 18.3,
          'temp_max': 18.3,
          'temp_min': 18.3},
 'name': 'Kawai',
 'rain': {'1h': 0.32},
 'sys': {'country': 'JP',
         'id': 2092872,
         'sunrise': 1714507077,
         'sunset': 1714555965,
         'type': 2},
 'timezone': 32400,
 'visibility': 10000,
 'weather': [{'description': '小雨', 'icon': '10d', 'id': 500, 'main': 'Rain'}],
 'wind': {'deg': 284, 'gust': 4.82, 'speed': 2.68}}

ただし mainではrain

次に別の日に

weather.py      

を実行
天気は曇り

{'base': 'stations',
 'clouds': {'all': 100},
 'cod': 200,
 'coord': {'lat': 緯度, 'lon': 経度},
 'dt': 1714595140,
 'id': 0,
 'main': {'feels_like': 13.04,
          'grnd_level': 1012,
          'humidity': 90,
          'pressure': 1013,
          'sea_level': 1013,
          'temp': 13.3,
          'temp_max': 13.3,
          'temp_min': 13},
 'name': 'Kawai',
 'sys': {'country': 'JP',
         'id': 2092872,
         'sunrise': 1714593416,
         'sunset': 1714642414,
         'type': 2},
 'timezone': 32400,
 'visibility': 10000,
 'weather': [{'description': '厚い雲',
              'icon': '04d',
              'id': 804,
              'main': 'Clouds'}],
 'wind': {'deg': 87, 'gust': 6.7, 'speed': 3.31}}

つまりweather の main の部分を取得して表示すればOK
descriptionにしてしまうと
小雨とか分類が多くなってしまう

なので

    #天気の取得
    # weather_description = jsondata['weather'][0]['description']
    weather_description = jsondata['weather'][0]['main']

というようにすれば
現在地の天気は

Weather: Clouds

となる

あとは一時間後の天気を取得する部分だけど
これはGPTのコードではできているけど
ほんとにあっているか検証する