Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pythonapp-gpu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ jobs:
strategy:
matrix:
environment:
- "PT19+CUDA114"
- "PT17+CUDA102"
- "PT18+CUDA102"
- "PT18+CUDA112"
- "PT19+CUDA114"
- "PT110+CUDA116"
- "PT110+CUDA102"
- "PT111+CUDA102"
Expand Down
15 changes: 10 additions & 5 deletions monai/data/image_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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__()
Expand All @@ -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:
"""
Expand Down Expand Up @@ -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
Expand All @@ -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())
Expand All @@ -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):
Expand Down
23 changes: 19 additions & 4 deletions monai/data/image_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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,
):
"""
Expand All @@ -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:
Expand All @@ -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
Expand Down
6 changes: 4 additions & 2 deletions tests/test_bundle_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,17 @@ 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)

# here test the script with `google fire` tool as CLI
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)

Expand Down
2 changes: 1 addition & 1 deletion tests/testing_data/inference.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion tests/testing_data/inference.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down