Skip to content

Backend pipelines

Compose accepts registered transform objects from Kornia, TorchVision, and Albumentations, but it does not inherit every native container's input or numerical contract. The common safe interface is a batch-first BCHW torch tensor.

Native builder

Use from_params when you need rotation, scale, shear, translation, horizontal or vertical flips, brightness, or contrast without an optional augmentation backend:

from fuse_augmentations import Compose, ReorderPolicy

augment = Compose.from_params(
    rotation=(-20.0, 20.0),
    shear_x=(-5.0, 5.0),
    brightness=0.1,
    reorder=ReorderPolicy.NONE,
)

PyTorch is still mandatory; “native” means independent of an augmentation backend, not framework-free.

Kornia

import kornia.augmentation as K
import torch

from fuse_augmentations import Compose, ReorderPolicy

augment = Compose(
    [
        K.RandomRotation(degrees=15.0, p=0.8),
        K.RandomAffine(degrees=0.0, scale=(0.9, 1.1), p=0.7),
        K.RandomHorizontalFlip(p=0.5),
    ],
    reorder=ReorderPolicy.NONE,
)

output = augment(torch.rand(8, 3, 224, 224))

Kornia has the broadest declarative capability matrix in the current package. Registered transforms still use package-level segment interpolation and padding semantics.

TorchVision

import torch
import torchvision.transforms.v2 as T

from fuse_augmentations import Compose

augment = Compose(
    [
        T.RandomRotation(degrees=15.0),
        T.RandomAffine(degrees=0.0, scale=(0.9, 1.1)),
        T.RandomHorizontalFlip(p=0.5),
    ],
)

output = augment(torch.rand(8, 3, 224, 224))

The fused path requires BCHW input. A native TorchVision Compose may accept CHW tensors or PIL images; this package does not generally do so. RandomRotation(expand=True) is explicitly unsupported, and center/fill/interpolation behavior is not pixel-equivalent to native TorchVision.

Albumentations

The cross-backend tensor path accepts BCHW float tensors:

import albumentations as A
import torch

from fuse_augmentations import Compose

augment = Compose(
    [A.Rotate(limit=15, p=0.8), A.HorizontalFlip(p=0.5)],
    execution="cv2",
)

output = augment(torch.rand(4, 3, 224, 224))

execution="cv2" is the default CPU path. execution="torch" applies registered geometry through a batched torch sampling grid and can remain on a torch device, but its border and subpixel numerics differ from OpenCV. An opaque CPU-only Albumentations passthrough still transfers an accelerator tensor to the host and back; execution="torch" keeps the fused warp on-device but cannot make that native operation device-native. execution="auto" resolves per call — host data to cv2, accelerator data to torch — and pipeline.resolved_execution reports which one ran; it trades cross-environment bit-reproducibility for that convenience, so Reproducibility covers when it is the wrong choice.

An HWC NumPy path also exists. Without data_keys it transforms the image only:

import numpy as np

image = np.zeros((224, 224, 3), dtype=np.uint8)
result = augment(image=image)
assert result["image"].shape == image.shape

Declare data_keys and the same call carries masks, boxes, keypoints and rotated boxes:

with_boxes = Compose(
    [A.Affine(rotate=(-10.0, 10.0), p=1.0)],
    data_keys=["input", "bbox_xyxy"],
)
out = with_boxes(image=image, bboxes=np.zeros((4, 4), dtype=np.float32))
assert out["image"].dtype == np.uint8

A NumPy image comes back in the dtype it was passed in, as Albumentations returns it. Routed NumPy masks are labels rather than image intensities: their dtype and values are preserved through the round trip, and they are never normalized to [0, 1]; nearest sampling is the hard-label default. Labels and Albumentations' own processor behaviour are still not replicated — filtering instances after a warp is the caller's, and Auxiliary targets covers the helpers for it. Tensor input is a separate contract: images stay float32 [0, 1] throughout, while native Albumentations transforms that expect uint8 ranges can behave incorrectly on image tensors.

Mixed backends

import kornia.augmentation as K
import torchvision.transforms.v2 as T

from fuse_augmentations import Compose

augment = Compose(
    [
        T.RandomRotation(10.0),
        K.RandomHorizontalFlip(p=0.5),
        K.RandomBrightness(brightness=(0.9, 1.1), p=1.0),
    ],
)

Each transform is routed to its registered adapter. A backend change is a hard segment boundary: matrices from different backends are not combined into one warp. Inspect fusion_plan_descriptors to make the cost visible.

Passthrough warnings are contracts

An unregistered transform becomes a passthrough barrier when the adapter can safely call it. That does not mean the operation is transparent or target-safe.

Danger

With data_keys, an unknown or unclassified spatial transform is refused before any segment executes, so it cannot silently leave auxiliary targets stale. Image-only calls may still execute native passthrough; replace the transform with a registered operation or route targets through a native target-aware pipeline when you need explicit semantics.