Transformers documentation
インスタンスセグメンテーション
インスタンスセグメンテーション
インスタンスセグメンテーションは、画像内のオブジェクトを検出し、各オブジェクトをピクセルレベルでセグメンテーションするコンピュータビジョンのタスクです。オブジェクト検出(バウンディングボックスを出力する)とは異なり、インスタンスセグメンテーションは検出された各オブジェクトに対して正確なマスクを生成するため、オブジェクトが重なっていても個々のインスタンスを区別できます。
このガイドでは、次のことを学びます。
- Hugging Face Hub からインスタンスセグメンテーションのデータセットを読み込みます。
- TransformersのTrainerを使用して、TransformerベースのインスタンスセグメンテーションモデルであるRF-DETR-Segをファインチューニングします。
- 平均IoUでモデルを評価します。
- 推論を実行し、予測結果を可視化します。
satellite-building-segmentation データセットを使用します。このデータセットには、建物のインスタンスマスクでアノテーションされた約9,600枚の衛星画像が含まれています。
このタスクと互換性のあるすべてのアーキテクチャとチェックポイントを確認するには、タスクページを参照することをお勧めします。
開始する前に、必要なライブラリをすべてインストールしてください:
pip install -Uq "transformers>=5.9" datasets torchvisionモデルをコミュニティと共有することをお勧めします。トレーニングが完了したら、Hugging Face アカウントにログインして、Hub にアップロードしてください:
>>> from huggingface_hub import notebook_login
>>> notebook_login()データセットを読み込む
satellite-building-segmentation データセットは Hugging Face Datasets のネイティブ形式で提供されているため、load_dataset を使用して直接読み込むことができます。各行には以下が含まれます:
image: 衛星画像 (PIL)image_id: 画像の一意な識別子width/height: 画像の寸法objects:id、category、bbox、area、segmentation(ポリゴンの座標)、iscrowdを持つ、インスタンスごとのアノテーションの dict
>>> from datasets import load_dataset
>>> MODEL_ID = "Roboflow/rf-detr-seg-medium"
>>> DATASET_ID = "merve/satellite-building-segmentation"
>>> ds = load_dataset(DATASET_ID)
>>> train_ds = ds["train"]
>>> valid_ds = ds["validation"]
>>> print(f"Train: {len(train_ds)} images, Valid: {len(valid_ds)} images")
Train: 6764 images, Valid: 1934 images
単一の例を確認します。各レコードにはimage、image_id、およびインスタンスごとのアノテーションを含むobjects辞書が含まれます。各インスタンスには[x, y, width, height]形式のbboxと、ポリゴンの座標を持つsegmentationフィールドがあります。
>>> sample = train_ds[0]
>>> print(f"Image ID: {sample['image_id']}")
Image ID: 0
>>> print(f"Image size: {sample['image'].size}")
Image size: (512, 512)
>>> print(f"Number of instances: {len(sample['objects']['id'])}")
Number of instances: 7
>>> print(f"\nObjects keys: {list(sample['objects'].keys())}")
<BLANKLINE>
Objects keys: ['id', 'area', 'bbox', 'segmentation', 'category', 'iscrowd']
>>> print(f"First bbox: {sample['objects']['bbox'][0]}")
First bbox: [80.0, 0.0, 51.0, 51.5]
>>> print(f"First category: {sample['objects']['category'][0]}")
First category: 0
正解マスク付きの例を可視化します:
>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> from PIL import Image, ImageDraw
>>> sample = train_ds[0]
>>> image = sample["image"].convert("RGB")
>>> fig, axes = plt.subplots(1, 2, figsize=(14, 6))
>>> _ = axes[0].imshow(image)
>>> _ = axes[0].set_title("Original image")
>>> _ = axes[0].axis("off")
>>> overlay = image.copy()
>>> draw = ImageDraw.Draw(overlay, "RGBA")
>>> objects = sample["objects"]
>>> for seg in objects["segmentation"]:
... for poly in seg:
... coords = list(zip(poly[0::2], poly[1::2]))
... color = tuple(np.random.randint(50, 255, 3)) + (100,)
... draw.polygon(coords, fill=color, outline="red")
>>> _ = axes[1].imshow(overlay)
>>> _ = axes[1].set_title(f"Ground truth ({len(objects['id'])} buildings)")
>>> _ = axes[1].axis("off")
>>> plt.tight_layout()
>>> plt.show()
モデルと画像プロセッサを読み込みます
AutoImageProcessor と AutoModelForInstanceSegmentation を使用して、RF-DETR-Seg モデルを読み込みます。モデルを読み込む際は、id2label と label2id のマッピングを渡すことで、単一の “building” クラス用の分類ヘッドを設定します。事前学習済みモデルは COCO(91 クラス)で学習されているため、ignore_mismatched_sizes=True を設定し、出力数を正しくした状態で分類ヘッドを再初期化します。
画像プロセッサはすべての前処理を担当します。アスペクト比を維持したまま画像をリサイズし、ImageNet の統計量で正規化し、統一サイズにパディングします。また、インスタンスセグメンテーションにとって極めて重要ですが、ポリゴンのアノテーションをバイナリマスクに変換し、そのマスクをリサイズし、モデルが期待する [0, 1] 範囲の [cx, cy, w, h] 形式にバウンディングボックスを正規化します。
>>> from transformers import AutoImageProcessor, AutoModelForInstanceSegmentation
>>> id2label = {0: "building"}
>>> label2id = {"building": 0}
>>> image_processor = AutoImageProcessor.from_pretrained(MODEL_ID)
>>> model = AutoModelForInstanceSegmentation.from_pretrained(
... MODEL_ID,
... id2label=id2label,
... label2id=label2id,
... ignore_mismatched_sizes=True,
... )データを前処理する
モデルをファインチューニングするには、モデルが期待する形式に合わせてデータを前処理する必要があります。画像とCOCO形式のアノテーションをreturn_segmentation_masks=True付きでRfDetrImageProcessorに渡すと、すべての重い処理を代行してくれます:
- ポリゴンセグメンテーションをバイナリマスクにラスタライズします
- 画像、バウンディングボックス、マスクをモデルの入力サイズにリサイズします
- ImageNet の平均値/標準偏差でピクセル値を正規化します
- バウンディングボックスを
[x, y, w, h]から正規化された[cx, cy, w, h]に変換します - 画像を統一サイズにパディングし、
pixel_maskを作成します
この transform は、データセットの objects カラムから、画像プロセッサが期待する COCO 形式のアノテーション辞書を再構築します。
with_transform を使用すると、前処理を遅延評価(サンプルが読み込まれた時点でオンザフライに適用)として実行でき、処理済みのデータセット全体をメモリに保持する必要がなくなります。
>>> from functools import partial
>>> from typing import Any
>>> def transform_batch(examples: dict[str, Any], image_processor) -> dict[str, Any]:
... """Convert HF dataset rows into COCO-style dicts and pass to the processor."""
... images, targets = [], []
... for image, img_id, objects in zip(
... examples["image"], examples["image_id"], examples["objects"]
... ):
... if not objects["id"]:
... continue
... annotations = [
... {
... "id": ann_id,
... "image_id": img_id,
... "category_id": cat,
... "bbox": bbox,
... "area": area,
... "segmentation": seg,
... "iscrowd": crowd,
... }
... for ann_id, cat, bbox, area, seg, crowd in zip(
... objects["id"],
... objects["category"],
... objects["bbox"],
... objects["area"],
... objects["segmentation"],
... objects["iscrowd"],
... )
... ]
... images.append(image.convert("RGB"))
... targets.append({"image_id": img_id, "annotations": annotations})
...
... if not images:
... return {}
... return image_processor(
... images=images, annotations=targets, return_segmentation_masks=True, return_tensors="pt"
... )
>>> transform = partial(transform_batch, image_processor=image_processor)
>>> train_ds = train_ds.shuffle(seed=42).with_transform(transform)
>>> valid_ds = valid_ds.with_transform(transform)前処理済みの例を検証します。この例には、pixel_values(正規化された画像テンソル)、pixel_mask(実ピクセルとパディングの区別を示す)、labels(class_labels、正規化された中心形式のboxes、および二値のmasksを含む辞書)が含まれます。
>>> example = train_ds[0]
>>> print(f"pixel_values shape: {example['pixel_values'].shape}")
pixel_values shape: torch.Size([3, 432, 432])
>>> print(f"pixel_mask shape: {example['pixel_mask'].shape}")
pixel_mask shape: torch.Size([432, 432])
>>> print(f"labels keys: {list(example['labels'].keys())}")
labels keys: ['size', 'image_id', 'class_labels', 'boxes', 'area', 'iscrowd', 'orig_size', 'masks']
>>> print(f" class_labels: {example['labels']['class_labels']}")
class_labels: tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0])
>>> print(f" boxes shape: {example['labels']['boxes'].shape}")
boxes shape: torch.Size([26, 4])
>>> print(f" masks shape: {example['labels']['masks'].shape}")
masks shape: torch.Size([26, 432, 432])
データコラテーター
各画像のオブジェクトインスタンス数が異なるため、ラベルは可変長の辞書となり、単一のテンソルにスタックできません。pixel_values と pixel_mask は通常どおりスタックし、labels は画像ごとの辞書のリストとして保持する、カスタムの collate 関数を定義します。
>>> import torch
>>> def collate_fn(batch: list[dict[str, Any]]) -> dict[str, Any]:
... out = {
... "pixel_values": torch.stack([x["pixel_values"] for x in batch]),
... "labels": [x["labels"] for x in batch],
... }
... if "pixel_mask" in batch[0]:
... out["pixel_mask"] = torch.stack([x["pixel_mask"] for x in batch])
... return out評価指標を定義する
学習中のセグメンテーション品質を追跡するために、各評価エポックでunionベースの平均IoUを計算します。各画像について:
- 予測されたすべてのインスタンスマスクを単一の二値「buildings」マップにマージします(各クエリのマスクロジスティックを
sigmoid > 0.5で二値化) - すべての正解インスタンスマスクを単一のバイナリマップにマージします
- 2つのマップ間のIntersection-over-Unionを計算します
RF-DETR-Seg のクエリマスクはクラススコアの閾値でゲートされていないため、このメトリクスはクラススコアを無視し、クエリごとのマスクロジットを直接結合します。一致しないクエリはほぼ空のマスクを生成するため、結合結果を汚染しません。
これにより、モデルが建物をどれだけカバーしているかを示す画像ごとの指標が得られ、これは検証セット全体で平均化されます。
インスタンスセグメンテーションのベンチマーク(COCO など)では、マスクの平均平均精度(mAP)が報告されることが一般的です。mAP は、IoU の閾値の範囲にわたって各予測インスタンスマスクを正解データと比較してスコアを付けるため、個々のオブジェクトを正しく分離できた場合に高い評価を与えます。ここで使用されている Union ベースの平均 IoU は、よりシンプルで高速な代替指標です。インスタンスごとの品質ではなく、全体のピクセルカバレッジを測定するため、トレーニング中の進捗を追跡する際に便利です。標準的なインスタンス認識評価を行う場合は、代わりにマスク mAP を計算してください。例えば、
torchmetricsのMeanAveragePrecision(iou_type="segm")を使用できます。
サブクラス化せず、Trainer に compute_metrics 関数として渡します。eval_do_concat_batches=False(下の TrainingArguments で設定)の場合、標準的な評価パスで生成される予測とラベルは、バッチごとの出力のリストとして compute_metrics に渡されるため、メトリクスがこれらの予測を再利用でき、検証セットに対する2回目のフォワードパスは不要になります。モデル出力のタプルでは、インデックス 3 に pred_masks が格納されています:
>>> import torch.nn.functional as F
>>> @torch.no_grad()
... def compute_mean_iou(pred_masks, gt_masks, target_size):
... """Union-based IoU: merge all instances per image, then compute IoU."""
... pred_masks = F.interpolate(pred_masks[None], size=target_size, mode="bilinear", align_corners=False)[0]
... pred_union = (pred_masks.sigmoid() > 0.5).any(dim=0)
...
... gt_union = gt_masks.any(dim=0).bool()
...
... intersection = (pred_union & gt_union).sum().float()
... union = (pred_union | gt_union).sum().float()
... return (intersection / union.clamp(min=1)).item()
>>> @torch.no_grad()
... def compute_metrics(evaluation_results):
... predictions, targets = evaluation_results.predictions, evaluation_results.label_ids
... ious = []
... for pred_batch, target_batch in zip(predictions, targets):
... batch_masks = pred_batch[3]
... for i, gt_label in enumerate(target_batch):
... gt_masks = torch.as_tensor(gt_label["masks"])
... if gt_masks.numel() == 0:
... continue
... target_size = gt_masks.shape[-2:]
... pred_masks = torch.as_tensor(batch_masks[i])
... ious.append(compute_mean_iou(pred_masks, gt_masks, target_size))
...
... mean_iou = sum(ious) / len(ious) if ious else 0.0
... return {"mean_iou": mean_iou}Trainer は、返されたキーに自動的に eval_ というプレフィックスを付与するため、これにより、後述のチェックポイント選択で使用される eval_mean_iou メトリクスが生成されます。
トレーニング
データ、モデル、メトリクスが準備できたので、トレーニングを設定します。TrainingArgumentsに関する重要な点について、いくつか説明します。
remove_unused_columns=False: 変換が実行される前にカラムが削除されるデフォルトの動作を避けるため、この設定が必要です。eval_do_concat_batches=False: インスタンスセグメンテーションのラベルは可変長の辞書であるため、バッチ間で連結できません。これにより、予測結果もバッチごとにグループ化されたまま保持され、compute_metricsがラベルと対応付けられるようになります。metric_for_best_model="eval_mean_iou": ロスだけでなく、セグメンテーションの品質に基づいて最適なチェックポイントを選択します。fp16=True: 混合精度トレーニングにより、最新のGPU上でのトレーニング速度が大幅に向上します。
>>> from transformers import Trainer, TrainingArguments
>>> training_args = TrainingArguments(
... output_dir="rf-detr-seg-satellite-buildings",
... num_train_epochs=10,
... per_device_train_batch_size=16,
... per_device_eval_batch_size=16,
... learning_rate=1e-4,
... weight_decay=1e-4,
... lr_scheduler_type="cosine",
... warmup_steps=0.1,
... fp16=True,
... dataloader_num_workers=4,
... eval_strategy="epoch",
... save_strategy="epoch",
... save_total_limit=2,
... load_best_model_at_end=True,
... metric_for_best_model="eval_mean_iou",
... greater_is_better=True,
... remove_unused_columns=False,
... eval_do_concat_batches=False,
... push_to_hub=False,
... )
>>> trainer = Trainer(
... model=model,
... args=training_args,
... train_dataset=train_ds,
... eval_dataset=valid_ds,
... processing_class=image_processor,
... data_collator=collate_fn,
... compute_metrics=compute_metrics,
... )
>>> trainer.train()学習引数でpush_to_hub=Trueを設定すると、学習チェックポイントがHugging Face Hubにプッシュされます。学習が完了したら、push_to_hub()メソッドを呼び出して、最終モデルもHubにプッシュしてください。
>>> trainer.push_to_hub(
... dataset=DATASET_ID,
... tags=["instance-segmentation", "rf-detr-seg", "vision", "satellite", "building"],
... )推論
ファインチューニング済みのモデルが手元にあるので、新しい衛星画像に対して推論を実行します。ワークフローは以下の通りです。
- 画像プロセッサで画像を前処理します
- モデルをフォワードパスで実行します
post_process_instance_segmentationを使用して後処理を実行し、ピクセルレベルのマスクを取得します
後処理ステップでは、生のクエリ出力(logitsと低解像度マスク)をフル解像度のインスタンスセグメンテーションマップに変換し、スコアしきい値処理とマスクの二値化を適用します。
>>> from datasets import load_dataset
>>> test_ds = load_dataset(DATASET_ID, split="test")
>>> sample = test_ds[0]
>>> image = sample["image"].convert("RGB")
>>> device = next(model.parameters()).device
>>> inputs = image_processor(images=image, return_tensors="pt").to(device)
>>> with torch.no_grad():
... outputs = model(**inputs)
>>> results = image_processor.post_process_instance_segmentation(
... outputs, threshold=0.5, target_sizes=[(image.height, image.width)]
... )[0]
>>> print(f"Detected {len(results['segments_info'])} buildings")
Detected 0 buildings
>>> for seg_info in results["segments_info"][:5]:
... print(f" Building (score: {seg_info['score']:.3f})")予測を可視化します。セグメンテーションマップでは、各ピクセルにセグメントID(背景は-1)が割り当てられます。検出された各建物にランダムな色を重ねて表示します。
>>> fig, axes = plt.subplots(1, 2, figsize=(14, 6))
>>> _ = axes[0].imshow(image)
>>> _ = axes[0].set_title("Input satellite image")
>>> _ = axes[0].axis("off")
>>> seg_map = results["segmentation"].cpu().numpy()
>>> overlay = np.array(image).copy()
>>> for seg_info in results["segments_info"]:
... mask = seg_map == seg_info["id"]
... color = np.random.randint(0, 255, 3)
... overlay[mask] = (overlay[mask] * 0.4 + color * 0.6).astype(np.uint8)
>>> _ = axes[1].imshow(overlay)
>>> _ = axes[1].set_title(f"Predicted masks ({len(results['segments_info'])} buildings)")
>>> _ = axes[1].axis("off")
>>> plt.tight_layout()
>>> plt.show()