Skip to content

🗂️ Data

The target container every task shares, the datasets that read COCO, YOLO-format and DOTA annotations into it, and the augmentation transforms that move images and targets together. Generated from the docstrings in src/lucid_yolo/data/.

Targets

Targets dataclass

Geometry for a single image, kept aligned across the transform pipeline.

The boxes/labels/polygons triple shares one instance axis of length N: labels has one entry per box and polygons, when present, has one ring per box. rboxes lives on an independent axis of length M and carries no labels in this container (rotated-target labelling lands with the OBB head in Phase 8).

Attributes:

Name Type Description
boxes Tensor

(N, 4) float32 axis-aligned boxes in xyxy pixel coords.

labels Tensor

(N,) int64 class ids, one per box.

polygons list[Tensor]

List of (P_i, 2) float32 instance rings; length 0 (no masks) or N (one ring per box).

rboxes Tensor

(M, 5) float32 long-edge rotated boxes (cx, cy, w, h, theta); empty (0, 5) when there are none.

difficult Tensor

(N,) bool R18 difficult flags on the shared instance axis. Defaults to empty, which __post_init__ expands to an all-False row per instance — so "omitted" and "nothing is difficult" are the same statement and every consumer may read the field unconditionally.

keypoints Tensor

(N, K, 2) float32 xy pixel coordinates. Its count is either zero (absent) or N (one K-point set per box).

keypoint_vis Tensor

(N, K) int64 visibility values paired with keypoints. This container preserves values without constraining their meaning.

Examples:

>>> import torch
>>> t = Targets(
...     boxes=torch.tensor([[0.0, 0.0, 4.0, 4.0], [1.0, 1.0, 2.0, 2.0]]),
...     labels=torch.tensor([3, 7]),
... )
>>> len(t.labels)
2
>>> t.filter(torch.tensor([True, False])).labels.tolist()
[3]

__post_init__

__post_init__() -> None

Validate shapes and dtypes of every modality; raise on any mismatch.

clone

clone() -> Targets

Return a deep copy whose every tensor is independent of self.

Returns:

Type Description
Targets

A new :class:Targets with cloned box/label/rbox/keypoint/visibility

Targets

tensors and a fresh list of cloned polygon rings; mutating either leaves

Targets

the other intact.

Examples:

>>> import torch
>>> a = Targets(boxes=torch.zeros((1, 4)), labels=torch.zeros(1, dtype=torch.int64))
>>> b = a.clone()
>>> b.boxes.add_(1.0)  # doctest: +ELLIPSIS
tensor(...)
>>> a.boxes.sum().item()
0.0

filter

filter(keep: Tensor, rkeep: Tensor | None = None) -> Targets

Select a subset of targets, keeping every modality aligned.

keep selects along the shared instance axis (boxes, labels, keypoints and, when present, polygons). Because rotated boxes live on an independent axis, they need their own mask: when rboxes is non-empty rkeep is required, and when rboxes is empty rkeep must be omitted. This keeps the two axes explicit rather than silently coupling their lengths.

Parameters:

Name Type Description Default
keep Tensor

(N,) bool mask over the instance axis.

required
rkeep Tensor | None

(M,) bool mask over the rotated-box axis. Required iff rboxes is non-empty; must be None otherwise.

None

Returns:

Type Description
Targets

A new :class:Targets holding only the selected instances, keypoint rows,

Targets

and rotated boxes; the returned tensors are independent copies.

Raises:

Type Description
ValueError

If a mask has the wrong length, or rkeep is missing for non-empty rboxes / supplied for empty rboxes.

TypeError

If a mask is not boolean.

Examples:

>>> import torch
>>> t = Targets(
...     boxes=torch.tensor([[0.0, 0.0, 1.0, 1.0], [2.0, 2.0, 3.0, 3.0]]),
...     labels=torch.tensor([1, 2]),
...     polygons=[torch.zeros((3, 2)), torch.ones((4, 2))],
... )
>>> kept = t.filter(torch.tensor([False, True]))
>>> kept.labels.tolist(), len(kept.polygons)
([2], 1)

concat classmethod

concat(items: list[Targets]) -> Targets

Concatenate several images' targets into one (mosaic/mix assembly).

Boxes, labels and rotated boxes are concatenated along their axes. Polygon and keypoint presence must be consistent: either every contributing instance carries that modality or none does — a mix is rejected rather than silently dropped.

Parameters:

Name Type Description Default
items list[Targets]

Targets to merge, in order. An empty list yields :meth:empty.

required

Returns:

Type Description
Targets

A single :class:Targets with the merged instances, keypoint rows, and

Targets

rotated boxes.

Raises:

Type Description
ValueError

If polygon/keypoint presence is mixed across items, or present items carry different keypoint counts.

Examples:

>>> import torch
>>> a = Targets(boxes=torch.zeros((1, 4)), labels=torch.tensor([0]))
>>> b = Targets(boxes=torch.ones((2, 4)), labels=torch.tensor([1, 2]))
>>> Targets.concat([a, b]).labels.tolist()
[0, 1, 2]

empty classmethod

empty() -> Targets

Return a valid, fully-empty target set (zero instances, no rotated boxes or keypoints).

Returns:

Name Type Description
A Targets

class:Targets with (0, 4) boxes, (0,) labels, no polygons

Targets

and (0, 5) rboxes / (0, 0, 2) keypoints — a safe identity for

Targets

meth:concat and a valid input to :meth:filter.

Examples:

>>> t = Targets.empty()
>>> t.boxes.shape, t.labels.shape, t.rboxes.shape, t.keypoints.shape
(torch.Size([0, 4]), torch.Size([0]), torch.Size([0, 5]), torch.Size([0, 0, 2]))

TiledTargets dataclass

One window's targets, with the R18 bookkeeping the crop produced.

Attributes:

Name Type Description
targets Targets

The window-local targets, boxes/labels/rboxes on one instance axis (WP-056) and polygons empty.

difficult Tensor

(N,) bool, the R18 flag after the crop — the incoming flag OR-ed with visible_fraction < 0.7 (A39: carried, not acted on).

visible_fraction Tensor

(N,) float32, R18's U_i — clipped area over original area. Kept because the fitted box's area is not the clipped area, so the quantity the rule turns on is otherwise unrecoverable downstream.

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> empty = TiledTargets(Targets.empty(), torch.zeros(0, dtype=torch.bool), torch.zeros(0))
>>> empty.difficult.shape
torch.Size([0])

__post_init__

__post_init__() -> None

Validate that every per-instance field shares one axis with the targets.

Raises:

Type Description
TypeError

If difficult is not bool or visible_fraction not float32.

ValueError

If any field's length disagrees with the instance axis.

Datasets

CocoDetectionDataset

Bases: Dataset[tuple[Tensor, Targets]]

COCO-format detection/segmentation dataset yielding (image, Targets).

The instances JSON is parsed once at construction: category ids are remapped to contiguous int64 labels (the mapping is exposed as :attr:category_id_to_label / :attr:label_to_category_id) and annotations are grouped by image id. Each __getitem__ decodes its image to a CHW float32 tensor in [0, 1] and returns the targets built at construction: one xyxy box per kept annotation, and its first segmentation ring as a (P, 2) polygon. Crowd (iscrowd=1) and non-polygon (RLE / empty / degenerate) annotations are skipped, so a retained image carries one ring per box or an empty target set.

Where each box comes from is stated once, in the module docstring: an image whose instances all keep a ring gets ring-envelope boxes, so box and mask describe one object; an image with any ringless instance carries no polygons and gets COCO's bbox, clamped to the image bounds. Either way an instance whose box ends up zero-width or zero-height is dropped, matching what the train pipeline's instance_keep_mask does to one.

An optional single-image transforms (any :class:~lucid_yolo.data.transforms.GeometricTransform, e.g. :class:~lucid_yolo.data.letterbox.Letterbox for validation) is applied to each sample before it is returned. Multi-image assemblies (mosaic, mixup, copy-paste) are composed at the datamodule level, not here.

Parameters:

Name Type Description Default
images_dir Path

Directory holding the image files named by the annotations.

required
annotation_file Path

Path to the COCO instances_*.json file.

required
transforms GeometricTransform | None

Optional per-image geometric transform applied to every sample. Defaults to None (raw decoded sample).

None
oriented bool

Read each annotation's four-point ring as a rotated box (WP-088; see the module docstring). False (the default) leaves the reader, and every target it has ever produced, exactly as it was.

False
keypoints bool

Parse each retained annotation's COCO keypoints field into the WP-120 Targets.keypoints / keypoint_vis channels. False (the default) leaves the reader, and every target it has ever produced, exactly as it was.

False
cache_images bool

Decode every image once at construction and retain the RGB tensors in memory (:meth:cache_images, without its progress report) — uint8 for an 8-bit source, uint16 for a 16-bit PNG, which so takes twice the 8-bit budget. The cache is opt-in because its size is the uncompressed dataset, but it is shared copy-on-write by forked loader workers and removes repeated JPEG/PNG decoding from multi-epoch training.

False

Attributes:

Name Type Description
category_id_to_label

Mapping from COCO category id to contiguous label.

label_to_category_id

The inverse mapping, label to COCO category id.

keypoint_flip_pairs

The (left_index, right_index) pairs a mirror must swap, read off this file's own category keypoints names (A64), or None when keypoints=False or the schema names no left/right pair.

Examples:

>>> CocoDetectionDataset  # doctest: +SKIP
>>> # ds = CocoDetectionDataset(images_dir, annotation_file)
>>> # image, targets = ds[0]  # image: (3, H, W) float32 in [0, 1]

image_ids property

image_ids: tuple[int, ...]

The COCO image id of each loader position, in __getitem__ order.

Published because a consumer that pairs this dataset's positions with anything read off the same annotation file — the whole-image tile merge (:mod:lucid_yolo.eval.tile_merge) pairing a tile to its window, the COCO scorer naming the image an accumulated detection belongs to — otherwise has to infer the identity from the annotations that landed at a position. Two tiles carrying byte-identical annotation sets are indistinguishable that way and are not indistinguishable by id, so the pairing can be asserted rather than argued.

Returns:

Type Description
int

One id per position, ascending: records are sorted by image id at

...

construction, which is the order :meth:__getitem__ indexes.

Examples:

>>> import json, tempfile
>>> from pathlib import Path
>>> payload = {
...     "categories": [{"id": 1}],
...     "images": [
...         {"id": 7, "file_name": "b.png", "height": 4, "width": 4},
...         {"id": 3, "file_name": "a.png", "height": 4, "width": 4},
...     ],
...     "annotations": [],
... }
>>> with tempfile.TemporaryDirectory() as tmp:
...     path = Path(tmp) / "instances.json"
...     _ = path.write_text(json.dumps(payload), encoding="utf-8")
...     CocoDetectionDataset(Path(tmp), path).image_ids
(3, 7)

file_names property

file_names: tuple[str, ...]

The image file name of each loader position, in __getitem__ order.

The companion of :attr:image_ids for a consumer that arrives with a file rather than an id — a figure drawn over 000007.jpg needs that image's labels, and the file name is the one key both the picture on disk and the annotation record share (WP-189).

Returns:

Type Description
tuple[str, ...]

One name per position, as the JSON spells it, aligned with :attr:image_ids.

Examples:

>>> import json, tempfile
>>> from pathlib import Path
>>> payload = {
...     "categories": [{"id": 1}],
...     "images": [
...         {"id": 7, "file_name": "b.png", "height": 4, "width": 4},
...         {"id": 3, "file_name": "a.png", "height": 4, "width": 4},
...     ],
...     "annotations": [],
... }
>>> with tempfile.TemporaryDirectory() as tmp:
...     path = Path(tmp) / "instances.json"
...     _ = path.write_text(json.dumps(payload), encoding="utf-8")
...     CocoDetectionDataset(Path(tmp), path).file_names
('a.png', 'b.png')

__len__

__len__() -> int

Return the number of images in the dataset.

cache_images

cache_images(*, progress: bool = False) -> None

Decode every distinct image file once and keep it for every later draw.

cache_images=True at construction calls this; a reader built without the flag can call it later to the same effect. A second call is a no-op, and the cache is installed only once every file has decoded, so a failure part-way leaves the reader uncached rather than half-filled.

Records are deduplicated by file_name exactly as the annotation file spells it, with no path normalisation: two records naming a.png share one decoded tensor, while a.png and ./a.png are two spellings and are cached once each. The cost of an alias is memory only — each spelling still decodes the same file.

Parameters:

Name Type Description Default
progress bool

Report the cache's expected size before the first decode — a UserWarning naming sum(3 * height * width) bytes over the distinct files, at the sizes this annotation file states and for 8-bit sources (a 16-bit PNG decodes to uint16 and takes twice that) — and draw a progress bar over the decode. False (the default) keeps a library caller quiet; :class:~lucid_yolo.ptl.datamodule.DetectionDataModule turns it on.

False

Examples:

>>> import json, tempfile
>>> from pathlib import Path
>>> import torch
>>> from torchvision.io import write_png
>>> payload = {
...     "categories": [{"id": 1}],
...     "images": [{"id": 1, "file_name": "a.png", "height": 4, "width": 6}],
...     "annotations": [],
... }
>>> with tempfile.TemporaryDirectory() as tmp:
...     root = Path(tmp)
...     _ = (root / "instances.json").write_text(json.dumps(payload), encoding="utf-8")
...     _ = write_png(torch.zeros(3, 4, 6, dtype=torch.uint8), str(root / "a.png"))
...     dataset = CocoDetectionDataset(root, root / "instances.json")
...     dataset.cache_images()
...     (root / "a.png").unlink()
...     tuple(dataset[0][0].shape)
(3, 4, 6)

targets_at

targets_at(index: int) -> Targets

Return position index's targets without decoding its image.

:meth:__getitem__ decodes the picture and applies transforms; a consumer that wants the labels alone — to draw them over an image it reads itself, or to count instances — gets the precomputed, untransformed :class:~lucid_yolo.data.targets.Targets here at no decode (WP-189).

Parameters:

Name Type Description Default
index int

Zero-based image index, as :meth:__getitem__ takes it.

required

Returns:

Type Description
Targets

The targets built at construction, in original-image pixels.

Examples:

>>> import json, tempfile
>>> from pathlib import Path
>>> payload = {
...     "categories": [{"id": 1}],
...     "images": [{"id": 3, "file_name": "a.png", "height": 8, "width": 8}],
...     "annotations": [{"id": 1, "image_id": 3, "category_id": 1, "bbox": [1, 2, 3, 4], "iscrowd": 0}],
... }
>>> with tempfile.TemporaryDirectory() as tmp:
...     path = Path(tmp) / "instances.json"
...     _ = path.write_text(json.dumps(payload), encoding="utf-8")
...     CocoDetectionDataset(Path(tmp), path).targets_at(0).boxes.tolist()
[[1.0, 2.0, 4.0, 6.0]]

__getitem__

__getitem__(index: int) -> tuple[Tensor, Targets]

Return the (image, Targets) pair for image index.

Parameters:

Name Type Description Default
index int

Zero-based image index (COCO image ids are re-indexed to a dense 0..len-1 order at construction).

required

Returns:

Type Description
Tensor

A CHW float32 image in [0, 1] and its :class:~lucid_yolo.data.targets.Targets

Targets

(xyxy boxes, contiguous int64 labels, one polygon ring per box),

tuple[Tensor, Targets]

after the optional transforms.

YoloDetectionDataset

Bases: Dataset[tuple[Tensor, Targets]]

YOLO-format dataset yielding the same (image, Targets) pairs the COCO reader does.

Images are the sorted :data:IMAGE_SUFFIXES files of images_dir; each one's label file is the same stem under labels_dir. Labels are parsed per sample rather than at construction — one image's text is a handful of rows, so the copy-on-write pressure that made the COCO reader precompute (WP-073) does not arise, and reading them here keeps the image's own decoded size as the denormalizing scale. A malformed row therefore raises at the __getitem__ that reads it, not at construction; lucid-data check is the pre-flight that parses every row of the tree up front, and the module docstring states why the two readers differ here.

Parameters:

Name Type Description Default
images_dir Path

Directory holding the split's images.

required
labels_dir Path

Directory holding one .txt per image, matched by stem.

required
names Sequence[str]

Class names in index order; the length bounds every row's class index.

required
transforms GeometricTransform | None

Optional per-image geometric transform applied to every sample.

None
oriented bool

Read nine-field rows as quadrilaterals and fit rotated boxes, instead of five-field cls cx cy w h rows. The variant is declared, never sniffed.

False
allow_missing_labels bool

Read an image with no label file as a background image instead of raising. False (the default) treats the absence as the broken export it usually is; an empty label file means "no objects" either way.

False
cache_images bool

Decode all images once at construction and retain their RGB tensors (:meth:cache_images, without its progress report) — uint8 for an 8-bit source, uint16 for a 16-bit PNG, at twice the bytes. Opt-in because the cache occupies the uncompressed dataset size; forked loader workers share those read-only pages.

False

Attributes:

Name Type Description
names

The class names, in index order.

keypoint_flip_pairs list[tuple[int, int]] | None

Always None. Declared so both readers answer the question A64 makes the dataset's to answer, rather than leaving the caller to ask only the reader it expects to have points. The answer here is not a placeholder: the YOLO label row carries no keypoint fields to name sides with, which is the same reason keypoint_targets=True is refused on a YOLO root.

Examples:

>>> YoloDetectionDataset  # doctest: +SKIP
>>> # ds = YoloDetectionDataset.from_root(root, "train")
>>> # image, targets = ds[0]  # image: (3, H, W) float32 in [0, 1]

cache_images

cache_images(*, progress: bool = False) -> None

Decode every listed image once and keep it for every later draw.

cache_images=True at construction calls this; a reader built without the flag can call it later to the same effect. A second call is a no-op, and the cache is installed only once every file has decoded, so a failure part-way leaves the reader uncached rather than half-filled.

Entries are keyed by each listed path as the directory listing spells it, with no symlink resolution, so two directory entries aliasing one file — a symlink beside its target — are cached once each. The cost of an alias is memory only.

Parameters:

Name Type Description Default
progress bool

Draw a progress bar over the decode and, once it finishes, report the image count and the bytes the cache retains as a UserWarning. A YOLO tree states no image sizes, so there is nothing to estimate from before decoding. False (the default) keeps a library caller quiet; :class:~lucid_yolo.ptl.datamodule.DetectionDataModule turns it on.

False

Examples:

>>> import tempfile
>>> from pathlib import Path
>>> import torch
>>> from torchvision.io import write_png
>>> with tempfile.TemporaryDirectory() as tmp:
...     images, labels = Path(tmp) / "images", Path(tmp) / "labels"
...     images.mkdir()
...     labels.mkdir()
...     _ = write_png(torch.zeros(3, 4, 6, dtype=torch.uint8), str(images / "a.png"))
...     dataset = YoloDetectionDataset(images, labels, ["car"], allow_missing_labels=True)
...     dataset.cache_images()
...     (images / "a.png").unlink()
...     tuple(dataset[0][0].shape)
(3, 4, 6)

from_root classmethod

from_root(data_root: Path, split: str, transforms: GeometricTransform | None = None, *, oriented: bool = False, allow_missing_labels: bool = False, cache_images: bool = False) -> YoloDetectionDataset

Build a split's dataset from a dataset root alone.

The root's data.yaml is authoritative: it supplies the class names, and its own split entry supplies the image directory when it resolves. Where the two directories come from is :func:resolve_split_dirs, shared with the pre-run check so a validated root is the tree this reader then opens.

Parameters:

Name Type Description Default
data_root Path

Directory holding data.yaml and the split trees.

required
split str

Split name, e.g. "train" or "val".

required
transforms GeometricTransform | None

Optional per-image geometric transform.

None
oriented bool

Read the oriented nine-field rows (see the class docstring).

False
allow_missing_labels bool

Read an image with no label file as a background image.

False
cache_images bool

Decode and retain every split image as an RGB tensor, uint8 for an 8-bit source and uint16 for a 16-bit PNG.

False

Returns:

Type Description
YoloDetectionDataset

The dataset for that split.

Raises:

Type Description
ValueError

If the data.yaml is malformed, or the images directory's final component is not images so no labels directory can be derived from it.

OSError

If data.yaml or the images directory is missing.

Examples:

>>> YoloDetectionDataset.from_root  # doctest: +SKIP
>>> # ds = YoloDetectionDataset.from_root(root, "val", oriented=True)

__len__

__len__() -> int

Return the number of images in the split.

__getitem__

__getitem__(index: int) -> tuple[Tensor, Targets]

Return the (image, Targets) pair for image index.

Parameters:

Name Type Description Default
index int

Zero-based index into the sorted image listing.

required

Returns:

Type Description
Tensor

A CHW float32 image in [0, 1] and its targets in pixel coordinates, after the

Targets

optional transforms.

Raises:

Type Description
FileNotFoundError

If the image has no label file and allow_missing_labels is False; the message names both the image and the path it looked for.

ValueError

If any label row is malformed; the message names the file and line.

YoloDataConfig dataclass

A dataset's own data.yaml: its class names and its split image directories.

Split entries are kept as written and resolved on demand by :meth:images_dir, so a test: line pointing at a directory that was never downloaded costs nothing until someone asks for that split.

Attributes:

Name Type Description
root Path

Directory holding the data.yaml; split entries resolve against it.

names tuple[str, ...]

Class names in index order — the index into this tuple is the class id.

splits Mapping[str, str]

Raw split entries as written in the file, keyed by split name.

Examples:

>>> import tempfile
>>> from pathlib import Path
>>> text = "names:\n- car\n- truck\nnc: 2\ntrain: ../train/images\n"
>>> with tempfile.TemporaryDirectory() as tmp:
...     root = Path(tmp)
...     _ = (root / DATA_YAML_NAME).write_text(text, encoding="utf-8")
...     (root / "train" / "images").mkdir(parents=True)
...     config = YoloDataConfig.read(root / DATA_YAML_NAME)
...     (config.names, config.images_dir("train").name)
(('car', 'truck'), 'images')

read classmethod

read(data_yaml: Path) -> YoloDataConfig

Parse a data.yaml into its class names and split entries.

Keys the format does not define (the export's roboflow: metadata block, for instance) are ignored. nc, when present, must agree with the length of names: a file disagreeing with itself is one whose class space cannot be trusted.

Parameters:

Name Type Description Default
data_yaml Path

Path to the dataset's data.yaml.

required

Returns:

Type Description
YoloDataConfig

The parsed configuration, rooted at the file's own directory.

Raises:

Type Description
ValueError

If the document is not a mapping, names is missing or malformed, or nc contradicts it. The file is named in every message.

OSError

If the file cannot be read.

Examples:

>>> import tempfile
>>> from pathlib import Path
>>> with tempfile.TemporaryDirectory() as tmp:
...     path = Path(tmp) / DATA_YAML_NAME
...     _ = path.write_text("names: {0: plane, 1: ship}\nval: valid/images\n")
...     YoloDataConfig.read(path).names
('plane', 'ship')

images_dir

images_dir(split: str) -> Path

Resolve one split's image directory from the entry the file wrote.

The entry is tried literally against the file's own directory first, then with its leading .. components dropped — the published export writes ../train/images for a tree that sits at <root>/train/images, and the literal reading of that escapes the root.

Parameters:

Name Type Description Default
split str

Split name as keyed in the file, e.g. "train" or "val".

required

Returns:

Type Description
Path

The first candidate that is a directory.

Raises:

Type Description
KeyError

If the file names no such split.

FileNotFoundError

If no candidate exists; the message lists every path tried.

Examples:

>>> import tempfile
>>> from pathlib import Path
>>> with tempfile.TemporaryDirectory() as tmp:
...     root = Path(tmp)
...     (root / "valid" / "images").mkdir(parents=True)
...     config = YoloDataConfig(root=root, names=("car",), splits={"val": "../valid/images"})
...     config.images_dir("val") == root / "valid" / "images"
True

Transforms

Compose

Apply a sequence of :class:GeometricTransform in order.

Parameters:

Name Type Description Default
transforms Sequence[GeometricTransform]

The transforms to apply left-to-right. An empty sequence is the identity.

required

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> identity = Compose([])
>>> image = torch.zeros(3, 8, 8)
>>> out_image, out_targets = identity(image, Targets.empty())
>>> out_image.shape
torch.Size([3, 8, 8])

__call__

__call__(image: Tensor, targets: Targets) -> tuple[Tensor, Targets]

Run each transform in turn, threading the image/targets pair through.

Parameters:

Name Type Description Default
image Tensor

CHW image tensor.

required
targets Targets

The targets to carry alongside image.

required

Returns:

Type Description
tuple[Tensor, Targets]

The image and targets after all transforms have been applied.

Letterbox

Aspect-preserving resize-and-pad transform with an exact inverse (A10).

The image is scaled by r = min(target_h / H, target_w / W) (a single ratio for both axes) and padded to the target canvas with pad_value, in one upstream warp from the source canvas to the letterboxed one. Boxes, polygons, rotated-box centres and keypoints are warped through the same scale-plus-translation affine; rotated-box w/h scale by r and theta is unchanged, and keypoint visibilities carry over untouched (a letterbox never crops, so no annotated point leaves the canvas).

Parameters:

Name Type Description Default
target_size int | tuple[int, int]

Target canvas as a single int (square) or an explicit (height, width) pair.

required
pad_value float

Constant fill for the padded border, in the image's value range. Defaults to 114/255 (mid-grey, the YOLO-lineage convention).

_DEFAULT_PAD_VALUE
allow_upscale bool

Whether the content may be enlarged beyond native size when the target is larger than the source. Defaults to True for training-time parity; set False to cap the ratio at 1.0.

True

Examples:

>>> from lucid_yolo.data.transforms import GeometricTransform
>>> isinstance(Letterbox(640), GeometricTransform)
True

__call__

__call__(image: Tensor, targets: Targets) -> tuple[Tensor, Targets]

Letterbox image and warp targets through the matching affine.

Parameters:

Name Type Description Default
image Tensor

CHW image tensor (float, in the same value range as pad_value).

required
targets Targets

Geometry to warp alongside the image.

required

Returns:

Type Description
tuple[Tensor, Targets]

The padded (C, target_h, target_w) image and the warped targets.

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> lb = Letterbox(4)
>>> image = torch.zeros(3, 2, 4)
>>> t = Targets(boxes=torch.tensor([[0.0, 0.0, 4.0, 2.0]]), labels=torch.tensor([0]))
>>> out_image, out_targets = lb(image, t)
>>> out_image.shape
torch.Size([3, 4, 4])
>>> out_targets.boxes
tensor([[0., 1., 4., 3.]])

inverse_map

inverse_map(points: Tensor, orig_size: tuple[int, int], letterboxed_size: tuple[int, int]) -> Tensor

Map letterboxed points back to original-image coordinates exactly.

Parameters:

Name Type Description Default
points Tensor

(K, 2) points in the letterboxed canvas.

required
orig_size tuple[int, int]

Original image (height, width).

required
letterboxed_size tuple[int, int]

Letterboxed canvas (height, width) the points live in.

required

Returns:

Type Description
Tensor

(K, 2) points in original-image coordinates, in points's

Tensor

dtype.

Examples:

>>> import torch
>>> lb = Letterbox(4)
>>> pts = torch.tensor([[0.0, 1.0], [4.0, 3.0]])
>>> lb.inverse_map(pts, orig_size=(2, 4), letterboxed_size=(4, 4))
tensor([[0., 0.],
        [4., 2.]])

forward_affine

forward_affine(orig_h: int, orig_w: int) -> tuple[Tensor, int, int]

Return the forward affine and output size mapping a source into this canvas.

Exposes the pure scale-and-translation letterbox geometry for a source of (orig_h, orig_w) without resampling any image, so a caller can compose it into another warp or state it to one.

Parameters:

Name Type Description Default
orig_h int

Source image height in pixels.

required
orig_w int

Source image width in pixels.

required

Returns:

Type Description
Tensor

A (matrix, out_h, out_w) triple: the (3, 3) float64 forward

int

affine mapping source pixels into the letterboxed canvas, and the

int

target canvas height and width.

Examples:

>>> import torch
>>> matrix, out_h, out_w = Letterbox(4).forward_affine(8, 8)
>>> (out_h, out_w)
(4, 4)
>>> matrix[:2, :2]  # r = min(4/8, 4/8) = 0.5 on both axes
tensor([[0.5000, 0.0000],
        [0.0000, 0.5000]], dtype=torch.float64)

warp_targets

warp_targets(targets: Targets, orig_h: int, orig_w: int) -> Targets

Warp targets through the letterbox affine for a source size, no image.

The single target-side implementation: :meth:__call__ routes through it after resampling the image, and a fused warp calls it directly to letterbox targets another transform has already produced at the source canvas. Boxes, polygons and keypoints are mapped point-wise and rotated-box centres are warped with extents scaled and angle fixed — no image is touched either way.

Parameters:

Name Type Description Default
targets Targets

Geometry at the source canvas to map into the letterboxed canvas.

required
orig_h int

Source image height in pixels.

required
orig_w int

Source image width in pixels.

required

Returns:

Type Description
Targets

A new :class:~lucid_yolo.data.targets.Targets in letterboxed-canvas

Targets

coordinates.

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> box = Targets(boxes=torch.tensor([[0.0, 0.0, 4.0, 2.0]]), labels=torch.tensor([0]))
>>> Letterbox(4).warp_targets(box, orig_h=2, orig_w=4).boxes
tensor([[0., 1., 4., 3.]])

RandomAffine

Random affine warp applied jointly to an image and its targets (WP-010).

Each call samples a rotation in [-degrees, degrees], per-axis shears in [-shear, shear], a uniform scale in [1 - scale, 1 + scale] (floored just above zero) and a translation of [-translate, translate] times the canvas size on each axis — R1 Table S3's ranges, which is what this class still owns — and states them to one fuse-augmentations segment, which resamples the image once (bilinear, grey 114/255 fill). Every target modality rides the same composed matrix. Boxes/polygons are clipped to the canvas and filtered by min_box_size and min_visibility.

With letterbox set, upstream's aspect-preserving fit joins the same segment, so the source canvas reaches the letterboxed one in a single resample rather than two (WP-070's saving, WP-156's delegation). Targets are clipped and filtered at the source canvas and only then mapped through the letterbox affine.

Rotated boxes take the module docstring's corner-warp-and-re-fit path (WP-058) and are clipped and filtered by the same two thresholds, with boxes recomputed as the envelope of the rotated geometry. That path filters, so it requires rboxes 1:1 with boxes and no polygons (WP-056); anything else raises :class:ValueError.

Parameters:

Name Type Description Default
degrees float

Maximum absolute rotation in degrees. Defaults to 0.0.

0.0
translate float

Maximum absolute translation as a fraction of the canvas size (applied per axis). Defaults to 0.1.

0.1
scale float

Half-width of the uniform scale range [1 - scale, 1 + scale]; the sampled factor is floored just above zero. Defaults to 0.5.

0.5
shear float

Maximum absolute shear in degrees, sampled independently per axis. Defaults to 0.0.

0.0
generator Generator | None

Optional :class:torch.Generator for seeded, reproducible sampling. Defaults to None (global RNG).

None
min_box_size float

Minimum clipped side length in pixels for an instance to be kept. Defaults to 2.0.

2.0
min_visibility float

Minimum kept-area fraction (clipped-extent area divided by pre-clip-extent area) for an instance to be kept. Defaults to 0.1.

0.1
letterbox int | tuple[int, int] | None

Output canvas the warp lands on, as a single int (square) or an explicit (height, width) pair, composed into the same resample. Defaults to None (warp to the input canvas).

None
allow_upscale bool

Whether letterbox may enlarge content past its native size when the output canvas is larger than the source. Defaults to True; ignored without letterbox.

True

Attributes:

Name Type Description
last_params AffineParams | None

The :class:AffineParams sampled on the most recent call, or None before the first call.

last_matrix Tensor | None

The (3, 3) float64 forward matrix from the most recent call, or None before the first call. This is the source-canvas affine, before any letterbox.

letterbox

The :class:~lucid_yolo.data.letterbox.Letterbox supplying the output-canvas fit, or None when the warp lands on the input canvas.

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> from lucid_yolo.data.transforms import GeometricTransform
>>> isinstance(RandomAffine(), GeometricTransform)
True
>>> aff = RandomAffine(degrees=0.0, translate=0.0, scale=0.0, shear=0.0)
>>> image = torch.zeros(3, 8, 8)
>>> t = Targets(boxes=torch.tensor([[1.0, 1.0, 5.0, 5.0]]), labels=torch.tensor([0]))
>>> out_image, out_targets = aff(image, t)
>>> out_image.shape
torch.Size([3, 8, 8])
>>> out_targets.boxes
tensor([[1., 1., 5., 5.]])

The letterboxed form lands on its own canvas in one resample:

>>> fused = RandomAffine(degrees=0.0, translate=0.0, scale=0.0, letterbox=16)
>>> out_image, _ = fused(torch.rand(3, 20, 40), Targets.empty())
>>> out_image.shape
torch.Size([3, 16, 16])

__call__

__call__(image: Tensor, targets: Targets) -> tuple[Tensor, Targets]

Warp image and targets through one freshly-sampled affine.

Parameters:

Name Type Description Default
image Tensor

CHW image tensor (float, in the same value range as the grey fill, i.e. [0, 1]).

required
targets Targets

Geometry to warp alongside the image. Non-empty rboxes must share the instance axis with boxes and carry no polygons.

required

Returns:

Type Description
Tensor

The warped (C, H, W) image (same canvas size as the input) and the

Targets

warped, clipped and filtered targets.

Raises:

Type Description
ValueError

If rboxes is non-empty and breaks WP-056's instance-axis invariant (length mismatch, or polygons alongside).

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> gen = torch.Generator().manual_seed(0)
>>> aff = RandomAffine(degrees=10.0, translate=0.1, scale=0.2, generator=gen)
>>> out_image, _ = aff(torch.rand(3, 16, 16), Targets.empty())
>>> out_image.shape
torch.Size([3, 16, 16])
>>> aff.last_matrix.shape
torch.Size([3, 3])

sample

sample(height: int, width: int) -> AffineParams

Draw one :class:AffineParams from the configured ranges, consuming the RNG.

The sampling half of the seam: this is the only method that touches :attr:generator, so a caller that wants a stated transform rather than a drawn one skips it and builds :class:AffineParams directly (WP-147).

Parameters:

Name Type Description Default
height int

Canvas height in pixels; scales the vertical translation range.

required
width int

Canvas width in pixels; scales the horizontal translation range.

required

Returns:

Type Description
AffineParams

The sampled parameters. Nothing is stashed on the instance -- the

AffineParams

last_params / last_matrix attributes are set by :meth:apply.

Examples:

>>> import torch
>>> gen = torch.Generator().manual_seed(0)
>>> params = RandomAffine(degrees=0.0, translate=0.0, scale=0.0, generator=gen).sample(16, 16)
>>> (abs(params.angle), abs(params.translate_x), params.scale)
(0.0, 0.0, 1.0)

apply

apply(image: Tensor, targets: Targets, params: AffineParams) -> tuple[Tensor, Targets]

Warp image and targets through params, drawing nothing.

The application half of the seam. Given the same params this is a pure function of its inputs, which is what lets an expectation be frozen against stated parameters rather than against a seed (WP-147).

Parameters:

Name Type Description Default
image Tensor

CHW image tensor (float, in the same value range as the grey fill, i.e. [0, 1]).

required
targets Targets

Geometry to warp alongside the image. Non-empty rboxes must share the instance axis with boxes and carry no polygons.

required
params AffineParams

The affine to apply.

required

Returns:

Type Description
Tensor

The warped image — the input canvas, or letterbox's when one is

Targets

configured — and the warped, clipped and filtered targets.

Raises:

Type Description
ValueError

If rboxes is non-empty and breaks WP-056's instance-axis invariant (length mismatch, or polygons alongside).

RuntimeError

If the transforms did not fuse into one matrix-bearing geometric segment — see :meth:_segment.

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> quarter = AffineParams(
...     angle=0.0, shear_x=0.0, shear_y=0.0, scale=1.0, translate_x=2.0, translate_y=0.0
... )
>>> box = Targets(boxes=torch.tensor([[1.0, 1.0, 5.0, 5.0]]), labels=torch.tensor([0]))
>>> _, out = RandomAffine().apply(torch.zeros(3, 8, 8), box, quarter)
>>> out.boxes.tolist()
[[3.0, 1.0, 7.0, 5.0]]

HSVJitter

Photometric HSV jitter applied to the image alone (WP-013).

Each call samples three independent gains — hue from [-hsv_h, hsv_h], saturation from [-hsv_s, hsv_s] and value from [-hsv_v, hsv_v] — converts the image to HSV, applies the hue gain additively modulo 1, the saturation and value gains multiplicatively as channel * (1 + gain) clamped to [0, 1], and converts back to RGB. Targets are geometry only and pass through unchanged.

Parameters:

Name Type Description Default
hsv_h float

Half-width of the additive hue-gain range, as a fraction of the hue circle. Defaults to 0.015.

0.015
hsv_s float

Half-width of the multiplicative saturation-gain range. Defaults to 0.7.

0.7
hsv_v float

Half-width of the multiplicative value-gain range. Defaults to 0.4.

0.4
generator Generator | None

Optional :class:torch.Generator for seeded, reproducible sampling. Defaults to None (global RNG).

None

Attributes:

Name Type Description
last_gains tuple[float, float, float] | None

The (hue, saturation, value) gains sampled on the most recent call, or None before the first call.

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> from lucid_yolo.data.transforms import GeometricTransform
>>> isinstance(HSVJitter(), GeometricTransform)
True
>>> jitter = HSVJitter(hsv_h=0.0, hsv_s=0.0, hsv_v=0.0)
>>> image = torch.rand(3, 8, 8)
>>> out_image, out_targets = jitter(image, Targets.empty())
>>> torch.allclose(out_image, image, atol=1e-5)
True

__call__

__call__(image: Tensor, targets: Targets) -> tuple[Tensor, Targets]

Jitter image in HSV space; return it with targets unchanged.

Parameters:

Name Type Description Default
image Tensor

CHW RGB image tensor with values in [0, 1].

required
targets Targets

Geometry carried alongside the image; returned untouched.

required

Returns:

Type Description
tuple[Tensor, Targets]

The colour-jittered (C, H, W) image and the original targets.

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> gen = torch.Generator().manual_seed(0)
>>> jitter = HSVJitter(generator=gen)
>>> out_image, _ = jitter(torch.rand(3, 8, 8), Targets.empty())
>>> out_image.shape
torch.Size([3, 8, 8])
>>> len(jitter.last_gains)
3

sample

sample() -> HSVParams

Draw one :class:HSVParams from the configured ranges, consuming the RNG.

The sampling half of the seam: this is the only method that touches :attr:generator, so a caller wanting stated gains rather than drawn ones builds :class:HSVParams directly (WP-147).

Returns:

Type Description
HSVParams

The three sampled gains. Nothing is stashed on the instance --

HSVParams

last_gains is set by :meth:apply.

Examples:

>>> import torch
>>> gen = torch.Generator().manual_seed(0)
>>> jitter = HSVJitter(hsv_h=0.0, hsv_s=0.0, hsv_v=0.0, generator=gen)
>>> tuple(abs(gain) for gain in jitter.sample().as_tuple())
(0.0, 0.0, 0.0)

apply

apply(image: Tensor, targets: Targets, params: HSVParams) -> tuple[Tensor, Targets]

Apply params to image in HSV space, drawing nothing.

Parameters:

Name Type Description Default
image Tensor

CHW RGB image tensor with values in [0, 1].

required
targets Targets

Geometry carried alongside the image; returned untouched.

required
params HSVParams

The gains to apply.

required

Returns:

Type Description
tuple[Tensor, Targets]

The colour-jittered (C, H, W) image and the original targets.

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> image = torch.rand(3, 8, 8)
>>> out, _ = HSVJitter().apply(image, Targets.empty(), HSVParams(hue=0.0, saturation=0.0, value=0.0))
>>> torch.allclose(out, image, atol=1e-5)
True

HorizontalFlip

Left-right image flip with matching target mirroring (WP-013).

With probability p (drawn per call) the image columns are reversed and every target modality is mirrored about the vertical axis at x = (W - 1) / 2 — the axis the column reversal itself reflects about, since a coordinate names a sample point and the outermost samples sit at 0 and W - 1 (WP-154b). Boxes swap and reflect their x-extent (x1' = (W - 1) - x2, x2' = (W - 1) - x1), polygon and keypoint points reflect (x' = (W - 1) - x), and rotated boxes reflect their centre (cx' = (W - 1) - cx) with the long edge reflected and re-canonicalized (WP-058). When a dataset keypoint pair map is supplied, it then swaps left/right keypoint identities (WP-120) — coordinates and visibility flags together, so a mirrored "left elbow" arrives in the right slot carrying its own flag. With probability 1 - p the image and targets pass through unchanged.

The rotated-box mirror is exact — an isometry maps the rectangle to a rectangle, and -theta names the mirrored long edge up to the half turn a rectangle is invariant under — so nothing is dropped and, unlike :class:~lucid_yolo.data.affine.RandomAffine, this transform imposes no instance-axis requirement on the rotated modality.

Parameters:

Name Type Description Default
p float

Probability of flipping. Defaults to 0.5.

0.5
generator Generator | None

Optional :class:torch.Generator for a seeded, reproducible flip draw. Defaults to None (global RNG).

None
keypoint_flip_pairs list[tuple[int, int]] | None

Dataset-supplied (left_index, right_index) pairs on the keypoint axis. None mirrors every keypoint x-coordinate without an identity swap, the safe default when a task has no left/right symmetry.

None

Attributes:

Name Type Description
last_flipped bool | None

Whether the most recent call flipped, or None before the first call.

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> from lucid_yolo.data.transforms import GeometricTransform
>>> isinstance(HorizontalFlip(), GeometricTransform)
True
>>> flip = HorizontalFlip(p=1.0)
>>> image = torch.arange(4.0).reshape(1, 1, 4).expand(3, 1, 4).contiguous()
>>> boxes = torch.tensor([[0.0, 0.0, 1.0, 2.0]])
>>> _, out = flip(image, Targets(boxes=boxes, labels=torch.tensor([0])))
>>> out.boxes.tolist()
[[2.0, 0.0, 3.0, 2.0]]
>>> pose = Targets(
...     boxes=torch.zeros((1, 4)), labels=torch.tensor([0]),
...     keypoints=torch.tensor([[[1.0, 2.0], [3.0, 4.0]]]),
...     keypoint_vis=torch.tensor([[2, 1]]),
... )
>>> _, mirrored = HorizontalFlip(p=1.0, keypoint_flip_pairs=[(0, 1)])(
...     torch.zeros((3, 2, 4)), pose
... )
>>> mirrored.keypoints.tolist(), mirrored.keypoint_vis.tolist()
([[[0.0, 4.0], [2.0, 2.0]]], [[1, 2]])

__call__

__call__(image: Tensor, targets: Targets) -> tuple[Tensor, Targets]

Flip image and mirror targets with probability p.

Parameters:

Name Type Description Default
image Tensor

CHW image tensor.

required
targets Targets

Geometry to mirror when the image is flipped.

required

Returns:

Type Description
Tensor

Either the original (image, targets) (no flip) or the left-right

Targets

flipped image with every target modality mirrored.

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> flip = HorizontalFlip(p=0.0)
>>> image = torch.rand(3, 4, 6)
>>> out_image, _ = flip(image, Targets.empty())
>>> torch.equal(out_image, image)
True

sample

sample() -> FlipParams

Draw whether this call flips, consuming one uniform against p.

The sampling half of the seam: this is the only method that touches :attr:generator, so a caller wanting a stated flip rather than a drawn one builds :class:FlipParams directly (WP-147).

Returns:

Type Description
FlipParams

The drawn decision. Nothing is stashed on the instance --

FlipParams

last_flipped is set by :meth:apply.

Examples:

>>> HorizontalFlip(p=1.0).sample()
FlipParams(flipped=True)
>>> HorizontalFlip(p=0.0).sample()
FlipParams(flipped=False)

apply

apply(image: Tensor, targets: Targets, params: FlipParams) -> tuple[Tensor, Targets]

Mirror image and targets when params says so, drawing nothing.

Parameters:

Name Type Description Default
image Tensor

CHW image tensor.

required
targets Targets

Geometry to mirror when the image is flipped.

required
params FlipParams

Whether to mirror.

required

Returns:

Type Description
Tensor

Either the original (image, targets) or the left-right flipped image

Targets

with every target modality mirrored.

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> boxes = torch.tensor([[0.0, 0.0, 1.0, 2.0]])
>>> t = Targets(boxes=boxes, labels=torch.tensor([0]))
>>> _, out = HorizontalFlip().apply(torch.zeros(3, 2, 4), t, FlipParams(flipped=True))
>>> out.boxes.tolist()
[[2.0, 0.0, 3.0, 2.0]]

MosaicAssembly

Assemble four images and their targets into one 2S x 2S mosaic (WP-011).

Each call samples a centre (cx, cy) uniformly from [0.5S, 1.5S] on each axis, anchors the four images to that centre (image 0's bottom-right corner, image 1's bottom-left, image 2's top-right, image 3's top-left all meet at the centre), crops each to its quadrant, and grey-fills any uncovered canvas. Each image's boxes/polygons are shifted by its placement offset, clipped to the canvas, filtered by min_box_size and min_visibility, then merged via :meth:~lucid_yolo.data.targets.Targets.concat.

This is an assembly, not a :class:~lucid_yolo.data.transforms.GeometricTransform: it consumes four (image, targets) pairs rather than one, so its call signature deliberately differs from that protocol. The images may differ in size but must share a channel count, dtype and device.

Rotated boxes are shifted with their image, clipped to the canvas and filtered by the same two thresholds (WP-058, module docstring), with boxes recomputed as the envelope of the rotated geometry. Because that path filters, an input whose rboxes do not share the instance axis with its boxes, or that carries polygons alongside them, raises :class:ValueError.

Parameters:

Name Type Description Default
target_size int

The base size S; the assembled canvas is 2S x 2S.

required
generator Generator | None

Optional :class:torch.Generator for seeded, reproducible centre sampling. Defaults to None (global RNG).

None
pad_value float

Fill value for uncovered canvas. Defaults to 114 / 255.

_DEFAULT_PAD_VALUE
min_box_size float

Minimum clipped side length in pixels for an instance to be kept. Defaults to 2.0.

2.0
min_visibility float

Minimum kept-area fraction (clipped-extent area divided by pre-clip-extent area) for an instance to be kept. Defaults to 0.1.

0.1

Attributes:

Name Type Description
last_center tuple[int, int] | None

The (cx, cy) integer centre sampled on the most recent call, or None before the first call.

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> gen = torch.Generator().manual_seed(0)
>>> mosaic = MosaicAssembly(target_size=16, generator=gen)
>>> items = [(torch.zeros(3, 16, 16), Targets.empty()) for _ in range(4)]
>>> out_image, out_targets = mosaic(items)
>>> out_image.shape
torch.Size([3, 32, 32])
>>> out_targets.boxes.shape
torch.Size([0, 4])

__call__

__call__(items: list[tuple[Tensor, Targets]]) -> tuple[Tensor, Targets]

Assemble items into one mosaic image and its merged targets.

Parameters:

Name Type Description Default
items list[tuple[Tensor, Targets]]

Exactly four (image, targets) pairs. Each image is a CHW float tensor (in the same value range as the grey fill, i.e. [0, 1]); the images may differ in spatial size but must share a channel count, dtype and device. Non-empty rboxes must share the instance axis with that input's boxes, and polygon presence must be consistent across all four (a requirement of :meth:~lucid_yolo.data.targets.Targets.concat).

required

Returns:

Type Description
Tensor

The (C, 2S, 2S) stitched image and the shifted, clipped, filtered

Targets

and merged targets.

Raises:

Type Description
ValueError

If items does not hold exactly four pairs, or an input's non-empty rboxes break WP-056's instance-axis invariant.

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> gen = torch.Generator().manual_seed(0)
>>> mosaic = MosaicAssembly(target_size=8, generator=gen)
>>> box = Targets(boxes=torch.tensor([[1.0, 1.0, 5.0, 5.0]]), labels=torch.tensor([0]))
>>> items = [(torch.rand(3, 8, 8), box.clone()) for _ in range(4)]
>>> out_image, out_targets = mosaic(items)
>>> out_image.shape
torch.Size([3, 16, 16])
>>> mosaic.last_center is not None
True

sample

sample() -> MosaicParams

Draw the stitch centre, consuming two uniforms from the RNG.

The sampling half of the seam: this is the only method that touches :attr:generator, so a caller wanting a stated centre rather than a drawn one builds :class:MosaicParams directly (WP-147).

Returns:

Type Description
MosaicParams

The sampled centre, each axis an integer drawn uniformly from

MosaicParams

[0.5S, 1.5S]. Nothing is stashed on the instance -- last_center

MosaicParams

is set by :meth:apply.

Examples:

>>> import torch
>>> gen = torch.Generator().manual_seed(0)
>>> centre = MosaicAssembly(target_size=8, generator=gen).sample()
>>> 4 <= centre.center_x <= 12 and 4 <= centre.center_y <= 12
True

apply

apply(items: list[tuple[Tensor, Targets]], params: MosaicParams) -> tuple[Tensor, Targets]

Stitch items about params' centre, drawing nothing.

Parameters:

Name Type Description Default
items list[tuple[Tensor, Targets]]

Exactly four (image, targets) pairs, as :meth:__call__ requires.

required
params MosaicParams

The stitch centre to anchor the four images to.

required

Returns:

Type Description
Tensor

The (C, 2S, 2S) stitched image and the shifted, clipped, filtered

Targets

and merged targets.

Raises:

Type Description
ValueError

If items does not hold exactly four pairs, or an input's non-empty rboxes break WP-056's instance-axis invariant.

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> box = Targets(boxes=torch.tensor([[1.0, 1.0, 5.0, 5.0]]), labels=torch.tensor([0]))
>>> items = [(torch.rand(3, 8, 8), box.clone()) for _ in range(4)]
>>> out_image, _ = MosaicAssembly(target_size=8).apply(items, MosaicParams(center_x=8, center_y=8))
>>> out_image.shape
torch.Size([3, 16, 16])

Mixup

Convex pixel blend of two same-size images with concatenated targets (WP-012).

Each call draws a trigger uniform: with probability p the two images are blended lam * a + (1 - lam) * b with lam from Beta(alpha, alpha) and both target sets are concatenated unchanged (every box from both images is kept at full weight — detection-lineage mixup applies no lam label smoothing). With probability 1 - p the first pair passes through untouched.

This is an assembly, not a :class:~lucid_yolo.data.transforms.GeometricTransform: it consumes two (image, targets) pairs rather than one. The two images must share the same shape; concatenating the targets additionally requires consistent polygon presence (a :meth:~lucid_yolo.data.targets.Targets.concat rule).

Rotated boxes are carried through unchanged (WP-058): the blend moves no geometry, so both inputs' rboxes are concatenated exactly as their boxes and labels are, still canonical, with nothing clipped or dropped.

Parameters:

Name Type Description Default
p float

Probability of blending on a given call.

required
alpha float

Shared Beta concentration; both shape parameters equal alpha, so the blend factor is symmetric about 0.5 and (for large alpha) concentrated there. Defaults to 32.0.

32.0
generator Generator | None

Optional :class:torch.Generator for seeded, reproducible trigger and blend-factor draws. Defaults to None (global RNG).

None

Attributes:

Name Type Description
last_lam float | None

The blend factor used on the most recent call, or None if that call did not trigger (passed the first pair through) or has not run yet.

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> gen = torch.Generator().manual_seed(0)
>>> mixup = Mixup(p=1.0, generator=gen)
>>> a = (torch.ones(3, 8, 8), Targets(boxes=torch.zeros((1, 4)), labels=torch.tensor([0])))
>>> b = (torch.zeros(3, 8, 8), Targets(boxes=torch.ones((1, 4)), labels=torch.tensor([1])))
>>> out_image, out_targets = mixup([a, b])
>>> out_targets.labels.tolist()
[0, 1]
>>> bool(torch.allclose(out_image, torch.full_like(out_image, mixup.last_lam)))
True

__call__

__call__(items: list[tuple[Tensor, Targets]]) -> tuple[Tensor, Targets]

Blend items with probability p; otherwise return the first pair.

Parameters:

Name Type Description Default
items list[tuple[Tensor, Targets]]

Exactly two (image, targets) pairs. The two images must share shape, dtype and device; polygon presence must be consistent across both (a :meth:~lucid_yolo.data.targets.Targets.concat requirement). Rotated boxes, if any, are concatenated untouched.

required

Returns:

Type Description
Tensor

Either the blended (image, targets) — the convex pixel blend and the

Targets

concatenated targets — or, when the call does not trigger, the first

tuple[Tensor, Targets]

input pair untouched.

Raises:

Type Description
ValueError

If items does not hold exactly two pairs, or the two images differ in shape.

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> mixup = Mixup(p=0.0)
>>> a = (torch.ones(3, 4, 4), Targets.empty())
>>> b = (torch.zeros(3, 4, 4), Targets.empty())
>>> out_image, _ = mixup([a, b])
>>> torch.equal(out_image, a[0]) and mixup.last_lam is None
True

sample

sample() -> MixupParams

Draw the trigger and, when it fires, the blend factor.

The sampling half of the seam: this is the only method that touches :attr:generator, so a caller wanting a stated blend rather than a drawn one builds :class:MixupParams directly (WP-147). The trigger uniform is always consumed; the two gamma draws behind lam are consumed only when it fires, which is the draw order __call__ has always had.

Returns:

Type Description
MixupParams

The sampled decision: a blend factor, or None for a pass-through.

Examples:

>>> Mixup(p=0.0).sample()
MixupParams(lam=None)
>>> 0.0 <= (Mixup(p=1.0).sample().lam or 0.0) <= 1.0
True

apply

apply(items: list[tuple[Tensor, Targets]], params: MixupParams) -> tuple[Tensor, Targets]

Blend items at params' factor, or pass the first pair through.

Parameters:

Name Type Description Default
items list[tuple[Tensor, Targets]]

Exactly two (image, targets) pairs, as :meth:__call__ requires.

required
params MixupParams

The blend factor, or None for a pass-through.

required

Returns:

Type Description
Tensor

Either the blended (image, targets) or the first input pair

Targets

untouched.

Raises:

Type Description
ValueError

If the two images differ in shape, checked only when params calls for a blend -- a pass-through never reads the second image, which is the behaviour __call__ has always had.

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> a = (torch.ones(3, 4, 4), Targets.empty())
>>> b = (torch.zeros(3, 4, 4), Targets.empty())
>>> out_image, _ = Mixup(p=1.0).apply([a, b], MixupParams(lam=0.25))
>>> float(out_image[0, 0, 0])
0.25

CopyPaste

Paste polygon instances from one image onto another (WP-012).

The two (image, targets) inputs are the destination a and the source b. For each polygon-carrying instance of b a paste draw is taken; with probability p (and up to max_paste pastes) the instance's polygon is rasterised to a pixel mask, those source pixels overwrite the destination, and the instance's box/label/polygon are appended to a's targets. Instances of b without a polygon are never pasted — the mask is mandatory.

Because pasted instances always carry a polygon, the destination a must itself carry polygons (one ring per box) or be empty; otherwise the merged target set would mix polygon-present and polygon-absent instances, which :class:~lucid_yolo.data.targets.Targets rejects. This matches the segmentation lineage the augmentation comes from.

This is an assembly, not a :class:~lucid_yolo.data.transforms.GeometricTransform: it consumes two pairs rather than one. Both images must share the same shape.

Rotated boxes are not supported, for the reason the module docstring gives: the paste unit is a polygon mask and the oriented path carries no polygons (WP-056), so any input with a non-empty rboxes raises :class:NotImplementedError rather than have its rotated modality dropped.

Parameters:

Name Type Description Default
p float

Per-candidate-instance paste probability.

required
max_paste int | None

Optional cap on the number of instances pasted in one call. None (the default) means no cap — every candidate is drawn against p.

None
generator Generator | None

Optional :class:torch.Generator for seeded, reproducible paste draws. Defaults to None (global RNG).

None

Attributes:

Name Type Description
last_pasted int | None

The number of instances pasted on the most recent call, or None before the first call.

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> ring = torch.tensor([[1.0, 1.0], [4.0, 1.0], [4.0, 4.0], [1.0, 4.0]])
>>> src = Targets(boxes=torch.tensor([[1.0, 1.0, 4.0, 4.0]]), labels=torch.tensor([5]), polygons=[ring])
>>> a = (torch.zeros(3, 6, 6), Targets.empty())
>>> b = (torch.ones(3, 6, 6), src)
>>> copy_paste = CopyPaste(p=1.0)
>>> out_image, out_targets = copy_paste([a, b])
>>> out_targets.labels.tolist(), copy_paste.last_pasted
([5], 1)
>>> bool(out_image[0, 2, 2] == 1.0)
True

__call__

__call__(items: list[tuple[Tensor, Targets]]) -> tuple[Tensor, Targets]

Paste sampled source instances onto the destination image and targets.

Parameters:

Name Type Description Default
items list[tuple[Tensor, Targets]]

Exactly two (image, targets) pairs — destination first, source second. The two images must share shape, dtype and device; every rboxes must be empty. Only source instances that carry a polygon are paste candidates.

required

Returns:

Type Description
Tensor

The destination image with the pasted pixels and its targets with the

Targets

pasted instances appended. When nothing is pasted, a copy of the

tuple[Tensor, Targets]

destination image and a clone of its targets are returned.

Raises:

Type Description
ValueError

If items does not hold exactly two pairs, or the two images differ in shape.

NotImplementedError

If either input carries a non-empty rboxes.

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> ring = torch.tensor([[1.0, 1.0], [3.0, 1.0], [3.0, 3.0], [1.0, 3.0]])
>>> src = Targets(boxes=torch.tensor([[1.0, 1.0, 3.0, 3.0]]), labels=torch.tensor([2]), polygons=[ring])
>>> copy_paste = CopyPaste(p=0.0)
>>> a = (torch.zeros(3, 5, 5), Targets.empty())
>>> out_image, out_targets = copy_paste([a, (torch.ones(3, 5, 5), src)])
>>> torch.equal(out_image, a[0]) and out_targets.boxes.shape[0] == 0
True

sample

sample(candidate_count: int) -> CopyPasteParams

Draw each of candidate_count candidates against p, up to max_paste.

The sampling half of the seam: this is the only method that touches :attr:generator, so a caller wanting a stated selection rather than a drawn one builds :class:CopyPasteParams directly (WP-147). The draw count depends on the candidate count and the cap alone -- never on pixels -- which is what lets the loop split from the paste it used to interleave with.

Parameters:

Name Type Description Default
candidate_count int

Number of polygon-carrying source instances, i.e. len(targets_b.polygons).

required

Returns:

Type Description
CopyPasteParams

The selected indices, in paste order. Nothing is stashed on the instance

CopyPasteParams

-- last_pasted is set by :meth:apply.

Examples:

>>> CopyPaste(p=1.0).sample(3).selected
(0, 1, 2)
>>> CopyPaste(p=1.0, max_paste=1).sample(3).selected
(0,)
>>> CopyPaste(p=0.0).sample(3).selected
()

apply

apply(items: list[tuple[Tensor, Targets]], params: CopyPasteParams) -> tuple[Tensor, Targets]

Paste the instances params names onto the destination, drawing nothing.

Parameters:

Name Type Description Default
items list[tuple[Tensor, Targets]]

Exactly two (image, targets) pairs -- destination first, source second -- as :meth:__call__ requires.

required
params CopyPasteParams

The source instance indices to paste, in paste order.

required

Returns:

Type Description
Tensor

The destination image with the pasted pixels and its targets with the

Targets

pasted instances appended. When params selects nothing, a copy of the

tuple[Tensor, Targets]

destination image and a clone of its targets are returned.

Raises:

Type Description
ValueError

If the two images differ in shape.

NotImplementedError

If either input carries a non-empty rboxes. The guard is repeated here rather than left to :meth:__call__ because apply is its own entrance (WP-147): a caller building :class:CopyPasteParams directly never passes through the wrapper, and :meth:_merge would drop the modality instead of refusing it. :meth:__call__ keeps its own copy so that a rotated input is rejected before :meth:sample consumes a draw.

Examples:

>>> import torch
>>> from lucid_yolo.data.targets import Targets
>>> ring = torch.tensor([[1.0, 1.0], [4.0, 1.0], [4.0, 4.0], [1.0, 4.0]])
>>> src = Targets(boxes=torch.tensor([[1.0, 1.0, 4.0, 4.0]]), labels=torch.tensor([5]), polygons=[ring])
>>> items = [(torch.zeros(3, 6, 6), Targets.empty()), (torch.ones(3, 6, 6), src)]
>>> _, out = CopyPaste(p=1.0).apply(items, CopyPasteParams(selected=(0,)))
>>> out.labels.tolist()
[5]