Skip to content

Commit edcd90f

Browse files
authored
Use global statistics in Pad (and CropOrPad) (#1336)
1 parent 6ea3bf7 commit edcd90f

2 files changed

Lines changed: 77 additions & 15 deletions

File tree

  • src/torchio/transforms/preprocessing/spatial
  • tests/transforms/preprocessing

src/torchio/transforms/preprocessing/spatial/pad.py

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
from numbers import Number
33
from typing import Union
44

5-
import nibabel as nib
65
import numpy as np
76
import torch
7+
from nibabel.affines import apply_affine
88

9-
from ....data.image import LabelMap
9+
from ....data.image import Image
1010
from ....data.subject import Subject
1111
from .bounds_transform import BoundsTransform
1212
from .bounds_transform import TypeBounds
@@ -31,7 +31,10 @@ class Pad(BoundsTransform):
3131
:math:`w_{ini} = w_{fin} = h_{ini} = h_{fin} =
3232
d_{ini} = d_{fin} = n`.
3333
padding_mode: See possible modes in `NumPy docs`_. If it is a number,
34-
the mode will be set to ``'constant'``.
34+
the mode will be set to ``'constant'``. If it is ``'mean'``,
35+
``'maximum'``, ``'median'`` or ``'minimum'``, the statistic will be
36+
computed from the whole volume, unlike in NumPy, which computes it
37+
along the padded axis.
3538
**kwargs: See :class:`~torchio.transforms.Transform` for additional
3639
keyword arguments.
3740
@@ -78,26 +81,49 @@ def check_padding_mode(cls, padding_mode):
7881
)
7982
raise KeyError(message)
8083

84+
def _check_truncation(self, image: Image, mode: Union[str, float]) -> None:
85+
if mode not in ('mean', 'median'):
86+
return
87+
if torch.is_floating_point(image.data):
88+
return
89+
message = (
90+
f'The constant value computed for padding mode "{mode}" might '
91+
' be truncated in the output, as the input image is not'
92+
'floating point. Consider converting the image to a floating'
93+
' point type before applying this transform.'
94+
)
95+
warnings.warn(message, RuntimeWarning, stacklevel=2)
96+
8197
def apply_transform(self, subject: Subject) -> Subject:
8298
assert self.bounds_parameters is not None
8399
low = self.bounds_parameters[::2]
84100
for image in self.get_images(subject):
85-
if isinstance(image, LabelMap) and self.padding_mode == 'mean':
86-
message = (
87-
'Padding mode "mean" might create non-integer values in label maps'
88-
)
89-
warnings.warn(message, RuntimeWarning, stacklevel=2)
90-
new_origin = nib.affines.apply_affine(image.affine, -np.array(low))
101+
self._check_truncation(image, self.padding_mode)
102+
new_origin = apply_affine(image.affine, -np.array(low))
91103
new_affine = image.affine.copy()
92104
new_affine[:3, 3] = new_origin
93-
kwargs: dict[str, Union[str, float]]
105+
106+
mode: str | float = 'constant'
107+
constant: torch.Tensor | float | None = None
108+
kwargs: dict[str, str | float | torch.Tensor] = {}
94109
if isinstance(self.padding_mode, Number):
95-
kwargs = {
96-
'mode': 'constant',
97-
'constant_values': self.padding_mode,
98-
}
110+
constant = self.padding_mode # type: ignore[assignment]
111+
elif self.padding_mode == 'maximum':
112+
constant = image.data.max()
113+
elif self.padding_mode == 'mean':
114+
constant = image.data.float().mean()
115+
elif self.padding_mode == 'median':
116+
constant = torch.quantile(image.data.float(), 0.5)
117+
elif self.padding_mode == 'minimum':
118+
constant = image.data.min()
99119
else:
100-
kwargs = {'mode': self.padding_mode}
120+
constant = None
121+
mode = self.padding_mode
122+
123+
if constant is not None:
124+
kwargs['constant_values'] = constant
125+
kwargs['mode'] = mode
126+
101127
pad_params = self.bounds_parameters
102128
paddings = (0, 0), pad_params[:2], pad_params[2:4], pad_params[4:]
103129
padded = np.pad(image.data, paddings, **kwargs) # type: ignore[call-overload]

tests/transforms/preprocessing/test_pad.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import pytest
12
import SimpleITK as sitk
23
import torch
34

@@ -40,3 +41,38 @@ def padding_func():
4041
def test_padding_mean_label_map(self):
4142
with self.assertWarns(RuntimeWarning):
4243
tio.Pad(1, padding_mode='mean')(self.sample_subject.label)
44+
45+
def test_padding_modes_global(self):
46+
x = torch.ones(1, 1, 2, 2, dtype=torch.int)
47+
x[..., 0, 0] = 0
48+
# The image should look like this:
49+
# 0 1
50+
# 1 1
51+
52+
add_bottom_row = 0, 0, 0, 1, 0, 0
53+
with_zeros = tio.Pad(add_bottom_row)(x)
54+
assert with_zeros[0, 0, 2].tolist() == [0, 0]
55+
56+
with_minimum = tio.Pad(add_bottom_row, padding_mode='minimum')(x)
57+
assert with_minimum[0, 0, 2].tolist() == [0, 0]
58+
59+
with_maximum = tio.Pad(add_bottom_row, padding_mode='maximum')(x)
60+
assert with_maximum[0, 0, 2].tolist() == [1, 1]
61+
62+
with_median = tio.Pad(add_bottom_row, padding_mode='median')(x)
63+
assert with_median[0, 0, 2].tolist() == [1, 1]
64+
65+
# This is a special case: as we instantiated the tensor with integers,
66+
# the mean (3/4) will be trucated to 0.
67+
with_mean = tio.Pad(add_bottom_row, padding_mode='mean')(x)
68+
assert with_mean[0, 0, 2].tolist() == [0, 0]
69+
# So let's test with floats too
70+
x = x.float()
71+
with_mean = tio.Pad(add_bottom_row, padding_mode='mean')(x)
72+
assert with_mean[0, 0, 2].tolist() == [0.75, 0.75]
73+
74+
def test_truncation_warning(self):
75+
x = torch.ones(1, 1, 2, 2, dtype=torch.int)
76+
pad = tio.Pad(1, padding_mode='mean')
77+
with pytest.warns(RuntimeWarning):
78+
pad(x)

0 commit comments

Comments
 (0)