Skip to content

Pipeline

The Pipeline class is the core builder for image and array processing pipelines.

Overview

from polars_cv import Pipeline

pipe = (
    Pipeline()
    .source("image_bytes")
    .resize(height=224, width=224)
    .grayscale()
)

API Reference

Pipeline

Bases: _OpsMixin

Modular pipeline builder for image and array operations.

A pipeline defines a sequence of operations that can be applied to a Polars expression using the .cv.pipe() accessor. The pipeline is executed when .sink() is called on the resulting expression.

Parameters that carry a value — sizes, offsets, factors, thresholds, fill values, kernel coefficients, and non-structural enums such as filter or interpolation — accept either a literal or a Polars expression, resolved per row at execution time.

Parameters that fix the output shape, rank, or dtype at planning time are literal-only, so the lazy schema cannot desync from the produced data: reduction axis, perceptual_hash(hash_size), reshape/transpose /flip axis lists, rotate(expand), cast(dtype), normalize(method/out_dtype) and histogram(closed/output). Passing an expression to one of those raises TypeError at build time.

Example
>>> from polars_cv import Pipeline
>>> import polars as pl
>>>
>>> # Define a reusable pipeline (without a sink)
>>> preprocess = (
...     Pipeline()
...     .source("image_bytes")
...     .resize(height=224, width=224)
...     .grayscale()
... )
>>>
>>> # Apply to a DataFrame and choose the output format at the sink
>>> df = pl.DataFrame({"image": [img_bytes]})
>>> result = df.with_columns(
...     processed=pl.col("image").cv.pipe(preprocess).sink("numpy")
... )

Pipelines support typed domain tracking for transitions between images, geometry, and numeric results: - buffer: Image/array data (default) - contour: Polygon geometry - scalar: Single numeric values - vector: Multiple numeric values (e.g., bounding boxes)

source

source(
    format: str = "auto",
    *,
    allowed_roots: Sequence[str] | None = None,
    cloud_options: CloudOptions
    | dict[str, Any]
    | None = None,
    decode_max_size: int | None = None,
    dtype: str | None = None,
    on_error: str | None = None,
    require_contiguous: bool | None = None,
) -> Pipeline

Define the input source format.

The default "auto" infers the decode path from the column's Polars dtype at runtime. Pass an explicit format to override the inference (or when the column dtype cannot be routed, such as a plain numeric column).

Image sources ("image_bytes" and "file_path") auto-detect the encoding and preserve its dtype: PNG/JPEG decode to u8, 16-bit PNG to u16, and TIFF may produce u8, u16, f32 or f64. Decoded images are always 3D [H, W, C]. Until then the dtype is "auto": a list/array sink needs it known at planning time, from dtype= here, a cast(), or an operation that fixes it.

A "contour" source decodes to the contour domain; rasterize it with :meth:rasterize.

Each keyword applies to some formats and not others, and defaults to None (the format's own default). One that does not apply to the chosen format is rejected, naming the formats it applies to.

decode_max_size asserts the pipeline needs at most this many pixels on the decoded long side, so JPEG decoding uses IDCT scaling (1/8, 1/4 or 1/2) to skip work. The long side never drops below min(decode_max_size, original), so a downstream resize to that size never upscales; other encodings decode at full size. A scaled decode followed by a resize is not bit-identical to a full decode and the same resize, hence the explicit opt-in.

allowed_roots restricts which locations a path column may read from. An entry that parses as a remote URI ("s3://bucket/public/") is matched as a URI prefix, anything else ("/srv/images") as a local directory. Local paths are canonicalized first, so .. and symlinks cannot escape, and matching is component-wise ("/srv/images" does not admit "/srv/images-private"). A path matching no entry is refused, and the refusal is subject to on_error.

Parameters:

Name Type Description Default
format str

How to decode the input column. array: A Polars fixed-size Array column, one nesting level per axis. auto: Infer the decode path from the column's Polars dtype: String → file_path, List/Array → list/array, Binary → blob if VIEW-tagged else image_bytes. blob: The self-describing VIEW protocol. contour: A contour column (one contour or a set per row), decoded as the contour set extract_contours produces. A mask is the rasterize op's. file_path: A path (local, s3://, gs://, az://, http://) whose contents decode like image bytes. image_bytes: Encoded image bytes (PNG/JPEG/TIFF/...), always decoded to [H, W, C]. list: A Polars nested List column, one nesting level per axis; its sizes may differ from row to row. raw: Raw bytes, decoded as a flat 1-D buffer of dtype.

'auto'
allowed_roots Sequence[str] | None

auto, file_path: Locations a path column may read from (unrestricted when absent; see above).

None
cloud_options CloudOptions | dict[str, Any] | None

auto, file_path: Cloud-storage credentials: a CloudOptions or a dict.

None
decode_max_size int | None

auto, file_path, image_bytes: Decode only enough pixels for this long side (JPEG IDCT scaling; see above).

None
dtype str | None

array, list: Element dtype; inferred from the column when absent. auto: Asserted element dtype (a decoded image is cast to it). blob: Declared element dtype. A blob carries its own, so a declaration is checked at decode: a blob of another dtype is a row error. file_path, image_bytes: Asserted element dtype: a decoded image with another dtype is cast. raw: The element dtype: raw bytes carry no type metadata, so it is required.

None
on_error str | None

array, auto, blob, contour, image_bytes, list, raw: "raise" or "null": what a row that cannot be decoded does. file_path: "raise" or "null": what a row that cannot be read or decoded does.

None
require_contiguous bool | None

array, list: Require rectangular data (zero- copy); jagged rows are then an error. auto: Require rectangular data when the column is a List/Array.

None
Example
>>> # Decode PNG/JPEG bytes from a column
>>> pipe = Pipeline().source("image_bytes").resize(height=224, width=224)
>>>
>>> # Read from file paths or URLs, sandboxed
>>> pipe = Pipeline().source("file_path", allowed_roots=["/srv/images"])
>>>
>>> # Assert dtype for a list sink (cast if needed at runtime)
>>> pipe = Pipeline().source("image_bytes", dtype="f32")
>>>
>>> # Gracefully handle corrupt images as null
>>> pipe = Pipeline().source("image_bytes", on_error="null")
>>>
>>> # Rasterize a contour column to a mask
>>> pipe = Pipeline().source("contour").rasterize(width=64, height=64)

abs

abs() -> Pipeline

Absolute value (|x|).

Domain: buffer → buffer

add_constant

add_constant(value: FloatOrExpr) -> Pipeline

Add a constant to every value (x + value).

Domain: buffer → buffer

Parameters:

Name Type Description Default
value FloatOrExpr

Constant addend (literal or per-row expression).

required

adjust_contrast

adjust_contrast(factor: FloatOrExpr) -> Pipeline

Adjust image contrast: (pixel - mean) * factor + mean.

Domain: buffer → buffer

Parameters:

Name Type Description Default
factor FloatOrExpr

Contrast factor. 1.0 = no change, >1 = more contrast, <1 = less.

required
Example

pipe = Pipeline().source("image_bytes").adjust_contrast(factor=1.5)

adjust_gamma

adjust_gamma(gamma: FloatOrExpr) -> Pipeline

Apply gamma (power-law) correction: normalize to [0,1], raise to gamma, denormalize.

Domain: buffer → buffer

Parameters:

Name Type Description Default
gamma FloatOrExpr

Gamma value. <1 = brighter, >1 = darker, 1.0 = no change.

required
Example

pipe = Pipeline().source("image_bytes").adjust_gamma(gamma=0.5)

apply_lut

apply_lut(
    table: Sequence[Sequence[FloatOrExpr]],
    *,
    out_dtype: str = "u8",
) -> Pipeline

Map each value through a lookup table.

Each u8 or u16 value is a row index into table. Rows of one entry map every element in place, on any channel count (as cv2.LUT with one table); rows of K > 1 entries take a single-channel image to K channels (a palette). The entries are stored as out_dtype. A value with no row is an error for its row: clamp first, keeping the dtype (clamp(0, len(table) - 1, out_dtype="u8")), to send the excess to the last row.

Domain: buffer → buffer

Parameters:

Name Type Description Default
table Sequence[Sequence[FloatOrExpr]]

The rows, indexed by value: 1 to 65536 of them, each of the same length (1, or the palette's channel count). Each entry must fit out_dtype, and may be a Polars expression; the lengths are literal.

required
out_dtype str

The output element dtype the entries are stored as.

'u8'
Example

heat = Pipeline().source("image_bytes").grayscale().apply_lut([[v, 0, 255 - v] for v in range(256)])

blur

blur(sigma: FloatOrExpr) -> Pipeline

Apply Gaussian blur.

Domain: buffer → buffer

Parameters:

Name Type Description Default
sigma FloatOrExpr

Standard deviation for Gaussian kernel.

required

canny

canny(
    *,
    low_threshold: FloatOrExpr = 50.0,
    high_threshold: FloatOrExpr = 150.0,
) -> Pipeline

Canny edge detection, as cv2.Canny(image, low, high) computes it: 3x3 Sobel gradients (L1 magnitude), non-maximum suppression and double-threshold hysteresis. There is no pre-blur — add .blur() first for a smoothed edge map, as with OpenCV. A colour image uses, per pixel, the channel with the strongest gradient; alpha is ignored. Output is a U8 binary edge map (0 or 255).

Domain: buffer → buffer

Parameters:

Name Type Description Default
low_threshold FloatOrExpr

Lower hysteresis threshold.

50.0
high_threshold FloatOrExpr

Upper hysteresis threshold.

150.0
Example

edges = Pipeline().source("image_bytes").blur(sigma=1.4).canny(low_threshold=50, high_threshold=150)

cast

cast(dtype: str) -> Pipeline

Cast to a different data type.

Domain: buffer → buffer

Parameters:

Name Type Description Default
dtype str

Target data type (e.g., "f32", "u8").

required

ceil

ceil() -> Pipeline

Round toward positive infinity.

Domain: buffer → buffer

channel_select

channel_select(
    index: IntOrExpr, *, keepdims: bool = True
) -> Pipeline

Extract one channel of a multi-channel image: [H, W, 1] from a [H, W, C] input (the shape grayscale and rasterize give), or [H, W] with keepdims=False. A [H, W] input is returned as it is.

Domain: buffer → buffer

Parameters:

Name Type Description Default
index IntOrExpr

Channel index to extract (0-based). Accepts a Polars expression for per-row dynamic selection.

required
keepdims bool

Keep the channel axis, as size 1 (default), or drop it.

True
Example

pipe = Pipeline().source("image_bytes").channel_select(index=0) # Red channel

channel_swap

channel_swap(order: Sequence[IntOrExpr]) -> Pipeline

Reorder channels in a multi-channel image.

Domain: buffer → buffer

Parameters:

Name Type Description Default
order Sequence[IntOrExpr]

New channel ordering, e.g. [2, 1, 0] for RGB-to-BGR. Each index may be a literal or a Polars expression, so the permutation can vary per row. The list length is the channel count and must be literal.

required
Example

pipe = Pipeline().source("image_bytes").channel_swap(order=[2, 1, 0])

clamp_max

clamp_max(value: FloatOrExpr) -> Pipeline

Cap values at value (min(x, value)); one-sided clamp.

Domain: buffer → buffer

Parameters:

Name Type Description Default
value FloatOrExpr

Upper bound (literal or per-row expression).

required

clamp_min

clamp_min(value: FloatOrExpr) -> Pipeline

Floor values at value (max(x, value)); one-sided clamp.

Domain: buffer → buffer

Parameters:

Name Type Description Default
value FloatOrExpr

Lower bound (literal or per-row expression).

required

area

area(*, signed: BoolOrExpr = False) -> Pipeline

Compute the area of the contour using the Shoelace formula.

The area of the region the contour describes: the exterior minus the union of its hole rings, in either winding direction. Overlapping or nested hole rings are not double-subtracted. One value per contour for a contour set.

Domain: contour → vector

Parameters:

Name Type Description Default
signed BoolOrExpr

If True, return signed area (negative for CW winding).

False

boundary

boundary() -> Pipeline

A region's rings as open lines: the exterior, then each hole, each returning to its first point. The result is a set (holes add rings); an open contour, already a line, is refused.

Domain: contour → contour

bounding_box

bounding_box() -> Pipeline

Compute the axis-aligned bounding box of the contour.

Returns [x, y, width, height].

Domain: contour → vector

centroid

centroid() -> Pipeline

Compute the centroid (center of mass) of the contour.

Measured on the same region as area() — the exterior minus the union of the hole rings — so overlapping or nested holes are not subtracted twice.

Returns [x, y].

Domain: contour → vector

close

close() -> Pipeline

Join an open line's ends into the region it bounds. A line that returns to its first point (as boundary() draws one) drops the repeat, so close() undoes boundary() on a region without holes. A closed contour is refused.

Domain: contour → contour

convex_hull

convex_hull() -> Pipeline

Compute the convex hull of the contour.

Domain: contour → contour

densify

densify(max_segment_length: FloatOrExpr) -> Pipeline

Add points so no edge is longer than max_segment_length: each edge is split into ceil(length / max_segment_length) equal parts, and every existing point is kept. A region's closing edges are split too.

Domain: contour → contour

Parameters:

Name Type Description Default
max_segment_length FloatOrExpr

The longest edge allowed, a finite length > 0 (literal or expression).

required

largest

largest(*, k: IntOrExpr = 1) -> Pipeline

Keep the k largest contours of the set, by area: largest first, equal areas in their input order. A set of fewer keeps them all.

Domain: contour → contour

Parameters:

Name Type Description Default
k IntOrExpr

How many contours to keep, >= 1 (literal or expression).

1

offset

offset(
    distance: FloatOrExpr, *, join: str | Expr = "round"
) -> Pipeline

Grow the set's regions by distance (negative: shrink them): every point within distance of their union, or inside it and at least -distance from its edge.

The set is read as one union, so growing can merge regions and shrinking can split them or leave none: the result is always a set. Convex corners turn by join; concave ones stay sharp. Round arcs are polygons within 1/64 pixel of the true arc, or about distance / 2000 past a distance of 32 pixels. An open contour is refused.

Domain: contour → contour

Parameters:

Name Type Description Default
distance FloatOrExpr

How far to move the edge, in pixels: positive grows, negative shrinks (literal or expression).

required
join str | Expr

"round" (an arc about each corner), "miter" (the edges extended to meet, cut off past 4 times distance) or "bevel" (cut straight across). Accepts an expression.

'round'

perimeter

perimeter() -> Pipeline

Compute the perimeter (arc length) of the contour.

Domain: contour → vector

scale_contour

scale_contour(
    *,
    sx: FloatOrExpr,
    sy: FloatOrExpr,
    origin: str | Expr = "centroid",
) -> Pipeline

Scale the contour about origin.

Domain: contour → contour

Parameters:

Name Type Description Default
sx FloatOrExpr

X scale factor.

required
sy FloatOrExpr

Y scale factor.

required
origin str | Expr

Point to scale about — "centroid" (center of mass), "bbox_center" (bounding-box center) or "origin" (the coordinate origin (0, 0)). Accepts an expression for a per-row choice: which point the scale is measured from changes no output shape, rank or dtype.

'centroid'

simplify

simplify(tolerance: FloatOrExpr) -> Pipeline

Simplify the contour using the Douglas-Peucker algorithm.

Domain: contour → contour

Parameters:

Name Type Description Default
tolerance FloatOrExpr

Maximum distance from the original contour.

required

smooth

smooth(*, iterations: IntOrExpr = 1) -> Pipeline

Round off corners by Chaikin's corner cutting: each pass replaces every edge by its points a quarter and three quarters along, doubling the point count. A region's rings are cut all the way round; an open line keeps its two ends, unless it returns to its first point (as boundary() draws one), when it is cut through it as a loop.

Domain: contour → contour

Parameters:

Name Type Description Default
iterations IntOrExpr

Passes to make; 0 returns the contour as it is (literal or expression).

1

stroke

stroke(
    *,
    width: FloatOrExpr = 1.0,
    cap: str | Expr = "round",
    join: str | Expr = "round",
) -> Pipeline

The region the set's paths cover when stroked, as disjoint regions: every point within width / 2 of a path, its ends shaped by cap and its corners by join. Every member is read as its outline: an open line, capped at its ends (one returning to its first point, as boundary() draws one, is a closed path), and a region's rings, each a closed path with no ends. A ring of fewer than two points has no edge and covers nothing; a one-point line is a dot (its caps). The result is always a set, filled by rasterize(): stroke(width=2).rasterize(...) draws lines and outlines. Round arcs are polygons within 1/64 pixel of the true arc, or about width / 4000 past a width of 64 pixels.

Domain: contour → contour

Parameters:

Name Type Description Default
width FloatOrExpr

The stroke's full width in pixels, a finite width > 0 (literal or expression).

1.0
cap str | Expr

How an open line ends: "round" (a half disc), "square" (a half square) or "butt" (flat at its end point). Accepts an expression.

'round'
join str | Expr

How a path turns a corner: "round", "miter" (cut off past 4 half-widths) or "bevel". Accepts an expression.

'round'

translate

translate(*, dx: FloatOrExpr, dy: FloatOrExpr) -> Pipeline

Translate the contour by an offset.

Domain: contour → contour

Parameters:

Name Type Description Default
dx FloatOrExpr

X offset (horizontal translation).

required
dy FloatOrExpr

Y offset (vertical translation).

required

union_all

union_all() -> Pipeline

The union of the set's regions, as disjoint regions: overlapping members merge and a hole another member covers is filled. The result is always a set. An open contour is refused.

Domain: contour → contour

convolve2d

convolve2d(
    kernel: Sequence[FloatOrExpr],
    *,
    normalize: BoolOrExpr = False,
    border: str | Expr = "replicate",
) -> Pipeline

Apply generic 2D convolution with an arbitrary kernel.

Domain: buffer → buffer

Parameters:

Name Type Description Default
kernel Sequence[FloatOrExpr]

Flattened square kernel, row-major: its length is the square of an odd side (9 for 3×3, 25 for 5×5, ...), which is the kernel's size. Each coefficient may be a literal float or a Polars expression, so a batch can convolve with a different kernel per row; the length is structural.

required
normalize BoolOrExpr

If True, divide output by the sum of absolute kernel values.

False
border str | Expr

Border handling mode ("replicate", "zero", "reflect").

'replicate'
Example
>>> edge = Pipeline().source("image_bytes").convolve2d(
...     [-1, -1, -1, -1, 8, -1, -1, -1, -1]
... )

coordinate_grid

coordinate_grid(
    *, normalized: BoolOrExpr = False
) -> Pipeline

The pixel coordinates of the image: an f32 [H, W, 2] buffer holding each pixel's centre (x + 0.5, y + 0.5) — the point rasterize samples — or ((x + 0.5) / W, (y + 0.5) / H) when normalized.

Reads only the input's height and width, never its values, so it takes any dtype and channel count. The coordinates are in the frame of the buffer it follows: after a crop they start at the crop's corner. Combine them with ordinary ops for fields over positions (gradients, radial falloffs, vignettes).

Domain: buffer → buffer

Parameters:

Name Type Description Default
normalized BoolOrExpr

Divide x by the width and y by the height, so each lies in (0, 1). Accepts a Polars expression for a per-row choice.

False
Example

dx2 = Pipeline().source("image_bytes").coordinate_grid().add_constant(-32.0).square()

crop

crop(
    *,
    top: IntOrExpr = 0,
    left: IntOrExpr = 0,
    height: IntOrExpr | None = None,
    width: IntOrExpr | None = None,
) -> Pipeline

Extract a rectangular region.

Domain: buffer → buffer

Parameters:

Name Type Description Default
top IntOrExpr

Top offset.

0
left IntOrExpr

Left offset.

0
height IntOrExpr | None

Crop height (None = to end).

None
width IntOrExpr | None

Crop width (None = to end).

None

convert_color

convert_color(
    *, from_space: str, to_space: str
) -> Pipeline

Convert between color spaces.

Domain: buffer → buffer

Parameters:

Name Type Description Default
from_space str

Source color space (rgb, bgr, hsv, lab, ycbcr, gray).

required
to_space str

Target color space (rgb, bgr, hsv, lab, ycbcr, gray).

required
Example

pipe = Pipeline().source("image_bytes").convert_color(from_space="rgb", to_space="hsv")

dilate

dilate(
    *,
    ksize: IntOrExpr = 3,
    iterations: IntOrExpr = 1,
    footprint: str | Expr = "square",
) -> Pipeline

Morphological dilation: the local maximum over a footprint (a ksize × ksize square, or a disk), edges replicated. Requires single-channel input (e.g. after .grayscale() or .threshold()).

Domain: buffer → buffer

Parameters:

Name Type Description Default
ksize IntOrExpr

Size of the structuring element (the square's side, the disk's diameter). Must be odd and >= 1. Accepts a Polars expression for per-row dynamic values.

3
iterations IntOrExpr

Number of times the operation is applied. Accepts a Polars expression for per-row dynamic values.

1
footprint str | Expr

The structuring element: "square" (ksize x ksize) or "disk" (the pixels within Euclidean distance ksize // 2 of the centre). Accepts a Polars expression for a per-row choice.

'square'
Example

mask = Pipeline().source("image_bytes").grayscale().threshold(128).dilate(ksize=3)

distance_transform

distance_transform(
    *, signed: BoolOrExpr = False
) -> Pipeline

Exact Euclidean distance transform of a mask: an f32 image of the same shape. Requires single-channel input.

A pixel is foreground when it is non-zero (NaN included). Unsigned, each foreground pixel holds the distance from its centre to the nearest background pixel's centre and the background holds 0, as cv2.distanceTransform(DIST_L2, DIST_MASK_PRECISE) and scipy.ndimage.distance_transform_edt compute. Signed, every pixel holds its distance to the boundary between the two classes, the pixel edges extract_contours traces: negative inside, positive outside (as .point.signed_distance_to_contour), ±(d - 0.5) for the centre distance d to the nearest pixel of the other class. When a class has no pixel at all, the distance to it is infinite.

Domain: buffer → buffer

Parameters:

Name Type Description Default
signed BoolOrExpr

Signed distance to the mask's boundary (negative inside) instead of each foreground pixel's distance to the background. Accepts a Polars expression for a per-row choice.

False
Example

sdf = Pipeline().source("image_bytes").grayscale().threshold(128).distance_transform(signed=True)

equalize_histogram

equalize_histogram() -> Pipeline

Apply histogram equalization for contrast enhancement: map each pixel through the normalized CDF, per channel. Output is U8.

Domain: buffer → buffer

Example

eq = Pipeline().source("image_bytes").grayscale().equalize_histogram()

erode

erode(
    *,
    ksize: IntOrExpr = 3,
    iterations: IntOrExpr = 1,
    footprint: str | Expr = "square",
) -> Pipeline

Morphological erosion: the local minimum over a footprint (a ksize × ksize square, or a disk), edges replicated. Requires single-channel input (e.g. after .grayscale() or .threshold()).

Domain: buffer → buffer

Parameters:

Name Type Description Default
ksize IntOrExpr

Size of the structuring element (the square's side, the disk's diameter). Must be odd and >= 1. Accepts a Polars expression for per-row dynamic values.

3
iterations IntOrExpr

Number of times the operation is applied. Accepts a Polars expression for per-row dynamic values.

1
footprint str | Expr

The structuring element: "square" (ksize x ksize) or "disk" (the pixels within Euclidean distance ksize // 2 of the centre). Accepts a Polars expression for a per-row choice.

'square'
Example

mask = Pipeline().source("image_bytes").grayscale().threshold(128).erode(ksize=3)

expand_dims

expand_dims(axis: int) -> Pipeline

Insert an axis of size 1: [H, W] to [H, W, 1] with axis=-1. The same elements in the same order, with no copy.

Domain: buffer → buffer

Parameters:

Name Type Description Default
axis int

Where the new axis goes in the output, counted from the end when negative.

required
Example

pipe = Pipeline().source("array").expand_dims(-1)

extract_contours

extract_contours(
    *,
    mode: str | Expr = "external",
    method: str | Expr = "simple",
    min_area: FloatOrExpr | None = None,
    min_area_fraction: FloatOrExpr | None = None,
) -> Pipeline

Extract contours from binary mask.

The traced outline runs along the edges of the boundary pixels (pixel (x, y) is the square [x, x+1] x [y, y+1]), so it bounds exactly the pixels of its region: a blob filling w x h pixels comes back bounding w x h, a single pixel as its unit square, and every region's area is its pixel count. Rasterizing the result gives back the mask.

Regions are 8-connected: pixels touching only at a corner are one region, whose outline passes through that corner twice.

Borders come back as a flat list with no hierarchy. mode="all" yields the exterior plus one border for each enclosed background region — holes that touch or nest enclose one region between them — and reassembling a holed contour from those is the caller's job. mode="external" keeps only the outermost, discarding hole borders.

Domain: buffer → contour

Parameters:

Name Type Description Default
mode str | Expr

"external" (outer borders only) or "all" (hole borders too, as a flat list).

'external'
method str | Expr

"simple" (remove redundant), "none" (all points), "approx".

'simple'
min_area FloatOrExpr | None

Filter small contours: the minimum area in pixels. Accepts a Polars expression for per-row dynamic thresholds.

None
min_area_fraction FloatOrExpr | None

Filter small contours relative to the image: the minimum area as a fraction of its height x width, in (0, 1], resolved per image as it runs (a speck threshold that scales with resolution). Applies together with min_area: a contour must pass both. Accepts a Polars expression.

None

extract_shape

extract_shape() -> Pipeline

Extract the buffer's shape: one Float64 per dimension, in order ([height, width, channels] for an image), as a vector.

Domain: buffer → vector

Returns:

Type Description
Pipeline

List(Float64) of the dimension sizes.

flip

flip(axes: Sequence[int]) -> Pipeline

Flip along specified axes.

Domain: buffer → buffer

Parameters:

Name Type Description Default
axes Sequence[int]

Axes to flip.

required

floor

floor() -> Pipeline

Round toward negative infinity.

Domain: buffer → buffer

grayscale

grayscale() -> Pipeline

Convert to grayscale (luminance 0.299R + 0.587G + 0.114B).

Domain: buffer → buffer

histogram

histogram(
    *,
    bins: IntOrExpr | Sequence[float] = 256,
    range: Sequence[FloatOrExpr] | None = None,
    closed: str = "left",
    output: str = "buckets",
) -> Pipeline

Compute pixel value histogram.

Bins follow numpy.histogram: a value outside the range or the edges is in no bin (not counted; "quantized" gives it the index one past the last bin), as is NaN. Infinite outer edges are open bounds, e.g. bins=[-inf, 0, 10, inf] keeps every value.

Domain: buffer → vector; with output="quantized": buffer → buffer

Parameters:

Name Type Description Default
bins IntOrExpr | Sequence[float]

Number of bins (default 256), a Polars expression for per-row dynamic bin count, or an explicit list of non-decreasing bin edges (the outer ones may be infinite).

256
range Sequence[FloatOrExpr] | None

(min, max) tuple, finite. Auto-detected if None, widened to hold every value.

None
closed str

"left" or "right" interval inclusiveness (default "left").

'left'
output str

"buckets" (list of structs), "counts" (bin counts), "normalized" (sum to 1.0), "quantized" (pixel indices), "edges" (bin edges).

'buckets'
Example

Pipeline().source("image_bytes").grayscale().histogram(bins=8)

invert

invert() -> Pipeline

Invert values, keeping the dtype: MAX + MIN - x for an integer (255 - x for u8, -1 - x for a signed dtype), 1 - x for a float.

Domain: buffer → buffer

label_reduce

label_reduce(
    contours: Expr,
    *,
    reduction: str | Expr = "max",
    region_mode: str | Expr = "interior",
) -> Pipeline

Score contour regions against the current buffer values.

This is the buffer-space variant of label reduction. It accepts contours via a Polars expression and returns one score per contour.

Pixels are sampled at their centres. A contour with no area (a point or a line) or whose region catches no pixel centre (a sub-pixel contour) is scored on the pixels its outline passes through, in every region mode, rather than as 0.0.

Domain: buffer → vector

Parameters:

Name Type Description Default
contours Expr

Contour-set expression (List[Contour]) to score.

required
reduction str | Expr

Reduction over contour region values ("max", "mean", "sum").

'max'
region_mode str | Expr

Region selection mode. "interior" — only pixels strictly inside the contour polygon. "boundary" — interior pixels plus pixels whose centre lies on the contour boundary. "bbox" — all pixels within the bounding box.

'interior'

letterbox

letterbox(
    *,
    height: IntOrExpr,
    width: IntOrExpr,
    value: FloatOrExpr = 0.0,
    filter: str | Expr = "lanczos3",
) -> Pipeline

Resize image maintaining aspect ratio and pad to exact target size: fit within the target, then pad with centered positioning.

Domain: buffer → buffer

Parameters:

Name Type Description Default
height IntOrExpr

Target height (literal or expression).

required
width IntOrExpr

Target width (literal or expression).

required
value FloatOrExpr

Fill value for padding (default 0, typically black). Accepts a Polars expression for per-row dynamic values.

0.0
filter str | Expr

Resampling filter for the resize step (default "lanczos3").

'lanczos3'
Example

pipe = Pipeline().source("image_bytes").letterbox(height=224, width=224)

morphology_gradient

morphology_gradient(
    *,
    ksize: IntOrExpr = 3,
    footprint: str | Expr = "square",
) -> Pipeline

Morphological gradient (dilate - erode): an edge outline. Requires single-channel input.

Domain: buffer → buffer

Parameters:

Name Type Description Default
ksize IntOrExpr

Size of the structuring element. Must be odd and >= 1. Accepts a Polars expression for per-row dynamic values.

3
footprint str | Expr

The structuring element: "square" (ksize x ksize) or "disk" (the pixels within Euclidean distance ksize // 2 of the centre). Accepts a Polars expression for a per-row choice.

'square'
Example

edges = Pipeline().source("image_bytes").grayscale().threshold(128).morphology_gradient(ksize=3)

neg

neg() -> Pipeline

Negate every value (-x).

Domain: buffer → buffer

normalize

normalize(
    *,
    method: str = "minmax",
    mean: Sequence[FloatOrExpr] | None = None,
    std: Sequence[FloatOrExpr] | None = None,
    out_dtype: str | None = None,
) -> Pipeline

Normalize values to a standard range.

Domain: buffer → buffer

Parameters:

Name Type Description Default
method str

Normalization method: "minmax" scales values to [0, 1] using per- element min/max; "zscore" standardizes to mean=0, std=1 using per- element statistics; "preset" applies ImageNet-style channel-wise normalization, (x - mean[c]) / std[c], with the given mean and std.

'minmax'
mean Sequence[FloatOrExpr] | None

Per-channel mean values; required for, and only valid with, method="preset" (e.g. ImageNet [0.485, 0.456, 0.406]). Each element may be a literal float or a Polars expression; the list length is the channel count.

None
std Sequence[FloatOrExpr] | None

Per-channel standard deviation values; required for, and only valid with, method="preset" (e.g. ImageNet [0.229, 0.224, 0.225]). Each element accepts an expression, as with mean.

None
out_dtype str | None

Output dtype (default f32). Normalization computes in f32 and the result is cast to this dtype at execution. For half precision use the sink dtype instead (.sink("numpy", dtype="f16")).

None
Example

Pipeline().source().normalize(method="minmax") Pipeline().source().normalize( ... method="preset", ... mean=[0.485, 0.456, 0.406], ... std=[0.229, 0.224, 0.225], ... )

pad

pad(
    *,
    top: IntOrExpr = 0,
    bottom: IntOrExpr = 0,
    left: IntOrExpr = 0,
    right: IntOrExpr = 0,
    value: FloatOrExpr = 0.0,
    mode: str | Expr = "constant",
) -> Pipeline

Add padding to the image.

Domain: buffer → buffer

Parameters:

Name Type Description Default
top IntOrExpr

Padding on top edge.

0
bottom IntOrExpr

Padding on bottom edge.

0
left IntOrExpr

Padding on left edge.

0
right IntOrExpr

Padding on right edge.

0
value FloatOrExpr

Fill value for "constant" mode (default 0). Accepts a Polars expression for per-row dynamic values.

0.0
mode str | Expr

Padding mode - "constant", "edge", "reflect", "symmetric".

'constant'
Example

pipe = Pipeline().source("image_bytes").pad(top=10, bottom=10) pipe = Pipeline().source("image_bytes").pad(left=20, right=20, value=128)

pad_to_size

pad_to_size(
    *,
    height: IntOrExpr,
    width: IntOrExpr,
    position: str | Expr = "center",
    value: FloatOrExpr = 0.0,
) -> Pipeline

Pad image to exact target size (computed at runtime). A larger image is not cropped - resize first if needed.

Domain: buffer → buffer

Parameters:

Name Type Description Default
height IntOrExpr

Target height.

required
width IntOrExpr

Target width.

required
position str | Expr

Where to place original content: "center" (default), "top-left" or "bottom-right".

'center'
value FloatOrExpr

Fill value for padding (default 0). Accepts a Polars expression for per-row dynamic values.

0.0
Example

pipe = Pipeline().source("image_bytes").pad_to_size(height=100, width=200)

perceptual_hash

perceptual_hash(
    *, algorithm: str = "perceptual", hash_size: int = 64
) -> Pipeline

Compute a perceptual hash fingerprint.

Domain: buffer → vector

Parameters:

Name Type Description Default
algorithm str

"perceptual" (pHash), "average" (aHash), "difference" (dHash).

'perceptual'
hash_size int

Number of bits in the hash (must be power of 2). It fixes the output vector length, so it is literal-only.

64
Example

Pipeline().source("image_bytes").perceptual_hash()

reciprocal

reciprocal() -> Pipeline

Reciprocal (1 / x; ±inf at zero).

Domain: buffer → buffer

reduce_argmax

reduce_argmax(axis: int) -> Pipeline

Index of the maximum value along an axis.

Unlike other reductions it always requires an axis: a global index is ambiguous for a multi-dimensional array. The result has the reduced shape and an i64 dtype.

Domain: buffer → buffer, vector → buffer

Parameters:

Name Type Description Default
axis int

Axis along which to find the index.

required

reduce_argmin

reduce_argmin(axis: int) -> Pipeline

Index of the minimum value along an axis.

Unlike other reductions it always requires an axis: a global index is ambiguous for a multi-dimensional array. The result has the reduced shape and an i64 dtype.

Domain: buffer → buffer, vector → buffer

Parameters:

Name Type Description Default
axis int

Axis along which to find the index.

required

reduce_max

reduce_max(*, axis: int | None = None) -> Pipeline

Reduce buffer by computing the maximum value.

buffer → scalar, vector → scalar; with axis: buffer → buffer, vector

→ buffer

Parameters:

Name Type Description Default
axis int | None

Axis to reduce along. None for global reduction. It fixes the output rank, so it is literal-only.

None

reduce_mean

reduce_mean(*, axis: int | None = None) -> Pipeline

Compute arithmetic mean.

buffer → scalar, vector → scalar; with axis: buffer → buffer, vector

→ buffer

Parameters:

Name Type Description Default
axis int | None

Axis to reduce along. None for global reduction. It fixes the output rank, so it is literal-only.

None

reduce_min

reduce_min(*, axis: int | None = None) -> Pipeline

Reduce buffer by computing the minimum value.

buffer → scalar, vector → scalar; with axis: buffer → buffer, vector

→ buffer

Parameters:

Name Type Description Default
axis int | None

Axis to reduce along. None for global reduction. It fixes the output rank, so it is literal-only.

None

reduce_percentile

reduce_percentile(q: FloatOrExpr) -> Pipeline

Compute the q-th percentile of all values (linear interpolation, as numpy's default).

Domain: buffer → scalar, vector → scalar

Parameters:

Name Type Description Default
q FloatOrExpr

Percentile to compute, in [0, 100]. Accepts a Polars expression for per- row dynamic values.

required

reduce_popcount

reduce_popcount() -> Pipeline

Count set bits (1s) in the buffer.

Domain: buffer → scalar, vector → scalar

reduce_std

reduce_std(
    *, axis: int | None = None, ddof: IntOrExpr = 0
) -> Pipeline

Reduce buffer by computing the standard deviation.

buffer → scalar, vector → scalar; with axis: buffer → buffer, vector

→ buffer

Parameters:

Name Type Description Default
axis int | None

Axis to reduce along. None for global reduction.

None
ddof IntOrExpr

Delta degrees of freedom. 0 for population std (default), 1 for sample std. Accepts a Polars expression for per-row dynamic values.

0
Example

pipe = Pipeline().source("image_bytes").reduce_std(ddof=1)

reduce_sum

reduce_sum() -> Pipeline

Sum all elements in the buffer.

Domain: buffer → scalar, vector → scalar

relu

relu() -> Pipeline

Apply ReLU activation (max(0, x)): negative values become zero.

Domain: buffer → buffer

reshape

reshape(shape: Sequence[IntOrExpr]) -> Pipeline

Reshape array to new dimensions.

Domain: buffer → buffer

Parameters:

Name Type Description Default
shape Sequence[IntOrExpr]

New shape. The number of entries fixes the output rank; each entry may be a Polars expression.

required

resize

resize(
    *,
    height: IntOrExpr,
    width: IntOrExpr,
    filter: str | Expr = "lanczos3",
) -> Pipeline

Resize image to specified dimensions.

Domain: buffer → buffer

Parameters:

Name Type Description Default
height IntOrExpr

Target height.

required
width IntOrExpr

Target width.

required
filter str | Expr

Interpolation: "nearest", "bilinear", "lanczos3" (default).

'lanczos3'
Example

Pipeline().source("image_bytes").resize(height=224, width=224)

resize_max

resize_max(
    max_size: IntOrExpr, *, filter: str | Expr = "lanczos3"
) -> Pipeline

Resize image so the maximum dimension equals target, preserving aspect ratio (200x100 with max_size=50 gives 50x25).

Domain: buffer → buffer

Parameters:

Name Type Description Default
max_size IntOrExpr

Target for the maximum dimension (literal or expression).

required
filter str | Expr

Resize filter ("nearest", "bilinear", "lanczos3").

'lanczos3'
Example

pipe = Pipeline().source("image_bytes").resize_max(224)

resize_min

resize_min(
    min_size: IntOrExpr, *, filter: str | Expr = "lanczos3"
) -> Pipeline

Resize image so the minimum dimension equals target, preserving aspect ratio (200x100 with min_size=50 gives 100x50).

Domain: buffer → buffer

Parameters:

Name Type Description Default
min_size IntOrExpr

Target for the minimum dimension (literal or expression).

required
filter str | Expr

Resize filter ("nearest", "bilinear", "lanczos3").

'lanczos3'
Example

pipe = Pipeline().source("image_bytes").resize_min(224)

resize_to_height

resize_to_height(
    height: IntOrExpr, *, filter: str | Expr = "lanczos3"
) -> Pipeline

Resize image to target height, preserving aspect ratio (width is computed at runtime).

Domain: buffer → buffer

Parameters:

Name Type Description Default
height IntOrExpr

Target height (literal or expression).

required
filter str | Expr

Resize filter ("nearest", "bilinear", "lanczos3").

'lanczos3'
Example

pipe = Pipeline().source("image_bytes").resize_to_height(224)

resize_to_width

resize_to_width(
    width: IntOrExpr, *, filter: str | Expr = "lanczos3"
) -> Pipeline

Resize image to target width, preserving aspect ratio (height is computed at runtime).

Domain: buffer → buffer

Parameters:

Name Type Description Default
width IntOrExpr

Target width (literal or expression).

required
filter str | Expr

Resize filter ("nearest", "bilinear", "lanczos3").

'lanczos3'
Example

pipe = Pipeline().source("image_bytes").resize_to_width(224)

rotate

rotate(
    angle: FloatOrExpr,
    *,
    expand: bool = False,
    interpolation: str | Expr = "bilinear",
    border_value: FloatOrExpr = 0.0,
) -> Pipeline

Rotate image by specified angle.

For angles of 90, 180, or 270 degrees, this uses zero-copy view operations (interpolation and border_value do not apply: nothing is resampled and no out-of-bounds region is exposed). For arbitrary angles, the rotation is performed via an affine transformation using the specified interpolation and border value. For combined rotation + scale or explicit output sizing, use :meth:rotate_and_scale or :meth:warp_affine.

Domain: buffer → buffer

Parameters:

Name Type Description Default
angle FloatOrExpr

Rotation angle in degrees (positive = clockwise). Can be a literal float or Polars expression.

required
expand bool

If True, expand output dimensions to fit rotated image. If False (default), keep original dimensions (corners may be cropped).

False
interpolation str | Expr

Interpolation method for arbitrary angles -- "bilinear" (default) or "nearest". Not applicable to 90/180/270 degree rotations.

'bilinear'
border_value FloatOrExpr

Fill value for out-of-bounds pixels (default 0). Not applicable to 90/180/270 degree rotations.

0.0
Example
>>> pipe = Pipeline().source("image_bytes").rotate(90)
>>> pipe = Pipeline().source("image_bytes").rotate(45, expand=True)
>>> pipe = Pipeline().source("image_bytes").rotate(pl.col("angle"))
>>> pipe = Pipeline().source("image_bytes").rotate(30, interpolation="nearest")

rotate_and_scale

rotate_and_scale(
    *,
    angle: FloatOrExpr,
    center: Sequence[FloatOrExpr],
    output_size: Sequence[IntOrExpr],
    scale: FloatOrExpr = 1.0,
) -> Pipeline

Combined rotation and scaling around a center point.

Warps with OpenCV's getRotationMatrix2D(center, -angle, scale) matrix, built per row from the values the row resolves: the matrix is computed in one place (the engine), so a literal and the same value as an expression give identical output.

Domain: buffer → buffer

Parameters:

Name Type Description Default
angle FloatOrExpr

Rotation angle in degrees (positive = clockwise). Accepts a Polars expression for a per-row angle.

required
center Sequence[FloatOrExpr]

(cx, cy) center of rotation. Required: an image source's height/width are not known until execution, so there is no plan-time centre to default to. Each element accepts an expression.

required
output_size Sequence[IntOrExpr]

(height, width) of the output. Required, because the output shape is part of the plan-time schema. Each element accepts an expression.

required
scale FloatOrExpr

Scale factor (default 1.0). Accepts an expression.

1.0
Example
>>> pipe = Pipeline().source("image_bytes").rotate_and_scale(
...     angle=45.0, scale=1.2, center=(112, 112), output_size=(224, 224)
... )
>>> # Per-row angle from a column
>>> pipe = Pipeline().source("image_bytes").rotate_and_scale(
...     angle=pl.col("theta"), center=(112, 112), output_size=(224, 224)
... )

round

round() -> Pipeline

Round to nearest, ties to even (matches Polars/numpy).

Domain: buffer → buffer

sign

sign() -> Pipeline

Sign: -1/0/+1 (0 for ±0, NaN for NaN).

Domain: buffer → buffer

sqrt

sqrt() -> Pipeline

Square root (sqrt(x); NaN for negative input).

Domain: buffer → buffer

square

square() -> Pipeline

Square (x * x).

Domain: buffer → buffer

squeeze

squeeze(axis: int) -> Pipeline

Remove an axis of size 1: [H, W, 1] to [H, W] with axis=-1. The same elements in the same order, with no copy. An axis of another size is refused.

Domain: buffer → buffer

Parameters:

Name Type Description Default
axis int

The axis to remove, counted from the end when negative.

required
Example

pipe = Pipeline().source("array").squeeze(-1)

subtract_constant

subtract_constant(value: FloatOrExpr) -> Pipeline

Subtract a constant from every value (x - value).

Domain: buffer → buffer

Parameters:

Name Type Description Default
value FloatOrExpr

Constant subtrahend (literal or per-row expression).

required

threshold

threshold(value: FloatOrExpr) -> Pipeline

Apply binary threshold: a U8 mask, 255 where the element exceeds value and 0 elsewhere (for u8 input typically 0-255; for [0, 1] floats e.g. 0.5).

Domain: buffer → buffer

Parameters:

Name Type Description Default
value FloatOrExpr

Threshold value (int or float, or Polars expression).

required

transpose

transpose(axes: Sequence[int]) -> Pipeline

Transpose dimensions.

Domain: buffer → buffer

Parameters:

Name Type Description Default
axes Sequence[int]

New order of axes: a permutation of every input axis.

required

trunc

trunc() -> Pipeline

Round toward zero (drop the fractional part).

Domain: buffer → buffer

warp_affine

warp_affine(
    *,
    matrix: Sequence[FloatOrExpr],
    output_size: Sequence[IntOrExpr],
    interpolation: str | Expr = "bilinear",
    border_value: FloatOrExpr = 0.0,
) -> Pipeline

Apply a 2x3 affine transformation matrix.

The matrix [a, b, tx, c, d, ty] is a forward mapping from source to destination (same convention as OpenCV warpAffine):

x_dst = a * x_src + b * y_src + tx
y_dst = c * x_src + d * y_src + ty

The kernel inverts this matrix internally for interpolation.

Domain: buffer → buffer

Parameters:

Name Type Description Default
matrix Sequence[FloatOrExpr]

Six-element sequence representing the 2x3 affine matrix [a, b, tx, c, d, ty] (forward mapping). Each element may be a literal float or a Polars expression, so a batch can apply a different (e.g. random) affine per row in one call — the matrix is resolved per row at execution.

required
output_size Sequence[IntOrExpr]

(height, width) of the output image. Each element accepts a Polars expression for per-row dynamic values.

required
interpolation str | Expr

Interpolation method -- "bilinear" (default) or "nearest".

'bilinear'
border_value FloatOrExpr

Pixel value for out-of-bounds regions (default 0).

0.0
Example
>>> # Translate image by (50, 30)
>>> pipe = Pipeline().source("image_bytes").warp_affine(
...     matrix=[1.0, 0.0, 50.0, 0.0, 1.0, 30.0],
...     output_size=(224, 224),
... )
>>>
>>> # Per-sample random affine: each row uses its own matrix columns
>>> pipe = Pipeline().source("image_bytes").warp_affine(
...     matrix=[pl.col("a"), pl.col("b"), pl.col("tx"),
...             pl.col("c"), pl.col("d"), pl.col("ty")],
...     output_size=(224, 224),
... )

__init__

__init__() -> None

Initialize an empty pipeline.

on_error

on_error(policy: str) -> 'Pipeline'

Set the per-row error policy for the executed pipeline graph.

Controls what happens when producing a single row fails (source decode, operation execution, or output encoding):

  • "raise" (default): the first failing row fails the whole expression with its error.
  • "null": failing rows yield null for all of the graph's outputs; other rows are unaffected.
  • "null_with_message": as "null", plus the output becomes a struct with a reserved _error string field carrying the failure message for bad rows (null for good rows). Single-output pipelines become a two-field struct (_output + _error).

This is a graph-level setting: when pipelines are composed (merge_pipe, binary ops), all composed pipelines must agree on the policy.

Note: the per-source source(..., on_error="null") setting remains independent — it nulls only the outputs that depend on a failing source decode, while this policy covers any error producing the row.

Parameters:

Name Type Description Default
policy str

One of "raise", "null", "null_with_message".

required

Returns:

Type Description
'Pipeline'

New Pipeline with the error policy set.

Example

pipe = Pipeline().source("image_bytes").resize(height=224, width=224).on_error("null")

on_null_param

on_null_param(policy: str) -> 'Pipeline'

Set what a null in a per-row expression parameter means.

Parameters that take a Polars expression are read from ordinary columns, which may contain nulls:

  • "raise" (default): a null parameter fails the whole expression.
  • "null": rows whose parameter is null yield null, exactly as a null input image already does. Other rows are unaffected.

Under "null" only the outputs that actually depend on the affected operation go null — unlike on_error("null"), which nulls every output of a failing row. The two settings are independent: this one does not weaken error reporting for decode, encode or genuine operation failures.

To substitute a fallback value instead of nulling, fill the null in the expression itself — pl.col("scale").fill_null(1.0) — which is the idiomatic Polars way and needs nothing from this API.

This is a graph-level setting. Only a non-default policy is collected from the composed pipelines, so an explicit "raise" is indistinguishable from leaving it unset: composing a "null" pipeline with a "raise" one gives the whole graph "null", rather than being rejected as a conflict. (With only two values there is no combination that can conflict; on_error, which has two non-default values, does reject genuine disagreement.)

Parameters:

Name Type Description Default
policy str

One of "raise", "null".

required

Returns:

Type Description
'Pipeline'

New Pipeline with the null-parameter policy set.

Example

pipe = ( ... Pipeline() ... .source("image_bytes") ... .resize(height=pl.col("h"), width=pl.col("w")) ... .on_null_param("null") ... )

current_domain

current_domain() -> str

Get the current data domain of the pipeline.

Returns:

Type Description
str

Current domain: "buffer", "contour", "scalar", or "vector".

output_dtype

output_dtype() -> str

Get the expected output dtype of the pipeline.

This is the dtype of the buffer after all operations have been applied. Used for static type inference in list/array sinks. May be "auto" if the dtype has not yet been determined (e.g. an image source with no dtype-fixing operation applied).

Returns:

Type Description
str

Output dtype string: "u8", "f32", "f64", "auto", etc.

thumbnail

thumbnail(max_size: int) -> 'Pipeline'

Decode only a downscaled thumbnail of an image source.

Explicit, chainable form of source(..., decode_max_size=...): it asserts the pipeline needs at most max_size pixels on the decoded image's long side, so JPEG decoding uses IDCT scaling (1/8, 1/4 or 1/2) to skip work — a large CPU and memory win. The decoded long side never drops below min(max_size, original), so a downstream resize down to max_size never upscales. Non-JPEG formats (PNG, …) ignore the assertion and decode at full size.

This is the cheap front of a decode-aware curation pass: decode a thumbnail, compute a cheap signal (perceptual hash, mean, blur/quality score), filter on it, and only full-decode the survivors in a second pass. A scaled decode followed by a resize is not bit-identical to a full decode then the same resize (different resampling path) — hence the explicit opt-in.

Must be called after source(...) on an image_bytes/file_path source. It appends no op: it sets the source's decode_max_size.

Parameters:

Name Type Description Default
max_size int

Maximum pixels on the decoded long side. Positive int.

required

Returns:

Type Description
'Pipeline'

A new Pipeline whose source carries the decode-scale assertion.

Raises:

Type Description
ValueError

If there is no source, the source is not an image source, or max_size is not a positive int.

Example
>>> # Cheap perceptual-hash over thumbnails for dedup/curation
>>> pipe = (
...     Pipeline().source("image_bytes").thumbnail(64).perceptual_hash()
... )

assert_shape

assert_shape(
    *,
    dims: "Sequence[int | None] | None" = None,
    height: IntOrExpr | None = None,
    width: IntOrExpr | None = None,
    channels: IntOrExpr | None = None,
) -> "Pipeline"

Declare a shape the planner cannot work out for itself.

Use this when the source does not reveal its shape at plan time — a list column's rank and sizes are only known once the data arrives, so a fixed-shape .sink("array") has nothing to publish without a declaration. For a source the planner can read (image bytes, a file path, a fixed-size Array column), what it reveals is already planned and an assertion is at best redundant.

An assertion states a fact; it does not change one. A declaration that contradicts what the pipeline already knows — or that names a dimension the output rank does not have — is rejected here, at the line that wrote it. Execution checks it against every row where it was written, so a row whose data disagrees fails naming the assertion, and everything after it may rely on the declared shape.

Two spellings:

  • dims=[8, 8, 3] — positional and complete, of any rank. Entry i is the size of dimension i; None leaves one unknown. This also pins the output rank to len(dims), which is what lets a list source reach an array sink. An entry may be an expression: it is checked per row, and like any per-row size it publishes no shape.
  • height=/width=/channels= — the [H, W, C] spelling of dimensions 0, 1 and 2, for the common image case. The hints are positional, so these names only describe an [H, W, C] buffer: after a transpose([2, 0, 1]) dimension 0 is the channel axis, and calling it height would be a lie. A keyword naming a dimension the known rank does not have (channels= on a rank-2 buffer) is rejected; use dims= for anything that is not an image. Expressions are accepted and resolved per row, but a per-row size is not a plan-time fact, so it publishes no shape.

Parameters:

Name Type Description Default
dims 'Sequence[int | None] | None'

Full positional shape. Mutually exclusive with the height/width/channels keywords.

None
height IntOrExpr | None

Size of dimension 0 (literal or expression).

None
width IntOrExpr | None

Size of dimension 1 (literal or expression).

None
channels IntOrExpr | None

Size of dimension 2 (literal or expression).

None

Returns:

Type Description
'Pipeline'

A new pipeline carrying the declaration.

Raises:

Type Description
ValueError

If the declaration contradicts a known dimension or rank, names a dimension the rank does not have, or mixes dims= with the per-dimension keywords.

Example
# A list column's shape is not knowable at plan time; declare it.
pipe = Pipeline().source("list", dtype="f32").assert_shape(dims=[8, 8, 3])
df.with_columns(pl.col("arr").cv.pipe(pipe).sink("array"))

flip_h

flip_h() -> 'Pipeline'

Flip horizontally (along width axis).

Returns:

Type Description
'Pipeline'

Self for chaining.

flip_v

flip_v() -> 'Pipeline'

Flip vertically (along height axis).

Returns:

Type Description
'Pipeline'

Self for chaining.

scale

scale(
    factor: FloatOrExpr,
    out_dtype: str | None = None,
    preserve_dtype: bool = False,
) -> "Pipeline"

Multiply all values by a factor.

Parameters:

Name Type Description Default
factor FloatOrExpr

Scale factor.

required
out_dtype str | None

Dtype to leave the result in. Any :class:DType name; the multiply itself still happens in f32 (f64 for f64 input) and the result is cast, round-then-saturate for integer targets. Defaults to None — the op's own promote-to-float rule, which turns integers into f32 and preserves float input. Mutually exclusive with preserve_dtype.

None
preserve_dtype bool

If True, cast the result back to the input dtype, e.g. u8 in → u8 out instead of the promoted f32. The same mechanism as out_dtype, with the target read off the pipeline, so it requires that dtype to be known (not "auto").

False

Raises:

Type Description
ValueError

If domain is not buffer, or the output dtype cannot be honored (unknown dtype name, unknown input dtype under preserve_dtype, or both keywords at once).

clamp

clamp(
    min_val: FloatOrExpr,
    max_val: FloatOrExpr,
    out_dtype: str | None = None,
    preserve_dtype: bool = False,
) -> "Pipeline"

Clamp values to a range.

This operation accepts any numeric input dtype and automatically handles type promotion. Integers are promoted to float32; floats are preserved.

Parameters:

Name Type Description Default
min_val FloatOrExpr

Minimum value (literal or expression).

required
max_val FloatOrExpr

Maximum value (literal or expression).

required
out_dtype str | None

Dtype to leave the result in. Any :class:DType name; the clamp itself still happens in f32 (f64 for f64 input) and the result is cast, round-then-saturate for integer targets. Defaults to None — the op's own promote-to-float rule, which turns integers into f32 and preserves float input. Mutually exclusive with preserve_dtype.

None
preserve_dtype bool

If True, cast the result back to the input dtype, e.g. u8 in → u8 out instead of the promoted f32. The same mechanism as out_dtype, with the target read off the pipeline, so it requires that dtype to be known (not "auto").

False

Returns:

Type Description
'Pipeline'

Self for chaining.

Raises:

Type Description
ValueError

If domain is not buffer, or the output dtype cannot be honored (unknown dtype name, unknown input dtype under preserve_dtype, or both keywords at once).

adjust_brightness

adjust_brightness(
    *, factor: FloatOrExpr, preserve_dtype: bool = False
) -> "Pipeline"

Adjust image brightness by scaling pixel values.

Convenience method equivalent to .scale(factor).clamp(min_val=0, max_val=255).

Parameters:

Name Type Description Default
factor FloatOrExpr

Brightness factor. 1.0 = no change, >1 = brighter, <1 = darker.

required
preserve_dtype bool

If True, cast the result back to the dtype the pipeline had before this op (round-then-saturate for integer targets), e.g. u8 in → u8 out instead of the promoted f32. Requires the pipeline's dtype to be known (not "auto").

False

Returns:

Type Description
'Pipeline'

Self for chaining.

Example
>>> pipe = Pipeline().source("image_bytes").adjust_brightness(factor=1.2)

to_hsv

to_hsv() -> 'Pipeline'

Convert from RGB to HSV color space.

Returns:

Type Description
'Pipeline'

Self for chaining.

to_lab

to_lab() -> 'Pipeline'

Convert from RGB to CIE LAB color space.

Output dtype is promoted to f32 (L=[0,100], a/b~[-128,127]).

Returns:

Type Description
'Pipeline'

Self for chaining.

to_bgr

to_bgr() -> 'Pipeline'

Convert from RGB to BGR channel order.

Returns:

Type Description
'Pipeline'

Self for chaining.

to_ycbcr

to_ycbcr() -> 'Pipeline'

Convert from RGB to YCbCr color space.

Returns:

Type Description
'Pipeline'

Self for chaining.

sobel

sobel(*, axis: str = 'x') -> 'Pipeline'

Sobel gradient operator.

Convenience method that delegates to :meth:convolve2d with standard Sobel kernels.

Parameters:

Name Type Description Default
axis str

Gradient direction — "x" (horizontal) or "y" (vertical).

'x'

Returns:

Type Description
'Pipeline'

Self for chaining.

Example
>>> gx = Pipeline().source("image_bytes").grayscale().sobel(axis="x")

laplacian

laplacian() -> 'Pipeline'

Laplacian second-derivative operator.

Convenience method that delegates to :meth:convolve2d with a standard Laplacian kernel (the 3x3, 4-neighbour one).

Returns:

Type Description
'Pipeline'

Self for chaining.

Example
>>> lap = Pipeline().source("image_bytes").grayscale().laplacian()

sharpen

sharpen(*, strength: FloatOrExpr = 1.0) -> 'Pipeline'

Sharpen using an unsharp-mask-style kernel.

The kernel sum is 1 (brightness-preserving) with strength controlling how aggressively edges are enhanced. strength=0 produces the identity; higher values increase edge emphasis.

Parameters:

Name Type Description Default
strength FloatOrExpr

Sharpening strength (default 1.0). Accepts a Polars expression: the kernel coefficients are built from it element-wise, and convolve2d resolves each coefficient per row.

1.0

Returns:

Type Description
'Pipeline'

Self for chaining.

Example
>>> sharp = Pipeline().source("image_bytes").sharpen(strength=1.5)
>>> # Per-row strength from a column
>>> sharp = Pipeline().source("image_bytes").sharpen(
...     strength=pl.col("sharpness")
... )

morphology_open

morphology_open(
    *,
    ksize: IntOrExpr = OP_DEFAULTS["erode"]["ksize"],
    footprint: str | Expr = OP_DEFAULTS["erode"][
        "footprint"
    ],
) -> "Pipeline"

Morphological opening (erode then dilate).

Removes small bright spots while preserving larger structures. Equivalent to .erode(ksize=ksize, footprint=footprint) .dilate(ksize=ksize, footprint=footprint).

Parameters:

Name Type Description Default
ksize IntOrExpr

Size of the structuring element (the square's side, the disk's diameter). Must be odd and >= 1. Accepts a Polars expression for per-row dynamic values.

OP_DEFAULTS['erode']['ksize']
footprint str | Expr

The structuring element: "square" or "disk" (the pixels within Euclidean distance ksize // 2). Accepts a Polars expression for a per-row choice.

OP_DEFAULTS['erode']['footprint']

Returns:

Type Description
'Pipeline'

New Pipeline with opening applied.

Example
>>> cleaned = Pipeline().source("image_bytes").grayscale().threshold(128).morphology_open(ksize=3)

morphology_close

morphology_close(
    *,
    ksize: IntOrExpr = OP_DEFAULTS["dilate"]["ksize"],
    footprint: str | Expr = OP_DEFAULTS["dilate"][
        "footprint"
    ],
) -> "Pipeline"

Morphological closing (dilate then erode).

Fills small dark holes while preserving larger structures. Equivalent to .dilate(ksize=ksize, footprint=footprint) .erode(ksize=ksize, footprint=footprint).

Parameters:

Name Type Description Default
ksize IntOrExpr

Size of the structuring element (the square's side, the disk's diameter). Must be odd and >= 1. Accepts a Polars expression for per-row dynamic values.

OP_DEFAULTS['dilate']['ksize']
footprint str | Expr

The structuring element: "square" or "disk" (the pixels within Euclidean distance ksize // 2). Accepts a Polars expression for a per-row choice.

OP_DEFAULTS['dilate']['footprint']

Returns:

Type Description
'Pipeline'

New Pipeline with closing applied.

Example
>>> filled = Pipeline().source("image_bytes").grayscale().threshold(128).morphology_close(ksize=3)

resize_scale

resize_scale(
    *,
    scale: FloatOrExpr | None = None,
    scale_x: FloatOrExpr | None = None,
    scale_y: FloatOrExpr | None = None,
    filter: str | Expr = OP_DEFAULTS["resize_scale"][
        "filter"
    ],
) -> "Pipeline"

Resize image by scale factor.

Target dimensions are computed at runtime as: - new_width = input_width * scale_x - new_height = input_height * scale_y

Parameters:

Name Type Description Default
scale FloatOrExpr | None

Uniform scale factor (applies to both x and y).

None
scale_x FloatOrExpr | None

X (width) scale factor. If None, uses scale.

None
scale_y FloatOrExpr | None

Y (height) scale factor. If None, uses scale.

None
filter str | Expr

Resize filter ("nearest", "bilinear", "lanczos3").

OP_DEFAULTS['resize_scale']['filter']

Returns:

Type Description
'Pipeline'

Self for chaining.

Raises:

Type Description
ValueError

If neither scale nor scale_x/scale_y specified.

ValueError

If filter is invalid or current domain is not buffer.

Example
>>> # Uniform 50% downscale
>>> pipe = Pipeline().source("image_bytes").resize_scale(scale=0.5)
>>>
>>> # Non-uniform: half width, double height
>>> pipe = Pipeline().source("image_bytes").resize_scale(scale_x=0.5, scale_y=2.0)
>>>
>>> # Dynamic scale from column
>>> pipe = Pipeline().source("image_bytes").resize_scale(scale=pl.col("zoom"))

shear

shear(
    *,
    sx: FloatOrExpr = 0.0,
    sy: FloatOrExpr = 0.0,
    output_size: tuple[IntOrExpr, IntOrExpr],
) -> "Pipeline"

Apply a shear transformation.

Convenience wrapper that builds a shear matrix and delegates to :meth:warp_affine.

Parameters:

Name Type Description Default
sx FloatOrExpr

Horizontal shear factor (literal or per-row Polars expression).

0.0
sy FloatOrExpr

Vertical shear factor (literal or per-row Polars expression).

0.0
output_size tuple[IntOrExpr, IntOrExpr]

(height, width) of the output. Required, because the output shape is part of the plan-time schema and a shear does not imply one — an image source's height/width are not known until execution. Each element accepts a per-row Polars expression.

required

Returns:

Type Description
'Pipeline'

Self for chaining.

Example
>>> pipe = Pipeline().source("image_bytes").shear(sx=0.2, output_size=(100, 100))
>>>
>>> # Per-sample random shear from a column
>>> pipe = Pipeline().source("image_bytes").shear(
...     sx=pl.col("shear_x"), output_size=(100, 100)
... )

rasterize

rasterize(
    *,
    width: IntOrExpr | None = None,
    height: IntOrExpr | None = None,
    shape: "LazyPipelineExpr | None" = None,
    fill_value: IntOrExpr = OP_DEFAULTS["rasterize"][
        "fill_value"
    ],
    background: IntOrExpr = OP_DEFAULTS["rasterize"][
        "background"
    ],
    supersample: IntOrExpr = OP_DEFAULTS["rasterize"][
        "supersample"
    ],
) -> "Pipeline"

Rasterize contours to a mask: each region filled.

A pixel is filled when its centre — (x + 0.5, y + 0.5) — lies inside the contour, boundary included; holes are cut out by the same rule. This is the convention contains_point and the area measures follow, so for a shape whose vertices are integers on axis-aligned edges the mask holds exactly area() pixels.

The contour domain carries a set — extract_contours() generally yields more than one — and the mask is their union: each member's exterior minus its own holes. One member's hole never erases another's fill, and the result does not depend on the set's order. fill_value and background may be inverted; the same region is painted either way.

supersample=n anti-aliases: each pixel samples its n x n sub-pixel centres (x + (i + 0.5) / n, y + (j + 0.5) / n) and is painted background + (fill_value - background) * k / n**2 for the k inside, rounded half away from zero. n = 1 (the default) is the centre rule above.

An open contour (is_closed = False, a line) has no region to fill and is refused. stroke() turns lines (and regions' outlines) into the regions they cover, which this then fills: .stroke(width=2).rasterize(...).

Parameters:

Name Type Description Default
width IntOrExpr | None

Mask width.

None
height IntOrExpr | None

Mask height.

None
shape 'LazyPipelineExpr | None'

Match dimensions from another pipeline.

None
fill_value IntOrExpr

Inside value (default 255). Accepts a Polars expression for per-row dynamic values.

OP_DEFAULTS['rasterize']['fill_value']
background IntOrExpr

Outside value (default 0). Accepts a Polars expression for per-row dynamic values.

OP_DEFAULTS['rasterize']['background']
supersample IntOrExpr

Samples per pixel along each axis, 1 to 16 (default 1). Accepts a Polars expression for per-row dynamic values.

OP_DEFAULTS['rasterize']['supersample']

validate

validate() -> None

Validate that the pipeline is well-formed.

Raises:

Type Description
ValueError

If pipeline is invalid.

has_source

has_source() -> bool

Check if the pipeline has a source defined.

Returns:

Type Description
bool

True if the pipeline has a source defined.

to_graph

to_graph(column: Expr) -> 'PipelineGraph'

Convert this linear pipeline to a graph representation.

This is the unified execution path - all pipelines are converted to graphs before execution. A Pipeline becomes a single node in the graph.

For multi-output with intermediate checkpoints, use LazyPipelineExpr composition with .pipe() and .alias() instead.

Parameters:

Name Type Description Default
column Expr

The input column expression.

required

Returns:

Type Description
'PipelineGraph'

PipelineGraph representation of this pipeline.

Example
>>> from polars_cv import OptFlags
>>> pipe = Pipeline().source("image_bytes").resize(height=100, width=200)
>>> graph = pipe.to_graph(pl.col("image"))
>>> expr = graph.optimize(OptFlags.all()).to_expr()

to_expr() requires the optimization phase to have run — call graph.optimize(flags) first (or use the higher-level LazyPipelineExpr.sink, which does it for you).

__repr__

__repr__() -> str

Return string representation of pipeline: the chain as written.

explain

explain(
    *,
    optimized: bool = True,
    opt_flags: "OptFlags | bool | None" = None,
) -> str

Render the pipeline's op chain, logical or physical.

Mirrors Polars' .explain(optimized=...): the same pipeline can be inspected before and after the plan-time optimization phase, so a user sees the physical graph differ while the output stays identical.

Parameters:

Name Type Description Default
optimized bool

When False, render the op chain exactly as written (the logical plan). When True (default), render it after the optimization passes selected by opt_flags.

True
opt_flags 'OptFlags | bool | None'

Which passes to apply when optimized is True — same coercion as :meth:LazyPipelineExpr.sink (None reads the env default). Every node-scope logical pass runs, in :data:polars_cv._optimize.OPTIMIZATION_PASSES order — identity elimination, then spatial-window pushdown. Graph-scope CSE is inert for a single Pipeline (it only shares a prefix once sibling pipelines meet in a graph), and engine-tier passes are Rust lowering with no effect on the logical op chain; both are skipped.

None

Returns:

Type Description
str

A one-line Pipeline().… rendering of the chain.