Passed
Push — master ( e39d51...b12d2b )
by Fernando
01:18
created

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

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nop 1
dl 0
loc 3
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
8
from ..torchio import TypePath, DATA, TYPE, AFFINE, PATH, STEM, INTENSITY
9
from .io import read_image
10
11
12
class Image(dict):
13
    r"""Class to store information about an image.
14
15
    Args:
16
        path: Path to a file that can be read by
17
            :mod:`SimpleITK` or :mod:`nibabel` or to a directory containing
18
            DICOM files.
19
        type: Type of image, such as :attr:`torchio.INTENSITY` or
20
            :attr:`torchio.LABEL`. This will be used by the transforms to
21
            decide whether to apply an operation, or which interpolation to use
22
            when resampling.
23
        tensor: If :attr:`path` is not given, :attr:`tensor` must be a 4D
24
            :py:class:`torch.Tensor` with dimensions :math:`(C, D, H, W)`,
25
            where :math:`C` is the number of channels and :math:`D, H, W`
26
            are the spatial dimensions.
27
        affine: If :attr:`path` is not given, :attr:`affine` must be a
28
            :math:`4 \times 4` NumPy array. If ``None``, :attr:`affine` is an
29
            identity matrix.
30
        **kwargs: Items that will be added to image dictionary within the
31
            subject sample.
32
    """
33
    def __init__(
34
            self,
35
            path: Optional[TypePath] = None,
36
            type: str = INTENSITY,
37
            tensor: Optional[torch.Tensor] = None,
38
            affine: Optional[torch.Tensor] = None,
39
            **kwargs: Dict[str, Any],
40
            ):
41
        if path is None and tensor is None:
42
            raise ValueError('A value for path or tensor must be given')
43
        if path is not None:
44
            if tensor is not None or affine is not None:
45
                message = 'If a path is given, tensor and affine must be None'
46
                raise ValueError(message)
47
        self.tensor = self.parse_tensor(tensor)
48
        self.affine = self.parse_affine(affine)
49
        if self.affine is None:
50
            self.affine = np.eye(4)
51
        for key in (DATA, AFFINE, TYPE, PATH, STEM):
52
            if key in kwargs:
53
                raise ValueError(f'Key {key} is reserved. Use a different one')
54
55
        super().__init__(**kwargs)
56
        self.path = self._parse_path(path)
57
        self.type = type
58
        self.is_sample = False  # set to True by ImagesDataset
59
60
    @property
61
    def shape(self):
62
        return self[DATA].shape
63
64
    @property
65
    def spatial_shape(self):
66
        return self.shape[1:]
67
68
    def is_2d(self):
69
        return self.shape[-3] == 1
70
71
    def numpy(self):
72
        return self[DATA].numpy()
73
74
    @staticmethod
75
    def _parse_path(path: TypePath) -> Path:
76
        if path is None:
77
            return None
78
        try:
79
            path = Path(path).expanduser()
80
        except TypeError:
81
            message = f'Conversion to path not possible for variable: {path}'
82
            raise TypeError(message)
83
        if not (path.is_file() or path.is_dir()):  # might be a dir with DICOM
84
            raise FileNotFoundError(f'File not found: {path}')
85
        return path
86
87
    @staticmethod
88
    def parse_tensor(tensor: torch.Tensor) -> torch.Tensor:
89
        if tensor is None:
90
            return None
91
        num_dimensions = tensor.dim()
92
        if num_dimensions != 3:
93
            message = (
94
                'The input tensor must have 3 dimensions (D, H, W),'
95
                f' but has {num_dimensions}: {tensor.shape}'
96
            )
97
            raise RuntimeError(message)
98
        tensor = tensor.unsqueeze(0)  # add channels dimension
99
        return tensor
100
101
    @staticmethod
102
    def parse_affine(affine: np.ndarray) -> np.ndarray:
103
        if affine is None:
104
            return np.eye(4)
105
        if not isinstance(affine, np.ndarray):
106
            raise TypeError(f'Affine must be a NumPy array, not {type(affine)}')
107
        if affine.shape != (4, 4):
108
            raise ValueError(f'Affine shape must be (4, 4), not {affine.shape}')
109
        return affine
110
111
    def load(self, check_nans: bool = True) -> Tuple[torch.Tensor, np.ndarray]:
112
        r"""Load the image from disk.
113
114
        The file is expected to be monomodal/grayscale and 2D or 3D.
115
        A channels dimension is added to the tensor.
116
117
        Args:
118
            check_nans: If ``True``, issues a warning if NaNs are found
119
                in the image
120
121
        Returns:
122
            Tuple containing a 4D data tensor of size
123
            :math:`(1, D_{in}, H_{in}, W_{in})`
124
            and a 2D 4x4 affine matrix
125
        """
126
        if self.path is None:
127
            return self.tensor, self.affine
128
        tensor, affine = read_image(self.path)
129
        # https://github.com/pytorch/pytorch/issues/9410#issuecomment-404968513
130
        tensor = tensor[(None,) * (3 - tensor.ndim)]  # force to be 3D
131
        tensor = tensor.unsqueeze(0)  # add channels dimension
132
        if check_nans and torch.isnan(tensor).any():
133
            warnings.warn(f'NaNs found in file "{self.path}"')
134
        return tensor, affine
135