COCO JSON을 변환하지 않고 YOLO를 학습시키는 방법#
COCO JSON 형식의 Annotations은 .txt 파일로 먼저 변환하지 않고 Ultralytics YOLO 학습에 직접 사용할 수 있습니다. 이는 YOLODataset를 서브클래싱하여 COCO JSON을 실시간으로 파싱하고 커스텀 트레이너를 통해 학습 파이프라인에 연결하는 방식으로 작동합니다.
COCO JSON으로 직접 학습해야 하는 이유#
이 접근 방식은 COCO JSON을 단일 진실 공급원(single source of truth)으로 유지하며, convert_coco() 호출이나 디렉터리 재구성, 중간 라벨 파일이 필요하지 않습니다. YOLO26 및 기타 모든 Ultralytics YOLO 디텍션 모델이 지원됩니다. 세그멘테이션 및 포즈 모델에는 추가 라벨 필드가 필요합니다(FAQ 참조).
표준 convert_coco() 워크플로우는 COCO to YOLO Conversion guide를 참조하세요.
아키텍처 개요#
두 개의 클래스가 필요합니다:
COCODataset— 학습 중에 COCO JSON을 읽고 bounding boxes를 메모리상에서 YOLO 형식으로 변환합니다.COCOTrainer— 기본YOLODataset대신COCODataset를 사용하도록build_dataset()을 재정의합니다.
이 구현은 JSON 어노테이션을 직접 읽는 내장 GroundingDataset과 동일한 패턴을 따릅니다. get_img_files(), cache_labels(), get_labels()의 세 가지 메서드가 오버라이드됩니다.
COCO JSON 데이터셋 클래스 구축#
COCODataset 클래스는 YOLODataset을 상속하고 라벨 로딩 로직을 오버라이드합니다. 라벨 디렉터리에서 .txt 파일을 읽는 대신, COCO JSON 파일을 열고 이미지별로 그룹화된 어노테이션을 순회하며, 각 바운딩 박스를 COCO 픽셀 형식 [x_min, y_min, width, height]에서 YOLO 정규화된 중심 형식 [x_center, y_center, width, height]로 변환합니다. 군중 어노테이션(iscrowd: 1)과 면적이 0인 박스는 자동으로 건너뜁니다.
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 필드를 사용하여 이미지 디렉터리를 찾습니다. 두 개의 추가 필드인 train_json와 val_json는 COCOTrainer이 읽을 COCO annotation 파일을 지정합니다. 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() 호출을 통해 학습이 진행됩니다. 일반 학습 실행과의 유일한 차이점은 Ultralytics가 기본 로더 대신 커스텀 데이터셋 로더를 사용하도록 지시하는 trainer=COCOTrainer 인수입니다.
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)이제 annotation이 단일 진실 공급원(single source of truth)으로 유지되고 중간 .txt 파일이 없는 상태에서 Ultralytics YOLO를 COCO JSON에서 직접 학습시키는 최소한의 데이터셋과 트레이너가 준비되었습니다. 세분화(segmentation) 및 포즈(pose)를 다루려면 cache_labels() 메서드를 segments 또는 keypoints으로 확장하고, hyperparameter 튜닝 권장 사항은 Model Training Tips 가이드를 참조하세요.
FAQ#
convert_coco()은 일회성 변환으로.txt라벨 파일을 디스크에 기록합니다. 이 접근 방식은 각 학습 실행의 시작 부분에서 JSON을 파싱하고 메모리상에서 어노테이션을 변환합니다. 영구적인 YOLO 형식 라벨을 선호하는 경우convert_coco()을 사용하고, 추가 파일을 생성하지 않고 COCO JSON을 단일 진실 공급원으로 유지하려면 이 접근 방식을 사용하세요.기본적으로 YOLO
.txt라벨을 예상하는 현재 Ultralytics 파이프라인으로는 불가능합니다. 이 가이드는 하나의 데이터셋 클래스와 하나의 트레이너 클래스라는 최소한의 커스텀 코드를 제공합니다. 일단 정의되면 학습에는 표준model.train()호출만 필요합니다.이 가이드는 object detection을 다룹니다. instance segmentation 지원을 추가하려면 각 라벨 딕셔너리의
segments필드에 COCO annotation의segmentation폴리곤 데이터를 포함하세요. pose estimation의 경우keypoints를 포함하세요.GroundingDatasetsource code는 세그먼트 처리를 위한 참조 구현을 제공합니다.예.
COCODataset은YOLODataset을 확장하므로 mosaic, mixup, copy-paste 등을 포함한 모든 기본 제공 data augmentations이 수정 없이 실행됩니다.카테고리는
id을 기준으로 정렬되며 0부터 시작하는 순차적 인덱스로 매핑됩니다. 이는 1 기반 ID(표준 COCO), 0 기반 ID 및 비연속 ID를 처리합니다.dataset.yaml의names딕셔너리는 COCOcategories배열과 동일한 정렬 순서를 따라야 합니다.COCO JSON은 첫 번째 학습 실행 시 한 번 파싱됩니다. 파싱된 라벨은
.cache파일에 저장되므로, 후속 실행에서는 재파싱 없이 즉시 로드됩니다. 어노테이션이 메모리에 유지되므로 학습 속도는 표준 YOLO 학습과 동일합니다. JSON 파일이 변경되면 캐시가 자동으로 다시 빌드됩니다.