Skip to content

DFINE

The public, backend-agnostic detector class: build from typed params, load released weights, and predict / train / val / export / benchmark. Backend details never leak through its kwargs.

For task-oriented, copy-paste recipes see the examples cookbook and the runnable templates/.

Build a model

from dfine import DFINE, DFINEConfig

# From a released checkpoint — size + num_classes inferred, weights downloaded + loaded.
model = DFINE.from_pretrained("dfine-s")

# From a size preset (ImageNet backbone); every field is overridable inline.
model = DFINE(size="l", num_classes=80, imgsz=640, device="cuda")

# From a bare size + your own weights.
model = DFINE(size="m", num_classes=3).load("runs/train/best.pth")

# From a fully custom config object.
cfg = DFINEConfig.preset("s", num_classes=3, class_names=["cat", "dog", "bird"])
model = DFINE(config=cfg)

imgsz is a build-time choice

The encoder's positional embeddings are precomputed for the model's imgsz, so predict/export/benchmark must use that same size. To run at a different resolution, rebuild: DFINE(size=…, imgsz=…).

Predict

results = model.predict("street.jpg", conf=0.4)  # list[Results], one per image
r = results[0]
r.boxes.xyxy, r.boxes.conf, r.boxes.cls  # tensors, original-image scale
r.save("out.jpg")

# Folders, globs, and lists all work; save flags write a run dir.
model.predict("images/", save=True, save_txt=True, save_crop=True)

See Results & Boxes for the returned containers.

Train, validate, export

# Fine-tune on a COCO root (loaders built for you). Multi-GPU: devices=N.
model.train(data="coco/", epochs=72, batch_size=8, devices=2)

# 12 COCO metrics (+ optional analytics plots).
metrics = model.val(data="coco/", plots=True)
print(metrics["AP"])

# Deployable graph (postprocessor fused in).
model.export(format="onnx", simplify=True)

Inspect and time

model.info(verbose=True)  # layers / params / gradients / GFLOPs
model.benchmark(runs=100, batch=1)  # {"ms_per_image", "fps", "device", ...}

Video

model.predict_video("in.mp4", output="out.mp4", track=True)  # annotated mp4 + IDs
for r in model.predict_video("in.mp4", stream=True):  # per-frame Results
    ...

API reference

dfine.model.DFINE

DFINE(size: str | None = None, *, config: DFINEConfig | None = None, weights: str | PathLike | None = None, device: str | device | None = None, **params)

Config-first D-FINE detector with an ultralytics-style predict.

Source code in dfine/model.py
def __init__(
    self,
    size: str | None = None,
    *,
    config: DFINEConfig | None = None,
    weights: str | os.PathLike | None = None,
    device: str | torch.device | None = None,
    **params,
):
    if config is not None:
        if size is not None or params:
            raise ValueError("Pass either `config=` or `size=`/kwargs, not both.")
        self.config = config
    else:
        self.config = DFINEConfig.preset(size, **params) if size else DFINEConfig(**params)
    self.device = _resolve_device(self.config.device if device is None else device)
    self.names = _build_names(self.config)

    from .backends.native import DFINE as _NativeDFINE

    self.model = _NativeDFINE.from_config(self.config).to(self.device).eval()
    if self.config.task == "sem_seg":
        from .backends.native import SemSegPostProcessor

        self.postprocessor = SemSegPostProcessor.from_config(self.config).to(self.device).eval()
    else:
        from .backends.native import DFINEPostProcessor

        self.postprocessor = DFINEPostProcessor.from_config(self.config).to(self.device).eval()

    if weights is not None:
        self.load(weights)

from_pretrained classmethod

from_pretrained(name: str, device: str | device | None = None, **overrides) -> DFINE

Build a model matching a released checkpoint and load its weights.

name is a catalogue entry ("dfine-s", "dfine-l-obj365" ...); the size and num_classes are taken from it. See dfine models.

Source code in dfine/model.py
@classmethod
def from_pretrained(
    cls, name: str, device: str | torch.device | None = None, **overrides
) -> DFINE:
    """Build a model matching a released checkpoint and load its weights.

    ``name`` is a catalogue entry (``"dfine-s"``, ``"dfine-l-obj365"`` ...); the
    size and ``num_classes`` are taken from it. See ``dfine models``.
    """
    from .registry import config_for, resolve

    spec = resolve(name)
    cfg = config_for(spec, **{"backbone_pretrained": False, **overrides})
    model = cls(config=cfg, device=device)
    model.load(name)
    return model

load

load(weights: str | PathLike, use_ema: bool = True) -> DFINE

Load weights into the model, in place.

weights is either a catalogue name (downloaded + cached) or a local .pth path. Returns self for chaining: DFINE(size="s").load("dfine-s").

Source code in dfine/model.py
def load(self, weights: str | os.PathLike, use_ema: bool = True) -> DFINE:
    """Load weights into the model, in place.

    ``weights`` is either a catalogue name (downloaded + cached) or a local
    ``.pth`` path. Returns ``self`` for chaining: ``DFINE(size="s").load("dfine-s")``.
    """
    from .backends.native.loader import load_checkpoint
    from .downloads import download_weights
    from .registry import CHECKPOINTS

    if isinstance(weights, str) and weights.lower() in CHECKPOINTS:
        path = download_weights(weights.lower())
    else:
        path = Path(weights)
        if not path.exists():
            raise FileNotFoundError(
                f"{weights!r} is neither a known checkpoint name nor an existing file."
            )
    load_checkpoint(self.model, path, use_ema=use_ema, strict=True)
    self.model.to(self.device)
    return self

predict

predict(source, conf: float = 0.25, imgsz: int | None = None, mask_thresh: float = 0.5, *, save: bool = False, save_txt: bool = False, save_crop: bool = False, save_conf: bool = False, project: str = 'runs/detect', name: str = 'predict') -> list[Results]

Detect objects in source (path / PIL / array, or a list of them).

A source string that is a directory runs over all images in it (sorted), and one with glob magic (e.g. "imgs/*.jpg") over the matches; a list may mix folders, globs and explicit images. An empty directory/glob raises FileNotFoundError.

Returns one :class:~dfine.results.Results per image; boxes are in the original pixel scale. conf drops low-scoring detections. For a task="segment" model, each result also carries per-instance :class:~dfine.results.Masks (original scale), thresholded at mask_thresh. For a task="sem_seg" model each result instead carries a :class:~dfine.results.SemSeg label map (uint8, original scale) and no boxes.

Set any of save (annotated image), save_txt (YOLO-format labels under labels/; save_conf appends the score) or save_crop (per-detection crops under crops/) to write results to a fresh run directory project/name (auto-incremented to predict2, predict3, … so runs never clobber). Filenames come from each source path's stem, else image{i}.

Source code in dfine/model.py
@torch.no_grad()
def predict(
    self,
    source,
    conf: float = 0.25,
    imgsz: int | None = None,
    mask_thresh: float = 0.5,
    *,
    save: bool = False,
    save_txt: bool = False,
    save_crop: bool = False,
    save_conf: bool = False,
    project: str = "runs/detect",
    name: str = "predict",
) -> list[Results]:
    """Detect objects in ``source`` (path / PIL / array, or a list of them).

    A ``source`` string that is a **directory** runs over all images in it (sorted), and
    one with glob magic (e.g. ``"imgs/*.jpg"``) over the matches; a list may mix folders,
    globs and explicit images. An empty directory/glob raises ``FileNotFoundError``.

    Returns one :class:`~dfine.results.Results` per image; boxes are in the
    original pixel scale. ``conf`` drops low-scoring detections. For a
    ``task="segment"`` model, each result also carries per-instance
    :class:`~dfine.results.Masks` (original scale), thresholded at ``mask_thresh``.
    For a ``task="sem_seg"`` model each result instead carries a
    :class:`~dfine.results.SemSeg` label map (uint8, original scale) and no boxes.

    Set any of ``save`` (annotated image), ``save_txt`` (YOLO-format labels under
    ``labels/``; ``save_conf`` appends the score) or ``save_crop`` (per-detection
    crops under ``crops/``) to write results to a fresh run directory
    ``project/name`` (auto-incremented to ``predict2``, ``predict3``, … so runs never
    clobber). Filenames come from each source path's stem, else ``image{i}``.
    """
    sources = _resolve_sources(source)
    if not sources:
        raise FileNotFoundError(f"no images found for source {source!r}")
    images = [_to_pil(s) for s in sources]
    size = imgsz or self.config.imgsz
    if size != self.config.imgsz:
        raise ValueError(
            f"predict(imgsz={size}) must equal the model's imgsz ({self.config.imgsz}): the "
            "encoder's positional embeddings are precomputed for that resolution. Build the "
            f"model at this size instead — DFINE(size=..., imgsz={size})."
        )
    transform = T.Compose([T.Resize((size, size)), T.ToTensor()])

    batch = torch.stack([transform(im) for im in images]).to(self.device)
    orig_sizes = torch.tensor([[im.width, im.height] for im in images], device=self.device)

    outputs = self.model(batch)
    if self.config.task == "sem_seg":
        label_maps = self.postprocessor(outputs, orig_sizes)
        results = [self._to_semseg_results(im, m) for im, m in zip(images, label_maps)]
    else:
        detections = self.postprocessor(outputs, orig_sizes)
        pred_masks = outputs.get("pred_masks")
        results = [
            self._to_results(
                im, det, conf, None if pred_masks is None else pred_masks[b], mask_thresh
            )
            for b, (im, det) in enumerate(zip(images, detections))
        ]

    if save or save_txt or save_crop:
        self._save_predictions(
            results, sources, project, name, save, save_txt, save_crop, save_conf
        )
    return results

benchmark

benchmark(imgsz: int | None = None, runs: int = 50, warmup: int = 10, batch: int = 1) -> dict

Measure forward-pass inference speed on random input at the model's resolution.

Runs warmup untimed then runs timed forward passes (the postprocessor is not included — this is the compute-bound model latency), synchronizing CUDA around the timed region. Returns {"imgsz", "batch", "runs", "device", "ms_per_image", "fps"} and logs a one-line summary. imgsz must equal the model's imgsz (the encoder's positional embeddings are precomputed for it).

Source code in dfine/model.py
@torch.no_grad()
def benchmark(
    self, imgsz: int | None = None, runs: int = 50, warmup: int = 10, batch: int = 1
) -> dict:
    """Measure forward-pass inference speed on random input at the model's resolution.

    Runs ``warmup`` untimed then ``runs`` timed forward passes (the postprocessor is not
    included — this is the compute-bound model latency), synchronizing CUDA around the
    timed region. Returns ``{"imgsz", "batch", "runs", "device", "ms_per_image", "fps"}``
    and logs a one-line summary. ``imgsz`` must equal the model's ``imgsz`` (the encoder's
    positional embeddings are precomputed for it).
    """
    size = imgsz or self.config.imgsz
    if size != self.config.imgsz:
        raise ValueError(
            f"benchmark(imgsz={size}) must equal the model's imgsz ({self.config.imgsz})."
        )
    runs, batch = max(1, runs), max(1, batch)  # avoid div-by-zero / empty batch
    self.model.eval()
    x = torch.rand(batch, 3, size, size, device=self.device)
    cuda = self.device.type == "cuda"

    for _ in range(max(0, warmup)):
        self.model(x)
    if cuda:
        torch.cuda.synchronize()
    start = time.perf_counter()
    for _ in range(runs):
        self.model(x)
    if cuda:
        torch.cuda.synchronize()
    elapsed = time.perf_counter() - start

    ms_per_image = elapsed / (runs * batch) * 1000.0
    fps = (runs * batch) / elapsed
    result = {
        "imgsz": size,
        "batch": batch,
        "runs": runs,
        "device": str(self.device),
        "ms_per_image": ms_per_image,
        "fps": fps,
    }
    LOGGER.info(
        f"{colorstr('cyan', 'bold', 'Speed')}  "
        f"{ms_per_image:.2f} ms/image  ({fps:.1f} FPS)  "
        f"batch={batch}  imgsz={size}  {self.device}"
    )
    return result

info

info(verbose: bool = False) -> dict

Model summary: layer/parameter/gradient counts (+ GFLOPs if thop is installed).

Returns {"layers", "parameters", "gradients", "gflops"} (gflops is None when thop is unavailable) and logs a one-line summary. With verbose=True also logs the per-top-level-module (backbone/encoder/decoder) parameter breakdown.

Source code in dfine/model.py
def info(self, verbose: bool = False) -> dict:
    """Model summary: layer/parameter/gradient counts (+ GFLOPs if ``thop`` is installed).

    Returns ``{"layers", "parameters", "gradients", "gflops"}`` (``gflops`` is ``None``
    when ``thop`` is unavailable) and logs a one-line summary. With ``verbose=True`` also
    logs the per-top-level-module (backbone/encoder/decoder) parameter breakdown.
    """
    params = sum(p.numel() for p in self.model.parameters())
    gradients = sum(p.numel() for p in self.model.parameters() if p.requires_grad)
    layers = sum(1 for _ in self.model.modules())
    gflops = self._gflops()

    size = self.config.size or "custom"
    line = (
        f"DFINE {size} summary: {layers} layers, {params:,} parameters, {gradients:,} gradients"
    )
    if gflops is not None:
        line += f", {gflops:.1f} GFLOPs"
    LOGGER.info(colorstr("cyan", "bold", line))
    if verbose:
        for cname, child in self.model.named_children():
            n = sum(p.numel() for p in child.parameters())
            LOGGER.info(f"  {colorstr('gray', cname):<12} {n / 1e6:.2f}M params")
    return {"layers": layers, "parameters": params, "gradients": gradients, "gflops": gflops}

train

train(train_loader=None, epochs: int | None = None, *, data: str | PathLike | None = None, batch_size: int = 4, num_workers: int = 4, augment: bool = True, remap_mscoco_category: bool | None = None, val_split: float = 0.2, devices: int | None = None, val_loader=None, val_fn=None, output_dir: str = 'runs/train', resume: str | PathLike | bool | None = None, use_wandb: bool = False, visualize: bool = True, val_plots: bool = False)

Fine-tune the model (Phase 4).

Provide the data one of two ways:

  • data="path/to/coco" — a standard COCO dataset root (train/ + annotations/instances_train.json, optional val/; a stock MS-COCO train2017/ layout is auto-detected too). The train loader (full two-phase augmentation + multi-scale) and, if present, a val loader are built for you via :func:~dfine.train.dataset.build_coco_dataloaders. batch_size, num_workers, augment and remap_mscoco_category tune that build (set remap_mscoco_category=True for stock 80-class MS-COCO ids). For a task="segment" / "sem_seg" model, data= is instead a YOLO-style root (images/ + labels/: polygon .txt for segment, class-id .png for sem_seg) built via :func:~dfine.train.seg_dataset.build_seg_dataloaders. Train/val are split from images/{train,val} subdirs if present, else a deterministic val_split fraction (default 0.2; set 0 to train on everything) of the flat root; the seg val loader is auto-scored with mask AP (segment) / mIoU (sem_seg).
  • train_loader=... — a ready dataloader yielding (samples, targets) batches: samples a float BCHW image tensor, each target a dict with labels (LongTensor) and boxes (cxcywh, normalized).

Multi-GPU: pass devices=N to train on N GPUs — this call becomes the launcher and spawns one DDP worker per GPU (no torchrun needed); it requires data= (in-memory loaders can't be shipped to workers). Alternatively launch the script yourself with torchrun --nproc_per_node=N and call train(...) without devices — each worker detects the distributed env and joins the group.

Optimizer groups, LR schedule, EMA, AMP and grad-clip all come from this model's :class:~dfine.config.DFINEConfig. Progress is visualized like upstream D-FINE: a live tqdm progress bar (total loss + lr; the full per-term loss breakdown streams to TensorBoard) plus TensorBoard scalars and a loss_curve.png under output_dir (and W&B if use_wandb); only rank 0 writes them. Returns self; the trained (EMA) weights replace self.model.

Checkpoints (rank 0): last.pth after every epoch, best.pth whenever the primary validation metric improves (detection AP / mask mAP_50_95_mask / mIoU), and — when config.checkpoint_freq > 0 — a resumable weights/epoch{N}.pth snapshot every checkpoint_freq epochs.

Resume: pass resume= to continue an interrupted run — a checkpoint path, or True for output_dir/last.pth. Model, optimizer, LR scheduler, EMA and the best metric are restored and training picks up at the saved epoch + 1.

When a val_loader is available (passed, or auto-built from data) and no val_fn is given, COCO metrics are computed each epoch via :func:~dfine.train.evaluator.coco_val_fn and logged alongside the loss.

Pass val_plots=True to also render the full validation-analytics bundle (confusion matrix, P/R/F1 curves, per-class AP, worst-predictions gallery) every epoch under output_dir/val/epoch{N}/ — the same artifacts as val(plots=True), kept per epoch. Needs matplotlib (the [train] extra) and contiguous labels; it is ignored (with a warning) when remap_mscoco_category=True or for segment/sem_seg tasks. Off by default since it adds per-epoch compute and disk.

Source code in dfine/model.py
def train(
    self,
    train_loader=None,
    epochs: int | None = None,
    *,
    data: str | os.PathLike | None = None,
    batch_size: int = 4,
    num_workers: int = 4,
    augment: bool = True,
    remap_mscoco_category: bool | None = None,
    val_split: float = 0.2,
    devices: int | None = None,
    val_loader=None,
    val_fn=None,
    output_dir: str = "runs/train",
    resume: str | os.PathLike | bool | None = None,
    use_wandb: bool = False,
    visualize: bool = True,
    val_plots: bool = False,
):
    """Fine-tune the model (Phase 4).

    Provide the data one of two ways:

    * ``data="path/to/coco"`` — a standard COCO dataset root (``train/`` +
      ``annotations/instances_train.json``, optional ``val/``; a stock MS-COCO
      ``train2017/`` layout is auto-detected too). The train loader (full two-phase
      augmentation + multi-scale) and, if present, a val loader are built for you via
      :func:`~dfine.train.dataset.build_coco_dataloaders`. ``batch_size``,
      ``num_workers``, ``augment`` and ``remap_mscoco_category`` tune that build
      (set ``remap_mscoco_category=True`` for stock 80-class MS-COCO ids). For a
      ``task="segment"`` / ``"sem_seg"`` model, ``data=`` is instead a YOLO-style root
      (``images/`` + ``labels/``: polygon ``.txt`` for segment, class-id ``.png`` for
      sem_seg) built via :func:`~dfine.train.seg_dataset.build_seg_dataloaders`. Train/val
      are split from ``images/{train,val}`` subdirs if present, else a deterministic
      ``val_split`` fraction (default ``0.2``; set ``0`` to train on everything) of the flat
      root; the seg val loader is auto-scored with mask AP (segment) / mIoU (sem_seg).
    * ``train_loader=...`` — a ready dataloader yielding ``(samples, targets)``
      batches: ``samples`` a float ``BCHW`` image tensor, each ``target`` a dict
      with ``labels`` (``LongTensor``) and ``boxes`` (``cxcywh``, normalized).

    **Multi-GPU:** pass ``devices=N`` to train on ``N`` GPUs — this call becomes the
    launcher and spawns one DDP worker per GPU (no ``torchrun`` needed); it requires
    ``data=`` (in-memory loaders can't be shipped to workers). Alternatively launch
    the script yourself with ``torchrun --nproc_per_node=N`` and call ``train(...)``
    without ``devices`` — each worker detects the distributed env and joins the group.

    Optimizer groups, LR schedule, EMA, AMP and grad-clip all come from this
    model's :class:`~dfine.config.DFINEConfig`. Progress is visualized like upstream
    D-FINE: a live ``tqdm`` progress bar (total loss + lr; the full per-term loss
    breakdown streams to TensorBoard) plus TensorBoard scalars and a ``loss_curve.png``
    under ``output_dir`` (and W&B if ``use_wandb``); only rank 0 writes them. Returns
    ``self``; the trained (EMA) weights replace ``self.model``.

    **Checkpoints** (rank 0): ``last.pth`` after every epoch, ``best.pth`` whenever the
    primary validation metric improves (detection ``AP`` / mask ``mAP_50_95_mask`` /
    ``mIoU``), and — when ``config.checkpoint_freq > 0`` — a resumable
    ``weights/epoch{N}.pth`` snapshot every ``checkpoint_freq`` epochs.

    **Resume:** pass ``resume=`` to continue an interrupted run — a checkpoint path, or
    ``True`` for ``output_dir/last.pth``. Model, optimizer, LR scheduler, EMA and the
    best metric are restored and training picks up at the saved epoch + 1.

    When a ``val_loader`` is available (passed, or auto-built from ``data``) and no
    ``val_fn`` is given, COCO metrics are computed each epoch via
    :func:`~dfine.train.evaluator.coco_val_fn` and logged alongside the loss.

    Pass ``val_plots=True`` to also render the full validation-analytics bundle
    (confusion matrix, P/R/F1 curves, per-class AP, worst-predictions gallery) **every
    epoch** under ``output_dir/val/epoch{N}/`` — the same artifacts as
    ``val(plots=True)``, kept per epoch. Needs matplotlib (the ``[train]`` extra) and
    contiguous labels; it is ignored (with a warning) when
    ``remap_mscoco_category=True`` or for ``segment``/``sem_seg`` tasks. Off by default
    since it adds per-epoch compute and disk.
    """
    from .train.distributed import launched_via_torchrun, setup_distributed

    if epochs is not None and int(epochs) < 1:
        raise ValueError(f"epochs must be >= 1, got {epochs}.")
    remap_mscoco_category = self._resolve_coco_remap(remap_mscoco_category)

    if devices is not None and int(devices) > 1 and not launched_via_torchrun():
        return self._train_multigpu(
            int(devices),
            data=data,
            epochs=epochs,
            batch_size=batch_size,
            num_workers=num_workers,
            augment=augment,
            remap_mscoco_category=remap_mscoco_category,
            val_split=val_split,
            output_dir=output_dir,
            resume=resume,
            use_wandb=use_wandb,
            visualize=visualize,
            val_plots=val_plots,
        )

    if launched_via_torchrun():
        setup_distributed()
        self._bind_local_rank_device()

    self._fit(
        train_loader=train_loader,
        epochs=epochs,
        data=data,
        batch_size=batch_size,
        num_workers=num_workers,
        augment=augment,
        remap_mscoco_category=remap_mscoco_category,
        val_split=val_split,
        val_loader=val_loader,
        val_fn=val_fn,
        output_dir=output_dir,
        resume=resume,
        use_wandb=use_wandb,
        visualize=visualize,
        val_plots=val_plots,
    )
    return self

val

val(data: str | PathLike | None = None, *, val_loader=None, batch_size: int = 4, num_workers: int = 4, remap_mscoco_category: bool | None = None, plots: bool = False, output_dir: str = 'runs/val') -> dict[str, float]

Evaluate the model on a COCO val set and return the metrics dict.

Provide the data one of two ways (mutually exclusive):

  • data="path/to/coco" — a COCO root; the val loader is built from val/ + annotations/instances_val.json for you (a stock MS-COCO val2017/ layout is auto-detected too).
  • val_loader=... — a ready loader from build_coco_dataloader (its dataset must carry the ground-truth .coco).

Returns the 12 standard COCO metrics keyed by name (AP is the primary mAP@[.50:.95]); see :data:~dfine.train.evaluator.COCO_STAT_NAMES. For stock MS-COCO ground truth (sparse category ids), build the model with remap_mscoco_category=True so predicted labels match the annotations.

With plots=True also writes a confusion matrix + precision–recall curves and logs the per-class AP table under output_dir (default runs/val); needs matplotlib (the [train] extra). The confusion matrix assumes contiguous labels, so plots is ignored (with a warning) when remap_mscoco_category=True.

Source code in dfine/model.py
def val(
    self,
    data: str | os.PathLike | None = None,
    *,
    val_loader=None,
    batch_size: int = 4,
    num_workers: int = 4,
    remap_mscoco_category: bool | None = None,
    plots: bool = False,
    output_dir: str = "runs/val",
) -> dict[str, float]:
    """Evaluate the model on a COCO val set and return the metrics dict.

    Provide the data one of two ways (mutually exclusive):

    * ``data="path/to/coco"`` — a COCO root; the val loader is built from
      ``val/`` + ``annotations/instances_val.json`` for you (a stock MS-COCO
      ``val2017/`` layout is auto-detected too).
    * ``val_loader=...`` — a ready loader from ``build_coco_dataloader`` (its
      dataset must carry the ground-truth ``.coco``).

    Returns the 12 standard COCO metrics keyed by name (``AP`` is the primary
    mAP@[.50:.95]); see :data:`~dfine.train.evaluator.COCO_STAT_NAMES`. For stock
    MS-COCO ground truth (sparse category ids), build the model with
    ``remap_mscoco_category=True`` so predicted labels match the annotations.

    With ``plots=True`` also writes a confusion matrix + precision–recall curves and
    logs the per-class AP table under ``output_dir`` (default ``runs/val``); needs
    matplotlib (the ``[train]`` extra). The confusion matrix assumes contiguous labels,
    so ``plots`` is ignored (with a warning) when ``remap_mscoco_category=True``.
    """
    if data is None and val_loader is None:
        raise ValueError("Provide validation data via `data=` or `val_loader=`.")
    if data is not None and val_loader is not None:
        raise ValueError("Pass either `data=` or `val_loader=`, not both.")
    remap_mscoco_category = self._resolve_coco_remap(remap_mscoco_category)
    if data is not None:
        from .train.dataset import build_coco_val_dataloader

        val_loader = build_coco_val_dataloader(
            data,
            cfg=self.config,
            batch_size=batch_size,
            num_workers=num_workers,
            remap_mscoco_category=remap_mscoco_category,
        )

    if plots and remap_mscoco_category:
        # The analytics (confusion matrix / per-class AP) assume contiguous 0..N-1 labels;
        # with the sparse MS-COCO remap the GT label space disagrees, so the plots would be
        # silently wrong. Mirror DFINE.train's guard and skip them (COCO metrics unaffected).
        LOGGER.warning(
            "val(plots=True) needs contiguous labels — plots disabled because "
            "remap_mscoco_category=True (COCO metrics are unaffected)."
        )
        plots = False

    from .train.evaluator import evaluate

    return evaluate(
        self.model,
        self.postprocessor,
        val_loader,
        self.device,
        plots=plots,
        output_dir=output_dir,
        names=self.names,
    )

export

export(format: str = 'onnx', file: str | PathLike | None = None, *, imgsz: int | None = None, batch: int = 1, dynamic: bool = True, simplify: bool = False, opset: int = 16) -> Path

Export the model to a deployable graph (Phase 3).

format="onnx" (default) writes a single ONNX graph, batch dim dynamic by default; format="torchscript" writes a .torchscript traced at a fixed batch/imgsz (torch-only, no ONNX toolchain). The outputs follow the model's task:

  • detect(images, orig_target_sizes)(labels, boxes, scores).
  • segment — same inputs → (labels, boxes, scores, masks) (masks are the top-k queries' sigmoid maps at 1/4 res; threshold/resize/clip on the host).
  • sem_segimagessem_seg [N, H, W] uint8 label map (argmax fused in; resize to the original size on the host).

Returns the output :class:~pathlib.Path. ONNX needs pip install pydfine[export]; TorchScript needs only torch. file defaults to dfine-<size>.<ext>. ONNX-only knobs (dynamic/simplify/opset) are ignored for TorchScript. Use :func:dfine.export.tensorrt_command for a downstream trtexec engine. Export traces on CPU so the saved artifact is portable regardless of the live model's device.

Source code in dfine/model.py
def export(
    self,
    format: str = "onnx",
    file: str | os.PathLike | None = None,
    *,
    imgsz: int | None = None,
    batch: int = 1,
    dynamic: bool = True,
    simplify: bool = False,
    opset: int = 16,
) -> Path:
    """Export the model to a deployable graph (Phase 3).

    ``format="onnx"`` (default) writes a single ONNX graph, batch dim dynamic by
    default; ``format="torchscript"`` writes a ``.torchscript`` traced at a fixed
    ``batch``/``imgsz`` (torch-only, no ONNX toolchain). The outputs follow the model's
    ``task``:

    - ``detect``  — ``(images, orig_target_sizes)`` → ``(labels, boxes, scores)``.
    - ``segment`` — same inputs → ``(labels, boxes, scores, masks)`` (masks are the
      top-k queries' sigmoid maps at 1/4 res; threshold/resize/clip on the host).
    - ``sem_seg`` — ``images`` → ``sem_seg`` ``[N, H, W]`` uint8 label map (argmax
      fused in; resize to the original size on the host).

    Returns the output :class:`~pathlib.Path`. ONNX needs ``pip install pydfine[export]``;
    TorchScript needs only torch. ``file`` defaults to ``dfine-<size>.<ext>``. ONNX-only
    knobs (``dynamic``/``simplify``/``opset``) are ignored for TorchScript. Use
    :func:`dfine.export.tensorrt_command` for a downstream ``trtexec`` engine. Export
    traces on CPU so the saved artifact is portable regardless of the live model's device.
    """
    fmt = format.lower()
    if fmt not in ("onnx", "torchscript"):
        raise ValueError(
            f"Unsupported export format {format!r}; choose 'onnx' or 'torchscript'."
        )

    imgsz = imgsz or self.config.imgsz
    if imgsz != self.config.imgsz:
        raise ValueError(
            f"export imgsz={imgsz} must match the model's imgsz={self.config.imgsz}; "
            f"rebuild the model with DFINE(size=..., imgsz={imgsz}) to export at that size."
        )
    stem = f"dfine-{self.config.size or 'custom'}"

    if fmt == "torchscript":
        from .export.torchscript import export_torchscript

        file = Path(file) if file is not None else Path(f"{stem}.torchscript")
        return export_torchscript(
            self.model,
            self.postprocessor,
            file,
            task=self.config.task,
            imgsz=imgsz,
            batch=batch,
            device="cpu",
        )

    from .export.onnx import export_onnx

    file = Path(file) if file is not None else Path(f"{stem}.onnx")
    return export_onnx(
        self.model,
        self.postprocessor,
        file,
        task=self.config.task,
        imgsz=imgsz,
        batch=batch,
        opset=opset,
        dynamic=dynamic,
        simplify=simplify,
        device="cpu",
    )

predict_video

predict_video(source, output: str | PathLike = 'output.mp4', conf: float = 0.25, imgsz: int | None = None, stream: bool = False, track: bool = False)

Detect objects frame-by-frame in a video.

With stream=True returns a generator of per-frame :class:Results and writes nothing. Otherwise writes an annotated video to output (original resolution/fps) and returns its :class:~pathlib.Path.

With track=True each frame's detections are run through a ByteTrack tracker so boxes carry a persistent boxes.id across frames (rendered as #id and colored per track). Needs scipy (the [track] extra).

Source code in dfine/model.py
def predict_video(
    self,
    source,
    output: str | os.PathLike = "output.mp4",
    conf: float = 0.25,
    imgsz: int | None = None,
    stream: bool = False,
    track: bool = False,
):
    """Detect objects frame-by-frame in a video.

    With ``stream=True`` returns a generator of per-frame :class:`Results` and
    writes nothing. Otherwise writes an annotated video to ``output`` (original
    resolution/fps) and returns its :class:`~pathlib.Path`.

    With ``track=True`` each frame's detections are run through a ByteTrack tracker
    so boxes carry a persistent ``boxes.id`` across frames (rendered as ``#id`` and
    colored per track). Needs scipy (the ``[track]`` extra).
    """
    if stream:
        return self._iter_video(source, conf, imgsz, track)

    cv2 = _require_cv2()
    cap = cv2.VideoCapture(str(source))
    if not cap.isOpened():
        raise FileNotFoundError(f"Could not open video source: {source!r}")

    fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
    width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    writer = cv2.VideoWriter(str(output), cv2.VideoWriter_fourcc(*"mp4v"), fps, (width, height))
    tracker = self._make_tracker(fps) if track else None
    try:
        while True:
            ok, frame = cap.read()
            if not ok:
                break
            rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            result = self.predict(rgb, conf=conf, imgsz=imgsz)[0]
            if tracker is not None:
                result = tracker.update(result)
            writer.write(cv2.cvtColor(result.plot(), cv2.COLOR_RGB2BGR))
    finally:
        cap.release()
        writer.release()
    return Path(output)