Repository: aramis-lab/clinica

Issue:
[TEST] OASIS-to-BIDS : `_get_image_with_good_orientation` function not tested
In OASIS, all nifti images have the same header. The following function hard-codes one to make sure it is clean :

https://github.com/aramis-lab/clinica/blob/8180aef7a08c561fe7ad39a9dd589013008d42b6/clinica/converters/oasis_to_bids/_converter.py#L227-L269

It would be nice to move this function to the `_utils.py` module and test it. This is also an opportunity to learn more about nifti headers.

Discussion:

Solution patch:
diff --git a/clinica/converters/oasis_to_bids/_converter.py b/clinica/converters/oasis_to_bids/_converter.py
index 6b1c4418..f639e538 100644
--- a/clinica/converters/oasis_to_bids/_converter.py
+++ b/clinica/converters/oasis_to_bids/_converter.py
@@ -155,7 +155,7 @@ class OasisToBids(Converter):
         from clinica.converters.study_models import StudyName, bids_id_factory
         from clinica.utils.stream import cprint
 
-        from ._utils import get_first_image
+        from ._utils import get_first_image, get_image_with_good_orientation
 
         # This function is executed in a multiprocessing context
         # such that we need to re-configure the clinica logger in the child processes.
@@ -178,7 +178,7 @@ class OasisToBids(Converter):
 
         # In order do convert the Analyze format to Nifti the path to the .img file is required
         nb.save(
-            _get_image_with_good_orientation(get_first_image(t1_folder)),
+            get_image_with_good_orientation(get_first_image(t1_folder)),
             session_folder / "anat" / f"{participant_id}_ses-M000_T1w.nii.gz",
         )
 
@@ -222,48 +222,3 @@ class OasisToBids(Converter):
         else:
             with Pool(processes=n_procs) as pool:
                 pool.map(func, subjects_folders)
-
-
-def _get_image_with_good_orientation(image_path: Path) -> nb.Nifti1Image:
-    # First, convert to Nifti so that we can extract the s_form with NiBabel
-    # (NiBabel creates an 'Spm2AnalyzeImage' object that does not contain 'get_sform' method
-    img_with_wrong_orientation_analyze = nb.load(image_path)
-
-    # OASIS-1 images have the same header but sform is incorrect
-    # To solve this issue, we use header from images converted with FreeSurfer
-    # to generate a 'clean hard-coded' header
-    # affine:
-    # [[   0.    0.   -1.   80.]
-    #  [   1.    0.    0. -128.]
-    #  [   0.    1.    0. -128.]
-    #  [   0.    0.    0.    1.]]
-    # fmt: off
-    affine = np.array(
-        [
-            0, 0, -1, 80,
-            1, 0, 0, -128,
-            0, 1, 0, -128,
-            0, 0, 0, 1
-        ]
-    ).reshape(4, 4)
-    # fmt: on
-    s_form = affine.astype(np.int16)
-
-    hdr = nb.Nifti1Header()
-    hdr.set_data_shape((256, 256, 160))
-    hdr.set_data_dtype(np.int16)
-    hdr["bitpix"] = 16
-    hdr.set_sform(s_form, code="scanner")
-    hdr.set_qform(s_form, code="scanner")
-    hdr["extents"] = 16384
-    hdr["xyzt_units"] = 10
-
-    img_with_good_orientation_nifti = nb.Nifti1Image(
-        np.round(img_with_wrong_orientation_analyze.get_fdata(dtype="float32")).astype(
-            np.int16
-        ),
-        s_form,
-        header=hdr,
-    )
-    # Header correction to obtain dim0 = 3
-    return nb.funcs.four_to_three(img_with_good_orientation_nifti)[0]
diff --git a/clinica/converters/oasis_to_bids/_utils.py b/clinica/converters/oasis_to_bids/_utils.py
index ceb666d6..765689a5 100644
--- a/clinica/converters/oasis_to_bids/_utils.py
+++ b/clinica/converters/oasis_to_bids/_utils.py
@@ -1,6 +1,8 @@
 from pathlib import Path
 from typing import Iterable, Union
 
+import nibabel as nb
+import numpy as np
 import pandas as pd
 
 from clinica.converters.study_models import StudyName, bids_id_factory
@@ -10,6 +12,7 @@ __all__ = [
     "write_sessions_tsv",
     "write_scans_tsv",
     "get_first_image",
+    "get_image_with_good_orientation",
 ]
 
 
@@ -165,3 +168,60 @@ def get_first_image(input_folder: Path) -> Path:
             f"No file ending in .img found in {input_folder}.",
             FileNotFoundError,
         )
+
+
+def get_image_with_good_orientation(image_path: Path) -> nb.Nifti1Image:
+    """Convert an image from its path to Nifti with the correct orientation.
+
+    Parameters
+    ----------
+    image_path : Path
+        The path to the input image.
+
+    Returns
+    -------
+    nb.Nifti1Image :
+        The converted Nifti image with the correct orientation.
+    """
+    # First, convert to Nifti so that we can extract the s_form with NiBabel
+    # (NiBabel creates an 'Spm2AnalyzeImage' object that does not contain 'get_sform' method
+    img_with_wrong_orientation_analyze = nb.load(image_path)
+
+    # OASIS-1 images have the same header but sform is incorrect
+    # To solve this issue, we use header from images converted with FreeSurfer
+    # to generate a 'clean hard-coded' header
+    # affine:
+    # [[   0.    0.   -1.   80.]
+    #  [   1.    0.    0. -128.]
+    #  [   0.    1.    0. -128.]
+    #  [   0.    0.    0.    1.]]
+    # fmt: off
+    affine = np.array(
+        [
+            0, 0, -1, 80,
+            1, 0, 0, -128,
+            0, 1, 0, -128,
+            0, 0, 0, 1
+        ]
+    ).reshape(4, 4)
+    # fmt: on
+    s_form = affine.astype(np.int16)
+
+    hdr = nb.Nifti1Header()
+    hdr.set_data_shape((256, 256, 160))
+    hdr.set_data_dtype(np.int16)
+    hdr["bitpix"] = 16
+    hdr.set_sform(s_form, code="scanner")
+    hdr.set_qform(s_form, code="scanner")
+    hdr["extents"] = 16384
+    hdr["xyzt_units"] = 10
+
+    img_with_good_orientation_nifti = nb.Nifti1Image(
+        np.round(img_with_wrong_orientation_analyze.get_fdata(dtype="float32")).astype(
+            np.int16
+        ),
+        s_form,
+        header=hdr,
+    )
+    # Header correction to obtain dim0 = 3
+    return nb.funcs.four_to_three(img_with_good_orientation_nifti)[0]


Test patch:
diff --git a/test/unittests/converters/test_oasis_to_bids_utils.py b/test/unittests/converters/test_oasis_to_bids_utils.py
index d550980e..ad234aa1 100644
--- a/test/unittests/converters/test_oasis_to_bids_utils.py
+++ b/test/unittests/converters/test_oasis_to_bids_utils.py
@@ -1,5 +1,6 @@
 from pathlib import Path
 
+import nibabel as nb
 import numpy as np
 import pandas as pd
 import pytest
@@ -269,6 +270,46 @@ def test_get_first_image_not_found_error(tmp_path: Path) -> None:
         get_first_image(folder_path)
 
 
+def test_get_image_with_good_orientation(tmp_path: Path) -> None:
+    from clinica.converters.oasis_to_bids._utils import get_image_with_good_orientation
+
+    folder_path = tmp_path / "folder"
+    folder_path.mkdir()
+
+    # Define an empty array saved as a Nifti image
+    image_path = folder_path / "image.nii.gz"
+    zero_array = np.zeros((256, 256, 160, 1), dtype=np.float32)
+    nifti_img = nb.Nifti1Image(zero_array, np.eye(4))
+    nb.save(nifti_img, str(image_path))
+
+    # Load the corrected Nifti image
+    image = get_image_with_good_orientation(image_path)
+    hdr = image.header
+
+    affine = np.array([0, 0, -1, 80, 1, 0, 0, -128, 0, 1, 0, -128, 0, 0, 0, 1]).reshape(
+        4, 4
+    )
+    hdr_sform = affine.astype(np.int16)
+
+    # Check the values from header data and the image dimension
+    assert hdr.get_data_shape() == (256, 256, 160)
+    assert hdr.get_data_dtype() == np.int16
+    assert hdr["bitpix"] == 16
+
+    (sform_matrix, sform_code) = hdr.get_sform(coded=True)
+    assert np.array_equal(hdr_sform, sform_matrix)
+    assert sform_code == 1  # code 1 means scanner-based anatomical coordinates
+
+    (qform_matrix, qform_code) = hdr.get_qform(coded=True)
+    assert np.array_equal(hdr_sform, qform_matrix)
+    assert qform_code == 1  # code 1 means scanner-based anatomical coordinates
+
+    assert hdr["extents"] == 16384
+    assert hdr["xyzt_units"] == 10
+
+    assert len(image.shape) == 3  # the image dimension should be 3D
+
+
 @pytest.mark.parametrize(
     "cdr,diagnosis",
     [