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:
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:
- Be
LazyPipelineExprobjects (use.cv.pipe()) - Produce the same output shape
- Have compatible dtypes
Next Steps¶
- Multi-Output - Extract intermediate results
- Domains - Mask from contours