mohamad bcaae5ec22 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.
2026-09-10 01:58:07 +03:30

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.

S
Description
No description provided
Readme 68 KiB
Languages
Python 100%