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 the exact coordinate normalization formulas, a breakdown of VOC XML tags, and a zero-dependency Python script to convert entire directories of XML files into training-ready YOLO format.
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: $[x_{\min}, y_{\min}, x_{\max}, y_{\max}]$. YOLO uses normalized center coordinates and dimensions: $[x_{\text{center}}, y_{\text{center}}, w_{\text{norm}}, h_{\text{norm}}]$.
Given image dimensions $W$ (width) and $H$ (height):
$$x_{\text{center}} = \frac{x_{\min} + x_{\max}}{2 \times W}$$
$$y_{\text{center}} = \frac{y_{\min} + y_{\max}}{2 \times H}$$
$$w_{\text{norm}} = \frac{x_{\max} - x_{\min}}{W}$$
$$h_{\text{norm}} = \frac{y_{\max} - y_{\min}}{H}$$
3. Standalone Python Batch Converter Script
This script uses standard library modules (xml.etree.ElementTree, pathlib, glob) to recursively find all XML files, build the class index list, and generate normalized YOLO labels and a data.yaml file.
#!/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
# Clamp to [0, 1] range to avoid out-of-bounds training errors
return (
max(0.0, min(1.0, x_center)),
max(0.0, min(1.0, y_center)),
max(0.0, min(1.0, w)),
max(0.0, min(1.0, 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"):
# Check difficulty tag (optional: skip difficult items)
difficult = obj.find("difficult")
if difficult is not None and int(difficult.text) == 1:
pass # Can continue or include depending on your policy
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)
# Ensure coordinates are properly ordered
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
# Save classes.txt for reference
with open(out_path / "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:
- Drag and drop your XML files or ZIP archive into the LabelOp Format Converter.
- The tool automatically detects image sizes, extracts class taxonomies, and normalizes all bounding boxes.
- Download a ready-to-train YOLO ZIP archive with
labels/anddata.yaml.
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.