NIfTI Non-Local Means denoiser (Python port of EZminc mincnlm) - #181
zihuaihuai wants to merge 4 commits into
Conversation
Replace the C++/MINC `mincnlm` wrapper with a NIfTI-native Python tool. mincnlm wraps Coupé et al.'s optimized blockwise Non-Local Means (ONLM); dipy ships the same algorithm plus automatic noise estimation, so this is a re-wrapping (nibabel I/O + dipy) rather than a line-by-line port. - functions/nlm_denoise.py: CLI with mincnlm flags mapped over (-sigma/-v/-d/-w/-block/-mt); dipy imported lazily; run_denoise supports both new (nlmeans method=) and old (non_local_means) dipy APIs. - tests/test_nlm_denoise.py: unit tests for CLI/guards (no dipy) + dipy-gated integration tests asserting noise reduction and geometry preservation. - add dipy=1.7.0 to both conda env files (was missing; nibabel already present). Note: mincnlm's -beta bandwidth scale is fixed at 1.0 by dipy (warned on).
…re tests Make the NIfTI denoiser prudent enough to replace mincnlm in real pipelines: - Clean errors (DenoiseError -> "error: ..." exit) instead of tracebacks for missing/unreadable input, non-3D input, negative sigma, missing mask, and mask/image shape mismatch. - Reject non-positive --patch-radius / --block-radius at parse time. - sanitize() replaces non-finite voxels (NaN/Inf) with 0 and warns. - build_output() copies the input affine + header and sets the on-disk dtype to float32, so NIfTI geometry survives the round-trip. - Auto-create the output's parent directory. - Refactor into small testable seams (load_3d_volume, load_mask, sanitize, build_output, denoise_file). Tests grow from 10 to 23: bad-input guards, radius validation, non-finite sanitize, geometry/dtype preservation, parent-dir creation, .nii round-trip, masked run, and blockwise-vs-voxelwise. Unit tests need no dipy; integration tests are dipy-gated. All 23 pass.
|
Thanks for the clarification. I think there is an important nuance here regarding both the ONLM formulation and how EZminc implements The ONLM method defines similarity weights through an exponential kernel, where h controls the decay of patch similarity. In MRI-specific adaptations, h is typically parameterized as a function of the estimated noise level σ and a user-controlled scaling factor β. However, the original formulation does not strictly define a fixed relationship like h = β · σ; rather, β modulates the effective kernel width through this decomposition. In EZminc’s In this context, β acts as an additional degree of control over the effective bandwidth of the weighting function. Together, σ and β define the sensitivity of the patch similarity kernel, and collapsing this relationship (e.g., fixing β = 1.0 as in DIPY for compatibility) removes a meaningful tuning dimension. This distinction is particularly important in MRI applications. In practice, overestimation of σ can lead to overly aggressive smoothing, suppressing fine structural detail that may still be biologically meaningful but has low SNR. Conversely, underestimation leaves residual noise that can propagate into downstream analyses. I had already evaluated DIPY, ANTs, and other open-source denoising approaches (including Python-based implementations). My main reason for initially preferring EZminc’s C++/MINC This concern is especially relevant for high-resolution structural MRI (e.g., 7T), where small changes in denoising behavior can meaningfully affect downstream morphometric measures. Additionally, performance is a key factor: the C++ ONLM implementation is significantly faster and more memory efficient than Python-based implementations, which becomes critical for large 7T datasets. Reference: |
|
Additionally, currently we use EZminc in the 7T processing pipeline, so the goal was to replicate the same processing behavior while removing the dependency on minctools and moving to a NIfTI-native workflow. |
… job timeout, shared CI paths)
|
The benchmark does not address the concern being raised. |
Prototype for the dipy-vs-ANTs comparison (see PR discussion). ANTs DenoiseImage is a C++ adaptive-NLM (same ONLM family as EZminc mincnlm), NIfTI-native, ~1.7x faster than dipy in benchmarks, already in the micapipe base, and validated for structural MRI. - functions/ants_denoise.py: wraps `DenoiseImage`, mirroring nlm_denoise.py's CLI so the two engines are drop-in comparable (--noise-model -> Gaussian/ Rician, --patch-radius -> -p, --block-radius -> -r, --mask -> -x, --threads -> ITK threads). --sigma is accepted-but-ignored (ANTs auto-estimates). Clean-error guards for missing input/mask, clobber, missing binary, nonzero exit; auto-creates the output dir. - tests/test_ants_denoise.py: 12 unit tests (command mapping + guards via an injectable runner, no ANTs needed) + a DenoiseImage-gated integration test. Verified end-to-end in the micapipe container.
5b8268b to
40f042d
Compare
|
Closing this PR. The dipy-based approach here can't meet the stated requirement — functional equivalence with EZminc Moving instead to a faithful from-source reimplementation of Coupé et al.'s blockwise ONLM — ported directly from the EZminc C++ — as a standalone, NIfTI-native, pip/conda-installable package, validated against golden |
Replaces EZminc's C++/MINC
mincnlmwrapper with a NIfTI-native Python tool.mincnlm wraps Coupé et al.'s optimized blockwise Non-Local Means (ONLM); dipy
ships the same algorithm plus automatic noise estimation, so this is a
re-wrapping (nibabel I/O + dipy) rather than a line-by-line port.
What's here
functions/nlm_denoise.py— CLI with mincnlm flags mapped over(
-sigma/-v/-d/-w/-block/-mt). NIfTI in/out; input affine + headergeometry preserved. dipy imported lazily;
run_denoisesupports both new(
nlmeans(method=)) and old (non_local_means) dipy APIs.negative sigma, mask shape mismatch; non-positive radii rejected; NaN/Inf
sanitized; output parent dir auto-created.
tests/test_nlm_denoise.py— 23 tests. Unit tests (CLI/guards/pure helpers)need no dipy; integration tests are dipy-gated and assert noise reduction +
geometry/dtype preservation. All pass locally.
dipy=1.7.0to both conda env files (nibabel was already present).Notes
-beta(bandwidth scale) is fixed at 1.0 by dipy; a warning is emitted if adifferent value is passed.
micapipe_environment*.yml, so a base-image rebuild(
ci:build-baselabel) is needed before the container ships dipy.