Monday, November 2, 2026

How to Train YOLO on Your Own Images: A Step-by-Step Python Tutorial

How can you make an object detector recognize classes from your own labelled images? In this guide we fine-tune a pretrained YOLOv8n model on a small dataset, then inspect a prediction with Python.

Our example uses 12 synthetic training images and 4 synthetic validation images containing boats and buoys. In one local CPU run, the model predicted two boats and one buoy on the selected validation image. This is an educational example: the validation set helped select the checkpoint, and the result does not establish accuracy on independent real-world data.

Watch the tutorial

Watch the full Python and YOLO tutorial, or see the short demonstration.

Actual YOLO prediction on synthetic validation image showing two boats and one buoy
Actual prediction from the recorded CPU run on val_02.png. This is a validation image, not an independent final test.

What you will build

  1. Organize labelled images into training and validation folders.
  2. Understand the normalized YOLO bounding-box format.
  3. Point a dataset configuration at those folders.
  4. Fine-tune a local pretrained model and load the saved checkpoint.
  5. Run a prediction and interpret its limitations.

This is fine-tuning: it starts from pretrained weights, rather than initializing a detector from scratch. You can use your own appropriately licensed images and annotations. The particular synthetic fixture shown here is supplied with lesson 29 of my Computer Vision: OpenCV, Machine Learning and YOLO course.

1. Install the dependencies

The recorded experiment ran on Windows 11 using Python 3.13.9, Ultralytics 8.4.116 and PyTorch 2.13.0+cpu. The commands below create a Windows environment and pin the direct Ultralytics dependency; they do not lock every transitive dependency. Other versions, operating systems and hardware may yield different results.

py -3.13 -m venv .venv
.venv\Scripts\python.exe -m pip install --upgrade pip
.venv\Scripts\python.exe -m pip install ultralytics==8.4.116

Installing packages and obtaining the initial checkpoint need an internet connection unless you already have the required files cached. The training demonstration itself used local images and local weights. Your environment's dependencies and model files must be available first.

2. Organize images and annotations

Create this layout next to the training script. Every image has a corresponding label file with the same base name:

custom-yolo/
├── train_yolo.py
├── yolov8n.pt
└── synthetic_harbor_dataset/
    ├── data.yaml
    ├── images/
    │   ├── train/  (e.g. train_00.png)
    │   └── val/    (e.g. val_02.png)
    └── labels/
        ├── train/  (e.g. train_00.txt)
        └── val/    (e.g. val_02.txt)

For example, train_00.png corresponds to train_00.txt. For a different dataset, change the image paths and class names to match your data. Do not place images without permission into your project or public downloads.

Read one YOLO annotation row

Each row describes one object using five values:

class_index x_center y_center width height

The index identifies a class; the remaining four numbers are normalized by the image width or height and typically range from 0 to 1. In this fixture, the first row of labels/train/train_00.txt is:

0 0.271484 0.691406 0.277344 0.187500

Class 0 means boat. The center is approximately (0.271484, 0.691406); its width and height are 0.277344 and 0.187500. The input is 256 × 256 pixels, so the x center is approximately 0.271484 × 256 = 69.5 pixels from the left edge. The rectangle below is computed from this exact row, rather than drawn by eye.

Synthetic harbor training image with the first boat annotation drawn from its YOLO label and its center marked
One image and the box defined by the example row. Inspect all objects, not just a single row, before training.

Create and check a label file from pixel coordinates

Here is a small, reproducible way to turn pixel coordinates into a YOLO label. This example uses the synthetic train_00.png image from the course fixture. Its four rectangles contain two boats and two buoys. For this demonstration we reconstructed their pixel corners from the fixture’s existing annotations; they are not newly measured by an annotation tool. For your own image, determine the correct rectangle for every object first—for example using a suitable annotation tool—and use its pixel corners in place of these values.

For each box, left and right are horizontal pixel positions, and top and bottom are vertical positions measured from the upper-left corner. The code divides horizontal values by the image width and vertical values by its height. Class 0 is boat; class 1 is buoy. Use the same numeric mapping in your data.yaml.

Inside the project folder from step 1, create a separate annotation-demo folder and copy the example image to annotation-demo/input/train_00.png. Save the complete script below as annotation-demo/annotate_example.py. This image is 256 × 256 pixels; adapt the dimensions check, filename, class IDs, coordinates and class names to use a different dataset.

custom-yolo/
├── .venv/
└── annotation-demo/
    ├── annotate_example.py
    └── input/
        └── train_00.png

The script uses Pillow, included in the demonstrated environment. If it is not installed in your environment, run .venv\Scripts\python.exe -m pip install pillow from the project folder. Then, still from the project folder, run:

.venv\Scripts\python.exe annotation-demo\annotate_example.py

Complete example source: copyright © 2026 ARTSCI — Author of the AI/ML/CV course materials, licensed under GNU AGPL version 3 only, without warranty. The sample image and other course media are separate assets and are not automatically licensed under this code notice.

"""Create a new YOLO label from pixel corners; never overwrite an existing label."""
from pathlib import Path
from PIL import Image, ImageDraw

image_path = Path(__file__).parent / 'input' / 'train_00.png'
output_dir = Path(__file__).parent / 'output'
image = Image.open(image_path).convert('RGB')
width, height = image.size
assert (width, height) == (256, 256)

# class_id, left, top, right, bottom in pixels. Origin: upper-left.
# These corners were reconstructed from this fixture's existing annotations.
boxes = [
    (0, 34, 153, 105, 201),
    (0, 118, 122, 181, 172),
    (1, 46, 189, 66, 209),
    (1, 186, 149, 212, 175),
]
rows = []
for class_id, left, top, right, bottom in boxes:
    assert class_id in (0, 1) and 0 <= left < right <= width and 0 <= top < bottom <= height
    x_center = (left + right) / (2 * width)
    y_center = (top + bottom) / (2 * height)
    box_width = (right - left) / width
    box_height = (bottom - top) / height
    rows.append(f'{class_id} {x_center:.6f} {y_center:.6f} {box_width:.6f} {box_height:.6f}')

output_dir.mkdir(parents=True, exist_ok=True)
label_path = output_dir / f'{image_path.stem}.txt'
with label_path.open('x', encoding='utf-8', newline='\n') as label_file:
    label_file.write('\n'.join(rows) + '\n')

# Reconstruct the saved boxes for visual verification of the written bytes.
saved_rows = label_path.read_text(encoding='utf-8').splitlines()
assert len(saved_rows) == len(boxes)
overlay = image.copy()
draw = ImageDraw.Draw(overlay)
for row in saved_rows:
    cls, xc, yc, bw, bh = row.split()
    xc, yc, bw, bh = map(float, (xc, yc, bw, bh))
    rect = ((xc-bw/2)*width, (yc-bh/2)*height, (xc+bw/2)*width, (yc+bh/2)*height)
    draw.rectangle(rect, outline='#ffdb40' if cls == '0' else '#5effac', width=2)
overlay.save(output_dir / 'train_00_saved_labels.png')
print('Input:', image_path.name, width, 'x', height)
print('New label:', label_path.name)
print('Saved rows:')
print('\n'.join(saved_rows))
print('Verified overlay:', 'train_00_saved_labels.png')

The script creates annotation-demo/output/train_00.txt with four lines and annotation-demo/output/train_00_saved_labels.png, which draws the rectangles reconstructed from the saved label file. The first line is 0 0.271484 0.691406 0.277344 0.187500. Inspect the overlay and correct any missing or misplaced boxes before using a label for training.

The file is opened in exclusive-create mode to protect existing labels. Running the same demo again with the same output path raises FileExistsError. To repeat it, use a new demo directory or a different output filename; do not overwrite a label without reviewing it. Once your annotations for the entire image are complete and checked, put the label file under the matching labels/train or labels/val directory with the same base name as the image.

3. Create the dataset configuration

For the example layout, save this as synthetic_harbor_dataset/data.yaml:

train: images/train
val: images/val
names:
  0: boat
  1: buoy

The training and validation paths identify separate groups of images. The names mapping defines the meaning of each class index in this custom dataset. Check paths and labels against your actual installed Ultralytics version and project layout; the example was run with the YAML beside these folders.

4. Obtain the starting weights

Download the official YOLOv8n checkpoint as a separate setup step and place it next to train_yolo.py. The file used in the demonstration has SHA-256 f59b3d833e2ff32e194b5bb8e08d211dc7c5bdf144b90d2c8412c47ccfc83b36. Review the model asset license and Ultralytics licensing information for your use. A checksum identifies bytes; it does not by itself grant usage rights.

5. Train and predict with the complete script

Save the following as train_yolo.py in the project folder. It checks for the local checkpoint, dataset YAML and selected validation image, fine-tunes the checkpoint, loads the selected best.pt file, and saves the prediction as prediction_val_02.jpg.

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 ARTSCI - Author of the AI/ML/CV course materials.
"""Fine-tune a local YOLO checkpoint on an annotated dataset and inspect a validation image."""
from pathlib import Path

import cv2
from ultralytics import YOLO


def main():
    root = Path(__file__).resolve().parent
    dataset = root / "synthetic_harbor_dataset"
    data_yaml = dataset / "data.yaml"
    validation_image = dataset / "images" / "val" / "val_02.png"
    starting_weights = root / "yolov8n.pt"

    for file_path in (data_yaml, validation_image, starting_weights):
        if not file_path.is_file():
            raise FileNotFoundError(f"Required local file: {file_path}")

    model = YOLO(str(starting_weights))
    training = model.train(
        data=str(data_yaml), epochs=50, imgsz=256, batch=4, workers=0,
        device="cpu", project=str(root / "runs"), name="custom_yolo",
        exist_ok=False, verbose=False, plots=False, mosaic=0.0,
        seed=0, deterministic=True,
    )

    best_weights = Path(training.save_dir) / "weights" / "best.pt"
    if not best_weights.is_file():
        raise FileNotFoundError(f"Training did not produce a checkpoint: {best_weights}")
    final_model = YOLO(str(best_weights))
    result = final_model(
        str(validation_image), conf=0.25, iou=0.30, device="cpu", verbose=False,
    )[0]

    print(f"Best checkpoint: {best_weights}")
    for box in result.boxes:
        class_name = result.names[int(box.cls.item())]
        print(f"{class_name}: confidence {float(box.conf.item()):.3f}")
    output = root / "prediction_val_02.jpg"
    if not cv2.imwrite(str(output), result.plot(line_width=1, font_size=8)):
        raise OSError(f"Could not save prediction image: {output}")
    print(f"Saved validation-image prediction: {output}")


if __name__ == "__main__":
    main()

Complete sample source above: copyright © 2026 ARTSCI — Author of the AI/ML/CV course materials, licensed under GNU AGPL version 3, without warranty. Preserve applicable notices and provide corresponding source when sharing covered modifications. Ultralytics software and model weights retain their own terms. Dataset images, course media and other independent assets are not automatically licensed under this code notice.

From the project folder, run:

.venv\Scripts\python.exe train_yolo.py

data selects the YAML. epochs=50 means 50 passes through the training set; imgsz=256 selects the processing size, batch=4 controls batch size, and device="cpu" chooses the CPU. The seed and deterministic setting help reproduce this particular example, but they do not guarantee bit-identical results across every environment. Training duration depends on the computer.

6. Inspect the actual result

In the recorded run, prediction on val_02.png at conf=0.25 and iou=0.30 returned two boat boxes and one buoy box. These matched the three labelled classes in that particular image. The checkpoint and image shown in the video belong to that run; a new run can produce different confidences or boxes.

A box confidence is the model's score for that prediction, not a measurement of how often the system will be correct in real life. The four validation images were used during training and checkpoint selection. Their prediction and validation metrics are therefore not an independent final test.

7. Troubleshoot the common problems

  • Required local file is missing: check the project layout and filenames, including the checkpoint and data.yaml. For your own dataset, change validation_image to an existing image.
  • Images or labels are not found: check matching filename stems and whether train and val resolve to the intended folders.
  • Boxes look wrong: check that the annotations use normalized center coordinates and width/height, rather than pixel corners. Draw them on the source image before training.
  • No box appears: inspect your labels, classes and input, then investigate the confidence threshold and training output. Lowering a threshold changes which predictions appear; it does not guarantee correct detections.
  • Training is slow: CPU runtime depends on your hardware, configuration and dataset. Reduce or adjust a test run deliberately, and report the actual settings when comparing results.

What to do next

Try adding an image you have permission to use and manually checking every annotation. For a real application, collect representative examples across cameras and lighting, prevent similar images from leaking between splits, inspect errors, and evaluate on a separate test set that was not used for training or checkpoint selection. A tiny synthetic fixture teaches the steps; it does not establish production reliability.

For another application of these ideas, read how a Parking Space Counter assigns detections to spaces. You can explore more practical projects in my Computer Vision: OpenCV, Machine Learning and YOLO course.