Skip to main content

데이터셋 포맷

학습 목표

  • COCO, Pascal VOC, YOLO 포맷의 구조와 차이를 설명할 수 있다
  • 각 포맷의 좌표 표현 방식을 이해하고 변환할 수 있다
  • 프로젝트에 적합한 포맷을 선택할 수 있다
  • 포맷 간 변환 코드를 작성할 수 있다

왜 중요한가

CV 모델마다 요구하는 데이터 포맷이 다릅니다. YOLO는 YOLO 포맷, Detectron2는 COCO 포맷, 일부 레거시 도구는 Pascal VOC를 사용합니다. 라벨링 도구에서 내보낸 데이터를 학습 프레임워크에 맞게 변환하는 능력은 CV 실무의 필수 역량입니다.

포맷 비교 요약

비교 항목COCOPascal VOCYOLO
파일 형식JSON (1개)XML (이미지당 1개)TXT (이미지당 1개)
좌표 형식절대 좌표 (x, y, w, h)절대 좌표 (xmin, ymin, xmax, ymax)정규화 좌표 (cx, cy, w, h)
좌표 기준좌상단좌상단~우하단중심
메타데이터풍부 (카테고리, 이미지 정보)개별 파일에 포함없음 (별도 yaml)
지원 태스크탐지, 세그멘테이션, 키포인트탐지탐지, 세그멘테이션, 포즈
대표 프레임워크Detectron2, MMDetection레거시 프레임워크ultralytics (YOLO)

COCO 포맷

COCO(Common Objects in Context) 포맷은 하나의 JSON 파일에 모든 어노테이션을 저장합니다.

디렉터리 구조

dataset/
├── images/
│   ├── train/
│   │   ├── 000001.jpg
│   │   └── 000002.jpg
│   └── val/
└── annotations/
    ├── instances_train.json
    └── instances_val.json

JSON 구조

{
  "images": [
    {
      "id": 1,
      "file_name": "000001.jpg",
      "width": 640,
      "height": 480
    }
  ],
  "annotations": [
    {
      "id": 1,
      "image_id": 1,
      "category_id": 1,
      "bbox": [100, 150, 200, 300],
      "area": 60000,
      "iscrowd": 0
    }
  ],
  "categories": [
    {"id": 1, "name": "person"},
    {"id": 2, "name": "car"}
  ]
}
COCO의 bbox[x_min, y_min, width, height] 형식이며, 좌상단 모서리 기준 절대 좌표입니다. 세그멘테이션 어노테이션은 segmentation 필드에 다각형 좌표 리스트로 저장됩니다.

Pascal VOC 포맷

Pascal VOC 포맷은 이미지마다 개별 XML 파일을 사용합니다.

디렉터리 구조

dataset/
├── JPEGImages/
│   ├── 000001.jpg
│   └── 000002.jpg
├── Annotations/
│   ├── 000001.xml
│   └── 000002.xml
└── ImageSets/
    └── Main/
        ├── train.txt
        └── val.txt

XML 구조

<annotation>
  <filename>000001.jpg</filename>
  <size>
    <width>640</width>
    <height>480</height>
    <depth>3</depth>
  </size>
  <object>
    <name>person</name>
    <bndbox>
      <xmin>100</xmin>
      <ymin>150</ymin>
      <xmax>300</xmax>
      <ymax>450</ymax>
    </bndbox>
  </object>
</annotation>

YOLO 포맷

YOLO 포맷은 이미지마다 텍스트 파일을 사용하며, 정규화된 중심 좌표를 사용합니다.

디렉터리 구조

dataset/
├── images/
│   ├── train/
│   │   ├── 000001.jpg
│   │   └── 000002.jpg
│   └── val/
├── labels/
│   ├── train/
│   │   ├── 000001.txt
│   │   └── 000002.txt
│   └── val/
└── dataset.yaml

레이블 파일 (TXT)

# class_id center_x center_y width height (모두 0~1 정규화)
0 0.3125 0.5208 0.3125 0.625
1 0.7500 0.3000 0.2000 0.4000

dataset.yaml

path: /path/to/dataset
train: images/train
val: images/val

nc: 2  # 클래스 수
names: ['person', 'car']

좌표 변환

좌표 형식 정리

변환 코드

def coco_to_yolo(bbox, img_w, img_h):
    """COCO [x_min, y_min, w, h] → YOLO [cx, cy, w, h] 정규화"""
    x_min, y_min, w, h = bbox
    cx = (x_min + w / 2) / img_w
    cy = (y_min + h / 2) / img_h
    return [cx, cy, w / img_w, h / img_h]

def yolo_to_coco(bbox, img_w, img_h):
    """YOLO [cx, cy, w, h] 정규화 → COCO [x_min, y_min, w, h]"""
    cx, cy, w, h = bbox
    x_min = (cx - w / 2) * img_w
    y_min = (cy - h / 2) * img_h
    return [x_min, y_min, w * img_w, h * img_h]

def voc_to_yolo(bbox, img_w, img_h):
    """VOC [x_min, y_min, x_max, y_max] → YOLO [cx, cy, w, h] 정규화"""
    x_min, y_min, x_max, y_max = bbox
    cx = (x_min + x_max) / 2 / img_w
    cy = (y_min + y_max) / 2 / img_h
    w = (x_max - x_min) / img_w
    h = (y_max - y_min) / img_h
    return [cx, cy, w, h]

def yolo_to_voc(bbox, img_w, img_h):
    """YOLO [cx, cy, w, h] 정규화 → VOC [x_min, y_min, x_max, y_max]"""
    cx, cy, w, h = bbox
    x_min = (cx - w / 2) * img_w
    y_min = (cy - h / 2) * img_h
    x_max = (cx + w / 2) * img_w
    y_max = (cy + h / 2) * img_h
    return [x_min, y_min, x_max, y_max]

COCO → YOLO 전체 변환 스크립트

import json
from pathlib import Path

def convert_coco_to_yolo(coco_json_path, output_dir):
    """COCO JSON을 YOLO TXT 포맷으로 변환합니다."""
    with open(coco_json_path) as f:
        coco = json.load(f)

    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    # 이미지 ID → 정보 매핑
    images = {img['id']: img for img in coco['images']}

    # 카테고리 ID → 인덱스 매핑 (0부터 시작)
    cat_map = {cat['id']: idx for idx, cat in enumerate(coco['categories'])}

    # 이미지별 어노테이션 수집
    annotations = {}
    for ann in coco['annotations']:
        img_id = ann['image_id']
        if img_id not in annotations:
            annotations[img_id] = []
        annotations[img_id].append(ann)

    # YOLO 포맷으로 변환
    for img_id, img_info in images.items():
        img_w = img_info['width']
        img_h = img_info['height']
        filename = Path(img_info['file_name']).stem

        lines = []
        for ann in annotations.get(img_id, []):
            cls_idx = cat_map[ann['category_id']]
            yolo_bbox = coco_to_yolo(ann['bbox'], img_w, img_h)
            line = f"{cls_idx} {' '.join(f'{v:.6f}' for v in yolo_bbox)}"
            lines.append(line)

        with open(output_dir / f"{filename}.txt", 'w') as f:
            f.write('\n'.join(lines))

    print(f"변환 완료: {len(images)}개 이미지")

# 사용 예시
convert_coco_to_yolo('annotations/instances_train.json', 'labels/train')

분류 데이터셋 구조

분류는 별도의 어노테이션 파일 없이 폴더 구조로 레이블을 표현합니다.
dataset/
├── train/
│   ├── cat/
│   │   ├── 001.jpg
│   │   └── 002.jpg
│   └── dog/
│       ├── 001.jpg
│       └── 002.jpg
└── val/
    ├── cat/
    └── dog/
from torchvision.datasets import ImageFolder

# 폴더명이 곧 클래스 레이블
dataset = ImageFolder('dataset/train/')
print(dataset.classes)     # ['cat', 'dog']
print(dataset.class_to_idx)  # {'cat': 0, 'dog': 1}
ultralytics YOLO를 사용한다면 YOLO 포맷, Detectron2를 사용한다면 COCO 포맷을 선택하세요. 라벨링 도구 대부분은 여러 포맷으로 내보내기를 지원하므로, 학습 프레임워크에 맞는 포맷을 선택하면 됩니다.
COCO 포맷은 segmentation 필드에 다각형 좌표를, YOLO 세그멘테이션은 레이블 파일에 class_id x1 y1 x2 y2 ... xn yn 형태로 다각형 좌표를 저장합니다. 별도의 마스크 이미지(PNG)로 저장하는 방식도 자주 사용됩니다.
iscrowd=1은 군중처럼 개별 객체를 구분하기 어려운 영역을 표시합니다. 학습 시 이 영역은 무시하거나 별도 처리됩니다. 일반적인 프로젝트에서는 모두 0으로 설정합니다.

체크리스트

  • COCO, VOC, YOLO 포맷의 디렉터리 구조를 안다
  • 각 포맷의 좌표 표현 방식 차이를 이해했다
  • 포맷 간 좌표 변환 로직을 작성할 수 있다
  • 분류 데이터셋의 폴더 구조를 안다
  • 프로젝트에 맞는 포맷을 선택할 수 있다

다음 문서