Repository: Project-MONAI/MONAI

Issue:
Recommended reader for `.nrrd` files? `NRRDReader` gives wrong output for non-diagonal orientations
**Describe the bug**

The default reader for '.nrrd' file extensions is the `NRRDReader` as shown in the docs, (https://docs.monai.io/en/stable/transforms.html#loadimage) as well as used by `LoadImage` when trying to load the file type (all dependencies are installed) 

However, `NRRDReader` does not work accurately for non-diagonal orientation matrices. 

`ITKReader` seems to work reliably for each use case, whereas `NRRDReader` doesn't. 

Is there a reason why `NRRDReader` is preferred over the `ITKReader`? Also, since `ITKReader` reads nifti files, why is it not the default reader selected for all such formats?

**To Reproduce**


Code using the default `NRRDReader`:
```python
import monai
import matplotlib.pyplot as plt

nrrd_datalist = ["path_to_nrrd_file"]

transform = monai.transforms.Compose([
                                      monai.transforms.LoadImage(image_only=True, ensure_channel_first=True),
                                      monai.transforms.DataStats(additional_info=lambda x: x.affine, prefix="On load"),
                                      monai.transforms.Orientation(axcodes="LPS"), 
                                      monai.transforms.Transpose(indices=[0, 3, 2, 1]),
                                      monai.transforms.DataStats(additional_info=lambda x: x.affine, prefix="After transform")
                                      ])

out = transform(nrrd_datalist)


# Get the mid indexes for each dimension
img = out[0][0]
mid_dim0 = img.shape[0] // 2
mid_dim1 = img.shape[1] // 2
mid_dim2 = img.shape[2] // 2

# Plot mid-axial, sagittal and coronal views
fig, axs = plt.subplots(1, 3, figsize=(15, 5))
axs[0].imshow(img[mid_dim0, :, :], cmap='gray')
axs[0].set_title('Mid dim0')
axs[1].imshow(img[:, mid_dim1, :], cmap='gray')
axs[1].set_title('Mid dim1')
axs[2].imshow(img[:, :, mid_dim2], cmap='gray')
axs[2].set_title('Mid dim2')
plt.show()
```
On load statistics:
Type: <class 'monai.data.meta_tensor.MetaTensor'> torch.float32
Shape: torch.Size([1, 512, 512, 78])
Value range: (-1024.0, 3071.0)
Additional info: tensor([[   0.0000,   -0.4941,    0.0000, -262.2529],
        [   0.4941,    0.0000,    0.0000,   56.2529],
        [   0.0000,    0.0000,    1.5000,   59.5000],
        [   0.0000,    0.0000,    0.0000,    1.0000]], dtype=torch.float64)

After transform statistics:
Type: <class 'monai.data.meta_tensor.MetaTensor'> torch.float32
Shape: torch.Size([1, 78, 512, 512])
Value range: (-1024.0, 3071.0)
Additional info: tensor([[  -0.4941,    0.0000,    0.0000, -262.2529],
        [   0.0000,   -0.4941,    0.0000,  308.7588],
        [   0.0000,    0.0000,    1.5000,   59.5000],
        [   0.0000,    0.0000,    0.0000,    1.0000]], dtype=torch.float64)

![image](https://github.com/Project-MONAI/MONAI/assets/10467804/d645accf-53f1-4c2f-8080-b34a35ff478c)



Code using `ITKReader`:
```python
import monai
import matplotlib.pyplot as plt

nrrd_datalist = ["path_to_nrrd_file"]

transform = monai.transforms.Compose([
                                      monai.transforms.LoadImage(image_only=True, ensure_channel_first=True, reader="ITKReader"),
                                      monai.transforms.DataStats(additional_info=lambda x: x.affine, prefix="On load"),
                                      monai.transforms.Orientation(axcodes="LPS"), 
                                      monai.transforms.Transpose(indices=[0, 3, 2, 1]),
                                      monai.transforms.DataStats(additional_info=lambda x: x.affine, prefix="After transform")
                                      ])

out = transform(nrrd_datalist)


# Get the mid indexes for each dimension
img = out[0][0]
mid_dim0 = img.shape[0] // 2
mid_dim1 = img.shape[1] // 2
mid_dim2 = img.shape[2] // 2

# Plot mid-axial, sagittal and coronal views
fig, axs = plt.subplots(1, 3, figsize=(15, 5))
axs[0].imshow(img[mid_dim0, :, :], cmap='gray')
axs[0].set_title('Mid dim0')
axs[1].imshow(img[:, mid_dim1, :], cmap='gray')
axs[1].set_title('Mid dim1')
axs[2].imshow(img[:, :, mid_dim2], cmap='gray')
axs[2].set_title('Mid dim2')
plt.show()

```
On load statistics:
Type: <class 'monai.data.meta_tensor.MetaTensor'> torch.float32
Shape: torch.Size([1, 512, 512, 78])
Value range: (-1024.0, 3071.0)
Additional info: tensor([[   0.0000,    0.4941,    0.0000, -262.2529],
        [  -0.4941,    0.0000,    0.0000,   56.2529],
        [   0.0000,    0.0000,    1.5000,   59.5000],
        [   0.0000,    0.0000,    0.0000,    1.0000]], dtype=torch.float64)

After transform statistics:
Type: <class 'monai.data.meta_tensor.MetaTensor'> torch.float32
Shape: torch.Size([1, 78, 512, 512])
Value range: (-1024.0, 3071.0)
Additional info: tensor([[-0.4941,  0.0000,  0.0000, -9.7471],
        [ 0.0000, -0.4941,  0.0000, 56.2529],
        [ 0.0000,  0.0000,  1.5000, 59.5000],
        [ 0.0000,  0.0000,  0.0000,  1.0000]], dtype=torch.float64)

![image](https://github.com/Project-MONAI/MONAI/assets/10467804/3730ef94-d77c-4caf-aa28-f48194299064)


**Environment**
[monai_env_config.txt](https://github.com/Project-MONAI/MONAI/files/13846385/monai_env_config.txt)





Discussion:
To add, this is the output from the `NRRDReader` for a diagonal orientation matrix. 

```python
import monai
import matplotlib.pyplot as plt

nrrd_datalist = ["/mnt/data1/RadiomicsFoundationModel/LUNG1/NRRDs/LUNG1-001/CT.nrrd"]


transform = monai.transforms.Compose([
                                      monai.transforms.LoadImage(image_only=True, ensure_channel_first=True),
                                      monai.transforms.DataStats(additional_info=lambda x: x.affine, prefix="On load"),
                                      monai.transforms.Orientation(axcodes="LPS"),
                                      monai.transforms.Transpose(indices=[0, 3, 2, 1]),
                                      monai.transforms.DataStats(additional_info=lambda x: x.affine, prefix="After transform")
                                      ])

out = transform(nrrd_datalist)


# Get the mid indexes for each dimension
img = out[0][0]
mid_dim0 = img.shape[0] // 2
mid_dim1 = img.shape[1] // 2
mid_dim2 = img.shape[2] // 2

# Plot mid-axial, sagittal and coronal views
fig, axs = plt.subplots(1, 3, figsize=(15, 5))
axs[0].imshow(img[mid_dim0, :, :], cmap='gray')
axs[0].set_title('Mid dim0')
axs[1].imshow(img[:, mid_dim1, :], cmap='gray')
axs[1].set_title('Mid dim1')
axs[2].imshow(img[:, :, mid_dim2], cmap='gray')
axs[2].set_title('Mid dim2')
plt.show()

```

On load statistics:
Type: <class 'monai.data.meta_tensor.MetaTensor'> torch.float32
Shape: torch.Size([1, 512, 512, 134])
Value range: (-1024.0, 3034.0)
Additional info: tensor([[  -0.9766,    0.0000,    0.0000,  249.5117],
        [   0.0000,   -0.9766,    0.0000,  460.5117],
        [   0.0000,    0.0000,    3.0000, -681.5000],
        [   0.0000,    0.0000,    0.0000,    1.0000]], dtype=torch.float64)

After transform statistics:
Type: <class 'monai.data.meta_tensor.MetaTensor'> torch.float32
Shape: torch.Size([1, 134, 512, 512])
Value range: (-1024.0, 3034.0)
Additional info: tensor([[  -0.9766,    0.0000,    0.0000,  249.5117],
        [   0.0000,   -0.9766,    0.0000,  460.5117],
        [   0.0000,    0.0000,    3.0000, -681.5000],
        [   0.0000,    0.0000,    0.0000,    1.0000]], dtype=torch.float64)

![image](https://github.com/Project-MONAI/MONAI/assets/10467804/24b21f87-26d7-4a81-8c79-d631cca6a98b)


As you can see, the orientations differ from the non-diagonal case while `ITKReader` is consistent
Hi @surajpaib, would you mind sharing the data then I could reproduce the issue? Thanks.
Hi @surajpaib 
I've noticed the same problem. The issue is that MONAI interprets the "space direction" variable in .nrrd files incorrectly in NRRDReader. Looking at the definition of the .nrrd format https://teem.sourceforge.net/nrrd/format.html#spacedirections, the vectors in "space direction" should be interpreted as the columns in the upper left 3x3 part of the affine matrix, however MONAI instead inserts them as rows.

The solution is easy, if you want to use NRRDReader you need to transpose the "space direction" matrix read by pynrrd. Ideally, it should of course be fixed in MONAI.

Changes needed:
monai/data/image_reader.py 
L1339: direction = img.header["space directions"].T







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
@@ -168,8 +168,8 @@ class ITKReader(ImageReader):
         series_name: the name of the DICOM series if there are multiple ones.
             used when loading DICOM series.
         reverse_indexing: whether to use a reversed spatial indexing convention for the returned data array.
-            If ``False``, the spatial indexing follows the numpy convention;
-            otherwise, the spatial indexing convention is reversed to be compatible with ITK. Default is ``False``.
+            If ``False``, the spatial indexing convention is reversed to be compatible with ITK;
+            otherwise, the spatial indexing follows the numpy convention. Default is ``False``.
             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``.
@@ -1323,7 +1323,7 @@ def get_data(self, img: NrrdImage | list[NrrdImage]) -> tuple[np.ndarray, dict]:
             header = dict(i.header)
             if self.index_order == "C":
                 header = self._convert_f_to_c_order(header)
-            header[MetaKeys.ORIGINAL_AFFINE] = self._get_affine(i)
+            header[MetaKeys.ORIGINAL_AFFINE] = self._get_affine(header)
 
             if self.affine_lps_to_ras:
                 header = self._switch_lps_ras(header)
@@ -1344,7 +1344,7 @@ def get_data(self, img: NrrdImage | list[NrrdImage]) -> tuple[np.ndarray, dict]:
 
         return _stack_images(img_array, compatible_meta), compatible_meta
 
-    def _get_affine(self, img: NrrdImage) -> np.ndarray:
+    def _get_affine(self, header: dict) -> np.ndarray:
         """
         Get the affine matrix of the image, it can be used to correct
         spacing, orientation or execute spatial transforms.
@@ -1353,8 +1353,8 @@ def _get_affine(self, img: NrrdImage) -> np.ndarray:
             img: A `NrrdImage` loaded from image file
 
         """
-        direction = img.header["space directions"]
-        origin = img.header["space origin"]
+        direction = header["space directions"]
+        origin = header["space origin"]
 
         x, y = direction.shape
         affine_diam = min(x, y) + 1


Test patch: