Skip to content

Commit e2c3de1

Browse files
authored
ENH: Restore CIFTI-2 generation (#3129)
Builds on #3126. This one splits out the selection of the MNI template for the CIFTI-2 generation from the rest of the resampling to volumetric templates. If someone requests MNI152NLin6Asym and CIFTI, then we will resample twice in the working directory, but given that resampling is significantly faster now and does not scale files per-time point, I'm okay with that. I think it's a pretty rare case, and I suspect we'll want to resample a smaller ROI than the full brain at some point, so a separate pathway for that seems okay.
2 parents c294028 + 21b79e7 commit e2c3de1

File tree

5 files changed

+204
-182
lines changed

5 files changed

+204
-182
lines changed

fmriprep/interfaces/gifti.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class CreateROIOutputSpec(TraitedSpec):
2424

2525

2626
class CreateROI(SimpleInterface):
27-
"""Prepare GIFTI shape file for use in"""
27+
"""Prepare GIFTI thickness file for use as a cortical ROI"""
2828

2929
input_spec = CreateROIInputSpec
3030
output_spec = CreateROIOutputSpec
@@ -46,17 +46,21 @@ def _run_interface(self, runtime):
4646
# wb_command -metric-math "abs(var * -1) > 0"
4747
roi = np.abs(darray.data) > 0
4848

49+
# Divergence: Set datatype to uint8, since the values are boolean
50+
# wb_command sets datatype to float32
4951
darray = nb.gifti.GiftiDataArray(
5052
roi,
5153
intent=darray.intent,
52-
datatype=darray.datatype,
54+
datatype='uint8',
5355
encoding=darray.encoding,
5456
endian=darray.endian,
5557
coordsys=darray.coordsys,
5658
ordering=darray.ind_ord,
5759
meta=meta,
5860
)
5961

62+
img.darrays[0] = darray
63+
6064
out_filename = os.path.join(runtime.cwd, f"{subject}.{hemi}.roi.native.shape.gii")
6165
img.to_filename(out_filename)
6266
self._results["roi_file"] = out_filename

fmriprep/workflows/base.py

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ def init_single_subject_wf(subject_id: str):
147147
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
148148
from niworkflows.interfaces.bids import BIDSDataGrabber, BIDSInfo
149149
from niworkflows.interfaces.nilearn import NILEARN_VERSION
150+
from niworkflows.interfaces.utility import KeySelect
150151
from niworkflows.utils.bids import collect_data
151152
from niworkflows.utils.misc import fix_multi_T1w_source_name
152153
from niworkflows.utils.spaces import Reference
@@ -332,8 +333,9 @@ def init_single_subject_wf(subject_id: str):
332333
]) # fmt:skip
333334

334335
# Set up the template iterator once, if used
336+
template_iterator_wf = None
335337
if config.workflow.level == "full":
336-
if spaces.get_spaces(nonstandard=False, dim=(3,)):
338+
if spaces.cached.get_spaces(nonstandard=False, dim=(3,)):
337339
template_iterator_wf = init_template_iterator_wf(spaces=spaces)
338340
workflow.connect([
339341
(anat_fit_wf, template_iterator_wf, [
@@ -342,6 +344,34 @@ def init_single_subject_wf(subject_id: str):
342344
]),
343345
]) # fmt:skip
344346

347+
# Thread MNI152NLin6Asym standard outputs to CIFTI subworkflow, skipping
348+
# the iterator, which targets only output spaces.
349+
# This can lead to duplication in the working directory if people actually
350+
# want MNI152NLin6Asym outputs, but we'll live with it.
351+
if config.workflow.cifti_output:
352+
from smriprep.interfaces.templateflow import TemplateFlowSelect
353+
354+
ref = Reference(
355+
"MNI152NLin6Asym",
356+
{"res": 2 if config.workflow.cifti_output == "91k" else 1},
357+
)
358+
359+
select_MNI6_xfm = pe.Node(
360+
KeySelect(fields=["anat2std_xfm"], key=ref.fullname),
361+
name="select_MNI6",
362+
run_without_submitting=True,
363+
)
364+
select_MNI6_tpl = pe.Node(
365+
TemplateFlowSelect(template=ref.fullname, resolution=ref.spec['res']),
366+
name="select_MNI6_tpl",
367+
)
368+
workflow.connect([
369+
(anat_fit_wf, select_MNI6_xfm, [
370+
("outputnode.anat2std_xfm", "anat2std_xfm"),
371+
("outputnode.template", "keys"),
372+
]),
373+
]) # fmt:skip
374+
345375
if config.workflow.anat_only:
346376
return clean_datasinks(workflow)
347377

@@ -362,7 +392,6 @@ def init_single_subject_wf(subject_id: str):
362392
f"{[e.method for e in fmap_estimators]}."
363393
)
364394

365-
from niworkflows.interfaces.utility import KeySelect
366395
from sdcflows import fieldmaps as fm
367396
from sdcflows.workflows.base import init_fmap_preproc_wf
368397

@@ -507,6 +536,12 @@ def init_single_subject_wf(subject_id: str):
507536
('outputnode.subjects_dir', 'inputnode.subjects_dir'),
508537
('outputnode.subject_id', 'inputnode.subject_id'),
509538
('outputnode.fsnative2t1w_xfm', 'inputnode.fsnative2t1w_xfm'),
539+
('outputnode.white', 'inputnode.white'),
540+
('outputnode.pial', 'inputnode.pial'),
541+
('outputnode.midthickness', 'inputnode.midthickness'),
542+
('outputnode.thickness', 'inputnode.thickness'),
543+
('outputnode.sphere_reg_fsLR', 'inputnode.sphere_reg_fsLR'),
544+
('outputnode.anat_ribbon', 'inputnode.anat_ribbon'),
510545
]),
511546
]) # fmt:skip
512547
if fieldmap_id:
@@ -522,16 +557,27 @@ def init_single_subject_wf(subject_id: str):
522557
]) # fmt:skip
523558

524559
if config.workflow.level == "full":
525-
workflow.connect([
526-
(template_iterator_wf, bold_wf, [
527-
("outputnode.anat2std_xfm", "inputnode.anat2std_xfm"),
528-
("outputnode.space", "inputnode.std_space"),
529-
("outputnode.resolution", "inputnode.std_resolution"),
530-
("outputnode.cohort", "inputnode.std_cohort"),
531-
("outputnode.std_t1w", "inputnode.std_t1w"),
532-
("outputnode.std_mask", "inputnode.std_mask"),
533-
]),
534-
]) # fmt:skip
560+
if template_iterator_wf is not None:
561+
workflow.connect([
562+
(template_iterator_wf, bold_wf, [
563+
("outputnode.anat2std_xfm", "inputnode.anat2std_xfm"),
564+
("outputnode.space", "inputnode.std_space"),
565+
("outputnode.resolution", "inputnode.std_resolution"),
566+
("outputnode.cohort", "inputnode.std_cohort"),
567+
("outputnode.std_t1w", "inputnode.std_t1w"),
568+
("outputnode.std_mask", "inputnode.std_mask"),
569+
]),
570+
]) # fmt:skip
571+
572+
# Thread MNI152NLin6Asym standard outputs to CIFTI subworkflow, skipping
573+
# the iterator, which targets only output spaces.
574+
# This can lead to duplication in the working directory if people actually
575+
# want MNI152NLin6Asym outputs, but we'll live with it.
576+
if config.workflow.cifti_output:
577+
workflow.connect([
578+
(select_MNI6_xfm, bold_wf, [("anat2std_xfm", "inputnode.anat2mni6_xfm")]),
579+
(select_MNI6_tpl, bold_wf, [("brain_mask", "inputnode.mni6_mask")]),
580+
]) # fmt:skip
535581

536582
return clean_datasinks(workflow)
537583

fmriprep/workflows/bold/base.py

Lines changed: 100 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
init_ds_registration_wf,
5555
init_ds_volumes_wf,
5656
init_func_derivatives_wf,
57+
prepare_timing_parameters,
5758
)
5859
from .registration import init_bold_reg_wf, init_bold_t1_trans_wf
5960
from .resampling import (
@@ -189,9 +190,16 @@ def init_bold_wf(
189190
"t1w_mask",
190191
"t1w_dseg",
191192
"t1w_tpms",
193+
# FreeSurfer outputs
192194
"subjects_dir",
193195
"subject_id",
194196
"fsnative2t1w_xfm",
197+
"white",
198+
"midthickness",
199+
"pial",
200+
"thickness",
201+
"sphere_reg_fsLR",
202+
"anat_ribbon",
195203
# Fieldmap registration
196204
"fmap",
197205
"fmap_ref",
@@ -206,6 +214,9 @@ def init_bold_wf(
206214
"std_cohort",
207215
"std_t1w",
208216
"std_mask",
217+
# MNI152NLin6Asym warp, for CIFTI use
218+
"anat2mni6_xfm",
219+
"mni6_mask",
209220
],
210221
),
211222
name="inputnode",
@@ -389,7 +400,7 @@ def init_bold_wf(
389400
(bold_anat_wf, ds_bold_t1_wf, [('outputnode.bold_file', 'inputnode.bold')]),
390401
]) # fmt:skip
391402

392-
if spaces.get_spaces(nonstandard=False, dim=(3,)):
403+
if spaces.cached.get_spaces(nonstandard=False, dim=(3,)):
393404
# Missing:
394405
# * Clipping BOLD after resampling
395406
# * Resampling parcellations
@@ -462,6 +473,87 @@ def init_bold_wf(
462473
(bold_anat_wf, bold_surf_wf, [("outputnode.bold_file", "inputnode.bold_t1w")]),
463474
]) # fmt:skip
464475

476+
if config.workflow.cifti_output:
477+
from .resampling import init_bold_fsLR_resampling_wf, init_bold_grayords_wf
478+
479+
bold_MNI6_wf = init_bold_volumetric_resample_wf(
480+
metadata=all_metadata[0],
481+
fieldmap_id=fieldmap_id if not multiecho else None,
482+
omp_nthreads=omp_nthreads,
483+
name='bold_MNI6_wf',
484+
)
485+
486+
bold_fsLR_resampling_wf = init_bold_fsLR_resampling_wf(
487+
estimate_goodvoxels=config.workflow.project_goodvoxels,
488+
grayord_density=config.workflow.cifti_output,
489+
omp_nthreads=omp_nthreads,
490+
mem_gb=mem_gb["resampled"],
491+
)
492+
493+
bold_grayords_wf = init_bold_grayords_wf(
494+
grayord_density=config.workflow.cifti_output,
495+
mem_gb=mem_gb["resampled"],
496+
repetition_time=all_metadata[0]["RepetitionTime"],
497+
)
498+
499+
ds_bold_cifti = pe.Node(
500+
DerivativesDataSink(
501+
base_directory=fmriprep_dir,
502+
space='fsLR',
503+
density=config.workflow.cifti_output,
504+
suffix='bold',
505+
compress=False,
506+
TaskName=all_metadata[0].get('TaskName'),
507+
**prepare_timing_parameters(all_metadata[0]),
508+
),
509+
name='ds_bold_cifti',
510+
run_without_submitting=True,
511+
)
512+
ds_bold_cifti.inputs.source_file = bold_file
513+
514+
workflow.connect([
515+
# Resample BOLD to MNI152NLin6Asym, may duplicate bold_std_wf above
516+
(inputnode, bold_MNI6_wf, [
517+
("mni6_mask", "inputnode.target_ref_file"),
518+
("mni6_mask", "inputnode.target_mask"),
519+
("anat2mni6_xfm", "inputnode.anat2std_xfm"),
520+
("fmap_ref", "inputnode.fmap_ref"),
521+
("fmap_coeff", "inputnode.fmap_coeff"),
522+
("fmap_id", "inputnode.fmap_id"),
523+
]),
524+
(bold_fit_wf, bold_MNI6_wf, [
525+
("outputnode.coreg_boldref", "inputnode.bold_ref_file"),
526+
("outputnode.boldref2fmap_xfm", "inputnode.boldref2fmap_xfm"),
527+
("outputnode.boldref2anat_xfm", "inputnode.boldref2anat_xfm"),
528+
]),
529+
(bold_native_wf, bold_MNI6_wf, [
530+
("outputnode.bold_minimal", "inputnode.bold_file"),
531+
("outputnode.motion_xfm", "inputnode.motion_xfm"),
532+
]),
533+
# Resample T1w-space BOLD to fsLR surfaces
534+
(inputnode, bold_fsLR_resampling_wf, [
535+
("white", "inputnode.white"),
536+
("pial", "inputnode.pial"),
537+
("midthickness", "inputnode.midthickness"),
538+
("thickness", "inputnode.thickness"),
539+
("sphere_reg_fsLR", "inputnode.sphere_reg_fsLR"),
540+
("anat_ribbon", "inputnode.anat_ribbon"),
541+
]),
542+
(bold_anat_wf, bold_fsLR_resampling_wf, [
543+
("outputnode.bold_file", "inputnode.bold_file"),
544+
]),
545+
(bold_MNI6_wf, bold_grayords_wf, [
546+
("outputnode.bold_file", "inputnode.bold_std"),
547+
]),
548+
(bold_fsLR_resampling_wf, bold_grayords_wf, [
549+
("outputnode.bold_fsLR", "inputnode.bold_fsLR"),
550+
]),
551+
(bold_grayords_wf, ds_bold_cifti, [
552+
('outputnode.cifti_bold', 'in_file'),
553+
(('outputnode.cifti_metadata', _read_json), 'meta_dict'),
554+
]),
555+
]) # fmt:skip
556+
465557
bold_confounds_wf = init_bold_confs_wf(
466558
mem_gb=mem_gb["largemem"],
467559
metadata=all_metadata[0],
@@ -657,7 +749,6 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False):
657749
spaces = config.workflow.spaces
658750
fmriprep_dir = str(config.execution.fmriprep_dir)
659751
freesurfer_spaces = spaces.get_fs_spaces()
660-
project_goodvoxels = config.workflow.project_goodvoxels and config.workflow.cifti_output
661752

662753
ref_file = bold_file
663754
wf_name = _get_wf_name(ref_file, "func_preproc")
@@ -745,52 +836,6 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False):
745836
# SURFACES ##################################################################################
746837

747838
# CIFTI output
748-
if config.workflow.cifti_output:
749-
from .resampling import init_bold_fsLR_resampling_wf, init_bold_grayords_wf
750-
751-
bold_fsLR_resampling_wf = init_bold_fsLR_resampling_wf(
752-
estimate_goodvoxels=project_goodvoxels,
753-
grayord_density=config.workflow.cifti_output,
754-
omp_nthreads=omp_nthreads,
755-
mem_gb=mem_gb["resampled"],
756-
)
757-
758-
bold_grayords_wf = init_bold_grayords_wf(
759-
grayord_density=config.workflow.cifti_output,
760-
mem_gb=mem_gb["resampled"],
761-
repetition_time=metadata["RepetitionTime"],
762-
)
763-
764-
# fmt:off
765-
workflow.connect([
766-
(inputnode, bold_fsLR_resampling_wf, [
767-
("surfaces", "inputnode.surfaces"),
768-
("morphometrics", "inputnode.morphometrics"),
769-
("sphere_reg_fsLR", "inputnode.sphere_reg_fsLR"),
770-
("anat_ribbon", "inputnode.anat_ribbon"),
771-
]),
772-
(bold_t1_trans_wf, bold_fsLR_resampling_wf, [
773-
("outputnode.bold_t1", "inputnode.bold_file"),
774-
]),
775-
(bold_std_trans_wf, bold_grayords_wf, [
776-
("outputnode.bold_std", "inputnode.bold_std"),
777-
("outputnode.spatial_reference", "inputnode.spatial_reference"),
778-
]),
779-
(bold_fsLR_resampling_wf, bold_grayords_wf, [
780-
("outputnode.bold_fsLR", "inputnode.bold_fsLR"),
781-
]),
782-
(bold_fsLR_resampling_wf, func_derivatives_wf, [
783-
("outputnode.goodvoxels_mask", "inputnode.goodvoxels_mask"),
784-
]),
785-
(bold_fsLR_resampling_wf, outputnode, [
786-
("outputnode.weights_text", "weights_text"),
787-
]),
788-
(bold_grayords_wf, outputnode, [
789-
("outputnode.cifti_bold", "bold_cifti"),
790-
("outputnode.cifti_metadata", "cifti_metadata"),
791-
]),
792-
])
793-
# fmt:on
794839

795840
if spaces.get_spaces(nonstandard=False, dim=(3,)):
796841
carpetplot_wf = init_carpetplot_wf(
@@ -950,3 +995,10 @@ def get_img_orientation(imgf):
950995
"""Return the image orientation as a string"""
951996
img = nb.load(imgf)
952997
return "".join(nb.aff2axcodes(img.affine))
998+
999+
1000+
def _read_json(in_file):
1001+
from json import loads
1002+
from pathlib import Path
1003+
1004+
return loads(Path(in_file).read_text())

0 commit comments

Comments
 (0)