Ultralytics YOLO27: Join the waitlist.
Get Started
Train your first YOLO model·Data·Lesson 4/10
Lessonbeginner

Prepare a Custom Dataset

Lay out images and labels in the Ultralytics YOLO format, convert pixel boxes to normalized label lines, and catch broken labels before training.

A single misnamed folder or un-normalized label doesn't crash training. Ultralytics skips the broken images and keeps going, so you train on part of your data, get a mediocre model, and lose an afternoon looking for the cause in the wrong place.

This lesson covers the mechanical part of a detection dataset: folders, label lines, conversion, and checks. You'll work with the real 115-image medical pills dataset that the rest of this course trains on. Deciding what to collect and how to label it is covered in Annotation Best Practices.

Outcome

You can lay out a dataset in the Ultralytics YOLO format, convert pixel boxes into normalized label lines, and spot missing or corrupt labels from the scan line before training.

Fast Track
If you already know your way around, here's the short version.
  1. Create images/{train,val} and labels/{train,val} with matching file names.

  2. Write one class x_center y_center width height line per object, every value from 0 to 1.

  3. Convert pixel boxes by dividing by image width and height.

  4. Draw a label file on its image, and check the scan line says 0 corrupt.

Hands-on

Get the Practice Dataset#

Ultralytics Platform New Dataset dialog: a drop zone for images, videos, or YOLO, COCO, and NDJSON datasets, then name, URL, description, and task type

The layout below is also what Ultralytics Platform's New Dataset dialog accepts as a zip, so a dataset you prepare here uploads unchanged. Download 92 training and 23 validation frames of pills in blister packs, and unpack them into my_dataset:

curl -L -o medical-pills.zip https://github.com/ultralytics/assets/releases/download/v0.0.0/medical-pills.zip
unzip -q medical-pills.zip -d my_dataset
my_dataset/
├── images/
│   ├── train/   Frame_0.jpg, Frame_100.jpg, ... (92 images)
│   └── val/     Frame_144.jpg, ... (23 images)
└── labels/
    ├── train/   Frame_0.txt, Frame_100.txt, ... (92 files)
    └── val/     Frame_144.txt, ... (23 files)

There's no manifest. Ultralytics finds each label by replacing /images/ with /labels/ in the image path and the extension with .txt, so images/val/Frame_144.jpg reads labels/val/Frame_144.txt. Two consequences:

  • The folders must be named exactly images and labels, side by side.
  • An image with an empty or missing label file is a background image: valid, and useful for teaching the model what not to detect, but only when you meant it.

Read a Label Line#

A YOLO label file for zidane.jpg, one row per object: class index, then normalized x center, y center, width, and height

Each line is one object: an integer class index, then the bounding box center and size as fractions of the image width and height. A real line from labels/train/Frame_0.txt:

0 0.330729 0.816204 0.103125 0.121296
  • 0: class index 0, which the YAML will name pill.
  • 0.330729 0.816204: the box center sits 33% of the way across and 82% of the way down.
  • 0.103125 0.121296: the box is about 10% of the image wide and 12% tall.

Worked Example: Convert a Pixel Box#

Labeling tools often export corners in pixels (x1, y1, x2, y2). Divide by the image size. The leftmost person in the 810×1080 bus.jpg from the last lesson spans (49, 398) to (240, 902):

FieldFormulaValue
x_center(x1 + x2) / 2 / width(49 + 240) / 2 / 810 = 0.178395
y_center(y1 + y2) / 2 / height(398 + 902) / 2 / 1080 = 0.601852
width(x2 − x1) / width191 / 810 = 0.235802
height(y2 − y1) / height504 / 1080 = 0.466667

Most labeling tools, and tools like Roboflow, can export YOLO format directly. For COCO JSON, follow the COCO JSON training guide; the JSON2YOLO repository covers other formats. For Pascal VOC XML, the formula above fits in a short script:

import xml.etree.ElementTree as ET
from pathlib import Path

CLASSES = ["person", "bus"]  # index = class id; must match names in data.yaml

def voc_to_yolo(xml_file: Path, out_dir: Path) -> None:
    root = ET.parse(xml_file).getroot()
    w, h = int(root.find("size/width").text), int(root.find("size/height").text)
    lines = []
    for obj in root.iter("object"):
        name = obj.find("name").text
        if name not in CLASSES:
            continue
        box = obj.find("bndbox")
        x1, y1, x2, y2 = (float(box.find(k).text) for k in ("xmin", "ymin", "xmax", "ymax"))
        xc, yc, bw, bh = (x1 + x2) / 2 / w, (y1 + y2) / 2 / h, (x2 - x1) / w, (y2 - y1) / h
        lines.append(f"{CLASSES.index(name)} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}")
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / f"{xml_file.stem}.txt").write_text("\n".join(lines) + "\n")

voc_to_yolo(Path("bus.xml"), Path("labels"))
print(Path("labels/bus.txt").read_text())

Run on a bus.xml holding the person and bus boxes from the previous lesson, it prints the worked example and the bus:

0 0.178395 0.601852 0.235802 0.466667
1 0.495679 0.454167 0.991358 0.482407

Check Before You Train#

Draw a few label files on their images. A shifted class, swapped x and y, or boxes around shadows are obvious to the eye and invisible in metrics:

from ultralytics.data.utils import visualize_image_annotations

visualize_image_annotations(
    "my_dataset/images/train/Frame_0.jpg",
    "my_dataset/labels/train/Frame_0.txt",
    {0: "pill"},
)

Then let Ultralytics check every file. It scans labels the first time it reads a dataset and decides, line by line, whether each image is usable:

The scan line reports the result: 23 images, 0 backgrounds, 0 corrupt is what a healthy dataset looks like. Anything else is one of the mistakes below.

Avoid These Mistakes#

Forgotten normalization

Symptom: ignoring corrupt image/label: non-normalized or out of bounds coordinates [634. 881. 198. 163.] and a nonzero corrupt count. Cause: pixel values instead of fractions. Fix: divide by the image width and height. Any label value above 1 means the box was never normalized.

Labels folder misnamed

Symptom: yolo val scans 0 images, 23 backgrounds and warns No labels found, and training stops with ValueError: train: No labels found — here because the label folder was called annotations. Fix: name the folders images and labels exactly, with matching file names.

Class indices start at 1

Symptom: Label class 1 exceeds dataset class count 1. Possible class labels are 0-0. Cause: the export tool numbered classes from 1. Fix: YOLO classes start at 0; subtract 1 in the conversion script or fix the export setting.

Corners vs centers

If boxes draw shifted half a box down and to the right, you wrote x1 y1 w h instead of x_center y_center w h. Drawing one file catches this in seconds; no scan check can.

For systematic checks across a whole dataset, see Dataset Quality Control. Built-in sets such as coco8, coco128, and african-wildlife are good layouts to copy; the datasets overview lists them all. The trainer adds data augmentation on top of your labels, so a quick pass of data cleaning here pays off in every epoch.

Try It

Download the pill dataset into my_dataset. Deliver three checks: the image and label counts for each split, the output of a script that prints any label value outside 0–1 (it should print nothing), and one label file drawn on its image with visualize_image_annotations. If you have your own labeled images, convert 10 of them to the same layout and draw one.

Done When
You've finished the lesson when all of these are true.
  • my_dataset has images/train, images/val, labels/train, and labels/val with matching file names and counts.

  • Your check script finds no label value outside 0–1.

  • You've drawn at least one label file on its image and the boxes sit on the pills.

Show solution
for s in train val; do
  echo "$s: $(ls my_dataset/images/$s | wc -l) images, $(ls my_dataset/labels/$s | wc -l) labels"
done
# train: 92 images, 92 labels
# val:   23 images, 23 labels
from pathlib import Path

for txt in Path("my_dataset/labels").rglob("*.txt"):
    for line in txt.read_text().splitlines():
        values = [float(v) for v in line.split()[1:]]
        if any(v < 0 or v > 1 for v in values):
            print(txt, line)

No output from the script means every coordinate is normalized. Equal counts per split mean every image has a label file; the next lesson's scan line confirms there are no corrupt ones.

What's next

The folders are ready. Next, write the one YAML file that tells Ultralytics where they are and what the classes mean.