画像データの水増し

画像データの水増し

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でアノテーションその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つのものとしてみなすため
意味がなかった

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

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

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

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

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

として保存

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

Gmail読み上げとメールのお知らせを作る

Gmail読み上げとメールのお知らせを作る

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

でテキストファイルの作成とPDF取得ができているので
お知らせメッセージの
notice.wavを再生する

GPTによれば

pip install playsound

でインストールし
コードを変更する

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 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")
                else:
                    print("プレビューリンクが見つかりませんでした。")
        else:
            print("メールにURLが見つかりませんでした。")

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

if __name__ == "__main__":
    main()

変更点は

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

で音声ファイルを読み込んでいること

Main.pyの中で

    # 未読メールがある場合は音声ファイルを再生
    if body:
        playsound('notice.wav')

を追加して音声を出している

次にPDFの文字数を調べて100文字以上なら
音声でPDFの内容を確認するように促す

あとはメール本文の読み上げ処理だけ

PyMuPDFを使い
文字数をカウント、文字数が100文字以上ならPDFを確認する音声を再生するようにコード変更

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

あとはvoicevoxの処理のみ

とりあえずdockerを起動させる

docker run -d -p '192.168.1.69:50021:50021' voicevox/voicevox_engine:cpu-ubuntu20.04-latest

でやったら普通に音声が再生できたので
単純に負荷でdockerが落ちてたみたい

文章を音声で読み上げしたが
どうやら最初の文章に日付と宛先の名前がくっついている
毎回main3.pyを実行するたびに他のメールでも同じなので
最初の文章を削除する

Gmail未読メールの読み上げ

Gmail未読メールの読み上げ

cp mail_message.py unread_mail_message.py


ファイルをコピーしてから改造
未読の最新の1件のみ取得するようにする

ChatGPTでの出力
最新の未読メール1件のみを取得するようにコードを修正します。
既にmessages.list API呼び出しでmaxResults=1を指定しているため、最新の1件だけを取得しています。
しかし、一番新しいメールが未読でない可能性があるため、
念のためにメッセージのリストをループする部分を削除し、最初のメッセージのみを処理するようする

from __future__ import print_function
import os.path
import re
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
import base64
import email
import dateutil.parser

# スコープの設定
SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']

# トークンとクレデンシャルのパス
tokenPath = "token.json"
credentialsPath = "credentials.json"

# メール本文のデコード関数
def decode(encoded):
    decoded = base64.urlsafe_b64decode(encoded).decode()
    return decoded

# URLを削除する関数
def remove_urls(text):
    # URLにマッチする正規表現パターン
    url_pattern = r'https?://\S+|www\.\S+'
    return re.sub(url_pattern, '', text)

# 最新の未読メール本文を取得する関数
def gmail_get_latest_unread_message_body(service, labelIdsValue):
    messages = service.users().messages()
    msg_list = messages.list(userId='me', labelIds=labelIdsValue, q="is:unread", maxResults=1).execute()

    if 'messages' not in msg_list:
        return "No unread messages found."

    # 最新のメッセージ(最新の未読メール)を選択
    msg = msg_list['messages'][0]
    date = gmail_get_messages_body_date(messages, msg)
    topid = msg['id']
    msg = messages.get(userId='me', id=topid).execute()

    # メールの本文を取得
    body = ""
    if msg["payload"]["body"]["size"] != 0:
        body = decode(msg["payload"]["body"]["data"])
    elif 'parts' in msg["payload"]:
        # メール本文が parts 属性にある場合
        for part in msg["payload"]["parts"]:
            if part["body"]["size"] != 0:
                body = decode(part["body"]["data"])
                break

    if not body:
        return date + "<br> No body content"

    # URLを削除
    body_no_urls = remove_urls(body)
    return date + "<br>" + body_no_urls

# メールの受信日時を取得する関数(変更なし)
def gmail_get_messages_body_date(messages, msg):
    msg_id = msg['id']
    m = messages.get(userId='me', id=msg_id, format='raw').execute()
    raw = base64.urlsafe_b64decode(m['raw'])
    eml = email.message_from_bytes(raw)
    date = dateutil.parser.parse(eml.get('Date')).strftime("%Y-%m-%d_%H-%M-%S")
    return date

# Gmail API 初期化関数(変更なし)
def gmail_init():
    creds = None
    if os.path.exists(tokenPath):
        creds = Credentials.from_authorized_user_file(tokenPath, SCOPES)
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                credentialsPath, SCOPES)
            creds = flow.run_local_server(port=0)
        with open(tokenPath, 'w') as token:
            token.write(creds.to_json())
    service = build('gmail', 'v1', credentials=creds)
    return service

# メイン処理
service = gmail_init()

# ラベル ID を指定して最新の未読メール本文を取得
latest_unread_message_body = gmail_get_latest_unread_message_body(service, "Label_4")
print(latest_unread_message_body)

これで最新の1件のみ取得できるようになった
さらに差し出し元などもなく本文のみ取得している

URLの削除もできているが
メルマガによっては
——
などで区切っているところがある
これは読み上げるとどうなるのか?

もしダメなら、削除項目を増やすこと

とりあえず本文を読み上げる

まず支援学校のメールのIDになるように

latest_unread_message_body = gmail_get_latest_unread_message_body(service, "Label_4")

Label_4の部分を変える

次に
本文が

特別支援学校よりメッセージをお預かりしています。
下記URLをクリックして内容を確認してください。



※学校からのお知らせの保存期間は1年間です。
重要なお知らせは、スクリーンショットなどでお手元に保存してください。
※お手元で保存された画像等データのお取り扱いにつきましては、個人情報保護に十分ご配慮ください。

となって取得できているので
記号がどうなるか試すため
Docker を起動して実験

docker run -d  -p '192.168.1.69:50021:50021' voicevox/voicevox_engine:cpu-ubuntu20.04-latest

あとは
取得したメールの本文が
mail.txt
にあるので中のテキストをもとにして音声を作成する

curl -s -X POST "192.168.1.69:50021/audio_query?speaker=1" --get --data-urlencode text@mail.txt > query.json

curl -s -H "Content-Type: application/json" -X POST -d @query.json "192.168.1.69:50021/synthesis?speaker=1" > mail_audio.wav

普通に問題なく記号や句読点などは読み上げることなく成功した

支援学校は問題なさそうなので
メルマガで実験する

サーチマン佐藤さんのメルマガを使い実験

mail_searchman.txt

として保存し

curl -s -X POST "192.168.1.69:50021/audio_query?speaker=1" --get --data-urlencode text@mail_searchman.txt > query.json

curl -s -H "Content-Type: application/json" -X POST -d @query.json "192.168.1.69:50021/synthesis?speaker=1" > mail_searchman_audio.wav

で作成し音声を作成

しかし途中でdocker が過負荷で落ちる
このためおそらくこの方法ではメルマガなどの長文を再生することは出来なさそう

現在地の天気情報の取得その2

現在地の天気情報の取得その2

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"])

で実行すると

{'base': 'stations',
 'clouds': {'all': 0},
 'cod': 200,
 'coord': {'lat': 緯度, 'lon': 経度},
 'dt': 1701631236,
 'id': 0,
 'main': {'feels_like': 4.12,
          'humidity': 60,
          'pressure': 1021,
          'temp': 6.65,
          'temp_max': 6.65,
          'temp_min': 6.65},
 'name': 'Kawai',
 'sys': {'country': 'JP',
         'id': 2008260,
         'sunrise': 1701639567,
         'sunset': 1701675443,
         'type': 2},
 'timezone': 32400,
 'visibility': 10000,
 'weather': [{'description': 'clear sky',
              'icon': '01n',
              'id': 800,
              'main': 'Clear'}],
 'wind': {'deg': 288, 'gust': 5.36, 'speed': 3.58}}
天気: Clear
天気詳細: clear sky
都市名: 番地
気温: 6.65
体感気温: 4.12
最低気温: 6.65
最高気温: 6.65
気圧: 1021
湿度: 60
風速: 3.58
風の方角: 288

となる

デフォルトがだと気温が華氏で取得されます。摂氏を基準としたい場合はクエリに units=metric を含めます

これだと英語なので日本語で天気を取得したい

https://zenn.dev/masaru21/articles/d26519ec888f01
を元に

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>日本語のタイトル</title>
    <!--
    <link rel="stylesheet" href="/css/style.css" type="text/css">
    <script type="text/javascript" src="/js/main.js"></script>

    -->

  </head>
  <body>
  <script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script>
  <script>

  $(function() {
    var API_KEY = 'APIキー'
    var item_id = 'lat=35.2932718&lon=139.97671'
    
    var url = 'https://api.openweathermap.org/data/2.5/forecast?' + item_id + '&lang=ja&units=metric&appid=' + API_KEY

    $.ajax({
      url: url,
      dataType: "json",
      type: 'GET',
    })
    .done(function(data) {
      var insertHTML = "";
      var cityName = '<h2>' + data.city.name + '</h2>';
      $('#city-name').html(cityName);
      for (var i = 0; i <= 39; i = i + 8) {
        insertHTML += buildHTML(data, i);
      }
      $('#weather').html(insertHTML);
    })
    .fail(function(data) {
      console.log("失敗しました");
    });
  });

  function buildHTML(data, i) {
    var Week = new Array("(日)","(月)","(火)","(水)","(木)","(金)","(土)");
    var date = new Date (data.list[i].dt_txt);
    date.setHours(date.getHours() + 9);
    var month = date.getMonth()+1;
    var day = month + "月" + date.getDate() + "日" + Week[date.getDay()] + date.getHours() + ":00";
    var icon = data.list[i].weather[0].icon;
    var html =
    '<div class="weather-report">' +
      '<div id="cityname">' + data.city.name + '</div>' +
      '<img src="https://openweathermap.org/img/w/' + icon + '.png">' +
      '<div class="weather-date">' + day + '</div>' +
      '<div class="weather-main">'+ data.list[i].weather[0].description + '</div>' +
      '<div class="weather-temp">' + Math.round(data.list[i].main.temp) + '℃</div>' +
    '</div>';
    return html
  }

  </script>
  <style>
    .content {
        text-align: center;
        margin-left: auto;
        margin-right: auto;
    }

    .weather-report {
        margin-right: 20px;
        margin-left: 20px;
        float: left;
    }

  </style>   
    <div class="content">
      <div id="weather"></div>

    </div>
 

  </body>
</html>

をvscode の golive で実行してみた

これで5日間の天気が表示される
ただし、場所が

http://api.openweathermap.org/geo/1.0/direct?q=fukuokashi&limit=5&appid={API KEY}

で福岡に指定している

ソースの中で

 var item_id = 'lat=35.2932718&lon=139.97671'

で緯度経度を指定している

とりあえずchat GPT で日本語で天気を取得する方法を調べたら

Pythonの辞書を使って英語の天気の説明を日本語にマッピングする
英語から日本語への基本的な翻訳の辞書を作り
APIから取得した英語の天気説明を日本語に変換

書に含まれていない天気の条件には「不明」と表示されますが、必要に応じて辞書を拡張する

とのこと

# 天気の英語から日本語への翻訳辞書
weather_translation = {
    "Clear": "晴れ",
    "Clouds": "曇り",
    "Rain": "雨",
    "Snow": "雪",
    "Thunderstorm": "雷雨",
    "Drizzle": "霧雨",
    "Mist": "霧",
    "Fog": "濃霧"
}

# APIから取得した天気を翻訳する
weather_english = jsondata["weather"][0]["main"]
weather_japanese = weather_translation.get(weather_english, "不明")  # 辞書にない場合は「不明」と表示

print("天気(日本語):", weather_japanese)

この処理を加え

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キー")

# 天気の英語から日本語への翻訳辞書
weather_translation = {
    "Clear": "晴れ",
    "Clouds": "曇り",
    "Rain": "雨",
    "Snow": "雪",
    "Thunderstorm": "雷雨",
    "Drizzle": "霧雨",
    "Mist": "霧",
    "Fog": "濃霧"
}


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

# APIから取得した天気を翻訳する
weather_english = jsondata["weather"][0]["main"]
weather_japanese = weather_translation.get(weather_english, "不明")  # 辞書にない場合は「不明」と表示

pprint(jsondata)

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

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"])

とすることで天気が日本語になった

つまり自分で辞書を作成しないとだめ

とりあえず
* Clear sky(晴天)
* Few clouds(少し曇り)
* Scattered clouds(散らばった雲)
* Broken clouds(雲が多い)
* Overcast clouds(曇天)
* Light rain(小雨)
* Moderate rain(中雨)
* Heavy intensity rain(大雨)
* Very heavy rain(非常に激しい雨)
* Extreme rain(極端な雨)
* Freezing rain(凍雨)
* Light intensity shower rain(弱いにわか雨)
* Shower rain(にわか雨)
* Heavy intensity shower rain(激しいにわか雨)
* Ragged shower rain(不規則なにわか雨)
* Light snow(小雪)
* Snow(雪)
* Heavy snow(大雪)
* Sleet(みぞれ)
* Light shower sleet(軽いにわかみぞれ)
* Shower sleet(にわかみぞれ)
* Light rain and snow(小雨と雪)
* Rain and snow(雨と雪)
* Light shower snow(軽いにわか雪)
* Shower snow(にわか雪)
* Heavy shower snow(激しいにわか雪)
* Mist(靄)
* Smoke(煙)
* Haze(霞)
* Sand/ dust whirls(砂塵旋風)
* Fog(霧)
* Sand(砂)
* Dust(ほこり)
* Volcanic ash(火山灰)
* Squalls(スコール)
* Tornado(竜巻)
* Thunderstorm with light rain(雷雨と小雨)
* Thunderstorm with rain(雷雨)
* Thunderstorm with heavy rain(雷雨と大雨)
* Light thunderstorm(軽い雷雨)
* Thunderstorm(雷雨)
* Heavy thunderstorm(激しい雷雨)
* Ragged thunderstorm(不規則な雷雨)
* Thunderstorm with light drizzle(雷雨と弱い霧雨)
* Thunderstorm with drizzle(雷雨と霧雨)
* Thunderstorm with heavy drizzle(雷雨と激しい霧雨)

となるらしい

面倒なので

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

としたら詳細天気は英語から日本語になったけど
天気は英語のままなのでやっぱり辞書は必要かも

とりあえず一日の最高気温と最低気温を取得したいのなら
もう少し処理が必要

import requests
import json
from datetime import datetime, timedelta

# OpenWeather APIのURLとパラメータを設定
url = "https://api.openweathermap.org/data/2.5/onecall?lat={lat}&lon={lon}&exclude=hourly,minutely&units=metric&appid={API_key}"
latitude = "緯度"  # 例としての緯度
longitude = "経度"  # 例としての経度
API_key = "YOUR_API_KEY"  # あなたのAPIキーをここに入力

# APIリクエスト
url = url.format(lat=latitude, lon=longitude, API_key=API_key)
response = requests.get(url)
jsondata = response.json()

# 今日の日付を取得
today = datetime.now().date()

# 今日の最高気温と最低気温を探す
for daily in jsondata["daily"]:
    date = datetime.fromtimestamp(daily["dt"]).date()
    if date == today:
        min_temp = daily["temp"]["min"]
        max_temp = daily["temp"]["max"]
        print("今日の最高気温:", max_temp, "度")
        print("今日の最低気温:", min_temp, "度")
        break

というように緯度経度で取得する

とりあえず日本語で天気取得できたので
これを音声にする

YouTube live カメラ動画をopencvで表示

YouTube live カメラ動画をopencvで表示

静岡県の薩埵峠のyoutubeライブ画像をopencv で表示する

当初は

YouTube動画をOpenCVでキャプチャするスクリプト
を参考に実行したがエラーとなる

YouTubeのライブ配信をOpenCVで再生する
も同様にエラーとなる

ChatGpt で
opencv でYouTubeライブカメラの画像を表示するPythonコード
を表示し実行したが
これもエラー

ERROR: Unable to extract uploader id; please report this issue on https://yt-dl.org/bug . Make sure you are using the latest version; see  https://yt-dl.org/update  on how to update. Be sure to call youtube-dl with the --verbose flag and include its complete output.
Traceback (most recent call last):
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/youtube_dl/YoutubeDL.py", line 815, in wrapper
    return func(self, *args, **kwargs)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/youtube_dl/YoutubeDL.py", line 836, in __extract_info
    ie_result = ie.extract(url)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/youtube_dl/extractor/common.py", line 534, in extract
    ie_result = self._real_extract(url)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/youtube_dl/extractor/youtube.py", line 1794, in _real_extract
    'uploader_id': self._search_regex(r'/(?:channel|user)/([^/?&#]+)', owner_profile_url, 'uploader id') if owner_profile_url else None,
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/youtube_dl/extractor/common.py", line 1012, in _search_regex
    raise RegexNotFoundError('Unable to extract %s' % _name)
youtube_dl.utils.RegexNotFoundError: Unable to extract uploader id; please report this issue on https://yt-dl.org/bug . Make sure you are using the latest version; see  https://yt-dl.org/update  on how to update. Be sure to call youtube-dl with the --verbose flag and include its complete output.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/youtuvelive.py", line 36, in <module>
    display_youtube_stream(youtube_url)
  File "/Users/snowpool/aw10s/youtuvelive.py", line 11, in display_youtube_stream
    info_dict = ydl.extract_info(url, download=False)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/youtube_dl/YoutubeDL.py", line 808, in extract_info
    return self.__extract_info(url, ie, download, extra_info, process)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/youtube_dl/YoutubeDL.py", line 824, in wrapper
    self.report_error(compat_str(e), e.format_traceback())
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/youtube_dl/YoutubeDL.py", line 628, in report_error
    self.trouble(error_message, tb)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/youtube_dl/YoutubeDL.py", line 598, in trouble
    raise DownloadError(message, exc_info)
youtube_dl.utils.DownloadError: ERROR: Unable to extract uploader id; please report this issue on https://yt-dl.org/bug . Make sure you are using the latest version; see  https://yt-dl.org/update  on how to update. Be sure to call youtube-dl with the --verbose flag and include its complete output.

となる

youtube_dlライブラリがYouTubeのビデオからアップローダーIDを抽出することができない
で検索

Pythonのパッケージyoutube_dlで、DownloadErrorが発生する。

を参考に

pip install yt-dlp

でインストールし

import youtube_dl

の代わりに

from yt_dlp import YoutubeDL

でインポート

import cv2
import youtube_dl
from yt_dlp import YoutubeDL


def display_youtube_stream(url):
    ydl_opts = {
        'format': 'best[ext=mp4]',  # mp4 format, you can change this to other formats
        'quiet': True,
    }

    with YoutubeDL() as ydl:
        info_dict = ydl.extract_info(url, download=False)
        video_url = info_dict['url']

    cap = cv2.VideoCapture(video_url)

    if not cap.isOpened():
        print("Error: Could not open stream.")
        exit()

    while True:
        ret, frame = cap.read()
        if not ret:
            print("Failed to grab frame.")
            break

        cv2.imshow('YouTube Live Stream', frame)

        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    cap.release()
    cv2.destroyAllWindows()

# Replace with your YouTube live stream URL
youtube_url = 'https://www.youtube.com/watch?v=6S4qvf97cbQ'
display_youtube_stream(youtube_url)

として保存

これで実行すると
YouTube live画像が表示されます

なお表示しているのは
LIVE】静岡市さった峠 交通の要衝

Mac Jupyter notebook メモ

Mac Jupyter notebook メモ

Cmd + ←
で行頭へ移動後

Shift + cmd + →
で行を選択できる

単語ごとの選択は
Option + ←

単語の削除は
Option + backspace

これで
効率的にコードを書ける

[/python]
search_indicate[‘actual’] =search_indicate[‘actual’].str.replace(r”\D”, “”,regex=True)

search_indicate[‘forecast’] =search_indicate[‘forecast’].str.replace(r”\D”, “”,regex=True)

search_indicate[‘previous’] =search_indicate[‘previous’].str.replace(r”\D”, “”,regex=True)

search_indicate
[/python]

これで変換して

date2 =[]

for i in search_indicate['date']:

    new_date = datetime.datetime.strptime(i,"%d/%m/%Y").strftime("%Y-%m-%d")

    date2.append(new_date)

search_indicate['date']=search_indicate['date'].str.replace('/','-')

search_indicate['date'] = date2

search_indicate.fillna(0)

で日付を修正

あとは

actual =(

    alt.Chart(search_indicate)

    .mark_line(opacity=0.8,clip=True)

    .encode(

        x="date:T",

        y=alt.Y("actual:Q",stack=None)

    )

)



forecast =(

    alt.Chart(search_indicate)

    .mark_line(opacity=0.8,clip=True,color='red')

    .encode(

        x="date:T",

        y=alt.Y("forecast:Q",stack=None),      

    )

)



previous =(

    alt.Chart(search_indicate)

    .mark_line(opacity=0.8,clip=True,color='green')

    .encode(

        x="date:T",

        y=alt.Y("previous:Q",stack=None),      

    )

)

alt.layer(actual,forecast,previous).resolve_scale(

    y = 'independent'

)

それぞれのチャートを表示

次に
indicatorの ( 以降を
削除する

まずは

# s = 'abcxyz-xyz('



# print(my_removesuffix(s, '-xyz'))

txt ="ANZ Job Advertisements (MoM)"

print(txt)

として ( 以降を削除

Pythonで文字列の先頭と末尾から空白や文字列を削除する:strip()

を参考にしたが
strip ではできなかった

Replace と正規表現で行う

0回以上の繰り返し
*

任意の英数字は
[a-zA-Z0-9_]

\w

txt.replace("(","")

だとできた

エスケープは不要

year_economic_data2['event']=year_economic_data2['event'].replace("\([a-zA-Z0-9_]+\)","",regex=True)

year_economic_data2

とすることで解決

あとは経済指標カレンダーも同様に処理

# dealing_calender['event']= dealing_calender['event'].str.replace('(','\(').str.replace(')','\)')

dealing_calender['event']=dealing_calender['event'].replace("\([a-zA-Z0-9_]+\)","",regex=True)

とすることで解決

ただし

search_indicate['actual'] =search_indicate['actual'].str.replace(r"\D", "",regex=True)

search_indicate['forecast'] =search_indicate['forecast'].str.replace(r"\D", "",regex=True)

search_indicate['previous'] =search_indicate['previous'].str.replace(r"\D", "",regex=True)

search_indicate

だと

小数点のものまで書き換えてしまい
マイナス、小数点まで削除してしまう

不等号などはそのままにして
%
英数字のみ
削除すればいけるはず

Python: 正規表現ライブラリを使って、グループ化による置換

を参考に

import re

print(re.sub(r'([A-Z][a-z]?)(\d{1,3})', r"\\Nuc{\1}{\2}{}", "He4"))

# \Nuc{He}{4}{}

print(re.sub(r'([A-Z][a-z]?)(\d{1,3})L', r"\\Nuc{\1}{\2}{\\Lambda}", "He4L"))

# \Nuc{He}{4}{\Lambda}

みたいに

search_indicate['actual'] =search_indicate['actual'].str.replace(r"[a-zA-Z%]", "",regex=True)

search_indicate['forecast'] =search_indicate['forecast'].str.replace(r"[a-zA-Z%]", "",regex=True)

search_indicate['previous'] =search_indicate['previous'].str.replace(r"[a-zA-Z%]", "",regex=True)

search_indicate

とすればできた

とりあえず問題はあるけど
経済指標の取得後の処理はできてきた

次はIMM ポジションの取得

https://www.gaitame.com/market/imm/imm_currency.xls

からダウンロードしたいが

Mac だとそのままだとできないらしい

と思ったが Jupiter notebook からだとファイルを開けないみたい

Finder からだと普通にひらけた

Wgetは Mac にはないけど
Curl で代用できる

curl コマンド 使い方メモ

を参考に

-O オプションを付けることでファイル取得と保存ができる

!curl  "https://www.gaitame.com/market/imm/imm_currency.xls" -O

これで取得はコマンドでできるけど
Excel ファイル読み込みでエラー

No module xlrd

のエラーが出たので

【Python エラー】No module xlrd の対処法。権限がなくてxlrdモジュールをインストールできないとき

を参考に

!pip3 install xlrd

でインストール

あと excel の操作もできるように

!pip3 install openpyxl

で追加

あとは notebook を再起動すると
普通にExcelファイルの取り扱いできた

とりあえず
データ取得関連はほぼできた