Passed
Push — master ( 46cc7d...31b29f )
by Fernando
01:33
created

torchio.data.image.Image.spacing()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
import warnings
2
from pathlib import Path
3
from typing import Any, Dict, Tuple, Optional
4
5
import torch
6
import numpy as np
7
import nibabel as nib
8
import SimpleITK as sitk
9
10
from ..utils import nib_to_sitk, get_rotation_and_spacing_from_affine
11
from ..torchio import (
12
    TypePath,
13
    TypeTripletInt,
14
    TypeTripletFloat,
15
    DATA,
16
    TYPE,
17
    AFFINE,
18
    PATH,
19
    STEM,
20
    INTENSITY,
21
)
22
from .io import read_image
23
24
25
class Image(dict):
26
    r"""Class to store information about an image.
27
28
    Args:
29
        path: Path to a file that can be read by
30
            :mod:`SimpleITK` or :mod:`nibabel` or to a directory containing
31
            DICOM files.
32
        type: Type of image, such as :attr:`torchio.INTENSITY` or
33
            :attr:`torchio.LABEL`. This will be used by the transforms to
34
            decide whether to apply an operation, or which interpolation to use
35
            when resampling.
36
        tensor: If :attr:`path` is not given, :attr:`tensor` must be a 4D
37
            :py:class:`torch.Tensor` with dimensions :math:`(C, D, H, W)`,
38
            where :math:`C` is the number of channels and :math:`D, H, W`
39
            are the spatial dimensions.
40
        affine: If :attr:`path` is not given, :attr:`affine` must be a
41
            :math:`4 \times 4` NumPy array. If ``None``, :attr:`affine` is an
42
            identity matrix.
43
        **kwargs: Items that will be added to image dictionary within the
44
            subject sample.
45
    """
46
    def __init__(
47
            self,
48
            path: Optional[TypePath] = None,
49
            type: str = INTENSITY,
50
            tensor: Optional[torch.Tensor] = None,
51
            affine: Optional[torch.Tensor] = None,
52
            **kwargs: Dict[str, Any],
53
            ):
54
        if path is None and tensor is None:
55
            raise ValueError('A value for path or tensor must be given')
56
        if path is not None:
57
            if tensor is not None or affine is not None:
58
                message = 'If a path is given, tensor and affine must be None'
59
                raise ValueError(message)
60
        self._tensor = self.parse_tensor(tensor)
61
        self._affine = self.parse_affine(affine)
62
        if self._affine is None:
63
            self._affine = np.eye(4)
64
        for key in (DATA, AFFINE, TYPE, PATH, STEM):
65
            if key in kwargs:
66
                raise ValueError(f'Key {key} is reserved. Use a different one')
67
68
        super().__init__(**kwargs)
69
        self.path = self._parse_path(path)
70
        self.type = type
71
        self.is_sample = False  # set to True by ImagesDataset
72
73
    @property
74
    def data(self):
75
        return self[DATA]
76
77
    @property
78
    def affine(self):
79
        return self[AFFINE]
80
81
    @property
82
    def shape(self) -> Tuple[int, int, int, int]:
83
        return self[DATA].shape
84
85
    @property
86
    def spatial_shape(self) -> TypeTripletInt:
87
        return self.shape[1:]
88
89
    @property
90
    def orientation(self):
91
        return nib.aff2axcodes(self[AFFINE])
92
93
    @property
94
    def spacing(self):
95
        _, spacing = get_rotation_and_spacing_from_affine(self.affine)
96
        return tuple(spacing)
97
98
    @staticmethod
99
    def _parse_path(path: TypePath) -> Path:
100
        if path is None:
101
            return None
102
        try:
103
            path = Path(path).expanduser()
104
        except TypeError:
105
            message = f'Conversion to path not possible for variable: {path}'
106
            raise TypeError(message)
107
        if not (path.is_file() or path.is_dir()):  # might be a dir with DICOM
108
            raise FileNotFoundError(f'File not found: {path}')
109
        return path
110
111
    @staticmethod
112
    def parse_tensor(tensor: torch.Tensor) -> torch.Tensor:
113
        if tensor is None:
114
            return None
115
        num_dimensions = tensor.dim()
116
        if num_dimensions != 3:
117
            message = (
118
                'The input tensor must have 3 dimensions (D, H, W),'
119
                f' but has {num_dimensions}: {tensor.shape}'
120
            )
121
            raise RuntimeError(message)
122
        tensor = tensor.unsqueeze(0)  # add channels dimension
123
        return tensor
124
125
    @staticmethod
126
    def parse_affine(affine: np.ndarray) -> np.ndarray:
127
        if affine is None:
128
            return np.eye(4)
129
        if not isinstance(affine, np.ndarray):
130
            raise TypeError(f'Affine must be a NumPy array, not {type(affine)}')
131
        if affine.shape != (4, 4):
132
            raise ValueError(f'Affine shape must be (4, 4), not {affine.shape}')
133
        return affine
134
135
    def load(self, check_nans: bool = True) -> Tuple[torch.Tensor, np.ndarray]:
136
        r"""Load the image from disk.
137
138
        The file is expected to be monomodal/grayscale and 2D or 3D.
139
        A channels dimension is added to the tensor.
140
141
        Args:
142
            check_nans: If ``True``, issues a warning if NaNs are found
143
                in the image
144
145
        Returns:
146
            Tuple containing a 4D data tensor of size
147
            :math:`(1, D_{in}, H_{in}, W_{in})`
148
            and a 2D 4x4 affine matrix
149
        """
150
        if self.path is None:
151
            return self._tensor, self._affine
152
        tensor, affine = read_image(self.path)
153
        # https://github.com/pytorch/pytorch/issues/9410#issuecomment-404968513
154
        tensor = tensor[(None,) * (3 - tensor.ndim)]  # force to be 3D
155
        # Remove next line and uncomment the two following ones once/if this issue
156
        # gets fixed:
157
        # https://github.com/pytorch/pytorch/issues/29010
158
        # See also https://discuss.pytorch.org/t/collating-named-tensors/78650/4
159
        tensor = tensor.unsqueeze(0)  # add channels dimension
160
        # name_dimensions(tensor, affine)
161
        # tensor = tensor.align_to('channels', ...)
162
        if check_nans and torch.isnan(tensor).any():
163
            warnings.warn(f'NaNs found in file "{self.path}"')
164
        return tensor, affine
165
166
    def is_2d(self) -> bool:
167
        return self.shape[-3] == 1
168
169
    def numpy(self) -> np.ndarray:
170
        return self[DATA].numpy()
171
172
    def as_sitk(self) -> sitk.Image:
173
        return nib_to_sitk(self[DATA], self[AFFINE])
174
175
    def get_center(self, lps: bool = False) -> TypeTripletFloat:
176
        """Get image center in RAS (default) or LPS coordinates."""
177
        image = self.as_sitk()
178
        size = np.array(image.GetSize())
179
        center_index = (size - 1) / 2
180
        l, p, s = image.TransformContinuousIndexToPhysicalPoint(center_index)
181
        if lps:
182
            return (l, p, s)
183
        else:
184
            return (-l, -p, s)
185