Skip to content

Add partial-volume-aware label resampling mode - #1477

Merged
fepegar merged 8 commits into
mainfrom
fepegar/label-interpolation
Jun 21, 2026
Merged

Add partial-volume-aware label resampling mode#1477
fepegar merged 8 commits into
mainfrom
fepegar/label-interpolation

Conversation

@fepegar

@fepegar fepegar commented Jun 17, 2026

Copy link
Copy Markdown
Member

[Partially generated by a coding agent]


image image image

Summary

Adds an opt-in label_interpolation="label" mode to the spatial transforms (Spatial, Resample, Affine, ElasticDeformation) for proper, partial-volume-aware resampling of label maps.

Nearest-neighbor interpolation (the default) produces staircase artifacts, biases label volumes, and erodes small structures under strong downsampling. The new mode instead:

  1. one-hot encodes the label map using the unique label values present (robust to non-contiguous labels such as {0, 2, 5});
  2. optionally Gaussian-smooths each channel before downsampling when antialias=True (reusing the existing Cardoso et al., MICCAI 2015 sigma);
  3. resamples every channel with linear interpolation; and
  4. takes the per-voxel argmax, remapping back to the original label values.

Out-of-bounds voxels use default_pad_label. Multi-channel (already one-hot) label inputs are resampled linearly without re-encoding.

Behavior / compatibility

  • Defaults unchanged: label_interpolation stays "nearest" everywhere; this is purely opt-in.
  • image_interpolation="label" raises a clear ValueError (the mode is label-only).
  • The mode never produces label values absent from the input.

Example

import torchio as tio

transform = tio.Resample(2, label_interpolation="label", antialias=True)

Tests

New TestLabelInterpolation in tests/test_spatial.py: no invalid labels (up/downsampling), non-contiguous round-trip, default_pad_label fill, image_interpolation="label" raises, antialias path, multi-channel input, and a round-trip Dice check showing "label" beats "nearest".

uv run pytest tests/test_spatial.py tests/test_one_hot.py -q -> 99 passed. ruff and ty clean.

Notes

  • For extreme downsampling of highly convoluted multi-tissue maps, nearest can occasionally match/beat "label" on Dice because argmax correctly drops sub-voxel-thin structures; "label" wins clearly at moderate-to-strong factors and on small structures.
  • A separate, pre-existing bug was observed during testing: copy.deepcopy of a CropOrPad output reverts the image to its original (pre-crop) shape, which makes Resample compute the wrong output grid (since forward deepcopies inputs). Not addressed here; can file separately.

Add a new opt-in `label_interpolation="label"` mode to the spatial
transforms (Spatial/Resample/Affine/ElasticDeformation).

For label maps, the mode one-hot encodes by unique label values
(robust to non-contiguous labels), optionally antialiases the
one-hot channels with the existing Cardoso et al. sigma when
downsampling, resamples each channel linearly, takes the per-voxel
argmax, and remaps back to the original label values. Out-of-bounds
voxels use `default_pad_label`. This avoids the staircase artifacts
and biased label volumes of nearest-neighbor interpolation.

Defaults are unchanged: `label_interpolation` stays "nearest"
everywhere and users opt in. `image_interpolation="label"` raises.
Copilot AI review requested due to automatic review settings June 17, 2026 23:19
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

📖 Docs Preview

Preview of the documentation for this PR:

🔗 https://smokeshow.helpmanual.io/3g5j630h1v1y5e513r4t/

Built from c112917

Copilot AI left a comment

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.

Pull request overview

Adds an opt-in label_interpolation="label" mode to TorchIO’s unified spatial transform pipeline to resample discrete label maps in a partial-volume-aware way (one-hot → linear resample → argmax), with optional antialiasing, plus tests covering core behaviors.

Changes:

  • Extend interpolation parsing/support to include a new "label" mode for LabelMap resampling.
  • Implement _resample_label_partial_volume() and route label batches through it when requested.
  • Add TestLabelInterpolation coverage for validity, padding, antialias, and multi-channel label behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/torchio/transforms/spatial/spatial.py Adds "label" interpolation option, validation, and the partial-volume label resampling helper integrated into the spatial batch application path.
tests/test_spatial.py Adds tests validating parsing/validation and functional behavior of the new label interpolation mode.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/torchio/transforms/spatial/spatial.py Outdated
Comment thread src/torchio/transforms/spatial/spatial.py Outdated
Comment thread src/torchio/transforms/spatial/spatial.py Outdated
Comment thread src/torchio/transforms/spatial/spatial.py Outdated
- Clarify in docstrings that the "label" mode can introduce
  `default_pad_label` for out-of-bounds voxels (single-channel path).
- Document that multi-channel (C>1) inputs are resampled linearly,
  fill out-of-bounds with 0 (not `default_pad_label`), and return
  partial-volume floats.
- Preserve floating-point output for non-floating multi-channel inputs
  so integer one-hot encodings are not truncated back to 0/1.
- Add a test covering partial-volume preservation for integer one-hot
  multi-channel inputs.

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread src/torchio/transforms/spatial/spatial.py Outdated
Comment thread tests/test_spatial.py Outdated
- Replace the unresolved mkdocstrings shortcut reference to
  `_antialias_sigmas` with plain inline code.
- Relax the round-trip Dice test to assert "label" is not worse than
  "nearest" to avoid flakiness from grid alignment / library versions.

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread src/torchio/transforms/spatial/spatial.py Outdated
Introduce `TypeImageInterpolation` (no "label") and
`TypeLabelInterpolation` (adds "label"), and annotate
`image_interpolation` parameters/attributes with the narrower type so
that `image_interpolation="label"` is rejected by static type checkers,
not only at runtime. `TypeInterpolation` remains as a broad alias for
internal helpers.

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment thread src/torchio/transforms/spatial/spatial.py Outdated
Comment thread src/torchio/transforms/spatial/spatial.py Outdated
Comment thread src/torchio/transforms/spatial/spatial.py Outdated
- Clarify the comment on the label-mode out-of-bounds test: the
  `sum > 0.5` threshold matches the `mask > 0.5` fill convention used for
  intensity images, treating voxels sampled mostly from outside as
  out-of-bounds (not only all-zero ones).
- Document that image/label interpolation also accept higher-order
  B-spline modes ("quadratic".."seventh") and integer orders 0-7.
- Note that the "label" mode only guarantees no invented labels for
  single-channel inputs; multi-channel maps yield partial volumes.

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/torchio/transforms/spatial/spatial.py:89

  • _parse_interpolation() and the docstring state that integer interpolation orders 07 are supported, and tests already pass image_interpolation=3. However, the public type aliases/signatures use TypeImageInterpolation/TypeLabelInterpolation that only accept string literals, so static type checkers will flag valid usage (e.g. tio.Affine(image_interpolation=3)). Consider extending the type aliases to include the allowed integer orders to keep the public typing consistent with runtime behavior and docs.
TypeImageInterpolation: TypeAlias = Literal[
    "nearest",
    "linear",
    "quadratic",
    "cubic",

`_parse_interpolation` already accepts integer B-spline orders 0-7 at
runtime (and the docs/tests use them), but the public parameter
annotations were string-only, so `image_interpolation=3` failed static
type checking. Widen the constructor annotations to
`TypeImageInterpolation | int` / `TypeLabelInterpolation | int` to match
the documented and tested behavior.

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@romainVala

Copy link
Copy Markdown
Contributor

Hi there

This is a cool add on !
I use a 'home made' version of this interpolation, which is indeed very useful especially when you want to upsample a label file

Just a small comment about step 3
3 .resamples every channel with linear interpolation;

It would be nice to add an extra parametre (let call it one_hot_label_interpolation=linear by default ) but the user could then also use other interpolation (bspline, sinc ect ...)

Cheers

Romain

(may be a note to add in the documentation: this can be extremely memory demanding as the number of labels augments)
I work now with more thant 300 labels, and this is a typical use case where applying this transform patch by patch with a gridSampler would help to adjust this process to available memory

fepegar added a commit that referenced this pull request Jun 19, 2026
Let users choose the interpolation used to resample the one-hot channels
of the partial-volume "label" mode, instead of hard-coding linear. It
accepts the same modes as image interpolation (nearest, linear, B-spline
orders 2-7 / integer orders 0-7) but not "label". Defaults to "linear",
so behavior is unchanged. Higher-order modes give smoother label
boundaries, which is useful when upsampling.

Also document that the "label" mode's memory scales with the number of
labels, and point to GridSampler + PatchAggregator for patch-based
processing of many-label maps.

Threaded through Spatial/Resample/Affine/ElasticDeformation, the inverse,
history replay (with a "linear" fallback for old params), and the
_resample_label_partial_volume helper. Adds tests covering the default,
higher-order difference, integer orders, and rejection of "label".

Addresses feedback from @romainVala on #1477.
Let users choose the interpolation used to resample the one-hot channels
of the partial-volume "label" mode, instead of hard-coding linear. It
accepts the same modes as image interpolation (nearest, linear, B-spline
orders 2-7 / integer orders 0-7) but not "label". Defaults to "linear",
so behavior is unchanged. Higher-order modes give smoother label
boundaries, which is useful when upsampling.

Also document that the "label" mode's memory scales with the number of
labels, and point to GridSampler + PatchAggregator for patch-based
processing of many-label maps.

Threaded through Spatial/Resample/Affine/ElasticDeformation, the inverse,
history replay (with a "linear" fallback for old params), and the
_resample_label_partial_volume helper. Adds tests covering the default,
higher-order difference, integer orders, and rejection of "label".

Addresses reviewer feedback on PR #1477.
@fepegar
fepegar force-pushed the fepegar/label-interpolation branch from 38f8fd7 to e7f8e0f Compare June 19, 2026 09:44
@fepegar

fepegar commented Jun 19, 2026

Copy link
Copy Markdown
Member Author

Thanks, @romainVala. Your suggestions have been addressed in e7f8e0f.

…lation

# Conflicts:
#	src/torchio/transforms/spatial/spatial.py
@fepegar
fepegar merged commit 5904e99 into main Jun 21, 2026
30 checks passed
@fepegar
fepegar deleted the fepegar/label-interpolation branch June 21, 2026 21:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants