YOLO’s annotation format is deceptively simple. One text file per image. Five numbers per object. Everything normalised. This reference covers exactly what those numbers mean, how the directory structure works, and what to watch out for when converting from other formats.
The per-line format
Each line in a YOLO .txt annotation file describes one annotated object:
<class_id> <x_center> <y_center> <width> <height>
All values are space-separated. The class ID is an integer. The four geometry values are floating-point numbers between 0 and 1.
Normalised coordinates
YOLO uses normalised coordinates — values divided by the image dimension. For a 640×480 image:
- An x value is divided by 640
- A y value is divided by 480
This means the same annotation file works regardless of what resolution your model processes the image at.
x_center — horizontal centre of the bounding box, as a fraction of image width. A box centred at pixel 320 in a 640-wide image is 0.5.
y_center — vertical centre, as a fraction of image height. A box centred at pixel 240 in a 480-tall image is 0.5.
width — box width as a fraction of image width. A 128-pixel-wide box in a 640-wide image is 0.2.
height — box height as a fraction of image height. A 64-pixel-tall box in a 480-tall image is 0.1333....
So a box covering the full image would be: 0 0.5 0.5 1.0 1.0
Converting pixel coordinates to YOLO
def bbox_to_yolo(x1, y1, x2, y2, img_w, img_h):
"""Convert pixel bounding box to YOLO normalised format."""
cx = ((x1 + x2) / 2) / img_w
cy = ((y1 + y2) / 2) / img_h
w = (x2 - x1) / img_w
h = (y2 - y1) / img_h
return cx, cy, w, h
def yolo_to_bbox(cx, cy, w, h, img_w, img_h):
"""Convert YOLO normalised format to pixel bounding box."""
x1 = int((cx - w/2) * img_w)
y1 = int((cy - h/2) * img_h)
x2 = int((cx + w/2) * img_w)
y2 = int((cy + h/2) * img_h)
return x1, y1, x2, y2
Class IDs and data.yaml
Class IDs are zero-indexed integers that map to class names via a data.yaml file. The class ID in the annotation and the position in the names list in data.yaml must match exactly.
Example data.yaml:
path: /datasets/my_dataset
train: images/train
val: images/val
test: images/test # optional
nc: 3 # number of classes
names:
0: person
1: car
2: bicycle
An annotation line 1 0.5 0.5 0.3 0.6 describes a car (class 1) centred at (0.5, 0.5) with width 0.3 and height 0.6.
Common mistake: class IDs in annotation files and the position in names must stay in sync. If you reorder your names list, all annotations referring to those class IDs become incorrect.
Directory structure
YOLO training scripts expect a specific directory layout:
dataset/
├── images/
│ ├── train/
│ │ ├── frame_001.jpg
│ │ └── frame_002.jpg
│ └── val/
│ └── frame_003.jpg
├── labels/
│ ├── train/
│ │ ├── frame_001.txt
│ │ └── frame_002.txt
│ └── val/
│ └── frame_003.txt
└── data.yaml
The labels directory mirrors the images directory exactly. Each .txt file has the same stem as the corresponding image file. Images with no annotations have an empty .txt file (or no file — behaviour depends on the framework version).
Handling images with no annotations
An image in the training set with no objects should have an empty .txt file. Some frameworks handle missing annotation files by assuming zero objects; others error. The safest practice is to include empty annotation files for all images in the dataset.
YOLO segmentation format (YOLOv8+)
YOLOv8 added polygon segmentation support. The format adds normalised polygon coordinates after the class ID:
<class_id> <x1> <y1> <x2> <y2> <x3> <y3> ...
Each (x, y) pair is a normalised polygon vertex. There’s no fixed vertex count — the polygon closes implicitly.
RegionKit exports polygon annotations in this format when you select YOLO TXT export.
Converting from COCO to YOLO
import json
def coco_to_yolo(coco_path, output_dir, img_w, img_h):
with open(coco_path) as f:
coco = json.load(f)
cat_map = {c['id']: i for i, c in enumerate(coco['categories'])}
for ann in coco['annotations']:
img_id = ann['image_id']
x, y, w, h = ann['bbox'] # COCO bbox: top-left x, y, width, height
cx = (x + w/2) / img_w
cy = (y + h/2) / img_h
nw = w / img_w
nh = h / img_h
class_id = cat_map[ann['category_id']]
fname = f"{output_dir}/{img_id:06d}.txt"
with open(fname, 'a') as f:
f.write(f"{class_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}\n")
Reading YOLO files in Python
def read_yolo_annotations(txt_path, img_w, img_h):
annotations = []
try:
with open(txt_path) as f:
for line in f:
parts = line.strip().split()
if not parts:
continue
class_id = int(parts[0])
cx, cy, w, h = map(float, parts[1:5])
x1, y1, x2, y2 = yolo_to_bbox(cx, cy, w, h, img_w, img_h)
annotations.append({
'class_id': class_id,
'bbox': [x1, y1, x2, y2],
})
except FileNotFoundError:
pass # image has no annotations
return annotations
Exporting YOLO from RegionKit
RegionKit exports both bounding box and polygon YOLO annotations. Rectangle annotations become standard YOLO bounding box lines; polygon annotations become YOLO segmentation lines. The class label from the annotation’s label field is mapped to class IDs in order of first appearance.
Export: Export → YOLO TXT. The downloaded file is the annotation .txt for the loaded image.
Related: How to Annotate Images for YOLO Training · YOLO Annotation Tool Online