diff --git a/.github/workflows/pythonapp-gpu.yml b/.github/workflows/pythonapp-gpu.yml index c2c0164306..8ec6bdd3c7 100644 --- a/.github/workflows/pythonapp-gpu.yml +++ b/.github/workflows/pythonapp-gpu.yml @@ -19,10 +19,10 @@ jobs: strategy: matrix: environment: + - "PT19+CUDA114" - "PT17+CUDA102" - "PT18+CUDA102" - "PT18+CUDA112" - - "PT19+CUDA114" - "PT110+CUDA116" - "PT110+CUDA102" - "PT111+CUDA102" diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index 0be7feb1e5..13fa47b0af 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -18,7 +18,7 @@ from torch.utils.data._utils.collate import np_str_obj_array_pattern from monai.config import DtypeLike, KeysCollection, PathLike -from monai.data.utils import correct_nifti_header_if_necessary, is_supported_format +from monai.data.utils import correct_nifti_header_if_necessary, is_supported_format, orientation_ras_lps from monai.transforms.utility.array import EnsureChannelFirst from monai.utils import ensure_tuple, ensure_tuple_rep, optional_import, require_pkg @@ -161,6 +161,8 @@ class ITKReader(ImageReader): This option does not affect the metadata. series_meta: whether to load the metadata of the DICOM series (using the metadata from the first slice). This flag is checked only when loading DICOM series. Default is ``False``. + affine_lps_to_ras: whether to convert the affine matrix from "LPS" to "RAS". Defaults to ``True``. + Set to ``True`` to be consistent with ``NibabelReader``, otherwise the affine matrix remains in the ITK convention. kwargs: additional args for `itk.imread` API. more details about available args: https://github.com/InsightSoftwareConsortium/ITK/blob/master/Wrapping/Generators/Python/itk/support/extras.py @@ -172,6 +174,7 @@ def __init__( series_name: str = "", reverse_indexing: bool = False, series_meta: bool = False, + affine_lps_to_ras: bool = True, **kwargs, ): super().__init__() @@ -180,6 +183,7 @@ def __init__( self.series_name = series_name self.reverse_indexing = reverse_indexing self.series_meta = series_meta + self.affine_lps_to_ras = affine_lps_to_ras def verify_suffix(self, filename: Union[Sequence[PathLike], PathLike]) -> bool: """ @@ -261,7 +265,7 @@ def get_data(self, img): data = self._get_array_data(i) img_array.append(data) header = self._get_meta_dict(i) - header["original_affine"] = self._get_affine(i) + header["original_affine"] = self._get_affine(i, self.affine_lps_to_ras) header["affine"] = header["original_affine"].copy() header["spatial_shape"] = self._get_spatial_shape(i) if self.channel_dim is None: # default to "no_channel" or -1 @@ -286,13 +290,14 @@ def _get_meta_dict(self, img) -> Dict: meta_dict["spacing"] = np.asarray(img.GetSpacing()) return meta_dict - def _get_affine(self, img): + def _get_affine(self, img, lps_to_ras: bool = True): """ Get or construct the affine matrix of the image, it can be used to correct spacing, orientation or execute spatial transforms. Args: img: an ITK image object loaded from an image file. + lps_to_ras: whether to convert the affine matrix from "LPS" to "RAS". Defaults to True. """ direction = itk.array_from_matrix(img.GetDirection()) @@ -304,8 +309,8 @@ def _get_affine(self, img): affine: np.ndarray = np.eye(sr + 1) affine[:sr, :sr] = direction[:sr, :sr] @ np.diag(spacing[:sr]) affine[:sr, -1] = origin[:sr] - flip_diag = [[-1, 1], [-1, -1, 1], [-1, -1, 1, 1]][sr - 1] # itk to nibabel affine - affine = np.diag(flip_diag) @ affine + if lps_to_ras: + affine = orientation_ras_lps(affine) return affine def _get_spatial_shape(self, img): diff --git a/monai/data/image_writer.py b/monai/data/image_writer.py index e9f753fb34..cf9ef90e8c 100644 --- a/monai/data/image_writer.py +++ b/monai/data/image_writer.py @@ -354,10 +354,13 @@ class ITKWriter(ImageWriter): """ - def __init__(self, output_dtype: DtypeLike = np.float32, **kwargs): + def __init__(self, output_dtype: DtypeLike = np.float32, affine_lps_to_ras: bool = True, **kwargs): """ Args: output_dtype: output data type. + affine_lps_to_ras: whether to convert the affine matrix from "LPS" to "RAS". Defaults to ``True``. + Set to ``True`` to be consistent with ``NibabelWriter``, + otherwise the affine matrix is assumed already in the ITK convention. kwargs: keyword arguments passed to ``ImageWriter``. The constructor will create ``self.output_dtype`` internally. @@ -366,7 +369,9 @@ def __init__(self, output_dtype: DtypeLike = np.float32, **kwargs): - user-specified ``affine`` should be set in ``set_metadata``, - user-specified ``channel_dim`` should be set in ``set_data_array``. """ - super().__init__(output_dtype=output_dtype, affine=None, channel_dim=0, **kwargs) + super().__init__( + output_dtype=output_dtype, affine_lps_to_ras=affine_lps_to_ras, affine=None, channel_dim=0, **kwargs + ) def set_data_array( self, data_array: NdarrayOrTensor, channel_dim: Optional[int] = 0, squeeze_end_dims: bool = True, **kwargs @@ -432,7 +437,12 @@ def write(self, filename: PathLike, verbose: bool = False, **kwargs): """ super().write(filename, verbose=verbose) self.data_obj = self.create_backend_obj( - self.data_obj, channel_dim=self.channel_dim, affine=self.affine, dtype=self.output_dtype, **kwargs # type: ignore + self.data_obj, + channel_dim=self.channel_dim, + affine=self.affine, + dtype=self.output_dtype, # type: ignore + affine_lps_to_ras=self.affine_lps_to_ras, # type: ignore + **kwargs, ) itk.imwrite( self.data_obj, filename, compression=kwargs.pop("compression", False), imageio=kwargs.pop("imageio", None) @@ -445,6 +455,7 @@ def create_backend_obj( channel_dim: Optional[int] = 0, affine: Optional[NdarrayOrTensor] = None, dtype: DtypeLike = np.float32, + affine_lps_to_ras: bool = True, **kwargs, ): """ @@ -455,6 +466,9 @@ def create_backend_obj( channel_dim: channel dimension of the data array. This is used to create a Vector Image if it is not ``None``. affine: affine matrix of the data array. This is used to compute `spacing`, `direction` and `origin`. dtype: output data type. + affine_lps_to_ras: whether to convert the affine matrix from "LPS" to "RAS". Defaults to ``True``. + Set to ``True`` to be consistent with ``NibabelWriter``, + otherwise the affine matrix is assumed already in the ITK convention. kwargs: keyword arguments. Current `itk.GetImageFromArray` will read ``ttype`` from this dictionary. see also: @@ -472,7 +486,8 @@ def create_backend_obj( if affine is None: affine = np.eye(d + 1, dtype=np.float64) _affine = convert_data_type(affine, np.ndarray)[0] - _affine = orientation_ras_lps(to_affine_nd(d, _affine)) + if affine_lps_to_ras: + _affine = orientation_ras_lps(to_affine_nd(d, _affine)) spacing = affine_to_spacing(_affine, r=d) _direction: np.ndarray = np.diag(1 / spacing) _direction = _affine[:d, :d] @ _direction diff --git a/tests/test_bundle_run.py b/tests/test_bundle_run.py index b0ce353240..618a9a8ebe 100644 --- a/tests/test_bundle_run.py +++ b/tests/test_bundle_run.py @@ -66,7 +66,9 @@ def test_shape(self, config_file, expected_shape): # test with `monai.bundle` as CLI entry directly cmd = f"-m monai.bundle run evaluator --postprocessing#transforms#2#output_postfix seg {override}" la = [f"{sys.executable}"] + cmd.split(" ") + ["--meta_file", meta_file] + ["--config_file", config_file] - ret = subprocess.check_call(la + ["--args_file", def_args_file]) + test_env = os.environ.copy() + print(f"CUDA_VISIBLE_DEVICES in {__file__}", test_env.get("CUDA_VISIBLE_DEVICES")) + ret = subprocess.check_call(la + ["--args_file", def_args_file], env=test_env) self.assertEqual(ret, 0) self.assertTupleEqual(saver(os.path.join(tempdir, "image", "image_seg.nii.gz")).shape, expected_shape) @@ -74,7 +76,7 @@ def test_shape(self, config_file, expected_shape): cmd = "-m fire monai.bundle.scripts run --runner_id evaluator" cmd += f" --evaluator#amp False {override}" la = [f"{sys.executable}"] + cmd.split(" ") + ["--meta_file", meta_file] + ["--config_file", config_file] - ret = subprocess.check_call(la) + ret = subprocess.check_call(la, env=test_env) self.assertEqual(ret, 0) self.assertTupleEqual(saver(os.path.join(tempdir, "image", "image_trans.nii.gz")).shape, expected_shape) diff --git a/tests/testing_data/inference.json b/tests/testing_data/inference.json index b96968496d..6cc6de88ef 100644 --- a/tests/testing_data/inference.json +++ b/tests/testing_data/inference.json @@ -1,5 +1,5 @@ { - "device": "$torch.device('cuda' if torch.cuda.is_available() else 'cpu')", + "device": "$torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')", "network_def": { "_target_": "UNet", "spatial_dims": 3, diff --git a/tests/testing_data/inference.yaml b/tests/testing_data/inference.yaml index 58eeca8191..eb2870ee03 100644 --- a/tests/testing_data/inference.yaml +++ b/tests/testing_data/inference.yaml @@ -1,5 +1,5 @@ --- -device: "$torch.device('cuda' if torch.cuda.is_available() else 'cpu')" +device: "$torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')" network_def: _target_: UNet spatial_dims: 3