Skip to content

NIfTI Non-Local Means denoiser (Python port of EZminc mincnlm) - #181

Closed
zihuaihuai wants to merge 4 commits into
v1from
enning/mincnlm-nifti-rewrite
Closed

zihuaihuai wants to merge 4 commits into
v1from
enning/mincnlm-nifti-rewrite

Conversation

@zihuaihuai

@zihuaihuai zihuaihuai commented Jul 2, 2026 •

Copy link
Copy Markdown
Collaborator

Replaces EZminc's 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.

What's here

  • functions/nlm_denoise.py — CLI with mincnlm flags mapped over
    (-sigma/-v/-d/-w/-block/-mt). NIfTI in/out; input affine + header
    geometry preserved. dipy imported lazily; run_denoise supports both new
    (nlmeans(method=)) and old (non_local_means) dipy APIs.
  • Defensive input handling: clean errors for missing/unreadable/non-3D input,
    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.
  • Adds dipy=1.7.0 to both conda env files (nibabel was already present).

Notes

  • -beta (bandwidth scale) is fixed at 1.0 by dipy; a warning is emitted if a
    different value is passed.
  • Not yet wired into any pipeline stage — this PR adds the standalone tool.
  • The env change touches micapipe_environment*.yml, so a base-image rebuild
    (ci:build-base label) is needed before the container ships dipy.

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.
@rcruces

rcruces commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the clarification. I think there is an important nuance here regarding both the ONLM formulation and how EZminc implements mincnlm.

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 mincnlm, σ is either estimated automatically from the data (when not provided or set to 0) or taken as user input if explicitly specified. This matters because σ directly controls the scale of the similarity kernel and therefore the strength of denoising.

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 mincnlm is that DIPY’s noise estimation can be overly aggressive in some structural MRI contexts, and its patch-based optimization is not always stable for high-resolution data. This can introduce subtle intensity and texture changes that propagate into downstream steps such as surface reconstruction and tissue segmentation.

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:
Coupé et al., An optimized blockwise nonlocal means denoising filter for 3-D magnetic resonance images, Journal of Magnetic Resonance Imaging, 2008. https://doi.org/10.1002/jmri.22003

@rcruces

rcruces commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

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.
For reference, the closest available denoising implementation is ANTs rather than DIPY. However, ANTs denoising also differs in that it does not expose the same tuning parameters (β and σ), and it is significantly slower, particularly for high-resolution 7T datasets:
https://antspyx.readthedocs.io/en/latest/api/ants.ops.denoise_image.html

@rcruces

rcruces commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

The benchmark does not address the concern being raised.
It is based on 3T CI data and synthetic cubes, which are not representative of the actual use case: 7T MP2RAGE 0.5 mm isotropic structural images. At this resolution, the issue is not global denoising metrics, but subtle changes in high-frequency anatomical detail that can propagate into surface reconstruction and segmentation.
The reported metrics (RMSE, gradient correlation, residual structure) are voxel-level proxies and do not capture downstream effects on cortical surfaces or morphometry, where differences typically appear.
This is also not a general comparison of “denoising quality” between tools. The requirement here is functional equivalence to the existing EZminc-based 7T pipeline, since downstream processing is already tuned to that output.
So the relevant validation is:
same 7T MP2RAGE subject
full pipeline (proc_structural + surface reconstruction)
comparison of surfaces and segmentation outputs
Without that, the conclusion that DIPY or ANTs are interchangeable for this use case is not supported.

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.
@zihuaihuai
zihuaihuai force-pushed the enning/mincnlm-nifti-rewrite branch from 5b8268b to 40f042d Compare July 2, 2026 19:55
@zihuaihuai

Copy link
Copy Markdown
Collaborator Author

Closing this PR. The dipy-based approach here can't meet the stated requirement — functional equivalence with EZminc mincnlm: dipy fixes β=1.0 and uses a different (more aggressive) noise estimator, exactly the concerns raised above.

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 mincnlm outputs for I/O equivalence. Will link the new repo here once it's up.

@zihuaihuai zihuaihuai closed this Jul 2, 2026
@zihuaihuai
zihuaihuai deleted the enning/mincnlm-nifti-rewrite branch July 2, 2026 22:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants