BUG: Skip voting-undecided pixels in MultiLabelSTAPLE seeding#6579
Merged
hjmjohnson merged 1 commit intoJul 10, 2026
Merged
Conversation
Contributor
hjmjohnson
marked this pull request as draft
July 10, 2026 11:49
InitializeConfusionMatrixArrayFromVoting() seeds each per-rater confusion matrix from a LabelVotingImageFilter run over the same inputs. Each matrix is allocated (m_TotalLabelCount + 1) rows by m_TotalLabelCount columns. On a voting tie the seed filter writes its undecided label, which equals m_TotalLabelCount -- one past the last valid column. Array2D::operator[] is an unchecked row pointer into a flat row-major buffer, so the increment for such a pixel lands on the next row's column 0 ([in + 1][0]) instead of a real slot, silently corrupting the next observed label's row with phantom counts. The EM loop and both normalizations only ever read rows and columns indexed by real labels [0, m_TotalLabelCount), so a voting-undecided pixel carries no (observed label, estimated true label) evidence. Guard the increment so those pixels are skipped instead of writing past the matrix row. Adds a deterministic regression test with two raters whose votes tie on half the pixels, asserting the seeded confusion matrices and the combined output. Addresses item B10 of InsightSoftwareConsortium#6575.
hjmjohnson
force-pushed
the
bug-multilabel-staple-undecided-seed
branch
from
July 10, 2026 12:08
aeadf48 to
6df2f5f
Compare
hjmjohnson
approved these changes
Jul 10, 2026
hjmjohnson
marked this pull request as ready for review
July 10, 2026 13:11
physwkim
added a commit
to physwkim/sitk-rs
that referenced
this pull request
Jul 12, 2026
++CM[k][in.Get()][out.Get()] with out.Get() == max_label + 1 indexes one past the row and, in the flat row-major Array2D, lands on [in.Get() + 1][0] -- every seeding-vote tie fabricates a count for the next input label deciding label 0. A tie carries no evidence about any rater's confusion between two real labels, so it now contributes no count. Matches upstream fix PR InsightSoftwareConsortium/ITK#6579.
This was referenced Jul 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses item B10 of #6575.
MultiLabelSTAPLEImageFilterseeds each rater's confusion matrix from aLabelVotingImageFilterpass over the same inputs. On voting ties that filter writes its undecided label, which equals the total label count — one past the confusion matrix's last valid column.Array2D::operator[]is an unchecked row pointer into a flat row-major buffer, so++CM[k][in][out]without == m_TotalLabelCountlands onCM[k][in + 1][0]: inside the allocation (the matrix has a spare row), but silently corrupting the NEXT observed label's row with phantom "rater observedin+1, estimated truth 0" counts. Every tie pixel in the seeding vote injects one such phantom count, skewing the row-normalized initial confusion matrices the EM iteration starts from — and the filter's publicGetConfusionMatrix()output.The EM loop and both normalizations only ever index rows and columns by real labels, so an undecided voting pixel has no legitimate slot in the matrix: it carries no (observed label, estimated true label) evidence. The fix skips such pixels in the seeding loop instead of letting the increment run past the row.
The new regression test engineers deterministic ties (two raters, four pixels, two of them split 1–1) and sets
MaximumNumberOfIterations = 0, which leaves the matrices exactly as seeded. Unfixed, both raters' matrices come out with row 1 = [0.5, 0.5] and row 2 = [1, 0] (the leaked counts) and the output labels are wrong at three of four pixels ([0, 2, 0, 0]); fixed, the matrices are the exact row-normalized vote statistics (row 0 = [1, 0], row 1 = [0, 1], row 2 = [0, 0]) and the output is [0, 1, 2, 2] — agreements labeled, ties undecided. The full pass-by-pass trace is in the test's header comment.Verified locally (Linux, GCC 13, Release): the new test fails on unmodified
main(confusion matrix (1,0) = 0.5 expected 0, (2,0) = 1 expected 0; output [0, 2, 0, 0] expected [0, 1, 2, 2]) and passes with the fix; the full ITKLabelVoting suite plus the ITKImageStatistics, ITKReview, ITKDisplacementField, and ITKRegionGrowing suites pass with the fix applied.PR Checklist
AI assistance