diff --git a/docs/changes/newsfragments/409.bugfix b/docs/changes/newsfragments/409.bugfix new file mode 100644 index 000000000..ea6a19408 --- /dev/null +++ b/docs/changes/newsfragments/409.bugfix @@ -0,0 +1 @@ +Use correct file output suffices for AFNI-based markers by `Synchon Mandal`_ diff --git a/docs/changes/newsfragments/409.enh b/docs/changes/newsfragments/409.enh new file mode 100644 index 000000000..ce3d91d0c --- /dev/null +++ b/docs/changes/newsfragments/409.enh @@ -0,0 +1 @@ +Ease asset storage for AFNI-based markers by `Synchon Mandal`_ diff --git a/junifer/markers/falff/_afni_falff.py b/junifer/markers/falff/_afni_falff.py index e09131573..3c9fb52cb 100644 --- a/junifer/markers/falff/_afni_falff.py +++ b/junifer/markers/falff/_afni_falff.py @@ -50,7 +50,7 @@ class AFNIALFF(metaclass=Singleton): @lru_cache(maxsize=None, typed=True) def compute( self, - data: "Nifti1Image", + input_path: Path, highpass: float, lowpass: float, tr: Optional[float], @@ -59,8 +59,8 @@ class AFNIALFF(metaclass=Singleton): Parameters ---------- - data : 4D Niimg-like object - Images to process. + input_path : pathlib.Path + Path to the input data. highpass : positive float Highpass cutoff frequency. lowpass : positive float @@ -82,19 +82,17 @@ class AFNIALFF(metaclass=Singleton): """ logger.debug("Creating cache for ALFF computation via AFNI") - # Create component-scoped tempdir - tempdir = WorkDirManager().get_tempdir(prefix="afni_alff+falff") - - # Save target data to a component-scoped tempfile - nifti_in_file_path = tempdir / "input.nii" # needs to be .nii - nib.save(data, nifti_in_file_path) + # Create element-scoped tempdir + element_tempdir = WorkDirManager().get_element_tempdir( + prefix="afni_lff" + ) # Set 3dRSFC command - alff_falff_out_path_prefix = tempdir / "alff_falff" + lff_out_path_prefix = element_tempdir / "output" bp_cmd = [ "3dRSFC", - f"-prefix {alff_falff_out_path_prefix.resolve()}", - f"-input {nifti_in_file_path.resolve()}", + f"-prefix {lff_out_path_prefix.resolve()}", + f"-input {input_path.resolve()}", f"-band {highpass} {lowpass}", "-no_rsfa -nosat -nodetrend", ] @@ -104,49 +102,48 @@ class AFNIALFF(metaclass=Singleton): # Call 3dRSFC run_ext_cmd(name="3dRSFC", cmd=bp_cmd) - # Create element-scoped tempdir so that the ALFF and fALFF maps are - # available later as nibabel stores file path reference for - # loading on computation - element_tempdir = WorkDirManager().get_element_tempdir( - prefix="afni_alff_falff" - ) - + # Read header to get output suffix + niimg = nib.load(input_path) + header = niimg.header + sform_code = header.get_sform(coded=True)[1] + if sform_code == 4: + output_suffix = "tlrc" + else: + output_suffix = "orig" + # Set params suffix params_suffix = f"_{highpass}_{lowpass}_{tr}" # Convert alff afni to nifti - alff_afni_to_nifti_out_path = ( - element_tempdir / f"alff{params_suffix}_output.nii" + alff_nifti_out_path = ( + element_tempdir / f"output_alff{params_suffix}.nii" ) # needs to be .nii convert_alff_cmd = [ "3dAFNItoNIFTI", - f"-prefix {alff_afni_to_nifti_out_path.resolve()}", - f"{alff_falff_out_path_prefix}_ALFF+orig.BRIK", + f"-prefix {alff_nifti_out_path.resolve()}", + f"{lff_out_path_prefix}_ALFF+{output_suffix}.BRIK", ] # Call 3dAFNItoNIFTI run_ext_cmd(name="3dAFNItoNIFTI", cmd=convert_alff_cmd) # Convert falff afni to nifti - falff_afni_to_nifti_out_path = ( - element_tempdir / f"falff{params_suffix}_output.nii" + falff_nifti_out_path = ( + element_tempdir / f"output_falff{params_suffix}.nii" ) # needs to be .nii convert_falff_cmd = [ "3dAFNItoNIFTI", - f"-prefix {falff_afni_to_nifti_out_path.resolve()}", - f"{alff_falff_out_path_prefix}_fALFF+orig.BRIK", + f"-prefix {falff_nifti_out_path.resolve()}", + f"{lff_out_path_prefix}_fALFF+{output_suffix}.BRIK", ] # Call 3dAFNItoNIFTI run_ext_cmd(name="3dAFNItoNIFTI", cmd=convert_falff_cmd) # Load nifti - alff_data = nib.load(alff_afni_to_nifti_out_path) - falff_data = nib.load(falff_afni_to_nifti_out_path) - - # Delete tempdir - WorkDirManager().delete_tempdir(tempdir) + alff_data = nib.load(alff_nifti_out_path) + falff_data = nib.load(falff_nifti_out_path) return ( alff_data, falff_data, - alff_afni_to_nifti_out_path, - falff_afni_to_nifti_out_path, - ) # type: ignore + alff_nifti_out_path, + falff_nifti_out_path, + ) diff --git a/junifer/markers/falff/_junifer_falff.py b/junifer/markers/falff/_junifer_falff.py index bfe6ef635..29b6ac99e 100644 --- a/junifer/markers/falff/_junifer_falff.py +++ b/junifer/markers/falff/_junifer_falff.py @@ -47,7 +47,7 @@ class JuniferALFF(metaclass=Singleton): @lru_cache(maxsize=None, typed=True) def compute( self, - data: "Nifti1Image", + input_path: Path, highpass: float, lowpass: float, tr: Optional[float], @@ -56,8 +56,8 @@ class JuniferALFF(metaclass=Singleton): Parameters ---------- - data : 4D Niimg-like object - Images to process. + input_path : pathlib.Path + Path to the input data. highpass : positive float Highpass cutoff frequency. lowpass : positive float @@ -80,9 +80,10 @@ class JuniferALFF(metaclass=Singleton): logger.debug("Creating cache for ALFF computation via junifer") # Get scan data - niimg_data = data.get_fdata().copy() + niimg = nib.load(input_path) + niimg_data = niimg.get_fdata().copy() if tr is None: - tr = float(data.header["pixdim"][4]) # type: ignore + tr = float(niimg.header["pixdim"][4]) # type: ignore logger.info(f"`tr` not provided, using `tr` from header: {tr}") # Bandpass the data within the lowpass and highpass cutoff freqs @@ -120,19 +121,17 @@ class JuniferALFF(metaclass=Singleton): # Calculate ALFF alff = numerator / np.sqrt(niimg_data.shape[-1]) alff_data = nimg.new_img_like( - ref_niimg=data, + ref_niimg=niimg, data=alff, ) falff_data = nimg.new_img_like( - ref_niimg=data, + ref_niimg=niimg, data=falff, ) - # Create element-scoped tempdir so that the ALFF and fALFF maps are - # available later as nibabel stores file path reference for - # loading on computation + # Create element-scoped tempdir element_tempdir = WorkDirManager().get_element_tempdir( - prefix="junifer_alff+falff" + prefix="junifer_lff" ) output_alff_path = element_tempdir / "output_alff.nii.gz" output_falff_path = element_tempdir / "output_falff.nii.gz" diff --git a/junifer/markers/falff/falff_base.py b/junifer/markers/falff/falff_base.py index d14017535..0960c0385 100644 --- a/junifer/markers/falff/falff_base.py +++ b/junifer/markers/falff/falff_base.py @@ -146,7 +146,7 @@ class ALFFBase(BaseMarker): estimator = JuniferALFF() # Compute ALFF + fALFF alff, falff, alff_path, falff_path = estimator.compute( # type: ignore - data=input_data["data"], + input_path=input_data["path"], highpass=self.highpass, lowpass=self.lowpass, tr=self.tr, diff --git a/junifer/markers/falff/tests/test_falff_spheres.py b/junifer/markers/falff/tests/test_falff_spheres.py index f36d81f99..f57022e35 100644 --- a/junifer/markers/falff/tests/test_falff_spheres.py +++ b/junifer/markers/falff/tests/test_falff_spheres.py @@ -69,7 +69,9 @@ def test_ALFFSpheres(caplog: pytest.LogCaptureFixture, tmp_path: Path) -> None: # Fit transform marker on data output = marker.fit_transform(element_data) - assert "Creating cache" in caplog.text + # Tests for ALFFParcels run before this with the same data and that + # should create the cache + assert "Calculating ALFF and fALFF" in caplog.text # Get BOLD output assert "BOLD" in output diff --git a/junifer/markers/reho/_afni_reho.py b/junifer/markers/reho/_afni_reho.py index d039eda27..e73cecc78 100644 --- a/junifer/markers/reho/_afni_reho.py +++ b/junifer/markers/reho/_afni_reho.py @@ -50,7 +50,7 @@ class AFNIReHo(metaclass=Singleton): @lru_cache(maxsize=None, typed=True) def compute( self, - data: "Nifti1Image", + input_path: Path, nneigh: int = 27, neigh_rad: Optional[float] = None, neigh_x: Optional[float] = None, @@ -65,8 +65,8 @@ class AFNIReHo(metaclass=Singleton): Parameters ---------- - data : 4D Niimg-like object - Images to process. + input_path : pathlib.Path + Path to the input data. nneigh : {7, 19, 27}, optional Number of voxels in the neighbourhood, inclusive. Can be: @@ -128,19 +128,17 @@ class AFNIReHo(metaclass=Singleton): """ logger.debug("Creating cache for ReHo computation via AFNI") - # Create component-scoped tempdir - tempdir = WorkDirManager().get_tempdir(prefix="afni_reho") - - # Save target data to a component-scoped tempfile - nifti_in_file_path = tempdir / "input.nii" # needs to be .nii - nib.save(data, nifti_in_file_path) + # Create element-scoped tempdir + element_tempdir = WorkDirManager().get_element_tempdir( + prefix="afni_reho" + ) # Set 3dReHo command - reho_out_path_prefix = tempdir / "reho" + reho_out_path_prefix = element_tempdir / "output" reho_cmd = [ "3dReHo", f"-prefix {reho_out_path_prefix.resolve()}", - f"-inset {nifti_in_file_path.resolve()}", + f"-inset {input_path.resolve()}", ] # Check ellipsoidal / cuboidal volume arguments if neigh_rad: @@ -164,28 +162,28 @@ class AFNIReHo(metaclass=Singleton): # Call 3dReHo run_ext_cmd(name="3dReHo", cmd=reho_cmd) - # Create element-scoped tempdir so that the ReHo map is - # available later as nibabel stores file path reference for - # loading on computation - element_tempdir = WorkDirManager().get_element_tempdir( - prefix="afni_reho" - ) + # Read header to get output suffix + niimg = nib.load(input_path) + header = niimg.header + sform_code = header.get_sform(coded=True)[1] + if sform_code == 4: + output_suffix = "tlrc" + else: + output_suffix = "orig" + # Convert afni to nifti - reho_afni_to_nifti_out_path = ( + reho_nifti_out_path = ( element_tempdir / "output.nii" # needs to be .nii ) convert_cmd = [ "3dAFNItoNIFTI", - f"-prefix {reho_afni_to_nifti_out_path.resolve()}", - f"{reho_out_path_prefix}+orig.BRIK", + f"-prefix {reho_nifti_out_path.resolve()}", + f"{reho_out_path_prefix}+{output_suffix}.BRIK", ] # Call 3dAFNItoNIFTI run_ext_cmd(name="3dAFNItoNIFTI", cmd=convert_cmd) # Load nifti - output_data = nib.load(reho_afni_to_nifti_out_path) + output_data = nib.load(reho_nifti_out_path) - # Delete tempdir - WorkDirManager().delete_tempdir(tempdir) - - return output_data, reho_afni_to_nifti_out_path # type: ignore + return output_data, reho_nifti_out_path diff --git a/junifer/markers/reho/_junifer_reho.py b/junifer/markers/reho/_junifer_reho.py index 6640c574d..a1189a8d1 100644 --- a/junifer/markers/reho/_junifer_reho.py +++ b/junifer/markers/reho/_junifer_reho.py @@ -48,15 +48,15 @@ class JuniferReHo(metaclass=Singleton): @lru_cache(maxsize=None, typed=True) def compute( self, - data: "Nifti1Image", + input_path: Path, nneigh: int = 27, ) -> tuple["Nifti1Image", Path]: """Compute ReHo map. Parameters ---------- - data : 4D Niimg-like object - Images to process. + input_path : pathlib.Path + Path to the input data. nneigh : {7, 19, 27, 125}, optional Number of voxels in the neighbourhood, inclusive. Can be: @@ -89,7 +89,8 @@ class JuniferReHo(metaclass=Singleton): logger.debug("Creating cache for ReHo computation via junifer") # Get scan data - niimg_data = data.get_fdata() + niimg = nib.load(input_path) + niimg_data = niimg.get_fdata().copy() # Get scan dimensions n_x, n_y, n_z, _ = niimg_data.shape @@ -119,7 +120,7 @@ class JuniferReHo(metaclass=Singleton): # after #299 is merged # Calculate whole brain mask mni152_whole_brain_mask = nmask.compute_brain_mask( - target_img=data, + target_img=niimg, threshold=0.5, mask_type="whole-brain", ) @@ -227,7 +228,7 @@ class JuniferReHo(metaclass=Singleton): # Create new image like target image output_data = nimg.new_img_like( - ref_niimg=data, + ref_niimg=niimg, data=reho_map, copy_header=False, ) diff --git a/junifer/markers/reho/reho_base.py b/junifer/markers/reho/reho_base.py index 0f90d4a9b..1877bacc5 100644 --- a/junifer/markers/reho/reho_base.py +++ b/junifer/markers/reho/reho_base.py @@ -125,7 +125,7 @@ class ReHoBase(BaseMarker): estimator = JuniferReHo() # Compute reho reho_map, reho_map_path = estimator.compute( # type: ignore - data=input_data["data"], + input_path=input_data["path"], **reho_params, )