From bbdd9c860042fddaac95dfe214d97b86e52d7368 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Tue, 16 Sep 2025 16:47:30 +0200 Subject: [PATCH 01/10] update: enable FSLWarper to support native to MNI space warping --- junifer/preprocess/warping/_fsl_warper.py | 247 ++++++++++++++------- junifer/preprocess/warping/space_warper.py | 53 ++++- 2 files changed, 214 insertions(+), 86 deletions(-) diff --git a/junifer/preprocess/warping/_fsl_warper.py b/junifer/preprocess/warping/_fsl_warper.py index 3edae21f2..62e502388 100644 --- a/junifer/preprocess/warping/_fsl_warper.py +++ b/junifer/preprocess/warping/_fsl_warper.py @@ -40,6 +40,7 @@ class FSLWarper: self, input: dict[str, Any], extra_input: dict[str, Any], + reference: str, ) -> dict[str, Any]: # pragma: no cover """Preprocess using FSL. @@ -50,6 +51,10 @@ class FSLWarper: extra_input : dict The other fields in the Junifer Data object. Should have ``T1w`` and ``Warp`` data types. + reference : str + The data type or template space to use as reference for warping. + Template space conversion is only possible from native space, + not from another template space. Returns ------- @@ -62,102 +67,188 @@ class FSLWarper: If warp file path could not be found in ``extra_input``. """ - logger.debug("Using FSL for space warping") - - # Get the min of the voxel sizes from input and use it as the - # resolution - resolution = np.min(input["data"].header.get_zooms()[:3]) - - # Get warp file path - warp_file_path = None - for entry in extra_input["Warp"]: - if entry["dst"] == "native": - warp_file_path = entry["path"] - if warp_file_path is None: - raise_error( - klass=RuntimeError, msg="Could not find correct warp file path" - ) - # Create element-specific tempdir for storing post-warping assets element_tempdir = WorkDirManager().get_element_tempdir( prefix="fsl_warper" ) - # Create a tempfile for resampled reference output - flirt_out_path = element_tempdir / "resampled_reference.nii.gz" - # Set flirt command - flirt_cmd = [ - "flirt", - "-interp spline", - f"-in {extra_input['T1w']['path'].resolve()}", - f"-ref {extra_input['T1w']['path'].resolve()}", - f"-applyisoxfm {resolution}", - f"-out {flirt_out_path.resolve()}", - ] - # Call flirt - run_ext_cmd(name="flirt", cmd=flirt_cmd) + # Warping to native space + if reference == "T1w": + logger.debug("Using FSL for space warping") - # Create a tempfile for warped output - applywarp_out_path = element_tempdir / "warped_data.nii.gz" - # Set applywarp command - applywarp_cmd = [ - "applywarp", - "--interp=spline", - f"-i {input['path'].resolve()}", - # use resampled reference - f"-r {flirt_out_path.resolve()}", - f"-w {warp_file_path.resolve()}", - f"-o {applywarp_out_path.resolve()}", - ] - # Call applywarp - run_ext_cmd(name="applywarp", cmd=applywarp_cmd) + # Get the min of the voxel sizes from input and use it as the + # resolution + resolution = np.min(input["data"].header.get_zooms()[:3]) - logger.debug("Updating warped data") - input.update( - { - # Update path to sync with "data" - "path": applywarp_out_path, - # Load nifti - "data": nib.load(applywarp_out_path), - # Use reference input's space as warped input's space - "space": extra_input["T1w"]["space"], - # Save resampled reference path - "reference": {"path": flirt_out_path}, - # Keep pre-warp space for further operations - "prewarp_space": input["space"], - } - ) + # Get warp file path + warp_file_path = None + for entry in extra_input["Warp"]: + if entry["dst"] == "native": + warp_file_path = entry["path"] + if warp_file_path is None: + raise_error( + klass=RuntimeError, + msg="Could not find correct warp file path", + ) - # Check for data type's mask and warp if found - if input.get("mask") is not None: - # Create a tempfile for warped mask output - applywarp_mask_out_path = element_tempdir / "warped_mask.nii.gz" + # Create a tempfile for resampled reference output + flirt_out_path = element_tempdir / "resampled_reference.nii.gz" + # Set flirt command + flirt_cmd = [ + "flirt", + "-interp spline", + f"-in {extra_input['T1w']['path'].resolve()}", + f"-ref {extra_input['T1w']['path'].resolve()}", + f"-applyisoxfm {resolution}", + f"-out {flirt_out_path.resolve()}", + ] + # Call flirt + run_ext_cmd(name="flirt", cmd=flirt_cmd) + + # Create a tempfile for warped output + applywarp_out_path = element_tempdir / "warped_data.nii.gz" # Set applywarp command - applywarp_mask_cmd = [ + applywarp_cmd = [ "applywarp", - "--interp=nn", - f"-i {input['mask']['path'].resolve()}", + "--interp=spline", + f"-i {input['path'].resolve()}", # use resampled reference - f"-r {input['reference']['path'].resolve()}", + f"-r {flirt_out_path.resolve()}", f"-w {warp_file_path.resolve()}", - f"-o {applywarp_mask_out_path.resolve()}", + f"-o {applywarp_out_path.resolve()}", ] # Call applywarp - run_ext_cmd(name="applywarp", cmd=applywarp_mask_cmd) + run_ext_cmd(name="applywarp", cmd=applywarp_cmd) - logger.debug("Updating warped mask data") + logger.debug("Updating warped data") input.update( { - "mask": { - # Update path to sync with "data" - "path": applywarp_mask_out_path, - # Load nifti - "data": nib.load(applywarp_mask_out_path), - # Use reference input's space as warped input mask's - # space - "space": extra_input["T1w"]["space"], - } + # Update path to sync with "data" + "path": applywarp_out_path, + # Load nifti + "data": nib.load(applywarp_out_path), + # Use reference input's space as warped input's space + "space": extra_input["T1w"]["space"], + # Save resampled reference path + "reference": {"path": flirt_out_path}, + # Keep pre-warp space for further operations + "prewarp_space": input["space"], } ) + # Check for data type's mask and warp if found + if input.get("mask") is not None: + # Create a tempfile for warped mask output + applywarp_mask_out_path = ( + element_tempdir / "warped_mask.nii.gz" + ) + # Set applywarp command + applywarp_mask_cmd = [ + "applywarp", + "--interp=nn", + f"-i {input['mask']['path'].resolve()}", + # use resampled reference + f"-r {input['reference']['path'].resolve()}", + f"-w {warp_file_path.resolve()}", + f"-o {applywarp_mask_out_path.resolve()}", + ] + # Call applywarp + run_ext_cmd(name="applywarp", cmd=applywarp_mask_cmd) + + logger.debug("Updating warped mask data") + input.update( + { + "mask": { + # Update path to sync with "data" + "path": applywarp_mask_out_path, + # Load nifti + "data": nib.load(applywarp_mask_out_path), + # Use reference input's space as warped input + # mask's space + "space": extra_input["T1w"]["space"], + } + } + ) + + # Warping from native to template space + else: + logger.debug( + f"Using FSL to warp data from native space to {reference} " + "space" + ) + + # Get warp file path + warp_file_path = None + for entry in extra_input["Warp"]: + if entry["src"] == "native" and entry["dst"] == reference: + warp_file_path = entry["path"] + if warp_file_path is None: + raise_error( + klass=RuntimeError, + msg="Could not find correct warp file path", + ) + + # Create a tempfile for warped output + applywarp_out_path = ( + element_tempdir + / f"warped_data_from_native_to_{reference}.nii.gz" + ) + # Set applywarp command + applywarp_cmd = [ + "applywarp", + "--interp=spline", + f"-i {input['path'].resolve()}", + f"-r {input['reference']['path'].resolve()}", + f"-w {warp_file_path.resolve()}", + f"-o {applywarp_out_path.resolve()}", + ] + # Call applywarp + run_ext_cmd(name="applywarp", cmd=applywarp_cmd) + + logger.debug("Updating warped data") + input.update( + { + # Update path to sync with "data" + "path": applywarp_out_path, + # Load nifti + "data": nib.load(applywarp_out_path), + # Switch space and prewarp_space + "space": reference, + "prewarp_space": input["space"], + } + ) + + # Check for data type's mask and warp if found + if input.get("mask") is not None: + # Create a tempfile for warped mask output + applywarp_mask_out_path = ( + element_tempdir + / f"warped_mask_from_native_to_{reference}.nii.gz" + ) + # Set applywarp command + applywarp_mask_cmd = [ + "applywarp", + "--interp=nn", + f"-i {input['mask']['path'].resolve()}", + f"-r {input['reference']['path'].resolve()}", + f"-w {warp_file_path.resolve()}", + f"-o {applywarp_mask_out_path.resolve()}", + ] + # Call applywarp + run_ext_cmd(name="applywarp", cmd=applywarp_mask_cmd) + + logger.debug("Updating warped mask data") + input.update( + { + "mask": { + # Update path to sync with "data" + "path": applywarp_mask_out_path, + # Load nifti + "data": nib.load(applywarp_mask_out_path), + # Update mask's space + "space": reference, + } + } + ) + return input diff --git a/junifer/preprocess/warping/space_warper.py b/junifer/preprocess/warping/space_warper.py index 789ea9a63..f958e330a 100644 --- a/junifer/preprocess/warping/space_warper.py +++ b/junifer/preprocess/warping/space_warper.py @@ -184,6 +184,7 @@ class SpaceWarper(BasePreprocessor): input = FSLWarper().preprocess( input=input, extra_input=extra_input, + reference=self.reference, ) elif self.using == "ants": input = ANTsWarper().preprocess( @@ -204,6 +205,7 @@ class SpaceWarper(BasePreprocessor): input = FSLWarper().preprocess( input=input, extra_input=extra_input, + reference=self.reference, ) elif warper == "ants": input = ANTsWarper().preprocess( @@ -230,14 +232,49 @@ class SpaceWarper(BasePreprocessor): extra_input={}, reference=self.reference, ) - # Transform to template space with FSL not possible + # Transform to template space with FSL possible conditionally elif self.using == "fsl" and self.reference != "T1w": - raise_error( - ( - f"Warping to {self.reference} space not possible with " - "FSL, use ANTs instead." - ), - klass=RuntimeError, - ) + input_space = input["space"] + # Transform from native to MNI possible conditionally + if input_space == "native": + # Check for BOLD reference as no T1w available + if input.get("reference") is None: + raise_error( + "`reference` key missing from input data type." + ) + # Check for extra inputs + if extra_input is None: + raise_error( + "No extra input provided, requires `Warp` " + "data type in particular." + ) + # Warp + input_prewarp_space = input["prewarp_space"] + warper = None + for entry in extra_input["Warp"]: + if ( + entry["src"] == input_space + and entry["dst"] == input_prewarp_space + ): + warper = entry["warper"] + if warper is None or warper != "fsl": + raise_error( + klass=RuntimeError, msg="Could not find correct warper" + ) + input = FSLWarper().preprocess( + input=input, + extra_input=extra_input, + reference=input_prewarp_space, + ) + # Transform from MNI to MNI template space not possible + else: + raise_error( + ( + f"Warping from {input_space} space to " + f"{self.reference} space not possible with " + "FSL, use ANTs instead." + ), + klass=RuntimeError, + ) return input, None -- 2.52.0 From d532c1ce3279c08eac08504d079ac5a657003f84 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Tue, 16 Sep 2025 16:48:22 +0200 Subject: [PATCH 02/10] chore: fix file naming for ANTsWarper mask warping --- junifer/preprocess/warping/_ants_warper.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/junifer/preprocess/warping/_ants_warper.py b/junifer/preprocess/warping/_ants_warper.py index 3ecb5711c..c90e09b3a 100644 --- a/junifer/preprocess/warping/_ants_warper.py +++ b/junifer/preprocess/warping/_ants_warper.py @@ -217,6 +217,9 @@ class ANTsWarper: # Call antsApplyTransforms run_ext_cmd(name="antsApplyTransforms", cmd=apply_transforms_cmd) + # Save input space to refer later in mask warp if needed + input_space = input["space"] + logger.debug("Updating warped data") input.update( { @@ -237,7 +240,7 @@ class ANTsWarper: if input.get("mask") is not None: # Create a tempfile for warped mask output apply_transforms_mask_out_path = element_tempdir / ( - f"warped_mask_from_{input['space']}_to_{reference}.nii.gz" + f"warped_mask_from_{input_space}_to_{reference}.nii.gz" ) # Set antsApplyTransforms command apply_transforms_mask_cmd = [ -- 2.52.0 From ea1547ec781d9c8406fb7ce8cbe5bfcb497d5b96 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 17 Sep 2025 13:25:37 +0200 Subject: [PATCH 03/10] chore: add no cover pragma in SpaceWarper --- junifer/preprocess/warping/space_warper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/preprocess/warping/space_warper.py b/junifer/preprocess/warping/space_warper.py index f958e330a..10a072b15 100644 --- a/junifer/preprocess/warping/space_warper.py +++ b/junifer/preprocess/warping/space_warper.py @@ -236,7 +236,7 @@ class SpaceWarper(BasePreprocessor): elif self.using == "fsl" and self.reference != "T1w": input_space = input["space"] # Transform from native to MNI possible conditionally - if input_space == "native": + if input_space == "native": # pragma: no cover # Check for BOLD reference as no T1w available if input.get("reference") is None: raise_error( -- 2.52.0 From cbe7b192c2a5491b10bcbe25e407c29d32ca887b Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 19 Sep 2025 12:18:19 +0200 Subject: [PATCH 04/10] update: add conditional flirt usage in FSLWarper for native to MNI warping --- junifer/preprocess/warping/_fsl_warper.py | 34 +++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/junifer/preprocess/warping/_fsl_warper.py b/junifer/preprocess/warping/_fsl_warper.py index 62e502388..cee1df3bf 100644 --- a/junifer/preprocess/warping/_fsl_warper.py +++ b/junifer/preprocess/warping/_fsl_warper.py @@ -188,6 +188,31 @@ class FSLWarper: msg="Could not find correct warp file path", ) + # Use flirt if input data resolution and reference resolution don't + # match + input_resolution = np.min(input["data"].header.get_zooms()[:3]) + ref_resolution = np.min( + input["reference"]["data"].header.get_zooms()[:3] + ) + if input_resolution != ref_resolution: + # Create a tempfile for resampled reference output + ref_path = ( + element_tempdir / f"resampled_reference-{reference}.nii.gz" + ) + # Set flirt command + flirt_cmd = [ + "flirt", + "-interp spline", + f"-in {input['reference']['path'].resolve()}", + f"-ref {input['reference']['path'].resolve()}", + f"-applyisoxfm {input_resolution}", + f"-out {ref_path.resolve()}", + ] + # Call flirt + run_ext_cmd(name="flirt", cmd=flirt_cmd) + else: + ref_path = input["reference"]["path"] + # Create a tempfile for warped output applywarp_out_path = ( element_tempdir @@ -198,7 +223,8 @@ class FSLWarper: "applywarp", "--interp=spline", f"-i {input['path'].resolve()}", - f"-r {input['reference']['path'].resolve()}", + # use resampled reference or original + f"-r {ref_path.resolve()}", f"-w {warp_file_path.resolve()}", f"-o {applywarp_out_path.resolve()}", ] @@ -215,6 +241,9 @@ class FSLWarper: # Switch space and prewarp_space "space": reference, "prewarp_space": input["space"], + # Save resampled reference path or overwrite original + # keeping it same + "reference": {"path": ref_path}, } ) @@ -230,7 +259,8 @@ class FSLWarper: "applywarp", "--interp=nn", f"-i {input['mask']['path'].resolve()}", - f"-r {input['reference']['path'].resolve()}", + # use resampled reference or original + f"-r {ref_path.resolve()}", f"-w {warp_file_path.resolve()}", f"-o {applywarp_mask_out_path.resolve()}", ] -- 2.52.0 From 8889ea52d0a4b9cecbf6d9a3968e19c2cbba6c5c Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 19 Sep 2025 15:11:23 +0200 Subject: [PATCH 05/10] update: enable ANTsWarper to support native to MNI space warping --- junifer/preprocess/warping/_ants_warper.py | 88 ++++++++++++++++------ junifer/preprocess/warping/space_warper.py | 49 ++++++++++-- 2 files changed, 105 insertions(+), 32 deletions(-) diff --git a/junifer/preprocess/warping/_ants_warper.py b/junifer/preprocess/warping/_ants_warper.py index c90e09b3a..9ad8896e2 100644 --- a/junifer/preprocess/warping/_ants_warper.py +++ b/junifer/preprocess/warping/_ants_warper.py @@ -180,27 +180,69 @@ class ANTsWarper: # Template space warping else: + input_space = input["space"] logger.debug( - f"Using ANTs to warp data from {input['space']} to {reference}" + f"Using ANTs to warp data from {input_space} space " + f"to {reference} space" ) - # Get xfm file - xfm_file_path = get_xfm(src=input["space"], dst=reference) - # Get template space image - template_space_img = get_template( - space=reference, - target_img=input["data"], - extra_input=None, - ) - # Save template - template_space_img_path = ( - element_tempdir / f"{reference}_T1w.nii.gz" - ) - nib.save(template_space_img, template_space_img_path) + # Native to MNI + if input_space == "native": + # Get warp file path + xfm_file_path = None + for entry in extra_input["Warp"]: + if entry["src"] == "native" and entry["dst"] == reference: + xfm_file_path = entry["path"] + if xfm_file_path is None: + raise_error( + klass=RuntimeError, + msg="Could not find correct warp file path", + ) + + # Use ResampleImage if input data resolution and reference + # resolution don't match + input_res = np.min(input["data"].header.get_zooms()[:3]) + ref_res = np.min( + input["reference"]["data"].header.get_zooms()[:3] + ) + if input_res != ref_res: + # Create a tempfile for resampled reference output + ref_path = ( + element_tempdir + / f"resampled_reference-{reference}.nii.gz" + ) + # Set ResampleImage command + resample_image_cmd = [ + "ResampleImage", + "3", # image dimension + f"{input['reference']['path'].resolve()}", + f"{ref_path.resolve()}", + f"{input_res}x{input_res}x{input_res}", + "0", # option for spacing and not size + "3 3", # Lanczos windowed sinc + ] + # Call ResampleImage + run_ext_cmd(name="ResampleImage", cmd=resample_image_cmd) + else: + ref_path = input["reference"]["path"] + + # MNI to MNI + else: + # Get xfm file + xfm_file_path = get_xfm(src=input_space, dst=reference) + # Get template space image in correct resolution + template_space_img = get_template( + space=reference, + target_img=input["data"], + extra_input=None, + ) + # Save template + ref_path = element_tempdir / f"{reference}_T1w.nii.gz" + nib.save(template_space_img, ref_path) # Create a tempfile for warped output warped_output_path = element_tempdir / ( - f"warped_data_from_{input['space']}_to_{reference}.nii.gz" + f"warped_data_from_{input_space}_to_{reference}.nii.gz" ) # Set antsApplyTransforms command @@ -210,16 +252,13 @@ class ANTsWarper: "-e 3", "-n LanczosWindowedSinc", f"-i {input['path'].resolve()}", - f"-r {template_space_img_path.resolve()}", + f"-r {ref_path.resolve()}", f"-t {xfm_file_path.resolve()}", f"-o {warped_output_path.resolve()}", ] # Call antsApplyTransforms run_ext_cmd(name="antsApplyTransforms", cmd=apply_transforms_cmd) - # Save input space to refer later in mask warp if needed - input_space = input["space"] - logger.debug("Updating warped data") input.update( { @@ -229,10 +268,11 @@ class ANTsWarper: "data": nib.load(warped_output_path), # Update warped input's space "space": reference, - # Save reference path - "reference": {"path": template_space_img_path}, + # Save resampled reference path or overwrite original + # keeping it same + "reference": {"path": ref_path}, # Keep pre-warp space for further operations - "prewarp_space": input["space"], + "prewarp_space": input_space, } ) @@ -249,8 +289,8 @@ class ANTsWarper: "-e 3", "-n 'GenericLabel[NearestNeighbor]'", f"-i {input['mask']['path'].resolve()}", - # use resampled reference - f"-r {input['reference']['path'].resolve()}", + # use resampled reference or original + f"-r {ref_path.resolve()}", f"-t {xfm_file_path.resolve()}", f"-o {apply_transforms_mask_out_path.resolve()}", ] diff --git a/junifer/preprocess/warping/space_warper.py b/junifer/preprocess/warping/space_warper.py index 10a072b15..7c9815991 100644 --- a/junifer/preprocess/warping/space_warper.py +++ b/junifer/preprocess/warping/space_warper.py @@ -133,7 +133,7 @@ class SpaceWarper(BasePreprocessor): # Does not add any new keys return input_type - def preprocess( + def preprocess( # noqa: C901 self, input: dict[str, Any], extra_input: Optional[dict[str, Any]] = None, @@ -215,8 +215,9 @@ class SpaceWarper(BasePreprocessor): ) # Transform to template space with ANTs possible elif self.using == "ants" and self.reference != "T1w": + input_space = input["space"] # Check pre-requirements for space manipulation - if self.reference == input["space"]: + if self.reference == input_space: raise_error( ( f"The target data is in {self.reference} space " @@ -226,12 +227,44 @@ class SpaceWarper(BasePreprocessor): ), klass=RuntimeError, ) - - input = ANTsWarper().preprocess( - input=input, - extra_input={}, - reference=self.reference, - ) + # Transform from native to MNI possible conditionally + if input_space == "native": # pragma: no cover + # Check for reference as no T1w available + if input.get("reference") is None: + raise_error( + "`reference` key missing from input data type." + ) + # Check for extra inputs + if extra_input is None: + raise_error( + "No extra input provided, requires `Warp` " + "data type in particular." + ) + # Warp + input_prewarp_space = input["prewarp_space"] + warper = None + for entry in extra_input["Warp"]: + if ( + entry["src"] == input_space + and entry["dst"] == input_prewarp_space + ): + warper = entry["warper"] + if warper is None or warper != "ants": + raise_error( + klass=RuntimeError, msg="Could not find correct warper" + ) + input = ANTsWarper().preprocess( + input=input, + extra_input=extra_input, + reference=input_prewarp_space, + ) + # Transform from MNI to MNI template space possible + else: + input = ANTsWarper().preprocess( + input=input, + extra_input={}, + reference=self.reference, + ) # Transform to template space with FSL possible conditionally elif self.using == "fsl" and self.reference != "T1w": input_space = input["space"] -- 2.52.0 From afdfdde4bcfa281289b7aad48e424690e6e93a41 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 19 Sep 2025 15:11:38 +0200 Subject: [PATCH 06/10] chore: update commentary in SpaceWarper --- junifer/preprocess/warping/space_warper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/preprocess/warping/space_warper.py b/junifer/preprocess/warping/space_warper.py index 7c9815991..b46bb705b 100644 --- a/junifer/preprocess/warping/space_warper.py +++ b/junifer/preprocess/warping/space_warper.py @@ -270,7 +270,7 @@ class SpaceWarper(BasePreprocessor): input_space = input["space"] # Transform from native to MNI possible conditionally if input_space == "native": # pragma: no cover - # Check for BOLD reference as no T1w available + # Check for reference as no T1w available if input.get("reference") is None: raise_error( "`reference` key missing from input data type." -- 2.52.0 From e3ee3a3ec4f8690eca20bc13b2ced7ed9971eb05 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Tue, 23 Sep 2025 14:25:37 +0200 Subject: [PATCH 07/10] chore: clean up native to mni warping logic in SpaceWarper --- junifer/preprocess/warping/space_warper.py | 88 ++++++++-------------- 1 file changed, 33 insertions(+), 55 deletions(-) diff --git a/junifer/preprocess/warping/space_warper.py b/junifer/preprocess/warping/space_warper.py index b46bb705b..a85e6955b 100644 --- a/junifer/preprocess/warping/space_warper.py +++ b/junifer/preprocess/warping/space_warper.py @@ -162,7 +162,7 @@ class SpaceWarper(BasePreprocessor): i.e., using ``"T1w"`` as reference. RuntimeError If warper could not be found in ``extra_input`` when - ``using="auto"`` or + ``using="auto"`` or converting from native space or if the data is in the correct space and does not require warping or if FSL is used when ``reference="T1w"``. @@ -213,11 +213,11 @@ class SpaceWarper(BasePreprocessor): extra_input=extra_input, reference=self.reference, ) - # Transform to template space with ANTs possible - elif self.using == "ants" and self.reference != "T1w": + # Transform to template space + if self.using in ["fsl", "ants"] and self.reference != "T1w": input_space = input["space"] # Check pre-requirements for space manipulation - if self.reference == input_space: + if self.using == "ants" and self.reference == input_space: raise_error( ( f"The target data is in {self.reference} space " @@ -249,65 +249,43 @@ class SpaceWarper(BasePreprocessor): and entry["dst"] == input_prewarp_space ): warper = entry["warper"] - if warper is None or warper != "ants": + if warper is None: raise_error( klass=RuntimeError, msg="Could not find correct warper" ) - input = ANTsWarper().preprocess( - input=input, - extra_input=extra_input, - reference=input_prewarp_space, - ) - # Transform from MNI to MNI template space possible - else: - input = ANTsWarper().preprocess( - input=input, - extra_input={}, - reference=self.reference, - ) - # Transform to template space with FSL possible conditionally - elif self.using == "fsl" and self.reference != "T1w": - input_space = input["space"] - # Transform from native to MNI possible conditionally - if input_space == "native": # pragma: no cover - # Check for reference as no T1w available - if input.get("reference") is None: - raise_error( - "`reference` key missing from input data type." + if warper == "fsl": + input = FSLWarper().preprocess( + input=input, + extra_input=extra_input, + reference=input_prewarp_space, ) - # Check for extra inputs - if extra_input is None: - raise_error( - "No extra input provided, requires `Warp` " - "data type in particular." + elif warper == "ants": + input = ANTsWarper().preprocess( + input=input, + extra_input=extra_input, + reference=input_prewarp_space, ) - # Warp - input_prewarp_space = input["prewarp_space"] - warper = None - for entry in extra_input["Warp"]: - if ( - entry["src"] == input_space - and entry["dst"] == input_prewarp_space - ): - warper = entry["warper"] - if warper is None or warper != "fsl": + else: raise_error( klass=RuntimeError, msg="Could not find correct warper" ) - input = FSLWarper().preprocess( - input=input, - extra_input=extra_input, - reference=input_prewarp_space, - ) - # Transform from MNI to MNI template space not possible else: - raise_error( - ( - f"Warping from {input_space} space to " - f"{self.reference} space not possible with " - "FSL, use ANTs instead." - ), - klass=RuntimeError, - ) + # Transform from MNI to MNI template space not possible + if self.using == "fsl": + raise_error( + ( + f"Warping from {input_space} space to " + f"{self.reference} space not possible with " + "FSL, use ANTs instead." + ), + klass=RuntimeError, + ) + # Transform from MNI to MNI template space possible + else: + input = ANTsWarper().preprocess( + input=input, + extra_input={}, + reference=self.reference, + ) return input, None -- 2.52.0 From 4df71716f3b65f879036acfbda49a9c90f38a33d Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Tue, 23 Sep 2025 15:07:22 +0200 Subject: [PATCH 08/10] chore: add changelog 462.feature --- docs/changes/newsfragments/462.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/changes/newsfragments/462.feature diff --git a/docs/changes/newsfragments/462.feature b/docs/changes/newsfragments/462.feature new file mode 100644 index 000000000..b709d4c83 --- /dev/null +++ b/docs/changes/newsfragments/462.feature @@ -0,0 +1 @@ +Enable :class:`.SpaceWarper` to warp data from native space to template spaces via both FSL and ANTs by `Synchon Mandal`_ -- 2.52.0 From 2284b50ee602c1c2eb9f4699dabbd7d1af5cdf56 Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Wed, 24 Sep 2025 14:31:40 +0200 Subject: [PATCH 09/10] Add some more debugging info --- junifer/preprocess/warping/_fsl_warper.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/junifer/preprocess/warping/_fsl_warper.py b/junifer/preprocess/warping/_fsl_warper.py index cee1df3bf..51178b946 100644 --- a/junifer/preprocess/warping/_fsl_warper.py +++ b/junifer/preprocess/warping/_fsl_warper.py @@ -194,7 +194,10 @@ class FSLWarper: ref_resolution = np.min( input["reference"]["data"].header.get_zooms()[:3] ) + logger.debug(f"Input resolution: {input_resolution}") + logger.debug(f"Reference resolution: {ref_resolution}") if input_resolution != ref_resolution: + logger.debug("Resampling reference to match input resolution") # Create a tempfile for resampled reference output ref_path = ( element_tempdir / f"resampled_reference-{reference}.nii.gz" @@ -211,6 +214,7 @@ class FSLWarper: # Call flirt run_ext_cmd(name="flirt", cmd=flirt_cmd) else: + logger.debug("Reference resolution matches input resolution") ref_path = input["reference"]["path"] # Create a tempfile for warped output @@ -249,6 +253,7 @@ class FSLWarper: # Check for data type's mask and warp if found if input.get("mask") is not None: + logger.debug("Warping associated mask") # Create a tempfile for warped mask output applywarp_mask_out_path = ( element_tempdir -- 2.52.0 From 6258ae5c4862b980f00128d30ed225394121b17c Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 24 Sep 2025 14:45:55 +0200 Subject: [PATCH 10/10] chore: improve debug logs for ANTsWarper --- junifer/preprocess/warping/_ants_warper.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/junifer/preprocess/warping/_ants_warper.py b/junifer/preprocess/warping/_ants_warper.py index 9ad8896e2..7a94432b1 100644 --- a/junifer/preprocess/warping/_ants_warper.py +++ b/junifer/preprocess/warping/_ants_warper.py @@ -205,6 +205,8 @@ class ANTsWarper: ref_res = np.min( input["reference"]["data"].header.get_zooms()[:3] ) + logger.debug(f"Input resolution: {input_res}") + logger.debug(f"Reference resolution: {ref_res}") if input_res != ref_res: # Create a tempfile for resampled reference output ref_path = ( @@ -224,6 +226,9 @@ class ANTsWarper: # Call ResampleImage run_ext_cmd(name="ResampleImage", cmd=resample_image_cmd) else: + logger.debug( + "Reference resolution matches input resolution" + ) ref_path = input["reference"]["path"] # MNI to MNI @@ -278,6 +283,7 @@ class ANTsWarper: # Check for data type's mask and warp if found if input.get("mask") is not None: + logger.debug("Warping associated mask") # Create a tempfile for warped mask output apply_transforms_mask_out_path = element_tempdir / ( f"warped_mask_from_{input_space}_to_{reference}.nii.gz" -- 2.52.0