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.
Image Filter
image-filter is a standalone, typed Python library for reproducible
filtering of in-memory NumPy images. It has no dependency on the Radiuma or
ViSERA application and performs no file or folder I/O.
The repository and distribution are named image-filter. Python imports use
image_filter because hyphens are not valid Python identifiers. Source lives
in src/image_filter/. Commands below run from this repository's root.
See the class diagrams for the Composite architecture and domain model, with editable Mermaid source files.
Local medical data
Real scans are kept locally in ignored data/samples/ and data/tests/;
they are not included in Git or the wheel. This Mac has the existing fixtures
copied into those directories. A fresh clone needs MRI.nii.gz, PET.nii.gz,
CT.nii.gz, SPECT.nii.gz and Ultrasound.nii.gz in data/samples/, plus
test4.nii.gz and 0001_MRI_Image_a.nii.gz in data/tests/ to reproduce the full
real-data suite. Alternatively, examples accept --input for your own files.
DICOM demo fixtures come from the installed pydicom package.
Install
python -m pip install .
For development:
python -m pip install -e '.[test,examples]'
python -m pytest tests
Quick start
import numpy as np
from image_filter import MeanFilter, Pipeline, WaveletFilter
image = np.asarray(...) # a 2D or 3D numeric image
pipeline = Pipeline([
MeanFilter(size=3),
WaveletFilter(configuration="LH", wavelet="db2"),
MeanFilter(size=4, origin=-1, mode="nearest"),
])
result = pipeline.apply(image, {"case_id": "anonymous-17"})
filtered = result.image
history = result.metadata["processing_history"]
Configuration objects and pipelines are immutable. Repeating a filter class is supported because a pipeline stores ordered filter instances, not a registry keyed by type.
Composite API and runnable code examples
ImageFilter is the abstract Component with apply(image, metadata) and
apply_batch(items). The ten filters are Leaves. Pipeline(ImageFilter) is
the Composite: its children can be filters, adapters, or nested pipelines.
Each child receives the preceding child's image and metadata. A nested
pipeline records its leaves in execution order, without duplicate pipeline
history entries. Empty pipelines are rejected. Shared instances are immutable.
from image_filter import GaussianFilter, ImageFilter, Pipeline, SobelFilter
def process(image, operation: ImageFilter):
return operation.apply(image)
preprocessing = Pipeline([GaussianFilter(sigma=1.0)])
edges = Pipeline([preprocessing, SobelFilter()])
# process(image, GaussianFilter()) and process(image, edges) use the same API.
All five files have the same structure: build_operation() -> ImageFilter,
then run_example(build_operation(), name=...). Only the operation changes.
The shared runner loads the image, applies the component, and saves the result.
It defaults to local MRI data; every example accepts the same input options.
| File | Demonstrates |
|---|---|
examples/01_single_filter.py |
One-filter pipeline |
examples/02_sequential_filters.py |
Mean → wavelet → mean with different settings |
examples/03_nested_pipeline.py |
Reusable preprocessing composite followed by Sobel |
examples/04_component_api.py |
A direct leaf passed to the same runner |
examples/05_slicewise_pipeline.py |
Gaussian → Canny; use --mode slice-wise for a volume |
From the library root, with example dependencies installed:
cd /Users/mohamad/vollabor/image-filter
for example in examples/0[1-5]_*.py; do
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src python3 "$example" || break
done
Outputs are NumPy arrays and JSON processing metadata in
example-output/pipelines/. Matching filenames are overwritten;
use different --output-dir values to retain multiple runs.
All five scripts accept these options:
| Option | Behavior |
|---|---|
--case mri |
Local case: mri, pet, ct, spect, ultrasound, mr-dicom, ct-series |
--input PATH |
NIfTI, NRRD, single DICOM, or DICOM series directory |
--series-uid UID |
Select a series when the directory contains several |
--mode plane |
Default: representative 2D plane, 1–99% window normalized to [0, 1] |
--mode volume |
Original intensities and dimensions; every step must support the input |
--mode slice-wise |
Original intensities; automatically wrap a 3D input in SliceWise |
--axis 2 |
Plane selection or slice-wise axis (0, 1, or 2) |
--modality CT |
Label for a user-supplied image |
--output-dir PATH |
Destination for .npy and .json files |
PYTHONPATH=src python3 examples/03_nested_pipeline.py --case ct
PYTHONPATH=src python3 examples/01_single_filter.py --case mri --mode volume
PYTHONPATH=src python3 examples/05_slicewise_pipeline.py --case mri --mode slice-wise
PYTHONPATH=src python3 examples/03_nested_pipeline.py --input /path/to/scan.nii.gz
slice-wise is an explicit user choice; within that mode a 2D input runs
directly and a 3D input gets the adapter automatically. The runner does not
change wavelet configuration or thresholds to accommodate incompatible data.
For example, LH in example 02 requires 2D execution: use plane or
slice-wise, or explicitly configure a 3D wavelet for volume mode.
The existing visual_compare.py still produces all ten independent medical
comparisons; it now executes each operation through a one-step composite.
These comparison panels are independent, whereas examples 02, 03 and 05 are
sequential pipelines.
Dimensionality and channels
Dimension is inferred for every image independently. A singleton spatial axis
does not change a 2D image into a volume and its original shape is retained:
(1, H, W), (H, 1, W), and (H, W, 1) are all 2D.
Always declare a channel axis:
result = MeanFilter(3).apply(rgb, {"channel_axis": -1})
A three-axis (H, W, 3) or (H, W, 4) array without metadata is deliberately
rejected because it may be either color data or a short volume. Set
channel_axis=2 for color, or spatial_dims=3 for a volume. Four-axis data
must declare channel_axis; time-series inference is intentionally outside the
core contract.
Explicit slice-wise filtering
Gabor and Canny are intrinsically 2D. It raises UnsupportedDimensionError for a volume.
Slice processing is available only through an explicit adapter with a chosen
axis:
from image_filter import GaborFilter, SliceWise
filter_3d = SliceWise(GaborFilter(frequency=0.2), axis=0)
result = filter_3d.apply(volume)
See docs/semantics.md for numerical contracts, boundary behavior, capability declarations, and intentionally unsupported options.
Collection execution
from image_filter import ImageItem
results = Pipeline([MeanFilter(3)]).apply_batch([
ImageItem(two_dimensional_image, {"id": "2d"}),
ImageItem(volume, {"id": "3d", "spatial_dims": 3}),
])
No caller metadata dictionary is mutated. Each output receives a deep-copied mapping with deterministic, ordered processing provenance.
Ten public filters
| Filter | Spatial dimensions | Output |
|---|---|---|
| MeanFilter | 2D, 3D | float32/float64 |
| GaussianFilter | 2D, 3D | float32/float64 |
| MedianFilter | 2D, 3D | float32/float64 |
| SobelFilter | 2D, 3D | float32/float64 magnitude by default |
| LaplacianFilter | 2D, 3D | float32/float64 signed response |
| LoGFilter | 2D, 3D | float32/float64 |
| LawsFilter | 2D, 3D | float32/float64 |
| GaborFilter | 2D | float32/float64 |
| CannyFilter | 2D | bool |
| WaveletFilter | 2D, 3D | float32/float64 |
from skimage.data import camera
from image_filter import GaussianFilter, MedianFilter, CannyFilter, Pipeline
edges = Pipeline([
GaussianFilter(sigma=0.8),
MedianFilter(size=3),
CannyFilter(low_threshold=0.1, high_threshold=0.3, use_quantiles=True),
]).apply(camera(), {"source": "public-domain camera"})
assert edges.image.dtype == bool
Canny follows scikit-image dtype scaling; float images are not automatically normalized. See the threshold and boundary contracts.
Reproduce validation on macOS
From the library root, with Python 3.10+ and the dependencies installed:
cd /Users/mohamad/vollabor/image-filter
python3 -m pip install -e '.[test,examples]' build
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src python3 -m pytest tests -p no:cacheprovider
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src MPLBACKEND=Agg python3 examples/visual_compare.py --all --output-dir example-output --contact-sheet
python3 -m build --wheel --no-isolation --outdir dist .
open example-output/source-overview.png
Real-data tests read local NIfTI assets and medical fixtures. They do not import application runtime code.
Generate the presentation examples
Generate seven consistently styled medical comparison panels and a source overview with one command, from the library root (development dependencies installed):
cd /Users/mohamad/vollabor/image-filter
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src python3 examples/visual_compare.py --all --output-dir example-output --contact-sheet
open example-output/source-overview.png
The bundled command uses local local NIfTI volumes and installed pydicom fixtures:
| Source | Input | Panel filename |
|---|---|---|
| mri | local MRI NIfTI volume | mri-filters.png |
| pet | local PET NIfTI volume | pet-filters.png |
| ct | local CT NIfTI volume | ct-filters.png |
| spect | local SPECT NIfTI volume | spect-filters.png |
| ultrasound | local ultrasound NIfTI volume | ultrasound-filters.png |
| mr-dicom | installed pydicom single MR DICOM | mr-dicom-filters.png |
| ct-series | installed pydicom five-slice CT series | ct-series-filters.png |
All sources are loaded locally; no network download is attempted. Volumes are canonicalized to RAS axes where the reader supports it. Each panel selects a deterministic high-contrast non-border slice and labels modality, format, source, axis/index, orientation, and file count. All ten filters operate independently on that selected 2D slice. Intensities are windowed to the 1st– 99th percentile and normalized to [0, 1] for the demo only; filter outputs are displayed with independent contrast.
All panels are 2400×1800 PNGs. The overview, source-overview.png, is
1800×1350 for the seven-case set. Outputs remain in the ignored
example-output/ directory, also the default directory
for --all or --images when --output-dir is omitted. Rerunning overwrites
matching filenames. The existing root filter-qa.png is preserved by this
multi-image command.
Select a subset with --images mri pet ct, or supply user files with
--input /path/case.nii.gz /path/case.nrrd /path/image.dcm and
--dicom-series /path/series-directory. NRRD is supported through SimpleITK,
but no bundled NRRD fixture exists in this checkout, so it is not claimed in
the default demonstration. A DICOM directory is read as one ordered volume
using SimpleITK's series reader. --slice-axis and --slice-index override
the deterministic selection. --contact-sheet is optional. --output and
--output-dir are mutually exclusive. cases.json records the exact source,
format, dimensions, slice choice, and window for each generated panel.