BUG: Fix MultiLabelSTAPLE label-loop wrap and undecided-label propagation#6607
BUG: Fix MultiLabelSTAPLE label-loop wrap and undecided-label propagation#6607physwkim wants to merge 2 commits into
Conversation
|
| Filename | Overview |
|---|---|
| Modules/Segmentation/LabelVoting/include/itkMultiLabelSTAPLEImageFilter.hxx | Updates the voting seed setup and label-count loops, but the new undecided-label comparison can skip valid votes when the undecided value is also a real label. |
| Modules/Segmentation/LabelVoting/test/itkMultiLabelSTAPLEImageFilterGTest.cxx | Adds tests for the saturated-label hang and custom undecided-label propagation. |
Reviews (1): Last reviewed commit: "BUG: Propagate undecided label to MultiL..." | Re-trigger Greptile
a185f51 to
eb54220
Compare
|
First force-push = plain rebase on main; second = review fix only (sentinel undecided label for the voting seed). Local |
|
Follow-up force-push = high-effort review fixes for the saturated-label path and the new tests. What changed
Known limitation (documented, not fixable from the voting output alone): in the saturated case the seeding pass cannot distinguish a genuine consensus on the caller's undecided label from a tie, so those samples are excluded from the seed. The non-saturated path — every realistic case — now seeds exactly. Local |
eb54220 to
20b778f
Compare
20b778f to
6d82a79
Compare
|
Force-push = plain rebase onto current main, folding the new GTest source into the |
6d82a79 to
a8e390a
Compare
|
Force-push = rebase onto current Why there were conflictsThis branch predates One substantive-but-small conflict: this PR's own second commit had already tightened |
a8e390a to
9eff1e7
Compare
|
Content-only force-push ( Undecided-sentinel collision (dominant fix). In the saturated path ( Hang-guard test. On timeout the test detached the worker, leaking a CPU-spinning thread that could corrupt later tests in the same binary. A stuck Note on the pre-existing ConsensusOnLabelZero testThe existing Local validationNew test fails on the prior code ( |
9eff1e7 to
1ed9e6f
Compare
|
Force-push |
| for (unsigned int k = 0; k < numberOfInputs; ++k) | ||
| { | ||
| for (InputPixelType inLabel = 0; inLabel < this->m_TotalLabelCount + 1; ++inLabel) | ||
| for (size_t inLabel = 0; inLabel < this->m_TotalLabelCount + 1; ++inLabel) |
There was a problem hiding this comment.
Use "VotingLabelType" instead of the size_t.
There was a problem hiding this comment.
Left as size_t here, deliberately. size_t is m_TotalLabelCount's declared type, which is the point of commit 1 (BUG: Type MultiLabelSTAPLE label loops by count, not pixel): the counter should follow the bound, not the pixel. VotingLabelType is pixel-derived, so a counter typed on it has the same shape as B84 — it is safe in practice (unsigned short reaches 257 for uint8 input, unsigned int reaches 65537 for uint16) and only wraps for uint32 input, which needs a confusion matrix of ~10^20 bytes and is unreachable. But it reads as a counterexample to the commit sitting directly beneath it.
VotingLabelType's job is narrower: carry the voting sentinel in a type where it cannot collide with a real label. Scoping it to that keeps the two concerns separate.
Happy to switch if you would rather have the consistency within the function — say the word and it is a one-line change.
Loop counters bounded by m_TotalLabelCount were typed as InputPixelType/OutputPixelType. A saturated unsigned char label space sets m_TotalLabelCount to 256, so an 8-bit counter wraps 255->0 and the loop never terminates. Counters now use size_t, matching m_TotalLabelCount, as itkLabelVotingImageFilter already does. Addresses item B84 of InsightSoftwareConsortium#6575.
The confusion matrices are seeded from an internal LabelVotingImageFilter whose automatic undecided sentinel is m_TotalLabelCount cast to its output pixel type. Typed on TOutputImage, a saturated label space wraps that cast onto real label 0, so tie pixels seed label-0 consensus and the skip test against m_TotalLabelCount never excludes them. Voting into a type one step wider than the input keeps the sentinel representable and distinct from every real label. Addresses item B85 of InsightSoftwareConsortium#6575.
1ed9e6f to
ba88d10
Compare
MultiLabelSTAPLEImageFilterhangs forever on a saturateduint8label space, and its voting-seeded confusion matrices count ties as label-0 consensus. Addresses B84 and B85 of #6575. Follows #6579 and #6597 in the same filter.B84 — infinite loop (reproduced, not source analysis)
18 loop counters in
itkMultiLabelSTAPLEImageFilter.hxxare bounded bym_TotalLabelCount(asize_t) but typed by the pixel type:With
InputPixelType == unsigned charand all 256 label values in use,m_TotalLabelCount == 256, the counter wraps255 -> 0, and the loop never terminates. The first one reached is inInitializeConfusionMatrixArrayFromVoting(), beforeInitializePriorProbabilities()or the E-M loop. This was hit as a live hang (process pinned at 100% CPU) while testing #6597 — it is a path #6597's new exception does not cover, because an explicitSetLabelForUndecidedPixels()is legal there.All 18 counters are retyped to
size_t, matching whatm_TotalLabelCountactually is.itkLabelVotingImageFilter.hxx:125already does this, which is why the sibling filter never had the bug. Anchorrg 'for \((InputPixelType|OutputPixelType|PixelType)\s+\w+\s*='overModules/Segmentation/LabelVoting/include/: 18 hits, all in this one file, all fixed; no sibling file has any.B85 — voting-seed sentinel collides with a real label
InitializeConfusionMatrixArrayFromVoting()seeds the confusion matrices from an internalLabelVotingImageFilter, whose automatic undecided sentinel ism_TotalLabelCountcast to its output pixel type. Instantiated onTOutputImage, a saturated label space wraps that cast onto real label 0. Tie pixels then seed label-0 consensus, and the skip testnever excludes them, because it only means "is this the undecided sentinel" while the sentinel happens to equal the auto-computed default.
The seed pass now votes into
NumericTraits<InputPixelType>::AccumulateType— one step wider than the input — so the sentinel is always representable and distinct from every real label, and the skip test compares against the sentinel the voting filter actually used.LabelVotingImageFilterconstrains its input to an unsigned integer (InputUnsignedIntCheck), soAccumulateTypeis always a wider unsigned integer here.Note that the caller's
SetLabelForUndecidedPixels()is deliberately not forwarded to the internal voting filter. The seed pass needs a sentinel disjoint from every real label, not the caller's output label; forwarding it reintroduces the collision whenever the caller picks a label that is also in use (e.g.SetLabelForUndecidedPixels(0)).The intermediate voting image is
AccumulateTyperather than a fixed-widthSizeValueType, keeping it at 2 bytes/voxel for the commonuint8case instead of 8.Local validation
Four tests added to the existing
itkMultiLabelSTAPLEImageFilterGTest.cxx:SaturatedUnsignedCharLabelCountDoesNotHang— runs the filter on a worker thread behind a watchdog, aborting rather than leaking a spinning thread into the rest of the driver. Fail-before: watchdog expires, still spinning.SaturatedLabelsWithoutExplicitUndecidedLabelThrows— a saturated space with no representable undecided label must refuse rather than wrap the default onto real label 0.SaturatedConsensusOnLabelZeroSeedsMatrixWhenUndecidedLabelIsZero— label-0 consensus must still seed the matrices when the caller maps undecided pixels to 0.ConsensusOnLabelZeroSeedsMatrixWhenUndecidedLabelIsZero— same invariant unsaturated; only genuine ties are excluded.ctest -R "LabelVoting|MultiLabelSTAPLE": 14/14 pass, including #6579'sVotingUndecidedPixelsDoNotCorruptSeededConfusionMatrixand the pre-existingitkMultiLabelSTAPLEImageFilterTest, unchanged. Both test drivers build without warnings.Negative control, to confirm the new tests have teeth rather than merely passing: forcing
VotingLabelType = InputPixelTypemakesSaturatedConsensusOnLabelZeroSeedsMatrixWhenUndecidedLabelIsZerofail andSaturatedUnsignedCharLabelCountDoesNotHangabort on its watchdog.AI assistance