|
1
|
|
|
from numbers import Number |
|
2
|
|
|
from typing import Union |
|
3
|
|
|
|
|
4
|
|
|
import numpy as np |
|
5
|
|
|
import nibabel as nib |
|
6
|
|
|
import torch |
|
7
|
|
|
|
|
8
|
|
|
from ....data.subject import Subject |
|
9
|
|
|
from .bounds_transform import BoundsTransform, TypeBounds |
|
10
|
|
|
|
|
11
|
|
|
|
|
12
|
|
|
class Pad(BoundsTransform): |
|
13
|
|
|
r"""Pad an image. |
|
14
|
|
|
|
|
15
|
|
|
Args: |
|
16
|
|
|
padding: Tuple |
|
17
|
|
|
:math:`(w_{ini}, w_{fin}, h_{ini}, h_{fin}, d_{ini}, d_{fin})` |
|
18
|
|
|
defining the number of values padded to the edges of each axis. |
|
19
|
|
|
If the initial shape of the image is |
|
20
|
|
|
:math:`W \times H \times D`, the final shape will be |
|
21
|
|
|
:math:`(w_{ini} + W + w_{fin}) \times (h_{ini} + H + h_{fin}) |
|
22
|
|
|
\times (d_{ini} + D + d_{fin})`. |
|
23
|
|
|
If only three values :math:`(w, h, d)` are provided, then |
|
24
|
|
|
:math:`w_{ini} = w_{fin} = w`, |
|
25
|
|
|
:math:`h_{ini} = h_{fin} = h` and |
|
26
|
|
|
:math:`d_{ini} = d_{fin} = d`. |
|
27
|
|
|
If only one value :math:`n` is provided, then |
|
28
|
|
|
:math:`w_{ini} = w_{fin} = h_{ini} = h_{fin} = |
|
29
|
|
|
d_{ini} = d_{fin} = n`. |
|
30
|
|
|
padding_mode: See possible modes in `NumPy docs`_. If it is a number, |
|
31
|
|
|
the mode will be set to ``'constant'``. |
|
32
|
|
|
**kwargs: See :class:`~torchio.transforms.Transform` for additional keyword arguments. |
|
33
|
|
|
|
|
34
|
|
|
.. _NumPy docs: https://numpy.org/doc/stable/reference/generated/numpy.pad.html |
|
35
|
|
|
""" |
|
36
|
|
|
|
|
37
|
|
|
PADDING_MODES = ( |
|
38
|
|
|
'empty', |
|
39
|
|
|
'edge', |
|
40
|
|
|
'wrap', |
|
41
|
|
|
'constant', |
|
42
|
|
|
'linear_ramp', |
|
43
|
|
|
'maximum', |
|
44
|
|
|
'mean', |
|
45
|
|
|
'median', |
|
46
|
|
|
'minimum', |
|
47
|
|
|
'reflect', |
|
48
|
|
|
'symmetric', |
|
49
|
|
|
) |
|
50
|
|
|
|
|
51
|
|
|
def __init__( |
|
52
|
|
|
self, |
|
53
|
|
|
padding: TypeBounds, |
|
54
|
|
|
padding_mode: Union[str, float] = 0, |
|
55
|
|
|
**kwargs |
|
56
|
|
|
): |
|
57
|
|
|
super().__init__(padding, **kwargs) |
|
58
|
|
|
self.padding = padding |
|
59
|
|
|
self.check_padding_mode(padding_mode) |
|
60
|
|
|
self.padding_mode = padding_mode |
|
61
|
|
|
self.args_names = 'padding', 'padding_mode' |
|
62
|
|
|
|
|
63
|
|
|
@classmethod |
|
64
|
|
|
def check_padding_mode(cls, padding_mode): |
|
65
|
|
|
if not (padding_mode in cls.PADDING_MODES or isinstance(padding_mode, Number)): |
|
66
|
|
|
message = ( |
|
67
|
|
|
f'Padding mode "{padding_mode}" not valid. Valid options are' |
|
68
|
|
|
f' {list(cls.PADDING_MODES)} or a number' |
|
69
|
|
|
) |
|
70
|
|
|
raise KeyError(message) |
|
71
|
|
|
|
|
72
|
|
|
def apply_transform(self, subject: Subject) -> Subject: |
|
73
|
|
|
low = self.bounds_parameters[::2] |
|
74
|
|
|
for image in self.get_images(subject): |
|
75
|
|
|
new_origin = nib.affines.apply_affine(image.affine, -np.array(low)) |
|
76
|
|
|
new_affine = image.affine.copy() |
|
77
|
|
|
new_affine[:3, 3] = new_origin |
|
78
|
|
|
if isinstance(self.padding_mode, Number): |
|
79
|
|
|
kwargs = {'mode': 'constant', |
|
80
|
|
|
'constant_values': self.padding_mode} |
|
81
|
|
|
else: |
|
82
|
|
|
kwargs = {'mode': self.padding_mode} |
|
83
|
|
|
pad_params = self.bounds_parameters |
|
84
|
|
|
paddings = (0, 0), pad_params[:2], pad_params[2:4], pad_params[4:] |
|
85
|
|
|
padded = np.pad(image.data, paddings, **kwargs) |
|
86
|
|
|
image.data = torch.from_numpy(padded) |
|
87
|
|
|
image.affine = new_affine |
|
88
|
|
|
return subject |
|
89
|
|
|
|
|
90
|
|
|
def inverse(self): |
|
91
|
|
|
from .crop import Crop |
|
92
|
|
|
return Crop(self.padding) |
|
93
|
|
|
|