Skip to main content
Blog
Tutorial
Aug 19, 20263 min

Pascal VOC XML to YOLO TXT: Complete Migration Guide (with Python Script)

Migrate legacy Pascal VOC XML annotations to normalized YOLO format for YOLOv8, YOLOv11, and YOLOv12. Includes coordinate math, XML parsing, and a batch Python converter.

The Pascal VOC format (XML files with <bndbox> tags) has been a cornerstone of computer vision research for over fifteen years.

However, modern deep learning frameworks — including Ultralytics YOLO (v8, v11, v12) — require per-image .txt files with normalized center coordinates.

This guide provides coordinate normalization formulas, a breakdown of VOC XML tags, and a zero-dependency Python script to convert a directory of XML files into YOLO label files. You still need matching images, a train/validation split, and a dataset YAML before training.

Need an instant in-browser conversion? Try our Pascal VOC to YOLO Converter Tool.


1. Pascal VOC XML vs. YOLO TXT Structure

Pascal VOC XML Example (image_001.xml)

<annotation>
  <filename>image_001.jpg</filename>
  <size>
    <width>800</width>
    <height>600</height>
    <depth>3</depth>
  </size>
  <object>
    <name>pedestrian</name>
    <pose>Unspecified</pose>
    <truncated>0</truncated>
    <difficult>0</difficult>
    <bndbox>
      <xmin>120</xmin>
      <ymin>80</ymin>
      <xmax>240</xmax>
      <ymax>320</ymax>
    </bndbox>
  </object>
</annotation>

Corresponding YOLO TXT (image_001.txt)

0 0.225000 0.333333 0.150000 0.400000

2. Coordinate Conversion Mathematics

Pascal VOC uses corner coordinates: [xmin, ymin, xmax, ymax]. YOLO uses normalized center coordinates and dimensions: [x_center, y_center, width, height].

Given image dimensions $W$ (width) and $H$ (height):

x_center = (xmin + xmax) / (2 * W)
y_center = (ymin + ymax) / (2 * H)
width    = (xmax - xmin) / W
height   = (ymax - ymin) / H

3. Standalone Python Batch Converter Script

This script uses standard library modules (xml.etree.ElementTree and pathlib) to read XML files in one directory, build a class index, and generate YOLO labels plus a classes.txt reference file outside the labels folder. It does not copy images or create data.yaml.

#!/usr/bin/env python3
"""
voc_to_yolo.py
Converts Pascal VOC XML annotations to YOLO TXT format.
Zero external dependencies required.
"""

import xml.etree.ElementTree as ET
from pathlib import Path
from typing import List, Dict, Tuple


def convert_voc_box(
    size: Tuple[int, int],
    box: Tuple[float, float, float, float]
) -> Tuple[float, float, float, float]:
    """Convert VOC xmin, ymin, xmax, ymax to YOLO center_x, center_y, width, height."""
    dw = 1.0 / size[0]
    dh = 1.0 / size[1]
    
    xmin, ymin, xmax, ymax = box
    
    # Calculate center and dimensions
    x_center = (xmin + xmax) / 2.0
    y_center = (ymin + ymax) / 2.0
    w = xmax - xmin
    h = ymax - ymin
    
    # Normalize by image width and height
    x_center = x_center * dw
    w = w * dw
    y_center = y_center * dh
    h = h * dh
    
    return x_center, y_center, w, h


def batch_convert_voc_to_yolo(
    voc_xml_dir: str,
    output_labels_dir: str,
    classes_list: List[str] = None
):
    xml_path = Path(voc_xml_dir)
    out_path = Path(output_labels_dir)
    out_path.mkdir(parents=True, exist_ok=True)

    xml_files = list(xml_path.glob("*.xml"))
    print(f"Found {len(xml_files)} VOC XML files in {xml_path}")

    # Discover classes automatically if not provided
    discovered_classes = set()
    if classes_list is None:
        for xml_file in xml_files:
            tree = ET.parse(xml_file)
            root = tree.getroot()
            for obj in root.iter("object"):
                cls_name = obj.find("name").text.strip()
                discovered_classes.add(cls_name)
        classes_list = sorted(list(discovered_classes))

    class_to_id = {cls_name: idx for idx, cls_name in enumerate(classes_list)}
    print(f"Classes ({len(classes_list)}): {class_to_id}")

    total_boxes = 0
    converted_files = 0

    for xml_file in xml_files:
        tree = ET.parse(xml_file)
        root = tree.getroot()

        # Extract image dimensions
        size_node = root.find("size")
        if size_node is None:
            continue
        
        width = int(size_node.find("width").text)
        height = int(size_node.find("height").text)

        if width <= 0 or height <= 0:
            continue

        yolo_lines = []

        for obj in root.iter("object"):
            cls_name = obj.find("name").text.strip()
            if cls_name not in class_to_id:
                continue

            cls_id = class_to_id[cls_name]
            xml_box = obj.find("bndbox")
            
            xmin = float(xml_box.find("xmin").text)
            ymin = float(xml_box.find("ymin").text)
            xmax = float(xml_box.find("xmax").text)
            ymax = float(xml_box.find("ymax").text)

            # Clip corners before normalization; reject boxes outside the image.
            xmin = max(0.0, min(width, xmin))
            ymin = max(0.0, min(height, ymin))
            xmax = max(0.0, min(width, xmax))
            ymax = max(0.0, min(height, ymax))
            if xmax <= xmin or ymax <= ymin:
                continue

            yolo_box = convert_voc_box((width, height), (xmin, ymin, xmax, ymax))
            yolo_lines.append(f"{cls_id} {yolo_box[0]:.6f} {yolo_box[1]:.6f} {yolo_box[2]:.6f} {yolo_box[3]:.6f}")
            total_boxes += 1

        # Write output .txt
        txt_filename = out_path / f"{xml_file.stem}.txt"
        with open(txt_filename, "w", encoding="utf-8") as f:
            f.write("\n".join(yolo_lines) + ("\n" if yolo_lines else ""))

        converted_files += 1

    # Keep the class list outside the labels folder.
    with open(out_path.parent / "classes.txt", "w", encoding="utf-8") as f:
        f.write("\n".join(classes_list) + "\n")

    print(f"Successfully converted {converted_files} files with {total_boxes} boxes.")
    print(f"Saved to: {out_path.resolve()}")


if __name__ == "__main__":
    import sys
    if len(sys.argv) < 3:
        print("Usage: python voc_to_yolo.py <voc_xml_dir> <output_labels_dir>")
    else:
        batch_convert_voc_to_yolo(sys.argv[1], sys.argv[2])

4. Converting In-Browser Without Scripts

If you do not want to set up local Python environments or install command-line tools:

  1. Drag and drop your XML files or ZIP archive into the LabelOp Format Converter.
  2. The tool automatically detects image sizes, extracts class taxonomies, and normalizes all bounding boxes.
  3. Download the converted YOLO label files and class list. Pair each label with its source image, create separate train and validation folders, and write data.yaml before training.

For an Ultralytics dataset, place matching image and label stems under images/train, labels/train, images/val, and labels/val. A minimal data.yaml is:

path: ./dataset
train: images/train
val: images/val
names:
  0: pedestrian

Replace the class names with the order in classes.txt, and validate the split and boxes before training.


Summary

Migrating from Pascal VOC to YOLO is straightforward once coordinate normalization is applied. Use the script above for automated local pipelines or use LabelOp Tools for zero-setup browser conversions.

Take the next step with your dataset.

Use local AI, managed cloud labeling, and team review in LabelOp. Export your work or train with your own Vast.ai account.

Use Web Converter