📋 このプロジェクトで学ぶこと
- 実際のビジネス課題に対する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では、独立プロジェクトに挑戦します!