コンテンツにスキップ

変換せずにCOCO YOLO 学習させる方法

なぜCOCO で直接トレーニングするのか

注釈COCO このフォーマットは、直接 Ultralytics YOLO 変換せずにトレーニングを行う .txt まずファイルから。これはサブクラス化を行うことで実現されます YOLODataset COCO オンザフライで解析し、カスタムトレーナーを通じてトレーニングパイプラインに組み込む。

このアプローチでは、COCO を唯一の信頼できる情報源として維持します — いいえ convert_coco() 呼び出し、ディレクトリの再編成なし、中間ラベルファイルなし。 YOLO26 およびその他のUltralytics YOLO モデルがサポートされています。セグメンテーションおよびポーズモデルには、追加のラベルフィールドが必要です(詳細は よくある質問)。

その代わり、1回限りの変換をお探しですか?

以下を COCO YOLO COCO YOLO ガイド 標準仕様として convert_coco() ワークフロー。

アーキテクチャの概要

2つのクラスが必要です:

  1. COCOJSONDataset —COCO を読み込み、変換します バウンディングボックス トレーニング中にメモリ内でYOLO に変換する
  2. COCOJSONTrainer — 上書き build_dataset() 使用するには COCOJSONDataset デフォルトの代わりに YOLODataset

この実装は、組み込みの機能と同じパターンに従っています GroundingDatasetまた、JSONアノテーションも直接読み取ります。3つのメソッドがオーバーライドされています: get_img_files(), cache_labels()、および get_labels().

COCO データセットクラスの作成

The COCOJSONDataset クラスは~から継承する YOLODataset そして、ラベルの読み込みロジックを上書きします。代わりに読み込むのではなく .txt labelsディレクトリ内のファイルを読み込み、COCO ファイルを開き、画像ごとにグループ化されたアノテーションを順に処理し、各バウンディングボックスCOCO 形式から変換します [x_min, y_min, width, height] YOLO センター形式へ [x_center, y_center, width, height]. ユーザーによる注釈 (iscrowd: 1) および面積がゼロのボックスは自動的にスキップされます。

The get_img_files() このメソッドは、画像のパスがJSONから解決されるため、空のリストを返します file_name 内部のフィールド cache_labels()カテゴリIDはソートされ、0を起点とするクラスインデックスに再マッピングされるため、1を起点とする(標準COCO)ID体系と、連続していないID体系のどちらも正しく機能します。

import json
from collections import defaultdict
from pathlib import Path

import numpy as np

from ultralytics.data.dataset import DATASET_CACHE_VERSION, YOLODataset
from ultralytics.data.utils import get_hash, load_dataset_cache_file, save_dataset_cache_file
from ultralytics.utils import TQDM


class COCOJSONDataset(YOLODataset):
    """Dataset that reads COCO JSON annotations directly without conversion to .txt files."""

    def __init__(self, *args, json_file="", **kwargs):
        self.json_file = json_file
        super().__init__(*args, data={"channels": 3}, **kwargs)

    def get_img_files(self, img_path):
        """Image paths are resolved from the JSON file, not from scanning a directory."""
        return []

    def cache_labels(self, path=Path("./labels.cache")):
        """Parse COCO JSON and convert annotations to YOLO format. Results are saved to a .cache file."""
        x = {"labels": []}
        with open(self.json_file) as f:
            coco = json.load(f)

        images = {img["id"]: img for img in coco["images"]}

        # Sort categories by ID and map to 0-indexed classes
        categories = {cat["id"]: i for i, cat in enumerate(sorted(coco["categories"], key=lambda c: c["id"]))}

        img_to_anns = defaultdict(list)
        for ann in coco["annotations"]:
            img_to_anns[ann["image_id"]].append(ann)

        for img_info in TQDM(coco["images"], desc="reading annotations"):
            h, w = img_info["height"], img_info["width"]
            im_file = Path(self.img_path) / img_info["file_name"]
            if not im_file.exists():
                continue

            self.im_files.append(str(im_file))
            bboxes = []
            for ann in img_to_anns.get(img_info["id"], []):
                if ann.get("iscrowd", False):
                    continue
                # COCO: [x, y, w, h] top-left in pixels -> YOLO: [cx, cy, w, h] center normalized
                box = np.array(ann["bbox"], dtype=np.float32)
                box[:2] += box[2:] / 2  # top-left to center
                box[[0, 2]] /= w  # normalize x
                box[[1, 3]] /= h  # normalize y
                if box[2] <= 0 or box[3] <= 0:
                    continue
                cls = categories[ann["category_id"]]
                bboxes.append([cls, *box.tolist()])

            lb = np.array(bboxes, dtype=np.float32) if bboxes else np.zeros((0, 5), dtype=np.float32)
            x["labels"].append(
                {
                    "im_file": str(im_file),
                    "shape": (h, w),
                    "cls": lb[:, 0:1],
                    "bboxes": lb[:, 1:],
                    "segments": [],
                    "normalized": True,
                    "bbox_format": "xywh",
                }
            )
        x["hash"] = get_hash([self.json_file, str(self.img_path)])
        save_dataset_cache_file(self.prefix, path, x, DATASET_CACHE_VERSION)
        return x

    def get_labels(self):
        """Load labels from .cache file if available, otherwise parse JSON and create the cache."""
        cache_path = Path(self.json_file).with_suffix(".cache")
        try:
            cache = load_dataset_cache_file(cache_path)
            assert cache["version"] == DATASET_CACHE_VERSION
            assert cache["hash"] == get_hash([self.json_file, str(self.img_path)])
            self.im_files = [lb["im_file"] for lb in cache["labels"]]
        except (FileNotFoundError, AssertionError, AttributeError, KeyError, ModuleNotFoundError):
            cache = self.cache_labels(cache_path)
        cache.pop("hash", None)
        cache.pop("version", None)
        return cache["labels"]

解析されたラベルは .cache JSONファイルの隣にあるファイル(例: instances_train.cache). 以降のトレーニング実行時には、JSONの解析をスキップしてキャッシュが直接読み込まれます。JSONファイルが変更されると、ハッシュチェックに失敗し、キャッシュが自動的に再構築されます。

データセットをトレーニングパイプラインに接続する

トレーナーで必要な変更は、オーバーライドすることだけです build_dataset(). デフォルト DetectionTrainer を構築する YOLODataset …をスキャンする .txt ラベルファイル。これを次のように置き換えることで COCOJSONDataset代わりに、トレーナーはCOCO を読み込みます。

JSONファイルのパスは、カスタム train_json / val_json field in the data config (see Step 3). During training, mode="train" ~することを決意する train_json; 検証の際、 mode="val" ~することを決意する val_json. もし val_json が設定されていない場合、デフォルトでは train_json.

from ultralytics.models.yolo.detect import DetectionTrainer
from ultralytics.utils import colorstr


class COCOJSONTrainer(DetectionTrainer):
    """Trainer that uses COCOJSONDataset for direct COCO JSON training."""

    def build_dataset(self, img_path, mode="train", batch=None):
        json_file = self.data["train_json"] if mode == "train" else self.data.get("val_json", self.data["train_json"])
        return COCOJSONDataset(
            img_path=img_path,
            json_file=json_file,
            imgsz=self.args.imgsz,
            batch_size=batch,
            augment=mode == "train",
            hyp=self.args,
            rect=self.args.rect or mode == "val",
            cache=self.args.cache or None,
            single_cls=self.args.single_cls or False,
            stride=int(self.model.stride.max()) if hasattr(self, "model") and self.model else 32,
            pad=0.0 if mode == "train" else 0.5,
            prefix=colorstr(f"{mode}: "),
            task=self.args.task,
            classes=self.args.classes,
            fraction=self.args.fraction if mode == "train" else 1.0,
        )

COCO yaml .yaml の設定

The dataset.yaml 標準を使用します path, train、および val 画像ディレクトリを指定するためのフィールド。さらに2つのフィールド、 train_json および val_json、COCO ファイルを指定して COCOJSONTrainer と書かれている。その nc および names fields は、クラスの数とその名前を定義し、ソートされた順序と一致します。 categories JSON内で。

path: /path/to/images # root directory with train/ and val/ subfolders
train: train
val: val

# COCO JSON annotation files
train_json: /path/to/annotations/instances_train.json
val_json: /path/to/annotations/instances_val.json

nc: 80
names:
    0: person
    1: bicycle
    # ... remaining class names

想定されるディレクトリ構造:

my_dataset/
  images/
    train/
      img_001.jpg
      ...
    val/
      img_100.jpg
      ...
  annotations/
    instances_train.json
    instances_val.json
  dataset.yaml

COCO でのトレーニングの実行

データセットクラス、トレーナークラス、およびYAML設定ファイルが整えば、トレーニングは標準の手順に従って実行されます model.train() 呼び出し。通常のトレーニング走行との唯一の違いは、 trainer=COCOJSONTrainer この引数を指定すると、Ultralytics デフォルトのデータセットローダーではなく、カスタムデータセットローダーUltralytics ようになります。

from ultralytics import YOLO

model = YOLO("yolo26n.pt")
model.train(data="dataset.yaml", epochs=100, imgsz=640, trainer=COCOJSONTrainer)

検証、チェックポイントの保存、メトリクスの記録を含め、トレーニングパイプライン全体が想定通りに実行されています。

完全な導入

便宜上、完全な実装を1つのスクリプトとして以下に示します。これには、カスタムデータセット、カスタムトレーナー、およびトレーニング呼び出しが含まれています。これを、ご自身の dataset.yaml そして、それを直接実行します。

import json
from collections import defaultdict
from pathlib import Path

import numpy as np

from ultralytics import YOLO
from ultralytics.data.dataset import DATASET_CACHE_VERSION, YOLODataset
from ultralytics.data.utils import get_hash, load_dataset_cache_file, save_dataset_cache_file
from ultralytics.models.yolo.detect import DetectionTrainer
from ultralytics.utils import TQDM, colorstr


class COCOJSONDataset(YOLODataset):
    """Dataset that reads COCO JSON annotations directly without conversion to .txt files."""

    def __init__(self, *args, json_file="", **kwargs):
        self.json_file = json_file
        super().__init__(*args, data={"channels": 3}, **kwargs)

    def get_img_files(self, img_path):
        return []

    def cache_labels(self, path=Path("./labels.cache")):
        x = {"labels": []}
        with open(self.json_file) as f:
            coco = json.load(f)

        images = {img["id"]: img for img in coco["images"]}
        categories = {cat["id"]: i for i, cat in enumerate(sorted(coco["categories"], key=lambda c: c["id"]))}

        img_to_anns = defaultdict(list)
        for ann in coco["annotations"]:
            img_to_anns[ann["image_id"]].append(ann)

        for img_info in TQDM(coco["images"], desc="reading annotations"):
            h, w = img_info["height"], img_info["width"]
            im_file = Path(self.img_path) / img_info["file_name"]
            if not im_file.exists():
                continue

            self.im_files.append(str(im_file))
            bboxes = []
            for ann in img_to_anns.get(img_info["id"], []):
                if ann.get("iscrowd", False):
                    continue
                box = np.array(ann["bbox"], dtype=np.float32)
                box[:2] += box[2:] / 2
                box[[0, 2]] /= w
                box[[1, 3]] /= h
                if box[2] <= 0 or box[3] <= 0:
                    continue
                cls = categories[ann["category_id"]]
                bboxes.append([cls, *box.tolist()])

            lb = np.array(bboxes, dtype=np.float32) if bboxes else np.zeros((0, 5), dtype=np.float32)
            x["labels"].append(
                {
                    "im_file": str(im_file),
                    "shape": (h, w),
                    "cls": lb[:, 0:1],
                    "bboxes": lb[:, 1:],
                    "segments": [],
                    "normalized": True,
                    "bbox_format": "xywh",
                }
            )
        x["hash"] = get_hash([self.json_file, str(self.img_path)])
        save_dataset_cache_file(self.prefix, path, x, DATASET_CACHE_VERSION)
        return x

    def get_labels(self):
        cache_path = Path(self.json_file).with_suffix(".cache")
        try:
            cache = load_dataset_cache_file(cache_path)
            assert cache["version"] == DATASET_CACHE_VERSION
            assert cache["hash"] == get_hash([self.json_file, str(self.img_path)])
            self.im_files = [lb["im_file"] for lb in cache["labels"]]
        except (FileNotFoundError, AssertionError, AttributeError, KeyError, ModuleNotFoundError):
            cache = self.cache_labels(cache_path)
        cache.pop("hash", None)
        cache.pop("version", None)
        return cache["labels"]


class COCOJSONTrainer(DetectionTrainer):
    """Trainer that uses COCOJSONDataset for direct COCO JSON training."""

    def build_dataset(self, img_path, mode="train", batch=None):
        json_file = self.data["train_json"] if mode == "train" else self.data.get("val_json", self.data["train_json"])
        return COCOJSONDataset(
            img_path=img_path,
            json_file=json_file,
            imgsz=self.args.imgsz,
            batch_size=batch,
            augment=mode == "train",
            hyp=self.args,
            rect=self.args.rect or mode == "val",
            cache=self.args.cache or None,
            single_cls=self.args.single_cls or False,
            stride=int(self.model.stride.max()) if hasattr(self, "model") and self.model else 32,
            pad=0.0 if mode == "train" else 0.5,
            prefix=colorstr(f"{mode}: "),
            task=self.args.task,
            classes=self.args.classes,
            fraction=self.args.fraction if mode == "train" else 1.0,
        )


model = YOLO("yolo26n.pt")
model.train(data="dataset.yaml", epochs=100, imgsz=640, trainer=COCOJSONTrainer)

ハイパーパラメータの推奨値については、『モデルトレーニングのヒント』ガイドを参照してください。

よくある質問

これとconvert_coco()の違いは何ですか?

convert_coco() と書いている .txt ラベル付きファイルをディスクに書き出すのは、一度限りの変換として行います。このアプローチでは、各トレーニング実行の開始時にJSONを解析し、メモリ内でアノテーションを変換します。使用方法 convert_coco() YOLO恒久的なラベルを使用する場合、このアプローチを採用することで、追加のファイルを生成することなく、COCO を唯一の信頼できる情報源として維持できます。

YOLO 、カスタムコードなしでCOCO を使ってYOLO できますか?

現在のUltralytics では、YOLO想定されているため、そうではありません .txt デフォルトではラベルが設定されます。このガイドでは、必要な最小限のカスタムコード(データセットクラス1つとトレーナークラス1つ)を紹介します。これらを定義すれば、トレーニングには標準的な model.train() 電話。

これはセグメンテーションと姿勢推定に対応していますか?

このガイドでは、以下の内容を取り上げます 物体検出. 追加するには インスタンスセグメンテーション サポート、以下を含める segmentation COCO からのポリゴンデータ segments 各ラベル辞書のフィールド。 姿勢推定、を含む keypointsGroundingDataset ソースコード セグメントを処理するための参照実装を提供します。

このカスタムデータセットで拡張機能は動作しますか?

はい。 COCOJSONDataset 拡張する YOLODataset, したがって、すべての組み込み データ拡張モザイク, mixup, コピー&ペースト、およびその他のプログラムは、変更を加えることなく実行できます。

カテゴリIDはクラスインデックスにどのように対応付けられているのですか?

カテゴリは以下で並べ替えられています id そして、0 から始まる連続したインデックスにマッピングされます。これにより、1 から始まる ID(標準のCOCO)、0 から始まる ID、および連続していない ID に対応できます。この names 辞書 dataset.yaml COCOと同じ並べ替え順序に従うべきである categories 配列。

あらかじめ変換済みのラベルと比較して、パフォーマンス上のオーバーヘッドはありますか?

COCO は、最初のトレーニング実行時に一度だけ解析されます。解析されたラベルは .cache ファイルに保存されるため、その後の実行では再解析を行うことなく即座に読み込まれます。アノテーションはメモリ内に保持されるため、トレーニング速度は標準的なYOLO と同等です。JSONファイルが変更されると、キャッシュは自動的に再構築されます。



📅 0日前に作成✏️ 0日前に更新
glenn-jocherraimbekovm

コメント