Skip to content

BUG: Fix uninitialized output in LabelSet erode and dilate at zero scale#6631

Merged
hjmjohnson merged 1 commit into
InsightSoftwareConsortium:mainfrom
physwkim:bug-labelset-morphology-zero-scale-6575
Jul 15, 2026
Merged

BUG: Fix uninitialized output in LabelSet erode and dilate at zero scale#6631
hjmjohnson merged 1 commit into
InsightSoftwareConsortium:mainfrom
physwkim:bug-labelset-morphology-zero-scale-6575

Conversation

@physwkim

Copy link
Copy Markdown
Contributor

LabelSetErodeImageFilter and LabelSetDilateImageFilter only call AllocateOutputs() and gate every write on m_Scale[d] > 0, so a zero radius on the last axis skips erosion's only output-writing pass and the filter returns uninitialized memory. Addresses B90 of #6575.

Root cause and fix

Erosion commits to the output only on lastpass, defined as the geometrically last dimension. Under UseImageSpacing, a zero radius on that axis makes its scale zero, the axis pass never runs, lastpass is never reached, and the output is whatever AllocateOutputs() left there. With every radius zero, neither filter writes the output at all.

Two changes, both in the shared base: lastpass is now gated on m_LastActiveDimension — the highest-index axis that actually runs a pass — so the commit happens on an axis that exists; and when no axis is active at all, the output is seeded from the input, which is the correct identity result for a zero-radius morphological operation.

The seed is deliberately scoped to the fully-degenerate case. An unconditional seed was measured at 7-10% of GenerateData() wall time on a 182x218x182 label volume and is pure waste, because on any active configuration the lastpass block writes every pixel of the region — verified by poisoning the seed with a sentinel and confirming all six baseline-compared tests still match bit-for-bit.

Local validation

New GoogleTest itkLabelSetMorphBaseImageFilterGTest.cxx in a new driver:

  • ErodeZeroRadiusOnLastAxisStillErodesActiveAxis — fail-before: uninitialized garbage. Pass-after: correct erosion on the active axis.
  • ErodeAllZeroRadiusIsIdentity / DilateAllZeroRadiusIsIdentity — fail-before: uninitialized garbage (e.g. 147 where 0 was expected). Pass-after: identity.

ctest -L LabelErodeDilate: 8/8 pass, bit-identical baselines.

AI assistance

@github-actions github-actions Bot added type:Bug Inconsistencies or issues which will cause an incorrect result under some or all circumstances 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 area:Filtering Issues affecting the Filtering module labels Jul 14, 2026
@greptile-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes zero-scale LabelSet morphology output handling. The main changes are:

  • Commit erosion output on the last active dimension.
  • Copy input to output when no dimension pass runs.
  • Add regression tests for partially zero erosion and all-zero erode/dilate cases.
  • Add GoogleTest wiring for the new tests.

Confidence Score: 5/5

This looks safe to merge.

No blocking issues found in the changed code.

No files need attention.

T-Rex T-Rex Logs

What T-Rex did

  • I reviewed the general-contract-validation-proof and confirmed it documents command detection, pip install, configure output, partial build output, CTest listing, and an attempt to build the LabelErodeDilate tests.
  • I noted that the cmake --build command for LabelErodeDilateGTestDriver and LabelErodeDilateTestDriver did not finish within the allotted budget and was terminated after progressing only through dependency compilation.
  • I confirmed that no LabelSetMorphBase zero-radius GTest binary was produced to run tests due to the incomplete build.
  • I anchored the proof log as the primary artifact for reviewers to inspect the setup steps and the blocker details.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
Modules/Filtering/LabelErodeDilate/include/itkLabelSetErodeImageFilter.hxx Uses the last active dimension when deciding where erosion writes final output.
Modules/Filtering/LabelErodeDilate/include/itkLabelSetMorphBaseImageFilter.h Adds state for the last dimension with active scale.
Modules/Filtering/LabelErodeDilate/include/itkLabelSetMorphBaseImageFilter.hxx Computes active dimensions and seeds identity output for fully inactive morphology.
Modules/Filtering/LabelErodeDilate/itk-module.cmake Adds the GoogleTest test dependency.
Modules/Filtering/LabelErodeDilate/test/CMakeLists.txt Registers the new GoogleTest driver source.
Modules/Filtering/LabelErodeDilate/test/itkLabelSetMorphBaseImageFilterGTest.cxx Adds tests for zero-radius erosion and dilation behavior.

Reviews (2): Last reviewed commit: "BUG: Fix uninitialized output in LabelSe..." | Re-trigger Greptile

@hjmjohnson
hjmjohnson marked this pull request as draft July 14, 2026 14:58
@hjmjohnson
hjmjohnson force-pushed the bug-labelset-morphology-zero-scale-6575 branch from a384847 to 143c105 Compare July 14, 2026 18:25
@hjmjohnson
hjmjohnson marked this pull request as ready for review July 15, 2026 00:28
@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Want your agent to iterate on Greptile's feedback? Try greploops.

GenerateData only called AllocateOutputs, so output stayed
uninitialized until a per-axis pass wrote it. Erode's write was
gated on lastpass comparing to the geometrically-last dimension
instead of the last axis with nonzero scale, so a zero scale on
the last axis skipped the only write; a fully zero radius skipped
it for both filters.

Gate erode's lastpass on the last axis with nonzero scale, and
seed identity only when no axis will run a pass at all.

Addresses item B90 of InsightSoftwareConsortium#6575.
@hjmjohnson
hjmjohnson force-pushed the bug-labelset-morphology-zero-scale-6575 branch from 143c105 to 6d14b6b Compare July 15, 2026 02:39
@hjmjohnson

Copy link
Copy Markdown
Member

Force-push fixes the m_BaseSigma/NaN dependency left by this PR's own fix. ctest -L LabelErodeDilate: 8/8 pass.

Root cause and fix

firstval (the axis whose scale seeds m_BaseSigma) was keyed on m_Radius[P] != 0, while the per-axis passes and the new m_LastActiveDimension gate are keyed on m_Scale[P] > 0. With UseImageSpacing() at its default of false, the +1 margin means m_Scale[P] is positive even at m_Radius[P] == 0 — so the two conditions diverge. With radius=[0,3]: m_Scale=[1, 5.5], firstval=1 (first nonzero radius) → m_BaseSigma=5.5, but axis 0's pass actually runs with sigma=1. The commit's own new erosion test in itkLabelSetUtils.h is an exact float equality against m_BaseSigma, which the axis-0 pass can never reach — zeroing the entire output. Separately, an all-zero radius with UseImageSpacing on produced m_Scale = [0, NaN] (0.0/0.0), and the new identity-path guard was only correct because NaN > 0 evaluates false — a fragile, undocumented dependency.

Fixed by keying firstval on m_Scale[P] > 0 (the same predicate already used for m_LastActiveDimension), and guarding the normalization divide so it only runs when a nonzero-scale axis was actually found. This makes the identity-path guard (renamed from anyScaleActive, now reusing firstValFound — they were computing the exact same condition) correct by construction instead of by NaN comparison semantics.

Two finder passes in review proposed re-keying on m_Radius[P] != 0 instead of m_Scale; that's disprovable: with UseImageSpacing off and radius=[3,0], m_Scale=[5.5, 1], so axis 1 is active and its pass does run — keying on radius would again pick the wrong axis. m_Scale is the only field that says which axis actually gets a pass.

Also fixed: m_LastActiveDimension had no initializer (indeterminate int on a protected base member until first assigned).

Local validation

New test ErodeWithDefaultSpacingAndZeroRadiusOnFirstAxisIsNotAllZero (default UseImageSpacing=false, radius=[0,3]) — the filter's actual default was previously untested; all three existing tests use UseImageSpacingOn(). Verified fail-before/pass-after by stashing the fix and re-running: fails against the unfixed code (interior pixel reads 0 instead of the label), passes with the fix.

pre-commit run --all-files: exit 0. LabelErodeDilateGTestDriver: 4/4 pass. ctest -L LabelErodeDilate: 8/8 pass, 0 regressions.

@hjmjohnson
hjmjohnson merged commit 4357acc into InsightSoftwareConsortium:main Jul 15, 2026
21 checks passed
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 type:Bug Inconsistencies or issues which will cause an incorrect result under some or all circumstances 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.

2 participants