Skip to content

Binary Operations

Combine two pipelines element-wise for operations like blending, differencing, and masking.

Overview

Binary operations take two LazyPipelineExpr operands and produce a new one:

result = expr_a.add(expr_b)
result = expr_a.blend(expr_b)
result = expr_a.apply_mask(expr_b)

Available Operations

Operation Description Integer behaviour Float behaviour
add Element-wise addition Saturating to the dtype's range IEEE
subtract Element-wise subtraction Saturating to the dtype's range IEEE
multiply Element-wise multiplication Saturating to the dtype's range IEEE
divide True division Promotes to a float: f32 for 8/16-bit integers, f64 for 32/64-bit ones (and with an f64 operand); x / 0 is inf, 0 / 0 is nan IEEE
blend Normalized product round(a*b / MAX), i.e. (a/MAX)*(b/MAX)*MAX a * b
maximum / minimum Element-wise extremes Exact IEEE comparison
bitwise_and / or / xor Bitwise Exact Integer-only

Both operands must have the same shape, and that is the output's shape. Unlike NumPy nothing broadcasts: [H, W, 1] against [H, W], or [H, W, 1] against [H, W, 3], is refused where it is written when the sizes are known, and fails only its own row when they are known per row. Change a rank explicitly with squeeze(axis) (removes an axis of size 1) or expand_dims(axis) (inserts one); both are zero-copy views and take a negative axis counted from the end. To spread a single-channel mask over an image's channels, use apply_mask, which does that by contract.

gray = pl.col("image").cv.pipe(Pipeline().source("image_bytes").grayscale())
# gray is [H, W, 1]; channel_select(0, keepdims=False) is [H, W].
# Say which rank you mean:
doubled = gray.add(gray.channel_select(0, keepdims=False).expand_dims(-1))

Operands of different dtypes meet in NumPy's promoted dtype (np.result_type): the smallest dtype that holds every value of both, so u8 with i8 is i16 and u32 with f32 is f64; u64 with a signed integer has none and is f64 (a bitwise op on that pair is refused). Every dtype is computed natively, so no value is rounded through f32.

Basic Usage

import polars as pl
from polars_cv import Pipeline

# Two pipelines processing the same image differently
img1 = pl.col("image").cv.pipe(
    Pipeline().source("image_bytes").resize(height=128, width=128)
)
img2 = pl.col("image").cv.pipe(
    Pipeline().source("image_bytes").resize(height=128, width=128).blur(5.0)
)

# Binary operations
added = img1.add(img2)
subtracted = img1.subtract(img2)  # Edge detection effect
blended = img1.blend(img2)

result = df.with_columns(
    diff=subtracted.sink("png")
)

Mask Application

Apply a binary mask to an image:

# Image pipeline
img = pl.col("image").cv.pipe(
    Pipeline().source("image_bytes").resize(height=128, width=128)
)

# Mask pipeline (from contour)
mask = pl.col("contour").cv.pipe(
    Pipeline().source("contour").rasterize(width=128, height=128)
)

# Apply mask: pixels where mask=0 become 0
masked = img.apply_mask(mask)
result = df.with_columns(masked=masked.sink("png"))

Shape Inference

Automatically match mask dimensions to image:

# Define image pipeline
img = pl.col("image").cv.pipe(
    Pipeline().source("image_bytes").resize(height=150, width=100)  # Non-square
)

# Contour source with shape inference
mask = pl.col("contour").cv.pipe(
    Pipeline().source("contour").rasterize(shape=img)  # Auto: 150x100
)

# Apply
masked = img.apply_mask(mask)

Convenience Method

Use apply_contour_mask() for the common case:

img = pl.col("image").cv.pipe(
    Pipeline().source("image_bytes").resize(height=128, width=128)
)
contour = pl.col("contour").cv.pipe(Pipeline().source("contour"))

# Rasterizes the contours onto the image's canvas, then applies the mask
masked = img.apply_contour_mask(contour)

Multi-Source Operations

Combine data from different DataFrame columns:

# Base image
base = pl.col("base_image").cv.pipe(
    Pipeline().source("image_bytes").resize(height=128, width=128)
)

# Overlay image (different column)
overlay = pl.col("overlay_image").cv.pipe(
    Pipeline().source("image_bytes").resize(height=128, width=128).flip_h()
)

# Blend two different images
blended = base.blend(overlay)
result = df.with_columns(blended=blended.sink("png"))

Chaining Binary Operations

Binary operations return LazyPipelineExpr, so you can chain:

# (A + B) * mask
result = img_a.add(img_b).apply_mask(mask)

# Blend A with blurred version, then apply mask
blurred = img_a.pipe(Pipeline().blur(5.0))
result = img_a.blend(blurred).apply_mask(mask)

Use Cases

Edge Detection

Subtract blurred from original to find edges:

original = pl.col("image").cv.pipe(
    Pipeline().source("image_bytes").grayscale()
)
blurred = original.pipe(Pipeline().blur(3.0))

edges = original.subtract(blurred)  # Saturating subtraction

Overlay Composition

Blend two images with alpha:

# For alpha blending, normalize inputs first
img1_norm = pl.col("image1").cv.pipe(
    Pipeline().source("image_bytes").normalize()
)
img2_norm = pl.col("image2").cv.pipe(
    Pipeline().source("image_bytes").normalize()
)

# Blend produces (a * b) effect on normalized inputs
blended = img1_norm.blend(img2_norm)

Attention/Mask Weighting

Apply learned attention weights:

feature_map = pl.col("features").cv.pipe(...)
attention = pl.col("attention").cv.pipe(...)

# Multiply features by attention weights
weighted = feature_map.multiply(attention)

Requirements

Both operands must:

  1. Be LazyPipelineExpr objects (use .cv.pipe())
  2. Produce the same output shape
  3. Have compatible dtypes

Next Steps