From 5f9a3598216f9780bc300d602d35414d64cf669a Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 1 Dec 2025 14:01:42 -0500 Subject: [PATCH 1/4] Add target res to GenerateSamplingReference. --- niworkflows/interfaces/nibabel.py | 70 +++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index dd5a6afa848..dabeeb284d8 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -385,7 +385,14 @@ class _GenerateSamplingReferenceInputSpec(BaseInterfaceInputSpec): 'the volume extent given by fixed_image, fast forward ' 'fixed_image otherwise.', ) - + target_resolution = traits.Tuple( + traits.Float, + traits.Float, + traits.Float, + desc='target resolution (mm)', + default=None, + usedefault=True, + ) class _GenerateSamplingReferenceOutputSpec(TraitedSpec): out_file = File(exists=True, desc='one file with all inputs flattened') @@ -569,6 +576,42 @@ def reorient_image(img: nb.spatialimages.SpatialImage, target_ornt: str): return r_img +def _calculate_target_affine(base_img, target_resolution): + """Calculate the target affine and shape for a given base image and target resolution. + + Parameters + ---------- + base_img : nibabel.SpatialImage + The base image to calculate the target affine and shape for. + target_resolution : tuple of 3 floats + The target resolution to calculate the target affine and shape for. + + Returns + ------- + new_affine : 4x4 numpy.ndarray + The target affine. + new_shape : tuple of 3 ints + The target shape. + """ + import numpy as np + + if len(target_resolution) != 3: + raise ValueError('target_resolution must be a tuple of 3 floats') + + # determine appropriate shape + zooms = np.array(base_img.header.get_zooms())[:3] + ratios = zooms / np.array(target_resolution) + new_shape = np.array(base_img.shape) * ratios + new_shape = tuple(np.round(new_shape).astype(int)) + + # patch in voxel sizes to affine + new_affine = base_img.affine.copy() + for i in range(3): + new_affine[i, i] = target_resolution[i] + + return new_affine, new_shape + + def _gen_reference( fixed_image, moving_image, @@ -577,6 +620,7 @@ def _gen_reference( message=None, force_xform_code=None, newpath=None, + target_resolution=None, ): """Generate a sampling reference, and makes sure xform matrices/codes are correct.""" import nilearn.image as nli @@ -586,14 +630,24 @@ def _gen_reference( # Moving images may not be RAS/LPS (more generally, transverse-longitudinal-axial) reoriented_moving_img = nb.as_closest_canonical(nb.load(moving_image)) - new_zooms = reoriented_moving_img.header.get_zooms()[:3] - # Avoid small differences in reported resolution to cause changes to - # FOV. See https://github.com/nipreps/fmriprep/issues/512 - # A positive diagonal affine is RAS, hence the need to reorient above. - new_affine = np.diag(np.round(new_zooms, 3)) - - resampled = nli.resample_img(fixed_image, target_affine=new_affine, interpolation='nearest') + if target_resolution is not None: + new_affine, new_shape = _calculate_target_affine(reoriented_moving_img, target_resolution) + else: + new_zooms = reoriented_moving_img.header.get_zooms()[:3] + + # Avoid small differences in reported resolution to cause changes to + # FOV. See https://github.com/nipreps/fmriprep/issues/512 + # A positive diagonal affine is RAS, hence the need to reorient above. + new_affine = np.diag(np.round(new_zooms, 3)) + new_shape = fixed_image.shape[:3] + + resampled = nli.resample_img( + fixed_image, + target_affine=new_affine, + target_shape=new_shape, + interpolation='nearest', + ) if fov_mask is not None: # If we have a mask, resample again dropping (empty) samples From d793caa52be19d5713c7f35bacf35397926f5a79 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 1 Dec 2025 14:02:47 -0500 Subject: [PATCH 2/4] Update nibabel.py --- niworkflows/interfaces/nibabel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index dabeeb284d8..2d284a8e52b 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -430,6 +430,7 @@ def _run_interface(self, runtime): force_xform_code=self.inputs.xform_code, message=f'{self.__class__.__name__} (niworkflows v{__version__})', newpath=runtime.cwd, + target_resolution=self.inputs.target_resolution, ) return runtime From 50982493d2f2cec93037b71645446fd7317e743d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 19:04:08 +0000 Subject: [PATCH 3/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- niworkflows/interfaces/nibabel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index 2d284a8e52b..759b72b5ec7 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -394,6 +394,7 @@ class _GenerateSamplingReferenceInputSpec(BaseInterfaceInputSpec): usedefault=True, ) + class _GenerateSamplingReferenceOutputSpec(TraitedSpec): out_file = File(exists=True, desc='one file with all inputs flattened') From 4e67c5fe55b60c675e745a2488e4849adc7f1656 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Tue, 2 Dec 2025 11:17:48 -0500 Subject: [PATCH 4/4] Update nibabel.py --- niworkflows/interfaces/nibabel.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index 759b72b5ec7..9fd462c0791 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -385,12 +385,10 @@ class _GenerateSamplingReferenceInputSpec(BaseInterfaceInputSpec): 'the volume extent given by fixed_image, fast forward ' 'fixed_image otherwise.', ) - target_resolution = traits.Tuple( - traits.Float, - traits.Float, - traits.Float, + target_resolution = traits.Either( + None, + traits.Tuple(traits.Float, traits.Float, traits.Float), desc='target resolution (mm)', - default=None, usedefault=True, )