Albumentations物体検出データ拡張

目次

概要

Albumentationsライブラリを用いた物体検出用データ拡張技術を実装・体験する。データ拡張とは、既存の訓練データに変換処理を施して人工的にデータ量を増加させる手法である。

主要技術:Albumentations(画像拡張ライブラリ)

本プログラムでは、Albumentationsライブラリを使用してサンプル画像に対して複数の変換手法を組み合わせたデータ拡張を実行する。幾何学的変換(回転、スケール、平行移動)、色彩変換(明度、コントラスト調整)、ノイズ追加などの処理を適用し、バウンディングボックス座標を変換に合わせて自動調整する。拡張前後の画像とバウンディングボックスの変化を可視化することで、データ拡張の効果を確認できる。

論文:Buslaev, A., Iglovikov, V. I., Khvedchenya, E., Parinov, A., Druzhinin, M., & Kalinin, A. A. (2020). Albumentations: Fast and Flexible Image Augmentations. Information, 11(2), 125. https://doi.org/10.3390/info11020125

新規性・特徴

特徴を活かせるアプリ例: 自動運転での物体認識、監視カメラでの異常検知、医用画像での病変検出

体験価値: データ拡張による物体検出精度向上の確認、バウンディングボックス座標変換の自動化の体験、変換手法の組み合わせ効果の観察

事前準備

Python, Windsurfをインストールしていない場合の手順(インストール済みの場合は実行不要)。

  1. 管理者権限でコマンドプロンプトを起動する(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)。
  2. 以下のコマンドをそれぞれ実行する(winget コマンドは1つずつ実行)。
REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent
REM Windsurf をシステム領域にインストール
winget install --scope machine --id Codeium.Windsurf -e --silent
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul
REM Windsurf のパス設定
set "WINDSURF_PATH=C:\Program Files\Windsurf"
if exist "%WINDSURF_PATH%" (
    echo "%PATH%" | find /i "%WINDSURF_PATH%" >nul
    if errorlevel 1 setx PATH "%PATH%;%WINDSURF_PATH%" /M >nul
)

必要ライブラリのインストール。コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。

pip install albumentations opencv-python matplotlib numpy japanize-matplotlib

プログラムコード


# Albumentations物体検出データ拡張プログラム
#   物体検出用画像データの包括的拡張処理
#   論文: "Albumentations: Fast and Flexible Image Augmentations" (Information 2020)
#   GitHub: https://github.com/albumentations-team/albumentations
#   特徴: Albumentationsは高速で柔軟な画像拡張ライブラリ、物体検出でのSOTA性能
#         バウンディングボックス座標の自動変換、豊富な変換手法、高い実行速度
#   前準備: pip install albumentations opencv-python matplotlib numpy japanize-matplotlib

# 利用可能な学習済みモデル:
#   - YOLOv8, YOLOv9, YOLOv10, YOLOv11, YOLOv12: 最新YOLO系列(リアルタイム処理)
#   - Faster R-CNN: 高精度、特に小物体検出に優秀
#   - SSD (Single Shot Detector): 高速処理に最適化
#   - DETR (Detection Transformer): Transformer基盤のエンドツーエンド検出
#   - RF-DETR: リアルタイム対応のDETR改良版
#   - YOLO-NAS: Deci社開発、YOLOv5/v7/v8を上回る性能
#   - RetinaNet: 小物体検出とクラス不均衡対応
#   - EfficientDet: 効率的なスケーリングと高性能

import cv2
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import albumentations as A
import japanize_matplotlib

# 定数定義
IMAGE_SIZE = (416, 416)  # YOLO系モデルでよく使用される入力サイズ
RANDOM_SEED = 42  # 再現可能な結果のための乱数シード
BBOX_COLORS = ['red', 'blue', 'green', 'yellow', 'purple', 'orange']
AUGMENTATION_COUNT = 5  # 生成する拡張画像数
BATCH_SAMPLE_COUNT = 3  # バッチ処理のサンプル数

print("=== 物体検出用データ拡張デモ ===")

# 乱数シード設定
np.random.seed(RANDOM_SEED)

# メイン処理
print("1. サンプルデータ生成")

# サンプル画像生成
sample_image = np.random.randint(0, 255, (416, 416, 3), dtype=np.uint8)

# 物体を模擬した矩形描画
cv2.rectangle(sample_image, (50, 50), (150, 120), (255, 0, 0), -1)
cv2.rectangle(sample_image, (200, 180), (320, 280), (0, 255, 0), -1)
cv2.rectangle(sample_image, (100, 300), (200, 380), (0, 0, 255), -1)

sample_bounding_boxes = [[50, 50, 150, 120], [200, 180, 320, 280], [100, 300, 200, 380]]
sample_class_labels = [0, 1, 2]

print(f"画像サイズ: {sample_image.shape}")
print(f"バウンディングボックス数: {len(sample_bounding_boxes)}個")
print(f"クラス数: {len(set(sample_class_labels))}種類")

print("2. 単一画像の拡張")

# 訓練用変換設定
training_transform = A.Compose([
    A.LongestMaxSize(max_size=max(IMAGE_SIZE), p=1.0),
    A.PadIfNeeded(min_height=IMAGE_SIZE[0], min_width=IMAGE_SIZE[1],
                 border_mode=cv2.BORDER_CONSTANT, value=0, p=1.0),
    A.OneOf([
        A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.2, rotate_limit=15,
                         border_mode=cv2.BORDER_CONSTANT, value=0, p=1.0),
        A.Perspective(scale=(0.05, 0.15), p=1.0),
        A.Affine(translate_percent=(-0.1, 0.1), scale=(0.8, 1.2),
                shear=(-10, 10), p=1.0),

], p=0.7),
    A.HorizontalFlip(p=0.5),
    A.OneOf([
        A.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2,
                     hue=0.1, p=1.0),
        A.HueSaturationValue(hue_shift_limit=20, sat_shift_limit=30,
                           val_shift_limit=20, p=1.0),
        A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2,
                                 p=1.0),

], p=0.6),
    A.OneOf([
        A.GaussNoise(var_limit=(10, 50), p=1.0),
        A.MotionBlur(blur_limit=3, p=1.0),

], p=0.3),
    A.CoarseDropout(max_holes=8, max_height=32, max_width=32,
                   min_holes=1, fill_value=0, p=0.2),

], bbox_params=A.BboxParams(format='pascal_voc', label_fields=['labels'],
                           min_visibility=0.3))

# 元データ保存
original_image_data = {'image': sample_image, 'bboxes': sample_bounding_boxes,
                      'labels': sample_class_labels}

# データ拡張実行
augmented_image_list = []
for i in range(AUGMENTATION_COUNT):
    transformation_result = training_transform(image=sample_image,
                                             bboxes=sample_bounding_boxes,
                                             labels=sample_class_labels)
    augmented_image_list.append(transformation_result)
    print(f"拡張{i+1}: バウンディングボックス数 = {len(transformation_result['bboxes'])}個")

print("3. バッチ拡張")

# バッチデータ拡張
batch_processing_data = []
batch_processing_data.append({'image': sample_image, 'bboxes': sample_bounding_boxes,
                             'labels': sample_class_labels})
for i in range(BATCH_SAMPLE_COUNT - 1):
    batch_result = training_transform(image=sample_image,
                                    bboxes=sample_bounding_boxes,
                                    labels=sample_class_labels)
    batch_processing_data.append(batch_result)

print(f"バッチ拡張結果: {len(batch_processing_data)}件")

# 結果出力
print("4. 結果可視化")
print("グラフウィンドウが表示されます")

# 可視化処理
figure, subplot_axes = plt.subplots(2, 2, figsize=(16, 12))
subplot_axes = subplot_axes.flatten()

# 元画像表示
subplot_axes[0].imshow(original_image_data['image'])
subplot_axes[0].set_title("元画像")
subplot_axes[0].axis('off')

for bbox_coords, class_label in zip(original_image_data['bboxes'],
                                   original_image_data['labels']):
    x1, y1, x2, y2 = bbox_coords
    bbox_width, bbox_height = x2 - x1, y2 - y1

    rectangle_patch = patches.Rectangle((x1, y1), bbox_width, bbox_height,
                                       linewidth=2,
                                       edgecolor=BBOX_COLORS[int(class_label) % len(BBOX_COLORS)],
                                       facecolor='none')
    subplot_axes[0].add_patch(rectangle_patch)
    subplot_axes[0].text(x1, y1 - 5, f'クラス {int(class_label)}',
                        color=BBOX_COLORS[int(class_label) % len(BBOX_COLORS)],
                        fontsize=8, weight='bold')

# 拡張画像表示
for subplot_index in range(1, min(4, len(augmented_image_list) + 1)):
    data_index = min(subplot_index-1, len(augmented_image_list)-1)
    current_augmented_data = augmented_image_list[data_index]

    subplot_axes[subplot_index].imshow(current_augmented_data['image'])
    subplot_axes[subplot_index].set_title(f"拡張画像 {subplot_index}")
    subplot_axes[subplot_index].axis('off')

    for bbox_coords, class_label in zip(current_augmented_data['bboxes'],
                                       current_augmented_data['labels']):
        x1, y1, x2, y2 = bbox_coords
        bbox_width, bbox_height = x2 - x1, y2 - y1

        rectangle_patch = patches.Rectangle((x1, y1), bbox_width, bbox_height,
                                           linewidth=2,
                                           edgecolor=BBOX_COLORS[int(class_label) % len(BBOX_COLORS)],
                                           facecolor='none')
        subplot_axes[subplot_index].add_patch(rectangle_patch)
        subplot_axes[subplot_index].text(x1, y1 - 5, f'クラス {int(class_label)}',
                                        color=BBOX_COLORS[int(class_label) % len(BBOX_COLORS)],
                                        fontsize=8, weight='bold')

plt.tight_layout()
plt.show()

print("=== デモ完了 ===")
print("結果の読み方:")
print("- 元画像: 変換前のサンプル画像(3つの矩形物体)")
print("- 拡張画像: 各種変換を適用した結果(位置、色、形状の変化)")
print("- バウンディングボックス: 物体位置を示す枠(色別にクラス識別)")

使用方法

実行手順

  1. 上記のプログラムを実行する

実行結果と確認方法

コンソールに処理進行状況が表示される。2×2のグラフウィンドウが表示される。元画像(左上)と3つの拡張画像が表示される。各物体にバウンディングボックスとクラス番号が表示される。

元画像では3つの矩形物体(青、緑、赤)が表示される。拡張画像では回転、スケール、色調変更、ノイズ追加等の組み合わせが適用される。バウンディングボックスは物体の位置変化に合わせて調整される。元画像と比較して物体の位置や色が変化しているか、バウンディングボックスが適切に追従しているかを確認する。

実験・探求のアイデア

変換パラメータの調整実験

rotate_limitを5から30に変更して回転角度の影響を観察する(値を大きくするとバウンディングボックスが画像外に出る可能性がある)。scale_limitを0.1から0.5に変更してスケール変化の効果を確認する(値が極端な場合は物体が小さくなりすぎる)。brightness_limitを0.1から0.4に変更して明度変化の影響を検証する。

変換手法の組み合わせ実験

OneOfの確率値(p)を変更して適用頻度を調整する。A.CutoutA.GridDistortionを追加して変換効果を確認する。A.RandomCropを追加して画像切り取り効果を観察する。

バウンディングボックス保持実験

min_visibilityを0.1から0.8に変更して物体の可視性閾値を調整する(値が高い場合は変換後に物体が除外されやすくなる)。スケール変更の範囲を0.3-2.0に設定してバウンディングボックス消失を観察する。回転角度を45度に設定してバウンディングボックスの変化を確認する。

データセット形式の活用

実際のデータセット形式での実験も可能である。COCO形式やPascal VOC形式(物体検出データセットの標準的なアノテーション形式)を使用することで、実用的な検証ができる。