Transformers documentation

オブジェクト検出

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v5.22.2).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

オブジェクト検出

Object detectionは、画像内のインスタンス(人間、建物、車など)を検出するコンピュータビジョンのタスクです。Object detectionモデルは画像を入力として受け取り、検出されたオブジェクトのバウンディングボックスの座標と関連するラベルを出力します。1つの画像には複数のオブジェクトが含まれることがあり、それぞれに独自のバウンディングボックスとラベル(例:車と建物)が付与されます。また、各オブジェクトは画像内の異なる部分に存在する可能性があります(例:複数の車)。このタスクは、歩行者、道路標識、信号機などの検出に、自動運転でよく使用されます。その他の応用としては、画像内のオブジェクトの個数カウント、画像検索などがあります。

このガイドでは、次のことを学びます。

  1. mobile-ui-design データセットで RF-DETR をファインチューニングし、モバイルアプリのスクリーンショット内のUI要素を検出します。
  2. ファインチューニングしたモデルを推論に使用します。

このタスクと互換性のあるすべてのアーキテクチャとチェックポイントを確認するには、タスクページを参照することをお勧めします。

開始前に、必要なライブラリがすべてインストールされていることを確認してください。

pip install -q datasets transformers accelerate timm trackio torchmetrics pycocotools

🤗 Datasets を使用して Hugging Face Hub からデータセットを読み込み、🤗 Transformers を使用してモデルを学習します。

コミュニティにモデルを共有することをお勧めします。Hugging Face アカウントにログインして、Hub にアップロードしてください。 プロンプトが表示されたら、ログイン用のトークンを入力してください:

>>> from huggingface_hub import notebook_login

>>> notebook_login()

グローバル定数、すなわちモデル名と画像サイズを定義します。このチュートリアルではRF-DETRを使用していますが、Transformersのオブジェクト検出モデルであれば何でも選択できます。

>>> MODEL_NAME = "Roboflow/rf-detr-medium"

mobile-ui-design データセットを読み込みます

mobile-ui-design データセットには、テキスト、画像、長方形、グループなどの UI 要素を検出するためのアノテーション付きのモバイルアプリのスクリーンショットが含まれています。

データセットを読み込み、カテゴリラベルを抽出することから始めます。データセットにはすでにスプリットが含まれています。

>>> from datasets import load_dataset

>>> ds = load_dataset("merve/mobile-ui-design")

>>> CATEGORIES = sorted(set(
...     cat for split in ds.values() for example in split for cat in example["objects"]["category"]
... ))
>>> label2id = {label: i for i, label in enumerate(CATEGORIES)}
>>> id2label = {i: label for label, i in label2id.items()}
>>> print(f"Categories ({len(CATEGORIES)}): {CATEGORIES}")
Categories (4): ['group', 'image', 'rectangle', 'text']

データセットは、文字列のカテゴリ名と、COCO形式のバウンディングボックス(x, y, w, h)を使用しています。学習前に、カテゴリを整数IDに変換し、面積を計算して、退化したバウンディングボックスを除外してください。

>>> def prepare_example(example, idx):
...     objects = example["objects"]
...     in_bboxes = objects["bbox"]
...     categories = objects["category"]
...     img_w, img_h = example["width"], example["height"]
...     bboxes, cats, areas, ids = [], [], [], []
...     for i, (bbox, cat) in enumerate(zip(in_bboxes, categories)):
...         x, y, w, h = bbox
...         if w <= 0 or h <= 0:
...             continue
...         x = max(0.0, min(x, img_w))
...         y = max(0.0, min(y, img_h))
...         w = min(w, img_w - x)
...         h = min(h, img_h - y)
...         if w <= 0 or h <= 0:
...             continue
...         bboxes.append([x, y, w, h])
...         cats.append(label2id[cat])
...         areas.append(w * h)
...         ids.append(i)
...     return {
...         "image_id": idx, "image": example["image"],
...         "width": example["width"], "height": example["height"],
...         "objects": {"id": ids, "bbox": bboxes, "category": cats, "area": areas},
...     }

>>> ds_prepared = ds["train"].map(prepare_example, with_indices=True, remove_columns=ds["train"].column_names)
>>> ds_prepared = ds_prepared.filter(lambda x: len(x["objects"]["bbox"]) > 0)

>>> split = ds_prepared.train_test_split(test_size=0.15, seed=1337)
>>> train_ds = split["train"]
>>> val_ds = split["test"]
>>> print(f"Train: {len(train_ds)}, Validation: {len(val_ds)}")
Train: 6669, Validation: 1177

データを前処理する

AutoImageProcessorは、モデルの学習に使用できるpixel_values、pixel_mask、labelsを生成するために、画像データの処理を行います。画像プロセッサは、リサイズ、パディング、正規化を処理します。さらに、汎化性能を向上させるために、ランダムなデータ拡張を任意で追加できます(下記を参照)。

>>> import numpy as np
>>> from functools import partial
>>> from transformers import AutoImageProcessor

>>> image_processor = AutoImageProcessor.from_pretrained(MODEL_NAME)

image_processor は COCO 形式の注釈を期待します: {'image_id': int, 'annotations': list[Dict]}。各サンプルの注釈をこの形式に整形し、残りはプロセッサに委ねてください:

>>> def format_image_annotations_as_coco(image_id, categories, areas, bboxes):
...     annotations = []
...     for category, area, bbox in zip(categories, areas, bboxes):
...         annotations.append({
...             "image_id": image_id,
...             "category_id": category,
...             "iscrowd": 0,
...             "area": area,
...             "bbox": list(bbox),
...         })
...     return {"image_id": image_id, "annotations": annotations}

>>> def transform_batch(examples, image_processor):
...     images = []
...     annotations = []
...     for image_id, image, objects in zip(examples["image_id"], examples["image"], examples["objects"]):
...         images.append(np.array(image.convert("RGB")))
...         formatted = format_image_annotations_as_coco(
...             image_id, objects["category"], objects["area"], objects["bbox"]
...         )
...         annotations.append(formatted)
...     result = image_processor(images=images, annotations=annotations, return_tensors="pt")
...     result.pop("pixel_mask", None)
...     return result

>>> transform_fn = partial(transform_batch, image_processor=image_processor)
>>> train_ds = train_ds.with_transform(transform_fn)
>>> val_ds = val_ds.with_transform(transform_fn)

データ拡張

上記のtransformは、画像のリサイズと正規化のみを行います。trainingスプリットに適用されるランダムなデータ拡張は通常、汎化性能の向上に寄与しますが、評価を決定論的に保つため、validationスプリットには拡張を適用しないようにします。一般的な選択肢として、画像とバウンディングボックスを同時に拡張するAlbumentationsがあります。bbox_paramsを指定してパイプラインを定義し、ボックスが画像と一貫して変換されるようにしてから、拡張されたボックスから面積を再計算します。

>>> import albumentations as A

>>> train_augment = A.Compose(
...     [
...         A.Perspective(p=0.1),
...         A.HorizontalFlip(p=0.5),
...         A.RandomBrightnessContrast(p=0.5),
...         A.HueSaturationValue(p=0.1),
...     ],
...     bbox_params=A.BboxParams(format="coco", label_fields=["category"], clip=True, min_area=25),
... )

>>> def augment_and_transform_batch(examples, image_processor, transform):
...     images = []
...     annotations = []
...     for image_id, image, objects in zip(examples["image_id"], examples["image"], examples["objects"]):
...         image = np.array(image.convert("RGB"))
...         output = transform(image=image, bboxes=objects["bbox"], category=objects["category"])
...         images.append(output["image"])
...         areas = [w * h for (_, _, w, h) in output["bboxes"]]
...         formatted = format_image_annotations_as_coco(
...             image_id, output["category"], areas, output["bboxes"]
...         )
...         annotations.append(formatted)
...     result = image_processor(images=images, annotations=annotations, return_tensors="pt")
...     result.pop("pixel_mask", None)
...     return result

拡張変換はトレーニングスプリットにのみ適用し、検証にはプレーンなtransform_fnを使用します:

>>> train_augment_fn = partial(augment_and_transform_batch, image_processor=image_processor, transform=train_augment)
>>> train_ds = train_ds.with_transform(train_augment_fn)
>>> val_ds = val_ds.with_transform(transform_fn)

画像をバッチ処理するために、カスタムのcollate_fnを作成します:

>>> import torch

>>> def collate_fn(batch):
...     data = {}
...     data["pixel_values"] = torch.stack([x["pixel_values"] for x in batch])
...     data["labels"] = [x["labels"] for x in batch]
...     if "pixel_mask" in batch[0]:
...         data["pixel_mask"] = torch.stack([x["pixel_mask"] for x in batch])
...     return data

mAPを計算する関数の準備

オブジェクト検出モデルは、COCO形式のメトリクスのセットで評価されることが一般的です。torchmetricsを使用してmAP(平均平均精度)とmAR(平均平均再現率)のメトリクスを計算し、Trainerでの評価に使用するためにcompute_metrics関数にラップします。

学習に使用されるボックスの中間形式はYOLO(正規化)ですが、ボックスの面積を正しく処理するために、Pascal VOC(絶対値)形式のボックスでメトリクスを計算します。以下に、バウンディングボックスをPascal VOC形式に変換する関数を定義します。

>>> from transformers.image_transforms import center_to_corners_format

>>> def convert_bbox_yolo_to_pascal(boxes, image_size):
...     """
...     Convert bounding boxes from YOLO format (x_center, y_center, width, height) in range [0, 1]
...     to Pascal VOC format (x_min, y_min, x_max, y_max) in absolute coordinates.

...     Args:
...         boxes (torch.Tensor): Bounding boxes in YOLO format
...         image_size (tuple[int, int]): Image size in format (height, width)

...     Returns:
...         torch.Tensor: Bounding boxes in Pascal VOC format (x_min, y_min, x_max, y_max)
...     """
...     # convert center to corners format
...     boxes = center_to_corners_format(boxes)

...     # convert to absolute coordinates
...     height, width = image_size
...     boxes = boxes * torch.tensor([[width, height, width, height]])

...     return boxes

次に、compute_metrics 関数では、評価ループの結果からpredicted と target のバウンディングボックス、スコア、ラベルを収集し、スコアリング関数に渡します。

>>> import numpy as np
>>> from dataclasses import dataclass
>>> from torchmetrics.detection.mean_ap import MeanAveragePrecision


>>> @dataclass
>>> class ModelOutput:
...     logits: torch.Tensor
...     pred_boxes: torch.Tensor


>>> def _get_orig_size(image_target):
...     """Robust orig_size extraction - Trainer serialization can truncate to 1 element."""
...     orig = np.atleast_1d(np.asarray(image_target["orig_size"])).flatten()
...     if len(orig) >= 2:
...         return (int(orig[0]), int(orig[1]))
...     return (int(orig[0]), int(orig[0]))

>>> @torch.no_grad()
>>> def compute_metrics(evaluation_results, image_processor, threshold=0.0, id2label=None):
...     predictions, targets = evaluation_results.predictions, evaluation_results.label_ids
...     image_sizes = []
...     post_processed_targets = []
...     post_processed_predictions = []
...
...     for batch in targets:
...         batch_sizes = []
...         for image_target in batch:
...             h, w = _get_orig_size(image_target)
...             batch_sizes.append([h, w])
...             boxes = torch.tensor(image_target["boxes"])
...             boxes = convert_bbox_yolo_to_pascal(boxes, (h, w))
...             labels = torch.tensor(image_target["class_labels"])
...             post_processed_targets.append({"boxes": boxes, "labels": labels})
...         image_sizes.append(torch.tensor(batch_sizes))
...
...     for batch, target_sizes in zip(predictions, image_sizes):
...         batch_logits, batch_boxes = batch[1], batch[2]
...         output = ModelOutput(logits=torch.tensor(batch_logits), pred_boxes=torch.tensor(batch_boxes))
...         post_processed_output = image_processor.post_process_object_detection(
...             output, threshold=threshold, target_sizes=target_sizes
...         )
...         post_processed_predictions.extend(post_processed_output)
...
...     metric = MeanAveragePrecision(box_format="xyxy", class_metrics=True)
...     metric.update(post_processed_predictions, post_processed_targets)
...     metrics = metric.compute()
...
...     classes = metrics.pop("classes")
...     map_per_class = metrics.pop("map_per_class")
...     mar_100_per_class = metrics.pop("mar_100_per_class")
...     for class_id, class_map, class_mar in zip(classes, map_per_class, mar_100_per_class):
...         class_name = id2label[class_id.item()] if id2label is not None else class_id.item()
...         metrics[f"map_{class_name}"] = class_map
...         metrics[f"mar_100_{class_name}"] = class_mar
...
...     metrics = {k: round(v.item(), 4) for k, v in metrics.items()}
...     return metrics

>>> eval_compute_metrics_fn = partial(
...     compute_metrics, image_processor=image_processor, id2label=id2label, threshold=0.0
... )

検出モデルの学習

前節で大部分の準備が整ったので、いよいよモデルの学習を開始できます。 このデータセットの画像は、リサイズ後もまだかなり大きいため、このモデルのファインチューニングには少なくとも1つのGPUが必要です。

トレーニングには、以下の手順が含まれます。

  1. 前処理で使用したのと同じチェックポイントで、AutoModelForObjectDetectionを使ってモデルを読み込みます。
  2. TrainingArguments で、学習ハイパーパラメータを定義します。
  3. モデル、データセット、画像プロセッサ、データコラテーターとともに、学習引数をTrainerに渡します。
  4. train()を呼び出して、モデルをファインチューニングします。

前処理で使用した同じチェックポイントからモデルを読み込む際は、データセットのメタデータから作成した label2id と id2label のマップを渡すことを忘れないでください。また、既存の分類ヘッドを新しいものに置き換えるため、ignore_mismatched_sizes=True を指定します。

>>> from transformers import AutoModelForObjectDetection

>>> model = AutoModelForObjectDetection.from_pretrained(
...     MODEL_NAME,
...     id2label=id2label,
...     label2id=label2id,
...     ignore_mismatched_sizes=True,
... )

TrainingArguments で output_dir を使用してモデルの保存先を指定し、その後、ハイパーパラメータを必要に応じて設定します。num_train_epochs=5 の場合、A100 GPU では約35分かかります。より良い結果を得るには、エポック数を増やしてください。

重要な注意事項:

  • 未使用の列を削除しないでください。そうすると画像の列が削除されてしまいます。画像の列がないと、pixel_values を作成できません。そのため、remove_unused_columns を False に設定してください。
  • 適切な評価結果を得るには、eval_do_concat_batches=False を設定してください。画像ごとにターゲットボックスの数が異なるため、バッチを連結すると、どのボックスがどの画像に属しているかを特定できなくなります。

モデルを Hub にプッシュして共有する場合は、push_to_hub を True に設定してください(モデルをアップロードするには、Hugging Face にサインインしている必要があります)。

>>> from transformers import TrainingArguments

>>> training_args = TrainingArguments(
...     output_dir="rf_detr_finetuned_mobile_ui",
...     num_train_epochs=5,
...     bf16=True,
...     per_device_train_batch_size=8,
...     dataloader_num_workers=4,
...     learning_rate=5e-5,
...     lr_scheduler_type="cosine",
...     weight_decay=1e-4,
...     max_grad_norm=0.01,
...     metric_for_best_model="eval_map",
...     greater_is_better=True,
...     load_best_model_at_end=True,
...     eval_strategy="epoch",
...     save_strategy="epoch",
...     save_total_limit=2,
...     remove_unused_columns=False,
...     report_to="trackio",
...     run_name="mobile-ui-detection",
...     eval_do_concat_batches=False,
...     push_to_hub=True,
... )

最後に、すべての要素をまとめ、train()を呼び出します。

>>> from transformers import Trainer

>>> trainer = Trainer(
...     model=model,
...     args=training_args,
...     train_dataset=train_ds,
...     eval_dataset=val_ds,
...     processing_class=image_processor,
...     data_collator=collate_fn,
...     compute_metrics=eval_compute_metrics_fn,
... )

>>> trainer.train()
[2085/2085 38:39, Epoch 5/5]
エポック学習損失検証損失マップMap 50Map 75Map SmallMap MediumMap Large3月1日3月10日3月100日マー・スモール3月 中Mar LargeMap GroupMar 100 グループMap ImageMar 100 画像Map RectangleMar 100 長方形テキストのマッピングMar 100 テキスト
1ログなし9.92340.13030.22360.14780.09090.20300.25240.04210.25200.46830.31130.56070.67820.12440.51220.09580.50350.12850.43280.17250.4413
2ログなし9.84720.18930.30170.21240.13470.27890.30380.05490.29610.51400.34330.59410.74060.13050.54230.19790.55780.19640.46480.23240.4437
3ログなし9.64010.22750.35470.26570.16980.33360.38920.06110.32040.52700.36250.61430.74960.16020.56840.26170.57630.22490.46840.26310.4692
4ログなし9.57700.27330.40680.31330.21000.38670.43430.06680.34560.55930.38750.63930.77250.20130.59410.31580.60650.27330.49980.30280.4756
510.370011.05000.28270.41930.29130.20210.28140.37630.06090.34030.56680.41380.56690.73170.20920.59790.33340.62950.27930.52450.30890.5151

training_args で push_to_hub を True に設定している場合、トレーニングのチェックポイントは Hugging Face Hub にプッシュされます。トレーニングが完了したら、push_to_hub() メソッドを呼び出して、最終モデルも Hub にプッシュしてください。

>>> trainer.push_to_hub()

評価

>>> from pprint import pprint

>>> metrics = trainer.evaluate(eval_dataset=val_ds, metric_key_prefix="test")
>>> pprint(metrics)
{'test_loss': 11.05,
 'test_map': 0.2827,
 'test_map_50': 0.4193,
 'test_map_75': 0.2913,
 'test_map_group': 0.2092,
 'test_map_image': 0.3334,
 'test_map_large': 0.3763,
 'test_map_medium': 0.2814,
 'test_map_rectangle': 0.2793,
 'test_map_small': 0.2021,
 'test_map_text': 0.3089,
 'test_mar_1': 0.0609,
 'test_mar_10': 0.3403,
 'test_mar_100': 0.5668,
 'test_mar_100_group': 0.5979,
 'test_mar_100_image': 0.6295,
 'test_mar_100_rectangle': 0.5245,
 'test_mar_100_text': 0.5151,
 'test_mar_large': 0.7317,
 'test_mar_medium': 0.5669,
 'test_mar_small': 0.4138}

TrainingArguments でエポック数を増やす、または他のハイパーパラメータを調整することで、これらの結果をさらに改善できます。ぜひ試してみてください!

推論

モデルのファインチューニング、評価、そして Hugging Face Hub へのアップロードが完了したので、推論に使用できます。

>>> import torch
>>> from PIL import Image, ImageDraw
>>> from transformers import AutoImageProcessor, AutoModelForObjectDetection
>>> from datasets import load_dataset

>>> ds = load_dataset("merve/mobile-ui-design", split="train")
>>> image = ds[5]["image"].convert("RGB")

Hugging Face Hubからモデルと画像プロセッサを読み込みます(このセッションで既に学習済みの場合はスキップしてください):

>>> model_repo = "merve/rf_detr_finetuned_mobile_ui"

>>> image_processor = AutoImageProcessor.from_pretrained(model_repo)
>>> model = AutoModelForObjectDetection.from_pretrained(model_repo)
>>> model.eval()

バウンディングボックスを検出します:

>>> with torch.no_grad():
...     inputs = image_processor(images=[image], return_tensors="pt")
...     outputs = model(**inputs)
...     target_sizes = torch.tensor([[image.size[1], image.size[0]]])
...     results = image_processor.post_process_object_detection(outputs, threshold=0.5, target_sizes=target_sizes)[0]

>>> for score, label, box in zip(results["scores"], results["labels"], results["boxes"]):
...     box = [round(i, 2) for i in box.tolist()]
...     print(
...         f"Detected {model.config.id2label[label.item()]} with confidence "
...         f"{round(score.item(), 3)} at location {box}"
...     )
Detected text with confidence 0.727 at location [324.02, 340.55, 339.52, 359.12]
Detected rectangle with confidence 0.717 at location [39.97, 705.14, 335.93, 753.54]
Detected text with confidence 0.702 at location [199.94, 473.66, 213.41, 490.6]
Detected text with confidence 0.678 at location [153.14, 474.81, 165.33, 491.0]
Detected text with confidence 0.675 at location [262.67, 718.28, 281.44, 740.81]
Detected rectangle with confidence 0.655 at location [143.57, 242.51, 214.32, 274.26]
Detected text with confidence 0.653 at location [298.68, 637.77, 345.68, 656.26]

結果をプロットしてみましょう:

>>> draw = ImageDraw.Draw(image)

>>> colors = {"group": "blue", "image": "green", "rectangle": "red", "text": "orange"}
>>> for score, label, box in zip(results["scores"], results["labels"], results["boxes"]):
...     box = [round(i, 2) for i in box.tolist()]
...     x, y, x2, y2 = tuple(box)
...     label_name = model.config.id2label[label.item()]
...     color = colors.get(label_name, "red")
...     draw.rectangle((x, y, x2, y2), outline=color, width=2)
...     draw.text((x, y), f"{label_name} {score:.2f}", fill=color)

>>> image
Object detection result on a cart screen
Object detection result on a followers screen
Update on GitHub