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
+155
View File
@@ -0,0 +1,155 @@
"""Offline medical-image filter comparisons from local volumes and DICOM."""
from __future__ import annotations
import argparse
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import json
import re
import textwrap
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent))
from medical_io import CASE_NAMES, bundled_cases, load_medical, representative_plane
from image_filter import (
CannyFilter, GaussianFilter, MedianFilter, SobelFilter, LaplacianFilter,
GaborFilter, LawsFilter, LoGFilter, MeanFilter, WaveletFilter, Pipeline,
)
DEFAULT_DIR = Path(__file__).resolve().parents[1] / "example-output"
STYLE = {"font.family": "DejaVu Sans", "font.size": 11,
"text.color": "#18283b", "axes.titlecolor": "#18283b",
"figure.facecolor": "#f4f6f9", "savefig.facecolor": "#f4f6f9"}
def render_panel(name: str, image: np.ndarray, info: dict, output: Path) -> None:
operations = [
("Mean 5×5", MeanFilter(5)),
("Gaussian σ=1.5", GaussianFilter(1.5)),
("Median 5×5", MedianFilter(5)),
("Sobel magnitude", SobelFilter()),
("Laplacian", LaplacianFilter()),
("Canny σ=1.2 (quantiles .1/.3)", CannyFilter(1.2, .1, .3, True)),
("Laws L5E5 energy r=3", LawsFilter("L5E5", energy_radius=3)),
("LoG σ=1.5", LoGFilter(1.5)),
("Gabor magnitude", GaborFilter(0.15)),
("SWT LH (db2)", WaveletFilter("LH", "db2")),
]
# One-step composites preserve the existing independent comparisons.
panels = [("Input", image)] + [
(title, Pipeline([operation]).apply(image).image)
for title, operation in operations
]
with plt.rc_context(STYLE):
figure, axes = plt.subplots(3, 4, figsize=(16, 12))
figure.subplots_adjust(left=.025, right=.975, bottom=.035, top=.88,
hspace=.23, wspace=.10)
figure.text(.025, .965, "IMAGE FILTER", fontsize=12, weight="bold", color="#426080")
figure.text(.025, .925, f"{info['modality']} · {info['format']} · {name}", fontsize=23, weight="bold")
for index, (axis, (title, values)) in enumerate(zip(axes.flat, panels)):
limits = (0, 1) if values.dtype == np.bool_ else np.percentile(values, (1, 99))
axis.imshow(values, cmap="gray", vmin=limits[0], vmax=limits[1])
axis.set_title(f"{index:02d} {title}", fontsize=12, loc="left", pad=9)
axis.axis("off")
note = axes.flat[-1]
note.axis("off")
selection = f"Axis {info['slice_axis']} / slice {info['slice_index']}" if info['slice_index'] is not None else "Native 2D frame"
source_label = textwrap.fill(info['source'], width=30)
note.text(.02, .95, "CASE / PROCESSING", transform=note.transAxes, weight="bold", fontsize=12)
note.text(.02, .83,
f"Source: {source_label}\n{selection} · {info['files']} file(s)\nInput: {image.shape[1]} × {image.shape[0]}\n{info['orientation']}\n\n1–99% window → [0, 1] input.\nAll filters act on this 2D slice.\nIndependent output display contrast.\nCanny: boolean edges.",
transform=note.transAxes, fontsize=10, va="top", linespacing=1.55)
figure.savefig(output, dpi=150, metadata={"Title": f"Image Filter comparison: {name}"})
plt.close(figure)
def render_overview(originals: dict[str, tuple[np.ndarray, dict]], output: Path) -> None:
rows = (len(originals) + 2) // 3
with plt.rc_context(STYLE):
figure, axes = plt.subplots(rows, 3, figsize=(12, 4 * rows + 1), squeeze=False)
figure.subplots_adjust(left=.035, right=.965, bottom=.045, top=1 - 1.25 / (4 * rows + 1),
hspace=.25, wspace=.12)
figure.suptitle("Image Filter · medical imaging examples", fontsize=20, weight="bold", y=.975)
for axis in axes.flat:
axis.axis("off")
for axis, (name, (original, info)) in zip(axes.flat, originals.items()):
axis.imshow(original, cmap="gray", vmin=0, vmax=1)
axis.set_anchor("N")
axis.set_title(f"{info['modality']} · {info['format']}\n{name}-filters.png", fontsize=12, loc="left")
figure.savefig(output, dpi=150)
plt.close(figure)
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
selection = parser.add_mutually_exclusive_group()
selection.add_argument("--all", action="store_true", help="All available local medical fixtures")
selection.add_argument("--images", nargs="+", choices=CASE_NAMES, help="Select local medical cases")
parser.add_argument("--input", type=Path, nargs="+", default=[], help="User NIfTI, NRRD or single DICOM paths")
parser.add_argument("--dicom-series", type=Path, nargs="+", default=[], help="DICOM series directories (one volume each)")
parser.add_argument("--series-uid", help="Select a series when a directory contains multiple series")
parser.add_argument("--modality", default="Unknown", help="Modality label for user NIfTI/NRRD files")
parser.add_argument("--slice-axis", type=int, choices=(0, 1, 2), default=2, help="Canonical voxel axis for volumes")
parser.add_argument("--slice-index", type=int, help="Override deterministic non-empty slice selection")
destination = parser.add_mutually_exclusive_group()
destination.add_argument("--output", type=Path, help="Single PNG output")
destination.add_argument("--output-dir", type=Path, default=None, help="Default: package example-output directory")
parser.add_argument("--contact-sheet", action="store_true", help="Write source-overview.png")
args = parser.parse_args(argv)
args.names = list(CASE_NAMES) if args.all else list(dict.fromkeys(args.images or ([] if args.input or args.dicom_series else ['mri'])))
count = len(args.names) + len(args.input) + len(args.dicom_series)
if args.output and (count != 1 or args.output.suffix.lower() != '.png'):
parser.error('--output requires exactly one source and a .png extension')
if args.output and args.output.name == 'source-overview.png':
parser.error('--output must not use the reserved overview filename')
return args
def main(argv: list[str] | None = None) -> None:
args = parse_args(argv)
catalog = bundled_cases()
selections = []
for name in args.names:
if name not in catalog or not catalog[name][0].exists():
if args.all:
print(f"Unavailable bundled case: {name}; no download attempted")
continue
raise SystemExit(f"Unavailable bundled case: {name}")
path, modality = catalog[name]
selections.append((name, path, modality))
for path in args.input + args.dicom_series:
name = re.sub(r'[^a-z0-9_-]+', '-', path.name.lower().removesuffix('.gz').removesuffix('.nii').removesuffix('.nrrd').removesuffix('.dcm')).strip('-')
selections.append((name or 'case', path, args.modality))
if len({name for name, _, _ in selections}) != len(selections):
raise SystemExit('Source basenames collide; run cases separately with distinct --output names')
if not selections:
raise SystemExit('No local medical fixtures available')
directory = args.output.parent if args.output else (args.output_dir or DEFAULT_DIR)
directory.mkdir(parents=True, exist_ok=True)
results = {}
for name, path, modality in selections:
try:
case = load_medical(path, modality, args.series_uid)
image, info = representative_plane(case, args.slice_axis, args.slice_index)
except (ValueError, FileNotFoundError, RuntimeError, ImportError) as exc:
raise SystemExit(f"{name}: {exc}") from exc
output = args.output or directory / f'{name}-filters.png'
render_panel(name, image, info, output)
results[name] = (image, info)
print(output.resolve())
(directory / 'cases.json').write_text(json.dumps({name: info for name, (_, info) in results.items()}, indent=2) + '\n')
if args.contact_sheet:
output = directory / 'source-overview.png'
render_overview(results, output)
print(output.resolve())
if args.all:
print('Bundled NRRD coverage unavailable; use --input /path/to/case.nrrd. No downloads performed.')
if __name__ == "__main__":
main()