Passed
Push — master ( 7b848f...cac223 )
by Fernando
02:39
created

torchio.data.image.Image.__init__()   C

Complexity

Conditions 9

Size

Total Lines 26
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 24
nop 6
dl 0
loc 26
rs 6.6666
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
34
    def __init__(
35
            self,
36
            path: Optional[TypePath] = None,
37
            type: str = INTENSITY,
38
            tensor: Optional[torch.Tensor] = None,
39
            affine: Optional[torch.Tensor] = None,
40
            **kwargs: Dict[str, Any],
41
            ):
42
        if path is None and tensor is None:
43
            raise ValueError('A value for path or tensor must be given')
44
        if path is not None:
45
            if tensor is not None or affine is not None:
46
                message = 'If a path is given, tensor and affine must be None'
47
                raise ValueError(message)
48
        self.tensor = self.parse_tensor(tensor)
49
        self.affine = self.parse_affine(affine)
50
        if self.affine is None:
51
            self.affine = np.eye(4)
52
        for key in (DATA, AFFINE, TYPE, PATH, STEM):
53
            if key in kwargs:
54
                raise ValueError(f'Key {key} is reserved. Use a different one')
55
56
        super().__init__(**kwargs)
57
        self.path = self._parse_path(path)
58
        self.type = type
59
        self.is_sample = False  # set to True by ImagesDataset
60
61
    @staticmethod
62
    def _parse_path(path: TypePath) -> Path:
63
        if path is None:
64
            return
65
        try:
66
            path = Path(path).expanduser()
67
        except TypeError:
68
            message = f'Conversion to path not possible for variable: {path}'
69
            raise TypeError(message)
70
        if not (path.is_file() or path.is_dir()):  # might be a dir with DICOM
71
            raise FileNotFoundError(f'File not found: {path}')
72
        return path
73
74
    @staticmethod
75
    def parse_tensor(tensor: torch.Tensor) -> torch.Tensor:
76
        if tensor is None:
77
            return None
78
        num_dimensions = tensor.dim()
79
        if num_dimensions != 3:
80
            message = (
81
                'The input tensor must have 3 dimensions (D, H, W),'
82
                f' but has {num_dimensions}: {tensor.shape}'
83
            )
84
            raise RuntimeError(message)
85
        tensor = tensor.unsqueeze(0)  # add channels dimension
86
        return tensor
87
88
    @staticmethod
89
    def parse_affine(affine: np.ndarray) -> np.ndarray:
90
        if affine is None:
91
            return np.eye(4)
92
        if not isinstance(affine, np.ndarray):
93
            raise TypeError(f'Affine must be a NumPy array, not {type(affine)}')
94
        if affine.shape != (4, 4):
95
            raise ValueError(f'Affine shape must be (4, 4), not {affine.shape}')
96
        return affine
97
98
    def load(self, check_nans: bool = True) -> Tuple[torch.Tensor, np.ndarray]:
99
        r"""Load the image from disk.
100
101
        The file is expected to be monomodal/grayscale and 2D or 3D.
102
        A channels dimension is added to the tensor.
103
104
        Args:
105
            check_nans: If ``True``, issues a warning if NaNs are found
106
                in the image
107
108
        Returns:
109
            Tuple containing a 4D data tensor of size
110
            :math:`(1, D_{in}, H_{in}, W_{in})`
111
            and a 2D 4x4 affine matrix
112
        """
113
        if self.path is None:
114
            return self.tensor, self.affine
115
        tensor, affine = read_image(self.path)
116
        # https://github.com/pytorch/pytorch/issues/9410#issuecomment-404968513
117
        tensor = tensor[(None,) * (3 - tensor.ndim)]  # force to be 3D
118
        tensor = tensor.unsqueeze(0)  # add channels dimension
119
        if check_nans and torch.isnan(tensor).any():
120
            warnings.warn(f'NaNs found in file "{self.path}"')
121
        return tensor, affine
122