Repository: Project-MONAI/MONAI

Issue:
ITKReader silently changes the affine matrix to RAS without informing the users
**Describe the bug**
The function _get_affine(self, img) of ITKReader() in monai/data/image_reader.py
silently flips the affine matrix to match with the format of nibabel.
See line 307: flip_diag = [[-1, 1], [-1, -1, 1], [-1, -1, 1, 1]][sr - 1]  # itk to nibabel affine

However, the user is not aware of this operation unless they check the source code of ITKReader().
If they thought the affine was the raw affine, it would cause mistakes.

**Additional context**
Three possible solutions:
1.  let the operation of flipping affine matrix be a separate transform
2. set a bool input for ITKReader() to indicate whether to flip the affine.
3. add a key in image_mata_dict to indicate that the image affine has been flipped.

There might be other solutions, but users need to know this operation has been done.
monai.bundle run tests OOM
**Describe the bug**
```
2022-03-14T15:49:57.6853812Z 2022-03-14 15:49:57,684 - INFO - 
2022-03-14T15:49:57.6854284Z --- input summary of monai.bundle.scripts.run ---
2022-03-14T15:49:57.6854826Z 2022-03-14 15:49:57,684 - INFO - > config_file: '/__w/MONAI/MONAI/tests/testing_data/inference.json'
2022-03-14T15:49:57.6855287Z 2022-03-14 15:49:57,685 - INFO - > runner_id: 'evaluator'
2022-03-14T15:49:57.6856056Z 2022-03-14 15:49:57,685 - INFO - > meta_file: '/tmp/tmp5l91p7s9/meta.yaml'
2022-03-14T15:49:57.6856589Z 2022-03-14 15:49:57,685 - INFO - > postprocessing#transforms#2#output_postfix: 'seg'
2022-03-14T15:49:57.6857092Z 2022-03-14 15:49:57,685 - INFO - > network: '%/tmp/tmp5l91p7s9/override1.JSON#move_net'
2022-03-14T15:49:57.6857614Z 2022-03-14 15:49:57,685 - INFO - > dataset#_target_: '%/tmp/tmp5l91p7s9/jsons/override2.JSON'
2022-03-14T15:49:57.6858016Z 2022-03-14 15:49:57,685 - INFO - ---
2022-03-14T15:49:57.6858694Z 
2022-03-14T15:49:57.6858700Z 
2022-03-14T15:50:02.1080152Z .Current run is terminating due to exception: CUDA out of memory. Tried to allocate 28.00 MiB (GPU 0; 15.75 GiB total capacity; 235.40 MiB already allocated; 9.88 MiB free; 296.00 MiB reserved in total by PyTorch) If reserved memory is >> allocated memory try setting max_split_size_mb to avoid fragmentation.  See documentation for Memory Management and PYTORCH_CUDA_ALLOC_CONF
2022-03-14T15:50:02.1629767Z Engine run is terminating due to exception: CUDA out of memory. Tried to allocate 28.00 MiB (GPU 0; 15.75 GiB total capacity; 235.40 MiB already allocated; 9.88 MiB free; 296.00 MiB reserved in total by PyTorch) If reserved memory is >> allocated memory try setting max_split_size_mb to avoid fragmentation.  See documentation for Memory Management and PYTORCH_CUDA_ALLOC_CONF
2022-03-14T15:50:02.1707910Z Traceback (most recent call last):
2022-03-14T15:50:02.1708299Z   File "/usr/lib/python3.7/runpy.py", line 193, in _run_module_as_main
2022-03-14T15:50:02.1708789Z     "__main__", mod_spec)
2022-03-14T15:50:02.1709136Z   File "/usr/lib/python3.7/runpy.py", line 85, in _run_code
2022-03-14T15:50:02.1709412Z     exec(code, run_globals)
2022-03-14T15:50:02.1709726Z   File "/__w/MONAI/MONAI/monai/bundle/__main__.py", line 19, in <module>
2022-03-14T15:50:02.1710002Z     fire.Fire()
2022-03-14T15:50:02.1710710Z   File "/usr/local/lib/python3.7/dist-packages/fire/core.py", line 141, in Fire
2022-03-14T15:50:02.1711154Z     component_trace = _Fire(component, args, parsed_flag_args, context, name)
2022-03-14T15:50:02.1712081Z   File "/usr/local/lib/python3.7/dist-packages/fire/core.py", line 471, in _Fire
2022-03-14T15:50:02.1712782Z     target=component.__name__)
2022-03-14T15:50:02.1713640Z   File "/usr/local/lib/python3.7/dist-packages/fire/core.py", line 681, in _CallAndUpdateTrace
2022-03-14T15:50:02.1713990Z     component = fn(*varargs, **kwargs)
2022-03-14T15:50:02.1714319Z   File "/__w/MONAI/MONAI/monai/bundle/scripts.py", line 122, in run
2022-03-14T15:50:02.1714595Z     workflow.run()
2022-03-14T15:50:02.1714916Z   File "/__w/MONAI/MONAI/monai/engines/evaluator.py", line 137, in run
2022-03-14T15:50:02.1715209Z     super().run()
2022-03-14T15:50:02.1715488Z   File "/__w/MONAI/MONAI/monai/engines/workflow.py", line 282, in run
2022-03-14T15:50:02.1715870Z     super().run(data=self.data_loader, max_epochs=self.state.max_epochs)
2022-03-14T15:50:02.1716861Z   File "/usr/local/lib/python3.7/dist-packages/ignite/engine/engine.py", line 704, in run
2022-03-14T15:50:02.1717373Z     return self._internal_run()
2022-03-14T15:50:02.1717814Z   File "/usr/local/lib/python3.7/dist-packages/ignite/engine/engine.py", line 783, in _internal_run
2022-03-14T15:50:02.1718151Z     self._handle_exception(e)
2022-03-14T15:50:02.1718614Z   File "/usr/local/lib/python3.7/dist-packages/ignite/engine/engine.py", line 466, in _handle_exception
2022-03-14T15:50:02.1718911Z     raise e
2022-03-14T15:50:02.1719522Z   File "/usr/local/lib/python3.7/dist-packages/ignite/engine/engine.py", line 753, in _internal_run
2022-03-14T15:50:02.1720121Z     time_taken = self._run_once_on_dataset()
2022-03-14T15:50:02.1720787Z   File "/usr/local/lib/python3.7/dist-packages/ignite/engine/engine.py", line 854, in _run_once_on_dataset
2022-03-14T15:50:02.1721122Z     self._handle_exception(e)
2022-03-14T15:50:02.1721543Z   File "/usr/local/lib/python3.7/dist-packages/ignite/engine/engine.py", line 466, in _handle_exception
2022-03-14T15:50:02.1721975Z     raise e
2022-03-14T15:50:02.1722390Z   File "/usr/local/lib/python3.7/dist-packages/ignite/engine/engine.py", line 840, in _run_once_on_dataset
2022-03-14T15:50:02.1722788Z     self.state.output = self._process_function(self, self.state.batch)
2022-03-14T15:50:02.1723157Z   File "/__w/MONAI/MONAI/monai/engines/evaluator.py", line 265, in _iteration
2022-03-14T15:50:02.1723698Z     engine.state.output[Keys.PRED] = self.inferer(inputs, self.network, *args, **kwargs)  # type: ignore
2022-03-14T15:50:02.1724081Z   File "/__w/MONAI/MONAI/monai/inferers/inferer.py", line 182, in __call__
2022-03-14T15:50:02.1724320Z     **kwargs,
2022-03-14T15:50:02.1724612Z   File "/__w/MONAI/MONAI/monai/inferers/utils.py", line 134, in sliding_window_inference
2022-03-14T15:50:02.1724993Z     seg_prob = predictor(window_data, *args, **kwargs).to(device)  # batched patch segmentation
2022-03-14T15:50:02.1725484Z   File "/usr/local/lib/python3.7/dist-packages/torch/nn/modules/module.py", line 1110, in _call_impl
2022-03-14T15:50:02.1725821Z     return forward_call(*input, **kwargs)
2022-03-14T15:50:02.1726098Z   File "/__w/MONAI/MONAI/monai/networks/nets/unet.py", line 281, in forward
2022-03-14T15:50:02.1726368Z     x = self.model(x)
2022-03-14T15:50:02.1726796Z   File "/usr/local/lib/python3.7/dist-packages/torch/nn/modules/module.py", line 1110, in _call_impl
2022-03-14T15:50:02.1727102Z     return forward_call(*input, **kwargs)
2022-03-14T15:50:02.1727538Z   File "/usr/local/lib/python3.7/dist-packages/torch/nn/modules/container.py", line 141, in forward
2022-03-14T15:50:02.1727827Z     input = module(input)
2022-03-14T15:50:02.1728252Z   File "/usr/local/lib/python3.7/dist-packages/torch/nn/modules/module.py", line 1110, in _call_impl
2022-03-14T15:50:02.1728578Z     return forward_call(*input, **kwargs)
2022-03-14T15:50:02.1728874Z   File "/__w/MONAI/MONAI/monai/networks/layers/simplelayers.py", line 128, in forward
2022-03-14T15:50:02.1729161Z     y = self.submodule(x)
2022-03-14T15:50:02.1729566Z   File "/usr/local/lib/python3.7/dist-packages/torch/nn/modules/module.py", line 1110, in _call_impl
2022-03-14T15:50:02.1730260Z     return forward_call(*input, **kwargs)
2022-03-14T15:50:02.1730752Z   File "/usr/local/lib/python3.7/dist-packages/torch/nn/modules/container.py", line 141, in forward
2022-03-14T15:50:02.1731106Z     input = module(input)
2022-03-14T15:50:02.1731581Z   File "/usr/local/lib/python3.7/dist-packages/torch/nn/modules/module.py", line 1110, in _call_impl
2022-03-14T15:50:02.1731929Z     return forward_call(*input, **kwargs)
2022-03-14T15:50:02.1732421Z   File "/usr/local/lib/python3.7/dist-packages/torch/nn/modules/container.py", line 141, in forward
2022-03-14T15:50:02.1732755Z     input = module(input)
2022-03-14T15:50:02.1733545Z   File "/usr/local/lib/python3.7/dist-packages/torch/nn/modules/module.py", line 1110, in _call_impl
2022-03-14T15:50:02.1733875Z     return forward_call(*input, **kwargs)
2022-03-14T15:50:02.1734178Z   File "/__w/MONAI/MONAI/monai/networks/blocks/convolutions.py", line 325, in forward
2022-03-14T15:50:02.1734543Z     cx: torch.Tensor = self.conv(x)  # apply x to sequence of operations
2022-03-14T15:50:02.1735014Z   File "/usr/local/lib/python3.7/dist-packages/torch/nn/modules/module.py", line 1110, in _call_impl
2022-03-14T15:50:02.1735353Z     return forward_call(*input, **kwargs)
2022-03-14T15:50:02.1735797Z   File "/usr/local/lib/python3.7/dist-packages/torch/nn/modules/container.py", line 141, in forward
2022-03-14T15:50:02.1736097Z     input = module(input)
2022-03-14T15:50:02.1736531Z   File "/usr/local/lib/python3.7/dist-packages/torch/nn/modules/module.py", line 1110, in _call_impl
2022-03-14T15:50:02.1737093Z     return forward_call(*input, **kwargs)
2022-03-14T15:50:02.1737548Z   File "/usr/local/lib/python3.7/dist-packages/torch/nn/modules/container.py", line 141, in forward
2022-03-14T15:50:02.1737857Z     input = module(input)
2022-03-14T15:50:02.1738254Z   File "/usr/local/lib/python3.7/dist-packages/torch/nn/modules/module.py", line 1110, in _call_impl
2022-03-14T15:50:02.1738648Z     return forward_call(*input, **kwargs)
2022-03-14T15:50:02.1739068Z   File "/usr/local/lib/python3.7/dist-packages/torch/nn/modules/container.py", line 141, in forward
2022-03-14T15:50:02.1739373Z     input = module(input)
2022-03-14T15:50:02.1739790Z   File "/usr/local/lib/python3.7/dist-packages/torch/nn/modules/module.py", line 1110, in _call_impl
2022-03-14T15:50:02.1740094Z     return forward_call(*input, **kwargs)
2022-03-14T15:50:02.1740531Z   File "/usr/local/lib/python3.7/dist-packages/torch/nn/modules/activation.py", line 1111, in forward
2022-03-14T15:50:02.1740842Z     return F.prelu(input, self.weight)
2022-03-14T15:50:02.1741437Z RuntimeError: CUDA out of memory. Tried to allocate 28.00 MiB (GPU 0; 15.75 GiB total capacity; 235.40 MiB already allocated; 9.88 MiB free; 296.00 MiB reserved in total by PyTorch) If reserved memory is >> allocated memory try setting max_split_size_mb to avoid fragmentation.  See documentation for Memory Management and PYTORCH_CUDA_ALLOC_CONF
```


Discussion:

Resolving patch:
diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py
--- 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
--- 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


Test patch:
diff --git a/tests/test_bundle_run.py b/tests/test_bundle_run.py
--- 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
--- 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
--- 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