Passed
Push — master ( fc47ec...2d88b3 )
by Fernando
01:32
created

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

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
import warnings
2
from pathlib import Path
3
from collections.abc import Iterable
4
from typing import Any, Dict, Tuple, Optional, Union, Sequence, List, Callable
5
6
import torch
7
import humanize
8
import numpy as np
9
import nibabel as nib
10
import SimpleITK as sitk
11
from deprecated import deprecated
12
13
from ..utils import get_stem
14
from ..typing import TypeData, TypePath, TypeTripletInt, TypeTripletFloat
15
from ..constants import DATA, TYPE, AFFINE, PATH, STEM, INTENSITY, LABEL
16
from .io import (
17
    ensure_4d,
18
    read_image,
19
    write_image,
20
    nib_to_sitk,
21
    sitk_to_nib,
22
    check_uint_to_int,
23
    get_rotation_and_spacing_from_affine,
24
)
25
26
27
PROTECTED_KEYS = DATA, AFFINE, TYPE, PATH, STEM
28
TypeBound = Tuple[float, float]
29
TypeBounds = Tuple[TypeBound, TypeBound, TypeBound]
30
31
deprecation_message = (
32
    'Setting the image data with the property setter is deprecated. Use the'
33
    ' set_data() method instead'
34
)
35
36
37
class Image(dict):
38
    r"""TorchIO image.
39
40
    For information about medical image orientation, check out `NiBabel docs`_,
41
    the `3D Slicer wiki`_, `Graham Wideman's website`_, `FSL docs`_ or
42
    `SimpleITK docs`_.
43
44
    Args:
45
        path: Path to a file or sequence of paths to files that can be read by
46
            :mod:`SimpleITK` or :mod:`nibabel`, or to a directory containing
47
            DICOM files. If :attr:`tensor` is given, the data in
48
            :attr:`path` will not be read.
49
            If a sequence of paths is given, data
50
            will be concatenated on the channel dimension so spatial
51
            dimensions must match.
52
        type: Type of image, such as :attr:`torchio.INTENSITY` or
53
            :attr:`torchio.LABEL`. This will be used by the transforms to
54
            decide whether to apply an operation, or which interpolation to use
55
            when resampling. For example, `preprocessing`_ and `augmentation`_
56
            intensity transforms will only be applied to images with type
57
            :attr:`torchio.INTENSITY`. Spatial transforms will be applied to
58
            all types, and nearest neighbor interpolation is always used to
59
            resample images with type :attr:`torchio.LABEL`.
60
            The type :attr:`torchio.SAMPLING_MAP` may be used with instances of
61
            :class:`~torchio.data.sampler.weighted.WeightedSampler`.
62
        tensor: If :attr:`path` is not given, :attr:`tensor` must be a 4D
63
            :class:`torch.Tensor` or NumPy array with dimensions
64
            :math:`(C, W, H, D)`.
65
        affine: :math:`4 \times 4` matrix to convert voxel coordinates to world
66
            coordinates. If ``None``, an identity matrix will be used. See the
67
            `NiBabel docs on coordinates`_ for more information.
68
        check_nans: If ``True``, issues a warning if NaNs are found
69
            in the image. If ``False``, images will not be checked for the
70
            presence of NaNs.
71
        channels_last: If ``True``, the read tensor will be permuted so the
72
            last dimension becomes the first. This is useful, e.g., when
73
            NIfTI images have been saved with the channels dimension being the
74
            fourth instead of the fifth.
75
        reader: Callable object that takes a path and returns a 4D tensor and a
76
            2D, :math:`4 \times 4` affine matrix. This can be used if your data
77
            is saved in a custom format, such as ``.npy`` (see example below).
78
            If the affine matrix is ``None``, an identity matrix will be used.
79
        **kwargs: Items that will be added to the image dictionary, e.g.
80
            acquisition parameters.
81
82
    TorchIO images are `lazy loaders`_, i.e. the data is only loaded from disk
83
    when needed.
84
85
    Example:
86
        >>> import torchio as tio
87
        >>> import numpy as np
88
        >>> image = tio.ScalarImage('t1.nii.gz')  # subclass of Image
89
        >>> image  # not loaded yet
90
        ScalarImage(path: t1.nii.gz; type: intensity)
91
        >>> times_two = 2 * image.data  # data is loaded and cached here
92
        >>> image
93
        ScalarImage(shape: (1, 256, 256, 176); spacing: (1.00, 1.00, 1.00); orientation: PIR+; memory: 44.0 MiB; type: intensity)
94
        >>> image.save('doubled_image.nii.gz')
95
        >>> numpy_reader = lambda path: np.load(path), np.eye(4)
96
        >>> image = tio.ScalarImage('t1.npy', reader=numpy_reader)
97
98
    .. _lazy loaders: https://en.wikipedia.org/wiki/Lazy_loading
99
    .. _preprocessing: https://torchio.readthedocs.io/transforms/preprocessing.html#intensity
100
    .. _augmentation: https://torchio.readthedocs.io/transforms/augmentation.html#intensity
101
    .. _NiBabel docs: https://nipy.org/nibabel/image_orientation.html
102
    .. _NiBabel docs on coordinates: https://nipy.org/nibabel/coordinate_systems.html#the-affine-matrix-as-a-transformation-between-spaces
103
    .. _3D Slicer wiki: https://www.slicer.org/wiki/Coordinate_systems
104
    .. _FSL docs: https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/Orientation%20Explained
105
    .. _SimpleITK docs: https://simpleitk.readthedocs.io/en/master/fundamentalConcepts.html
106
    .. _Graham Wideman's website: http://www.grahamwideman.com/gw/brain/orientation/orientterms.htm
107
    """
108
    def __init__(
109
            self,
110
            path: Union[TypePath, Sequence[TypePath], None] = None,
111
            type: str = None,
112
            tensor: Optional[TypeData] = None,
113
            affine: Optional[TypeData] = None,
114
            check_nans: bool = False,  # removed by ITK by default
115
            channels_last: bool = False,
116
            reader: Callable = read_image,
117
            **kwargs: Dict[str, Any],
118
            ):
119
        self.check_nans = check_nans
120
        self.channels_last = channels_last
121
        self.reader = reader
122
123
        if type is None:
124
            warnings.warn(
125
                'Not specifying the image type is deprecated and will be'
126
                ' mandatory in the future. You can probably use tio.ScalarImage'
127
                ' or tio.LabelMap instead',
128
            )
129
            type = INTENSITY
130
131
        if path is None and tensor is None:
132
            raise ValueError('A value for path or tensor must be given')
133
        self._loaded = False
134
135
        tensor = self._parse_tensor(tensor)
136
        affine = self._parse_affine(affine)
137
        if tensor is not None:
138
            self.set_data(tensor)
139
            self.affine = affine
140
            self._loaded = True
141
        for key in PROTECTED_KEYS:
142
            if key in kwargs:
143
                message = f'Key "{key}" is reserved. Use a different one'
144
                raise ValueError(message)
145
146
        super().__init__(**kwargs)
147
        self.path = self._parse_path(path)
148
149
        self[PATH] = '' if self.path is None else str(self.path)
150
        self[STEM] = '' if self.path is None else get_stem(self.path)
151
        self[TYPE] = type
152
153
    def __repr__(self):
154
        properties = []
155
        if self._loaded:
156
            properties.extend([
157
                f'shape: {self.shape}',
158
                f'spacing: {self.get_spacing_string()}',
159
                f'orientation: {"".join(self.orientation)}+',
160
                f'memory: {humanize.naturalsize(self.memory, binary=True)}',
161
            ])
162
        else:
163
            properties.append(f'path: "{self.path}"')
164
        if self._loaded:
165
            properties.append(f'dtype: {self.data.type()}')
166
        properties = '; '.join(properties)
167
        string = f'{self.__class__.__name__}({properties})'
168
        return string
169
170
    def __getitem__(self, item):
171
        if item in (DATA, AFFINE):
172
            if item not in self:
173
                self.load()
174
        return super().__getitem__(item)
175
176
    def __array__(self):
177
        return self.data.numpy()
178
179
    def __copy__(self):
180
        kwargs = dict(
181
            tensor=self.data,
182
            affine=self.affine,
183
            type=self.type,
184
            path=self.path,
185
        )
186
        for key, value in self.items():
187
            if key in PROTECTED_KEYS: continue
188
            kwargs[key] = value  # should I copy? deepcopy?
189
        return self.__class__(**kwargs)
190
191
    @property
192
    def data(self) -> torch.Tensor:
193
        """Tensor data. Same as :class:`Image.tensor`."""
194
        return self[DATA]
195
196
    @data.setter
197
    @deprecated(version='0.18.16', reason=deprecation_message)
198
    def data(self, tensor: TypeData):
199
        self.set_data(tensor)
200
201
    def set_data(self, tensor: TypeData):
202
        """Store a 4D tensor in the :attr:`data` key and attribute.
203
204
        Args:
205
            tensor: 4D tensor with dimensions :math:`(C, W, H, D)`.
206
        """
207
        self[DATA] = self._parse_tensor(tensor, none_ok=False)
208
209
    @property
210
    def tensor(self) -> torch.Tensor:
211
        """Tensor data. Same as :class:`Image.data`."""
212
        return self.data
213
214
    @property
215
    def affine(self) -> np.ndarray:
216
        """Affine matrix to transform voxel indices into world coordinates."""
217
        return self[AFFINE]
218
219
    @affine.setter
220
    def affine(self, matrix):
221
        self[AFFINE] = self._parse_affine(matrix)
222
223
    @property
224
    def type(self) -> str:
225
        return self[TYPE]
226
227
    @property
228
    def shape(self) -> Tuple[int, int, int, int]:
229
        """Tensor shape as :math:`(C, W, H, D)`."""
230
        return tuple(self.data.shape)
231
232
    @property
233
    def spatial_shape(self) -> TypeTripletInt:
234
        """Tensor spatial shape as :math:`(W, H, D)`."""
235
        return self.shape[1:]
236
237
    def check_is_2d(self) -> None:
238
        if not self.is_2d():
239
            message = f'Image is not 2D. Spatial shape: {self.spatial_shape}'
240
            raise RuntimeError(message)
241
242
    @property
243
    def height(self) -> int:
244
        """Image height, if 2D."""
245
        self.check_is_2d()
246
        return self.spatial_shape[1]
247
248
    @property
249
    def width(self) -> int:
250
        """Image width, if 2D."""
251
        self.check_is_2d()
252
        return self.spatial_shape[0]
253
254
    @property
255
    def orientation(self) -> Tuple[str, str, str]:
256
        """Orientation codes."""
257
        return nib.aff2axcodes(self.affine)
258
259
    @property
260
    def spacing(self) -> Tuple[float, float, float]:
261
        """Voxel spacing in mm."""
262
        _, spacing = get_rotation_and_spacing_from_affine(self.affine)
263
        return tuple(spacing)
264
265
    @property
266
    def itemsize(self):
267
        """Element size of the data type."""
268
        return self.data.element_size()
269
270
    @property
271
    def memory(self) -> float:
272
        """Number of Bytes that the tensor takes in the RAM."""
273
        return np.prod(self.shape) * self.itemsize
274
275
    @property
276
    def bounds(self) -> np.ndarray:
277
        """Position of centers of voxels in smallest and largest coordinates."""
278
        ini = 0, 0, 0
279
        fin = np.array(self.spatial_shape) - 1
280
        point_ini = nib.affines.apply_affine(self.affine, ini)
281
        point_fin = nib.affines.apply_affine(self.affine, fin)
282
        return np.array((point_ini, point_fin))
283
284
    @property
285
    def num_channels(self) -> int:
286
        """Get the number of channels in the associated 4D tensor."""
287
        return len(self.data)
288
289
    def axis_name_to_index(self, axis: str) -> int:
290
        """Convert an axis name to an axis index.
291
292
        Args:
293
            axis: Possible inputs are ``'Left'``, ``'Right'``, ``'Anterior'``,
294
                ``'Posterior'``, ``'Inferior'``, ``'Superior'``. Lower-case
295
                versions and first letters are also valid, as only the first
296
                letter will be used.
297
298
        .. note:: If you are working with animals, you should probably use
299
            ``'Superior'``, ``'Inferior'``, ``'Anterior'`` and ``'Posterior'``
300
            for ``'Dorsal'``, ``'Ventral'``, ``'Rostral'`` and ``'Caudal'``,
301
            respectively.
302
303
        .. note:: If your images are 2D, you can use ``'Top'``, ``'Bottom'``,
304
            ``'Left'`` and ``'Right'``.
305
        """
306
        # Top and bottom are used for the vertical 2D axis as the use of
307
        # Height vs Horizontal might be ambiguous
308
309
        if not isinstance(axis, str):
310
            raise ValueError('Axis must be a string')
311
        axis = axis[0].upper()
312
313
        # Generally, TorchIO tensors are (C, W, H, D)
314
        if axis in 'TB':  # Top, Bottom
315
            return -2
316
        else:
317
            try:
318
                index = self.orientation.index(axis)
319
            except ValueError:
320
                index = self.orientation.index(self.flip_axis(axis))
321
            # Return negative indices so that it does not matter whether we
322
            # refer to spatial dimensions or not
323
            index = -3 + index
324
            return index
325
326
    # flake8: noqa: E701
327
    @staticmethod
328
    def flip_axis(axis: str) -> str:
329
        if axis == 'R': flipped_axis = 'L'
330
        elif axis == 'L': flipped_axis = 'R'
331
        elif axis == 'A': flipped_axis = 'P'
332
        elif axis == 'P': flipped_axis = 'A'
333
        elif axis == 'I': flipped_axis = 'S'
334
        elif axis == 'S': flipped_axis = 'I'
335
        elif axis == 'T': flipped_axis = 'B'
336
        elif axis == 'B': flipped_axis = 'T'
337
        else:
338
            values = ', '.join('LRPAISTB')
339
            message = f'Axis not understood. Please use one of: {values}'
340
            raise ValueError(message)
341
        return flipped_axis
342
343
    def get_spacing_string(self) -> str:
344
        strings = [f'{n:.2f}' for n in self.spacing]
345
        string = f'({", ".join(strings)})'
346
        return string
347
348
    def get_bounds(self) -> TypeBounds:
349
        """Get minimum and maximum world coordinates occupied by the image."""
350
        first_index = 3 * (-0.5,)
351
        last_index = np.array(self.spatial_shape) - 0.5
352
        first_point = nib.affines.apply_affine(self.affine, first_index)
353
        last_point = nib.affines.apply_affine(self.affine, last_index)
354
        array = np.array((first_point, last_point))
355
        bounds_x, bounds_y, bounds_z = array.T.tolist()
356
        return bounds_x, bounds_y, bounds_z
357
358
    @staticmethod
359
    def _parse_single_path(
360
            path: TypePath
361
            ) -> Path:
362
        try:
363
            path = Path(path).expanduser()
364
        except TypeError:
365
            message = (
366
                f'Expected type str or Path but found {path} with type'
367
                f' {type(path)} instead'
368
            )
369
            raise TypeError(message)
370
        except RuntimeError:
371
            message = (
372
                f'Conversion to path not possible for variable: {path}'
373
            )
374
            raise RuntimeError(message)
375
376
        if not (path.is_file() or path.is_dir()):   # might be a dir with DICOM
377
            raise FileNotFoundError(f'File not found: "{path}"')
378
        return path
379
380
    def _parse_path(
381
            self,
382
            path: Union[TypePath, Sequence[TypePath]]
383
            ) -> Optional[Union[Path, List[Path]]]:
384
        if path is None:
385
            return None
386
        if isinstance(path, Iterable) and not isinstance(path, str):
387
            return [self._parse_single_path(p) for p in path]
388
        else:
389
            return self._parse_single_path(path)
390
391
    def _parse_tensor(
392
            self,
393
            tensor: TypeData,
394
            none_ok: bool = True,
395
            ) -> torch.Tensor:
396
        if tensor is None:
397
            if none_ok:
398
                return None
399
            else:
400
                raise RuntimeError('Input tensor cannot be None')
401
        if isinstance(tensor, np.ndarray):
402
            tensor = check_uint_to_int(tensor)
403
            tensor = torch.as_tensor(tensor)
404
        elif not isinstance(tensor, torch.Tensor):
405
            message = (
406
                'Input tensor must be a PyTorch tensor or NumPy array,'
407
                f' but type "{type(tensor)}" was found'
408
            )
409
            raise TypeError(message)
410
        ndim = tensor.ndim
411
        if ndim != 4:
412
            raise ValueError(f'Input tensor must be 4D, but it is {ndim}D')
413
        if tensor.dtype == torch.bool:
414
            tensor = tensor.to(torch.uint8)
415
        if self.check_nans and torch.isnan(tensor).any():
416
            warnings.warn(f'NaNs found in tensor', RuntimeWarning)
417
        return tensor
418
419
    def parse_tensor_shape(self, tensor: torch.Tensor) -> torch.Tensor:
420
        return ensure_4d(tensor)
421
422
    @staticmethod
423
    def _parse_affine(affine: TypeData) -> np.ndarray:
424
        if affine is None:
425
            return np.eye(4)
426
        if isinstance(affine, torch.Tensor):
427
            affine = affine.numpy()
428
        if not isinstance(affine, np.ndarray):
429
            raise TypeError(f'Affine must be a NumPy array, not {type(affine)}')
430
        if affine.shape != (4, 4):
431
            raise ValueError(f'Affine shape must be (4, 4), not {affine.shape}')
432
        return affine.astype(np.float64)
433
434
    def load(self) -> None:
435
        r"""Load the image from disk.
436
437
        Returns:
438
            Tuple containing a 4D tensor of size :math:`(C, W, H, D)` and a 2D
439
            :math:`4 \times 4` affine matrix to convert voxel indices to world
440
            coordinates.
441
        """
442
        if self._loaded:
443
            return
444
        paths = self.path if isinstance(self.path, list) else [self.path]
445
        tensor, affine = self.read_and_check(paths[0])
446
        tensors = [tensor]
447
        for path in paths[1:]:
448
            new_tensor, new_affine = self.read_and_check(path)
449
            if not np.array_equal(affine, new_affine):
450
                message = (
451
                    'Files have different affine matrices.'
452
                    f'\nMatrix of {paths[0]}:'
453
                    f'\n{affine}'
454
                    f'\nMatrix of {path}:'
455
                    f'\n{new_affine}'
456
                )
457
                warnings.warn(message, RuntimeWarning)
458
            if not tensor.shape[1:] == new_tensor.shape[1:]:
459
                message = (
460
                    f'Files shape do not match, found {tensor.shape}'
461
                    f'and {new_tensor.shape}'
462
                )
463
                RuntimeError(message)
464
            tensors.append(new_tensor)
465
        tensor = torch.cat(tensors)
466
        self.set_data(tensor)
467
        self.affine = affine
468
        self._loaded = True
469
470
    def read_and_check(self, path: TypePath) -> Tuple[torch.Tensor, np.ndarray]:
471
        tensor, affine = self.reader(path)
472
        tensor = self.parse_tensor_shape(tensor)
473
        tensor = self._parse_tensor(tensor)
474
        affine = self._parse_affine(affine)
475
        if self.channels_last:
476
            tensor = tensor.permute(3, 0, 1, 2)
477
        if self.check_nans and torch.isnan(tensor).any():
478
            warnings.warn(f'NaNs found in file "{path}"', RuntimeWarning)
479
        return tensor, affine
480
481
    def save(self, path: TypePath, squeeze: bool = True) -> None:
482
        """Save image to disk.
483
484
        Args:
485
            path: String or instance of :class:`pathlib.Path`.
486
            squeeze: If ``True``, singleton dimensions will be removed
487
                before saving.
488
        """
489
        write_image(
490
            self.data,
491
            self.affine,
492
            path,
493
            squeeze=squeeze,
494
        )
495
496
    def is_2d(self) -> bool:
497
        return self.shape[-1] == 1
498
499
    def numpy(self) -> np.ndarray:
500
        """Get a NumPy array containing the image data."""
501
        return np.asarray(self)
502
503
    def as_sitk(self, **kwargs) -> sitk.Image:
504
        """Get the image as an instance of :class:`sitk.Image`."""
505
        return nib_to_sitk(self.data, self.affine, **kwargs)
506
507
    @classmethod
508
    def from_sitk(cls, sitk_image):
509
        tensor, affine = sitk_to_nib(sitk_image)
510
        return cls(tensor=tensor, affine=affine)
511
512
    def as_pil(self, transpose=True):
513
        """Get the image as an instance of :class:`PIL.Image`.
514
515
        .. note:: Values will be clamped to 0-255 and cast to uint8.
516
        .. note:: To use this method, `Pillow` needs to be installed:
517
            `pip install Pillow`.
518
        """
519
        try:
520
            from PIL import Image as ImagePIL
521
        except ModuleNotFoundError as e:
522
            message = (
523
                'Please install Pillow to use Image.as_pil():'
524
                ' pip install Pillow'
525
            )
526
            raise RuntimeError(message) from e
527
528
        self.check_is_2d()
529
        tensor = self.data
530
        if len(tensor) == 1:
531
            tensor = torch.cat(3 * [tensor])
532
        if len(tensor) != 3:
533
            raise RuntimeError('The image must have 1 or 3 channels')
534
        if transpose:
535
            tensor = tensor.permute(3, 2, 1, 0)
536
        else:
537
            tensor = tensor.permute(3, 1, 2, 0)
538
        array = tensor.clamp(0, 255).numpy()[0]
539
        return ImagePIL.fromarray(array.astype(np.uint8))
540
541
    def get_center(self, lps: bool = False) -> TypeTripletFloat:
542
        """Get image center in RAS+ or LPS+ coordinates.
543
544
        Args:
545
            lps: If ``True``, the coordinates will be in LPS+ orientation, i.e.
546
                the first dimension grows towards the left, etc. Otherwise, the
547
                coordinates will be in RAS+ orientation.
548
        """
549
        size = np.array(self.spatial_shape)
550
        center_index = (size - 1) / 2
551
        r, a, s = nib.affines.apply_affine(self.affine, center_index)
552
        if lps:
553
            return (-r, -a, s)
554
        else:
555
            return (r, a, s)
556
557
    def set_check_nans(self, check_nans: bool) -> None:
558
        self.check_nans = check_nans
559
560
    def plot(self, **kwargs) -> None:
561
        """Plot image."""
562
        if self.is_2d():
563
            self.as_pil().show()
564
        else:
565
            from ..visualization import plot_volume  # avoid circular import
566
            plot_volume(self, **kwargs)
567
568
569
class ScalarImage(Image):
570
    """Image whose pixel values represent scalars.
571
572
    Example:
573
        >>> import torch
574
        >>> import torchio as tio
575
        >>> # Loading from a file
576
        >>> t1_image = tio.ScalarImage('t1.nii.gz')
577
        >>> dmri = tio.ScalarImage(tensor=torch.rand(32, 128, 128, 88))
578
        >>> image = tio.ScalarImage('safe_image.nrrd', check_nans=False)
579
        >>> data, affine = image.data, image.affine
580
        >>> affine.shape
581
        (4, 4)
582
        >>> image.data is image[tio.DATA]
583
        True
584
        >>> image.data is image.tensor
585
        True
586
        >>> type(image.data)
587
        torch.Tensor
588
589
    See :class:`~torchio.Image` for more information.
590
    """
591
    def __init__(self, *args, **kwargs):
592
        if 'type' in kwargs and kwargs['type'] != INTENSITY:
593
            raise ValueError('Type of ScalarImage is always torchio.INTENSITY')
594
        kwargs.update({'type': INTENSITY})
595
        super().__init__(*args, **kwargs)
596
597
598
class LabelMap(Image):
599
    """Image whose pixel values represent categorical labels.
600
601
    Example:
602
        >>> import torch
603
        >>> import torchio as tio
604
        >>> labels = tio.LabelMap(tensor=torch.rand(1, 128, 128, 68) > 0.5)
605
        >>> labels = tio.LabelMap('t1_seg.nii.gz')  # loading from a file
606
        >>> tpm = tio.LabelMap(                     # loading from files
607
        ...     'gray_matter.nii.gz',
608
        ...     'white_matter.nii.gz',
609
        ...     'csf.nii.gz',
610
        ... )
611
612
    Intensity transforms are not applied to these images.
613
614
    Nearest neighbor interpolation is always used to resample label maps,
615
    independently of the specified interpolation type in the transform
616
    instantiation.
617
618
    See :class:`~torchio.Image` for more information.
619
    """
620
    def __init__(self, *args, **kwargs):
621
        if 'type' in kwargs and kwargs['type'] != LABEL:
622
            raise ValueError('Type of LabelMap is always torchio.LABEL')
623
        kwargs.update({'type': LABEL})
624
        super().__init__(*args, **kwargs)
625