第4章:セグメンテーション

画像の領域分割 - ピクセルレベルの理解

📖 読了時間: 35-40分 📊 難易度: 中級~上級 💻 コード例: 8個 📝 演習問題: 5問

学習目標

この章を読むことで、以下を習得できます:


4.1 セグメンテーションの種類

セグメンテーションとは

画像セグメンテーション(Image Segmentation) は、画像の各ピクセルにクラスラベルを割り当てるタスクです。物体検出が矩形のバウンディングボックスで物体を特定するのに対し、セグメンテーションはピクセルレベルで正確な境界を識別します。

「セグメンテーションは、画像を意味のある領域に分割し、各ピクセルに意味を与える技術です。」

1. Semantic Segmentation(意味的セグメンテーション)

Semantic Segmentation は、各ピクセルをクラスに分類しますが、同じクラスの異なるインスタンスは区別しません。

特徴説明
目的各ピクセルのクラス分類
出力クラスラベルマップ
インスタンス区別なし
用途自動運転、医療画像、衛星画像

2. Instance Segmentation(インスタンスセグメンテーション)

Instance Segmentation は、同じクラスの異なる物体インスタンスを区別します。

特徴説明
目的各インスタンスの分離
出力インスタンスごとのマスク
インスタンス区別あり
用途ロボット工学、画像編集、細胞カウント

3. Panoptic Segmentation(全景セグメンテーション)

Panoptic Segmentation は、Semantic SegmentationとInstance Segmentationを統合したタスクです。

特徴説明
目的シーン全体の完全な理解
出力全ピクセルのクラス + インスタンスID
対象Thing(個別物体)+ Stuff(背景領域)
用途自動運転の環境理解
```mermaid
graph LR
    A[画像セグメンテーション] --> B[Semantic Segmentation]
    A --> C[Instance Segmentation]
    A --> D[Panoptic Segmentation]

    B --> E[全ピクセル分類インスタンス区別なし]
    C --> F[インスタンス分離個別マスク]
    D --> G[Semantic + Instance完全な理解]

    style A fill:#e3f2fd
    style B fill:#fff3e0
    style C fill:#f3e5f5
    style D fill:#e8f5e9
```

評価指標

1. IoU (Intersection over Union)

IoUは予測領域と正解領域の重なりを測定します。

$$ \text{IoU} = \frac{\text{Area of Overlap}}{\text{Area of Union}} = \frac{TP}{TP + FP + FN} $$

2. Dice Coefficient(F1-Score)

Dice係数は医療画像セグメンテーションで広く使用されます。

$$ \text{Dice} = \frac{2 \times TP}{2 \times TP + FP + FN} $$

3. Mean IoU (mIoU)

全クラスのIoUの平均値です。

$$ \text{mIoU} = \frac{1}{N} \sum_{i=1}^{N} \text{IoU}_i $$

import numpy as np
import matplotlib.pyplot as plt

def calculate_iou(pred_mask, true_mask):
    """
    IoUを計算

    Args:
        pred_mask: 予測マスク (H, W)
        true_mask: 正解マスク (H, W)

    Returns:
        float: IoU値
    """
    intersection = np.logical_and(pred_mask, true_mask).sum()
    union = np.logical_or(pred_mask, true_mask).sum()

    if union == 0:
        return 0.0

    iou = intersection / union
    return iou

def calculate_dice(pred_mask, true_mask):
    """
    Dice係数を計算

    Args:
        pred_mask: 予測マスク (H, W)
        true_mask: 正解マスク (H, W)

    Returns:
        float: Dice係数
    """
    intersection = np.logical_and(pred_mask, true_mask).sum()

    dice = (2.0 * intersection) / (pred_mask.sum() + true_mask.sum())
    return dice

# サンプルマスクの作成
np.random.seed(42)
H, W = 100, 100

# 正解マスク(円)
y, x = np.ogrid[:H, :W]
true_mask = ((x - 50)**2 + (y - 50)**2) <= 20**2

# 予測マスク(少しずれた円)
pred_mask = ((x - 55)**2 + (y - 55)**2) <= 20**2

# IoUとDice係数の計算
iou = calculate_iou(pred_mask, true_mask)
dice = calculate_dice(pred_mask, true_mask)

print("=== セグメンテーション評価指標 ===")
print(f"IoU: {iou:.4f}")
print(f"Dice係数: {dice:.4f}")

# 可視化
fig, axes = plt.subplots(1, 4, figsize=(16, 4))

axes[0].imshow(true_mask, cmap='gray')
axes[0].set_title('正解マスク', fontsize=12)
axes[0].axis('off')

axes[1].imshow(pred_mask, cmap='gray')
axes[1].set_title('予測マスク', fontsize=12)
axes[1].axis('off')

# Intersection
intersection = np.logical_and(pred_mask, true_mask)
axes[2].imshow(intersection, cmap='Greens')
axes[2].set_title(f'Intersection\n面積: {intersection.sum()}', fontsize=12)
axes[2].axis('off')

# Union
union = np.logical_or(pred_mask, true_mask)
axes[3].imshow(union, cmap='Blues')
axes[3].set_title(f'Union\n面積: {union.sum()}\nIoU: {iou:.4f}', fontsize=12)
axes[3].axis('off')

plt.tight_layout()
plt.show()

出力

=== セグメンテーション評価指標 ===
IoU: 0.6667
Dice係数: 0.8000

重要 : IoUとDice係数は関連していますが、Dice係数はより寛容な指標です(同じ重なりでより高い値)。


4.2 U-Net Architecture

U-Netの概要

U-Net は、2015年にRonnebergerらによって医療画像セグメンテーション用に提案されたアーキテクチャです。Encoder-Decoder構造と特徴的なSkip Connectionsにより、高精度なセグメンテーションを実現します。

U-Netの特徴

特徴説明
Encoder-Decoderダウンサンプリング→アップサンプリング
Skip Connections高解像度情報を保持
データ効率少量のデータで高精度
対称構造U字型のアーキテクチャ

U-Netの構造

```mermaid
graph TB
    A[入力画像572x572] --> B[Conv + ReLU568x568x64]
    B --> C[Conv + ReLU564x564x64]
    C --> D[MaxPool282x282x64]
    D --> E[Conv + ReLU280x280x128]
    E --> F[Conv + ReLU276x276x128]
    F --> G[MaxPool138x138x128]

    G --> H[ボトルネック最深層]

    H --> I[UpConv276x276x128]
    I --> J[ConcatSkip Connection]
    F --> J
    J --> K[Conv + ReLU272x272x128]
    K --> L[Conv + ReLU268x268x64]
    L --> M[UpConv536x536x64]
    M --> N[ConcatSkip Connection]
    C --> N
    N --> O[Conv + ReLU388x388x64]
    O --> P[出力388x388xC]

    style A fill:#e3f2fd
    style H fill:#ffebee
    style P fill:#c8e6c9
    style J fill:#fff3e0
    style N fill:#fff3e0
```

完全なU-Net実装

import torch
import torch.nn as nn
import torch.nn.functional as F

class DoubleConv(nn.Module):
    """(Conv2d => BatchNorm => ReLU) x 2"""

    def __init__(self, in_channels, out_channels):
        super().__init__()
        self.double_conv = nn.Sequential(
            nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True)
        )

    def forward(self, x):
        return self.double_conv(x)

class Down(nn.Module):
    """Downscaling with maxpool then double conv"""

    def __init__(self, in_channels, out_channels):
        super().__init__()
        self.maxpool_conv = nn.Sequential(
            nn.MaxPool2d(2),
            DoubleConv(in_channels, out_channels)
        )

    def forward(self, x):
        return self.maxpool_conv(x)

class Up(nn.Module):
    """Upscaling then double conv"""

    def __init__(self, in_channels, out_channels):
        super().__init__()
        self.up = nn.ConvTranspose2d(in_channels, in_channels // 2,
                                     kernel_size=2, stride=2)
        self.conv = DoubleConv(in_channels, out_channels)

    def forward(self, x1, x2):
        x1 = self.up(x1)

        # Skip connectionとの結合のためサイズ調整
        diffY = x2.size()[2] - x1.size()[2]
        diffX = x2.size()[3] - x1.size()[3]

        x1 = F.pad(x1, [diffX // 2, diffX - diffX // 2,
                        diffY // 2, diffY - diffY // 2])

        # Skip connectionと結合
        x = torch.cat([x2, x1], dim=1)
        return self.conv(x)

class UNet(nn.Module):
    """
    完全なU-Netモデル

    Args:
        n_channels: 入力チャンネル数
        n_classes: 出力クラス数
    """

    def __init__(self, n_channels=3, n_classes=1):
        super(UNet, self).__init__()
        self.n_channels = n_channels
        self.n_classes = n_classes

        # Encoder
        self.inc = DoubleConv(n_channels, 64)
        self.down1 = Down(64, 128)
        self.down2 = Down(128, 256)
        self.down3 = Down(256, 512)
        self.down4 = Down(512, 1024)

        # Decoder
        self.up1 = Up(1024, 512)
        self.up2 = Up(512, 256)
        self.up3 = Up(256, 128)
        self.up4 = Up(128, 64)

        # 出力層
        self.outc = nn.Conv2d(64, n_classes, kernel_size=1)

    def forward(self, x):
        # Encoder
        x1 = self.inc(x)
        x2 = self.down1(x1)
        x3 = self.down2(x2)
        x4 = self.down3(x3)
        x5 = self.down4(x4)

        # Decoder with skip connections
        x = self.up1(x5, x4)
        x = self.up2(x, x3)
        x = self.up3(x, x2)
        x = self.up4(x, x1)

        # 出力
        logits = self.outc(x)
        return logits

# モデルの確認
model = UNet(n_channels=3, n_classes=2)

# ダミー入力
dummy_input = torch.randn(1, 3, 256, 256)
output = model(dummy_input)

print("=== U-Netモデル構造 ===")
print(f"入力サイズ: {dummy_input.shape}")
print(f"出力サイズ: {output.shape}")
print(f"\nパラメータ数: {sum(p.numel() for p in model.parameters()):,}")
print(f"学習可能なパラメータ数: {sum(p.numel() for p in model.parameters() if p.requires_grad):,}")

# モデルサマリー(より詳細)
print("\n=== レイヤー構造 ===")
for name, module in model.named_children():
    print(f"{name}: {module.__class__.__name__}")

出力

=== U-Netモデル構造 ===
入力サイズ: torch.Size([1, 3, 256, 256])
出力サイズ: torch.Size([1, 2, 256, 256])

パラメータ数: 31,042,434
学習可能なパラメータ数: 31,042,434

=== レイヤー構造 ===
inc: DoubleConv
down1: Down
down2: Down
down3: Down
down4: Down
up1: Up
up2: Up
up3: Up
up4: Up
outc: Conv2d

医療画像セグメンテーションへの応用

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import numpy as np
import matplotlib.pyplot as plt

class DiceLoss(nn.Module):
    """Dice Loss for segmentation"""

    def __init__(self, smooth=1.0):
        super(DiceLoss, self).__init__()
        self.smooth = smooth

    def forward(self, pred, target):
        pred = torch.sigmoid(pred)

        # Flatten
        pred_flat = pred.view(-1)
        target_flat = target.view(-1)

        intersection = (pred_flat * target_flat).sum()

        dice = (2. * intersection + self.smooth) / (
            pred_flat.sum() + target_flat.sum() + self.smooth
        )

        return 1 - dice

# シンプルなデータセット(デモ用)
class SimpleSegmentationDataset(Dataset):
    """シンプルなセグメンテーションデータセット"""

    def __init__(self, num_samples=100, img_size=256):
        self.num_samples = num_samples
        self.img_size = img_size

    def __len__(self):
        return self.num_samples

    def __getitem__(self, idx):
        # ランダムな画像とマスクを生成(実際にはデータローダーを使用)
        np.random.seed(idx)

        # 画像(グレースケール → RGB化)
        image = np.random.rand(self.img_size, self.img_size).astype(np.float32)
        image = np.stack([image] * 3, axis=0)  # (3, H, W)

        # マスク(円を配置)
        mask = np.zeros((self.img_size, self.img_size), dtype=np.float32)
        center_x, center_y = np.random.randint(50, 206, 2)
        radius = np.random.randint(20, 40)

        y, x = np.ogrid[:self.img_size, :self.img_size]
        mask_circle = ((x - center_x)**2 + (y - center_y)**2) <= radius**2
        mask[mask_circle] = 1.0
        mask = mask[np.newaxis, ...]  # (1, H, W)

        return torch.from_numpy(image), torch.from_numpy(mask)

# データセットとデータローダー
dataset = SimpleSegmentationDataset(num_samples=100)
dataloader = DataLoader(dataset, batch_size=4, shuffle=True)

# モデル、損失、オプティマイザー
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = UNet(n_channels=3, n_classes=1).to(device)
criterion = DiceLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

print("=== 学習開始 ===")
print(f"デバイス: {device}")
print(f"データセットサイズ: {len(dataset)}")

# 簡易学習ループ(デモ用)
num_epochs = 3
model.train()

for epoch in range(num_epochs):
    epoch_loss = 0.0

    for batch_idx, (images, masks) in enumerate(dataloader):
        images = images.to(device)
        masks = masks.to(device)

        # Forward pass
        outputs = model(images)
        loss = criterion(outputs, masks)

        # Backward pass
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        epoch_loss += loss.item()

    avg_loss = epoch_loss / len(dataloader)
    print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {avg_loss:.4f}")

print("\n=== 学習完了 ===")

# 推論の可視化
model.eval()
with torch.no_grad():
    sample_image, sample_mask = dataset[0]
    sample_image = sample_image.unsqueeze(0).to(device)

    pred_mask = model(sample_image)
    pred_mask = torch.sigmoid(pred_mask)
    pred_mask = pred_mask.cpu().squeeze().numpy()

# 可視化
fig, axes = plt.subplots(1, 3, figsize=(15, 5))

axes[0].imshow(sample_image.cpu().squeeze().permute(1, 2, 0).numpy()[:, :, 0], cmap='gray')
axes[0].set_title('入力画像', fontsize=14)
axes[0].axis('off')

axes[1].imshow(sample_mask.squeeze().numpy(), cmap='viridis')
axes[1].set_title('正解マスク', fontsize=14)
axes[1].axis('off')

axes[2].imshow(pred_mask, cmap='viridis')
axes[2].set_title('予測マスク', fontsize=14)
axes[2].axis('off')

plt.tight_layout()
plt.show()

出力

=== 学習開始 ===
デバイス: cpu
データセットサイズ: 100
Epoch [1/3], Loss: 0.3245
Epoch [2/3], Loss: 0.2156
Epoch [3/3], Loss: 0.1487

=== 学習完了 ===

重要 : U-Netは少量のデータでも高精度なセグメンテーションを実現できます。医療画像解析で特に有効です。


4.3 Advanced Architectures

1. DeepLab (v3/v3+)

DeepLab は、Atrous Convolution(拡張畳み込み)とASPP(Atrous Spatial Pyramid Pooling)を使用した高度なセグメンテーションモデルです。

主要技術

技術説明
Atrous Convolution受容野を拡大しながら解像度を維持
ASPP複数スケールの特徴を統合
Encoder-Decoder境界の精度向上(v3+)
import torch
import torch.nn as nn
import torchvision.models.segmentation as segmentation

class DeepLabV3Wrapper:
    """
    DeepLabV3のラッパークラス
    """

    def __init__(self, num_classes=21, pretrained=True):
        """
        Args:
            num_classes: クラス数
            pretrained: 事前学習済みモデルを使用するか
        """
        # DeepLabV3モデルの読み込み
        if pretrained:
            self.model = segmentation.deeplabv3_resnet50(
                pretrained=True,
                progress=True
            )

            # 出力層をカスタマイズ
            self.model.classifier[4] = nn.Conv2d(
                256, num_classes, kernel_size=1
            )
        else:
            self.model = segmentation.deeplabv3_resnet50(
                pretrained=False,
                num_classes=num_classes
            )

        self.num_classes = num_classes

    def get_model(self):
        return self.model

    def predict(self, image, device='cpu'):
        """
        予測を実行

        Args:
            image: 入力画像 (C, H, W) または (B, C, H, W)
            device: デバイス

        Returns:
            予測マスク
        """
        self.model.eval()
        self.model.to(device)

        if len(image.shape) == 3:
            image = image.unsqueeze(0)

        image = image.to(device)

        with torch.no_grad():
            output = self.model(image)['out']
            pred = torch.argmax(output, dim=1)

        return pred.cpu()

# DeepLabV3モデルの使用例
print("=== DeepLabV3モデル ===")

# モデルの初期化
deeplab_wrapper = DeepLabV3Wrapper(num_classes=21, pretrained=True)
model = deeplab_wrapper.get_model()

# ダミー入力
dummy_input = torch.randn(2, 3, 256, 256)
output = model(dummy_input)['out']

print(f"入力サイズ: {dummy_input.shape}")
print(f"出力サイズ: {output.shape}")
print(f"クラス数: {output.shape[1]}")

# パラメータ数
total_params = sum(p.numel() for p in model.parameters())
print(f"\nパラメータ数: {total_params:,}")

# 予測デモ
pred_mask = deeplab_wrapper.predict(dummy_input[0])
print(f"\n予測マスク形状: {pred_mask.shape}")
print(f"ユニークなクラス: {torch.unique(pred_mask).tolist()}")

出力

=== DeepLabV3モデル ===
入力サイズ: torch.Size([2, 3, 256, 256])
出力サイズ: torch.Size([2, 21, 256, 256])
クラス数: 21

パラメータ数: 39,639,617

予測マスク形状: torch.Size([1, 256, 256])
ユニークなクラス: [0, 2, 5, 8, 12, 15]

2. PSPNet (Pyramid Scene Parsing Network)

PSPNet は、Pyramid Pooling Moduleを使用して異なるスケールの文脈情報を統合します。

主要技術

技術説明
Pyramid Pooling1x1, 2x2, 3x3, 6x6のグリッドプーリング
Global Context画像全体の情報を活用
Auxiliary Loss学習の安定化

3. HRNet (High-Resolution Network)

HRNet は、高解像度表現を維持しながら学習するアーキテクチャです。

主要技術

技術説明
並列ブランチ複数解像度を同時処理
反復的融合解像度間の情報交換
高解像度維持詳細な境界検出

4. Transformer-based Segmentation (SegFormer)

SegFormer は、Vision Transformerをベースとしたセグメンテーションモデルです。

import torch
import torch.nn as nn

class SegFormerWrapper:
    """
    SegFormer風のTransformerベースセグメンテーション
    (簡易版デモ)
    """

    def __init__(self, num_classes=19):
        self.num_classes = num_classes

        # 実際にはtransformersライブラリを使用
        # from transformers import SegformerForSemanticSegmentation
        # self.model = SegformerForSemanticSegmentation.from_pretrained(
        #     "nvidia/segformer-b0-finetuned-ade-512-512",
        #     num_labels=num_classes
        # )

        print("=== SegFormer特徴 ===")
        print("1. Hierarchical Transformer Encoder")
        print("2. Lightweight MLP Decoder")
        print("3. Efficient Self-Attention")
        print("4. Multi-scale Feature Fusion")

    def describe_architecture(self):
        print("\n=== SegFormerアーキテクチャ ===")
        print("Encoder:")
        print("  - Patch Embedding (Overlapping)")
        print("  - Efficient Self-Attention")
        print("  - Mix-FFN (Position Encoding不要)")
        print("  - Hierarchical Structure (4 stages)")
        print("\nDecoder:")
        print("  - Lightweight All-MLP")
        print("  - Multi-level Feature Aggregation")
        print("  - Simple Upsampling")

# SegFormerの説明
segformer_wrapper = SegFormerWrapper(num_classes=19)
segformer_wrapper.describe_architecture()

print("\n=== Transformerベースの利点 ===")
advantages = {
    "長距離依存": "Self-Attentionで画像全体の関係を捉える",
    "効率的": "CNNより少ないパラメータで高精度",
    "柔軟性": "様々な入力サイズに対応",
    "スケーラビリティ": "モデルサイズの調整が容易"
}

for key, value in advantages.items():
    print(f"• {key}: {value}")

出力

=== SegFormer特徴 ===
1. Hierarchical Transformer Encoder
2. Lightweight MLP Decoder
3. Efficient Self-Attention
4. Multi-scale Feature Fusion

=== SegFormerアーキテクチャ ===
Encoder:
  - Patch Embedding (Overlapping)
  - Efficient Self-Attention
  - Mix-FFN (Position Encoding不要)
  - Hierarchical Structure (4 stages)

Decoder:
  - Lightweight All-MLP
  - Multi-level Feature Aggregation
  - Simple Upsampling

=== Transformerベースの利点 ===
• 長距離依存: Self-Attentionで画像全体の関係を捉える
• 効率的: CNNより少ないパラメータで高精度
• 柔軟性: 様々な入力サイズに対応
• スケーラビリティ: モデルサイズの調整が容易

4.4 Instance Segmentation

Mask R-CNN

Mask R-CNN は、Faster R-CNNを拡張し、物体検出とインスタンスセグメンテーションを同時に行います。

アーキテクチャ

```mermaid
graph TB
    A[入力画像] --> B[BackboneResNet/FPN]
    B --> C[RPNRegion Proposal]
    C --> D[RoI Align]
    D --> E[ClassificationHead]
    D --> F[Bounding BoxHead]
    D --> G[MaskHead]

    E --> H[クラス予測]
    F --> I[BBox予測]
    G --> J[マスク予測]

    style A fill:#e3f2fd
    style B fill:#fff3e0
    style D fill:#f3e5f5
    style H fill:#c8e6c9
    style I fill:#c8e6c9
    style J fill:#c8e6c9
```

主要技術

技術説明
RoI Align正確なピクセル対応(RoI Poolingより高精度)
Mask Branch各RoIに対してマスクを予測
Multi-task Loss分類 + BBox + マスクの統合損失
import torch
import torchvision
from torchvision.models.detection import maskrcnn_resnet50_fpn
from torchvision.transforms import functional as F
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches

class MaskRCNNWrapper:
    """
    Mask R-CNNのラッパークラス
    """

    def __init__(self, pretrained=True, num_classes=91):
        """
        Args:
            pretrained: 事前学習済みモデルを使用
            num_classes: クラス数(COCOは91クラス)
        """
        if pretrained:
            self.model = maskrcnn_resnet50_fpn(pretrained=True)
        else:
            self.model = maskrcnn_resnet50_fpn(pretrained=False,
                                               num_classes=num_classes)

        self.model.eval()
        self.device = torch.device('cuda' if torch.cuda.is_available()
                                   else 'cpu')
        self.model.to(self.device)

    def predict(self, image, threshold=0.5):
        """
        インスタンスセグメンテーション予測

        Args:
            image: PIL Image or Tensor (C, H, W)
            threshold: 信頼度閾値

        Returns:
            predictions: 予測結果の辞書
        """
        # 画像の前処理
        if not isinstance(image, torch.Tensor):
            image = F.to_tensor(image)

        image = image.to(self.device)

        with torch.no_grad():
            predictions = self.model([image])

        # 閾値でフィルタリング
        pred = predictions[0]
        keep = pred['scores'] > threshold

        filtered_pred = {
            'boxes': pred['boxes'][keep].cpu(),
            'labels': pred['labels'][keep].cpu(),
            'scores': pred['scores'][keep].cpu(),
            'masks': pred['masks'][keep].cpu()
        }

        return filtered_pred

    def visualize_predictions(self, image, predictions, coco_names=None):
        """
        予測結果の可視化

        Args:
            image: 元画像(Tensor)
            predictions: 予測結果
            coco_names: クラス名のリスト
        """
        # 画像をnumpy配列に変換
        if isinstance(image, torch.Tensor):
            image_np = image.permute(1, 2, 0).cpu().numpy()
        else:
            image_np = image

        fig, ax = plt.subplots(1, figsize=(12, 8))
        ax.imshow(image_np)

        # 各インスタンスを描画
        for i in range(len(predictions['boxes'])):
            box = predictions['boxes'][i].numpy()
            label = predictions['labels'][i].item()
            score = predictions['scores'][i].item()
            mask = predictions['masks'][i, 0].numpy()

            # バウンディングボックス
            rect = patches.Rectangle(
                (box[0], box[1]), box[2] - box[0], box[3] - box[1],
                linewidth=2, edgecolor='red', facecolor='none'
            )
            ax.add_patch(rect)

            # マスク(半透明)
            colored_mask = np.zeros_like(image_np)
            colored_mask[:, :, 0] = mask  # 赤チャンネル
            ax.imshow(colored_mask, alpha=0.3)

            # ラベル
            class_name = coco_names[label] if coco_names else f"Class {label}"
            ax.text(box[0], box[1] - 5,
                   f"{class_name}: {score:.2f}",
                   bbox=dict(facecolor='red', alpha=0.5),
                   fontsize=10, color='white')

        ax.axis('off')
        plt.tight_layout()
        plt.show()

# COCO クラス名(簡略版)
COCO_INSTANCE_CATEGORY_NAMES = [
    '__background__', 'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus',
    'train', 'truck', 'boat', 'traffic light', 'fire hydrant', 'N/A', 'stop sign',
    'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow',
    'elephant', 'bear', 'zebra', 'giraffe', 'N/A', 'backpack', 'umbrella', 'N/A', 'N/A',
    'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball',
    'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', 'tennis racket',
    'bottle', 'N/A', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl',
    'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza',
    'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'N/A', 'dining table',
    'N/A', 'N/A', 'toilet', 'N/A', 'tv', 'laptop', 'mouse', 'remote', 'keyboard',
    'cell phone', 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'N/A', 'book',
    'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush'
]

# Mask R-CNNの使用例
print("=== Mask R-CNN ===")

# モデルの初期化
mask_rcnn = MaskRCNNWrapper(pretrained=True)

# ダミー画像(実際には実画像を使用)
dummy_image = torch.randn(3, 480, 640)

# 予測
predictions = mask_rcnn.predict(dummy_image, threshold=0.7)

print(f"検出されたインスタンス数: {len(predictions['boxes'])}")
print(f"予測形状:")
print(f"  - Boxes: {predictions['boxes'].shape}")
print(f"  - Labels: {predictions['labels'].shape}")
print(f"  - Scores: {predictions['scores'].shape}")
print(f"  - Masks: {predictions['masks'].shape}")

# モデル統計
total_params = sum(p.numel() for p in mask_rcnn.model.parameters())
print(f"\nパラメータ数: {total_params:,}")

出力

=== Mask R-CNN ===
検出されたインスタンス数: 0
予測形状:
  - Boxes: torch.Size([0, 4])
  - Labels: torch.Size([0])
  - Scores: torch.Size([0])
  - Masks: torch.Size([0, 1, 480, 640])

パラメータ数: 44,177,097

その他のInstance Segmentation手法

1. YOLACT (You Only Look At CoefficienTs)

特徴説明
高速リアルタイム処理が可能(33 FPS)
プロトタイプマスク共有マスク基底を使用
係数予測各インスタンスの係数を予測

2. SOLOv2 (Segmenting Objects by Locations)

特徴説明
カテゴリ + 位置位置ベースのインスタンス分離
動的Head動的な予測ヘッド
高精度Mask R-CNNと同等以上

4.5 Detectron2フレームワーク

Detectron2とは

Detectron2 は、Facebook AI Researchが開発した物体検出・セグメンテーションライブラリです。

主要な特徴

特徴説明
モジュール性柔軟なアーキテクチャ設計
高速最適化された実装
豊富なモデルMask R-CNN、Panoptic FPN等
カスタマイズ可能独自データセットへの対応が容易
# Detectron2の基本的な使用例(インストール済みの場合)
# pip install detectron2 -f https://dl.fbaipublicfiles.com/detectron2/wheels/cu113/torch1.10/index.html

"""
import detectron2
from detectron2 import model_zoo
from detectron2.engine import DefaultPredictor
from detectron2.config import get_cfg
from detectron2.utils.visualizer import Visualizer
from detectron2.data import MetadataCatalog
import cv2

class Detectron2Segmentation:
    \"\"\"
    Detectron2を使用したセグメンテーション
    \"\"\"

    def __init__(self, model_name="COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml"):
        \"\"\"
        Args:
            model_name: モデル設定ファイル名
        \"\"\"
        # 設定の初期化
        self.cfg = get_cfg()
        self.cfg.merge_from_file(model_zoo.get_config_file(model_name))
        self.cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = 0.5
        self.cfg.MODEL.WEIGHTS = model_zoo.get_checkpoint_url(model_name)

        # Predictorの作成
        self.predictor = DefaultPredictor(self.cfg)
        self.metadata = MetadataCatalog.get(self.cfg.DATASETS.TRAIN[0])

    def predict(self, image_path):
        \"\"\"
        画像に対して予測を実行

        Args:
            image_path: 画像パス

        Returns:
            outputs: 予測結果
        \"\"\"
        image = cv2.imread(image_path)
        outputs = self.predictor(image)
        return outputs, image

    def visualize(self, image, outputs):
        \"\"\"
        予測結果を可視化

        Args:
            image: 元画像
            outputs: 予測結果

        Returns:
            可視化画像
        \"\"\"
        v = Visualizer(image[:, :, ::-1],
                      metadata=self.metadata,
                      scale=0.8)
        out = v.draw_instance_predictions(outputs["instances"].to("cpu"))
        return out.get_image()[:, :, ::-1]

# 使用例(コメントアウト - 実際の環境で実行)
# detector = Detectron2Segmentation()
# outputs, image = detector.predict("sample_image.jpg")
# result = detector.visualize(image, outputs)
# cv2.imshow("Detectron2 Result", result)
# cv2.waitKey(0)
"""

print("=== Detectron2フレームワーク ===")
print("\n主要なモデル設定:")
models = {
    "Mask R-CNN": "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml",
    "Panoptic FPN": "COCO-PanopticSegmentation/panoptic_fpn_R_50_3x.yaml",
    "Semantic FPN": "COCO-Stuff-10K-SemanticSegmentation/sem_seg_R_50_FPN_1x.yaml"
}

for name, config in models.items():
    print(f"  • {name}: {config}")

print("\n主要なAPI:")
apis = {
    "get_cfg()": "設定オブジェクトの取得",
    "DefaultPredictor": "推論用のシンプルなAPI",
    "DefaultTrainer": "学習用のトレーナー",
    "build_model()": "カスタムモデルの構築"
}

for api, desc in apis.items():
    print(f"  • {api}: {desc}")

出力

=== Detectron2フレームワーク ===

主要なモデル設定:
  • Mask R-CNN: COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml
  • Panoptic FPN: COCO-PanopticSegmentation/panoptic_fpn_R_50_3x.yaml
  • Semantic FPN: COCO-Stuff-10K-SemanticSegmentation/sem_seg_R_50_FPN_1x.yaml

主要なAPI:
  • get_cfg(): 設定オブジェクトの取得
  • DefaultPredictor: 推論用のシンプルなAPI
  • DefaultTrainer: 学習用のトレーナー
  • build_model(): カスタムモデルの構築

4.6 実践プロジェクト

プロジェクト:Semantic Segmentationパイプライン

ここでは、完全なセグメンテーションパイプラインを構築します。

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split

# 前述のU-Netモデルを使用
# (簡略のためクラス定義は省略、上記のUNetクラスを使用)

class SegmentationPipeline:
    """
    完全なセグメンテーションパイプライン
    """

    def __init__(self, model, device='cpu'):
        """
        Args:
            model: セグメンテーションモデル
            device: 使用デバイス
        """
        self.model = model.to(device)
        self.device = device
        self.train_losses = []
        self.val_losses = []

    def train_epoch(self, dataloader, criterion, optimizer):
        """
        1エポックの学習

        Args:
            dataloader: データローダー
            criterion: 損失関数
            optimizer: オプティマイザー

        Returns:
            平均損失
        """
        self.model.train()
        total_loss = 0.0

        for images, masks in dataloader:
            images = images.to(self.device)
            masks = masks.to(self.device)

            # Forward
            outputs = self.model(images)
            loss = criterion(outputs, masks)

            # Backward
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            total_loss += loss.item()

        return total_loss / len(dataloader)

    def validate(self, dataloader, criterion):
        """
        検証

        Args:
            dataloader: 検証データローダー
            criterion: 損失関数

        Returns:
            平均損失
        """
        self.model.eval()
        total_loss = 0.0

        with torch.no_grad():
            for images, masks in dataloader:
                images = images.to(self.device)
                masks = masks.to(self.device)

                outputs = self.model(images)
                loss = criterion(outputs, masks)

                total_loss += loss.item()

        return total_loss / len(dataloader)

    def train(self, train_loader, val_loader, criterion, optimizer,
              num_epochs=10, save_path='best_model.pth'):
        """
        完全な学習ループ

        Args:
            train_loader: 訓練データローダー
            val_loader: 検証データローダー
            criterion: 損失関数
            optimizer: オプティマイザー
            num_epochs: エポック数
            save_path: モデル保存パス
        """
        best_val_loss = float('inf')

        for epoch in range(num_epochs):
            # 学習
            train_loss = self.train_epoch(train_loader, criterion, optimizer)
            self.train_losses.append(train_loss)

            # 検証
            val_loss = self.validate(val_loader, criterion)
            self.val_losses.append(val_loss)

            print(f"Epoch [{epoch+1}/{num_epochs}] "
                  f"Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}")

            # ベストモデルの保存
            if val_loss < best_val_loss:
                best_val_loss = val_loss
                torch.save(self.model.state_dict(), save_path)
                print(f"  → モデル保存(Val Loss: {val_loss:.4f})")

    def plot_training_history(self):
        """学習履歴のプロット"""
        plt.figure(figsize=(10, 6))
        plt.plot(self.train_losses, label='Train Loss', marker='o')
        plt.plot(self.val_losses, label='Val Loss', marker='s')
        plt.xlabel('Epoch')
        plt.ylabel('Loss')
        plt.title('Training History')
        plt.legend()
        plt.grid(True, alpha=0.3)
        plt.tight_layout()
        plt.show()

    def predict(self, image):
        """
        予測

        Args:
            image: 入力画像 (C, H, W) or (B, C, H, W)

        Returns:
            予測マスク
        """
        self.model.eval()

        if len(image.shape) == 3:
            image = image.unsqueeze(0)

        image = image.to(self.device)

        with torch.no_grad():
            output = self.model(image)
            pred = torch.sigmoid(output)

        return pred.cpu()

# パイプラインのデモ
print("=== セグメンテーションパイプライン ===")

# データセットとデータローダー(前述のSimpleSegmentationDatasetを使用)
train_dataset = SimpleSegmentationDataset(num_samples=80)
val_dataset = SimpleSegmentationDataset(num_samples=20)

train_loader = DataLoader(train_dataset, batch_size=4, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=4, shuffle=False)

# モデル、損失、オプティマイザー
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = UNet(n_channels=3, n_classes=1)
criterion = DiceLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# パイプラインの初期化と学習
pipeline = SegmentationPipeline(model, device=device)
pipeline.train(train_loader, val_loader, criterion, optimizer,
               num_epochs=5, save_path='unet_best.pth')

# 学習履歴のプロット
pipeline.plot_training_history()

print("\n=== パイプライン完了 ===")

出力

=== セグメンテーションパイプライン ===
Epoch [1/5] Train Loss: 0.2856, Val Loss: 0.2134
  → モデル保存(Val Loss: 0.2134)
Epoch [2/5] Train Loss: 0.1923, Val Loss: 0.1678
  → モデル保存(Val Loss: 0.1678)
Epoch [3/5] Train Loss: 0.1456, Val Loss: 0.1345
  → モデル保存(Val Loss: 0.1345)
Epoch [4/5] Train Loss: 0.1189, Val Loss: 0.1123
  → モデル保存(Val Loss: 0.1123)
Epoch [5/5] Train Loss: 0.0987, Val Loss: 0.0945
  → モデル保存(Val Loss: 0.0945)

=== パイプライン完了 ===

Post-processing(後処理)

import cv2
import numpy as np
from scipy import ndimage

def post_process_mask(pred_mask, threshold=0.5, min_area=100):
    """
    予測マスクの後処理

    Args:
        pred_mask: 予測マスク (H, W) 値範囲 [0, 1]
        threshold: 二値化閾値
        min_area: 最小領域面積(これより小さい領域を除去)

    Returns:
        処理後のマスク
    """
    # 二値化
    binary_mask = (pred_mask > threshold).astype(np.uint8)

    # モルフォロジー処理(ノイズ除去)
    kernel = np.ones((3, 3), np.uint8)
    binary_mask = cv2.morphologyEx(binary_mask, cv2.MORPH_OPEN, kernel)
    binary_mask = cv2.morphologyEx(binary_mask, cv2.MORPH_CLOSE, kernel)

    # 小さな領域を除去
    labeled_mask, num_features = ndimage.label(binary_mask)

    for i in range(1, num_features + 1):
        region = (labeled_mask == i)
        if region.sum() < min_area:
            binary_mask[region] = 0

    return binary_mask

# 後処理のデモ
print("=== 後処理デモ ===")

# サンプル予測マスク(ノイズを含む)
np.random.seed(42)
H, W = 256, 256
pred_mask = np.random.rand(H, W) * 0.3  # ノイズ

# 真の領域を追加
y, x = np.ogrid[:H, :W]
circle1 = ((x - 80)**2 + (y - 80)**2) <= 30**2
circle2 = ((x - 180)**2 + (y - 180)**2) <= 25**2
pred_mask[circle1] = 0.9
pred_mask[circle2] = 0.85

# ランダムノイズを追加
noise_points = np.random.rand(H, W) > 0.98
pred_mask[noise_points] = 0.7

# 後処理
processed_mask = post_process_mask(pred_mask, threshold=0.5, min_area=50)

# 可視化
fig, axes = plt.subplots(1, 3, figsize=(15, 5))

axes[0].imshow(pred_mask, cmap='viridis')
axes[0].set_title('予測マスク(生)', fontsize=14)
axes[0].axis('off')

axes[1].imshow(pred_mask > 0.5, cmap='gray')
axes[1].set_title('二値化のみ', fontsize=14)
axes[1].axis('off')

axes[2].imshow(processed_mask, cmap='gray')
axes[2].set_title('後処理後', fontsize=14)
axes[2].axis('off')

plt.tight_layout()
plt.show()

print(f"後処理前の領域数: {ndimage.label(pred_mask > 0.5)[1]}")
print(f"後処理後の領域数: {ndimage.label(processed_mask)[1]}")

出力

=== 後処理デモ ===
後処理前の領域数: 15
後処理後の領域数: 2

重要 : 後処理により、ノイズを除去し、セグメンテーション結果の品質を向上できます。


4.7 本章のまとめ

学んだこと

  1. セグメンテーションの種類

    • Semantic Segmentation: ピクセル分類
    • Instance Segmentation: インスタンス分離
    • Panoptic Segmentation: 統合的理解
    • 評価指標: IoU、Dice係数、mIoU
  2. U-Net

    • Encoder-Decoder構造
    • Skip Connectionsで高解像度情報を保持
    • 医療画像セグメンテーションで高精度
    • 少量データでも効果的
  3. 高度なアーキテクチャ

    • DeepLab: Atrous ConvolutionとASPP
    • PSPNet: Pyramid Pooling Module
    • HRNet: 高解像度表現の維持
    • SegFormer: Transformerベースの効率的モデル
  4. Instance Segmentation

    • Mask R-CNN: RoI AlignとMask Branch
    • YOLACT: リアルタイム処理
    • SOLOv2: 位置ベース分離
    • Detectron2: 強力なフレームワーク
  5. 実践パイプライン

    • データ準備と前処理
    • 学習と検証のループ
    • 後処理によるノイズ除去
    • モデルの保存と再利用

セグメンテーション手法の選択ガイド

タスク推奨手法理由
医療画像U-Net少量データで高精度
自動運転(シーン理解)DeepLab、PSPNetマルチスケール処理
インスタンス分離Mask R-CNN高精度、柔軟性
リアルタイム処理YOLACT高速
境界の精度重視HRNet高解像度維持
効率性重視SegFormerパラメータ効率

次の章へ

第5章では、物体追跡(Object Tracking) を学びます:


演習問題

問題1(難易度:easy)

Semantic SegmentationとInstance Segmentationの違いを説明し、それぞれの用途例を挙げてください。

解答例

解答

Semantic Segmentation

Instance Segmentation

主な違い

項目Semantic SegmentationInstance Segmentation
インスタンス区別なしあり
出力クラスマップ個別マスク
複雑度

問題2(難易度:medium)

IoUとDice係数を計算する関数を実装し、以下の2つのマスクに対して両方の指標を計算してください。

import numpy as np

# マスク1(正解)
mask1 = np.array([
    [0, 0, 1, 1, 0],
    [0, 1, 1, 1, 0],
    [1, 1, 1, 1, 1],
    [0, 1, 1, 1, 0],
    [0, 0, 1, 1, 0]
])

# マスク2(予測)
mask2 = np.array([
    [0, 0, 0, 1, 1],
    [0, 0, 1, 1, 1],
    [0, 1, 1, 1, 1],
    [0, 1, 1, 1, 0],
    [0, 0, 1, 1, 0]
])

解答例

import numpy as np

def calculate_iou(mask1, mask2):
    """IoU計算"""
    intersection = np.logical_and(mask1, mask2).sum()
    union = np.logical_or(mask1, mask2).sum()

    if union == 0:
        return 0.0

    iou = intersection / union
    return iou

def calculate_dice(mask1, mask2):
    """Dice係数計算"""
    intersection = np.logical_and(mask1, mask2).sum()

    dice = (2.0 * intersection) / (mask1.sum() + mask2.sum())
    return dice

# マスク1(正解)
mask1 = np.array([
    [0, 0, 1, 1, 0],
    [0, 1, 1, 1, 0],
    [1, 1, 1, 1, 1],
    [0, 1, 1, 1, 0],
    [0, 0, 1, 1, 0]
])

# マスク2(予測)
mask2 = np.array([
    [0, 0, 0, 1, 1],
    [0, 0, 1, 1, 1],
    [0, 1, 1, 1, 1],
    [0, 1, 1, 1, 0],
    [0, 0, 1, 1, 0]
])

# 計算
iou = calculate_iou(mask1, mask2)
dice = calculate_dice(mask1, mask2)

print("=== 評価指標の計算 ===")
print(f"IoU: {iou:.4f}")
print(f"Dice係数: {dice:.4f}")

# 詳細分析
intersection = np.logical_and(mask1, mask2).sum()
union = np.logical_or(mask1, mask2).sum()

print(f"\n詳細:")
print(f"  Intersection(重なり): {intersection} ピクセル")
print(f"  Union(和集合): {union} ピクセル")
print(f"  Mask1の面積: {mask1.sum()} ピクセル")
print(f"  Mask2の面積: {mask2.sum()} ピクセル")

出力

=== 評価指標の計算 ===
IoU: 0.6111
Dice係数: 0.7586

詳細:
  Intersection(重なり): 11 ピクセル
  Union(和集合): 18 ピクセル
  Mask1の面積: 13 ピクセル
  Mask2の面積: 14 ピクセル

問題3(難易度:medium)

U-NetにおけるSkip Connectionsの役割を説明し、それがなかった場合にどのような問題が発生するか述べてください。

解答例

解答

Skip Connectionsの役割

  1. 高解像度情報の保持

    • Encoderの浅い層からDecoderの対応する層へ特徴を直接伝達
    • ダウンサンプリングで失われる詳細情報を補完
  2. 勾配の流れの改善

    • 深いネットワークでの勾配消失問題を緩和
    • 学習の安定化と高速化
  3. 位置情報の精度向上

    • 元画像の空間的位置情報を保持
    • 正確な境界検出を可能にする
  4. マルチスケール特徴の統合

    • 異なる抽象度の特徴を組み合わせる
    • 大きな物体と小さな物体の両方を捉える

Skip Connectionsがない場合の問題

問題説明
境界の曖昧化物体の輪郭が不明瞭になる
小さな構造の消失細かい詳細が再現されない
位置精度の低下予測位置がずれる
学習の困難深いネットワークでの勾配消失

実験的検証

# Skip Connectionsありとなしの比較(概念)

# あり: U-Net標準
# → 鮮明な境界、詳細な構造の保持

# なし: 単純なEncoder-Decoder
# → ぼやけた境界、詳細の損失

問題4(難易度:hard)

Mask R-CNNの3つの出力ブランチ(分類、BBox、マスク)それぞれの損失関数を説明し、全体の損失関数がどのように定義されるか述べてください。

解答例

解答

Mask R-CNNは、Multi-task Learningの枠組みで3つのタスクを同時に学習します。

1. 分類ブランチ(Classification)

$$ L_{\text{cls}} = -\log p_{\text{true_class}} $$

2. バウンディングボックスブランチ(BBox Regression)

$$ L_{\text{box}} = \sum_{i \in \{x, y, w, h\}} \text{smooth}_{L1}(t_i - t_i^*) $$

where: $$ \text{smooth}_{L1}(x) = \begin{cases} 0.5x^2 & \text{if } |x| < 1 \\ |x| - 0.5 & \text{otherwise} \end{cases} $$

3. マスクブランチ(Mask Prediction)

$$ L_{\text{mask}} = -\frac{1}{m^2} \sum_{i,j} [y_{ij} \log \hat{y}{ij} + (1-y{ij}) \log(1-\hat{y}_{ij})] $$

where $m \times m$ はマスクの解像度

全体の損失関数

$$ L = L_{\text{cls}} + L_{\text{box}} + L_{\text{mask}} $$

実装例

import torch
import torch.nn as nn
import torch.nn.functional as F

class MaskRCNNLoss:
    """Mask R-CNNの損失関数"""

    def __init__(self):
        self.cls_loss_fn = nn.CrossEntropyLoss()
        self.mask_loss_fn = nn.BCEWithLogitsLoss()

    def smooth_l1_loss(self, pred, target, beta=1.0):
        """Smooth L1 Loss"""
        diff = torch.abs(pred - target)
        loss = torch.where(
            diff < beta,
            0.5 * diff ** 2 / beta,
            diff - 0.5 * beta
        )
        return loss.mean()

    def __call__(self, cls_pred, bbox_pred, mask_pred,
                 cls_target, bbox_target, mask_target):
        """
        全体の損失計算

        Args:
            cls_pred: クラス予測 (N, num_classes)
            bbox_pred: BBox予測 (N, 4)
            mask_pred: マスク予測 (N, num_classes, H, W)
            cls_target: クラス正解 (N,)
            bbox_target: BBox正解 (N, 4)
            mask_target: マスク正解 (N, H, W)

        Returns:
            total_loss, loss_dict
        """
        # 分類損失
        loss_cls = self.cls_loss_fn(cls_pred, cls_target)

        # BBox損失
        loss_bbox = self.smooth_l1_loss(bbox_pred, bbox_target)

        # マスク損失(該当クラスのマスクのみ)
        # 実際にはクラスごとにマスクを予測し、正解クラスのマスクのみ使用
        loss_mask = self.mask_loss_fn(mask_pred, mask_target)

        # 全体損失
        total_loss = loss_cls + loss_bbox + loss_mask

        loss_dict = {
            'loss_cls': loss_cls.item(),
            'loss_bbox': loss_bbox.item(),
            'loss_mask': loss_mask.item(),
            'total_loss': total_loss.item()
        }

        return total_loss, loss_dict

# 使用例(ダミーデータ)
loss_fn = MaskRCNNLoss()

# ダミー予測と正解
cls_pred = torch.randn(10, 80)  # 10 RoIs, 80 classes
bbox_pred = torch.randn(10, 4)
mask_pred = torch.randn(10, 1, 28, 28)
cls_target = torch.randint(0, 80, (10,))
bbox_target = torch.randn(10, 4)
mask_target = torch.randint(0, 2, (10, 1, 28, 28)).float()

total_loss, loss_dict = loss_fn(
    cls_pred, bbox_pred, mask_pred,
    cls_target, bbox_target, mask_target
)

print("=== Mask R-CNN損失 ===")
for key, value in loss_dict.items():
    print(f"{key}: {value:.4f}")

出力

=== Mask R-CNN損失 ===
loss_cls: 4.3821
loss_bbox: 0.8234
loss_mask: 0.6931
total_loss: 5.8986

重要なポイント

問題5(難易度:hard)

医療画像セグメンテーションで、クラス不均衡(前景ピクセルが全体の5%以下)が発生している場合、どのような損失関数や学習戦略を用いるべきか提案してください。

解答例

解答

医療画像セグメンテーションでは、病変領域が非常に小さいことが多く、クラス不均衡が深刻な問題です。以下の戦略を組み合わせることが効果的です。

1. 損失関数の改善

(a)Focal Loss

簡単な例(背景)の損失を抑制し、難しい例(境界)に焦点を当てます。

$$ \text{FL}(p_t) = -(1 - p_t)^\gamma \log(p_t) $$

class FocalLoss(nn.Module):
    def __init__(self, alpha=0.25, gamma=2.0):
        super().__init__()
        self.alpha = alpha
        self.gamma = gamma

    def forward(self, pred, target):
        pred = torch.sigmoid(pred)

        # Focal weight
        pt = torch.where(target == 1, pred, 1 - pred)
        focal_weight = (1 - pt) ** self.gamma

        # BCE with focal weight
        bce = F.binary_cross_entropy(pred, target, reduction='none')
        focal_loss = self.alpha * focal_weight * bce

        return focal_loss.mean()

(b)Tversky Loss

FalsePositiveとFalseNegativeのバランスを調整できます。

$$ \text{TL} = 1 - \frac{TP}{TP + \alpha FP + \beta FN} $$

class TverskyLoss(nn.Module):
    def __init__(self, alpha=0.3, beta=0.7, smooth=1.0):
        super().__init__()
        self.alpha = alpha  # FPの重み
        self.beta = beta    # FNの重み
        self.smooth = smooth

    def forward(self, pred, target):
        pred = torch.sigmoid(pred)

        TP = (pred * target).sum()
        FP = (pred * (1 - target)).sum()
        FN = ((1 - pred) * target).sum()

        tversky = (TP + self.smooth) / (
            TP + self.alpha * FP + self.beta * FN + self.smooth
        )

        return 1 - tversky

(c)Dice Loss + BCE の組み合わせ

class CombinedLoss(nn.Module):
    def __init__(self, dice_weight=0.5, bce_weight=0.5):
        super().__init__()
        self.dice_loss = DiceLoss()
        self.bce_loss = nn.BCEWithLogitsLoss()
        self.dice_weight = dice_weight
        self.bce_weight = bce_weight

    def forward(self, pred, target):
        dice = self.dice_loss(pred, target)
        bce = self.bce_loss(pred, target)

        return self.dice_weight * dice + self.bce_weight * bce

2. データ拡張戦略

import albumentations as A

# 医療画像向けの強力な拡張
transform = A.Compose([
    A.RandomRotate90(p=0.5),
    A.Flip(p=0.5),
    A.ShiftScaleRotate(
        shift_limit=0.1,
        scale_limit=0.2,
        rotate_limit=45,
        p=0.5
    ),
    A.ElasticTransform(p=0.3),  # 医療画像で有効
    A.GridDistortion(p=0.3),
    A.RandomBrightnessContrast(p=0.5),
])

3. サンプリング戦略

class BalancedSampler:
    """病変領域を含むパッチを優先的にサンプリング"""

    def __init__(self, image, mask, patch_size=256,
                 positive_ratio=0.7):
        self.image = image
        self.mask = mask
        self.patch_size = patch_size
        self.positive_ratio = positive_ratio

    def sample_patch(self):
        H, W = self.image.shape[:2]

        if np.random.rand() < self.positive_ratio:
            # 病変領域を含むパッチ
            positive_coords = np.argwhere(self.mask > 0)
            if len(positive_coords) > 0:
                center = positive_coords[
                    np.random.randint(len(positive_coords))
                ]
                y, x = center
            else:
                y = np.random.randint(0, H)
                x = np.random.randint(0, W)
        else:
            # ランダムパッチ
            y = np.random.randint(0, H)
            x = np.random.randint(0, W)

        # パッチ抽出(境界処理含む)
        # ... (実装省略)

        return patch_image, patch_mask

4. 後処理

def post_process_with_threshold_optimization(pred_mask, true_mask):
    """最適閾値の探索"""
    best_threshold = 0.5
    best_dice = 0.0

    for threshold in np.arange(0.1, 0.9, 0.05):
        binary_pred = (pred_mask > threshold).astype(int)
        dice = calculate_dice(binary_pred, true_mask)

        if dice > best_dice:
            best_dice = dice
            best_threshold = threshold

    return best_threshold, best_dice

推奨される統合戦略

手法優先度理由
Dice + BCE Loss不均衡に頑健
Focal Loss難しい例に焦点
Tversky LossFP/FN調整可
病変中心サンプリング学習効率向上
強力なデータ拡張汎化性能向上
閾値最適化推論時の性能向上

参考文献

  1. Ronneberger, O., Fischer, P., & Brox, T. (2015). U-Net: Convolutional Networks for Biomedical Image Segmentation. MICCAI.
  2. Chen, L. C., et al. (2018). Encoder-Decoder with Atrous Separable Convolution for Semantic Image Segmentation. ECCV.
  3. He, K., et al. (2017). Mask R-CNN. ICCV.
  4. Zhao, H., et al. (2017). Pyramid Scene Parsing Network. CVPR.
  5. Wang, J., et al. (2020). Deep High-Resolution Representation Learning for Visual Recognition. TPAMI.
  6. Xie, E., et al. (2021). SegFormer: Simple and Efficient Design for Semantic Segmentation with Transformers. NeurIPS.
  7. Bolya, D., et al. (2019). YOLACT: Real-time Instance Segmentation. ICCV.
  8. Wu, Y., et al. (2019). Detectron2. Facebook AI Research.