ディープラーニングのための並列処理を用いた画像の前処理方法

## 本記事について
ディープラーニングで学習させる前に行う画像データの前処理は、
機械学習モデルの精度を向上させるために重要です。

本記事では、縦向きの画像を横向きに回転し、画像を正方形にトリミングし、指定したサイズにリサイズする一連の処理を行うPythonモジュールを紹介します。
また、処理速度を向上させるためにマルチプロセッシングを利用して並列処理を実装します。

## 必要なライブラリ
* pathlib: ファイルパス操作を簡素化
* PIL (Pillow): 画像処理
* shutil: ディレクトリ操作
* time: 処理時間の計測
* multiprocessing: 並列処理

## ソースコード
```Python
"""縦向きの画像を横向きに回転して、画像を正方形にトリミングして、画像をリサイズするモジュール"""
from pathlib import Path
from PIL import Image, ImageOps
import shutil
import time
from multiprocessing import Pool

def process_images_in_parallel(input_image_file_path_list: list[Path], output_image_file_path_list: list[Path], resize_size: tuple[int, int]):
    """
    マルチプロセッシングで画像加工を実施する
    """
    start_time = time.time()

    # Pool()オブジェクトを生成して、pool.starmap()を用いて各画像に対して process_image関数を並列に実行する
    with Pool() as pool:
        # pool.starmapメソッドの第一引数は並列実行させたい関数を指定する
        # pool.starmapメソッドの第二引数は関数に渡すリストを指定する。リスト内の各タプルの要素を展開して、それぞれの要素を process_image関数の引数として渡す
        pool.starmap(process_image, [(input_path, output_path, resize_size) for input_path, output_path in zip(input_image_file_path_list, output_image_file_path_list)])
        
    total_time = time.time() - start_time
    print(f"Total processing time: {total_time:.2f} seconds")

def generate_file_path(input_file_path: Path, output_folder_path: Path) -> Path:
    file_name = input_file_path.name
    return output_folder_path / file_name

def process_image(input_image_file_path: Path, output_image_file_path: Path, resize_size: tuple[int, int]):
    """
    指定された画像ファイルに対して以下の処理を順次処理を実施する

    (1) 縦向きの画像を横向きに回転する
    (2) 画像を中心から均等にトリミングして正方形にする
    (3) 指定サイズで画像をリサイズする

    Parameters:
        input_image_file_path (Path): 入力画像ファイルパス
        output_image_file_path (Path): 出力画像ファイルパス
        resize_size (tuple[int, int]): リサイズ後の画像サイズ (幅, 高さ)
    """
    # 画像の回転処理を実行
    rotate_image_if_portrait(input_image_file_path, output_image_file_path)

    # 画像を正方形にトリミングして保存
    trim_image(input_image_file_path, output_image_file_path)

    # リサイズを行う
    resize_image(input_image_file_path, output_image_file_path, resize_size)


def rotate_image_if_portrait(input_image_file_path: Path, output_image_file_path:Path) -> Path:
    """
    縦向きの画像を検出し、必要に応じて90度回転させて保存する関数

    Parameters:
        input_image_file_path (Path): 入力画像ファイルパス
        output_image_file_path (Path): 出力画像ファイルパス

    Returns:
        Path: 出力画像ファイルパス
    """
    # 出力画像フォルダー生成
    output_image_file_path.parent.mkdir(parents=True, exist_ok=True)

    with Image.open(input_image_file_path) as img:
        # 画像のサイズを取得
        width, height = img.size
        
        # 画像が縦向きの場合、90度回転させる
        if height > width:
            rotated_img = img.rotate(-90, expand=True)
            rotated_img.save(output_image_file_path)
        else:
            img.save(output_image_file_path)

    return output_image_file_path


def trim_image(input_image_file_path: Path, output_image_file_path: Path) -> Path:
    """
    画像を中心から均等にトリミングして正方形にし、指定されたパスに保存する関数

    Parameters:
        input_image_file_path (Path): 入力画像ファイルパス
        output_image_file_path (Path): 出力画像ファイルパス
    Returns:
        Path: 出力画像ファイルパス
    """
    # 画像を開く
    with Image.open(input_image_file_path) as img:
        width, height = img.size
        min_side = min(width, height)
        left = (width - min_side) // 2
        top = (height - min_side) // 2
        right = (width + min_side) // 2
        bottom = (height + min_side) // 2

        # 画像を正方形にトリミング
        cropped_img = img.crop((left, top, right, bottom))

        # トリミングされた画像を保存
        cropped_img.save(output_image_file_path)
    
    return output_image_file_path


def resize_image(input_image_file_path: Path, output_image_file_path: Path, resize_size: tuple[int, int]) -> Path:
    """
    画像を指定されたサイズにリサイズして保存する関数

    Parameters:
        input_image_file_path (Path): 入力画像ファイルパス
        output_image_file_path (Path): 出力画像ファイルパス
        resize_size (tuple[int, int]): リサイズ後の画像サイズ (幅, 高さ)

    Returns:
        Path: 出力画像ファイルパス
    """
    with Image.open(input_image_file_path) as img:
        resized_img = ImageOps.fit(img, resize_size, Image.LANCZOS)
        resized_img.save(output_image_file_path)
    return output_image_file_path


if __name__ == '__main__':
    # ベースフォルダーとサブフォルダーを設定する
    input_folder = "input"
    output_folder = "output"
    pumpkin_folder = "pumpkin"
    green_pepper_folder = "green_papper"
    tomato_folder = "tomato"
    network_pumpkin_folder = "network_pumpkin"
    network_green_papper_folder = "network_green_papper"
    network_tomato_folder = "network_tomato"

    subfolders = [pumpkin_folder, green_pepper_folder, tomato_folder]
    network_subfolders = [network_pumpkin_folder, network_green_papper_folder, network_tomato_folder]
    subfolders = subfolders + network_subfolders

    # 画像のリサイズサイズを設定する
    resize_height = 128
    resize_width = 128
    resize_size: tuple[int, int]= (resize_width, resize_height)

    # 拡張画像数を設定する
    num_augmented = 1000

    # 出力フォルダーを削除して新規に生成する
    output_folder_path = Path(output_folder)
    if output_folder_path.exists():
        shutil.rmtree(output_folder)

    for subfolder in subfolders:
        # 入力画像フォルダーパスを生成する
        input_folder_path = Path(input_folder) / subfolder

        # 出力画像フォルダーパスを生成する
        output_folder_path = Path(output_folder) / subfolder

        # 入力画像ファイルパスリストを生成する (画像ファイルの拡張子はすべてjpgとする)
        input_image_file_path: list[Path] = list(input_folder_path.glob("*.jpg"))

        # 出力画像ファイルパスリストを生成する
        output_image_file_path: list[Path] = [generate_file_path(path, output_folder_path) for path in input_image_file_path]

        # 画像加工処理を実行する
        process_images_in_parallel(input_image_file_path, output_image_file_path, resize_size)
```


## 使用ケース(ネットから収集した画像加工)
Google Colabを使用して、icrawlerモジュールを用いて、Edgeから画像を集めた場合は、
画像のサイズはバラバラで取得されます。

そこで、本記事の処理を用いて加工すると、画像サイズを合わせることができ、
DeepLearningの学習に使いやすくなります。


インターネットから画像収取するコードを参考に記載します。
指定した画像以外も取得されてしまうので、手動で画像選別が必要な点には注意してください。

``` python
from pathlib import Path
import os

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


#必要な機能のインストール
!pip install icrawler

#pythonライブラリの「icrawler」でBing用モジュールをインポート
from icrawler.builtin import BingImageCrawler


def collect_image(search_word: str, target_folder: str):
    #ダウンロードするキーワード
    crawler = BingImageCrawler(storage={'root_dir': target_folder})
    #ダウンロードする画像の最大枚数は130枚
    crawler.crawl(keyword=search_word, max_num=500)

base_folder = "/content/drive/MyDrive/images/vegetables/"

collect_image("トマト", Path(base_folder / "tomato"))
collect_image("ナス", Path(base_folder / "eggplant"))
collect_image("かぼちゃ", Path(base_folder / "pumpkin"))
collect_image("ピーマン", Path(base_folder / "green_papper"))
```

## まとめ
本記事では、縦向きの画像を横向きに回転し、正方形にトリミングし、指定サイズにリサイズする一連の画像前処理を並列処理を用いて効率的に行う方法を紹介させていただきました。

並列処理の画像加工により、大幅に短縮することが可能となります。
並列処理を用いて、画像加工をどしどし行いましょう✨