fix: region selection and per-sample statistics - #59
Open
Hendrik-code wants to merge 1 commit into
Open
Conversation
Three ways a transform's output depended on something it should not. * _apply_region_mode reduced the mask's class axis with `argmax(mask, dim) > 0`. That reads "is anything labelled here" only if class 0 is background, and it is not. For an ordinary single-channel [B, 1, D, H, W] mask -- what nnU-Net hands over -- argmax over a length-1 axis is always 0, so the result was all-False: in_seg applied the transform nowhere and out_seg applied it everywhere. Both knobs did nothing and the opposite of nothing. For a one-hot mask it dropped the first foreground class, since collapse_onehot_to_index encodes channel c as label c + 1. Now `amax > 0`, extracted as _foreground, which asks the question that was meant and matches what collapse_onehot_to_index already does. * The elementwise function transforms normalised with a bare x.min()/x.max(), reducing over the whole [N, ...] slab. A volume's augmentation therefore depended on which other volumes happened to share its batch: the same volume in two different batches came out different. Now per-sample amin/amax over dim=1.., as every other transform in the file already does. * aug_redistribute_seg built its structuring element with a hardcoded rank of 3, so a 2-D image raised "structure rank must match input rank". Taken from the data instead. Also RandomHistogramEqualizationGPU takes input[:, c].clone() rather than the view it used to write back through. This one is not observable today and its tests say so: the loop assigns into channel_data[b], so by the time the non-finite guard continue'd, everything it meant to withhold was already in the batch and the closing input[:, c] = channel_data was a no-op. Reaching the guard needs the equalisation to produce a non-finite value from finite input, which nothing constructed here manages -- a caller-supplied NaN raises out of torch.histc first. The clone makes the guard mean what it says. unit_tests/test_region_and_stats.py: six checks fail against the previous implementation, verified by reverting the call sites alone. Models trained before this change saw the old behaviour and will not reproduce against it -- in_seg/out_seg configs most of all, since those knobs were inert. No config key, parameter or default changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Stacked on #58 (which is stacked on #57) — this PR's diff is only its own commit.
Three ways a transform's output depended on something it should not.
in_seg/out_segdid nothing, and the opposite of nothing_apply_region_modereduced the mask's class axis withtorch.argmax(mask, dim) > 0. That reads "is anything labelled here" only if class 0 is background — and it is not.[B, 1, D, H, W]mask, which is what nnU-Net hands over,argmaxover a length-1 axis is always0, so the result was all-False.in_segapplied the transform nowhere;out_segapplied it everywhere.collapse_onehot_to_indexingpu/fromSeg.pyencodes channelcas labelc + 1with background implicit.Now
amax > 0, extracted as_foreground, which asks the question that was meant and matches whatcollapse_onehot_to_indexalready does withseg_raw.any(dim=1).The function transforms coupled every volume in a batch
_RandomFunctionBaseGPUnormalised with a barex.min()/x.max(), reducing over the whole[N, ...]slab. A volume's augmentation therefore depended on which other volumes happened to share its batch — the same volume in two different batches came out different. Now per-sampleamin/amaxoverdim=1.., as every other transform in the file already does.aug_redistribute_segcould not handle a 2-D imageThe structuring element was built with a hardcoded rank of 3, so scipy raised
structure rank must match input rank. Taken from the data instead.Also, one hazard rather than a fix
RandomHistogramEqualizationGPUnow takesinput[:, c].clone()rather than the view it wrote back through. This is not observable today and its tests say so. The loop assigns intochannel_data[b], so by the time the non-finite guardcontinued, everything it meant to withhold was already in the batch, and the closinginput[:, c] = channel_datawas a no-op. Reaching the guard needs the equalisation to produce a non-finite value from finite input, which nothing I could construct manages — a caller-supplied NaN raises out oftorch.histcfirst. The clone makes the guard mean what it says. Same category as theresamplechange in #57.Testing
unit_tests/test_region_and_stats.py, 19 tests. Six fail against the parent commit. Because the new_foregroundhelper would break the import, I verified by reverting only the behavioural call sites and re-running:test_the_old_argmax_reduction_really_did_collapse_itpins the old expression as a control. The end-to-end test drives a Scharr transform throughAugmentationSequentialCustomwithin_seg=1.0— the path that actually routes the mask intoparams["seg"]— and asserts both that nothing outside the mask changed and that something inside it did, which is the half the argmax bug broke.Full suite: 57 passed, 167 subtests.
ruffandmypyclean.Compatibility
Models trained before this change saw the old behaviour and will not reproduce against it — configs using
in_seg/out_segmost of all, since those knobs were inert. No config key, parameter or default changed.