Skip to content

Data & convert

Bring a YOLO detection dataset into the COCO layout DFINE.train(data=...) and DFINE.val(data=...) consume — no manual reshuffling.

The two layouts

YOLO stores one .txt per image (class cx cy w h, normalized, class 0-indexed), with images under images/<split>/ and labels under the mirror labels/<split>/:

yolo/
  images/{train,val}/*.jpg
  labels/{train,val}/*.txt
  data.yaml                      # optional: class names + split paths

yolo_to_coco writes the COCO layout D-FINE trains on:

coco/
  train/  val/                   # images
  annotations/
    instances_train.json
    instances_val.json

Category ids stay 0-indexed (= the YOLO class id), so they line up with the model's contiguous labels under the default remap_mscoco_category=False.

Quickstart

from dfine import yolo_to_coco

written = yolo_to_coco("yolo/", "coco/")
# {"train": "coco/annotations/instances_train.json", "val": ".../instances_val.json"}

Then train straight on the output:

from dfine import DFINE
import json

num_classes = len(json.load(open(written["train"]))["categories"])
model = DFINE(size="s", num_classes=num_classes, imgsz=640)
model.train(data="coco/", epochs=100)

Or from the shell:

dfine convert yolo/ coco/ --names cat dog bird

Where splits and class names come from

  • Class names: an explicit class_names=[...] wins; otherwise data.yaml's names (list or {id: name} dict) is used; otherwise names are inferred as class_<i> from the label ids.
  • Splits: an explicit splits={...} wins; otherwise the train/val/test paths declared in data.yaml are used (resolved relative to the yaml, including the common Roboflow ../valid/images form); otherwise folders are auto-detected (images/<split> and <split>/images, with valid/validation accepted as val-split aliases). A split declared in data.yaml but not found on disk, or a missing val split, logs a warning instead of being silently dropped.

Roboflow / Ultralytics exports

These declare their splits in data.yaml (often val: ../valid/images) and name the validation folder valid. yolo_to_coco reads those paths and folder aliases directly, so a stock Roboflow export converts both splits with no extra flags.

Common variations

# Explicit class names (skip data.yaml)
yolo_to_coco("yolo/", "coco/", class_names=["cat", "dog", "bird"])

# Point at split image dirs yourself (relative to the root, or absolute)
yolo_to_coco("yolo/", "coco/", splits={"train": "images/train", "val": "images/val"})

# Symlink images instead of copying (saves disk on large datasets)
yolo_to_coco("yolo/", "coco/", copy_images=False)

# Rename the output split folders
yolo_to_coco("yolo/", "coco/", split_names={"train": "train2017", "val": "val2017"})

Segmentation-style rows (a class id followed by polygon points) are accepted too — their bounding box is derived. The converter is torch-free (only needs Pillow for image sizes, and PyYAML to read a data.yaml).

API

dfine.convert.yolo_to_coco

yolo_to_coco(yolo_root: str | PathLike, output_dir: str | PathLike, *, class_names: list[str] | None = None, splits: dict[str, str] | None = None, copy_images: bool = True, split_names: dict[str, str] | None = None) -> dict[str, str]

Convert a YOLO detection dataset to the COCO layout under output_dir.

Parameters:

Name Type Description Default
yolo_root str | PathLike

dataset root (with images/<split> + labels/<split>, and an optional data.yaml).

required
output_dir str | PathLike

where the COCO train//val/ + annotations/ are written (consumable directly by DFINE.train(data=output_dir)).

required
class_names list[str] | None

class names (index = class id). Falls back to data.yaml's names, then to inferred class_<i> if neither is available.

None
splits dict[str, str] | None

explicit {split: image_dir} (relative to yolo_root or absolute). Defaults to the train/val/test paths declared in data.yaml, then to auto-detecting images/<split> and <split>/images (valid is accepted as a val-split folder alias).

None
copy_images bool

copy images (default) or symlink them into the output.

True
split_names dict[str, str] | None

override the split→folder map (default train→train, val→val, test→test).

None

Returns:

Type Description
dict[str, str]

{output_split_name: annotation_json_path} for each converted split.

Source code in dfine/convert.py
def yolo_to_coco(
    yolo_root: str | os.PathLike,
    output_dir: str | os.PathLike,
    *,
    class_names: list[str] | None = None,
    splits: dict[str, str] | None = None,
    copy_images: bool = True,
    split_names: dict[str, str] | None = None,
) -> dict[str, str]:
    """Convert a YOLO detection dataset to the COCO layout under ``output_dir``.

    Args:
        yolo_root: dataset root (with ``images/<split>`` + ``labels/<split>``, and an
            optional ``data.yaml``).
        output_dir: where the COCO ``train/``/``val/`` + ``annotations/`` are written
            (consumable directly by ``DFINE.train(data=output_dir)``).
        class_names: class names (index = class id). Falls back to ``data.yaml``'s
            ``names``, then to inferred ``class_<i>`` if neither is available.
        splits: explicit ``{split: image_dir}`` (relative to ``yolo_root`` or absolute).
            Defaults to the ``train``/``val``/``test`` paths declared in ``data.yaml``,
            then to auto-detecting ``images/<split>`` and ``<split>/images`` (``valid``
            is accepted as a val-split folder alias).
        copy_images: copy images (default) or symlink them into the output.
        split_names: override the split→folder map (default ``train→train``,
            ``val→val``, ``test→test``).

    Returns:
        ``{output_split_name: annotation_json_path}`` for each converted split.
    """
    yolo_root = Path(yolo_root)
    output_dir = Path(output_dir)
    split_names = split_names or _DEFAULT_SPLIT_NAMES

    names = _resolve_class_names(yolo_root, class_names)
    split_dirs = _detect_splits(yolo_root, splits)
    (output_dir / "annotations").mkdir(parents=True, exist_ok=True)

    num_classes_seen: list[int] = []
    coco_splits: dict[str, dict] = {}
    for split, image_dir in split_dirs.items():
        out_name = split_names.get(split, split)
        coco_splits[out_name] = _convert_split(
            image_dir, output_dir / out_name, num_classes_seen, copy_images
        )
        logger.info(
            "converted %s: %d images, %d annotations",
            split,
            len(coco_splits[out_name]["images"]),
            len(coco_splits[out_name]["annotations"]),
        )

    if names is None:
        n = (max(num_classes_seen) + 1) if num_classes_seen else 0
        names = [f"class_{i}" for i in range(n)]
    elif num_classes_seen and max(num_classes_seen) >= len(names):
        raise ValueError(
            f"labels reference class id {max(num_classes_seen)} but only {len(names)} class "
            f"name(s) were provided — pass class_names with at least "
            f"{max(num_classes_seen) + 1} entries, or omit it to infer names from the labels."
        )
    categories = [{"id": i, "name": name} for i, name in enumerate(names)]

    written: dict[str, str] = {}
    for out_name, coco in coco_splits.items():
        coco["categories"] = categories
        ann_path = output_dir / "annotations" / f"instances_{out_name}.json"
        ann_path.write_text(json.dumps(coco))
        written[out_name] = str(ann_path)
    return written