feat: initialize standalone image-filter

Rename the distribution to image-filter and imports to image_filter.
Keep medical fixtures local and document their setup for fresh clones.
Include composite pipelines, consistent examples, and class diagrams.
This commit is contained in:
mohamad
2026-09-10 01:58:07 +03:30
commit bcaae5ec22
31 changed files with 2831 additions and 0 deletions
+174
View File
@@ -0,0 +1,174 @@
# Numerical semantics and scope
This package is a greenfield implementation. Its numerical contracts are the
documented behavior of SciPy, scikit-image, PyWavelets, and NumPy—not any
application-specific filter implementation.
## Shared contract
- Inputs are finite, real numeric NumPy arrays with two or three inferred
non-singleton spatial axes.
- Filters preserve the complete input shape, including singleton and channel
axes.
- Numeric output is explicitly `float32` or `float64` (default `float64`).
Canny always returns a boolean mask.
- SciPy-backed filters accept `reflect`, `constant`, `nearest`, `mirror`, and
`wrap`. `cval` is used only by `constant` mode.
- Each filter exposes a `Capabilities` object. Unsupported dimensions and
recognized-but-unimplemented options use domain-specific exceptions.
## MeanFilter
`MeanFilter` calls `scipy.ndimage.uniform_filter` exactly, with a scalar kernel
size applied only to spatial axes. Channel and retained singleton axes use size
one.
SciPy's `origin=0` alignment is the default. Odd kernels are centered on a
sample. Even kernels necessarily have one extra sample on one side of the
origin; the exact placement is SciPy's documented convention. Set `origin`
explicitly to shift the footprint. Valid origins are checked at construction.
This policy makes even-kernel behavior deliberate and testable.
## LoGFilter
`LoGFilter` sums second-order `scipy.ndimage.gaussian_filter` derivatives over
the inferred spatial axes. This is numerically equivalent to
`scipy.ndimage.gaussian_laplace` when every array axis is spatial, while also
preventing smoothing or differentiation across channels. Sigma is expressed in
pixels and may be scalar or one value per effective spatial axis.
## LawsFilter
Laws responses use normalized standard L3/E3/S3 and L5/E5/S5/W5/R5 vectors
with `scipy.ndimage.convolve1d`. A configuration contains one two-character
kernel name per spatial axis, such as `L5E5` or `L5E5S5`. Optional texture
energy is the local uniform mean of the absolute response with window width
`2 * energy_radius + 1`.
Rotation-invariant pooling is intentionally unsupported. Construct explicit
orientation filters and combine their outputs according to the domain's chosen
statistic instead of relying on an undocumented convention.
## GaborFilter
Gabor responses call `skimage.filters.gabor` for each orientation. Frequency is
in cycles per pixel, angles are radians, and `sigma_y = sigma / gamma`.
Available complex response projections are magnitude, real, and imaginary;
multiple orientations use max, min, mean, or sum pooling.
The filter supports 2D data, including 2D multichannel arrays with an explicit
channel axis. A 3D volume requires `SliceWise`, whose axis is part of the public
configuration and provenance.
## WaveletFilter
Wavelet coefficients use PyWavelets' shift-invariant `pywt.swtn`. Configuration
letters map directly to PyWavelets keys: `L` to approximation (`a`) and `H` to
detail (`d`) in the order of inferred spatial axes. The selected array is the
requested coefficient at the final decomposition level.
SWT requires transformed lengths divisible by `2**level`. Each spatial axis is
padded only at its high edge to the next valid length using `numpy.pad`, then
the coefficient is cropped to the original shape. `pad_mode` is therefore a
NumPy padding mode rather than a PyWavelets signal-extension mode. The optional
`normalize=True` is passed directly to `pywt.swtn`.
Rotation-invariant coefficient pooling and Riesz steering are not implemented;
requesting rotation invariance raises `UnsupportedOptionError`. Decimated DWT
reconstruction is a distinct operation and is outside this first API.
## GaussianFilter
2D/3D Gaussian smoothing calls [scipy.ndimage.gaussian_filter](https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.gaussian_filter.html).
`sigma` is a finite positive scalar or immutable nonempty tuple, one value per
inferred spatial axis in array order. Tuple length is validated on application.
`truncate` is finite and positive (default 4 standard deviations). Input and
intermediate values use the selected floating output dtype; no intensity
normalization occurs. Channels and singleton axes are indexed out before the
backend call, preserving SciPy 1.10 compatibility.
## MedianFilter
2D/3D rank filtering calls [scipy.ndimage.median_filter](https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.median_filter.html).
`size` is a positive integer applied along each spatial axis. `origin` must be
an integer in `[-size//2, (size-1)//2]`, interpreting the lower bound as
`-(size//2)`. Even footprints select sorted element `n//2`, rather than averaging
the middle pair. Input is converted to the selected floating dtype before
filtering, including fractional `cval` padding for integer images and support
for float16 input. Large 3D windows can require substantial backend memory.
## SobelFilter
2D/3D gradients call [scipy.ndimage.sobel](https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.sobel.html)
on each spatial block. The default `axis=None` returns Euclidean magnitude
`sqrt(sum(sobel(image, axis=i)**2))` over spatial axes, accumulated with `hypot`
to avoid intermediate squared overflow. There is no division by dimensionality
or kernel gain. Optional integer `axis` selects a signed component, indexing the
inferred spatial axes (negative indices supported). Channels are neither
smoothed nor differentiated. Computation uses the selected floating dtype.
## LaplacianFilter
2D/3D [scipy.ndimage.laplace](https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.laplace.html)
sums discrete second differences over spatial axes. Input is cast to the
selected floating dtype, retaining signed responses. This operation adds no
Gaussian smoothing; use LoGFilter when that is desired.
## CannyFilter
[skimage.feature.canny](https://scikit-image.org/docs/stable/api/skimage.feature.html#skimage.feature.canny)
produces a boolean edge mask from each 2D plane/channel. Singleton axes are
restored afterward. A 3D image is rejected; use `SliceWise(CannyFilter(), axis=0)`
with an explicitly chosen array axis. There is no implicit projection or slicing.
Canny rejects int64/uint64 with `InvalidImageError`, matching the backend
restriction declared in its capabilities. Callers may explicitly convert to float
and choose thresholds in the resulting intensity units.
The original supported input dtype reaches Canny. Its image conversion scales unsigned
integers by their dtype maximum and signed integers to the signed unit range;
floating input keeps its values, with no min/max normalization or clipping.
Absolute thresholds are expressed in original dtype intensity units; the
backend divides integer thresholds by the dtype maximum. For floating input,
thresholds are already in the unchanged intensity units. Missing low/high
thresholds become 0.1/0.2 in the backend's normalized scale. `use_quantiles=True`
instead interprets thresholds as gradient-magnitude quantiles in [0, 1], with
defaults 0.1/0.2. Thresholds must be finite, nonnegative, and ordered; ordering
against missing defaults for absolute thresholds is checked by the backend at
application time and converted to `InvalidConfigurationError`.
`sigma` must be finite and positive. `cval` is in the converted image's units.
`mode` defaults to `constant`; it governs Gaussian padding, and Canny excludes
the outermost edge pixels during mask erosion. There is no public mask option.
## Boundary and precision details for the new filters
All five accept `reflect` (half-sample reflection), `mirror` (whole-sample
reflection), `nearest` (edge replication), `wrap` (periodic), and `constant`
(finite `cval`, default 0). SciPy aliases are intentionally excluded. Numeric
filters default to `reflect`. Boolean configuration values are not numbers.
Like their backends, floating filters can lose precision or overflow for extreme
finite inputs; finite outputs are verified on normal image ranges, not promised
for arbitrary floating-point magnitudes. Provenance records parameters, effective
spatial axes, shapes and dtypes; SliceWise records the nested filter and axis.
### Composite contract
`ImageFilter` defines `apply` and shared `apply_batch` behavior. Numerical
leaves reuse the internal `NumericFilter` implementation for validation and
history. `Pipeline` is an `ImageFilter` containing a nonempty immutable tuple
of components. Nested pipelines are supported at arbitrary depth and execute
depth-first in child order. History contains each executed leaf once; composite
boundaries do not add history entries. Errors propagate from the failing child.
Pipeline dimensions are the intersection of child dimensions. This declaration
does not guarantee that intermediate ranges or dtypes satisfy later children;
each child validates its actual input. The last child determines output dtype.
Batch processing uses the same component API for both leaves and pipelines.
`SliceWise` accepts any component with 2D capability, including a pipeline,
and calls its public API on each plane with remapped channel metadata. The
actual first plane result determines output dtype (including boolean Canny
results). Its outer history records one adapter entry containing the nested
configuration, rather than repeating history for every slice.