画像データの水増し

画像データの水増し

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のコードではできているけど
ほんとにあっているか検証する

OpenWeather API を使い現在地の今日の天気を取得

OpenWeather API を使い現在地の今日の天気を取得。
一時間以内に雨が降るなら音声で知らせるものを作る

以前
https://github.com/Snowpooll/face_weather
で作成した顔を認識したら今日の天気を知らせる
の応用で
顔を認識した時に1時間以内に雨が降るなら
音声で知らせる
を作成する

GPTで
OpenWeather API を使い現在地の今日の天気を取得。
一時間以内に雨が降るならアラートを表示するpythonコード
としたとき

import requests
import json

# OpenWeather API キーと緯度経度を設定
api_key = 'YOUR_API_KEY'
latitude = 'YOUR_LATITUDE'
longitude = 'YOUR_LONGITUDE'

# OpenWeather One Call API の URL
url = f"https://api.openweathermap.org/data/2.5/onecall?lat={latitude}&lon={longitude}&exclude=minutely,daily&appid={api_key}&units=metric"

def get_weather_data(url):
    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 から天気データを取得
weather_data = get_weather_data(url)

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

となった

しかし
現在地をどうせなら自動で取得したいので
GPTで
現在地の緯度経度を取得するpythonコード
とすると

ipinfo.ioとNominatim ジオコーダ
2つの取得方法が提示される

なお移動しながらの位置情報取得はGPSをお勧めされた

とりあえず精度などからだと

家庭のように固定された場所の位置情報を取得する場合は、Nominatim ジオコーダを使用するのが一般的に適しています。その理由は以下の通りです:
1. 正確性:Nominatim は OpenStreetMap のデータに基づいており、具体的な住所や場所名から精密な緯度と経度を提供できます。これは家庭や建物の正確な位置を特定するのに最適です。
2. 詳細レベル:Nominatim は詳細な逆ジオコーディングを提供し、具体的な住所情報を含めることができます。これにより、特定の住所の地理的な位置を正確に把握できます。
3. 汎用性:地図上での場所検索や、特定の地点の座標を取得するために、Nominatim は広範なアプリケーションで利用可能です。地図関連の多くの機能を実装する場合にも対応しやすいです。
一方で、ipinfo.io は IP アドレスに基づいて地理的な位置を特定するサービスであり、特定の住所に対してはそれほど詳細な情報を提供することはできません。IP ベースの位置特定は、あくまでおおまかな地域情報を提供するため、家庭のように固定された具体的な位置を必要とする場合には適していません。
したがって、家庭のような固定の場所の正確な位置情報を求める場合は、Nominatim ジオコーダがより適切な選択肢と言えるでしょう。

とのこと

とりあえず

pip install geopy

でライブラリを入れる

しかし

from geopy.geocoders import Nominatim
import geopy

def get_location():
    # ジオロケーターを初期化
    geolocator = Nominatim(user_agent="geoapiExercises")
    
    try:
        # IPアドレスから位置情報を取得
        location = geolocator.geocode("現在地の住所", timeout=10)
        
        # 緯度と経度を取得
        latitude = location.latitude
        longitude = location.longitude
        
        return latitude, longitude
    
    except geopy.exc.GeocoderTimedOut:
        return "Geocoder service timed out"
    
    except AttributeError:
        return "Location not found"

# 現在地の緯度経度を取得
current_location = get_location()
print("Latitude:", current_location[0])
print("Longitude:", current_location[1])

を実行すると

Latitude: L
Longitude: o

となる

ご提示のコードで Latitude: L と Longitude: o という結果が出力されるのは、おそらく何らかのエラーが発生しているため、エラーメッセージの文字列を返している可能性が高いです。get_location 関数では、例外が発生した場合に文字列を返しています。しかし、それが数字のタプルではないため、current_location[0] と current_location[1] で文字列の最初の文字が表示されてしまうのです。
問題を解決するために、エラー発生時のハンドリングを少し変更し、エラーメッセージを確認できるように修正すると良いでしょう。以下に修正したコードを示します。

from geopy.geocoders import Nominatim
import geopy

def get_location():
    # ジオロケーターを初期化
    geolocator = Nominatim(user_agent="geoapiExercises")
    
    try:
        # 住所から位置情報を取得
        location = geolocator.geocode("現在地の住所", timeout=10)
        
        # 緯度と経度を取得
        if location:
            latitude = location.latitude
            longitude = location.longitude
            return (latitude, longitude)
        else:
            return "Location not found", None
    
    except geopy.exc.GeocoderTimedOut:
        return "Geocoder service timed out", None
    
    except Exception as e:
        return str(e), None

# 現在地の緯度経度を取得
current_location = get_location()

if isinstance(current_location, tuple) and None not in current_location:
    print("Latitude:", current_location[0])
    print("Longitude:", current_location[1])
else:
    print(current_location[0])  # エラーメッセージを出力

の結果は

Location not found

住所の入力方法に問題ありかと思って
https://note.com/asahi_ictrad/n/nc948388e7c72#66ef102d-ebb9-421c-9e94-ea6e86c1a063
を参考に

from geopy.geocoders import Nominatim
import geopy

def get_location():
    # ジオロケーターを初期化
    geolocator = Nominatim(user_agent="geoapiExercises")
    
    try:
        # 住所から位置情報を取得
        location = geolocator.geocode("日本、〒郵便番号 住所", timeout=10)
        
        # 緯度と経度を取得
        if location:
            latitude = location.latitude
            longitude = location.longitude
            return (latitude, longitude)
        else:
            return "Location not found", None
    
    except geopy.exc.GeocoderTimedOut:
        return "Geocoder service timed out", None
    
    except Exception as e:
        return str(e), None

# 現在地の緯度経度を取得
current_location = get_location()

if isinstance(current_location, tuple) and None not in current_location:
    print("Latitude:", current_location[0])
    print("Longitude:", current_location[1])
else:
    print(current_location[0])  # エラーメッセージを出力

としたが変わらない

このため

現在地の緯度と経度を取得するには、Pythonで geopy ライブラリを使用するのが一般的です。geopy を使って、デバイスの IP アドレスを基にして位置情報を推測する方法
を試す

pip install geopy requests

でライブラリインポート

from geopy.geocoders import Nominatim
import requests

def get_public_ip():
    # 公開 API を使用して公衆 IP アドレスを取得
    response = requests.get('https://api.ipify.org')
    return response.text

def get_location_by_ip(ip_address):
    # IP アドレスから位置情報を推定
    url = f'https://ipinfo.io/{ip_address}/json'
    response = requests.get(url)
    data = response.json()
    loc = data.get('loc', None)
    if loc:
        latitude, longitude = loc.split(',')
        return latitude, longitude
    return None, None

# 公衆 IP アドレスを取得
public_ip = get_public_ip()

# IP アドレスから位置情報を取得
latitude, longitude = get_location_by_ip(public_ip)
print(f"Latitude: {latitude}, Longitude: {longitude}")

これなら成功

なのでこれをモジュールにして呼び出すようにする

location_utils.py

として

# location_utils.py

from geopy.geocoders import Nominatim
import requests

def get_public_ip():
    """公開 API を使用して公衆 IP アドレスを取得する関数"""
    response = requests.get('https://api.ipify.org')
    return response.text

def get_location_by_ip(ip_address):
    """IP アドレスから位置情報(緯度と経度)を推定する関数"""
    url = f'https://ipinfo.io/{ip_address}/json'
    response = requests.get(url)
    data = response.json()
    loc = data.get('loc', None)
    if loc:
        latitude, longitude = loc.split(',')
        return latitude, longitude
    return None, None

で保存

次に

import requests
import json

# OpenWeather API キーと緯度経度を設定
api_key = 'YOUR_API_KEY'
latitude = 'YOUR_LATITUDE'
longitude = 'YOUR_LONGITUDE'

# OpenWeather One Call API の URL
url = f"https://api.openweathermap.org/data/2.5/onecall?lat={latitude}&lon={longitude}&exclude=minutely,daily&appid={api_key}&units=metric"

def get_weather_data(url):
    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 から天気データを取得
weather_data = get_weather_data(url)

# 雨が予報されているかどうかをチェック
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_utils.py

を呼び出して緯度経度を自動取得できるようにしたい

location_utils.py モジュールから緯度と経度を取得する機能を weather_check.py に組み込むために、以下のようにコードを修正します。まず、location_utils.py が緯度と経度を直接返すようにし、その後、この情報を weather_check.py で利用して OpenWeather API に問い合わせます。

location_utils.py を以下のように更新します。このコードは緯度と経度を直接取得し、戻り値として返します

# location_utils.py

import requests

def get_public_ip():
    response = requests.get('https://api.ipify.org')
    return response.text

def get_location_by_ip(ip_address):
    url = f'https://ipinfo.io/{ip_address}/json'
    response = requests.get(url)
    data = response.json()
    loc = data.get('loc', None)
    if loc:
        latitude, longitude = loc.split(',')
        return latitude, longitude
    return None, None

def get_current_location():
    ip_address = get_public_ip()
    return get_location_by_ip(ip_address)

次に

weather_check.py

location_utils から緯度と経度を取得し、その情報を使って天気情報を取得

# weather_check.py

import requests
import location_utils

# OpenWeather API キー
api_key = 'APIキー'

# 緯度と経度を location_utils から取得
latitude, longitude = location_utils.get_current_location()
if latitude is None or longitude is None:
    print("Failed to get location.")
    exit()

# OpenWeather One Call API の URL
url = f"https://api.openweathermap.org/data/2.5/onecall?lat={latitude}&lon={longitude}&exclude=minutely,daily&appid={api_key}&units=metric"

def get_weather_data(url):
    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 から天気データを取得
weather_data = get_weather_data(url)

# 雨が予報されているかどうかをチェック
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.

となる

しかしこれだけだと本当に合っているかわからないので
IPアドレスから住所を表示するのと
一時間毎の天気を表示するものを作る

住所を確認したが違ってた

表示された緯度経度が本来の緯度経度とずれている

他のものも試してみた

pip install geocoder

の後に

import geocoder

def get_current_location():
    # 自分の IP アドレスに基づいて現在地の位置情報を取得
    g = geocoder.ip('me')
    if g.ok:
        return g.latlng
    else:
        return None

# 現在地の緯度と経度を取得して表示
location = get_current_location()
if location:
    latitude, longitude = location
    print(f"Latitude: {latitude}, Longitude: {longitude}")
else:
    print("Unable to determine current location")

国土地理院なら

import requests
import urllib

makeUrl = "https://msearch.gsi.go.jp/address-search/AddressSearch?q="
s_quote = urllib.parse.quote('千葉県南房総市富浦町青木123-1')
response = requests.get(makeUrl + s_quote)
print(response.json()[0]["geometry"]["coordinates"])

[137.91481, 34.743805]

取り合えず住所から入力して緯度経度取得にするなどを考えていたが
よくよく考えればどうやって緯度経度を出したか調べたら

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}"
# xxxxx
url = url.format(zip_place = "郵便番号,JP", API_key = "取得したAPIキー")

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

print("天気:",jsondata["weather"][0]["main"])
print("天気詳細:",jsondata["weather"][0]["description"])

print("都市名:",jsondata["name"])
print("気温:",jsondata["main"]["temp"])
print("体感気温:",jsondata["main"]["feels_like"])
print("最低気温:",jsondata["main"]["temp_min"])
print("最高気温:",jsondata["main"]["temp_max"])
print("気圧:",jsondata["main"]["pressure"])
print("湿度:",jsondata["main"]["humidity"])

print("風速:",jsondata["wind"]["speed"])
print("風の方角:",jsondata["wind"]["deg"])

で緯度経度を取得していた

つまり

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 = "郵便番号,JP", API_key = "取得したAPIキー")

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

pprint(jsondata)

の結果で

{'base': 'stations',
 'clouds': {'all': 100},
 'cod': 200,
 'coord': {'lat': 緯度, 'lon': 経度},
 'dt': 1714340555,
 'id': 0,
 'main': {'feels_like': 17.59,
          'grnd_level': 1014,
          'humidity': 77,
          'pressure': 1015,
          'sea_level': 1015,
          'temp': 17.75,
          'temp_max': 17.75,
          'temp_min': 17.75},
 'name': 'Kawai',
 'rain': {'1h': 0.1},
 'sys': {'country': 'JP',
         'id': 2092872,
         'sunrise': 1714334403,
         'sunset': 1714383067,
         'type': 2},
 'timezone': 32400,
 'visibility': 10000,
 'weather': [{'description': '厚い雲',
              'icon': '04d',
              'id': 804,
              'main': 'Clouds'}],
 'wind': {'deg': 255, 'gust': 1.55, 'speed': 0.83}}

の実行結果の中から

 'coord': {'lat': 緯度, 'lon': 経度},


緯度経度のみ抽出して表示したい

これは

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

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

とすればOK

次にモジュールにする
location_util2.pyの中身の

# location_utils.py

import requests
from geopy.geocoders import Nominatim

def get_public_ip():
    response = requests.get('https://api.ipify.org')
    return response.text

def get_location_by_ip(ip_address):
    url = f'https://ipinfo.io/{ip_address}/json'
    response = requests.get(url)
    data = response.json()
    loc = data.get('loc', None)
    if loc:
        latitude, longitude = loc.split(',')
        return latitude, longitude
    return None, None

def get_address_from_coordinates(latitude, longitude):
    geolocator = Nominatim(user_agent="geoapiExercises")
    location = geolocator.reverse((latitude, longitude), exactly_one=True)
    return location.address if location else "Address not found."

def get_current_location_and_address():
    ip_address = get_public_ip()
    location = get_location_by_ip(ip_address)
    if location:
        latitude, longitude = location
        address = get_address_from_coordinates(latitude, longitude)
        return latitude, longitude, address
    return None, None, "Location not found."

# 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']
    
    # 住所の取得(オプショナル)
    geolocator = Nominatim(user_agent="geoapiExercises")
    location = geolocator.reverse((latitude, longitude), language='ja')
    address = location.address if location else None
    
    return latitude, longitude, address

として保存

Address.pyの中身を

import location_utils2
latitude, longitude, address = location_utils2.get_current_location_and_address()
if latitude and longitude:
    print(f"Latitude: {latitude}, Longitude: {longitude}")
if address:
    print("Address:", address)
から
import location_utils2

latitude, longitude, address = location_utils2.get_current_location_and_address()
if latitude and longitude:
    print(f"Latitude: {latitude}, Longitude: {longitude}")
if address:
    print("Address:", address)

って同じじゃん

実行し
https://www.geocoding.jp
で調べたら
緯度 経度
が住所の緯度経度

つまり
Nominatim
による住所への変換がダメってことがわかった

# 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']
    
    # 住所の取得(オプショナル)
    geolocator = Nominatim(user_agent="geoapiExercises")
    location = geolocator.reverse((latitude, longitude), language='ja')
    address = location.address if location else None
    
    return latitude, longitude, address

の内容を

import requests

def get_current_location_and_address():
    # APIキーとZIPコードを設定
    API_key = "YOUR_API_KEY"
    zip_place = "YOUR_ZIP_CODE,JP"
    
    # OpenWeatherMap APIを使用して緯度経度を取得
    weather_url = f"https://api.openweathermap.org/data/2.5/weather?zip={zip_place}&units=metric&lang=ja&appid={API_key}"
    response = requests.get(weather_url)
    jsondata = response.json()
    latitude = jsondata['coord']['lat']
    longitude = jsondata['coord']['lon']
    
    # 国土地理院APIを使用して住所を取得
    geo_url = f"https://mreversegeocoder.gsi.go.jp/reverse-geocoder/LonLatToAddress?lat={latitude}&lon={longitude}"
    geo_response = requests.get(geo_url)
    address_data = geo_response.json()
    
    # 住所情報を取得(存在する場合)
    address = address_data['results']['lv01Nm'] if 'results' in address_data and 'lv01Nm' in address_data['results'] else None
    
    return latitude, longitude, address

とした結果
とりあえず郵便番号まで取得できて大体の住所がでるなら
Nominatim
で事足りる

重要なのはそこじゃなくて緯度経度が合ってるかどうか
なので
住所検索の機能はオミットする予定だったが

import location_utils2

latitude, longitude, _ = location_utils2.get_current_location_and_address()  # 住所情報は無視
print(f"Latitude: {latitude}, Longitude: {longitude}")

というように別のファイルで呼び出すときに
指定しなければ緯度経度のみ抽出できる

次に

# weather_check.py

import requests
import location_utils

# OpenWeather API キー
api_key = 'APIキー'

# 緯度と経度を location_utils から取得
latitude, longitude = location_utils.get_current_location()
if latitude is None or longitude is None:
    print("Failed to get location.")
    exit()

# OpenWeather One Call API の URL
url = f"https://api.openweathermap.org/data/2.5/onecall?lat={latitude}&lon={longitude}&exclude=minutely,daily&appid={api_key}&units=metric"

def get_weather_data(url):
    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 から天気データを取得
weather_data = get_weather_data(url)

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

のコードを
これを使って緯度経度取得したものへ変更

# 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/2.5/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, _ = 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.

となる

次に6時間毎の天気を表示する

weather_forecast_6hour.py

import requests

def get_weather_forecast(api_key, latitude, longitude):
    url = f"https://api.openweathermap.org/data/2.5/onecall?lat={latitude}&lon={longitude}&units=metric&lang=ja&appid={api_key}"
    response = requests.get(url)
    return response.json()

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

latitude = 35.681236  # 東京駅の緯度
longitude = 139.767125  # 東京駅の経度

weather_data = get_weather_forecast(api_key, latitude, longitude)

# 現在の天気、1時間毎の天気(次の6時間)、1日毎の天気(次の7日間)を表示
print("Current Weather:", weather_data['current'])
print("Hourly Forecast for next 6 hours:")
for hour in weather_data['hourly'][:6]:
    print(hour)
print("Daily Forecast for next 7 days:")
for day in weather_data['daily'][:7]:
    print(day)

としたが

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/rain_alert/weather_forecast_6hour.py", line 17, in <module>
    print("Current Weather:", weather_data['current'])
KeyError: 'current'

となる

「現在・1時間毎・1日毎」の天気予報を取得
https://dev.classmethod.jp/articles/use-open-weather-map/
のコードが古いようだ

次に
https://3pysci.com/openweathermap-5/#index_id5
を参考にまずは全データの取得

だがだめ

公式を確認する
https://openweathermap.org/api/one-call-3#current

https://api.openweathermap.org/data/3.0/onecall?lat={lat}&lon={lon}&exclude={part}&appid={API key}
となっていて
	optional	By using this parameter you can exclude some parts of the weather data from the API response. It should be a comma-delimited list (without spaces).
Available values:
	•	current
	•	minutely
	•	hourly
	•	daily
	•	alerts

やるべきことは
現在の天気を取得し
雨以外の天気なら
一時間以内に雨が降るかを調べるものを作成する

雨が降りそうな時のみ音声を流す
これを顔認識したら実行するものを作成することで
洗濯物を取り込めるようにする

出かけたり買い物の時間を考えれば三時間がベストなので

print("天気:",jsondata["current"]["weather"][0]["main"])

で実験したらNG

3時間にしても

    hourly_forecast = weather_data['hourly'][:24]  # 24時間分のデータを取得

の部分が間違っている

つまり時間毎の指定ができないとだめ

https://zenn.dev/daifukuninja/articles/5e696cd0a75ba8#たたいてみた
を参考に
現在地の天気は

curl 'https://api.openweathermap.org/data/2.5/weather?zip=169-0072,JP&appid={your-api-key}&lang=jp'

で取得できるので

curl 'https://api.openweathermap.org/data/2.5/forecast?lon=緯度&lat=経度&appid={your-api-key}&lang=jp'

とりあえず現在の郵便番号から
緯度経度、天気を取得するように

# 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']
    
    # 住所の取得(オプショナル)
    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

へコード変更

メール読み上げを設定ファイルからに変更

メール読み上げを設定ファイルからに変更

config.ini

を作成し

[Settings]
server_ip = 192.168.1.69:50021
label_id = GmailのラベルID
detection_interval = 1200
notification_sound = notice.wav
pdf_notification_sound = notice_pdf.wav

とする

configparser モジュールを使用して、INIファイルから設定を読みこむ

kao.py

import configparser

を追加

# configparserを使用してINIファイルを読み込む
config = configparser.ConfigParser()
config.read('config.ini')

# 設定を変数に格納
server_ip = config['Settings']['server_ip']
label_id = config['Settings']['label_id']
detection_interval = int(config['Settings']['detection_interval'])

        if lastTime is None or time.perf_counter() - lastTime > 1200:
を
        if lastTime is None or time.perf_counter() - lastTime > detection_interval:

というように設定値の定数へ変更

次にサーバーIPも設定ファイルから読み込むように変更する

email_processor,py

のコードを変更

import configparser

# 設定ファイルの読み込み
config = configparser.ConfigParser()
config.read('config.ini')
server_ip = config['Settings']['server_ip']
label_id = config['Settings']['label_id']
notification_sound = config['Settings']['notification_sound']
pdf_notification_sound = config['Settings']['pdf_notification_sound']

を追記

音声を生成する部部の

    command_json = [
        "curl", "-s", "-X", "POST",
        "192.168.1.69:50021/audio_query?speaker=1",
        "--get", "--data-urlencode", f"text={text}"
    ]
    command_audio = [
        "curl", "-s", "-H", "Content-Type: application/json", "-X", "POST",
        "-d", "@query.json", "192.168.1.69:50021/synthesis?speaker=1"
    ]
でサーバーIPの部分を
    command_json = [
        "curl", "-s", "-X", "POST",
        f"http://{server_ip}/audio_query?speaker=1",
        "--get", "--data-urlencode", f"text={text}"
    ]
    command_audio = [
        "curl", "-s", "-H", "Content-Type: application/json", "-X", "POST",
        "-d", "@query.json", f"http://{server_ip}/synthesis?speaker=1"
    ]

へ変更

また再生する音声を

        playsound('notice.wav')

から

playsound(notification_sound)

へ変更

                        playsound('notice_pdf.wav')

                        playsound(pdf_notification_sound)

へ変更

これで
Voicevox のdocker サーバーIP
お知らせの音声
顔認識してメールを読み上げる時間の指定
Gmailで読み上げるラベルの指定
を設定ファイルで行えるように変更完了

あとは
GithubへSSHでアップできるようにする

顔を認識したらメールを読み上げる

顔を認識したらメールを読み上げる

import fitz
from playsound import playsound
import subprocess
import pygame
import time

# テキストから音声を生成して再生する関数
def generate_and_play_audio_from_text(text):
    command_json = [
        "curl", "-s", "-X", "POST",
        "192.168.1.69:50021/audio_query?speaker=1",
        "--get", "--data-urlencode", f"text={text}"
    ]
    command_audio = [
        "curl", "-s", "-H", "Content-Type: application/json", "-X", "POST",
        "-d", "@query.json", "192.168.1.69:50021/synthesis?speaker=1"
    ]
    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.init()
    pygame.mixer.init()
    sound = pygame.mixer.Sound("audio_output.wav")
    sound.play()
    while pygame.mixer.get_busy():
        time.sleep(0.1)

# PDFファイルから文字数をカウントする関数
def count_pdf_characters(file_path):
    doc = fitz.open(file_path)
    text = ""
    for page in doc:
        text += page.get_text()
    return len(text)

# メールの処理を行う関数
def process_email(service, label_id):
    from gmail_utils import gmail_get_latest_unread_message_body
    from pdf_downloader import find_preview_link, download_pdf

    body, urls = gmail_get_latest_unread_message_body(service, label_id)
    if body:
        playsound('notice.wav')
        generate_and_play_audio_from_text(body)
        if urls:
            for url in urls:
                preview_url = find_preview_link(url)
                if preview_url:
                    download_pdf(preview_url, "downloaded_file.pdf")
                    char_count = count_pdf_characters("downloaded_file.pdf")
                    if char_count >= 100:
                        playsound('notice_pdf.wav')
                else:
                    print("プレビューリンクが見つかりませんでした。")
        else:
            print("メールにURLが見つかりませんでした。")
    else:
        print("未読メールはありません。")

というようにモジュールにして保存
ファイル名は

email_processor.py

とした

次に
Kao,pyで顔認識したらメールを読み上げるようにする

git clone https://github.com/Snowpooll/face_weather.git

で以前作成した顔認識したら天気を知らせるの中にあるものを

cp ../../face_weather/haarcascade_* .

でコピーして使う

import cv2
import time
from email_processor import process_email
from gmail_utils import gmail_init

# Haar Cascade分類器の読み込み
face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')

# Webカメラの設定
cap = cv2.VideoCapture(0)  # 0番目のカメラを使用する場合

# Gmailサービスの初期化
service = gmail_init()
label_id = "ラベルID"  # 例として特定のラベルIDを設定

# 最後の顔検出時刻
lastTime = None

# メインループ
while True:
    # カメラからのフレームの取得
    ret, frame = cap.read()
    
    # フレームのグレースケール化
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    
    # 顔の検出
    faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30))
    
    # 検出された顔に対する処理
    for (x, y, w, h) in faces:
        # 検出自の処理(検出から1分たったら再度イベント動かす)
        if lastTime is None or time.perf_counter() - lastTime > 60:
            # 検出時刻更新
            lastTime = time.perf_counter()
            # メール処理関数を呼び出し
            process_email(service, label_id)

# 後処理
cap.release()
cv2.destroyAllWindows()

で実行したら成功したが
間隔を一分にしたため
メールばかり読み上げになる

なので感覚を20分ぐらいに変更する

        # 検出自の処理(検出から20分たったら再度イベント動かす)
        if lastTime is None or time.perf_counter() - lastTime > 1200:

へ変更した

とりあえず動作確認は取れたので
次に設定ファイルを作成し
コードのメンテをしやすくする

とりあえず
docker サーバーIP
gmailのラベル
次の検出までの待ち時間
は設定ファイルにまとめておき
これを編集するだけでできるようにする

ダウンロード機能の修正とモジュール化

ダウンロード機能の修正とモジュール化

import fitz  # PyMuPDF
from playsound import playsound
from gmail_utils import gmail_init, gmail_get_latest_unread_message_body
from pdf_downloader import find_preview_link, download_pdf

def save_email_body_to_text(body, filename="email_body.txt"):
    with open(filename, "w", encoding="utf-8") as file:
        file.write(body)

def count_pdf_characters(file_path):
    doc = fitz.open(file_path)
    text = ""
    for page in doc:
        text += page.get_text()
    return len(text)

def main():
    # Gmail API サービスを初期化
    service = gmail_init()
    
    # ラベル ID を指定して最新の未読メール本文とURLを取得
    body, urls = gmail_get_latest_unread_message_body(service, "ラベルID")
    
    # 未読メールがある場合は音声ファイルを再生
    if body:
        playsound('notice.wav')

        if urls:
            for url in urls:
                print(f"プレビューリンクを検索するURL: {url}")
                # プレビューリンクを取得
                preview_url = find_preview_link(url)
                if preview_url:
                    print(f"プレビューリンク: {preview_url}")
                    # プレビューリンクからPDFファイルのダウンロードを試みる
                    download_pdf(preview_url, file_path="downloaded_file.pdf")
                    
                    # PDFファイルから文字数をカウント
                    char_count = count_pdf_characters("downloaded_file.pdf")
                    print(f"PDF内の文字数: {char_count}")
                    
                    # 文字数が100文字以上の場合は別の音声ファイルを再生
                    if char_count >= 100:
                        playsound('notice_pdf.wav')

                else:
                    print("プレビューリンクが見つかりませんでした。")
        else:
            print("メールにURLが見つかりませんでした。")

        # メール本文をテキストファイルに保存
        save_email_body_to_text(body)
    else:
        print("未読メールはありません。")

if __name__ == "__main__":
    main()

としたらリンクがエラーになる

from gmail_utils import gmail_init, gmail_get_latest_unread_message_body
from pdf_downloader import find_preview_link, download_pdf

def save_email_body_to_text(body, filename="email_body.txt"):
    with open(filename, "w", encoding="utf-8") as file:
        file.write(body)

def main():
    # Gmail API サービスを初期化
    service = gmail_init()
    
    # ラベル ID を指定して最新の未読メール本文とURLを取得
    body, urls = gmail_get_latest_unread_message_body(service, "ラベルID")
    if urls:
        for url in urls:
            print(f"プレビューリンクを検索するURL: {url}")
            # プレビューリンクを取得
            preview_url = find_preview_link(url)
            if preview_url:
                print(f"プレビューリンク: {preview_url}")
                # プレビューリンクからPDFファイルのダウンロードを試みる
                download_pdf(preview_url, file_path="downloaded_file.pdf")
            else:
                print("プレビューリンクが見つかりませんでした。")
    else:
        print("メールにURLが見つかりませんでした。")

    # メール本文をテキストファイルに保存
    save_email_body_to_text(body)

if __name__ == "__main__":
    main()

だと問題ない

とりあえず

import fitz  # PyMuPDF
from playsound import playsound
from gmail_utils import gmail_init, gmail_get_latest_unread_message_body
from pdf_downloader import find_preview_link, download_pdf
import subprocess
import pygame
import time

def generate_and_play_audio_from_text(text):
    # JSONファイルを作成するためのcurlコマンド
    command_json = [
        "curl", "-s", "-X", "POST",
        "192.168.1.69:50021/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", "192.168.1.69:50021/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)

def count_pdf_characters(file_path):
    doc = fitz.open(file_path)
    text = ""
    for page in doc:
        text += page.get_text()
    return len(text)

def main():
    # Gmail API サービスを初期化
    service = gmail_init()
    
    # ラベル ID を指定して最新の未読メール本文とURLを取得
    body, urls = gmail_get_latest_unread_message_body(service, "ラベルID")
    
    # 未読メールがある場合は音声ファイルを再生
    if body:
        playsound('notice.wav')
        generate_and_play_audio_from_text(body)
        

        if urls:
            for url in urls:
                print(f"プレビューリンクを検索するURL: {url}")
                # プレビューリンクを取得
                preview_url = find_preview_link(url)
                if preview_url:
                    print(f"プレビューリンク: {preview_url}")
                    # プレビューリンクからPDFファイルのダウンロードを試みる
                    download_pdf(preview_url, file_path="downloaded_file.pdf")
                    
                    # PDFファイルから文字数をカウント
                    char_count = count_pdf_characters("downloaded_file.pdf")
                    print(f"PDF内の文字数: {char_count}")
                    
                    # 文字数が100文字以上の場合は別の音声ファイルを再生
                    if char_count >= 100:
                        playsound('notice_pdf.wav')

                else:
                    print("プレビューリンクが見つかりませんでした。")
        else:
            print("メールにURLが見つかりませんでした。")
    else:
        print("未読メールはありません。")

if __name__ == "__main__":
    main()

とすることで
メールの読み上げと
PDFのダウンロードができた
また長文のPDFに関してはPDFを見るように促すようにした

あとは顔を認識したら起動するようにする

まずはこれをモジュールにする

import fitz
from playsound import playsound
import subprocess
import pygame
import time

# テキストから音声を生成して再生する関数
def generate_and_play_audio_from_text(text):
    command_json = [
        "curl", "-s", "-X", "POST",
        "192.168.1.69:50021/audio_query?speaker=1",
        "--get", "--data-urlencode", f"text={text}"
    ]
    command_audio = [
        "curl", "-s", "-H", "Content-Type: application/json", "-X", "POST",
        "-d", "@query.json", "192.168.1.69:50021/synthesis?speaker=1"
    ]
    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.init()
    pygame.mixer.init()
    sound = pygame.mixer.Sound("audio_output.wav")
    sound.play()
    while pygame.mixer.get_busy():
        time.sleep(0.1)

# PDFファイルから文字数をカウントする関数
def count_pdf_characters(file_path):
    doc = fitz.open(file_path)
    text = ""
    for page in doc:
        text += page.get_text()
    return len(text)

# メールの処理を行う関数
def process_email(service, label_id):
    from gmail_utils import gmail_get_latest_unread_message_body
    from pdf_downloader import find_preview_link, download_pdf

    body, urls = gmail_get_latest_unread_message_body(service, label_id)
    if body:
        playsound('notice.wav')
        generate_and_play_audio_from_text(body)
        if urls:
            for url in urls:
                preview_url = find_preview_link(url)
                if preview_url:
                    download_pdf(preview_url, "downloaded_file.pdf")
                    char_count = count_pdf_characters("downloaded_file.pdf")
                    if char_count >= 100:
                        playsound('notice_pdf.wav')
                else:
                    print("プレビューリンクが見つかりませんでした。")
        else:
            print("メールにURLが見つかりませんでした。")
    else:
        print("未読メールはありません。")

として

email_processor.py

として保存

次に顔を検出したらこれを呼び出すようにする