Skip to content

📦 Export

Writing a trained checkpoint as an ONNX file: graph_from_module rebuilds a task's suppression-free one-to-one graph from a loaded checkpoint, and export_graph writes it. The lucid-export command wraps both; how to run it is under Exporting to ONNX. Generated from the docstrings in src/lucid_yolo/export.py.

From a checkpoint

graph_from_module

graph_from_module(module: DetectionLitModule, canvas: tuple[int, int], *, device: device | None = None) -> E2EExportGraph

Build a checkpoint-loaded module's export graph, its weights checked key for key.

A Lightning checkpoint loads into a :class:~lucid_yolo.ptl.module.DetectionLitModule (:func:~lucid_yolo.eval.checkpoint.load_eval_module), and the export graphs wrap a composite model's deploy() view, which the Lightning module does not have. This is the bridge between the two. It reads the task from the module, rebuilds that task's composite — :class:~lucid_yolo.models.build.Detector, :class:~lucid_yolo.models.build.Segmenter, :class:~lucid_yolo.models.build.OrientedDetector or :class:~lucid_yolo.models.build.KeypointDetector — at the module's own multipliers, class count and point count, copies the module's weights into it, and wraps its deploy() view in the task's graph class.

The rebuild is at raw multipliers rather than a variant name, so a module built outside the registry's five rows exports as readily as one the training CLI configured. The copy is checked strictly in both directions: the module must supply every key the composite holds and nothing else, apart from the training-only loss state in :data:_TRAINING_ONLY_PREFIXES. That works because the module keeps its stages as flat backbone/neck/head attributes (plus the three segmentation stages), the same names the composites use, so the two state dicts share their keys.

The graph owns a copy of the weights: module is read and not modified, and stays on whatever device it was on. Whichever weights module holds are what is exported, so a module :func:~lucid_yolo.eval.checkpoint.load_eval_module returned with use_ema=True exports its EMA shadow.

Parameters:

Name Type Description Default
module DetectionLitModule

The module to export, as :func:~lucid_yolo.eval.checkpoint.load_eval_module returns it.

required
canvas tuple[int, int]

Input canvas (height, width) in pixels, each divisible by every head stride. Baked into the graph: the result answers only for this size.

required
device device | None

Device the graph is built on, as :class:E2EExportGraph describes. Defaults to CPU.

None

Returns:

Type Description
E2EExportGraph

The task's eval-mode graph, ready for :func:export_graph.

Raises:

Type Description
ValueError

If the module's task is not one of detect, segment, obb and keypoints (the message lists them), if its weights miss a key the composite holds or carry one it does not (the message names them), if a weight's shape disagrees with the composite's, or if canvas is refused by the task's graph class.

Examples:

>>> import torch
>>> from lucid_yolo.ptl.module import DetectionLitModule
>>> module = DetectionLitModule(depth=0.34, width=0.25, max_channels=64, num_classes=2).eval()
>>> graph = graph_from_module(module, canvas=(64, 64))
>>> type(graph).__name__, graph.training
('DetectExportGraph', False)
>>> with torch.no_grad():
...     detections = graph(torch.zeros(1, 3, 64, 64))
>>> detections.shape
torch.Size([1, 300, 6])

export_graph

export_graph(graph: E2EExportGraph, path: str | Path, *, example: Tensor | None = None, opset_version: int = ONNX_OPSET) -> Path

Write one export graph to an ONNX file at a pinned opset.

The shipped counterpart to the four graph classes above: they compose the decode, this emits the file. It exists so the opset is pinned somewhere a consumer runs rather than left to whatever :func:torch.onnx.export's default is on the torch a given machine installed — see :data:ONNX_OPSET for what that pin buys. The export gate calls this function rather than :func:torch.onnx.export directly, so the graph an operator gets is the graph the gate certified.

The emitted file is static in every axis: no dynamic_axes is passed, so it answers for exactly the shape of example — the canvas graph baked into its buffers, and that example's batch. A consumer needing a second shape exports a second file.

Parameters:

Name Type Description Default
graph E2EExportGraph

A built task graph — :class:DetectExportGraph, :class:SegmentExportGraph, :class:OrientedExportGraph or :class:KeypointExportGraph — in eval mode.

required
path str | Path

Destination .onnx file. Its parent directory must exist.

required
example Tensor | None

Input the graph is traced on, of shape (B, 3, H, W) matching graph.canvas. Defaults to a single zero image on the graph's own device, which is enough: the graph has no data-dependent control flow, so the trace records the same ops whatever the values are.

None
opset_version int

ONNX opset the graph is emitted at. Defaults to :data:ONNX_OPSET; a caller pinned to a different runtime overrides it, and the gate's op assertions then no longer speak for the result.

ONNX_OPSET

Returns:

Type Description
Path

The written path.

Raises:

Type Description
OnnxExporterError

If the onnx package is not installed. torch's TorchScript exporter imports it to finalise the file, so exporting needs it even though nothing here imports it; pip install "lucid-yolo[export]" adds it.

Examples:

>>> import tempfile
>>> import torch
>>> from pathlib import Path
>>> from lucid_yolo.models.build import Detector
>>> deployed = Detector("n", num_classes=4).eval().deploy().eval()
>>> graph = DetectExportGraph(deployed, canvas=(64, 64)).eval()
>>> with tempfile.TemporaryDirectory() as tmp:
...     written = export_graph(graph, Path(tmp) / "detect.onnx")
...     written.is_file()
True

ONNX_OPSET module-attribute

ONNX_OPSET = 17

Graphs

E2EExportGraph

Bases: Module

A deploy() view plus its E2E decode, as one exportable module.

The shared base of :class:DetectExportGraph, :class:SegmentExportGraph, :class:OrientedExportGraph and :class:KeypointExportGraph: it bakes the anchor grid for one canvas into buffers and holds the :class:~lucid_yolo.decode.topk_e2e.TopKDecoder whose k fixes every task's output length — the axis-aligned tasks decode with it directly, the oriented task with a :class:~lucid_yolo.decode.topk_e2e.RotatedTopKDecoder built at the same k — so a subclass need only compose its own task's dense outputs into a decoded tuple. forward is left to subclasses — that is where the four tasks differ, per the module docstring's compositions — so this class alone is not directly exportable.

Parameters:

Name Type Description Default
deployed Module

The deploy() view whose dense outputs the decode consumes, as returned by :meth:~lucid_yolo.models.build.Detector.deploy, :meth:~lucid_yolo.models.build.Segmenter.deploy, :meth:~lucid_yolo.models.build.OrientedDetector.deploy or :meth:~lucid_yolo.models.build.KeypointDetector.deploy, matched to the subclass in use.

required
canvas tuple[int, int]

Input canvas (height, width) in pixels, each divisible by every stride in :data:~lucid_yolo.assign.grid.HEAD_STRIDES. Baked into the anchor buffers, so the graph this constructs answers only for images of this exact size.

required
device device | None

Device the anchor buffers are built on and deployed is moved to, so the two halves of the graph cannot end up on different devices — buffers built on an accelerator while the wrapped model stayed wherever the caller left it is a failure at the first forward, not at construction. The move is in place, as :func:~lucid_yolo.predict.predict_image moves the module it is handed: a caller reusing deployed afterwards gets it on this device. Defaults to CPU, the device :func:torch.onnx.export traces a graph from.

None

Raises:

Type Description
ValueError

If either canvas side is not positive and divisible by every stride, per :func:~lucid_yolo.assign.grid.require_grid_canvas, which :func:~lucid_yolo.assign.grid.anchor_grid applies as the buffers are built. The divisibility this class documents is therefore checked here rather than left to the caller: a canvas of 100 px would otherwise get 96 px worth of anchors and pair every prediction with the wrong pixel.

Examples:

>>> import torch
>>> from lucid_yolo.models.build import Detector
>>> deployed = Detector("n", num_classes=4).eval().deploy().eval()
>>> graph = E2EExportGraph(deployed, canvas=(128, 128))
>>> graph.anchor_points.shape, graph.strides.shape  # 16^2 + 8^2 + 4^2 = 336
(torch.Size([336, 2]), torch.Size([336]))

DetectExportGraph

Bases: E2EExportGraph

Detection E2E export graph: dense one-to-one outputs to the A9 tuple.

Reproduces :func:~lucid_yolo.predict.predict_image's "e2e" composition — the deployed model's one-to-one (cls, box) pair straight into :class:~lucid_yolo.decode.topk_e2e.TopKDecoder, same argument order — minus the letterbox inverse and the confidence filter, which are single-image concerns a batched, fixed-shape graph does not have.

Examples:

>>> import torch
>>> from lucid_yolo.models.build import Detector
>>> deployed = Detector("n", num_classes=4).eval().deploy().eval()
>>> graph = DetectExportGraph(deployed, canvas=(128, 128)).eval()
>>> with torch.no_grad():
...     detections = graph(torch.zeros(1, 3, 128, 128))
>>> detections.shape  # (B, k, 6): the A9 tuple, k = 300 (A9)
torch.Size([1, 300, 6])

forward

forward(image: Tensor) -> Tensor

Decode an image batch into fixed-size A9 detections.

Parameters:

Name Type Description Default
image Tensor

Input image batch of shape (B, 3, H, W) matching the canvas this graph was built for.

required

Returns:

Type Description
Tensor

Detections of shape (B, k, 6) whose last axis is the A9 tuple

Tensor

[x1, y1, x2, y2, score, class] (see

Tensor

meth:~lucid_yolo.decode.topk_e2e.TopKDecoder.forward).

SegmentExportGraph

Bases: E2EExportGraph

Segmentation E2E export graph: the A9 tuple beside its Eq. 7 instance masks.

Reproduces :func:~lucid_yolo.predict.predict_segmentation's "e2e" composition — the same decoder call for the boxes, the same :meth:~lucid_yolo.decode.topk_e2e.TopKDecoder.decode_with_indices source of the anchor indices mask coefficients are gathered by, and the same mask assembly, resize, crop and threshold operations. Export retains the original single-resize graph while eager decode chunks its full-canvas temporaries. One shape divergence remains: every one of the k rows is gathered, padding rows included, because the output shape is fixed.

That divergence is why this graph's supported canvases are narrower than its base class's: a canvas yielding fewer anchors than k is refused in __init__, where it can still be reported as a canvas choice. The coefficient gather is the shared clamp-and-zero one, so such a canvas would no longer fail at trace time; the module docstring states why the refusal is kept anyway.

Parameters:

Name Type Description Default
deployed Module

The deploy() view of a :class:~lucid_yolo.models.build.Segmenter, as :class:E2EExportGraph describes.

required
canvas tuple[int, int]

Input canvas (height, width) in pixels, additionally required to yield at least k anchors.

required
device device | None

Device the buffers are built on and deployed is moved to, as :class:E2EExportGraph describes.

None

Raises:

Type Description
ValueError

If canvas fails :class:E2EExportGraph's divisibility requirement, or yields fewer anchors than the decoder's k.

Examples:

>>> import torch
>>> from lucid_yolo.models.build import Segmenter
>>> deployed = Segmenter("n", num_classes=4).eval().deploy().eval()
>>> graph = SegmentExportGraph(deployed, canvas=(128, 128)).eval()
>>> with torch.no_grad():
...     detections, masks = graph(torch.zeros(1, 3, 128, 128))
>>> detections.shape, masks.shape  # (B, k, 6) beside (B, k, H, W)
(torch.Size([1, 300, 6]), torch.Size([1, 300, 128, 128]))
>>> SegmentExportGraph(deployed, canvas=(64, 64))  # 84 anchors, below k
Traceback (most recent call last):
    ...
ValueError: canvas (64, 64) yields 84 anchors, below the 300-detection cap; padding rows have no coefficients

forward

forward(image: Tensor) -> tuple[Tensor, Tensor]

Decode an image batch into fixed-size A9 detections and their masks.

Parameters:

Name Type Description Default
image Tensor

Input image batch of shape (B, 3, H, W) matching the canvas this graph was built for.

required

Returns:

Type Description
Tensor

A pair of the (B, k, 6) A9 detections and their

Tensor

(B, k, H, W) boolean instance masks on the letterboxed canvas

tuple[Tensor, Tensor]

(see :func:~lucid_yolo.eval.segment_decode.decode_instance_masks),

tuple[Tensor, Tensor]

row-aligned.

OrientedExportGraph

Bases: E2EExportGraph

Oriented E2E export graph: dense one-to-one outputs to the A45 tuple.

Reproduces :func:~lucid_yolo.predict.predict_oriented's "e2e" composition — one :class:~lucid_yolo.decode.topk_e2e.RotatedTopKDecoder call over the deployed model's one-to-one class logits, (box, angle) pair and the baked anchor grid, at the same k as the other graphs' decoder.

Examples:

>>> import torch
>>> from lucid_yolo.models.build import OrientedDetector
>>> deployed = OrientedDetector("n", num_classes=4).eval().deploy().eval()
>>> graph = OrientedExportGraph(deployed, canvas=(128, 128)).eval()
>>> with torch.no_grad():
...     detections = graph(torch.zeros(1, 3, 128, 128))
>>> detections.shape  # (B, k, 7): the A45 tuple, k = 300 (A9)
torch.Size([1, 300, 7])

forward

forward(image: Tensor) -> Tensor

Decode an image batch into fixed-size A45 rotated detections.

Parameters:

Name Type Description Default
image Tensor

Input image batch of shape (B, 3, H, W) matching the canvas this graph was built for.

required

Returns:

Type Description
Tensor

Detections of shape (B, k, 7) whose last axis is the A45 tuple

Tensor

[cx, cy, w, h, theta, score, class] (see

Tensor

meth:~lucid_yolo.decode.topk_e2e.RotatedTopKDecoder.forward).

KeypointExportGraph

Bases: E2EExportGraph

Keypoint E2E export graph: the A9 tuple beside each detection's point set.

The fourth task's graph, and the last of the four to exist -- 0.5.0 shipped a keypoint model that trains, validates and evaluates but had no exported view at all, so it was the only released task whose model could not leave PyTorch (WP-151).

The composition follows :class:SegmentExportGraph rather than :class:DetectExportGraph, for the reason segmentation has it: the point stem is dense over anchors, so the decode needs the source anchor of each detection row, which is what :meth:~lucid_yolo.decode.topk_e2e.TopKDecoder.decode_with_indices returns and plain :meth:~lucid_yolo.decode.topk_e2e.TopKDecoder.forward discards. The ordering is the one WP-062's angle branch established and :class:~lucid_yolo.ptl.module.DetectionLitModule already uses for its own val/oks_mAP: decode the whole dense tensor first, gather second. :func:~lucid_yolo.models.heads.keypoint.decode_keypoints needs the entire anchor grid and offers no per-detection indexing, so gathering first would mean reconstructing each kept row's anchor and stride by hand.

Padding rows are handled by :func:~lucid_yolo.decode.common.gather_by_anchor, which clamps a padding index to a valid position and then zeroes that row, so a padding row yields the origin rather than the last anchor's pose. A fixed-shape graph cannot drop rows, and this task's gather is defined on them, so this graph accepts every canvas its base class does; :class:SegmentExportGraph uses the same gather but refuses canvases that would produce padding rows.

Examples:

>>> import torch
>>> from lucid_yolo.models.build import KeypointDetector
>>> deployed = KeypointDetector("n", num_classes=4, num_keypoints=3).eval().deploy().eval()
>>> graph = KeypointExportGraph(deployed, canvas=(128, 128)).eval()
>>> with torch.no_grad():
...     detections, keypoints = graph(torch.zeros(1, 3, 128, 128))
>>> detections.shape, keypoints.shape  # (B, k, 6) beside (B, k, K, 2)
(torch.Size([1, 300, 6]), torch.Size([1, 300, 3, 2]))

forward

forward(image: Tensor) -> tuple[Tensor, Tensor]

Decode an image batch into fixed-size A9 detections and their point sets.

Parameters:

Name Type Description Default
image Tensor

Input image batch of shape (B, 3, H, W) matching the canvas this graph was built for.

required

Returns:

Type Description
Tensor

A pair of the (B, k, 6) A9 detections and their (B, k, K, 2)

Tensor

point coordinates in letterboxed-canvas pixels, row-aligned. Padding

tuple[Tensor, Tensor]

rows carry the origin and score zero.