STEP 24:ガイド付きプロジェクト

🚗 STEP 24: ガイド付きプロジェクト

自動車の損傷検出システムを構築します。
データ収集、アノテーション、YOLOv8による物体検出、評価まで実践的に学びます

📋 このプロジェクトで学ぶこと

  • 実際のビジネス課題に対するCVソリューションの設計
  • データ収集とアノテーションの実践
  • YOLOv8による物体検出モデルの訓練
  • モデルの評価とチューニング
  • mAP(mean Average Precision)の計算と解釈
  • (オプション)Webアプリ化(Gradio / Streamlit)

🎯 プロジェクト概要

ビジネス課題と解決策

【ビジネス課題】 自動車保険会社や中古車販売店では、車両の損傷を評価する必要がある 従来の方法の問題点: ・人間の検査員が目視で確認 ・時間がかかる(1台30分〜1時間) ・見落としのリスク ・人件費が高い ・検査員によって評価がばらつく 【AIによる解決策】 自動車損傷検出システム 入力: 車の写真(スマホで撮影可能) 出力: 損傷の位置と種類 検出対象: ・へこみ(Dent) ・傷(Scratch) ・ひび割れ(Crack) ・ガラスの損傷(Glass Damage) 【期待される効果】 ✓ 検査時間の短縮(30分 → 5分) ✓ 見落としの削減(AIは疲れない) ✓ 人件費の削減 ✓ 24時間365日稼働可能 ✓ 一貫した評価基準(人によるばらつきなし)

プロジェクトの全体像

工程 タスク 使用ツール 難易度
1. データ収集 車の損傷画像を収集(公開データセット or 自分で撮影) Roboflow Universe ★☆☆ 易
2. アノテーション 損傷にBounding Boxを付与、クラスを割り当て LabelImg / Roboflow ★★☆ 中
3. データ前処理 データ拡張、YOLO形式に変換、データの分割 Albumentations ★★☆ 中
4. モデル訓練 YOLOv8で物体検出モデルをファインチューニング Ultralytics YOLOv8 ★★☆ 中
5. 評価 mAP、Precision、Recall、Confusion Matrixで評価 YOLOv8 built-in ★★☆ 中
6. チューニング ハイパーパラメータ調整、モデル比較 YOLOv8 ★★★ 上
7. デモ作成 Webアプリでデモ(オプション) Gradio / Streamlit ★★☆ 中
📊 工程1: データ収集

最初のステップはデータの収集です。機械学習プロジェクトでは「データの質と量」が成功の鍵となります。

1-1. データセットの選択肢

【データ収集の3つの方法】 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 方法1: 公開データセットを使用(推奨) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Roboflow Universe(https://universe.roboflow.com/) 検索キーワード: “car damage” “vehicle damage” おすすめデータセット: ・Car Damage Detection Dataset ・Vehicle Parts and Damage Dataset 利点: ✓ すぐに使える ✓ アノテーション済み ✓ 様々なフォーマットでエクスポート可能 ✓ 無料で利用可能 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 方法2: 自分で撮影 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 対象: ・実際の損傷車両(自分の車、中古車販売店など) ・玩具の車(プロトタイプ作成用) 撮影のポイント: ・様々な角度から撮影(正面、斜め、近接) ・照明条件を変える(晴天、曇天、室内) ・背景を変える(駐車場、ガレージ、道路) ・損傷の種類を網羅する 最低枚数: 100枚以上(各クラス25枚以上) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 方法3: 画像検索で収集 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 検索ワード: “car dent” “車 へこみ” “car scratch” “車 傷” “cracked windshield” “フロントガラス ひび” 注意: ⚠️ 著作権に配慮(個人利用のみ) ⚠️ 商用利用の場合はライセンス確認必須

1-2. Roboflowからデータセットをダウンロード

Roboflowを使って、アノテーション済みのデータセットをダウンロードします。

⚠️ 事前準備

1. Roboflow(https://roboflow.com/)でアカウント作成(無料)
2. API Keyを取得(Settings → API Key)

※ コードが横に長い場合は横スクロールできます

# =================================================== # Roboflowからデータセットをダウンロード # =================================================== # 必要なライブラリのインストール # !pip install roboflow from roboflow import Roboflow # =================================================== # 1. Roboflowの初期化 # =================================================== # Roboflow(): Roboflow APIクライアントを作成 # api_key: Roboflowのアカウントで取得したAPIキー # ※ 自分のAPIキーに置き換えてください rf = Roboflow(api_key=”YOUR_API_KEY”) # workspace(): ワークスペースを指定 # project(): プロジェクトを指定 # ※ 公開データセットの場合は、そのプロジェクト名を指定 project = rf.workspace(“roboflow-universe-projects”).project(“car-damage-detection-u2fvz”) # version(): データセットのバージョンを指定 # download(): 指定フォーマットでダウンロード # “yolov8”: YOLOv8形式でダウンロード dataset = project.version(1).download(“yolov8″) print(f”ダウンロード先: {dataset.location}”)

実行結果:

Downloading Dataset Version Zip in car-damage-detection-1 to yolov8:: 100%|██████████| ダウンロード先: /content/car-damage-detection-1
# =================================================== # 2. データセットの構造を確認 # =================================================== import os # ダウンロードされたディレクトリの構造 dataset_path = dataset.location print(“データセットの構造:”) print(f”{‘─’ * 40}”) # os.walk(): ディレクトリを再帰的に探索 for root, dirs, files in os.walk(dataset_path): # 相対パスを計算 level = root.replace(dataset_path, ”).count(os.sep) indent = ‘│ ‘ * level folder_name = os.path.basename(root) print(f'{indent}├── {folder_name}/’) # ファイル数を表示(多い場合は省略) sub_indent = ‘│ ‘ * (level + 1) if len(files) > 3: print(f'{sub_indent}├── {files[0]}’) print(f'{sub_indent}├── …’) print(f'{sub_indent}└── ({len(files)}ファイル)’) else: for f in files: print(f'{sub_indent}├── {f}’)

実行結果:

データセットの構造: ──────────────────────────────────────── ├── car-damage-detection-1/ │ ├── train/ │ │ ├── images/ │ │ │ ├── img001.jpg │ │ │ ├── … │ │ │ └── (500ファイル) │ │ ├── labels/ │ │ │ ├── img001.txt │ │ │ ├── … │ │ │ └── (500ファイル) │ ├── valid/ │ │ ├── images/ │ │ │ └── (100ファイル) │ │ ├── labels/ │ │ │ └── (100ファイル) │ ├── test/ │ │ ├── images/ │ │ │ └── (50ファイル) │ │ ├── labels/ │ │ │ └── (50ファイル) │ └── data.yaml
# =================================================== # 3. データセットの統計情報を確認 # =================================================== import yaml # 各セットの画像数をカウント train_images = len(os.listdir(f”{dataset_path}/train/images”)) valid_images = len(os.listdir(f”{dataset_path}/valid/images”)) test_images = len(os.listdir(f”{dataset_path}/test/images”)) print(“データセットの統計:”) print(f”{‘─’ * 40}”) print(f”訓練データ: {train_images}枚”) print(f”検証データ: {valid_images}枚”) print(f”テストデータ: {test_images}枚”) print(f”合計: {train_images + valid_images + test_images}枚”) # data.yamlを読み込んでクラス情報を確認 # yaml.safe_load(): YAMLファイルをPython辞書に変換 with open(f”{dataset_path}/data.yaml”, ‘r’) as f: data_config = yaml.safe_load(f) print(f”\nクラス情報:”) print(f”{‘─’ * 40}”) print(f”クラス数: {data_config[‘nc’]}”) print(f”クラス名: {data_config[‘names’]}”)

実行結果:

データセットの統計: ──────────────────────────────────────── 訓練データ: 500枚 検証データ: 100枚 テストデータ: 50枚 合計: 650枚 クラス情報: ──────────────────────────────────────── クラス数: 4 クラス名: [‘dent’, ‘scratch’, ‘crack’, ‘glass_damage’]
🎯 YOLO形式のラベルファイル

各画像に対応する.txtファイルがあり、以下の形式で記述されています:

クラスID x_center y_center width height

例: 0 0.5 0.3 0.2 0.15
・クラスID: 0(dent)
・x_center: 0.5(画像幅の50%の位置)
・y_center: 0.3(画像高さの30%の位置)
・width: 0.2(画像幅の20%)
・height: 0.15(画像高さの15%)

すべての値は0〜1に正規化されています。

🏷️ 工程2: アノテーション(必要に応じて)

公開データセットを使用する場合、アノテーションは完了しています。自分で画像を収集した場合は、アノテーションが必要です。

2-1. アノテーションツールの選択

ツール 特徴 おすすめの場面
LabelImg ・無料、オープンソース
・ローカルで動作
・シンプルで軽量
少量のデータ(〜100枚)を個人で作業する場合
Roboflow ・Webブラウザで動作
・チーム共同作業可能
・データ拡張機能内蔵
チームで作業する場合、データ拡張も同時に行う場合
CVAT ・高機能
・動画アノテーション対応
・セルフホスト可能
大規模プロジェクト、動画データを扱う場合

2-2. LabelImgでアノテーション

【LabelImgの使い方】 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ インストールと起動 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ インストール: pip install labelImg 起動: labelImg ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 基本的な使用手順 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1. Open Dir(フォルダを開く) ・メニュー → Open Dir ・画像が入っているフォルダを選択 2. Change Save Dir(保存先を設定) ・メニュー → Change Save Dir ・ラベルファイルの保存先を選択 3. 保存形式をYOLOに変更 ・左側のメニューで「PascalVOC」をクリック ・「YOLO」に切り替え 4. アノテーション作業 ・「w」キーを押す → Bounding Box作成モード ・マウスでドラッグして損傷を囲む ・クラス名を入力または選択 ・「s」キーで保存 ・「d」キーで次の画像 5. クラスの事前定義 ・labelImgフォルダ内の「predefined_classes.txt」に記載 predefined_classes.txt: dent scratch crack glass_damage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ショートカットキー ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ w: Bounding Box作成 d: 次の画像 a: 前の画像 del: 選択したBoxを削除 Ctrl+s: 保存 Ctrl+d: ラベルを次の画像にコピー(同じ位置の物体用)
🔧 工程3: データ前処理と拡張

データが少ない場合は、データ拡張(Data Augmentation)で訓練データを増やすことができます。

3-1. データ拡張の種類

【物体検出で有効なデータ拡張】 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 幾何学的変換 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ・水平反転(Horizontal Flip) 左右を反転。車は左右対称なので有効 ・回転(Rotation) ±10〜20度の回転。撮影角度の変化をシミュレート ・スケール(Scale) 0.5〜1.5倍の拡大縮小。距離の変化をシミュレート ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 色調変換 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ・明るさ調整(Brightness) 照明条件の変化をシミュレート ・コントラスト調整(Contrast) 屋外/屋内の違いをシミュレート ・色相・彩度変更(Hue/Saturation) 車の色のバリエーションを増やす ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ノイズ・ぼかし ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ・ガウシアンぼかし(Gaussian Blur) カメラのピントずれをシミュレート ・ノイズ追加 低品質カメラをシミュレート

3-2. データ拡張の実装

# =================================================== # Albumentationsでデータ拡張 # =================================================== # !pip install albumentations import albumentations as A import cv2 import numpy as np from pathlib import Path import matplotlib.pyplot as plt # =================================================== # 1. データ拡張パイプラインの定義 # =================================================== # A.Compose(): 複数の変換を順番に適用 # bbox_params: Bounding Boxも一緒に変換するための設定 transform = A.Compose([ # 水平反転(50%の確率で適用) A.HorizontalFlip(p=0.5), # 明るさとコントラストの調整(30%の確率) A.RandomBrightnessContrast( brightness_limit=0.2, # 明るさの変化範囲 contrast_limit=0.2, # コントラストの変化範囲 p=0.3 ), # 色相・彩度・明度の調整(30%の確率) A.HueSaturationValue( hue_shift_limit=10, # 色相の変化範囲 sat_shift_limit=20, # 彩度の変化範囲 val_shift_limit=20, # 明度の変化範囲 p=0.3 ), # ガンマ補正(20%の確率) A.RandomGamma(gamma_limit=(80, 120), p=0.2), # 軽いぼかし(20%の確率) A.Blur(blur_limit=3, p=0.2), ], bbox_params=A.BboxParams( format=’yolo’, # YOLO形式のBounding Box label_fields=[‘class_labels’] # クラスラベルも一緒に変換 )) print(“データ拡張パイプラインを定義しました”)
# =================================================== # 2. 1枚の画像を拡張する関数 # =================================================== def augment_single_image(image_path, label_path, transform, num_augmentations=3): “”” 1枚の画像に対してデータ拡張を適用 Args: image_path: 元画像のパス label_path: ラベルファイルのパス transform: Albumentationsの変換パイプライン num_augmentations: 生成する拡張画像の数 Returns: augmented_images: 拡張された画像のリスト augmented_labels: 拡張されたラベルのリスト “”” # 画像を読み込む image = cv2.imread(str(image_path)) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # ラベルを読み込む(YOLO形式) bboxes = [] class_labels = [] with open(label_path, ‘r’) as f: for line in f: parts = line.strip().split() class_id = int(parts[0]) # YOLO形式: [x_center, y_center, width, height] bbox = [float(x) for x in parts[1:]] class_labels.append(class_id) bboxes.append(bbox) augmented_images = [] augmented_labels = [] # 指定回数だけ拡張を適用 for i in range(num_augmentations): # 変換を適用 transformed = transform( image=image, bboxes=bboxes, class_labels=class_labels ) augmented_images.append(transformed[‘image’]) augmented_labels.append({ ‘bboxes’: transformed[‘bboxes’], ‘class_labels’: transformed[‘class_labels’] }) return augmented_images, augmented_labels # 使用例 image_path = Path(f”{dataset_path}/train/images”) label_path = Path(f”{dataset_path}/train/labels”) # 最初の画像でテスト sample_image = list(image_path.glob(“*.jpg”))[0] sample_label = label_path / f”{sample_image.stem}.txt” aug_images, aug_labels = augment_single_image( sample_image, sample_label, transform, num_augmentations=4 ) print(f”元画像: {sample_image.name}”) print(f”拡張画像数: {len(aug_images)}”)

3-3. データセットの可視化

# =================================================== # アノテーション付き画像の可視化 # =================================================== import matplotlib.patches as patches def visualize_annotations(image_path, label_path, class_names, ax=None): “”” アノテーション付きの画像を表示 Args: image_path: 画像ファイルのパス label_path: ラベルファイルのパス class_names: クラス名のリスト ax: matplotlibのAxesオブジェクト(指定しない場合は新規作成) “”” # 画像を読み込む image = cv2.imread(str(image_path)) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) h, w = image.shape[:2] # Axesが指定されていない場合は新規作成 if ax is None: fig, ax = plt.subplots(1, figsize=(12, 8)) # 画像を表示 ax.imshow(image) # 色の定義(クラスごとに異なる色) colors = [‘red’, ‘green’, ‘blue’, ‘yellow’, ‘cyan’, ‘magenta’] # ラベルファイルが存在する場合のみ描画 if os.path.exists(label_path): with open(label_path, ‘r’) as f: for line in f: parts = line.strip().split() class_id = int(parts[0]) x_center, y_center, width, height = map(float, parts[1:]) # YOLO形式(正規化座標)からピクセル座標に変換 x_center *= w y_center *= h width *= w height *= h # 左上の座標を計算 x1 = x_center – width / 2 y1 = y_center – height / 2 # Bounding Boxを描画 color = colors[class_id % len(colors)] rect = patches.Rectangle( (x1, y1), width, height, linewidth=2, edgecolor=color, facecolor=’none’ ) ax.add_patch(rect) # クラス名を表示 ax.text( x1, y1 – 5, class_names[class_id], color=’white’, fontsize=10, fontweight=’bold’, bbox=dict(facecolor=color, alpha=0.7, edgecolor=’none’) ) ax.axis(‘off’) return ax # データセットのサンプルを可視化 class_names = [‘dent’, ‘scratch’, ‘crack’, ‘glass_damage’] fig, axes = plt.subplots(2, 3, figsize=(15, 10)) axes = axes.flatten() # 訓練データから6枚をサンプリング train_images = list(Path(f”{dataset_path}/train/images”).glob(“*.jpg”))[:6] for idx, img_path in enumerate(train_images): label_path = f”{dataset_path}/train/labels/{img_path.stem}.txt” visualize_annotations(img_path, label_path, class_names, axes[idx]) axes[idx].set_title(img_path.name, fontsize=10) plt.tight_layout() plt.savefig(‘dataset_samples.png’, dpi=150, bbox_inches=’tight’) plt.show() print(“データセットのサンプルを可視化しました”)
🎓 工程4: モデル訓練

YOLOv8を使って物体検出モデルを訓練します。事前学習済みのモデルをファインチューニングすることで、少ないデータでも高い精度を達成できます。

4-1. YOLOv8のモデルサイズ

モデル パラメータ数 速度(GPU) おすすめの用途
YOLOv8n 3.2M 最速 リアルタイム処理、モバイル、プロトタイプ
YOLOv8s 11.2M 高速 バランス型、中規模データ
YOLOv8m 25.9M 中速 精度重視、十分なGPUメモリがある場合
YOLOv8l 43.7M やや遅い 高精度が必要、大規模データ
YOLOv8x 68.2M 最も遅い 最高精度が必要、オフライン処理

4-2. 訓練の実行

# =================================================== # YOLOv8でモデルを訓練 # =================================================== # !pip install ultralytics from ultralytics import YOLO import torch # =================================================== # 1. デバイスの確認 # =================================================== # torch.cuda.is_available(): GPUが使用可能か確認 device = ‘cuda’ if torch.cuda.is_available() else ‘cpu’ print(f”使用デバイス: {device}”) if device == ‘cuda’: print(f”GPU名: {torch.cuda.get_device_name(0)}”) print(f”GPUメモリ: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB”)

実行結果:

使用デバイス: cuda GPU名: Tesla T4 GPUメモリ: 15.1 GB
# =================================================== # 2. 事前学習済みモデルのロード # =================================================== # YOLO(): YOLOv8モデルをロード # ‘yolov8n.pt’: nanoモデル(最軽量、最速) # 初回実行時は自動でダウンロードされる model = YOLO(‘yolov8n.pt’) print(“事前学習済みモデルをロードしました”) print(f”モデルタイプ: {model.type}”) # =================================================== # 3. data.yamlのパスを確認 # =================================================== # data.yamlには以下が定義されている: # – train: 訓練画像のパス # – val: 検証画像のパス # – nc: クラス数 # – names: クラス名のリスト data_yaml = f”{dataset_path}/data.yaml” print(f”\ndata.yamlのパス: {data_yaml}”) # 内容を確認 with open(data_yaml, ‘r’) as f: print(f”\ndata.yamlの内容:”) print(f.read())

実行結果:

事前学習済みモデルをロードしました モデルタイプ: detect data.yamlのパス: /content/car-damage-detection-1/data.yaml data.yamlの内容: train: ../train/images val: ../valid/images nc: 4 names: [‘dent’, ‘scratch’, ‘crack’, ‘glass_damage’]
# =================================================== # 4. 訓練の実行 # =================================================== # model.train(): モデルを訓練 # 多くのパラメータを指定可能 results = model.train( # === 必須パラメータ === data=data_yaml, # データセット設定ファイル epochs=50, # エポック数(データセット全体を何回学習するか) # === 画像・バッチ設定 === imgsz=640, # 入力画像サイズ(640×640にリサイズ) batch=16, # バッチサイズ(GPUメモリに応じて調整) # === 保存設定 === name=’car_damage_v1′, # 実験名(結果の保存先フォルダ名) save=True, # モデルを保存 plots=True, # 訓練曲線などのプロットを保存 # === 学習設定 === patience=10, # Early Stopping(10エポック改善なしで停止) device=device, # 使用デバイス(GPU/CPU) workers=4, # データローダーのワーカー数 # === データ拡張(訓練時に自動適用) === hsv_h=0.015, # 色相の変化 hsv_s=0.7, # 彩度の変化 hsv_v=0.4, # 明度の変化 degrees=10.0, # 回転角度 translate=0.1, # 平行移動 scale=0.5, # スケール変化 fliplr=0.5, # 水平反転の確率 mosaic=1.0, # Mosaic拡張(4枚の画像を組み合わせ) ) print(“\n訓練が完了しました!”)

実行結果(抜粋):

Ultralytics YOLOv8.0.0 🚀 Python-3.10.12 torch-2.0.1+cu118 CUDA:0 (Tesla T4, 15109MiB) Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size 1/50 2.4G 1.234 2.345 1.123 100 640 2/50 2.4G 1.123 2.123 1.034 100 640 3/50 2.4G 1.012 1.923 0.956 100 640 … 48/50 2.4G 0.345 0.456 0.234 100 640 49/50 2.4G 0.338 0.445 0.228 100 640 50/50 2.4G 0.332 0.438 0.223 100 640 Results saved to runs/detect/car_damage_v1/ 訓練が完了しました!
🎯 訓練ログの読み方

box_loss:Bounding Boxの位置の誤差(小さいほど良い)
cls_loss:クラス分類の誤差(小さいほど良い)
dfl_loss:Distribution Focal Loss(小さいほど良い)
Instances:そのバッチで処理したオブジェクト数

すべてのLossが徐々に下がっていけば、学習が進んでいる証拠です。

# =================================================== # 5. 訓練結果の確認 # =================================================== from PIL import Image import matplotlib.pyplot as plt # 結果のディレクトリ result_dir = ‘runs/detect/car_damage_v1′ # 保存されたファイルを確認 print(“保存されたファイル:”) print(“─” * 40) for item in os.listdir(result_dir): print(f” {item}”) # 訓練曲線を表示 results_img = Image.open(f'{result_dir}/results.png’) plt.figure(figsize=(16, 10)) plt.imshow(results_img) plt.axis(‘off’) plt.title(‘訓練結果’, fontsize=14) plt.tight_layout() plt.savefig(‘training_curves.png’, dpi=150, bbox_inches=’tight’) plt.show()

実行結果:

保存されたファイル: ──────────────────────────────────────── weights/ results.csv results.png confusion_matrix.png confusion_matrix_normalized.png F1_curve.png P_curve.png R_curve.png PR_curve.png labels.jpg labels_correlogram.jpg
📈 工程5: モデルの評価

訓練したモデルの性能を評価します。物体検出ではmAP(mean Average Precision)が主要な評価指標です。

5-1. 評価指標の理解

【物体検出の評価指標】 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Precision(精度) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 検出したもののうち、正しく検出できた割合 Precision = TP / (TP + FP) TP(True Positive): 正しく検出 FP(False Positive): 誤検出(存在しないのに検出) 例: 10個検出して、8個が正解 Precision = 8/10 = 0.8 (80%) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Recall(再現率) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 実際に存在するもののうち、検出できた割合 Recall = TP / (TP + FN) FN(False Negative): 見逃し(存在するのに検出できず) 例: 実際に12個存在、8個を検出 Recall = 8/12 = 0.67 (67%) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ AP(Average Precision) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Precision-Recall曲線の下の面積 1つのクラスに対する評価 AP@0.5: IoU閾値0.5での AP AP@0.5:0.95: IoU閾値0.5〜0.95での平均 AP ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ mAP(mean Average Precision) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 全クラスのAPの平均 物体検出の最も重要な指標 mAP = (AP_dent + AP_scratch + AP_crack + AP_glass) / 4 mAP@0.5: 一般的な評価指標(0.5以上で良い) mAP@0.5:0.95: より厳密な評価(COCOデータセットの標準) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ IoU(Intersection over Union) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 予測Boxと正解Boxの重なり具合 IoU = (予測 ∩ 正解) / (予測 ∪ 正解) IoU > 0.5: 正解とみなす(一般的) IoU > 0.75: より厳密な判定

5-2. テストセットでの評価

# =================================================== # テストセットで評価 # =================================================== # =================================================== # 1. 最良モデルをロード # =================================================== # 訓練中に最も良い性能を示したモデル(best.pt)をロード best_model = YOLO(‘runs/detect/car_damage_v1/weights/best.pt’) print(“最良モデルをロードしました”) # =================================================== # 2. 検証セットで評価 # =================================================== # model.val(): 評価を実行 val_results = best_model.val( data=data_yaml, # データセット設定 split=’val’, # 検証セットを使用 imgsz=640, # 画像サイズ batch=16, # バッチサイズ plots=True, # 評価プロットを保存 ) print(“\n検証セットの結果:”) print(“─” * 40) print(f”mAP@0.5: {val_results.box.map50:.4f}”) print(f”mAP@0.5:0.95: {val_results.box.map:.4f}”) print(f”Precision: {val_results.box.mp:.4f}”) print(f”Recall: {val_results.box.mr:.4f}”)

実行結果:

最良モデルをロードしました 検証セットの結果: ──────────────────────────────────────── mAP@0.5: 0.8234 mAP@0.5:0.95: 0.6123 Precision: 0.8567 Recall: 0.7890
# =================================================== # 3. クラス別の性能を確認 # =================================================== class_names = [‘dent’, ‘scratch’, ‘crack’, ‘glass_damage’] print(“\nクラス別の性能:”) print(“─” * 40) print(f”{‘クラス名’:<15} {'AP@0.5':<10} {'AP@0.5:0.95':<10}") print("─" * 40) for i, class_name in enumerate(class_names): ap50 = val_results.box.ap50[i] ap = val_results.box.ap[i] print(f"{class_name:<15} {ap50:<10.4f} {ap:<10.4f}")

実行結果:

クラス別の性能: ──────────────────────────────────────── クラス名 AP@0.5 AP@0.5:0.95 ──────────────────────────────────────── dent 0.8567 0.6234 scratch 0.7890 0.5678 crack 0.8123 0.5890 glass_damage 0.8356 0.6690

5-3. 予測と可視化

# =================================================== # テスト画像で予測と可視化 # =================================================== # =================================================== # 1. 単一画像で予測 # =================================================== # テスト画像のパス test_images = list(Path(f”{dataset_path}/test/images”).glob(“*.jpg”)) test_image = test_images[0] print(f”テスト画像: {test_image.name}”) # model.predict(): 予測を実行 results = best_model.predict( source=str(test_image), # 入力画像 imgsz=640, # 画像サイズ conf=0.25, # 信頼度閾値(これより低いものは無視) iou=0.45, # NMSのIoU閾値 save=True, # 結果を保存 save_txt=True, # ラベルも保存 ) # 結果を取得 result = results[0] # 検出結果を描画した画像を取得 result_img = result.plot() # 表示 plt.figure(figsize=(12, 8)) plt.imshow(cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB)) plt.title(f’予測結果: {test_image.name}’) plt.axis(‘off’) plt.savefig(‘prediction_result.png’, dpi=150, bbox_inches=’tight’) plt.show()
# =================================================== # 2. 検出結果の詳細を確認 # =================================================== print(“\n検出された損傷:”) print(“─” * 50) # result.boxes: 検出されたBounding Boxの情報 boxes = result.boxes if len(boxes) > 0: # xyxy: (x1, y1, x2, y2) 形式の座標 coords = boxes.xyxy.cpu().numpy() # conf: 信頼度スコア confidences = boxes.conf.cpu().numpy() # cls: クラスID class_ids = boxes.cls.cpu().numpy().astype(int) for i, (box, conf, cls_id) in enumerate(zip(coords, confidences, class_ids)): x1, y1, x2, y2 = box class_name = class_names[cls_id] print(f”損傷 {i+1}:”) print(f” 種類: {class_name}”) print(f” 信頼度: {conf:.3f}”) print(f” 位置: ({x1:.0f}, {y1:.0f}) – ({x2:.0f}, {y2:.0f})”) print() else: print(“損傷は検出されませんでした”)

実行結果:

検出された損傷: ────────────────────────────────────────────────── 損傷 1: 種類: dent 信頼度: 0.892 位置: (234, 156) – (412, 298) 損傷 2: 種類: scratch 信頼度: 0.756 位置: (89, 234) – (189, 267) 損傷 3: 種類: crack 信頼度: 0.634 位置: (456, 89) – (567, 123)
⚙️ 工程6: モデルのチューニング

精度を向上させるために、ハイパーパラメータを調整したり、異なるモデルサイズを試します。

6-1. チューニングの方針

【チューニングの主な方針】 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1. モデルサイズを変更 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ YOLOv8n → YOLOv8s → YOLOv8m 大きいモデルほど精度が上がるが、 訓練時間とメモリ消費が増える ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2. 画像サイズを変更 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 640 → 1280 高解像度にすると小さな損傷も検出しやすくなるが、 計算コストが増える ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3. 学習率を調整 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ デフォルト: lr0=0.01 学習が不安定な場合: 0.001 に下げる 学習が遅い場合: 0.02 に上げる ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4. データ拡張を調整 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 過学習している場合: データ拡張を強くする 未学習の場合: データ拡張を弱くする ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 5. エポック数を増やす ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ まだ学習が進んでいる場合は増やす Early Stoppingで自動停止するので多めに設定してOK

6-2. モデルの比較

# =================================================== # 複数モデルの性能比較 # =================================================== import pandas as pd # 比較するモデルを定義 models_to_compare = { ‘YOLOv8n (640)’: ‘runs/detect/car_damage_v1/weights/best.pt’, # 以下は追加で訓練した場合 # ‘YOLOv8s (640)’: ‘runs/detect/car_damage_v2/weights/best.pt’, # ‘YOLOv8n (1280)’: ‘runs/detect/car_damage_v3/weights/best.pt’, } # 各モデルを評価 results_list = [] for name, model_path in models_to_compare.items(): print(f”評価中: {name}”) model = YOLO(model_path) val_results = model.val(data=data_yaml, split=’test’, verbose=False) results_list.append({ ‘モデル’: name, ‘mAP@0.5’: val_results.box.map50, ‘mAP@0.5:0.95’: val_results.box.map, ‘Precision’: val_results.box.mp, ‘Recall’: val_results.box.mr, }) # データフレームに変換 df = pd.DataFrame(results_list) df = df.sort_values(‘mAP@0.5:0.95’, ascending=False) print(“\nモデル比較結果:”) print(“─” * 60) print(df.to_string(index=False))
🌐 工程7: デモアプリの作成(オプション)

訓練したモデルを使って、誰でも簡単に試せるWebアプリを作成します。

7-1. Gradioでデモアプリ

# =================================================== # Gradioでデモアプリを作成 # =================================================== # !pip install gradio import gradio as gr from ultralytics import YOLO import cv2 import numpy as np # =================================================== # 1. モデルとクラス名の準備 # =================================================== # モデルをロード model = YOLO(‘runs/detect/car_damage_v1/weights/best.pt’) # クラス名 class_names = [‘dent’, ‘scratch’, ‘crack’, ‘glass_damage’] class_names_jp = [‘へこみ’, ‘傷’, ‘ひび割れ’, ‘ガラス損傷’] # =================================================== # 2. 予測関数を定義 # =================================================== def predict_damage(image, conf_threshold): “”” 画像から損傷を検出する関数 Args: image: 入力画像(NumPy配列) conf_threshold: 信頼度閾値(0〜1) Returns: result_img: 検出結果を描画した画像 stats: 検出結果の統計情報(文字列) “”” # 予測を実行 results = model.predict( source=image, imgsz=640, conf=conf_threshold, iou=0.45, ) # 結果を描画 result_img = results[0].plot() # 検出された損傷の統計を集計 boxes = results[0].boxes class_ids = boxes.cls.cpu().numpy().astype(int) confidences = boxes.conf.cpu().numpy() # クラスごとにカウント stats_lines = [“【検出結果】\n”] if len(boxes) == 0: stats_lines.append(“損傷は検出されませんでした”) else: stats_lines.append(f”検出数: {len(boxes)}個\n”) for cls_id in range(len(class_names)): count = np.sum(class_ids == cls_id) if count > 0: stats_lines.append(f”・{class_names_jp[cls_id]}: {count}個”) stats_lines.append(“\n\n【詳細】”) for i, (cls_id, conf) in enumerate(zip(class_ids, confidences)): stats_lines.append(f”{i+1}. {class_names_jp[cls_id]} (信頼度: {conf:.1%})”) stats = “\n”.join(stats_lines) return result_img, stats # =================================================== # 3. Gradioインターフェースを作成 # =================================================== # gr.Interface(): Gradioアプリを作成 demo = gr.Interface( fn=predict_damage, # 実行する関数 inputs=[ gr.Image(label=”🚗 車の画像をアップロード”), gr.Slider( minimum=0.1, maximum=1.0, value=0.25, step=0.05, label=”信頼度閾値(低いほど多く検出、高いほど厳選)” ) ], outputs=[ gr.Image(label=”📊 検出結果”), gr.Textbox(label=”📝 検出された損傷”, lines=10) ], title=”🚗 自動車損傷検出システム”, description=””” 車の写真をアップロードすると、AIが損傷を自動検出します。 **検出可能な損傷:** – へこみ(Dent) – 傷(Scratch) – ひび割れ(Crack) – ガラスの損傷(Glass Damage) “””, examples=[ [f”{dataset_path}/test/images/test1.jpg”, 0.25], ] if os.path.exists(f”{dataset_path}/test/images”) else None, theme=”default”, ) # =================================================== # 4. アプリを起動 # =================================================== # demo.launch(): アプリを起動 # share=True: 公開URL(72時間有効)を生成 demo.launch( share=True, server_name=”0.0.0.0″, server_port=7860, ) # ブラウザで表示されたURLを開く

実行結果:

Running on local URL: http://localhost:7860 Running on public URL: https://abcd1234.gradio.live This share link expires in 72 hours.
🎯 Gradioアプリの特徴

share=True のメリット:
・他の人と共有できるURLが発行される
・72時間有効(無料版)
・スマホからもアクセス可能

本番環境にデプロイする場合:
・Hugging Face Spaces(無料)
・AWS / GCP / Azure
・自社サーバー

📝 プロジェクトのまとめ

✅ このプロジェクトで習得したスキル

1. データ収集:公開データセットの活用、Roboflowの使い方
2. アノテーション:LabelImg、Roboflowでのラベル付け
3. データ前処理:Albumentationsによるデータ拡張
4. モデル訓練:YOLOv8による物体検出モデルの訓練
5. 評価:mAP、Precision、Recall、Confusion Matrixの理解
6. チューニング:ハイパーパラメータの調整
7. デプロイ:Gradioでのデモアプリ作成

💡 実務でのポイント

データの質が最も重要:
・モデルの性能はデータの質に大きく依存
・アノテーションの一貫性を保つ
・様々な条件(照明、角度など)のデータを集める

反復的な改善:
・一度で完璧なモデルは作れない
・評価 → 分析 → 改善を繰り返す
・失敗例を分析して、データを追加

次のステップ:
・セグメンテーションの追加(損傷の形状も検出)
・損傷の重症度評価(軽度/中度/重度)
・修理コストの自動見積もり

次のSTEP 25では、独立プロジェクトに挑戦します!

📝

学習メモ

コンピュータビジョン(CV) - Step 24

📋 過去のメモ一覧
#artnasekai #学習メモ
LINE