Skip to content

Commit bc6a020

Browse files
committed
enh: deep revision of EstimateReferenceImage
1 parent 01f6474 commit bc6a020

File tree

1 file changed

+59
-39
lines changed

1 file changed

+59
-39
lines changed

niworkflows/interfaces/registration.py

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import numpy as np
99
from nilearn import image as nli
1010
from nilearn.image import index_img
11-
from nipype.utils.filemanip import fname_presuffix
11+
from nipype.utils.filemanip import fname_presuffix, ensure_list
1212
from nipype.interfaces.base import (
1313
traits,
1414
isdefined,
@@ -445,59 +445,81 @@ class _EstimateReferenceImageOutputSpec(TraitedSpec):
445445

446446
class EstimateReferenceImage(SimpleInterface):
447447
"""
448-
Given an 4D EPI file estimate an optimal reference image that could be later
449-
used for motion estimation and coregistration purposes. If detected uses
450-
T1 saturated volumes (non-steady state) otherwise a median of
448+
Generate a reference 3D map from BOLD and SBRef EPI images for BOLD datasets.
449+
450+
Given a 4D BOLD file or one or more 3/4D SBRefs, estimate a reference
451+
image for subsequent motion estimation and coregistration steps.
452+
For the case of BOLD datasets, it estimates a number of T1w saturated volumes
453+
(non-steady state at the beginning of the scan) and calculates the median
454+
across them.
455+
Otherwise (SBRefs or detected zero non-steady state frames), a median of
451456
of a subset of motion corrected volumes is used.
457+
If the input reference (BOLD or SBRef) is 3D already, it just returns a
458+
copy of the image with the NIfTI header extensions removed.
459+
460+
LIMITATION: If one wants to extract the reference from several SBRefs
461+
with several echoes each, the first echo should be selected elsewhere
462+
and run this interface in ``multiecho = False`` mode.
452463
"""
453464

454465
input_spec = _EstimateReferenceImageInputSpec
455466
output_spec = _EstimateReferenceImageOutputSpec
456467

457468
def _run_interface(self, runtime):
458-
# Select first EPI file
459-
ref_name = self.inputs.in_file[0]
460-
ref_nii = nb.load(ref_name)
461-
n_volumes_to_discard = _get_vols_to_discard(ref_nii)
462-
463-
self._results["n_volumes_to_discard"] = n_volumes_to_discard
464-
465-
if self.inputs.multiecho and (len(self.inputs.in_file) < 2):
466-
raise ValueError("Argument 'multiecho' is True but "
467-
"'in_file' has only one element")
469+
is_sbref = isdefined(self.inputs.sbref_file)
470+
ref_input = ensure_list(
471+
self.inputs.sbref_file if is_sbref else self.inputs.in_file
472+
)
468473

469-
out_ref_fname = os.path.join(runtime.cwd, "ref_bold.nii.gz")
470-
if isdefined(self.inputs.sbref_file):
471-
if self.inputs.multiecho and (len(self.inputs.sbref_file) < 2):
474+
if self.inputs.multiecho:
475+
if len(ref_input) < 2:
476+
input_name = "sbref_file" if is_sbref else "in_file"
472477
raise ValueError("Argument 'multiecho' is True but "
473-
"'sbref_file' has only one element")
474-
475-
# Select first SBRef file
476-
ref_name = self.inputs.sbref_file[0]
477-
ref_nii = nb.squeeze_image(nb.load(ref_name))
478+
f"'{input_name}' has only one element.")
479+
else:
480+
# Select only the first echo (see LIMITATION above for SBRefs)
481+
ref_input = ref_input[:1]
482+
elif not is_sbref and len(ref_input) > 1:
483+
raise ValueError("Input 'in_file' cannot point to more than one file "
484+
"for single-echo BOLD datasets.")
485+
486+
# Build the nibabel spatial image we will work with
487+
ref_im = []
488+
for im_i in ref_input:
489+
nib_i = nb.squeeze_image(nb.load(im_i))
490+
if nib_i.dataobj.ndim == 3:
491+
ref_im.append(nib_i)
492+
elif nib_i.dataobj.ndim == 4:
493+
ref_im += nb.four_to_three(nib_i)
494+
ref_im = nb.squeeze_image(nb.concat_images(ref_im))
495+
496+
# Volumes to discard only makes sense with BOLD inputs.
497+
if not is_sbref:
498+
n_volumes_to_discard = _get_vols_to_discard(ref_im)
499+
out_ref_fname = os.path.join(runtime.cwd, "ref_bold.nii.gz")
500+
else:
501+
n_volumes_to_discard = 0
478502
out_ref_fname = os.path.join(runtime.cwd, "ref_sbref.nii.gz")
479503

480-
# If reference is only 1 volume, return it directly
481-
if len(ref_nii.shape) == 3:
482-
ref_nii.header.extensions.clear()
483-
ref_nii.to_filename(out_ref_fname)
484-
self._results["ref_image"] = out_ref_fname
485-
return runtime
486-
else:
487-
# Reset this variable as it no longer applies
488-
# and value for the output is stored in self._results
489-
n_volumes_to_discard = 0
504+
# Set interface outputs
505+
self._results["n_volumes_to_discard"] = n_volumes_to_discard
506+
self._results["ref_image"] = out_ref_fname
490507

491508
# Slicing may induce inconsistencies with shape-dependent values in extensions.
492509
# For now, remove all. If this turns out to be a mistake, we can select extensions
493510
# that don't break pipeline stages.
494-
ref_nii.header.extensions.clear()
511+
ref_im.header.extensions.clear()
512+
513+
# If reference is only 1 volume, return it directly
514+
if ref_im.dataobj.ndim == 3:
515+
ref_im.to_filename(out_ref_fname)
516+
return runtime
495517

496518
if n_volumes_to_discard == 0:
497-
if ref_nii.shape[-1] > 40:
519+
if ref_im.shape[-1] > 40:
498520
ref_name = os.path.join(runtime.cwd, "slice.nii.gz")
499521
nb.Nifti1Image(
500-
ref_nii.dataobj[:, :, :, 20:40], ref_nii.affine, ref_nii.header
522+
ref_im.dataobj[:, :, :, 20:40], ref_im.affine, ref_im.header
501523
).to_filename(ref_name)
502524

503525
if self.inputs.mc_method == "AFNI":
@@ -516,14 +538,12 @@ def _run_interface(self, runtime):
516538
median_image_data = np.median(mc_slice_nii.get_fdata(), axis=3)
517539
else:
518540
median_image_data = np.median(
519-
ref_nii.dataobj[:, :, :, :n_volumes_to_discard], axis=3
541+
ref_im.dataobj[:, :, :, :n_volumes_to_discard], axis=3
520542
)
521543

522-
nb.Nifti1Image(median_image_data, ref_nii.affine, ref_nii.header).to_filename(
544+
nb.Nifti1Image(median_image_data, ref_im.affine, ref_im.header).to_filename(
523545
out_ref_fname
524546
)
525-
526-
self._results["ref_image"] = out_ref_fname
527547
return runtime
528548

529549

0 commit comments

Comments
 (0)