Skip to content

Ingest ITKAdaptiveDenoising into Modules/Filtering#6235

Open
hjmjohnson wants to merge 39 commits intoInsightSoftwareConsortium:mainfrom
hjmjohnson:ingest-AdaptiveDenoising
Open

Ingest ITKAdaptiveDenoising into Modules/Filtering#6235
hjmjohnson wants to merge 39 commits intoInsightSoftwareConsortium:mainfrom
hjmjohnson:ingest-AdaptiveDenoising

Conversation

@hjmjohnson
Copy link
Copy Markdown
Member

@hjmjohnson hjmjohnson commented May 8, 2026

Ingest the standalone remote module
ntustison/ITKAdaptiveDenoising
into Modules/Filtering/AdaptiveDenoising/. Patch-based adaptive
non-local-means denoising is now in-tree under
EXCLUDE_FROM_DEFAULT.

Built directly on upstream/main; the v4 ingestion guidelines
defined in #6204 (sanitize-history pass, deny-pattern pass,
whitelist-only paths, ghostflow-clean per-commit subjects) were
applied locally to produce this PR's content, but this PR does
not depend on #6204 merging first.

Test data is gated on
InsightSoftwareConsortium/ITKTestingData#48,
which uploads the 3 fixture blobs the new tests fetch via .cid
stubs. Closes the consolidation work of issue #6060 for this
module.

Stacked on #6236 (pixi 0.68 multi-line cmd compatibility fix). The visible diff includes #6236's pyproject.toml line-continuation changes until that PR merges; once it lands, this branch can be rebased onto main (using git rebase --rebase-merges to keep the 9 ingest merges intact) and the pyproject.toml hunks will drop out.

No raw binary blobs in ITK history

The upstream module shipped its 3 test fixtures
(r16slice.nrrd, r16denoised_mean_squares.nrrd,
r16denoised_pearson_correlation.nrrd) inline in git rather than
via the ITK ExternalData / content-link convention. AGENTS.md
decision 3 forbids merging an ingest with raw binary test/
assets.

Rather than carry the binaries through the merge and then swap
them at the tip, the rewritten upstream history was passed
through git filter-repo --path-glob '*.nrrd' --invert-paths
so that no commit reachable from this PR's HEAD adds a raw
.nrrd file
:

$ git log --diff-filter=A --name-only HEAD -- '*.nrrd' | grep -c '\.nrrd$'
0

The .cid stubs are added in a single dedicated commit at the
tip and point at the CID blobs published in
ITKTestingData PR #48.

Ingest results
Aspect Value
Upstream commits → after sanitize 62 → 34
Merge commits preserved 8 in the rewritten upstream branch (no linearization)
Final tree merge count 9 (incl. ingest merge)
Whitelisted files 17 (include/, src/, test/, wrapping/, CMakeLists.txt, itk-module.cmake, LICENSE)
Raw .nrrd files in history reachable from HEAD 0
Content-links at HEAD 3 .cid, 0 .md5, 0 .shaNNN, 0 raw binaries
examples/ directory Auto-excluded; remains in archived upstream
License Apache-2.0
git blame Walks back to original upstream contributors (Nicholas Tustison and others)
Local validation
  • pre-commit run --all-files: clean
  • cmake -DModule_AdaptiveDenoising:BOOL=ON .: configures clean
  • Compile check: new itkAdaptiveNonLocalMeansDenoisingImageFilterTest.cxx.o
    and itkNonLocalPatchBasedImageFilterEnums.cxx.o build clean
    (16 pre-existing warnings about long subject lines from the
    upstream's itkGetConstMacro-generated names — not new).
  • Test execution gated on
    ITKTestingData#48
    merging so the 3 .cid blobs are reachable through the standard
    mirror at fetch time.
Commits at PR tip (top → bottom)
  1. ENH: Enable Module_AdaptiveDenoising in configure-ci
  2. COMP: Remove AdaptiveDenoising .remote.cmake (in-tree)
  3. DOC: Add AdaptiveDenoising README pointing at upstream
  4. ENH: Add AdaptiveDenoising test data .cid stubs
    (3 stub files pointing at ITKTestingData PR ENH: Update CircleCI CMake version to 3.11.2 #48 blobs)
  5. ENH: Ingest ITKAdaptiveDenoising into Modules/Filtering
    (merge commit --no-ff --allow-unrelated-histories to
    sanitized + .nrrd-stripped filter-repo'd upstream history;
    preserves 8 upstream merges and git blame to original authors)
Phase B (post-merge) — upstream archival

After this PR merges, run Phase B from a fresh worktree:

Utilities/Maintenance/RemoteModuleIngest/archive-remote-module.sh \
    AdaptiveDenoising

Phase B publishes the upstream removal commit, promotes
MIGRATION_README.md to README.md so the GitHub landing page
renders the migration notice (per the
ingest-archive-readme rule), and flips
archived=true. No upstream side-effects happen until that step.

Refs: #6060

ntustison and others added 30 commits September 26, 2020 10:31
Use the `ITK_DISALLOW_COPY_AND_ASSIGN` macro to enhance consistency across
the toolkit when disallowing the copy constructor and the assign operator.

Move the `ITK_DISALLOW_COPY_AND_ASSIGN` calls to the `public` section
following the discussion in
https://discourse.itk.org/t/itk-disallow-copy-and-assign/648
…gnMacro

COMP: Use and move ITK_DISALLOW_COPY_AND_ASSIGN calls to public section.
Use strongly type enums.

Set `AdaptiveDenoising_LIBRARIES` for external builds too derived from the
changes involved by the presence of a C++ implementation file.
Test the similarity metric.

Take advantage of the commit to improve the test style to make a better
use of the ITK macros.
Replace the deprecated ITK_DISALLOW_COPY_AND_ASSIGN with
new better descriptive name ITK_DISALLOW_COPY_AND_MOVE.
For consistency, a ';' will be required in a future
version of ITK.
COMP: Update to new ITK_DISALLOW_COPY_AND_MOVE macro
Use of SetUseImageSpacingOn() is deprecated in the future.
'Set' prefixed functions have an argument that is used to
set an ivar.

UseImageSpacingOn() is also valid, but is less compatible
for older versions of ITK.
The ability to include either .h or .hxx files as
header files required recursively reading the
.h files twice.  The added complexity is
unnecessary, costly, and can confuse static
analysis tools that monitor header guardes (due
to reaching the maximum depth of recursion
limits for nested #ifdefs in checking).
…ludes

COMP: Remove inclusion of .hxx files as headers
Images with small values have standard deviations
that could be smaller than 1.0, but be perfectly valid.

For the case when the standard deviation is zero,
set all the z-score values to zero.
…utations

Fix small value zscore computations
ntustison and others added 4 commits June 1, 2022 13:08
ENH: Bump ITK and change http to https
When preparing for the future with ITK by setting
ITK_FUTURE_LEGACY_REMOVE:BOOL=ON
ITK_LEGACY_REMOVEBOOL=ON

The future preferred macro should be used
│ -  itkTypeMacro
│ +  itkOverrideGetNameOfClassMacro
Use static constexpr directly now that C++11 conformance
is required by all compilers.

:%s/itkStaticConstMacro *( *\([^,]*\),[ \_s]*\([^,]*\),\_s*\([^)]*\)) */static constexpr \2 \1 = \3/ge
…ix-legacy-remove

COMP: Use modern macro for name of class
@github-actions github-actions Bot added type:Infrastructure Infrastructure/ecosystem related changes, such as CMake or buildbots type:Enhancement Improvement of existing methods or implementation area:Python wrapping Python bindings for a class type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct area:Filtering Issues affecting the Filtering module area:Remotes Issues affecting the Remote module labels May 8, 2026
@hjmjohnson hjmjohnson force-pushed the ingest-AdaptiveDenoising branch from 7bff92d to d2e8ffa Compare May 8, 2026 11:49
@hjmjohnson hjmjohnson changed the title ENH: Ingest ITKAdaptiveDenoising into Modules/Filtering (v4, stacks on #6204; needs ITKTestingData#48) ENH: Ingest ITKAdaptiveDenoising into Modules/Filtering (needs ITKTestingData#48) May 8, 2026
@hjmjohnson hjmjohnson force-pushed the ingest-AdaptiveDenoising branch 2 times, most recently from 84fa925 to 67858a2 Compare May 8, 2026 13:42
hjmjohnson added 5 commits May 8, 2026 08:43
Phase A v4 ingest of the standalone remote module
ntustison/ITKAdaptiveDenoising into Modules/Filtering/AdaptiveDenoising/.
Whitelisted upstream history was sanitized via the v4 pipeline
(see InsightSoftwareConsortium#6204), then stripped of all raw
.nrrd test fixtures via 'git filter-repo --invert-paths' so that no
raw binary blobs land in ITK's git history.  Sibling .cid stubs are
added in a follow-up commit; their fetch path is published via
ITKTestingData PR InsightSoftwareConsortium#48.

Stacked on the pixi 0.68 multi-line cmd compatibility fix from InsightSoftwareConsortium#6236.
Test fixtures for the AdaptiveNonLocalMeansDenoising filter:

  bafkreiafwnstu4vzkrl7gpt73tl3q53ywpdylbs2d4emezgzutj2thoyfy
      r16slice.nrrd                          (test/Input)
  bafkreigltzq46r5ojiiyel37egvlosnxxgyth6ppwckzjtuxyywtlijoii
      r16denoised_mean_squares.nrrd          (test/Baseline)
  bafkreickjyvdwmq6eyz76e7cukkegm4dd75raimxj4pag2o6bkanvbofwm
      r16denoised_pearson_correlation.nrrd   (test/Baseline)

The blobs are pre-uploaded to ITKTestingData via PR InsightSoftwareConsortium#48; the
upstream remote module shipped these as raw .nrrd files inline in
git, which were stripped from the rewritten ingest history (no raw
binaries in ITK's history) and replaced by these .cid stubs at the
ingest tip.  CIDs were computed via 'ipfs add --cid-version=1
--raw-leaves'.
@hjmjohnson hjmjohnson force-pushed the ingest-AdaptiveDenoising branch from 67858a2 to 7417214 Compare May 8, 2026 13:43
@hjmjohnson hjmjohnson marked this pull request as ready for review May 8, 2026 15:02
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 8, 2026

Greptile Summary

This PR ingests the standalone remote module ntustison/ITKAdaptiveDenoising into Modules/Filtering/AdaptiveDenoising/ under EXCLUDE_FROM_DEFAULT, removes the corresponding .remote.cmake descriptor, replaces the upstream's raw .nrrd test fixtures with .cid content-link stubs, and enables the module in the CI configure command.

  • itkAdaptiveNonLocalMeansDenoisingImageFilter — the flagship patch-based adaptive non-local-means denoiser — is ingested along with its base class itkNonLocalPatchBasedImageFilter and a local itkVarianceImageFilter; Python wrapping and two regression tests are included.
  • Two code-correctness issues carried over from the upstream need attention before or shortly after merge: a thread-safety race in ThreadedGenerateData (writes to shared output pixels outside the per-thread region without synchronization) and a normalization fallthrough in VectorizeImagePatch (near-zero std-dev path zeroes then immediately overwrites with −mean/ε).
  • Style: itkAdaptiveNonLocalMeansDenoisingImageFilter.h and itkVarianceImageFilter.h use typedef throughout while the base-class header already uses using; the VarianceImageFilter Doxygen block misdescribes the filter as "averaging" and "linear".

Confidence Score: 3/5

The ingestion infrastructure (CMake, wrapping, content links, CI config) is correct, but the core filter implementation carries two pre-existing upstream bugs that affect correctness on multi-core builds.

The flagship filter's ThreadedGenerateData writes accumulated output values and contribution counts to arbitrary pixel locations shared across threads without any synchronization. On any machine with more than one core, the read-modify-write sequences on outputImage and m_ThreadContributionCountImage are data races that silently corrupt the denoised result. Separately, VectorizeImagePatch's near-zero standard-deviation branch zeroes the patch but immediately overwrites those zeros with -mean/tiny_stddev, defeating the guard entirely for any subclass calling the method with normalize=true. Both issues were present in the upstream remote module and are inherited here unchanged.

itkAdaptiveNonLocalMeansDenoisingImageFilter.hxx (thread-safety in ThreadedGenerateData) and itkNonLocalPatchBasedImageFilter.hxx (normalization fallthrough in VectorizeImagePatch).

Important Files Changed

Filename Overview
Modules/Filtering/AdaptiveDenoising/include/itkAdaptiveNonLocalMeansDenoisingImageFilter.hxx Flagship filter implementation; contains a thread-safety race condition where ThreadedGenerateData writes output and count-image pixels outside its assigned sub-region without synchronization, producing nondeterministic results on multi-core systems.
Modules/Filtering/AdaptiveDenoising/include/itkNonLocalPatchBasedImageFilter.hxx Base class implementation; VectorizeImagePatch normalization falls through after zeroing elements when std-dev is near-zero, producing large/infinite values instead of zeros for any normalize=true caller.
Modules/Filtering/AdaptiveDenoising/include/itkAdaptiveNonLocalMeansDenoisingImageFilter.h Filter header; uses old typedef style throughout while base class uses modern using aliases — inconsistent with ITK coding standards.
Modules/Filtering/AdaptiveDenoising/include/itkVarianceImageFilter.h Local variance filter header; has incorrect Doxygen ("averaging filter", "linear filter"), a typo ("variacne"), and uses typedef instead of using aliases.
Modules/Filtering/AdaptiveDenoising/include/itkNonLocalPatchBasedImageFilter.h Base class header; clean modern using aliases, enum class with streaming support, legacy aliases guarded by ITK_LEGACY_REMOVE — no issues found.
Modules/Filtering/AdaptiveDenoising/itk-module.cmake Module declaration with correct DEPENDS, COMPILE_DEPENDS, TEST_DEPENDS, EXCLUDE_FROM_DEFAULT, and ENABLE_SHARED — looks correct.
Modules/Filtering/AdaptiveDenoising/test/CMakeLists.txt Two test cases registered with DATA{} content-link references for .cid stubs — correctly follows ITK ExternalData convention.
Modules/Remote/AdaptiveDenoising.remote.cmake Remote module descriptor correctly removed now that the module is in-tree; deletion is the right action.
pyproject.toml Adds -DModule_AdaptiveDenoising:BOOL=ON to the CI configure command so the new module is exercised in CI builds.
Modules/Filtering/AdaptiveDenoising/wrapping/CMakeLists.txt Wrapping order declared correctly: VarianceImageFilter → NonLocalPatchBasedImageFilter → AdaptiveNonLocalMeansDenoisingImageFilter.
Modules/Filtering/AdaptiveDenoising/test/itkAdaptiveNonLocalMeansDenoisingImageFilterTest.cxx Test exercises both similarity metrics, verifies all public API getters/setters, and uses ITK_TRY_EXPECT_NO_EXCEPTION — good coverage of the public interface.

Reviews (1): Last reviewed commit: "ENH: Enable Module_AdaptiveDenoising in ..." | Re-trigger Greptile

Comment on lines +354 to +388
for (unsigned int n = 0; n < neighborhoodPatchSize; n++)
{
IndexType neighborhoodPatchIndex = centerIndex + neighborhoodPatchOffsetList[n];
if (!targetImageRegion.IsInside(neighborhoodPatchIndex))
{
continue;
}
if (this->m_UseRicianNoiseModel)
{
weightedAverageIntensities[n] += maxWeight * itk::Math::sqr(inputImage->GetPixel(neighborhoodPatchIndex));
}
else
{
weightedAverageIntensities[n] += maxWeight * inputImage->GetPixel(neighborhoodPatchIndex);
}
}
sumOfWeights += maxWeight;

if (sumOfWeights > itk::NumericTraits<RealType>::ZeroValue())
{
for (unsigned int n = 0; n < neighborhoodPatchSize; n++)
{
IndexType neighborhoodPatchIndex = centerIndex + neighborhoodPatchOffsetList[n];
if (!targetImageRegion.IsInside(neighborhoodPatchIndex))
{
continue;
}
typename OutputImageType::PixelType estimate = outputImage->GetPixel(neighborhoodPatchIndex);
estimate += (weightedAverageIntensities[n] / sumOfWeights);

outputImage->SetPixel(neighborhoodPatchIndex, estimate);
this->m_ThreadContributionCountImage->SetPixel(
neighborhoodPatchIndex, this->m_ThreadContributionCountImage->GetPixel(neighborhoodPatchIndex) + 1);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Race condition on shared output and count images

ThreadedGenerateData iterates over pixels in the per-thread region but writes outputImage and m_ThreadContributionCountImage at centerIndex + patchOffset, which can land in another thread's region. The read-modify-write sequences (GetPixel + SetPixel) on those shared images are not atomic, so concurrent threads silently corrupt each other's accumulations. The AfterThreadedGenerateData then divides the accumulated output by the corrupted counts, producing incorrect denoising results on any multi-core system. The same pattern appears for m_RicianBiasImage writes in the Rician-correction loop above (lines 234–251).

The canonical ITK fix is to split accumulation (one per-thread intermediate buffer) and merge in AfterThreadedGenerateData, or to use a mutex around the GetPixel/SetPixel pair.

Comment on lines +119 to +131
if (standardDeviation < NumericTraits<RealType>::epsilon())
{
for (auto & it : patchVector)
{
it = 0.;
}
}
const auto inv_standardDeviation = 1. / standardDeviation;
for (auto & it : patchVector)
{
it = (it - mean) * inv_standardDeviation;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Normalization fallthrough when standard deviation is near-zero

When standardDeviation < epsilon, the intent is to return a zero vector. The code zeroes every element in the loop (lines 121–124) but then falls through unconditionally to compute inv_standardDeviation = 1.0 / standardDeviation (a tiny or denormalized divisor) and applies it = (0 - mean) * inv_standardDeviation. The result is -mean / epsilon, which can be orders of magnitude above the expected zero — or even ±Inf. Any callers of VectorizeImagePatch with normalize=true (e.g. future subclasses) will silently receive corrupted patch vectors. An early return after zeroing, or wrapping the normalization block in an else, is needed.

Comment on lines +27 to +36
/** \class VarianceImageFilter
* \brief Applies an averaging filter to an image
*
* Computes an image where a given pixel is the variance value of the
* the pixels in a neighborhood about the corresponding input pixel.
*
* A variacne filter is one of the family of linear filters.
*
* \ingroup AdaptiveDenoising
*/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The class Doxygen comment has two inaccuracies and a typo: (1) it says "averaging filter" when the filter computes variance; (2) it claims variance is a "linear filter", but variance is a nonlinear operation; (3) "variacne" is a typo.

Suggested change
/** \class VarianceImageFilter
* \brief Applies an averaging filter to an image
*
* Computes an image where a given pixel is the variance value of the
* the pixels in a neighborhood about the corresponding input pixel.
*
* A variacne filter is one of the family of linear filters.
*
* \ingroup AdaptiveDenoising
*/
/** \class VarianceImageFilter
* \brief Applies a variance filter to an image
*
* Computes an image where a given pixel is the sample variance of the
* pixels in a neighborhood about the corresponding input pixel.
*
* A variance filter is a nonlinear (quadratic) neighborhood filter.
*
* \ingroup AdaptiveDenoising
*/

Comment on lines +56 to +91
/** Standard class typedefs. */
typedef AdaptiveNonLocalMeansDenoisingImageFilter Self;
typedef NonLocalPatchBasedImageFilter<TInputImage, TOutputImage> Superclass;
typedef SmartPointer<Self> Pointer;
typedef SmartPointer<const Self> ConstPointer;

/** Runtime information support. */
itkOverrideGetNameOfClassMacro(AdaptiveNonLocalMeansDenoisingImageFilter);

/** Standard New method. */
itkNewMacro(Self);

/** ImageDimension constants */
static constexpr unsigned int ImageDimension = TInputImage::ImageDimension;

/** Some convenient typedefs. */
typedef TInputImage InputImageType;
typedef typename InputImageType::PixelType InputPixelType;
typedef TOutputImage OutputImageType;
typedef typename Superclass::RegionType RegionType;

typedef TMaskImage MaskImageType;
typedef typename MaskImageType::PixelType MaskPixelType;
typedef typename MaskImageType::PixelType LabelType;

typedef typename Superclass::RealType RealType;
typedef typename Superclass::RealImageType RealImageType;
typedef typename Superclass::RealImagePointer RealImagePointer;
typedef typename Superclass::IndexType IndexType;

typedef typename Superclass::ConstNeighborhoodIteratorType ConstNeighborhoodIteratorType;
typedef typename Superclass::NeighborhoodRadiusType NeighborhoodRadiusType;
typedef typename Superclass::NeighborhoodOffsetType NeighborhoodOffsetType;
typedef typename Superclass::NeighborhoodOffsetListType NeighborhoodOffsetListType;

typedef GaussianOperator<RealType> ModifiedBesselCalculatorType;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 typedef style instead of using throughout this header and itkVarianceImageFilter.h

All type aliases in this header use the old typedef form rather than the using syntax required by ITK's enforced code style. The base class header itkNonLocalPatchBasedImageFilter.h already uses using consistently. The same issue appears in itkVarianceImageFilter.h (lines 48–71). Both files should be updated to using Foo = Bar style for consistency with the rest of the module and ITK's clang-tidy rules.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Member

@dzenanz dzenanz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly looks good.

ImageRegionIteratorWithIndex<OutputImageType> ItO(this->GetOutput(), this->GetOutput()->GetRequestedRegion());
ImageRegionConstIterator<RealImageType> ItL(this->m_ThreadContributionCountImage,
this->m_ThreadContributionCountImage->GetRequestedRegion());
this->m_ThreadContributionCountImage->GetRequestedRegion());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unwanted change?

set(AdaptiveDenoising_SRCS
itkNonLocalPatchBasedImageFilterEnums.cxx
)
set(AdaptiveDenoising_SRCS itkNonLocalPatchBasedImageFilterEnums.cxx)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unwanted change?

@dzenanz dzenanz changed the title ENH: Ingest ITKAdaptiveDenoising into Modules/Filtering (needs ITKTestingData#48) Ingest ITKAdaptiveDenoising into Modules/Filtering May 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Filtering Issues affecting the Filtering module area:Python wrapping Python bindings for a class area:Remotes Issues affecting the Remote module type:Enhancement Improvement of existing methods or implementation type:Infrastructure Infrastructure/ecosystem related changes, such as CMake or buildbots type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants