Skip to content

Commit 3457a19

Browse files
authored
ENH: Resample BOLD data to any surface template space using the Connectome Workbench (#3461)
## Changes proposed in this pull request Currently in fMRIPrep the Connectome Workbench is used to resample BOLD data to fsLR spaces, as part of the CIFTI workflow. This PR allows resampling BOLD data to any surface template space with a similar workflow, as long as (a) the "space-fsLR" spheres exist for the template (e.g., "tpl-onavg_space-fsLR_hemi-R_den-41k_sphere.surf.gii") or (b) the template is "fsLR". In the long run, we might want to generate alternative generic workflows, e.g., (a) using the [SurfaceTransform class](nipy/nitransforms#203) of nitransforms by @Shotgunosine and me, and (b) using FreeSurfer's resampling workflow. This PR depends on [a recent PR of smriprep](nipreps/smriprep#473) and updating TemplateFlow's files on AWS S3. Currently [the wrong subfolder on S3 was updated](https://github.com/templateflow/tpl-onavg/actions/runs/14868227699/job/41750227920#step:3:182) for some unknown reason. This generic resampling workflow might serve as the first step for the [generic CIFTI workflow](#3330). ## Documentation that should be reviewed I've updated the workflows' docstrings accordingly, but feel free to make suggestions/edits. @effigies @oesteban
2 parents cd2075b + bdd07c9 commit 3457a19

File tree

4 files changed

+445
-33
lines changed

4 files changed

+445
-33
lines changed

fmriprep/data/boilerplate.bib

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,3 +365,17 @@ @article{patriat_improved_2017
365365
keywords = {Motion, Correction, Methods, Rs-fMRI},
366366
pages = {74--82},
367367
}
368+
369+
@article{onavg,
370+
author = {Feilong, Ma and Jiahui, Guo and Gobbini, Maria Ida and Haxby, James V.},
371+
title = {A cortical surface template for human neuroscience},
372+
url = {https://www.nature.com/articles/s41592-024-02346-y},
373+
journal = {Nature Methods},
374+
issn = {1548-7105},
375+
number = {9},
376+
volume = {21},
377+
year = {2024},
378+
month = sep,
379+
pages = {1736--1742},
380+
doi = {10.1038/s41592-024-02346-y},
381+
}

fmriprep/workflows/base.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -532,9 +532,10 @@ def init_single_subject_wf(
532532
grayord_density=config.workflow.cifti_output,
533533
omp_nthreads=omp_nthreads,
534534
)
535+
fslr_density = '32k' if config.workflow.cifti_output == '91k' else '59k'
535536
resample_surfaces_wf = init_resample_surfaces_wf(
536537
surfaces=['white', 'pial', 'midthickness'],
537-
grayord_density=config.workflow.cifti_output,
538+
density=fslr_density,
538539
)
539540
ds_grayord_metrics_wf = init_ds_grayord_metrics_wf(
540541
bids_root=bids_root,
@@ -547,7 +548,7 @@ def init_single_subject_wf(
547548
surfaces=['white', 'pial', 'midthickness'],
548549
entities={
549550
'space': 'fsLR',
550-
'density': '32k' if config.workflow.cifti_output == '91k' else '59k',
551+
'density': fslr_density,
551552
},
552553
name='ds_fsLR_surfaces_wf',
553554
)

fmriprep/workflows/bold/base.py

Lines changed: 133 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ def init_bold_wf(
279279
spaces = config.workflow.spaces
280280
nonstd_spaces = set(spaces.get_nonstandard())
281281
freesurfer_spaces = spaces.get_fs_spaces()
282+
surf_std = [x for x in spaces.get_standard(dim=(2,)) if x.space != 'fsaverage']
282283

283284
#
284285
# Resampling outputs workflow:
@@ -503,6 +504,39 @@ def init_bold_wf(
503504
(merge_bold_sources, ds_bold_std_wf, [('out', 'inputnode.source_files')]),
504505
]) # fmt:skip
505506

507+
# Goodvoxels mask might be needed in any surface resampling
508+
if config.workflow.project_goodvoxels and (config.workflow.cifti_output or surf_std):
509+
from .resampling import init_goodvoxels_bold_mask_wf
510+
511+
goodvoxels_bold_mask_wf = init_goodvoxels_bold_mask_wf(mem_gb['resampled'])
512+
ds_goodvoxels_mask = pe.Node(
513+
DerivativesDataSink(
514+
base_directory=fmriprep_dir,
515+
dismiss_entities=dismiss_echo(),
516+
compress=True,
517+
space='T1w',
518+
desc='goodvoxels',
519+
suffix='mask',
520+
),
521+
name='ds_goodvoxels_mask',
522+
run_without_submitting=True,
523+
)
524+
ds_goodvoxels_mask.inputs.source_file = bold_file
525+
526+
workflow.__postdesc__ += """\
527+
A "goodvoxels" mask was applied during volume-to-surface sampling, excluding
528+
voxels whose time-series have a locally high coefficient of variation.
529+
"""
530+
workflow.connect([
531+
(inputnode, goodvoxels_bold_mask_wf, [('anat_ribbon', 'inputnode.anat_ribbon')]),
532+
(bold_anat_wf, goodvoxels_bold_mask_wf, [
533+
('outputnode.bold_file', 'inputnode.bold_file'),
534+
]),
535+
(goodvoxels_bold_mask_wf, ds_goodvoxels_mask, [
536+
('outputnode.goodvoxels_mask', 'in_file'),
537+
]),
538+
]) # fmt:skip
539+
506540
if config.workflow.run_reconall and freesurfer_spaces:
507541
workflow.__postdesc__ += """\
508542
Non-gridded (surface) resamplings were performed using `mri_vol2surf`
@@ -550,7 +584,6 @@ def init_bold_wf(
550584
from .resampling import (
551585
init_bold_fsLR_resampling_wf,
552586
init_bold_grayords_wf,
553-
init_goodvoxels_bold_mask_wf,
554587
)
555588

556589
bold_MNI6_wf = init_bold_volumetric_resample_wf(
@@ -569,42 +602,12 @@ def init_bold_wf(
569602
)
570603

571604
if config.workflow.project_goodvoxels:
572-
goodvoxels_bold_mask_wf = init_goodvoxels_bold_mask_wf(mem_gb['resampled'])
573-
574-
workflow.connect([
575-
(inputnode, goodvoxels_bold_mask_wf, [('anat_ribbon', 'inputnode.anat_ribbon')]),
576-
(bold_anat_wf, goodvoxels_bold_mask_wf, [
577-
('outputnode.bold_file', 'inputnode.bold_file'),
578-
]),
579-
]) # fmt:skip
580-
581-
ds_goodvoxels_mask = pe.Node(
582-
DerivativesDataSink(
583-
base_directory=fmriprep_dir,
584-
dismiss_entities=dismiss_echo(),
585-
compress=True,
586-
space='T1w',
587-
desc='goodvoxels',
588-
suffix='mask',
589-
),
590-
name='ds_goodvoxels_mask',
591-
run_without_submitting=True,
592-
)
593-
ds_goodvoxels_mask.inputs.source_file = bold_file
594605
workflow.connect([
595-
(goodvoxels_bold_mask_wf, ds_goodvoxels_mask, [
596-
('outputnode.goodvoxels_mask', 'in_file'),
597-
]),
598606
(goodvoxels_bold_mask_wf, bold_fsLR_resampling_wf, [
599607
('outputnode.goodvoxels_mask', 'inputnode.volume_roi'),
600608
]),
601609
]) # fmt:skip
602610

603-
bold_fsLR_resampling_wf.__desc__ += """\
604-
A "goodvoxels" mask was applied during volume-to-surface sampling in fsLR space,
605-
excluding voxels whose time-series have a locally high coefficient of variation.
606-
"""
607-
608611
bold_grayords_wf = init_bold_grayords_wf(
609612
grayord_density=config.workflow.cifti_output,
610613
mem_gb=1,
@@ -670,6 +673,105 @@ def init_bold_wf(
670673
]),
671674
]) # fmt:skip
672675

676+
if surf_std:
677+
from smriprep.workflows.surfaces import init_resample_surfaces_wf
678+
679+
from .resampling import (
680+
init_wb_surf_surf_wf,
681+
init_wb_vol_surf_wf,
682+
)
683+
684+
workflow.__postdesc__ += (
685+
'Non-gridded (surface) resamplings were performed using the Connectome Workbench.'
686+
)
687+
config.loggers.workflow.debug('Creating BOLD surface workbench resampling workflow.')
688+
689+
wb_vol_surf_wf = init_wb_vol_surf_wf(
690+
omp_nthreads=omp_nthreads,
691+
mem_gb=mem_gb['resampled'],
692+
dilate=True,
693+
)
694+
workflow.connect([
695+
(inputnode, wb_vol_surf_wf,[
696+
('white', 'inputnode.white'),
697+
('pial', 'inputnode.pial'),
698+
('midthickness', 'inputnode.midthickness'),
699+
]),
700+
(bold_anat_wf, wb_vol_surf_wf, [
701+
('outputnode.bold_file', 'inputnode.bold_file'),
702+
]),
703+
]) # fmt:skip
704+
705+
if config.workflow.project_goodvoxels:
706+
workflow.connect([
707+
(goodvoxels_bold_mask_wf, wb_vol_surf_wf, [
708+
('outputnode.goodvoxels_mask', 'inputnode.volume_roi'),
709+
]),
710+
]) # fmt:skip
711+
712+
for ref_ in surf_std:
713+
template = ref_.space
714+
density = ref_.spec.get('density') or ref_.spec.get('den') or None
715+
if density is None:
716+
config.loggers.warning(f'Cannot resample {ref_} without density specified.')
717+
continue
718+
719+
resample_surfaces_wf = init_resample_surfaces_wf(
720+
name=f'resample_surfaces_wf_{template}_{density}',
721+
surfaces=['midthickness'],
722+
template=template,
723+
density=density,
724+
)
725+
726+
wb_surf_surf_wf = init_wb_surf_surf_wf(
727+
template=template,
728+
density=density,
729+
omp_nthreads=omp_nthreads,
730+
mem_gb=mem_gb['resampled'],
731+
)
732+
733+
ds_bold_surf_wb = pe.Node(
734+
DerivativesDataSink(
735+
base_directory=fmriprep_dir,
736+
hemi=['L', 'R'],
737+
dismiss_entities=dismiss_echo(),
738+
space=template,
739+
density=density,
740+
suffix='bold',
741+
TaskName=all_metadata[0].get('TaskName'),
742+
extension='.func.gii',
743+
**prepare_timing_parameters(all_metadata[0]),
744+
),
745+
iterfield=('in_file', 'hemi'),
746+
name=f'ds_bold_surf_wb_{template}_{density}',
747+
run_without_submitting=True,
748+
)
749+
ds_bold_surf_wb.inputs.source_file = bold_file
750+
751+
workflow.connect([
752+
(inputnode, resample_surfaces_wf, [
753+
('midthickness', 'inputnode.midthickness'),
754+
('sphere_reg_fsLR', 'inputnode.sphere_reg_fsLR'),
755+
]),
756+
(wb_vol_surf_wf, wb_surf_surf_wf, [
757+
('outputnode.bold_fsnative', 'inputnode.bold_fsnative'),
758+
]),
759+
(inputnode, wb_surf_surf_wf, [
760+
('midthickness', 'inputnode.midthickness'),
761+
('sphere_reg_fsLR', 'inputnode.sphere_reg_fsLR'),
762+
]),
763+
(resample_surfaces_wf, wb_surf_surf_wf, [
764+
(
765+
f'outputnode.midthickness_{template}',
766+
'inputnode.midthickness_resampled'
767+
),
768+
]),
769+
(wb_surf_surf_wf, ds_bold_surf_wb, [
770+
('outputnode.bold_resampled', 'in_file'),
771+
# TODO: json metadata?
772+
]),
773+
]) # fmt:skip
774+
673775
bold_confounds_wf = init_bold_confs_wf(
674776
mem_gb=mem_gb['largemem'],
675777
metadata=all_metadata[0],

0 commit comments

Comments
 (0)