Mathematical Morphology module for PyTorch (CUDA), providing differentiable operators and learnable network layers.
This is an uv workspace:
packages/serron- the published kernel package (README).
Prebuilt wheels are published for CPython 3.12–3.14 on Linux (x86_64, aarch64) and Windows (x86_64).
pip install serronSerron works on standard PyTorch tensors laid out as (N, C, H, W) and living on a CUDA
or CPU device.
Stateless operators live in serron.functional and are re-exported at the top level.
Each takes an input tensor, a structuring element, and an optional border mode:
import torch
import serron
from serron import BorderMode
from serron import structuring_element as se
x = torch.rand(1, 1, 256, 256, device="cuda")
kernel = se.disk(3, device="cuda")
eroded = serron.erosion(x, kernel)
dilated = serron.dilation(x, kernel)
opened = serron.opening(x, kernel)
closed = serron.closing(x, kernel)
# Derived operators
grad = serron.gradient(x, kernel) # dilate(x) - erode(x)
white = serron.top_hat(x, kernel) # x - open(x)
black = serron.black_hat(x, kernel) # close(x) - x
# Control border handling
eroded_reflect = serron.erosion(x, kernel, border=BorderMode.REFLECT)Available operators: erosion, dilation, opening, closing, gradient,
top_hat, black_hat.
serron.structuring_element builds common SE shapes on the requested device:
from serron import structuring_element as se
se.square(5, device="cuda") # (5, 5) full square
se.cross(5, device="cuda") # (5, 5) plus shape
se.disk(3, device="cuda") # (7, 7) disk, radius 3
se.diamond(3, device="cuda") # (7, 7) diamond, radius 3
se.from_tensor(my_weights) # wrap an arbitrary 2-D tensor as a grayscale SEBorderMode controls how out-of-bounds neighbors are handled:
| Mode | Behavior |
|---|---|
BorderMode.REPLICATE |
Repeat the edge value (default) |
BorderMode.REFLECT |
Mirror across the edge |
BorderMode.CONSTANT |
Pad with a constant |
serron also exposes torch.nn.Module layers with a learnable structuring element,
so morphology can be trained end-to-end inside a network. Each layer takes the number
of channels and a kernel_size:
import torch
from serron import Erosion2d, Dilation2d, Opening2d, Closing2d
from serron import BorderMode
layer = Erosion2d(channels=3, kernel_size=5, border=BorderMode.REPLICATE).cuda()
x = torch.rand(8, 3, 64, 64, device="cuda")
y = layer(x) # forward pass; layer.weight is a trainable (C, k, k) SE
y.sum().backward() # gradients flow into layer.weightAvailable layers: Erosion2d, Dilation2d, Opening2d, Closing2d.
git clone git@github.com:vhrabar/serron.git
cd serrontorch is pulled from a specific wheel index via mutually-exclusive extras. Pick the
one matching your machine, plain uv sync (no extra) falls back to the default
CUDA-enabled wheel from PyPI:
uv sync --extra cu132 # CUDA 13.2 build
uv sync --extra cpu # CPU-only buildBuilding the CUDA extension from source needs the CUDA 13.X toolkit (nvcc):
uv sync --package serron --no-dev --group build --extra cu132
uv build --package serron --wheel --no-build-isolationOn a GPU-less machine, sync the CPU torch build instead; the extension then builds
C++ only (no nvcc required):
uv sync --package serron --no-dev --group build --extra cpu
uv build --package serron --wheel --no-build-isolationReleased under the MIT License. See LICENSE.
Copyright © 2026 Vedran Hrabar