COCO JSONを変換せずにYOLOで学習する方法#
COCO JSON 形式の アノテーション は、事前に .txt ファイルに変換することなく、Ultralytics YOLO のトレーニングに直接使用できます。これは、YOLODataset をサブクラス化して COCO JSON をオンザフライでパースし、カスタムトレーナーを通じてトレーニングパイプラインに組み込むことで機能します。
COCO JSONで直接学習する理由#
このアプローチでは、COCO JSON を唯一の信頼できる情報源(シングルソースオブトゥルース)として維持するため、convert_coco() の呼び出し、ディレクトリの再編成、中間ラベルファイルは不要です。YOLO26 およびその他のすべての Ultralytics YOLO 検出モデルがサポートされています。セグメンテーションモデルとポーズモデルでは、追加のラベルフィールドが必要です(FAQ を参照してください)。
標準的な convert_coco() ワークフローについては、COCO から YOLO への変換ガイド をご覧ください。
アーキテクチャの概要#
次の2つのクラスが必要です。
COCODataset— トレーニング中に COCO JSON を読み込み、bounding boxes をメモリ内で YOLO 形式に変換しますCOCOTrainer— デフォルトのYOLODatasetの代わりにCOCODatasetを使用するようにbuild_dataset()をオーバーライドします。
この実装は、JSON アノテーションも直接読み込む組み込みの GroundingDataset と同じパターンに従っています。get_img_files()、cache_labels()、および get_labels() の 3 つのメソッドが上書きされます。
COCO JSONデータセットクラスの構築#
COCODataset クラスは YOLODataset を継承し、ラベル読み込みロジックを上書きします。ラベルディレクトリから .txt ファイルを読み込む代わりに、COCO JSON ファイルを開き、画像ごとにグループ化されたアノテーションを反復処理し、各バウンディングボックスを COCO ピクセル形式 [x_min, y_min, width, height] から YOLO 正規化中心形式 [x_center, y_center, width, height] に変換します。クラウド(群衆)アノテーション(iscrowd: 1)および面積がゼロのボックスは自動的にスキップされます。
get_img_files() メソッドは空のリストを返します。これは、画像パスが cache_labels() 内の JSON file_name フィールドから解決されるためです。カテゴリ ID はソートされて 0 から始まるクラスインデックスに再マッピングされるため、1始まり(標準 COCO)および不連続な 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 COCODataset(YOLODataset):
"""Dataset that reads COCO JSON annotations directly without conversion to .txt files."""
def __init__(self, *args, json_file="", **kwargs):
"""Initialize the dataset with a COCO JSON annotation file."""
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)
# 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"]パースされたラベルは、JSON の隣にある .cache ファイル(例: instances_train.cache)に保存されます。次回のトレーニング実行以降は、キャッシュが直接読み込まれ、JSON のパースがスキップされます。JSON ファイルが変更された場合、ハッシュチェックが失敗し、キャッシュが自動的に再構築されます。
学習パイプラインへのデータセットの接続#
トレーナーで必要な変更は build_dataset() のオーバーライドのみです。デフォルトの DetectionTrainer は、.txt ラベルファイルをスキャンする YOLODataset を構築します。これを COCODataset に置き換えることで、トレーナーは代わりに COCO JSON から読み込みを行います。
JSON ファイルのパスは、データ設定内のカスタム train_json / val_json フィールドから取得されます(Configuring dataset.yaml を参照してください)。トレーニング中、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 COCOTrainer(DetectionTrainer):
"""Trainer that uses COCODataset for direct COCO JSON training."""
def build_dataset(self, img_path, mode="train", batch=None):
"""Build a COCODataset for the given split using the JSON file from the data config."""
json_file = self.data["train_json"] if mode == "train" else self.data.get("val_json", self.data["train_json"])
return COCODataset(
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 JSON用dataset.yamlの設定#
dataset.yaml は、標準の path、train、および val フィールドを使用して画像ディレクトリの場所を特定します。2 つの追加フィールド train_json および val_json は、COCOTrainer が読み込む COCO アノテーションファイルを指定します。nc および names フィールドは、JSON 内の categories のソート順に一致する、クラスの数とその名前を定義します。
path: /path/to/my_dataset/images # root with train/ and val/ image subfolders
train: train
val: val
# COCO JSON annotation files (use absolute paths; these custom keys are not resolved against `path`)
train_json: /path/to/my_dataset/annotations/instances_train.json
val_json: /path/to/my_dataset/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.yamlCOCO JSONでの学習の実行#
データセットクラス、トレーナークラス、および YAML 設定が揃ったら、標準の model.train() 呼び出しを通じてトレーニングが機能します。通常のトレーニング実行との唯一の違いは trainer=COCOTrainer 引数であり、これにより Ultralytics はデフォルトのローダーではなくカスタムデータセットローダーを使用するよう指示されます。
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
model.train(data="dataset.yaml", epochs=100, imgsz=640, trainer=COCOTrainer)完全な training パイプラインは、validation、チェックポイントの保存、およびメトリックのログ記録を含め、期待通りに実行されます。
完全な実装#
利便性のため、完全な実装を単一のコピー&ペースト可能なスクリプトとして以下に示します。これには、カスタムデータセット、カスタムトレーナー、およびトレーニングの呼び出しが含まれています。これを 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 COCODataset(YOLODataset):
"""Dataset that reads COCO JSON annotations directly without conversion to .txt files."""
def __init__(self, *args, json_file="", **kwargs):
"""Initialize the dataset with a COCO JSON annotation file."""
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)
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):
"""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"]
class COCOTrainer(DetectionTrainer):
"""Trainer that uses COCODataset for direct COCO JSON training."""
def build_dataset(self, img_path, mode="train", batch=None):
"""Build a COCODataset for the given split using the JSON file from the data config."""
json_file = self.data["train_json"] if mode == "train" else self.data.get("val_json", self.data["train_json"])
return COCODataset(
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=COCOTrainer)これで、アノテーションを信頼できる唯一の情報源(シングルソースオブトゥルース)とし、中間的な .txt ファイルを使用せずに、Ultralytics YOLO を COCO JSON で直接トレーニングする最小限のデータセットとトレーナーが完成しました。セグメンテーションとポーズをカバーするために cache_labels() メソッドを segments または keypoints で拡張し、ハイパーパラメータ チューニングの推奨事項については モデルトレーニングのヒント ガイドを参照してください。
よくある質問 (FAQ)#
convert_coco()は、1 回限りの変換として.txtラベルファイルをディスクに書き込みます。このアプローチでは、各トレーニングの開始時に JSON をパースし、メモリ内でアノテーションを変換します。永続的な YOLO 形式のラベルが好ましい場合はconvert_coco()を使用し、追加のファイルを生成せずに COCO JSON を唯一の信頼できる情報源として維持したい場合はこのアプローチを使用してください。デフォルトで YOLO
.txtラベルを想定している現在の Ultralytics パイプラインでは、そのままではできません。このガイドでは、必要な最小限のカスタムコード(1 つのデータセットクラスと 1 つのトレーナークラス)を提供します。定義したら、トレーニングには標準のmodel.train()呼び出しのみが必要です。このガイドでは オブジェクト検出 について説明します。インスタンスセグメンテーション のサポートを追加するには、各ラベル辞書の
segmentsフィールドに COCO アノテーションからのsegmentationポリゴンデータを含めます。姿勢推定 については、keypointsを含めます。GroundingDatasetの ソースコード は、セグメントを処理するためのリファレンス実装を提供します。カテゴリは
idによってソートされ、0 から始まるシーケンシャルなインデックスにマッピングされます。これにより、1ベースの ID(標準 COCO)、0ベースの ID、および非連続な ID が処理されます。dataset.yaml内のnames辞書は、COCO のcategories配列と同じソート順に従う必要があります。COCO JSON は最初のトレーニング実行時に一度だけパースされます。パースされたラベルは
.cacheファイルに保存されるため、2回目以降の実行では再パースなしで即座にロードされます。アノテーションがメモリ内に保持されるため、トレーニング速度は標準の YOLO トレーニングと同一です。JSON ファイルが変更された場合、キャッシュは自動的に再構築されます。