diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 92c2cc60..2a2bb133 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,6 @@ jobs: windows-latest, ] python: [ - '3.8', '3.9', '3.10', '3.11', diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e0d86882..60361634 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: rev: v3.19.0 hooks: - id: pyupgrade - args: ['--py38-plus', '--keep-runtime-typing'] + args: ['--py39-plus', '--keep-runtime-typing'] - repo: https://github.com/PyCQA/doc8 rev: v1.1.2 diff --git a/docs/source/conf.py b/docs/source/conf.py index dfd9d7c7..9526b6a5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,7 +12,6 @@ import os import sys from datetime import date -from typing import List sys.path.insert(0, os.path.abspath('../../src')) import torchio # noqa: E402 @@ -83,7 +82,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns: List[str] = [] +exclude_patterns: list[str] = [] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'friendly' diff --git a/pyproject.toml b/pyproject.toml index e4dcec74..017a443f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,6 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.12", @@ -35,7 +34,7 @@ classifiers = [ "Typing :: Typed" ] keywords = ["medical", "image processing", "pytorch", "augmentation", "mri"] -requires-python = ">=3.8,<3.13" +requires-python = ">=3.9,<3.13" dependencies = [ "deprecated", "humanize", diff --git a/src/torchio/data/dataset.py b/src/torchio/data/dataset.py index 55c9aa77..93b835b5 100644 --- a/src/torchio/data/dataset.py +++ b/src/torchio/data/dataset.py @@ -1,11 +1,10 @@ from __future__ import annotations import copy +from collections.abc import Iterable +from collections.abc import Sequence from typing import Callable -from typing import Iterable -from typing import List from typing import Optional -from typing import Sequence from torch.utils.data import Dataset @@ -107,7 +106,7 @@ def from_batch(cls, batch: dict) -> SubjectsDataset: batch: Dictionary generated by a data loader, containing data that can be converted to instances of :class:`~.torchio.Subject`. """ - subjects: List[Subject] = get_subjects_from_batch(batch) + subjects: list[Subject] = get_subjects_from_batch(batch) return cls(subjects) def dry_iter(self): diff --git a/src/torchio/data/image.py b/src/torchio/data/image.py index a48b21d6..e4f010bd 100644 --- a/src/torchio/data/image.py +++ b/src/torchio/data/image.py @@ -1,13 +1,10 @@ import warnings from collections import Counter +from collections.abc import Sequence from pathlib import Path from typing import Any from typing import Callable -from typing import Dict -from typing import List from typing import Optional -from typing import Sequence -from typing import Tuple from typing import Union import humanize @@ -51,8 +48,8 @@ from .io import write_image PROTECTED_KEYS = DATA, AFFINE, TYPE, PATH, STEM -TypeBound = Tuple[float, float] -TypeBounds = Tuple[TypeBound, TypeBound, TypeBound] +TypeBound = tuple[float, float] +TypeBounds = tuple[TypeBound, TypeBound, TypeBound] deprecation_message = ( 'Setting the image data with the property setter is deprecated. Use the' @@ -139,7 +136,7 @@ def __init__( affine: Optional[TypeData] = None, check_nans: bool = False, # removed by ITK by default reader: Callable = read_image, - **kwargs: Dict[str, Any], + **kwargs: dict[str, Any], ): self.check_nans = check_nans self.reader = reader @@ -336,7 +333,7 @@ def width(self) -> int: return self.spatial_shape[0] @property - def orientation(self) -> Tuple[str, str, str]: + def orientation(self) -> tuple[str, str, str]: """Orientation codes.""" return nib.aff2axcodes(self.affine) @@ -349,14 +346,14 @@ def direction(self) -> TypeDirection3D: return direction # type: ignore[return-value] @property - def spacing(self) -> Tuple[float, float, float]: + def spacing(self) -> tuple[float, float, float]: """Voxel spacing in mm.""" _, spacing = get_rotation_and_spacing_from_affine(self.affine) sx, sy, sz = spacing return sx, sy, sz @property - def origin(self) -> Tuple[float, float, float]: + def origin(self) -> tuple[float, float, float]: """Center of first voxel in array, in mm.""" ox, oy, oz = self.affine[:3, 3] return ox, oy, oz @@ -479,7 +476,7 @@ def _parse_single_path( def _parse_path( self, path: Optional[Union[TypePath, Sequence[TypePath]]], - ) -> Optional[Union[Path, List[Path]]]: + ) -> Optional[Union[Path, list[Path]]]: if path is None: return None elif isinstance(path, dict): @@ -565,7 +562,7 @@ def load(self) -> None: if self._loaded: return - paths: List[Path] + paths: list[Path] if self._is_multipath(): paths = self.path # type: ignore[assignment] else: @@ -807,12 +804,12 @@ def show(self, viewer_path: Optional[TypePath] = None) -> None: def _crop_from_slices( self, - slices: Union[TypeSlice, Tuple[TypeSlice, ...]], + slices: Union[TypeSlice, tuple[TypeSlice, ...]], ) -> 'Image': from ..transforms import Crop slices_tuple = to_tuple(slices) # type: ignore[assignment] - cropping: List[int] = [] + cropping: list[int] = [] for dim, slice_ in enumerate(slices_tuple): if isinstance(slice_, slice): pass @@ -914,7 +911,7 @@ def count_nonzero(self) -> int: """Get the number of voxels that are not 0.""" return int(self.data.count_nonzero().item()) - def count_labels(self) -> Dict[int, int]: + def count_labels(self) -> dict[int, int]: """Get the number of voxels in each label.""" values_list = self.data.flatten().tolist() counter = Counter(values_list) diff --git a/src/torchio/data/inference/aggregator.py b/src/torchio/data/inference/aggregator.py index dae0b728..445429b3 100644 --- a/src/torchio/data/inference/aggregator.py +++ b/src/torchio/data/inference/aggregator.py @@ -1,6 +1,5 @@ import warnings from typing import Optional -from typing import Tuple import numpy as np import torch @@ -58,7 +57,7 @@ def _crop_patch( patch: torch.Tensor, location: np.ndarray, overlap: np.ndarray, - ) -> Tuple[torch.Tensor, np.ndarray]: + ) -> tuple[torch.Tensor, np.ndarray]: half_overlap = overlap // 2 # overlap is always even in grid sampler index_ini, index_fin = location[:3], location[3:] diff --git a/src/torchio/data/io.py b/src/torchio/data/io.py index 33de9070..b1e05f0c 100644 --- a/src/torchio/data/io.py +++ b/src/torchio/data/io.py @@ -1,7 +1,6 @@ import warnings from pathlib import Path from typing import Optional -from typing import Tuple from typing import Union import nibabel as nib @@ -283,7 +282,7 @@ def _write_niftyreg_matrix(matrix: TypeData, txt_path: TypePath) -> None: def get_rotation_and_spacing_from_affine( affine: np.ndarray, -) -> Tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray]: # From https://github.com/nipy/nibabel/blob/master/nibabel/orientations.py rotation_zoom = affine[:3, :3] spacing = np.sqrt(np.sum(rotation_zoom * rotation_zoom, axis=0)) @@ -337,7 +336,7 @@ def nib_to_sitk( def sitk_to_nib( image: sitk.Image, keepdim: bool = False, -) -> Tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray]: data = sitk.GetArrayFromImage(image).transpose() data = check_uint_to_int(data) num_components = image.GetNumberOfComponentsPerPixel() @@ -391,7 +390,7 @@ def get_sitk_metadata_from_ras_affine( affine: np.ndarray, is_2d: bool = False, lps: bool = True, -) -> Tuple[TypeTripletFloat, TypeTripletFloat, TypeDirection]: +) -> tuple[TypeTripletFloat, TypeTripletFloat, TypeDirection]: direction_ras, spacing_array = get_rotation_and_spacing_from_affine(affine) origin_ras = affine[:3, 3] origin_lps = np.dot(FLIPXY_33, origin_ras) diff --git a/src/torchio/data/loader.py b/src/torchio/data/loader.py index 768d1936..149558b4 100644 --- a/src/torchio/data/loader.py +++ b/src/torchio/data/loader.py @@ -1,7 +1,5 @@ from typing import Any from typing import Callable -from typing import Dict -from typing import List from typing import Optional from typing import TypeVar @@ -19,7 +17,7 @@ class SubjectsLoader(DataLoader): def __init__( self, dataset: Dataset, - collate_fn: Optional[Callable[[List[T]], Any]] = None, + collate_fn: Optional[Callable[[list[T]], Any]] = None, **kwargs, ): if collate_fn is None: @@ -31,7 +29,7 @@ def __init__( ) @staticmethod - def _collate(subjects: List[Subject]) -> Dict[str, Any]: + def _collate(subjects: list[Subject]) -> dict[str, Any]: first_subject = subjects[0] batch_dict = {} for key in first_subject.keys(): diff --git a/src/torchio/data/queue.py b/src/torchio/data/queue.py index 1b215180..84f87e8a 100644 --- a/src/torchio/data/queue.py +++ b/src/torchio/data/queue.py @@ -1,6 +1,5 @@ +from collections.abc import Iterator from itertools import islice -from typing import Iterator -from typing import List from typing import Optional import humanize @@ -210,7 +209,7 @@ def __init__( self._num_sampled_subjects = 0 if start_background: self._initialize_subjects_iterable() - self.patches_list: List[Subject] = [] + self.patches_list: list[Subject] = [] if self.shuffle_subjects and self.subject_sampler is not None: raise ValueError( diff --git a/src/torchio/data/sampler/grid.py b/src/torchio/data/sampler/grid.py index 506c6337..e1dd542f 100644 --- a/src/torchio/data/sampler/grid.py +++ b/src/torchio/data/sampler/grid.py @@ -1,4 +1,4 @@ -from typing import Generator +from collections.abc import Generator from typing import Optional from typing import Union diff --git a/src/torchio/data/sampler/label.py b/src/torchio/data/sampler/label.py index 31e31f2c..e1b38e78 100644 --- a/src/torchio/data/sampler/label.py +++ b/src/torchio/data/sampler/label.py @@ -1,4 +1,3 @@ -from typing import Dict from typing import Optional import numpy as np @@ -63,7 +62,7 @@ def __init__( self, patch_size: TypeSpatialShape, label_name: Optional[str] = None, - label_probabilities: Optional[Dict[int, float]] = None, + label_probabilities: Optional[dict[int, float]] = None, ): super().__init__(patch_size, probability_map=label_name) self.label_probabilities_dict = label_probabilities @@ -106,7 +105,7 @@ def get_probability_map(self, subject: Subject) -> torch.Tensor: @staticmethod def get_probabilities_from_label_map( label_map: torch.Tensor, - label_probabilities_dict: Dict[int, float], + label_probabilities_dict: dict[int, float], patch_size: np.ndarray, ) -> torch.Tensor: """Create probability map according to label map probabilities.""" diff --git a/src/torchio/data/sampler/sampler.py b/src/torchio/data/sampler/sampler.py index 1c866b11..540e168b 100644 --- a/src/torchio/data/sampler/sampler.py +++ b/src/torchio/data/sampler/sampler.py @@ -1,4 +1,4 @@ -from typing import Generator +from collections.abc import Generator from typing import Optional import numpy as np diff --git a/src/torchio/data/sampler/uniform.py b/src/torchio/data/sampler/uniform.py index 117ae593..21b7b582 100644 --- a/src/torchio/data/sampler/uniform.py +++ b/src/torchio/data/sampler/uniform.py @@ -1,4 +1,4 @@ -from typing import Generator +from collections.abc import Generator from typing import Optional import torch diff --git a/src/torchio/data/sampler/weighted.py b/src/torchio/data/sampler/weighted.py index 5f997bc0..81562a38 100644 --- a/src/torchio/data/sampler/weighted.py +++ b/src/torchio/data/sampler/weighted.py @@ -1,4 +1,4 @@ -from typing import Generator +from collections.abc import Generator from typing import Optional import numpy as np diff --git a/src/torchio/data/subject.py b/src/torchio/data/subject.py index 9369d823..75ade087 100644 --- a/src/torchio/data/subject.py +++ b/src/torchio/data/subject.py @@ -2,14 +2,11 @@ import copy import pprint +from collections.abc import Sequence from typing import TYPE_CHECKING from typing import Any from typing import Callable -from typing import Dict -from typing import List from typing import Optional -from typing import Sequence -from typing import Tuple import numpy as np @@ -52,7 +49,7 @@ class Subject(dict): >>> subject = tio.Subject(subject_dict) """ - def __init__(self, *args, **kwargs: Dict[str, Any]): + def __init__(self, *args, **kwargs: dict[str, Any]): if args: if len(args) == 1 and isinstance(args[0], dict): kwargs.update(args[0]) @@ -62,7 +59,7 @@ def __init__(self, *args, **kwargs: Dict[str, Any]): super().__init__(**kwargs) self._parse_images(self.get_images(intensity_only=False)) self.update_attributes() # this allows me to do e.g. subject.t1 - self.applied_transforms: List[Tuple[str, dict]] = [] + self.applied_transforms: list[tuple[str, dict]] = [] def __repr__(self): num_images = len(self.get_images(intensity_only=False)) @@ -96,7 +93,7 @@ def __getitem__(self, item): return super().__getitem__(item) @staticmethod - def _parse_images(images: List[Image]) -> None: + def _parse_images(images: list[Image]) -> None: # Check that it's not empty if not images: raise TypeError('A subject without images cannot be created') @@ -162,7 +159,7 @@ def get_applied_transforms( self, ignore_intensity: bool = False, image_interpolation: Optional[str] = None, - ) -> List[Transform]: + ) -> list[Transform]: from ..transforms.intensity_transform import IntensityTransform from ..transforms.transform import Transform @@ -333,7 +330,7 @@ def check_consistent_space(self) -> None: ) raise RuntimeError(message) from e - def get_images_names(self) -> List[str]: + def get_images_names(self) -> list[str]: return list(self.get_images_dict(intensity_only=False).keys()) def get_images_dict( @@ -341,7 +338,7 @@ def get_images_dict( intensity_only=True, include: Optional[Sequence[str]] = None, exclude: Optional[Sequence[str]] = None, - ) -> Dict[str, Image]: + ) -> dict[str, Image]: images = {} for image_name, image in self.items(): if not isinstance(image, Image): @@ -360,7 +357,7 @@ def get_images( intensity_only=True, include: Optional[Sequence[str]] = None, exclude: Optional[Sequence[str]] = None, - ) -> List[Image]: + ) -> list[Image]: images_dict = self.get_images_dict( intensity_only=intensity_only, include=include, @@ -433,7 +430,7 @@ def plot(self, **kwargs) -> None: def _subject_copy_helper( old_obj: Subject, - new_subj_cls: Callable[[Dict[str, Any]], Subject], + new_subj_cls: Callable[[dict[str, Any]], Subject], ): result_dict = {} for key, value in old_obj.items(): diff --git a/src/torchio/datasets/bite.py b/src/torchio/datasets/bite.py index 23435b80..d85d31fe 100644 --- a/src/torchio/datasets/bite.py +++ b/src/torchio/datasets/bite.py @@ -1,6 +1,5 @@ import abc from pathlib import Path -from typing import Dict from typing import Optional from ..data import Image @@ -83,7 +82,7 @@ def _get_subjects_list(self, root: Path): subject_dir = subjects_dir / subject_id preop_path = subject_dir / f'{subject_id}_preop_mri.mnc' postop_path = subject_dir / f'{subject_id}_postop_mri.mnc' - images_dict: Dict[str, Image] = {} + images_dict: dict[str, Image] = {} images_dict['preop'] = ScalarImage(preop_path) images_dict['postop'] = ScalarImage(postop_path) for fp in subject_dir.glob('*tumor*'): diff --git a/src/torchio/datasets/ixi.py b/src/torchio/datasets/ixi.py index 511509be..3b2c170f 100644 --- a/src/torchio/datasets/ixi.py +++ b/src/torchio/datasets/ixi.py @@ -13,10 +13,10 @@ # Adapted from # https://pytorch.org/docs/stable/_modules/torchvision/datasets/mnist.html#MNIST import shutil +from collections.abc import Sequence from pathlib import Path from tempfile import NamedTemporaryFile from typing import Optional -from typing import Sequence from ..data import LabelMap from ..data import ScalarImage diff --git a/src/torchio/datasets/rsna_miccai.py b/src/torchio/datasets/rsna_miccai.py index 2e868524..02080610 100644 --- a/src/torchio/datasets/rsna_miccai.py +++ b/src/torchio/datasets/rsna_miccai.py @@ -1,9 +1,7 @@ import csv import warnings +from collections.abc import Sequence from pathlib import Path -from typing import Dict -from typing import List -from typing import Sequence from typing import Union from ..data import ScalarImage @@ -77,7 +75,7 @@ def _get_subjects( root_dir: Path, train: bool, ignore_empty: bool, - ) -> List[Subject]: + ) -> list[Subject]: subjects = [] if train: csv_path = root_dir / 'train_labels.csv' @@ -105,7 +103,7 @@ def _get_subjects( int(subject_id) except ValueError: continue - images_dict: Dict[str, Union[str, int, ScalarImage]] + images_dict: dict[str, Union[str, int, ScalarImage]] images_dict = {self.id_key: subject_dir.name} if train and labels_dict: images_dict[self.label_key] = labels_dict[subject_id] diff --git a/src/torchio/datasets/rsna_spine_fracture.py b/src/torchio/datasets/rsna_spine_fracture.py index a86a2c04..fd0bc05a 100644 --- a/src/torchio/datasets/rsna_spine_fracture.py +++ b/src/torchio/datasets/rsna_spine_fracture.py @@ -1,8 +1,6 @@ from pathlib import Path from types import ModuleType from typing import Any -from typing import Dict -from typing import List from typing import Optional from typing import Union @@ -13,7 +11,7 @@ from ..typing import TypePath from ..utils import normalize_path -TypeBoxes = List[Dict[str, Union[str, float, int]]] +TypeBoxes = list[dict[str, Union[str, float, int]]] class RSNACervicalSpineFracture(SubjectsDataset): @@ -44,14 +42,14 @@ def __init__( super().__init__(subjects, **kwargs) @staticmethod - def _get_image_dirs_dict(images_dir: Path) -> Dict[str, Path]: + def _get_image_dirs_dict(images_dir: Path) -> dict[str, Path]: dirs_dict = {} for dicom_dir in sorted(images_dir.iterdir()): dirs_dict[dicom_dir.name] = dicom_dir return dirs_dict @staticmethod - def _get_segs_paths_dict(segs_dir: Path) -> Dict[str, Path]: + def _get_segs_paths_dict(segs_dir: Path) -> dict[str, Path]: paths_dict = {} for image_path in sorted(segs_dir.iterdir()): key = image_path.name.replace('.gz', '').replace('.nii', '') @@ -62,7 +60,7 @@ def _get_subjects( self, add_segmentations: bool, add_bounding_boxes: bool, - ) -> List[Subject]: + ) -> list[Subject]: subjects = [] pd = get_pandas() from tqdm.auto import tqdm @@ -104,7 +102,7 @@ def _get_subjects( return subjects @staticmethod - def _filter_list(iterable: List[Path], target: str): + def _filter_list(iterable: list[Path], target: str): def _filter(path: Path): if path.is_dir(): return target == path.name @@ -122,12 +120,12 @@ def _filter(path: Path): def _get_subject( self, - csv_row_dict: Dict[str, Union[str, int]], + csv_row_dict: dict[str, Union[str, int]], image_dir: Path, seg_path: Optional[Path], boxes: TypeBoxes, ) -> Subject: - subject_dict: Dict[str, Any] = {} + subject_dict: dict[str, Any] = {} subject_dict.update(csv_row_dict) subject_dict['ct'] = ScalarImage(image_dir) if seg_path is not None: diff --git a/src/torchio/transforms/augmentation/composition.py b/src/torchio/transforms/augmentation/composition.py index 7c0d89fb..71f656c9 100644 --- a/src/torchio/transforms/augmentation/composition.py +++ b/src/torchio/transforms/augmentation/composition.py @@ -1,8 +1,7 @@ from __future__ import annotations import warnings -from typing import Dict -from typing import Sequence +from collections.abc import Sequence from typing import Union import numpy as np @@ -12,7 +11,7 @@ from ..transform import Transform from . import RandomTransform -TypeTransformsDict = Union[Dict[Transform, float], Sequence[Transform]] +TypeTransformsDict = Union[dict[Transform, float], Sequence[Transform]] class Compose(Transform): @@ -115,7 +114,7 @@ def apply_transform(self, subject: Subject) -> Subject: def _get_transforms_dict( self, transforms: TypeTransformsDict, - ) -> Dict[Transform, float]: + ) -> dict[Transform, float]: if isinstance(transforms, dict): transforms_dict = dict(transforms) self._normalize_probabilities(transforms_dict) @@ -140,7 +139,7 @@ def _get_transforms_dict( @staticmethod def _normalize_probabilities( - transforms_dict: Dict[Transform, float], + transforms_dict: dict[Transform, float], ) -> None: probabilities = np.array(list(transforms_dict.values()), dtype=float) if np.any(probabilities < 0): diff --git a/src/torchio/transforms/augmentation/intensity/random_bias_field.py b/src/torchio/transforms/augmentation/intensity/random_bias_field.py index c623d0c1..4308c9af 100644 --- a/src/torchio/transforms/augmentation/intensity/random_bias_field.py +++ b/src/torchio/transforms/augmentation/intensity/random_bias_field.py @@ -1,7 +1,4 @@ from collections import defaultdict -from typing import Dict -from typing import List -from typing import Tuple from typing import Union import numpy as np @@ -39,7 +36,7 @@ class RandomBiasField(RandomTransform, IntensityTransform): def __init__( self, - coefficients: Union[float, Tuple[float, float]] = 0.5, + coefficients: Union[float, tuple[float, float]] = 0.5, order: int = 3, **kwargs, ): @@ -55,7 +52,7 @@ def apply_transform(self, subject: Subject) -> Subject: if not images_dict: return subject - arguments: Dict[str, dict] = defaultdict(dict) + arguments: dict[str, dict] = defaultdict(dict) for image_name in images_dict: coefficients = self.get_params(self.order, self.coefficients_range) arguments['coefficients'][image_name] = coefficients @@ -67,8 +64,8 @@ def apply_transform(self, subject: Subject) -> Subject: def get_params( self, order: int, - coefficients_range: Tuple[float, float], - ) -> List[float]: + coefficients_range: tuple[float, float], + ) -> list[float]: # Sampling of the appropriate number of coefficients for the creation # of the bias field map random_coefficients = [] @@ -92,8 +89,8 @@ class BiasField(IntensityTransform): def __init__( self, - coefficients: Union[List[float], Dict[str, List[float]]], - order: Union[int, Dict[str, int]], + coefficients: Union[list[float], dict[str, list[float]]], + order: Union[int, dict[str, int]], **kwargs, ): super().__init__(**kwargs) diff --git a/src/torchio/transforms/augmentation/intensity/random_blur.py b/src/torchio/transforms/augmentation/intensity/random_blur.py index e1406dea..092af1e8 100644 --- a/src/torchio/transforms/augmentation/intensity/random_blur.py +++ b/src/torchio/transforms/augmentation/intensity/random_blur.py @@ -1,6 +1,4 @@ from collections import defaultdict -from typing import Dict -from typing import Tuple from typing import Union import numpy as np @@ -34,7 +32,7 @@ class RandomBlur(RandomTransform, IntensityTransform): keyword arguments. """ - def __init__(self, std: Union[float, Tuple[float, float]] = (0, 2), **kwargs): + def __init__(self, std: Union[float, tuple[float, float]] = (0, 2), **kwargs): super().__init__(**kwargs) self.std_ranges = self.parse_params(std, None, 'std', min_constraint=0) @@ -43,7 +41,7 @@ def apply_transform(self, subject: Subject) -> Subject: if not images_dict: return subject - arguments: Dict[str, dict] = defaultdict(dict) + arguments: dict[str, dict] = defaultdict(dict) for name in images_dict: std = self.get_params(self.std_ranges) # type: ignore[arg-type] arguments['std'][name] = std @@ -70,7 +68,7 @@ class Blur(IntensityTransform): def __init__( self, - std: Union[TypeTripletFloat, Dict[str, TypeTripletFloat]], + std: Union[TypeTripletFloat, dict[str, TypeTripletFloat]], **kwargs, ): super().__init__(**kwargs) diff --git a/src/torchio/transforms/augmentation/intensity/random_gamma.py b/src/torchio/transforms/augmentation/intensity/random_gamma.py index 5a2d2721..1f71b502 100644 --- a/src/torchio/transforms/augmentation/intensity/random_gamma.py +++ b/src/torchio/transforms/augmentation/intensity/random_gamma.py @@ -1,6 +1,4 @@ from collections import defaultdict -from typing import Dict -from typing import Tuple import numpy as np import torch @@ -73,7 +71,7 @@ def apply_transform(self, subject: Subject) -> Subject: if not images_dict: return subject - arguments: Dict[str, dict] = defaultdict(dict) + arguments: dict[str, dict] = defaultdict(dict) for name, image in images_dict.items(): gammas = [self.get_params(self.log_gamma_range) for _ in image.data] arguments['gamma'][name] = gammas @@ -82,7 +80,7 @@ def apply_transform(self, subject: Subject) -> Subject: assert isinstance(transformed, Subject) return transformed - def get_params(self, log_gamma_range: Tuple[float, float]) -> float: + def get_params(self, log_gamma_range: tuple[float, float]) -> float: gamma = np.exp(self.sample_uniform(*log_gamma_range)) return gamma diff --git a/src/torchio/transforms/augmentation/intensity/random_ghosting.py b/src/torchio/transforms/augmentation/intensity/random_ghosting.py index bb02b9ee..b2d87a56 100644 --- a/src/torchio/transforms/augmentation/intensity/random_ghosting.py +++ b/src/torchio/transforms/augmentation/intensity/random_ghosting.py @@ -1,8 +1,6 @@ from collections import defaultdict -from typing import Dict -from typing import Iterable +from collections.abc import Iterable from typing import Optional -from typing import Tuple from typing import Union import numpy as np @@ -57,9 +55,9 @@ class RandomGhosting(RandomTransform, IntensityTransform): def __init__( self, - num_ghosts: Union[int, Tuple[int, int]] = (4, 10), - axes: Union[int, Tuple[int, ...]] = (0, 1, 2), - intensity: Union[float, Tuple[float, float]] = (0.5, 1), + num_ghosts: Union[int, tuple[int, int]] = (4, 10), + axes: Union[int, tuple[int, ...]] = (0, 1, 2), + intensity: Union[float, tuple[float, float]] = (0.5, 1), restore: Optional[float] = None, **kwargs, ): @@ -103,7 +101,7 @@ def apply_transform(self, subject: Subject) -> Subject: if any(isinstance(axis, str) for axis in self.axes): subject.check_consistent_orientation() - arguments: Dict[str, dict] = defaultdict(dict) + arguments: dict[str, dict] = defaultdict(dict) for name, image in images_dict.items(): is_2d = image.is_2d() axes = [a for a in self.axes if a != 2] if is_2d else self.axes @@ -126,11 +124,11 @@ def apply_transform(self, subject: Subject) -> Subject: def get_params( self, - num_ghosts_range: Tuple[int, int], - axes: Tuple[int, ...], - intensity_range: Tuple[float, float], - restore_range: Optional[Tuple[float, float]], - ) -> Tuple[int, int, float, Optional[float]]: + num_ghosts_range: tuple[int, int], + axes: tuple[int, ...], + intensity_range: tuple[float, float], + restore_range: Optional[tuple[float, float]], + ) -> tuple[int, int, float, Optional[float]]: ng_min, ng_max = num_ghosts_range num_ghosts = int(torch.randint(ng_min, ng_max + 1, (1,)).item()) axis = axes[torch.randint(0, len(axes), (1,))] @@ -173,10 +171,10 @@ class Ghosting(IntensityTransform, FourierTransform): def __init__( self, - num_ghosts: Union[int, Dict[str, int]], - axis: Union[int, Dict[str, int]], - intensity: Union[float, Dict[str, float]], - restore: Union[Optional[float], Dict[str, Optional[float]]], + num_ghosts: Union[int, dict[str, int]], + axis: Union[int, dict[str, int]], + intensity: Union[float, dict[str, float]], + restore: Union[Optional[float], dict[str, Optional[float]]], **kwargs, ): super().__init__(**kwargs) @@ -187,10 +185,10 @@ def __init__( self.args_names = ['num_ghosts', 'axis', 'intensity', 'restore'] def apply_transform(self, subject: Subject) -> Subject: - axis: Union[int, Dict[str, int]] - num_ghosts: Union[int, Dict[str, int]] - intensity: Union[float, Dict[str, float]] - restore: Union[Optional[float], Dict[str, Optional[float]]] + axis: Union[int, dict[str, int]] + num_ghosts: Union[int, dict[str, int]] + intensity: Union[float, dict[str, float]] + restore: Union[Optional[float], dict[str, Optional[float]]] for name, image in self.get_images_dict(subject).items(): if self.arguments_are_dict(): assert isinstance(self.axis, dict) @@ -270,7 +268,7 @@ def _get_slices_to_restore( spectrum: torch.Tensor, axis: int, restore_center: Optional[float], - ) -> Tuple[torch.Tensor, Tuple[slice, ...]]: + ) -> tuple[torch.Tensor, tuple[slice, ...]]: dim_shape = spectrum.shape[axis] mid_idx = dim_shape // 2 slices = [slice(None)] * spectrum.ndim diff --git a/src/torchio/transforms/augmentation/intensity/random_labels_to_image.py b/src/torchio/transforms/augmentation/intensity/random_labels_to_image.py index ad266153..e2d78418 100644 --- a/src/torchio/transforms/augmentation/intensity/random_labels_to_image.py +++ b/src/torchio/transforms/augmentation/intensity/random_labels_to_image.py @@ -1,7 +1,5 @@ -from typing import List +from collections.abc import Sequence from typing import Optional -from typing import Sequence -from typing import Tuple import torch @@ -160,7 +158,7 @@ def parse_mean_and_std( self, mean: Sequence[TypeRangeFloat], std: Sequence[TypeRangeFloat], - ) -> Tuple[List[TypeRangeFloat], List[TypeRangeFloat]]: + ) -> tuple[list[TypeRangeFloat], list[TypeRangeFloat]]: if mean is not None: mean = self.parse_gaussian_parameters(mean, 'mean') if std is not None: @@ -176,7 +174,7 @@ def parse_gaussian_parameters( self, params: Sequence[TypeRangeFloat], name: str, - ) -> List[TypeRangeFloat]: + ) -> list[TypeRangeFloat]: check_sequence(params, name) params = [ self.parse_gaussian_parameter(p, f'{name}[{i}]') @@ -194,7 +192,7 @@ def parse_gaussian_parameters( def parse_gaussian_parameter( nums_range: TypeRangeFloat, name: str, - ) -> Tuple[float, float]: + ) -> tuple[float, float]: if isinstance(nums_range, (int, float)): return nums_range, nums_range @@ -273,7 +271,7 @@ def apply_transform(self, subject: Subject) -> Subject: assert isinstance(transformed, Subject) return transformed - def get_params(self, label: int) -> Tuple[float, float]: + def get_params(self, label: int) -> tuple[float, float]: if self.mean is None: mean_range = self.default_mean else: diff --git a/src/torchio/transforms/augmentation/intensity/random_motion.py b/src/torchio/transforms/augmentation/intensity/random_motion.py index 1e179f31..edabc722 100644 --- a/src/torchio/transforms/augmentation/intensity/random_motion.py +++ b/src/torchio/transforms/augmentation/intensity/random_motion.py @@ -1,8 +1,5 @@ from collections import defaultdict -from typing import Dict -from typing import List -from typing import Sequence -from typing import Tuple +from collections.abc import Sequence from typing import Union import numpy as np @@ -52,8 +49,8 @@ class RandomMotion(RandomTransform, IntensityTransform, FourierTransform): def __init__( self, - degrees: Union[float, Tuple[float, float]] = 10, - translation: Union[float, Tuple[float, float]] = 10, # in mm + degrees: Union[float, tuple[float, float]] = 10, + translation: Union[float, tuple[float, float]] = 10, # in mm num_transforms: int = 2, image_interpolation: str = 'linear', **kwargs, @@ -77,7 +74,7 @@ def apply_transform(self, subject: Subject) -> Subject: if not images_dict: return subject - arguments: Dict[str, dict] = defaultdict(dict) + arguments: dict[str, dict] = defaultdict(dict) for name, image in images_dict.items(): params = self.get_params( self.degrees_range, @@ -97,12 +94,12 @@ def apply_transform(self, subject: Subject) -> Subject: def get_params( self, - degrees_range: Tuple[float, float], - translation_range: Tuple[float, float], + degrees_range: tuple[float, float], + translation_range: tuple[float, float], num_transforms: int, perturbation: float = 0.3, is_2d: bool = False, - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: # If perturbation is 0, time intervals between movements are constant degrees_params = self.get_params_array( degrees_range, @@ -124,7 +121,7 @@ def get_params( return times_params, degrees_params, translation_params @staticmethod - def get_params_array(nums_range: Tuple[float, float], num_transforms: int): + def get_params_array(nums_range: tuple[float, float], num_transforms: int): tensor = torch.FloatTensor(num_transforms, 3).uniform_(*nums_range) return tensor.numpy() @@ -148,10 +145,10 @@ class Motion(IntensityTransform, FourierTransform): def __init__( self, - degrees: Union[TypeTripletFloat, Dict[str, TypeTripletFloat]], - translation: Union[TypeTripletFloat, Dict[str, TypeTripletFloat]], - times: Union[Sequence[float], Dict[str, Sequence[float]]], - image_interpolation: Union[Sequence[str], Dict[str, Sequence[str]]], + degrees: Union[TypeTripletFloat, dict[str, TypeTripletFloat]], + translation: Union[TypeTripletFloat, dict[str, TypeTripletFloat]], + times: Union[Sequence[float], dict[str, Sequence[float]]], + image_interpolation: Union[Sequence[str], dict[str, Sequence[str]]], **kwargs, ): super().__init__(**kwargs) @@ -210,7 +207,7 @@ def get_rigid_transforms( degrees_params: np.ndarray, translation_params: np.ndarray, image: sitk.Image, - ) -> List[sitk.Euler3DTransform]: + ) -> list[sitk.Euler3DTransform]: center_ijk = np.array(image.GetSize()) / 2 center_lps = image.TransformContinuousIndexToPhysicalPoint(center_ijk) identity = np.eye(4) @@ -247,7 +244,7 @@ def resample_images( image: sitk.Image, transforms: Sequence[sitk.Euler3DTransform], interpolation: str, - ) -> List[sitk.Image]: + ) -> list[sitk.Image]: floating = reference = image default_value = np.float64(sitk.GetArrayViewFromImage(image).min()) transforms = transforms[1:] # first is identity @@ -265,7 +262,7 @@ def resample_images( return images @staticmethod - def sort_spectra(spectra: List[torch.Tensor], times: np.ndarray): + def sort_spectra(spectra: list[torch.Tensor], times: np.ndarray): """Use original spectrum to fill the center of k-space.""" num_spectra = len(spectra) if np.any(times > 0.5): diff --git a/src/torchio/transforms/augmentation/intensity/random_noise.py b/src/torchio/transforms/augmentation/intensity/random_noise.py index 2560b4dd..be738ac7 100644 --- a/src/torchio/transforms/augmentation/intensity/random_noise.py +++ b/src/torchio/transforms/augmentation/intensity/random_noise.py @@ -1,7 +1,5 @@ from collections import defaultdict -from typing import Dict -from typing import Sequence -from typing import Tuple +from collections.abc import Sequence from typing import Union import torch @@ -35,8 +33,8 @@ class RandomNoise(RandomTransform, IntensityTransform): def __init__( self, - mean: Union[float, Tuple[float, float]] = 0, - std: Union[float, Tuple[float, float]] = (0, 0.25), + mean: Union[float, tuple[float, float]] = 0, + std: Union[float, tuple[float, float]] = (0, 0.25), **kwargs, ): super().__init__(**kwargs) @@ -48,7 +46,7 @@ def apply_transform(self, subject: Subject) -> Subject: if not images_dict: return subject - arguments: Dict[str, dict] = defaultdict(dict) + arguments: dict[str, dict] = defaultdict(dict) for image_name in images_dict: mean, std, seed = self.get_params(self.mean_range, self.std_range) arguments['mean'][image_name] = mean @@ -61,9 +59,9 @@ def apply_transform(self, subject: Subject) -> Subject: def get_params( self, - mean_range: Tuple[float, float], - std_range: Tuple[float, float], - ) -> Tuple[float, float, int]: + mean_range: tuple[float, float], + std_range: tuple[float, float], + ) -> tuple[float, float, int]: mean = self.sample_uniform(*mean_range) std = self.sample_uniform(*std_range) seed = self._get_random_seed() @@ -87,8 +85,8 @@ class Noise(IntensityTransform): def __init__( self, - mean: Union[float, Dict[str, float]], - std: Union[float, Dict[str, float]], + mean: Union[float, dict[str, float]], + std: Union[float, dict[str, float]], seed: Union[int, Sequence[int]], **kwargs, ): diff --git a/src/torchio/transforms/augmentation/intensity/random_spike.py b/src/torchio/transforms/augmentation/intensity/random_spike.py index 5557bcaa..03ea958a 100644 --- a/src/torchio/transforms/augmentation/intensity/random_spike.py +++ b/src/torchio/transforms/augmentation/intensity/random_spike.py @@ -1,7 +1,5 @@ from collections import defaultdict from numbers import Number -from typing import Dict -from typing import Tuple from typing import Union import numpy as np @@ -44,8 +42,8 @@ class RandomSpike(RandomTransform, IntensityTransform, FourierTransform): def __init__( self, - num_spikes: Union[int, Tuple[int, int]] = 1, - intensity: Union[float, Tuple[float, float]] = (1, 3), + num_spikes: Union[int, tuple[int, int]] = 1, + intensity: Union[float, tuple[float, float]] = (1, 3), **kwargs, ): super().__init__(**kwargs) @@ -53,7 +51,7 @@ def __init__( intensity, 'intensity_range', ) - self.num_spikes_range: Tuple[int, int] = self._parse_range( # type: ignore[assignment] + self.num_spikes_range: tuple[int, int] = self._parse_range( # type: ignore[assignment] num_spikes, 'num_spikes', min_constraint=0, @@ -65,7 +63,7 @@ def apply_transform(self, subject: Subject) -> Subject: if not images_dict: return subject - arguments: Dict[str, dict] = defaultdict(dict) + arguments: dict[str, dict] = defaultdict(dict) for image_name in images_dict: spikes_positions_param, intensity_param = self.get_params( self.num_spikes_range, @@ -80,9 +78,9 @@ def apply_transform(self, subject: Subject) -> Subject: def get_params( self, - num_spikes_range: Tuple[int, int], - intensity_range: Tuple[float, float], - ) -> Tuple[np.ndarray, float]: + num_spikes_range: tuple[int, int], + intensity_range: tuple[float, float], + ) -> tuple[np.ndarray, float]: ns_min, ns_max = num_spikes_range num_spikes_param = int(torch.randint(ns_min, ns_max + 1, (1,)).item()) intensity_param = self.sample_uniform(*intensity_range) @@ -111,8 +109,8 @@ class Spike(IntensityTransform, FourierTransform): def __init__( self, - spikes_positions: Union[np.ndarray, Dict[str, np.ndarray]], - intensity: Union[float, Dict[str, float]], + spikes_positions: Union[np.ndarray, dict[str, np.ndarray]], + intensity: Union[float, dict[str, float]], **kwargs, ): super().__init__(**kwargs) diff --git a/src/torchio/transforms/augmentation/intensity/random_swap.py b/src/torchio/transforms/augmentation/intensity/random_swap.py index 3eac412c..b2076a68 100644 --- a/src/torchio/transforms/augmentation/intensity/random_swap.py +++ b/src/torchio/transforms/augmentation/intensity/random_swap.py @@ -1,10 +1,7 @@ from __future__ import annotations from collections import defaultdict -from typing import Dict -from typing import List -from typing import Sequence -from typing import Tuple +from collections.abc import Sequence from typing import TypeVar from typing import Union @@ -18,7 +15,7 @@ from ...intensity_transform import IntensityTransform from .. import RandomTransform -TypeLocations = Sequence[Tuple[TypeTripletInt, TypeTripletInt]] +TypeLocations = Sequence[tuple[TypeTripletInt, TypeTripletInt]] TensorArray = TypeVar('TensorArray', np.ndarray, torch.Tensor) @@ -64,7 +61,7 @@ def get_params( tensor: torch.Tensor, patch_size: np.ndarray, num_iterations: int, - ) -> List[Tuple[TypeTripletInt, TypeTripletInt]]: + ) -> list[tuple[TypeTripletInt, TypeTripletInt]]: si, sj, sk = tensor.shape[-3:] spatial_shape = si, sj, sk # for mypy locations = [] @@ -93,7 +90,7 @@ def apply_transform(self, subject: Subject) -> Subject: if not images_dict: return subject - arguments: Dict[str, dict] = defaultdict(dict) + arguments: dict[str, dict] = defaultdict(dict) for name, image in images_dict.items(): locations = self.get_params( image.data, @@ -125,8 +122,8 @@ class Swap(IntensityTransform): def __init__( self, - patch_size: Union[TypeTripletInt, Dict[str, TypeTripletInt]], - locations: Union[TypeLocations, Dict[str, TypeLocations]], + patch_size: Union[TypeTripletInt, dict[str, TypeTripletInt]], + locations: Union[TypeLocations, dict[str, TypeLocations]], **kwargs, ): super().__init__(**kwargs) @@ -154,7 +151,7 @@ def apply_transform(self, subject: Subject) -> Subject: def _swap( tensor: torch.Tensor, patch_size: TypeTuple, - locations: List[Tuple[np.ndarray, np.ndarray]], + locations: list[tuple[np.ndarray, np.ndarray]], ) -> torch.Tensor: # Note this function modifies the input in-place tensor = tensor.clone() @@ -193,7 +190,7 @@ def _crop( def get_random_indices_from_shape( spatial_shape: Sequence[int], patch_size: Sequence[int], -) -> Tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray]: assert len(spatial_shape) == 3 assert len(patch_size) in (1, 3) shape_array = np.array(spatial_shape) diff --git a/src/torchio/transforms/augmentation/random_transform.py b/src/torchio/transforms/augmentation/random_transform.py index 83c27937..3b14a3c8 100644 --- a/src/torchio/transforms/augmentation/random_transform.py +++ b/src/torchio/transforms/augmentation/random_transform.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Tuple - import torch from ...typing import TypeRangeFloat @@ -29,13 +27,13 @@ def add_include_exclude(self, kwargs): def parse_degrees( self, degrees: TypeRangeFloat, - ) -> Tuple[float, float]: + ) -> tuple[float, float]: return self._parse_range(degrees, 'degrees') def parse_translation( self, translation: TypeRangeFloat, - ) -> Tuple[float, float]: + ) -> tuple[float, float]: return self._parse_range(translation, 'translation') @staticmethod diff --git a/src/torchio/transforms/augmentation/spatial/random_affine.py b/src/torchio/transforms/augmentation/spatial/random_affine.py index 973b2ef0..93797ea2 100644 --- a/src/torchio/transforms/augmentation/spatial/random_affine.py +++ b/src/torchio/transforms/augmentation/spatial/random_affine.py @@ -1,7 +1,6 @@ +from collections.abc import Sequence from numbers import Number from typing import Optional -from typing import Sequence -from typing import Tuple from typing import Union import numpy as np @@ -150,7 +149,7 @@ def get_params( degrees: TypeSextetFloat, translation: TypeSextetFloat, isotropic: bool, - ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: scaling_params = torch.as_tensor( self.sample_uniform_sextet(scales), dtype=torch.float64, diff --git a/src/torchio/transforms/augmentation/spatial/random_anisotropy.py b/src/torchio/transforms/augmentation/spatial/random_anisotropy.py index c50fe48c..f54cec52 100644 --- a/src/torchio/transforms/augmentation/spatial/random_anisotropy.py +++ b/src/torchio/transforms/augmentation/spatial/random_anisotropy.py @@ -1,5 +1,4 @@ import warnings -from typing import Tuple from typing import Union import torch @@ -48,7 +47,7 @@ class RandomAnisotropy(RandomTransform): def __init__( self, - axes: Union[int, Tuple[int, ...]] = (0, 1, 2), + axes: Union[int, tuple[int, ...]] = (0, 1, 2), downsampling: TypeRangeFloat = (1.5, 5), image_interpolation: str = 'linear', scalars_only: bool = True, @@ -67,15 +66,15 @@ def __init__( def get_params( self, - axes: Tuple[int, ...], - downsampling_range: Tuple[float, float], - ) -> Tuple[int, float]: + axes: tuple[int, ...], + downsampling_range: tuple[float, float], + ) -> tuple[int, float]: axis = axes[torch.randint(0, len(axes), (1,))] downsampling = self.sample_uniform(*downsampling_range) return axis, downsampling @staticmethod - def parse_axes(axes: Union[int, Tuple[int, ...]]): + def parse_axes(axes: Union[int, tuple[int, ...]]): axes_tuple = to_tuple(axes) for axis in axes_tuple: is_int = isinstance(axis, int) diff --git a/src/torchio/transforms/augmentation/spatial/random_elastic_deformation.py b/src/torchio/transforms/augmentation/spatial/random_elastic_deformation.py index cafb8514..5533ea79 100644 --- a/src/torchio/transforms/augmentation/spatial/random_elastic_deformation.py +++ b/src/torchio/transforms/augmentation/spatial/random_elastic_deformation.py @@ -1,7 +1,6 @@ import warnings +from collections.abc import Sequence from numbers import Number -from typing import Sequence -from typing import Tuple from typing import Union import numpy as np @@ -121,8 +120,8 @@ class RandomElasticDeformation(RandomTransform, SpatialTransform): def __init__( self, - num_control_points: Union[int, Tuple[int, int, int]] = 7, - max_displacement: Union[float, Tuple[float, float, float]] = 7.5, + num_control_points: Union[int, tuple[int, int, int]] = 7, + max_displacement: Union[float, tuple[float, float, float]] = 7.5, locked_borders: int = 2, image_interpolation: str = 'linear', label_interpolation: str = 'nearest', @@ -154,7 +153,7 @@ def __init__( @staticmethod def get_params( num_control_points: TypeTripletInt, - max_displacement: Tuple[float, float, float], + max_displacement: tuple[float, float, float], num_locked_borders: int, ) -> np.ndarray: grid_shape = num_control_points @@ -330,7 +329,7 @@ def _parse_num_control_points( def _parse_max_displacement( - max_displacement: Tuple[float, float, float], + max_displacement: tuple[float, float, float], ) -> None: for axis, number in enumerate(max_displacement): if not isinstance(number, Number) or number < 0: diff --git a/src/torchio/transforms/augmentation/spatial/random_flip.py b/src/torchio/transforms/augmentation/spatial/random_flip.py index 89a44ff8..e11882ed 100644 --- a/src/torchio/transforms/augmentation/spatial/random_flip.py +++ b/src/torchio/transforms/augmentation/spatial/random_flip.py @@ -1,5 +1,3 @@ -from typing import List -from typing import Tuple from typing import Union import numpy as np @@ -40,7 +38,7 @@ class RandomFlip(RandomTransform, SpatialTransform): def __init__( self, - axes: Union[int, Tuple[int, ...]] = 0, + axes: Union[int, tuple[int, ...]] = 0, flip_probability: float = 0.5, **kwargs, ): @@ -66,7 +64,7 @@ def apply_transform(self, subject: Subject) -> Subject: return transformed @staticmethod - def get_params(probability: float) -> List[bool]: + def get_params(probability: float) -> list[bool]: return (probability > torch.rand(3)).tolist() @@ -104,7 +102,7 @@ def inverse(self): return self -def _parse_axes(axes: Union[int, Tuple[int, ...]]): +def _parse_axes(axes: Union[int, tuple[int, ...]]): axes_tuple = to_tuple(axes) for axis in axes_tuple: is_int = isinstance(axis, int) diff --git a/src/torchio/transforms/data_parser.py b/src/torchio/transforms/data_parser.py index adb9f4ec..83354a2b 100644 --- a/src/torchio/transforms/data_parser.py +++ b/src/torchio/transforms/data_parser.py @@ -1,5 +1,5 @@ +from collections.abc import Sequence from typing import Optional -from typing import Sequence from typing import Union import nibabel as nib diff --git a/src/torchio/transforms/intensity_transform.py b/src/torchio/transforms/intensity_transform.py index f298be30..17d68e1c 100644 --- a/src/torchio/transforms/intensity_transform.py +++ b/src/torchio/transforms/intensity_transform.py @@ -1,6 +1,3 @@ -from typing import Dict -from typing import List - from ..data.image import Image from ..data.subject import Subject from .transform import Transform @@ -9,7 +6,7 @@ class IntensityTransform(Transform): """Transform that modifies voxel intensities only.""" - def get_images_dict(self, subject: Subject) -> Dict[str, Image]: + def get_images_dict(self, subject: Subject) -> dict[str, Image]: images_dict = subject.get_images_dict( intensity_only=True, include=self.include, @@ -17,7 +14,7 @@ def get_images_dict(self, subject: Subject) -> Dict[str, Image]: ) return images_dict - def get_images(self, subject: Subject) -> List[Image]: + def get_images(self, subject: Subject) -> list[Image]: images = subject.get_images( intensity_only=True, include=self.include, diff --git a/src/torchio/transforms/lambda_transform.py b/src/torchio/transforms/lambda_transform.py index 1cabc520..0db9b81b 100644 --- a/src/torchio/transforms/lambda_transform.py +++ b/src/torchio/transforms/lambda_transform.py @@ -1,5 +1,5 @@ +from collections.abc import Sequence from typing import Optional -from typing import Sequence import torch diff --git a/src/torchio/transforms/preprocessing/intensity/histogram_standardization.py b/src/torchio/transforms/preprocessing/intensity/histogram_standardization.py index 2851a003..2d4d7053 100644 --- a/src/torchio/transforms/preprocessing/intensity/histogram_standardization.py +++ b/src/torchio/transforms/preprocessing/intensity/histogram_standardization.py @@ -1,10 +1,8 @@ +from collections.abc import Iterable +from collections.abc import Sequence from pathlib import Path from typing import Callable -from typing import Dict -from typing import Iterable from typing import Optional -from typing import Sequence -from typing import Tuple from typing import Union import numpy as np @@ -19,7 +17,7 @@ DEFAULT_CUTOFF = 0.01, 0.99 STANDARD_RANGE = 0, 100 -TypeLandmarks = Union[TypePath, Dict[str, Union[TypePath, np.ndarray]]] +TypeLandmarks = Union[TypePath, dict[str, Union[TypePath, np.ndarray]]] class HistogramStandardization(NormalizationTransform): @@ -65,7 +63,7 @@ def __init__( self.args_names = ['landmarks', 'masking_method'] @staticmethod - def _parse_landmarks(landmarks: TypeLandmarks) -> Dict[str, np.ndarray]: + def _parse_landmarks(landmarks: TypeLandmarks) -> dict[str, np.ndarray]: if isinstance(landmarks, (str, Path)): path = Path(landmarks) if path.suffix not in ('.pt', '.pth'): @@ -104,7 +102,7 @@ def apply_normalization( def train( cls, images_paths: Sequence[TypePath], - cutoff: Optional[Tuple[float, float]] = None, + cutoff: Optional[tuple[float, float]] = None, mask_path: Optional[Union[Sequence[TypePath], TypePath]] = None, masking_function: Optional[Callable] = None, output_path: Optional[TypePath] = None, @@ -241,7 +239,7 @@ def _get_average_mapping(percentiles_database: np.ndarray) -> np.ndarray: return final_map -def _get_percentiles(percentiles_cutoff: Tuple[float, float]) -> np.ndarray: +def _get_percentiles(percentiles_cutoff: tuple[float, float]) -> np.ndarray: quartiles = np.arange(25, 100, 25).tolist() deciles = np.arange(10, 100, 10).tolist() all_percentiles = list(percentiles_cutoff) + quartiles + deciles @@ -253,7 +251,7 @@ def _normalize( tensor: torch.Tensor, landmarks: np.ndarray, mask: Optional[np.ndarray], - cutoff: Optional[Tuple[float, float]] = None, + cutoff: Optional[tuple[float, float]] = None, epsilon: float = 1e-5, ) -> torch.Tensor: cutoff_ = DEFAULT_CUTOFF if cutoff is None else cutoff diff --git a/src/torchio/transforms/preprocessing/intensity/mask.py b/src/torchio/transforms/preprocessing/intensity/mask.py index 4b8aaec7..85a3be16 100644 --- a/src/torchio/transforms/preprocessing/intensity/mask.py +++ b/src/torchio/transforms/preprocessing/intensity/mask.py @@ -1,6 +1,6 @@ import warnings +from collections.abc import Sequence from typing import Optional -from typing import Sequence import torch diff --git a/src/torchio/transforms/preprocessing/label/label_transform.py b/src/torchio/transforms/preprocessing/label/label_transform.py index 254cf15b..67ce59af 100644 --- a/src/torchio/transforms/preprocessing/label/label_transform.py +++ b/src/torchio/transforms/preprocessing/label/label_transform.py @@ -1,6 +1,3 @@ -from typing import Dict -from typing import List - from ....data.image import LabelMap from ....data.subject import Subject from ...transform import Transform @@ -9,7 +6,7 @@ class LabelTransform(Transform): """Transform that modifies label maps.""" - def get_images(self, subject: Subject) -> List[LabelMap]: + def get_images(self, subject: Subject) -> list[LabelMap]: images = subject.get_images( intensity_only=False, include=self.include, @@ -17,7 +14,7 @@ def get_images(self, subject: Subject) -> List[LabelMap]: ) return [im for im in images if isinstance(im, LabelMap)] - def get_images_dict(self, subject: Subject) -> Dict[str, LabelMap]: + def get_images_dict(self, subject: Subject) -> dict[str, LabelMap]: images = subject.get_images_dict( intensity_only=False, include=self.include, diff --git a/src/torchio/transforms/preprocessing/label/remap_labels.py b/src/torchio/transforms/preprocessing/label/remap_labels.py index 3ea68c36..da39b3c9 100644 --- a/src/torchio/transforms/preprocessing/label/remap_labels.py +++ b/src/torchio/transforms/preprocessing/label/remap_labels.py @@ -1,5 +1,3 @@ -from typing import Dict - from ...transform import TypeMaskingMethod from .label_transform import LabelTransform @@ -136,7 +134,7 @@ class RemapLabels(LabelTransform): def __init__( self, - remapping: Dict[int, int], + remapping: dict[int, int], masking_method: TypeMaskingMethod = None, **kwargs, ): diff --git a/src/torchio/transforms/preprocessing/label/remove_labels.py b/src/torchio/transforms/preprocessing/label/remove_labels.py index dbf82567..9fe6a305 100644 --- a/src/torchio/transforms/preprocessing/label/remove_labels.py +++ b/src/torchio/transforms/preprocessing/label/remove_labels.py @@ -1,4 +1,4 @@ -from typing import Sequence +from collections.abc import Sequence from ...transform import TypeMaskingMethod from .remap_labels import RemapLabels diff --git a/src/torchio/transforms/preprocessing/spatial/crop_or_pad.py b/src/torchio/transforms/preprocessing/spatial/crop_or_pad.py index 49e68edb..7152d9d6 100644 --- a/src/torchio/transforms/preprocessing/spatial/crop_or_pad.py +++ b/src/torchio/transforms/preprocessing/spatial/crop_or_pad.py @@ -1,7 +1,6 @@ import warnings +from collections.abc import Sequence from typing import Optional -from typing import Sequence -from typing import Tuple from typing import Union import numpy as np @@ -113,7 +112,7 @@ def __init__( self.labels = labels @staticmethod - def _bbox_mask(mask_volume: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + def _bbox_mask(mask_volume: np.ndarray) -> tuple[np.ndarray, np.ndarray]: """Return 6 coordinates of a 3D bounding box from a given mask. Taken from `this SO question `_. @@ -162,7 +161,7 @@ def _get_six_bounds_parameters( def _compute_cropping_padding_from_shapes( self, source_shape: TypeTripletInt, - ) -> Tuple[Optional[TypeSixBounds], Optional[TypeSixBounds]]: + ) -> tuple[Optional[TypeSixBounds], Optional[TypeSixBounds]]: diff_shape = np.array(self.target_shape) - source_shape cropping = -np.minimum(diff_shape, 0) @@ -182,7 +181,7 @@ def _compute_cropping_padding_from_shapes( def _compute_center_crop_or_pad( self, subject: Subject, - ) -> Tuple[Optional[TypeSixBounds], Optional[TypeSixBounds]]: + ) -> tuple[Optional[TypeSixBounds], Optional[TypeSixBounds]]: source_shape = subject.spatial_shape parameters = self._compute_cropping_padding_from_shapes(source_shape) padding_params, cropping_params = parameters @@ -191,7 +190,7 @@ def _compute_center_crop_or_pad( def _compute_mask_center_crop_or_pad( self, subject: Subject, - ) -> Tuple[Optional[TypeSixBounds], Optional[TypeSixBounds]]: + ) -> tuple[Optional[TypeSixBounds], Optional[TypeSixBounds]]: if self.mask_name not in subject: message = ( f'Mask name "{self.mask_name}"' diff --git a/src/torchio/transforms/preprocessing/spatial/pad.py b/src/torchio/transforms/preprocessing/spatial/pad.py index deaaae5f..0539d7ad 100644 --- a/src/torchio/transforms/preprocessing/spatial/pad.py +++ b/src/torchio/transforms/preprocessing/spatial/pad.py @@ -1,6 +1,5 @@ import warnings from numbers import Number -from typing import Dict from typing import Union import nibabel as nib @@ -91,7 +90,7 @@ def apply_transform(self, subject: Subject) -> Subject: new_origin = nib.affines.apply_affine(image.affine, -np.array(low)) new_affine = image.affine.copy() new_affine[:3, 3] = new_origin - kwargs: Dict[str, Union[str, float]] + kwargs: dict[str, Union[str, float]] if isinstance(self.padding_mode, Number): kwargs = { 'mode': 'constant', diff --git a/src/torchio/transforms/preprocessing/spatial/resample.py b/src/torchio/transforms/preprocessing/spatial/resample.py index 7b5b4614..0079a4a3 100644 --- a/src/torchio/transforms/preprocessing/spatial/resample.py +++ b/src/torchio/transforms/preprocessing/spatial/resample.py @@ -1,9 +1,8 @@ from collections.abc import Iterable +from collections.abc import Sized from numbers import Number from pathlib import Path from typing import Optional -from typing import Sized -from typing import Tuple from typing import Union import numpy as np @@ -19,7 +18,7 @@ from ....typing import TypeTripletFloat from ...spatial_transform import SpatialTransform -TypeSpacing = Union[float, Tuple[float, float, float]] +TypeSpacing = Union[float, tuple[float, float, float]] class Resample(SpatialTransform): @@ -105,7 +104,7 @@ def __init__( ] @staticmethod - def _parse_spacing(spacing: TypeSpacing) -> Tuple[float, float, float]: + def _parse_spacing(spacing: TypeSpacing) -> tuple[float, float, float]: result: Iterable if isinstance(spacing, Iterable) and len(spacing) == 3: result = spacing diff --git a/src/torchio/transforms/spatial_transform.py b/src/torchio/transforms/spatial_transform.py index 067a315c..616c192a 100644 --- a/src/torchio/transforms/spatial_transform.py +++ b/src/torchio/transforms/spatial_transform.py @@ -1,5 +1,3 @@ -from typing import List - from ..data import Image from ..data.subject import Subject from .transform import Transform @@ -8,7 +6,7 @@ class SpatialTransform(Transform): """Transform that modifies image bounds or voxels positions.""" - def get_images(self, subject: Subject) -> List[Image]: + def get_images(self, subject: Subject) -> list[Image]: images = subject.get_images( intensity_only=False, include=self.include, diff --git a/src/torchio/transforms/transform.py b/src/torchio/transforms/transform.py index 7d0ef894..8f676999 100644 --- a/src/torchio/transforms/transform.py +++ b/src/torchio/transforms/transform.py @@ -3,12 +3,9 @@ import warnings from abc import ABC from abc import abstractmethod +from collections.abc import Sequence from contextlib import contextmanager -from typing import Dict -from typing import List from typing import Optional -from typing import Sequence -from typing import Tuple from typing import TypeVar from typing import Union @@ -33,7 +30,7 @@ from .interpolation import Interpolation from .interpolation import get_sitk_interpolator -TypeSixBounds = Tuple[int, int, int, int, int, int] +TypeSixBounds = tuple[int, int, int, int, int, int] TypeBounds = Union[ int, TypeTripletInt, @@ -102,7 +99,7 @@ def __init__( include: TypeKeys = None, exclude: TypeKeys = None, keys: TypeKeys = None, - keep: Optional[Dict[str, str]] = None, + keep: Optional[dict[str, str]] = None, parse_input: bool = True, label_keys: TypeKeys = None, ): @@ -126,7 +123,7 @@ def __init__( # args_names is the sequence of parameters from self that need to be # passed to a non-random version of a random transform. They are also # used to invert invertible transforms - self.args_names: List[str] = [] + self.args_names: list[str] = [] def __call__(self, data: InputType) -> InputType: """Transform data and return a result of the same type. @@ -247,12 +244,12 @@ def parse_params(self, params, around, name, make_ranges=True, **kwargs): @staticmethod def _parse_range( - nums_range: Union[TypeNumber, Tuple[TypeNumber, TypeNumber]], + nums_range: Union[TypeNumber, tuple[TypeNumber, TypeNumber]], name: str, min_constraint: Optional[TypeNumber] = None, max_constraint: Optional[TypeNumber] = None, type_constraint: Optional[type] = None, - ) -> Tuple[TypeNumber, TypeNumber]: + ) -> tuple[TypeNumber, TypeNumber]: r"""Adapted from :class:`torchvision.transforms.RandomRotation`. Args: @@ -382,7 +379,7 @@ def parse_include_and_exclude_keys( include: TypeKeys, exclude: TypeKeys, label_keys: TypeKeys, - ) -> Tuple[TypeKeys, TypeKeys]: + ) -> tuple[TypeKeys, TypeKeys]: if include is not None and exclude is not None: raise ValueError('Include and exclude cannot both be specified') Transform.validate_keys_sequence(include, 'include') diff --git a/src/torchio/typing.py b/src/torchio/typing.py index 78898fad..5fc7c53c 100644 --- a/src/torchio/typing.py +++ b/src/torchio/typing.py @@ -1,8 +1,7 @@ +from collections.abc import Sequence from pathlib import Path from typing import Callable from typing import Optional -from typing import Sequence -from typing import Tuple from typing import Union import numpy as np @@ -13,25 +12,25 @@ TypeNumber = Union[int, float] TypeKeys = Optional[Sequence[str]] TypeData = Union[torch.Tensor, np.ndarray] -TypeDataAffine = Tuple[torch.Tensor, np.ndarray] +TypeDataAffine = tuple[torch.Tensor, np.ndarray] TypeSlice = Union[int, slice] -TypeDoubletInt = Tuple[int, int] -TypeTripletInt = Tuple[int, int, int] -TypeQuartetInt = Tuple[int, int, int, int] -TypeSextetInt = Tuple[int, int, int, int, int, int] +TypeDoubletInt = tuple[int, int] +TypeTripletInt = tuple[int, int, int] +TypeQuartetInt = tuple[int, int, int, int] +TypeSextetInt = tuple[int, int, int, int, int, int] -TypeDoubleFloat = Tuple[float, float] -TypeTripletFloat = Tuple[float, float, float] -TypeSextetFloat = Tuple[float, float, float, float, float, float] +TypeDoubleFloat = tuple[float, float] +TypeTripletFloat = tuple[float, float, float] +TypeSextetFloat = tuple[float, float, float, float, float, float] TypeTuple = Union[int, TypeTripletInt] TypeRangeInt = Union[int, TypeDoubletInt] TypeSpatialShape = Union[int, TypeTripletInt] -TypeRangeFloat = Union[float, Tuple[float, float]] +TypeRangeFloat = Union[float, tuple[float, float]] TypeCallable = Callable[[torch.Tensor], torch.Tensor] -TypeDirection2D = Tuple[float, float, float, float] -TypeDirection3D = Tuple[ +TypeDirection2D = tuple[float, float, float, float] +TypeDirection3D = tuple[ float, float, float, diff --git a/src/torchio/utils.py b/src/torchio/utils.py index e206af2e..b5125217 100644 --- a/src/torchio/utils.py +++ b/src/torchio/utils.py @@ -7,14 +7,11 @@ import shutil import sys import tempfile +from collections.abc import Iterable +from collections.abc import Sequence from pathlib import Path from typing import Any -from typing import Dict -from typing import Iterable -from typing import List from typing import Optional -from typing import Sequence -from typing import Tuple from typing import Union import numpy as np @@ -34,7 +31,7 @@ def to_tuple( value: Any, length: int = 1, -) -> Tuple[TypeNumber, ...]: +) -> tuple[TypeNumber, ...]: """Convert variable to tuple of length n. Example: @@ -64,7 +61,7 @@ def to_tuple( def get_stem( path: Union[TypePath, Sequence[TypePath]], -) -> Union[str, List[str]]: +) -> Union[str, list[str]]: """Get stem of path or paths. Example: @@ -84,7 +81,7 @@ def _get_stem(path_string: TypePath) -> str: def create_dummy_dataset( num_images: int, - size_range: Tuple[int, int], + size_range: tuple[int, int], directory: Optional[TypePath] = None, suffix: str = '.nii.gz', force: bool = False, @@ -103,7 +100,7 @@ def create_dummy_dataset( shutil.rmtree(images_dir) shutil.rmtree(labels_dir) - subjects: List[Subject] = [] + subjects: list[Subject] = [] if images_dir.is_dir(): for i in trange(num_images): image_path = images_dir / f'image_{i}{suffix}' @@ -220,7 +217,7 @@ def get_major_sitk_version() -> int: return major_version -def history_collate(batch: Sequence, collate_transforms=True) -> Dict: +def history_collate(batch: Sequence, collate_transforms=True) -> dict: attr = constants.HISTORY if collate_transforms else 'applied_transforms' # Adapted from # https://github.com/romainVala/torchQC/blob/master/segmentation/collate_functions.py @@ -238,7 +235,7 @@ def history_collate(batch: Sequence, collate_transforms=True) -> Dict: return dictionary -def get_subclasses(target_class: type) -> List[type]: +def get_subclasses(target_class: type) -> list[type]: subclasses = target_class.__subclasses__() subclasses += sum((get_subclasses(cls) for cls in subclasses), []) return subclasses @@ -248,7 +245,7 @@ def get_first_item(data_loader: DataLoader): return next(iter(data_loader)) -def get_batch_images_and_size(batch: Dict) -> Tuple[List[str], int]: +def get_batch_images_and_size(batch: dict) -> tuple[list[str], int]: """Get number of images and images names in a batch. Args: @@ -269,7 +266,7 @@ def get_batch_images_and_size(batch: Dict) -> Tuple[List[str], int]: return names, size -def get_subjects_from_batch(batch: Dict) -> List: +def get_subjects_from_batch(batch: dict) -> list: """Get list of subjects from collated batch. Args: @@ -313,7 +310,7 @@ def get_subjects_from_batch(batch: Dict) -> List: def add_images_from_batch( - subjects: List, + subjects: list, tensor: torch.Tensor, class_=None, name='prediction', diff --git a/tests/utils.py b/tests/utils.py index 989c8c2e..6046726b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -4,10 +4,9 @@ import shutil import tempfile import unittest +from collections.abc import Sequence from pathlib import Path from random import shuffle -from typing import Sequence -from typing import Set import numpy as np import pytest @@ -143,7 +142,7 @@ def get_subject_with_labels(self, labels): ) @staticmethod - def get_unique_labels(data: torch.Tensor) -> Set[int]: + def get_unique_labels(data: torch.Tensor) -> set[int]: labels = data.unique().tolist() return set(labels)