변환 없이 COCO JSON으로 YOLO를 학습하는 방법
COCO JSON으로 직접 학습해야 하는 이유
주석(Annotations) 내의 COCO JSON 형식은 먼저 Ultralytics YOLO 파일로 변환할 필요 없이 .txt 학습에 직접 사용할 수 있습니다. 이는 YOLODataset을 하위 클래스로 만들어 COCO JSON을 즉석에서 파싱하고 사용자 지정 트레이너를 통해 학습 파이프라인에 연결함으로써 수행됩니다.
이 방식은 COCO JSON을 유일한 신뢰 원본으로 유지하므로 convert_coco() 호출, 디렉토리 재구성, 중간 레이블 파일이 필요 없습니다. YOLO26 및 기타 모든 Ultralytics YOLO 감지 모델이 지원됩니다. 세그멘테이션 및 포즈 모델은 추가 레이블 필드가 필요합니다(참조: FAQ).
다음의 COCO to YOLO 변환 가이드 표준 convert_coco() 워크플로우를 참조하세요.
아키텍처 개요
두 가지 클래스가 필요합니다:
COCODataset— COCO JSON을 읽고 bounding boxes을 학습 중 메모리에서 YOLO 형식으로 변환합니다.COCOTrainer— 기본build_dataset()대신COCODataset을 사용하도록YOLODataset
을 재정의합니다. 구현은 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() 메서드는 이미지 경로가 JSON file_name 내부의 cache_labels() 필드에서 해결되므로 빈 리스트를 반환합니다. 카테고리 ID는 정렬되어 0부터 시작하는 클래스 인덱스로 다시 매핑되므로 1부터 시작하는 ID(표준 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):
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"]파싱된 레이블은 JSON 옆의 .cache 파일(예: instances_train.cache)에 저장됩니다. 이후 학습 실행 시 캐시가 직접 로드되어 JSON 파싱을 건너뜁니다. JSON 파일이 변경되면 해시 검사가 실패하고 캐시가 자동으로 다시 빌드됩니다.
학습 파이프라인에 데이터셋 연결
트레이너에서 유일하게 필요한 변경 사항은 build_dataset()을 재정의하는 것입니다. 기본 DetectionTrainer은 YOLODataset을 스캔하는 .txt를 빌드합니다. 이를 COCODataset로 대체하면 트레이너가 대신 COCO JSON에서 읽어옵니다.
JSON 파일 경로는 데이터 구성의 사용자 지정 train_json / val_json 필드에서 가져옵니다(3단계 참조). 학습 중에는 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):
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,
)로 대체됩니다.
dataset.yamlCOCO JSON을 위한 dataset.yaml 구성path, train, 그리고 val은 표준 train_json 및 val_json 필드를 사용하여 이미지 디렉토리를 찾습니다. 두 개의 추가 필드인 COCOTrainer, nc 및 names은 categories이 읽는 COCO 어노테이션 파일을 지정합니다.
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필드는 JSON 내
my_dataset/
images/
train/
img_001.jpg
...
val/
img_100.jpg
...
annotations/
instances_train.json
instances_val.json
dataset.yaml의 정렬된 순서에 맞춰 클래스 수와 이름을 정의합니다.
예상 디렉토리 구조:model.train()COCO JSON으로 학습 실행trainer=COCOTrainer데이터셋 클래스, 트레이너 클래스 및 YAML 구성이 준비되면 학습은 표준
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
model.train(data="dataset.yaml", epochs=100, imgsz=640, trainer=COCOTrainer)호출을 통해 작동합니다. 일반 학습 실행과의 유일한 차이점은 학습 인수이며, 이는 Ultralytics에게 기본 로더 대신 사용자 지정 데이터셋 로더를 사용하도록 지시합니다.검증전체
파이프라인은
, 체크포인트 저장 및 메트릭 로깅을 포함하여 예상대로 실행됩니다.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):
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 COCOTrainer(DetectionTrainer):
"""Trainer that uses COCODataset 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 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)편의를 위해 전체 구현을 아래에 단일 복사-붙여넣기 스크립트로 제공합니다. 여기에는 사용자 지정 데이터셋, 사용자 지정 트레이너 및 학습 호출이 포함되어 있습니다. 이를 와 함께 저장하고 직접 실행하세요.hyperparameter 가이드를 참조하십시오.
FAQ
권장 사항은
convert_coco()모델 학습 팁.txt을 참조하세요.convert_coco()이 방식과 convert_coco()의 차이점은 무엇인가요?
은 1회성 변환으로
레이블 파일을 디스크에 씁니다. 이 접근 방식은 각 학습 실행 시작 시 JSON을 파싱하고 메모리 내에서 어노테이션을 변환합니다. 영구적인 YOLO 형식 레이블이 선호될 때는 .txt을 사용하고, 추가 파일 생성 없이 COCO JSON을 유일한 신뢰 원본으로 유지하려면 이 접근 방식을 사용하세요.model.train()사용자 지정 코드 없이 COCO JSON으로 YOLO를 학습할 수 있나요?
기본적으로 YOLO
레이블을 기대하는 현재의 Ultralytics 파이프라인으로는 불가능합니다. 이 가이드는 필요한 최소한의 사용자 지정 코드(데이터셋 클래스 1개와 트레이너 클래스 1개)를 제공합니다. 정의가 완료되면 학습에는 표준 object detection 호출만 있으면 됩니다.YOLOv9는 객체 탐지 및 세그멘테이션 및 포즈 추정을 지원하나요?segmentation이 가이드는 segments을 다룹니다. , 지원을 추가하려면 각 레이블 사전의 keypoints 필드에 COCO 어노테이션의 GroundingDataset 소스 코드 다각형 데이터를 포함하세요.
의 경우
네. COCODataset을 포함하세요. YOLODataset은 세그먼트를 처리하기 위한 참조 구현을 제공합니다.이 사용자 지정 데이터셋에서 증강(Augmentation)이 작동하나요? — 은 , 을 확장하므로 모든 내장 , 데이터 증강mosaic
mixup
copy-pasteid 및 기타 기능이 수정 없이 실행됩니다.names카테고리 ID는 클래스 인덱스에 어떻게 매핑되나요?dataset.yaml카테고리는 categories별로 정렬되고 0부터 시작하는 순차 인덱스로 매핑됩니다. 이는 1부터 시작하는 ID(표준 COCO), 0부터 시작하는 ID, 비연속적인 ID를 모두 처리합니다.
사전에 변환된 레이블과 비교하여 성능 오버헤드가 있습니까?
COCO JSON은 첫 번째 학습 실행 시 한 번 구문 분석됩니다. 구문 분석된 레이블은 다음 파일에 저장됩니다..cache 따라서 이후 실행 시에는 재구문 분석 없이 즉시 로드됩니다. 주석(annotation)이 메모리에 유지되므로 학습 속도는 표준 YOLO 학습과 동일합니다. JSON 파일이 변경되면 캐시는 자동으로 다시 빌드됩니다.