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
+123
View File
@@ -0,0 +1,123 @@
# Image Filter class diagrams
These UML-style Mermaid diagrams describe the implemented classes in
`src/image_filter`. Open this Markdown in a Mermaid-capable viewer, or use
the adjacent `.mmd` source files with Mermaid CLI.
## Composite and numerical filters
```mermaid
classDiagram
direction TB
class ImageFilter {
<<abstract>>
+Capabilities capabilities
+apply(image, metadata) FilterResult
+apply_batch(items) list~FilterResult~
}
class NumericFilter {
<<abstract>>
+apply(image, metadata) FilterResult
#_apply_with_layout(image, metadata, layout) FilterResult
#_compute(image, layout) ndarray
#_history_entry(image, output, layout) dict
}
class Pipeline {
+tuple~ImageFilter~ steps
+Capabilities capabilities
+apply(image, metadata) FilterResult
}
class SliceWise {
+ImageFilter filter
+int axis
+apply(image, metadata) FilterResult
#_compute(image, layout) ndarray
}
ImageFilter <|-- NumericFilter
ImageFilter <|-- Pipeline
NumericFilter <|-- SliceWise
Pipeline "0..*" o-- "1..*" ImageFilter : ordered children
SliceWise "0..*" o-- "1" ImageFilter : adapts
NumericFilter <|-- MeanFilter
NumericFilter <|-- GaussianFilter
NumericFilter <|-- MedianFilter
NumericFilter <|-- SobelFilter
NumericFilter <|-- LaplacianFilter
NumericFilter <|-- LoGFilter
NumericFilter <|-- LawsFilter
NumericFilter <|-- GaborFilter
NumericFilter <|-- CannyFilter
NumericFilter <|-- WaveletFilter
```
ImageFilter is the public Component, Pipeline the Composite, and the ten
concrete numerical filters the Leaves. NumericFilter shares validation and
history behavior; it is an internal abstract implementation, not a separate
public filtering operation. SliceWise is an adapter that can wrap a Leaf or
a Pipeline.
The hollow diamond denotes aggregation: immutable children can be shared
between pipelines and remain valid independently of any parent. A Pipeline
contains at least one child and executes children in order. Inheritance arrows
point toward the base class. Public methods use +; internal methods use #.
## Input, output, and capability model
```mermaid
classDiagram
direction LR
class ImageItem {
+ndarray image
+Mapping metadata
}
class FilterResult {
+ndarray image
+Mapping metadata
}
class SpatialLayout {
+int dimension
+tuple spatial_axes
+tuple singleton_axes
+int channel_axis
}
class Capabilities {
+frozenset spatial_dimensions
+str accepted_dtypes
+str input_range
+str output_dtype
+bool preserves_shape
+bool supports_channels
}
class ImageFilter {
<<abstract>>
+apply(image, metadata) FilterResult
+apply_batch(items) list~FilterResult~
}
class NumericFilter {
<<abstract>>
}
ImageFilter <|-- NumericFilter
ImageFilter ..> ImageItem : consumes in batches
ImageFilter ..> FilterResult : returns
ImageFilter ..> Capabilities : declares
NumericFilter ..> SpatialLayout : validates and computes
```
ImageItem groups an array with metadata for batch execution. FilterResult
contains the resulting array and copied metadata. SpatialLayout describes the
effective spatial axes, ignoring singleton axes and excluding an explicitly
declared channel axis. Capabilities describes supported input dimensions and
other contracts. Pipeline derives its capabilities from its children.
## Source and presentation order
1. `src/image_filter/base.py`: Component and common numerical behavior.
2. `src/image_filter/filters.py`: the ten Leaves.
3. `src/image_filter/pipeline.py`: Composite and slice adapter.
4. `src/image_filter/model.py`: domain objects and dimension inference.
5. `examples/01_single_filter.py` through `05_slicewise_pipeline.py`:
a shared runner with different component configurations.
Editable sources: [components.mmd](components.mmd) and
[domain-model.mmd](domain-model.mmd). Keep their diagrams identical to the
embedded blocks here when changing class relationships.
+41
View File
@@ -0,0 +1,41 @@
classDiagram
direction TB
class ImageFilter {
<<abstract>>
+Capabilities capabilities
+apply(image, metadata) FilterResult
+apply_batch(items) list~FilterResult~
}
class NumericFilter {
<<abstract>>
+apply(image, metadata) FilterResult
#_apply_with_layout(image, metadata, layout) FilterResult
#_compute(image, layout) ndarray
#_history_entry(image, output, layout) dict
}
class Pipeline {
+tuple~ImageFilter~ steps
+Capabilities capabilities
+apply(image, metadata) FilterResult
}
class SliceWise {
+ImageFilter filter
+int axis
+apply(image, metadata) FilterResult
#_compute(image, layout) ndarray
}
ImageFilter <|-- NumericFilter
ImageFilter <|-- Pipeline
NumericFilter <|-- SliceWise
Pipeline "0..*" o-- "1..*" ImageFilter : ordered children
SliceWise "0..*" o-- "1" ImageFilter : adapts
NumericFilter <|-- MeanFilter
NumericFilter <|-- GaussianFilter
NumericFilter <|-- MedianFilter
NumericFilter <|-- SobelFilter
NumericFilter <|-- LaplacianFilter
NumericFilter <|-- LoGFilter
NumericFilter <|-- LawsFilter
NumericFilter <|-- GaborFilter
NumericFilter <|-- CannyFilter
NumericFilter <|-- WaveletFilter
+37
View File
@@ -0,0 +1,37 @@
classDiagram
direction LR
class ImageItem {
+ndarray image
+Mapping metadata
}
class FilterResult {
+ndarray image
+Mapping metadata
}
class SpatialLayout {
+int dimension
+tuple spatial_axes
+tuple singleton_axes
+int channel_axis
}
class Capabilities {
+frozenset spatial_dimensions
+str accepted_dtypes
+str input_range
+str output_dtype
+bool preserves_shape
+bool supports_channels
}
class ImageFilter {
<<abstract>>
+apply(image, metadata) FilterResult
+apply_batch(items) list~FilterResult~
}
class NumericFilter {
<<abstract>>
}
ImageFilter <|-- NumericFilter
ImageFilter ..> ImageItem : consumes in batches
ImageFilter ..> FilterResult : returns
ImageFilter ..> Capabilities : declares
NumericFilter ..> SpatialLayout : validates and computes
+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.