From 8ddbcefa5bc789b5a237976b0bc047c298877a50 Mon Sep 17 00:00:00 2001 From: Ma Feilong Date: Thu, 1 May 2025 15:25:27 -0400 Subject: [PATCH 1/7] Add a reference for the onavg template space. --- fmriprep/data/boilerplate.bib | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/fmriprep/data/boilerplate.bib b/fmriprep/data/boilerplate.bib index 4cc802f25..66b9c540d 100644 --- a/fmriprep/data/boilerplate.bib +++ b/fmriprep/data/boilerplate.bib @@ -365,3 +365,17 @@ @article{patriat_improved_2017 keywords = {Motion, Correction, Methods, Rs-fMRI}, pages = {74--82}, } + +@article{onavg, + author = {Feilong, Ma and Jiahui, Guo and Gobbini, Maria Ida and Haxby, James V.}, + title = {A cortical surface template for human neuroscience}, + url = {https://www.nature.com/articles/s41592-024-02346-y}, + journal = {Nature Methods}, + issn = {1548-7105}, + number = {9}, + volume = {21}, + year = {2024}, + month = sep, + pages = {1736--1742}, + doi = {10.1038/s41592-024-02346-y}, +} From 33de5a62d74aa5826fcf125106174ada163cfa24 Mon Sep 17 00:00:00 2001 From: Ma Feilong Date: Thu, 1 May 2025 15:31:23 -0400 Subject: [PATCH 2/7] Add init_bold_surf_wb_wf, which resamples data to surfaces using connectome workbench. --- fmriprep/workflows/bold/resampling.py | 206 ++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) diff --git a/fmriprep/workflows/bold/resampling.py b/fmriprep/workflows/bold/resampling.py index d725da6f0..55cc34e9e 100644 --- a/fmriprep/workflows/bold/resampling.py +++ b/fmriprep/workflows/bold/resampling.py @@ -514,6 +514,212 @@ def _calc_lower_thr(in_stats): return workflow +def init_bold_surf_wb_wf( + space: str, + density: ty.Literal['10k', '32k', '41k'], + omp_nthreads: int, + mem_gb: float, + name: str = 'bold_surf_wb_wf', +): + """Resample BOLD time series to surface using the Connectome Workbench. + + This workflow is modified from ``init_bold_fsLR_resampling_wf``. + + Workflow Graph + .. workflow:: + :graph2use: colored + :simple_form: yes + + from fmriprep.workflows.bold.resampling import init_bold_surf_wb_wf + wf = init_bold_surf_wb_wf( + space='onavg', + density='10k', + omp_nthreads=1, + mem_gb=1, + ) + Parameters + ---------- + space : :class:`str` + Surface template space, such as ``"onavg"`` or ``"fsLR"``. + density : :class:`str` + Either ``"10k"``, ``"32k"``, or ``"41k"``, representing the number of vertices + per hemisphere. + omp_nthreads : :class:`int` + Maximum number of threads an individual process may use. + mem_gb : :class:`float` + Size of BOLD file in GB. + name : :class:`str` + Name of workflow (default: ``bold_surf_wb_wf``). + + Inputs + ------ + bold_file : :class:`str` + Path to BOLD file resampled into T1 space + white : :class:`list` of :class:`str` + Path to left and right hemisphere white matter GIFTI surfaces. + pial : :class:`list` of :class:`str` + Path to left and right hemisphere pial GIFTI surfaces. + midthickness : :class:`list` of :class:`str` + Path to left and right hemisphere midthickness GIFTI surfaces. + midthickness_resampled : :class:`list` of :class:`str` + Path to left and right hemisphere midthickness GIFTI surfaces resampled into the output + space. + sphere_reg_fsLR : :class:`list` of :class:`str` + Path to left and right hemisphere sphere.reg GIFTI surfaces, mapping from subject to fsLR. + volume_roi : :class:`str` or Undefined + Pre-calculated goodvoxels mask. Not required. + + Outputs + ------- + bold_resampled : :class:`list` of :class:`str` + Path to BOLD series resampled as functional GIFTI files in template space. + + """ + import templateflow.api as tf + from niworkflows.engine.workflows import LiterateWorkflow as Workflow + from niworkflows.interfaces.utility import KeySelect + + from fmriprep.interfaces.workbench import VolumeToSurfaceMapping + + workflow = Workflow(name=name) + + space_str = 'onavg [@onavg]' if space == 'onavg' else space + workflow.__desc__ = f"""\ +The BOLD time-series were resampled onto the {space_str} space +using the Connectome Workbench [@hcppipelines]. +""" + + inputnode = pe.Node( + niu.IdentityInterface( + fields=[ + 'bold_file', + 'white', + 'pial', + 'midthickness', + 'midthickness_resampled', + 'sphere_reg_fsLR', + # 'cortex_mask', + 'volume_roi', + ] + ), + name='inputnode', + ) + + hemisource = pe.Node( + niu.IdentityInterface(fields=['hemi']), + name='hemisource', + iterables=[('hemi', ['L', 'R'])], + ) + + joinnode = pe.JoinNode( + niu.IdentityInterface(fields=['bold_resampled']), + name='joinnode', + joinsource='hemisource', + ) + + outputnode = pe.Node( + niu.IdentityInterface(fields=['bold_resampled']), + name='outputnode', + ) + + # select white, midthickness and pial surfaces based on hemi + select_surfaces = pe.Node( + KeySelect( + fields=[ + 'white', + 'pial', + 'midthickness', + 'midthickness_resampled', + 'sphere_reg_fsLR', + 'template_sphere', + # 'cortex_mask', + # 'template_roi', + ], + keys=['L', 'R'], + ), + name='select_surfaces', + run_without_submitting=True, + ) + select_surfaces.inputs.template_sphere = [ + str(sphere) + for sphere in tf.get( + template=space, + space=('fsLR' if space != 'fsLR' else None), + density=density, + suffix='sphere', + extension='.surf.gii', + ) + ] + + # RibbonVolumeToSurfaceMapping.sh + # Line 85 thru ... + volume_to_surface = pe.Node( + VolumeToSurfaceMapping(method='ribbon-constrained'), + name='volume_to_surface', + mem_gb=mem_gb * 3, + n_procs=omp_nthreads, + ) + metric_dilate = pe.Node( + MetricDilate(distance=10, nearest=True), + name='metric_dilate', + mem_gb=1, + n_procs=omp_nthreads, + ) + # mask_native = pe.Node(MetricMask(), name='mask_native') + resample_to_template = pe.Node( + MetricResample(method='ADAP_BARY_AREA', area_surfs=True), + name='resample_to_template', + mem_gb=1, + n_procs=omp_nthreads, + ) + # ... line 89 + # mask_fsLR = pe.Node(MetricMask(), name='mask_fsLR') + + workflow.connect([ + (inputnode, select_surfaces, [ + ('white', 'white'), + ('pial', 'pial'), + ('midthickness', 'midthickness'), + ('midthickness_resampled', 'midthickness_resampled'), + ('sphere_reg_fsLR', 'sphere_reg_fsLR'), + # ('cortex_mask', 'cortex_mask'), + ]), + (hemisource, select_surfaces, [('hemi', 'key')]), + # Resample BOLD to native surface, dilate and mask + (inputnode, volume_to_surface, [ + ('bold_file', 'volume_file'), + ('volume_roi', 'volume_roi'), + ]), + (select_surfaces, volume_to_surface, [ + ('midthickness', 'surface_file'), + ('white', 'inner_surface'), + ('pial', 'outer_surface'), + ]), + (select_surfaces, metric_dilate, [('midthickness', 'surf_file')]), + # (select_surfaces, mask_native, [('cortex_mask', 'mask')]), + (volume_to_surface, metric_dilate, [('out_file', 'in_file')]), + # (metric_dilate, mask_native, [('out_file', 'in_file')]), + # Resample BOLD to fsLR and mask + (select_surfaces, resample_to_template, [ + ('sphere_reg_fsLR', 'current_sphere'), + ('template_sphere', 'new_sphere'), + ('midthickness', 'current_area'), + ('midthickness_resampled', 'new_area'), + # ('cortex_mask', 'roi_metric'), + ]), + (metric_dilate, resample_to_template, [('out_file', 'in_file')]), + # (mask_native, resample_to_template, [('out_file', 'in_file')]), + # (select_surfaces, mask_fsLR, [('template_roi', 'mask')]), + # (resample_to_fsLR, mask_fsLR, [('out_file', 'in_file')]), + # Output + # (mask_fsLR, joinnode, [('out_file', 'bold_fsLR')]), + (resample_to_template, joinnode, [('out_file', 'bold_resampled')]), + (joinnode, outputnode, [('bold_resampled', 'bold_resampled')]), + ]) # fmt:skip + + return workflow + + def init_bold_fsLR_resampling_wf( grayord_density: ty.Literal['91k', '170k'], omp_nthreads: int, From 3c85d000d395c65bfe43a88befa50c1ed5339b78 Mon Sep 17 00:00:00 2001 From: Ma Feilong Date: Thu, 1 May 2025 15:38:55 -0400 Subject: [PATCH 3/7] Clean up code related to cortical masking. --- fmriprep/workflows/bold/resampling.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/fmriprep/workflows/bold/resampling.py b/fmriprep/workflows/bold/resampling.py index 55cc34e9e..aab6e3e60 100644 --- a/fmriprep/workflows/bold/resampling.py +++ b/fmriprep/workflows/bold/resampling.py @@ -598,7 +598,6 @@ def init_bold_surf_wb_wf( 'midthickness', 'midthickness_resampled', 'sphere_reg_fsLR', - # 'cortex_mask', 'volume_roi', ] ), @@ -632,8 +631,6 @@ def init_bold_surf_wb_wf( 'midthickness_resampled', 'sphere_reg_fsLR', 'template_sphere', - # 'cortex_mask', - # 'template_roi', ], keys=['L', 'R'], ), @@ -665,7 +662,6 @@ def init_bold_surf_wb_wf( mem_gb=1, n_procs=omp_nthreads, ) - # mask_native = pe.Node(MetricMask(), name='mask_native') resample_to_template = pe.Node( MetricResample(method='ADAP_BARY_AREA', area_surfs=True), name='resample_to_template', @@ -673,7 +669,6 @@ def init_bold_surf_wb_wf( n_procs=omp_nthreads, ) # ... line 89 - # mask_fsLR = pe.Node(MetricMask(), name='mask_fsLR') workflow.connect([ (inputnode, select_surfaces, [ @@ -682,7 +677,6 @@ def init_bold_surf_wb_wf( ('midthickness', 'midthickness'), ('midthickness_resampled', 'midthickness_resampled'), ('sphere_reg_fsLR', 'sphere_reg_fsLR'), - # ('cortex_mask', 'cortex_mask'), ]), (hemisource, select_surfaces, [('hemi', 'key')]), # Resample BOLD to native surface, dilate and mask @@ -696,23 +690,16 @@ def init_bold_surf_wb_wf( ('pial', 'outer_surface'), ]), (select_surfaces, metric_dilate, [('midthickness', 'surf_file')]), - # (select_surfaces, mask_native, [('cortex_mask', 'mask')]), (volume_to_surface, metric_dilate, [('out_file', 'in_file')]), - # (metric_dilate, mask_native, [('out_file', 'in_file')]), # Resample BOLD to fsLR and mask (select_surfaces, resample_to_template, [ ('sphere_reg_fsLR', 'current_sphere'), ('template_sphere', 'new_sphere'), ('midthickness', 'current_area'), ('midthickness_resampled', 'new_area'), - # ('cortex_mask', 'roi_metric'), ]), (metric_dilate, resample_to_template, [('out_file', 'in_file')]), - # (mask_native, resample_to_template, [('out_file', 'in_file')]), - # (select_surfaces, mask_fsLR, [('template_roi', 'mask')]), - # (resample_to_fsLR, mask_fsLR, [('out_file', 'in_file')]), # Output - # (mask_fsLR, joinnode, [('out_file', 'bold_fsLR')]), (resample_to_template, joinnode, [('out_file', 'bold_resampled')]), (joinnode, outputnode, [('bold_resampled', 'bold_resampled')]), ]) # fmt:skip From ce9d4476d19a450a42d15327295c26688f1b030a Mon Sep 17 00:00:00 2001 From: Ma Feilong Date: Fri, 2 May 2025 10:56:49 -0400 Subject: [PATCH 4/7] feat: Reusable volume-to-native-surface resampling workflow. --- fmriprep/workflows/bold/resampling.py | 170 +++++++++++++++++++++++++- 1 file changed, 169 insertions(+), 1 deletion(-) diff --git a/fmriprep/workflows/bold/resampling.py b/fmriprep/workflows/bold/resampling.py index aab6e3e60..c87707ad6 100644 --- a/fmriprep/workflows/bold/resampling.py +++ b/fmriprep/workflows/bold/resampling.py @@ -25,6 +25,8 @@ ++++++++++++++++++++ .. autofunction:: init_bold_surf_wf +.. autofunction:: init_wb_vol_surf_wf +.. autofunction:: init_bold_surf_wb_wf .. autofunction:: init_bold_fsLR_resampling_wf .. autofunction:: init_bold_grayords_wf .. autofunction:: init_goodvoxels_bold_mask_wf @@ -514,6 +516,172 @@ def _calc_lower_thr(in_stats): return workflow +def init_wb_vol_surf_wf( + omp_nthreads: int, + mem_gb: float, + name: str = 'wb_vol_surf_wf', + dilate: bool = True, +): + """Resample volume to native surface and dilate it using the Workbench. + + This workflow performs the first two steps of surface resampling: + 1. Resample volume to native surface using "ribbon-constrained" method + 2. Dilate the resampled surface to fix small holes using nearest neighbors + + The output of this workflow can be reused to resample to multiple template + spaces and resolutions. + + Workflow Graph + .. workflow:: + :graph2use: colored + :simple_form: yes + + from fmriprep.workflows.bold.resampling import init_wb_vol_surf_wf + wf = init_wb_vol_surf_wf(omp_nthreads=1, mem_gb=1) + + + Parameters + ---------- + omp_nthreads : :class:`int` + Maximum number of threads an individual process may use. + mem_gb : :class:`float` + Size of BOLD file in GB. + name : :class:`str` + Name of workflow (default: ``wb_vol_surf_wf``). + + Inputs + ------ + bold_file : :class:`str` + Path to BOLD file resampled into T1 space + white : :class:`list` of :class:`str` + Path to left and right hemisphere white matter GIFTI surfaces. + pial : :class:`list` of :class:`str` + Path to left and right hemisphere pial GIFTI surfaces. + midthickness : :class:`list` of :class:`str` + Path to left and right hemisphere midthickness GIFTI surfaces. + volume_roi : :class:`str` or Undefined + Pre-calculated goodvoxels mask. Not required. + + Outputs + ------- + bold_fsnative : :class:`list` of :class:`str` + Path to BOLD series resampled as functional GIFTI files in native + surface space. + """ + from niworkflows.engine.workflows import LiterateWorkflow as Workflow + from niworkflows.interfaces.utility import KeySelect + + from fmriprep.interfaces.workbench import VolumeToSurfaceMapping + + workflow = Workflow(name=name) + workflow.__desc__ = """\ +The BOLD time-series were resampled onto the native surface of the subject +using the "ribbon-constrained" method +""" + workflow.__desc__ += ' and then dilated by 10 mm.' if dilate else '.' + + inputnode = pe.Node( + niu.IdentityInterface( + fields=[ + 'bold_file', + 'white', + 'pial', + 'midthickness', + 'volume_roi', + ] + ), + name='inputnode', + ) + + hemisource = pe.Node( + niu.IdentityInterface(fields=['hemi']), + name='hemisource', + iterables=[('hemi', ['L', 'R'])], + ) + + joinnode = pe.JoinNode( + niu.IdentityInterface(fields=['bold_fsnative']), + name='joinnode', + joinsource='hemisource', + ) + + outputnode = pe.Node( + niu.IdentityInterface(fields=['bold_fsnative']), + name='outputnode', + ) + + select_surfaces = pe.Node( + KeySelect( + fields=[ + 'white', + 'pial', + 'midthickness', + ], + keys=['L', 'R'], + ), + name='select_surfaces', + run_without_submitting=True, + ) + + volume_to_surface = pe.Node( + VolumeToSurfaceMapping(method='ribbon-constrained'), + name='volume_to_surface', + mem_gb=mem_gb * 3, + n_procs=omp_nthreads, + ) + if dilate: + metric_dilate = pe.Node( + MetricDilate(distance=10, nearest=True), + name='metric_dilate', + mem_gb=1, + n_procs=omp_nthreads, + ) + + workflow.connect([ + (inputnode, select_surfaces, [ + ('white', 'white'), + ('pial', 'pial'), + ('midthickness', 'midthickness'), + ]), + (hemisource, select_surfaces, [('hemi', 'key')]), + (inputnode, volume_to_surface, [ + ('bold_file', 'volume_file'), + ('volume_roi', 'volume_roi'), + ]), + (select_surfaces, volume_to_surface, [ + ('midthickness', 'surface_file'), + ('white', 'inner_surface'), + ('pial', 'outer_surface'), + ]), + ]) # fmt:skip + if dilate: + workflow.connect([ + (select_surfaces, metric_dilate, [ + ('midthickness', 'surf_file'), + ]), + (volume_to_surface, metric_dilate, [ + ('out_file', 'in_file'), + ]), + (metric_dilate, joinnode, [ + ('out_file', 'bold_fsnative'), + ]), + (joinnode, outputnode, [ + ('bold_fsnative', 'bold_fsnative'), + ]), + ]) # fmt:skip + else: + workflow.connect([ + (volume_to_surface, joinnode, [ + ('out_file', 'bold_fsnative'), + ]), + (joinnode, outputnode, [ + ('bold_fsnative', 'bold_fsnative'), + ]), + ]) # fmt:skip + + return workflow + + def init_bold_surf_wb_wf( space: str, density: ty.Literal['10k', '32k', '41k'], @@ -691,7 +859,7 @@ def init_bold_surf_wb_wf( ]), (select_surfaces, metric_dilate, [('midthickness', 'surf_file')]), (volume_to_surface, metric_dilate, [('out_file', 'in_file')]), - # Resample BOLD to fsLR and mask + # Resample BOLD to output space and mask (select_surfaces, resample_to_template, [ ('sphere_reg_fsLR', 'current_sphere'), ('template_sphere', 'new_sphere'), From a856435c929dfa50e50f42c6c00e2413f7dbcdd2 Mon Sep 17 00:00:00 2001 From: Ma Feilong Date: Fri, 2 May 2025 12:39:32 -0400 Subject: [PATCH 5/7] feat: add init_wb_surf_surf_wf for native surface to template resampling. --- fmriprep/workflows/bold/resampling.py | 160 +++++++++++++++++++++++++- 1 file changed, 157 insertions(+), 3 deletions(-) diff --git a/fmriprep/workflows/bold/resampling.py b/fmriprep/workflows/bold/resampling.py index c87707ad6..fd75e3a26 100644 --- a/fmriprep/workflows/bold/resampling.py +++ b/fmriprep/workflows/bold/resampling.py @@ -26,6 +26,7 @@ .. autofunction:: init_bold_surf_wf .. autofunction:: init_wb_vol_surf_wf +.. autofunction:: init_wb_surf_surf_wf .. autofunction:: init_bold_surf_wb_wf .. autofunction:: init_bold_fsLR_resampling_wf .. autofunction:: init_bold_grayords_wf @@ -595,14 +596,14 @@ def init_wb_vol_surf_wf( hemisource = pe.Node( niu.IdentityInterface(fields=['hemi']), - name='hemisource', + name='hemisource_vol_surf', iterables=[('hemi', ['L', 'R'])], ) joinnode = pe.JoinNode( niu.IdentityInterface(fields=['bold_fsnative']), - name='joinnode', - joinsource='hemisource', + name='joinnode_vol_surf', + joinsource='hemisource_vol_surf', ) outputnode = pe.Node( @@ -682,6 +683,159 @@ def init_wb_vol_surf_wf( return workflow +def init_wb_surf_surf_wf( + space: str, + density: ty.Literal['10k', '32k', '41k'], + omp_nthreads: int, + mem_gb: float, + name: str = 'wb_surf_surf_wf', +): + """Resample BOLD time series from native surface to template surface. + + This workflow performs the third step of surface resampling: + 3. Resample the native surface to the template surface using the + Connectome Workbench + + Workflow Graph + .. workflow:: + :graph2use: colored + :simple_form: yes + + from fmriprep.workflows.bold.resampling import init_wb_surf_surf_wf + wf = init_wb_surf_surf_wf( + space='fsLR', + density='32k', + omp_nthreads=1, + mem_gb=1, + ) + + Parameters + ---------- + space : :class:`str` + Surface template space, such as ``"onavg"`` or ``"fsLR"``. + density : :class:`str` + Either ``"10k"``, ``"32k"``, or ``"41k"``, representing the number of + vertices per hemisphere. + omp_nthreads : :class:`int` + Maximum number of threads an individual process may use. + mem_gb : :class:`float` + Size of BOLD file in GB. + name : :class:`str` + Name of workflow (default: ``wb_surf_surf_wf``). + + Inputs + ------ + bold_fsnative : :class:`list` of :class:`str` + Path to BOLD series resampled as functional GIFTI files in native + surface space. + midthickness : :class:`list` of :class:`str` + Path to left and right hemisphere midthickness GIFTI surfaces. + midthickness_resampled : :class:`list` of :class:`str` + Path to left and right hemisphere midthickness GIFTI surfaces resampled + into the output space. + sphere_reg_fsLR : :class:`list` of :class:`str` + Path to left and right hemisphere sphere.reg GIFTI surfaces, mapping + from subject to fsLR. + + Outputs + ------- + bold_resampled : :class:`list` of :class:`str` + Path to BOLD series resampled as functional GIFTI files in the output + template space. + """ + import templateflow.api as tf + from niworkflows.engine.workflows import LiterateWorkflow as Workflow + from niworkflows.interfaces.utility import KeySelect + + workflow = Workflow(name=name) + + inputnode = pe.Node( + niu.IdentityInterface( + fields=[ + 'bold_fsnative', + 'midthickness', + 'midthickness_resampled', + 'sphere_reg_fsLR', + ] + ), + name='inputnode', + ) + + hemisource = pe.Node( + niu.IdentityInterface(fields=['hemi']), + name=name + '_hemisource_surf_surf', + iterables=[('hemi', ['L', 'R'])], + ) + + joinnode = pe.JoinNode( + niu.IdentityInterface(fields=['bold_resampled']), + name=name + '_joinnode_surf_surf', + joinsource=name + '_hemisource_surf_surf', + ) + + outputnode = pe.Node( + niu.IdentityInterface(fields=['bold_resampled']), + name='outputnode', + ) + + select_surfaces = pe.Node( + KeySelect( + fields=[ + 'bold_fsnative', + 'midthickness', + 'midthickness_resampled', + 'sphere_reg_fsLR', + 'template_sphere', + ], + keys=['L', 'R'], + ), + name=name + '_select_surfaces', + run_without_submitting=True, + ) + select_surfaces.inputs.template_sphere = [ + str(sphere) + for sphere in tf.get( + template=space, + space=('fsLR' if space != 'fsLR' else None), + density=density, + suffix='sphere', + extension='.surf.gii', + ) + ] + + resample_to_template = pe.Node( + MetricResample(method='ADAP_BARY_AREA', area_surfs=True), + name=name + '_resample_to_template', + mem_gb=1, + n_procs=omp_nthreads, + ) + + workflow.connect([ + (inputnode, select_surfaces, [ + ('bold_fsnative', 'bold_fsnative'), + ('midthickness', 'midthickness'), + ('midthickness_resampled', 'midthickness_resampled'), + ('sphere_reg_fsLR', 'sphere_reg_fsLR'), + ]), + (hemisource, select_surfaces, [('hemi', 'key')]), + (select_surfaces, resample_to_template, [ + ('bold_fsnative', 'in_file'), + ('sphere_reg_fsLR', 'current_sphere'), + ('template_sphere', 'new_sphere'), + ('midthickness', 'current_area'), + ('midthickness_resampled', 'new_area'), + ]), + (resample_to_template, joinnode, [ + ('out_file', 'bold_resampled'), + ]), + (joinnode, outputnode, [ + ('bold_resampled', 'bold_resampled'), + ]), + ]) # fmt:skip + + return workflow + + def init_bold_surf_wb_wf( space: str, density: ty.Literal['10k', '32k', '41k'], From e3c6cdc787539d1072371556ab7a72a333190426 Mon Sep 17 00:00:00 2001 From: Ma Feilong Date: Fri, 2 May 2025 12:41:50 -0400 Subject: [PATCH 6/7] Remove init_bold_surf_wb_wf as it's replaced by init_wb_vol_surf_wf and init_wb_surf_surf_wf. --- fmriprep/workflows/bold/resampling.py | 194 -------------------------- 1 file changed, 194 deletions(-) diff --git a/fmriprep/workflows/bold/resampling.py b/fmriprep/workflows/bold/resampling.py index fd75e3a26..0d6bce20e 100644 --- a/fmriprep/workflows/bold/resampling.py +++ b/fmriprep/workflows/bold/resampling.py @@ -27,7 +27,6 @@ .. autofunction:: init_bold_surf_wf .. autofunction:: init_wb_vol_surf_wf .. autofunction:: init_wb_surf_surf_wf -.. autofunction:: init_bold_surf_wb_wf .. autofunction:: init_bold_fsLR_resampling_wf .. autofunction:: init_bold_grayords_wf .. autofunction:: init_goodvoxels_bold_mask_wf @@ -836,199 +835,6 @@ def init_wb_surf_surf_wf( return workflow -def init_bold_surf_wb_wf( - space: str, - density: ty.Literal['10k', '32k', '41k'], - omp_nthreads: int, - mem_gb: float, - name: str = 'bold_surf_wb_wf', -): - """Resample BOLD time series to surface using the Connectome Workbench. - - This workflow is modified from ``init_bold_fsLR_resampling_wf``. - - Workflow Graph - .. workflow:: - :graph2use: colored - :simple_form: yes - - from fmriprep.workflows.bold.resampling import init_bold_surf_wb_wf - wf = init_bold_surf_wb_wf( - space='onavg', - density='10k', - omp_nthreads=1, - mem_gb=1, - ) - Parameters - ---------- - space : :class:`str` - Surface template space, such as ``"onavg"`` or ``"fsLR"``. - density : :class:`str` - Either ``"10k"``, ``"32k"``, or ``"41k"``, representing the number of vertices - per hemisphere. - omp_nthreads : :class:`int` - Maximum number of threads an individual process may use. - mem_gb : :class:`float` - Size of BOLD file in GB. - name : :class:`str` - Name of workflow (default: ``bold_surf_wb_wf``). - - Inputs - ------ - bold_file : :class:`str` - Path to BOLD file resampled into T1 space - white : :class:`list` of :class:`str` - Path to left and right hemisphere white matter GIFTI surfaces. - pial : :class:`list` of :class:`str` - Path to left and right hemisphere pial GIFTI surfaces. - midthickness : :class:`list` of :class:`str` - Path to left and right hemisphere midthickness GIFTI surfaces. - midthickness_resampled : :class:`list` of :class:`str` - Path to left and right hemisphere midthickness GIFTI surfaces resampled into the output - space. - sphere_reg_fsLR : :class:`list` of :class:`str` - Path to left and right hemisphere sphere.reg GIFTI surfaces, mapping from subject to fsLR. - volume_roi : :class:`str` or Undefined - Pre-calculated goodvoxels mask. Not required. - - Outputs - ------- - bold_resampled : :class:`list` of :class:`str` - Path to BOLD series resampled as functional GIFTI files in template space. - - """ - import templateflow.api as tf - from niworkflows.engine.workflows import LiterateWorkflow as Workflow - from niworkflows.interfaces.utility import KeySelect - - from fmriprep.interfaces.workbench import VolumeToSurfaceMapping - - workflow = Workflow(name=name) - - space_str = 'onavg [@onavg]' if space == 'onavg' else space - workflow.__desc__ = f"""\ -The BOLD time-series were resampled onto the {space_str} space -using the Connectome Workbench [@hcppipelines]. -""" - - inputnode = pe.Node( - niu.IdentityInterface( - fields=[ - 'bold_file', - 'white', - 'pial', - 'midthickness', - 'midthickness_resampled', - 'sphere_reg_fsLR', - 'volume_roi', - ] - ), - name='inputnode', - ) - - hemisource = pe.Node( - niu.IdentityInterface(fields=['hemi']), - name='hemisource', - iterables=[('hemi', ['L', 'R'])], - ) - - joinnode = pe.JoinNode( - niu.IdentityInterface(fields=['bold_resampled']), - name='joinnode', - joinsource='hemisource', - ) - - outputnode = pe.Node( - niu.IdentityInterface(fields=['bold_resampled']), - name='outputnode', - ) - - # select white, midthickness and pial surfaces based on hemi - select_surfaces = pe.Node( - KeySelect( - fields=[ - 'white', - 'pial', - 'midthickness', - 'midthickness_resampled', - 'sphere_reg_fsLR', - 'template_sphere', - ], - keys=['L', 'R'], - ), - name='select_surfaces', - run_without_submitting=True, - ) - select_surfaces.inputs.template_sphere = [ - str(sphere) - for sphere in tf.get( - template=space, - space=('fsLR' if space != 'fsLR' else None), - density=density, - suffix='sphere', - extension='.surf.gii', - ) - ] - - # RibbonVolumeToSurfaceMapping.sh - # Line 85 thru ... - volume_to_surface = pe.Node( - VolumeToSurfaceMapping(method='ribbon-constrained'), - name='volume_to_surface', - mem_gb=mem_gb * 3, - n_procs=omp_nthreads, - ) - metric_dilate = pe.Node( - MetricDilate(distance=10, nearest=True), - name='metric_dilate', - mem_gb=1, - n_procs=omp_nthreads, - ) - resample_to_template = pe.Node( - MetricResample(method='ADAP_BARY_AREA', area_surfs=True), - name='resample_to_template', - mem_gb=1, - n_procs=omp_nthreads, - ) - # ... line 89 - - workflow.connect([ - (inputnode, select_surfaces, [ - ('white', 'white'), - ('pial', 'pial'), - ('midthickness', 'midthickness'), - ('midthickness_resampled', 'midthickness_resampled'), - ('sphere_reg_fsLR', 'sphere_reg_fsLR'), - ]), - (hemisource, select_surfaces, [('hemi', 'key')]), - # Resample BOLD to native surface, dilate and mask - (inputnode, volume_to_surface, [ - ('bold_file', 'volume_file'), - ('volume_roi', 'volume_roi'), - ]), - (select_surfaces, volume_to_surface, [ - ('midthickness', 'surface_file'), - ('white', 'inner_surface'), - ('pial', 'outer_surface'), - ]), - (select_surfaces, metric_dilate, [('midthickness', 'surf_file')]), - (volume_to_surface, metric_dilate, [('out_file', 'in_file')]), - # Resample BOLD to output space and mask - (select_surfaces, resample_to_template, [ - ('sphere_reg_fsLR', 'current_sphere'), - ('template_sphere', 'new_sphere'), - ('midthickness', 'current_area'), - ('midthickness_resampled', 'new_area'), - ]), - (metric_dilate, resample_to_template, [('out_file', 'in_file')]), - # Output - (resample_to_template, joinnode, [('out_file', 'bold_resampled')]), - (joinnode, outputnode, [('bold_resampled', 'bold_resampled')]), - ]) # fmt:skip - - return workflow - - def init_bold_fsLR_resampling_wf( grayord_density: ty.Literal['91k', '170k'], omp_nthreads: int, From 9be14e0af38c2c53138ba3e4b0cd7aa35acf9512 Mon Sep 17 00:00:00 2001 From: Ma Feilong Date: Fri, 2 May 2025 14:19:23 -0400 Subject: [PATCH 7/7] feat: Resample to surface template spaces using the Connectome Workbench. --- fmriprep/workflows/bold/base.py | 109 ++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/fmriprep/workflows/bold/base.py b/fmriprep/workflows/bold/base.py index 060db8b08..c7f6f7830 100644 --- a/fmriprep/workflows/bold/base.py +++ b/fmriprep/workflows/bold/base.py @@ -498,6 +498,115 @@ def init_bold_wf( ]), ]) # fmt:skip + surf_std = spaces.get_standard(dim=(2,)) + if surf_std: # Probably ensure reconall and msmsulc are run + workflow.__postdesc__ += """\ +Non-gridded (surface) resamplings were performed using the Connectome +Workbench. +""" + config.loggers.workflow.debug('Creating BOLD surface workbench resampling workflow.') + from smriprep.workflows.surfaces import init_resample_surfaces_wb_wf + + from .resampling import ( + init_goodvoxels_bold_mask_wf, + init_wb_surf_surf_wf, + init_wb_vol_surf_wf, + ) + + wb_vol_surf_wf = init_wb_vol_surf_wf( + name='wb_vol_surf_wf', + omp_nthreads=omp_nthreads, + mem_gb=mem_gb['resampled'], + dilate=True, + ) + workflow.connect([ + (inputnode, wb_vol_surf_wf,[ + ('white', 'inputnode.white'), + ('pial', 'inputnode.pial'), + ('midthickness', 'inputnode.midthickness'), + ]), + (bold_anat_wf, wb_vol_surf_wf, [ + ('outputnode.bold_file', 'inputnode.bold_file'), + ]), + ]) # fmt:skip + + if config.workflow.project_goodvoxels: + goodvoxels_bold_mask_wf = init_goodvoxels_bold_mask_wf(mem_gb['resampled']) + + workflow.connect([ + (inputnode, goodvoxels_bold_mask_wf, [('anat_ribbon', 'inputnode.anat_ribbon')]), + (bold_anat_wf, goodvoxels_bold_mask_wf, [ + ('outputnode.bold_file', 'inputnode.bold_file'), + ]), + (goodvoxels_bold_mask_wf, wb_vol_surf_wf, [ + ('outputnode.goodvoxels_mask', 'inputnode.volume_roi'), + ]), + ]) # fmt:skip + workflow.__desc__ += """\ +A "goodvoxels" mask was applied during volume-to-surface sampling, excluding +voxels whose time-series have a locally high coefficient of variation. +""" + + for ref_ in surf_std: + space, den = ref_.space, ref_.spec['den'] + + resample_surfaces_wb_wf = init_resample_surfaces_wb_wf( + name=f'resample_surfaces_wb_wf_{space}_{den}', + surfaces=['midthickness'], + space=space, + density=den, + ) + + wb_surf_surf_wf = init_wb_surf_surf_wf( + space=space, + density=den, + name=f'wb_surf_surf_wf_{space}_{den}', + omp_nthreads=omp_nthreads, + mem_gb=mem_gb['resampled'], + ) + + ds_bold_surf_wb = pe.Node( + DerivativesDataSink( + base_directory=fmriprep_dir, + hemi=['L', 'R'], + dismiss_entities=dismiss_echo(), + space=space, + density=den, + suffix='bold', + # compress=False, # not sure if needed for gii. + TaskName=all_metadata[0].get('TaskName'), + extension='.func.gii', + **prepare_timing_parameters(all_metadata[0]), + ), + iterfield=('in_file', 'hemi'), + name=f'ds_bold_surf_wb_{space}_{den}', + run_without_submitting=True, + ) + ds_bold_surf_wb.inputs.source_file = bold_file + + workflow.connect([ + (inputnode, resample_surfaces_wb_wf, [ + ('midthickness', 'inputnode.midthickness'), + ('sphere_reg_fsLR', 'inputnode.sphere_reg_fsLR'), + ]), + (wb_vol_surf_wf, wb_surf_surf_wf, [ + ('outputnode.bold_fsnative', 'inputnode.bold_fsnative'), + ]), + (inputnode, wb_surf_surf_wf, [ + ('midthickness', 'inputnode.midthickness'), + # # TODO: check inputnode.midthickness_resampled + # ('midthickness_resampled', 'inputnode.midthickness_resampled'), + ('sphere_reg_fsLR', 'inputnode.sphere_reg_fsLR'), + ]), + (resample_surfaces_wb_wf, wb_surf_surf_wf, [ + ('outputnode.midthickness_resampled', 'inputnode.midthickness_resampled'), + ]), + (wb_surf_surf_wf, ds_bold_surf_wb, [ + ('outputnode.bold_resampled', 'in_file'), + # TODO: json metadata? + ]), + ]) # fmt:skip + if config.workflow.run_reconall and freesurfer_spaces: workflow.__postdesc__ += """\ Non-gridded (surface) resamplings were performed using `mri_vol2surf`