diff --git a/smriprep/interfaces/gifti.py b/smriprep/interfaces/gifti.py index 22f9925201..3d9d33c008 100644 --- a/smriprep/interfaces/gifti.py +++ b/smriprep/interfaces/gifti.py @@ -5,7 +5,7 @@ from nipype.interfaces.base import File, SimpleInterface, TraitedSpec, isdefined, traits -class InvertShapeInputSpec(TraitedSpec): +class MetricMathInputSpec(TraitedSpec): subject_id = traits.Str(desc='subject ID') hemisphere = traits.Enum( "L", @@ -13,50 +13,68 @@ class InvertShapeInputSpec(TraitedSpec): mandatory=True, desc='hemisphere', ) - shape = traits.Str(desc='name of shape to invert') - shape_file = File(exists=True, mandatory=True, desc='input GIFTI file') + metric = traits.Str(desc='name of metric to invert') + metric_file = File(exists=True, mandatory=True, desc='input GIFTI file') + operation = traits.Enum( + "invert", + "abs", + "bin", + mandatory=True, + desc='operation to perform', + ) -class InvertShapeOutputSpec(TraitedSpec): - shape_file = File(desc='output GIFTI file') +class MetricMathOutputSpec(TraitedSpec): + metric_file = File(desc='output GIFTI file') -class InvertShape(SimpleInterface): - """Prepare GIFTI shape file for use in MSMSulc +class MetricMath(SimpleInterface): + """Prepare GIFTI metric file for use in MSMSulc This interface mirrors the action of the following portion of FreeSurfer2CaretConvertAndRegisterNonlinear.sh:: - wb_command -set-structure ${shape_file} CORTEX_[LEFT|RIGHT] - wb_command -metric-math "var * -1" ${shape_file} -var var ${shape_file} - wb_command -set-map-names ${shape_file} -map 1 ${subject}_[L|R]_${shape} + wb_command -set-structure ${metric_file} CORTEX_[LEFT|RIGHT] + wb_command -metric-math "var * -1" ${metric_file} -var var ${metric_file} + wb_command -set-map-names ${metric_file} -map 1 ${subject}_[L|R]_${metric} + # If abs: + wb_command -metric-math "abs(var)" ${metric_file} -var var ${metric_file} We do not add palette information to the output file. """ - input_spec = InvertShapeInputSpec - output_spec = InvertShapeOutputSpec + input_spec = MetricMathInputSpec + output_spec = MetricMathOutputSpec def _run_interface(self, runtime): - subject, hemi, shape = self.inputs.subject_id, self.inputs.hemisphere, self.inputs.shape + subject, hemi, metric = self.inputs.subject_id, self.inputs.hemisphere, self.inputs.metric if not isdefined(subject): subject = 'sub-XYZ' - img = nb.GiftiImage.from_filename(self.inputs.shape_file) + img = nb.GiftiImage.from_filename(self.inputs.metric_file) # wb_command -set-structure img.meta["AnatomicalStructurePrimary"] = {'L': 'CortexLeft', 'R': 'CortexRight'}[hemi] darray = img.darrays[0] # wb_command -set-map-names meta = darray.meta - meta['Name'] = f"{subject}_{hemi}_{shape}" - - # wb_command -metric-math "var * -1" - inv = -darray.data + meta['Name'] = f"{subject}_{hemi}_{metric}" + + datatype = darray.datatype + if self.inputs.operation == "abs": + # wb_command -metric-math "abs(var)" + data = abs(darray.data) + elif self.inputs.operation == "invert": + # wb_command -metric-math "var * -1" + data = -darray.data + elif self.inputs.operation == "bin": + # wb_command -metric-math "var > 0" + data = darray.data > 0 + datatype = 'uint8' darray = nb.gifti.GiftiDataArray( - inv, + data, intent=darray.intent, - datatype=darray.datatype, + datatype=datatype, encoding=darray.encoding, endian=darray.endian, coordsys=darray.coordsys, @@ -65,7 +83,7 @@ def _run_interface(self, runtime): ) img.darrays[0] = darray - out_filename = os.path.join(runtime.cwd, f"{subject}.{hemi}.{shape}.native.shape.gii") + out_filename = os.path.join(runtime.cwd, f"{subject}.{hemi}.{metric}.native.shape.gii") img.to_filename(out_filename) - self._results["shape_file"] = out_filename + self._results["metric_file"] = out_filename return runtime diff --git a/smriprep/workflows/anatomical.py b/smriprep/workflows/anatomical.py index 23139ade94..a6b90f6d68 100644 --- a/smriprep/workflows/anatomical.py +++ b/smriprep/workflows/anatomical.py @@ -55,6 +55,8 @@ from .fit.registration import init_register_template_wf from .outputs import ( init_anat_reports_wf, + init_ds_anat_volumes_wf, + init_ds_grayord_metrics_wf, init_ds_surface_metrics_wf, init_ds_surfaces_wf, init_ds_template_wf, @@ -64,16 +66,20 @@ init_ds_template_registration_wf, init_ds_fs_registration_wf, init_anat_second_derivatives_wf, + init_template_iterator_wf, ) from .surfaces import ( init_anat_ribbon_wf, init_fsLR_reg_wf, init_gifti_morphometrics_wf, + init_hcp_morphometrics_wf, init_gifti_surfaces_wf, + init_morph_grayords_wf, init_msm_sulc_wf, init_surface_derivatives_wf, init_surface_recon_wf, init_refinement_wf, + init_resample_midthickness_wf, ) LOGGER = logging.getLogger("nipype.workflow") @@ -113,6 +119,8 @@ def init_anat_preproc_wf( from niworkflows.utils.spaces import SpatialReferences, Reference from smriprep.workflows.anatomical import init_anat_preproc_wf + spaces = SpatialReferences(spaces=['MNI152NLin2009cAsym', 'fsaverage5']) + spaces.checkpoint() wf = init_anat_preproc_wf( bids_root='.', output_dir='.', @@ -124,7 +132,7 @@ def init_anat_preproc_wf( t2w=[], skull_strip_mode='force', skull_strip_template=Reference('OASIS30ANTs'), - spaces=SpatialReferences(spaces=['MNI152NLin2009cAsym', 'fsaverage5']), + spaces=spaces, precomputed={}, omp_nthreads=1, ) @@ -261,14 +269,13 @@ def init_anat_preproc_wf( omp_nthreads=omp_nthreads, skull_strip_fixed_seed=skull_strip_fixed_seed, ) - anat_second_derivatives_wf = init_anat_second_derivatives_wf( + template_iterator_wf = init_template_iterator_wf(spaces=spaces) + ds_std_volumes_wf = init_ds_anat_volumes_wf( bids_root=bids_root, - freesurfer=freesurfer, output_dir=output_dir, - spaces=spaces, - cifti_output=cifti_output, + name="ds_std_volumes_wf", ) - # fmt:off + workflow.connect([ (inputnode, anat_fit_wf, [ ("t1w", "inputnode.t1w"), @@ -293,19 +300,32 @@ def init_anat_preproc_wf( (f"outputnode.sphere_reg_{'msm' if msm_sulc else 'fsLR'}", "sphere_reg_fsLR"), ("outputnode.anat_ribbon", "anat_ribbon"), ]), - (anat_fit_wf, anat_second_derivatives_wf, [ - ('outputnode.template', 'inputnode.template'), + (anat_fit_wf, template_iterator_wf, [ + ("outputnode.template", "inputnode.template"), + ("outputnode.anat2std_xfm", "inputnode.anat2std_xfm"), + ]), + (anat_fit_wf, ds_std_volumes_wf, [ ('outputnode.t1w_valid_list', 'inputnode.source_files'), ("outputnode.t1w_preproc", "inputnode.t1w_preproc"), ("outputnode.t1w_mask", "inputnode.t1w_mask"), ("outputnode.t1w_dseg", "inputnode.t1w_dseg"), ("outputnode.t1w_tpms", "inputnode.t1w_tpms"), - ('outputnode.anat2std_xfm', 'inputnode.anat2std_xfm'), ]), - ]) - # fmt:on - if freesurfer: + (template_iterator_wf, ds_std_volumes_wf, [ + ("outputnode.std_t1w", "inputnode.ref_file"), + ("outputnode.anat2std_xfm", "inputnode.anat2std_xfm"), + ("outputnode.space", "inputnode.space"), + ("outputnode.cohort", "inputnode.cohort"), + ("outputnode.resolution", "inputnode.resolution"), + ]), + ]) # fmt:skip + if freesurfer: + anat_second_derivatives_wf = init_anat_second_derivatives_wf( + bids_root=bids_root, + output_dir=output_dir, + cifti_output=cifti_output, + ) surface_derivatives_wf = init_surface_derivatives_wf( cifti_output=cifti_output, ) @@ -316,7 +336,6 @@ def init_anat_preproc_wf( bids_root=bids_root, output_dir=output_dir, metrics=["curv"], name="ds_curv_wf" ) - # fmt:off workflow.connect([ (anat_fit_wf, surface_derivatives_wf, [ ('outputnode.t1w_preproc', 'inputnode.reference'), @@ -336,18 +355,78 @@ def init_anat_preproc_wf( (surface_derivatives_wf, ds_curv_wf, [ ('outputnode.curv', 'inputnode.curv'), ]), + (anat_fit_wf, anat_second_derivatives_wf, [ + ('outputnode.t1w_valid_list', 'inputnode.source_files'), + ]), (surface_derivatives_wf, anat_second_derivatives_wf, [ ('outputnode.out_aseg', 'inputnode.t1w_fs_aseg'), ('outputnode.out_aparc', 'inputnode.t1w_fs_aparc'), - ('outputnode.cifti_morph', 'inputnode.cifti_morph'), - ('outputnode.cifti_metadata', 'inputnode.cifti_metadata'), ]), (surface_derivatives_wf, outputnode, [ ('outputnode.out_aseg', 't1w_aseg'), ('outputnode.out_aparc', 't1w_aparc'), ]), - ]) - # fmt:on + ]) # fmt:skip + + if cifti_output: + hcp_morphometrics_wf = init_hcp_morphometrics_wf(omp_nthreads=omp_nthreads) + resample_midthickness_wf = init_resample_midthickness_wf(grayord_density=cifti_output) + morph_grayords_wf = init_morph_grayords_wf( + grayord_density=cifti_output, omp_nthreads=omp_nthreads + ) + + ds_grayord_metrics_wf = init_ds_grayord_metrics_wf( + bids_root=bids_root, + output_dir=output_dir, + metrics=['curv', 'thickness', 'sulc'], + cifti_output=cifti_output, + ) + + workflow.connect([ + (anat_fit_wf, hcp_morphometrics_wf, [ + ('outputnode.subject_id', 'inputnode.subject_id'), + ('outputnode.sulc', 'inputnode.sulc'), + ('outputnode.thickness', 'inputnode.thickness'), + ('outputnode.midthickness', 'inputnode.midthickness'), + ]), + (surface_derivatives_wf, hcp_morphometrics_wf, [ + ('outputnode.curv', 'inputnode.curv'), + ]), + (anat_fit_wf, resample_midthickness_wf, [ + ('outputnode.midthickness', 'inputnode.midthickness'), + ( + f"outputnode.sphere_reg_{'msm' if msm_sulc else 'fsLR'}", + "inputnode.sphere_reg_fsLR", + ), + ]), + (anat_fit_wf, morph_grayords_wf, [ + ('outputnode.midthickness', 'inputnode.midthickness'), + ( + f"outputnode.sphere_reg_{'msm' if msm_sulc else 'fsLR'}", + "inputnode.sphere_reg_fsLR", + ), + ]), + (hcp_morphometrics_wf, morph_grayords_wf, [ + ('outputnode.curv', 'inputnode.curv'), + ('outputnode.sulc', 'inputnode.sulc'), + ('outputnode.thickness', 'inputnode.thickness'), + ('outputnode.roi', 'inputnode.roi'), + ]), + (resample_midthickness_wf, morph_grayords_wf, [ + ('outputnode.midthickness_fsLR', 'inputnode.midthickness_fsLR'), + ]), + (anat_fit_wf, ds_grayord_metrics_wf, [ + ('outputnode.t1w_valid_list', 'inputnode.source_files'), + ]), + (morph_grayords_wf, ds_grayord_metrics_wf, [ + ('outputnode.curv_fsLR', 'inputnode.curv'), + ('outputnode.curv_metadata', 'inputnode.curv_metadata'), + ('outputnode.thickness_fsLR', 'inputnode.thickness'), + ('outputnode.thickness_metadata', 'inputnode.thickness_metadata'), + ('outputnode.sulc_fsLR', 'inputnode.sulc'), + ('outputnode.sulc_metadata', 'inputnode.sulc_metadata'), + ]), + ]) # fmt:skip return workflow @@ -1264,7 +1343,7 @@ def init_anat_fit_wf( # fmt:on elif msm_sulc: LOGGER.info("ANAT Stage 10: Found pre-computed MSM-Sulc registration sphere") - fsLR_buffer.inputs.sphere_reg_msm = sorted(precomputed["sphere_reg_msm"]) + msm_buffer.inputs.sphere_reg_msm = sorted(precomputed["sphere_reg_msm"]) else: LOGGER.info("ANAT Stage 10: MSM-Sulc disabled") diff --git a/smriprep/workflows/base.py b/smriprep/workflows/base.py index 008c172e58..0c2dc70262 100644 --- a/smriprep/workflows/base.py +++ b/smriprep/workflows/base.py @@ -81,6 +81,8 @@ def init_smriprep_wf( os.environ['FREESURFER_HOME'] = os.getcwd() from smriprep.workflows.base import init_smriprep_wf from niworkflows.utils.spaces import SpatialReferences, Reference + spaces = SpatialReferences(spaces=['MNI152NLin2009cAsym', 'fsaverage5']) + spaces.checkpoint() wf = init_smriprep_wf( sloppy=False, debug=False, @@ -98,7 +100,7 @@ def init_smriprep_wf( skull_strip_fixed_seed=False, skull_strip_mode='force', skull_strip_template=Reference('OASIS30ANTs'), - spaces=SpatialReferences(spaces=['MNI152NLin2009cAsym', 'fsaverage5']), + spaces=spaces, subject_list=['smripreptest'], work_dir='.', bids_filters=None, @@ -250,6 +252,8 @@ def init_single_subject_wf( from niworkflows.utils.spaces import SpatialReferences, Reference from smriprep.workflows.base import init_single_subject_wf BIDSLayout = namedtuple('BIDSLayout', ['root']) + spaces = SpatialReferences(spaces=['MNI152NLin2009cAsym', 'fsaverage5']) + spaces.checkpoint() wf = init_single_subject_wf( sloppy=False, debug=False, @@ -266,7 +270,7 @@ def init_single_subject_wf( skull_strip_fixed_seed=False, skull_strip_mode='force', skull_strip_template=Reference('OASIS30ANTs'), - spaces=SpatialReferences(spaces=['MNI152NLin2009cAsym', 'fsaverage5']), + spaces=spaces, subject_id='test', bids_filters=None, cifti_output=None, diff --git a/smriprep/workflows/outputs.py b/smriprep/workflows/outputs.py index 99211b0faa..e6df726074 100644 --- a/smriprep/workflows/outputs.py +++ b/smriprep/workflows/outputs.py @@ -26,11 +26,10 @@ from nipype.pipeline import engine as pe from nipype.interfaces import utility as niu from niworkflows.interfaces.fixes import FixHeaderApplyTransforms as ApplyTransforms -from niworkflows.interfaces.nibabel import ApplyMask +from niworkflows.interfaces.nibabel import ApplyMask, GenerateSamplingReference from niworkflows.interfaces.space import SpaceDataSource from niworkflows.interfaces.utility import KeySelect from niworkflows.engine.workflows import LiterateWorkflow as Workflow -from niworkflows.utils.spaces import SpatialReferences from ..interfaces import DerivativesDataSink from ..interfaces.templateflow import TemplateFlowSelect @@ -804,12 +803,224 @@ def init_ds_surface_metrics_wf( return workflow +def init_ds_grayord_metrics_wf( + *, + bids_root: str, + output_dir: str, + metrics: list[str], + cifti_output: ty.Literal["91k", "170k"], + name="ds_grayord_metrics_wf", +) -> Workflow: + """ + Save CIFTI-2 surface metrics + + Parameters + ---------- + bids_root : :class:`str` + Root path of BIDS dataset + output_dir : :class:`str` + Directory in which to save derivatives + metrics : :class:`str` + List of metrics to generate DataSinks for + cifti_output : :class:`str` + Density of CIFTI-2 files to save + name : :class:`str` + Workflow name (default: ds_surface_metrics_wf) + + Inputs + ------ + source_files + List of input T1w images + ```` + CIFTI-2 scalar file for each metric passed to ``metrics`` + ``_metadata`` + JSON file containing metadata for each metric passed to ``metrics`` + + Outputs + ------- + ```` + CIFTI-2 scalar file in ``output_dir`` for each metric passed to ``metrics`` + + """ + workflow = Workflow(name=name) + + inputnode = pe.Node( + niu.IdentityInterface( + fields=["source_files"] + metrics + [f"{m}_metadata" for m in metrics] + ), + name="inputnode", + ) + outputnode = pe.Node(niu.IdentityInterface(fields=metrics), name="outputnode") + + for metric in metrics: + ds_metric = pe.Node( + DerivativesDataSink( + base_directory=output_dir, + space='fsLR', + density=cifti_output, + suffix=metric, + compress=False, + extension=".dscalar.nii", + ), + name=f"ds_{metric}", + run_without_submitting=True, + ) + + workflow.connect([ + (inputnode, ds_metric, [ + ('source_files', 'source_file'), + (metric, 'in_file'), + ((f'{metric}_metadata', _read_json), 'meta_dict'), + ]), + (ds_metric, outputnode, [('out_file', metric)]), + ]) # fmt:skip + + return workflow + + +def init_ds_anat_volumes_wf( + *, + bids_root: str, + output_dir: str, + name="ds_anat_volumes_wf", + tpm_labels=BIDS_TISSUE_ORDER, +) -> pe.Workflow: + + workflow = pe.Workflow(name=name) + inputnode = pe.Node( + niu.IdentityInterface( + fields=[ + # Original T1w image + 'source_files', + # T1w-space images + 't1w_preproc', + 't1w_mask', + 't1w_dseg', + 't1w_tpms', + # Template + 'ref_file', + 'anat2std_xfm', + # Entities + 'space', + 'cohort', + 'resolution', + ] + ), + name='inputnode', + ) + + raw_sources = pe.Node(niu.Function(function=_bids_relative), name='raw_sources') + raw_sources.inputs.bids_root = bids_root + + gen_ref = pe.Node(GenerateSamplingReference(), name="gen_ref", mem_gb=0.01) + + # Mask T1w preproc images + mask_t1w = pe.Node(ApplyMask(), name='mask_t1w') + + # Resample T1w-space inputs + anat2std_t1w = pe.Node( + ApplyTransforms( + dimension=3, + default_value=0, + float=True, + interpolation="LanczosWindowedSinc", + ), + name="anat2std_t1w", + ) + + anat2std_mask = pe.Node(ApplyTransforms(interpolation="MultiLabel"), name="anat2std_mask") + anat2std_dseg = pe.Node(ApplyTransforms(interpolation="MultiLabel"), name="anat2std_dseg") + anat2std_tpms = pe.MapNode( + ApplyTransforms(dimension=3, default_value=0, float=True, interpolation="Gaussian"), + iterfield=["input_image"], + name="anat2std_tpms", + ) + + ds_std_t1w = pe.Node( + DerivativesDataSink( + base_directory=output_dir, + desc="preproc", + compress=True, + ), + name="ds_std_t1w", + run_without_submitting=True, + ) + ds_std_t1w.inputs.SkullStripped = True + + ds_std_mask = pe.Node( + DerivativesDataSink( + base_directory=output_dir, desc="brain", suffix="mask", compress=True + ), + name="ds_std_mask", + run_without_submitting=True, + ) + ds_std_mask.inputs.Type = "Brain" + + ds_std_dseg = pe.Node( + DerivativesDataSink(base_directory=output_dir, suffix="dseg", compress=True), + name="ds_std_dseg", + run_without_submitting=True, + ) + + ds_std_tpms = pe.Node( + DerivativesDataSink(base_directory=output_dir, suffix="probseg", compress=True), + name="ds_std_tpms", + run_without_submitting=True, + ) + + # CRITICAL: the sequence of labels here (CSF-GM-WM) is that of the output of FSL-FAST + # (intensity mean, per tissue). This order HAS to be matched also by the ``tpms`` + # output in the data/io_spec.json file. + ds_std_tpms.inputs.label = tpm_labels + + workflow.connect([ + (inputnode, gen_ref, [ + ("ref_file", "fixed_image"), + (("resolution", _is_native), "keep_native"), + ]), + (inputnode, mask_t1w, [ + ("t1w_preproc", "in_file"), + ("t1w_mask", "in_mask"), + ]), + (mask_t1w, anat2std_t1w, [("out_file", "input_image")]), + (inputnode, anat2std_mask, [("t1w_mask", "input_image")]), + (inputnode, anat2std_dseg, [("t1w_dseg", "input_image")]), + (inputnode, anat2std_tpms, [("t1w_tpms", "input_image")]), + (inputnode, gen_ref, [("t1w_preproc", "moving_image")]), + (anat2std_t1w, ds_std_t1w, [("output_image", "in_file")]), + (anat2std_mask, ds_std_mask, [("output_image", "in_file")]), + (anat2std_dseg, ds_std_dseg, [("output_image", "in_file")]), + (anat2std_tpms, ds_std_tpms, [("output_image", "in_file")]), + ]) # fmt:skip + + workflow.connect( + # Connect apply transforms nodes + [ + (gen_ref, n, [('out_file', 'reference_image')]) + for n in (anat2std_t1w, anat2std_mask, anat2std_dseg, anat2std_tpms) + ] + + [ + (inputnode, n, [('anat2std_xfm', 'transforms')]) + for n in (anat2std_t1w, anat2std_mask, anat2std_dseg, anat2std_tpms) + ] + + [ + (inputnode, n, [ + ('source_files', 'source_file'), + ('space', 'space'), + ('cohort', 'cohort'), + ('resolution', 'resolution'), + ]) + for n in (ds_std_t1w, ds_std_mask, ds_std_dseg, ds_std_tpms) + ] + ) # fmt:skip + + return workflow + + def init_anat_second_derivatives_wf( *, bids_root: str, output_dir: str, - freesurfer: bool, - spaces: SpatialReferences, cifti_output: ty.Literal["91k", "170k", False], name="anat_second_derivatives_wf", tpm_labels=BIDS_TISSUE_ORDER, @@ -821,8 +1032,6 @@ def init_anat_second_derivatives_wf( ---------- bids_root : :obj:`str` Root path of BIDS dataset - freesurfer : :obj:`bool` - FreeSurfer was enabled output_dir : :obj:`str` Directory in which to save derivatives name : :obj:`str` @@ -869,17 +1078,8 @@ def init_anat_second_derivatives_wf( fields=[ "template", "source_files", - "t1w_preproc", - "t1w_mask", - "t1w_dseg", - "t1w_tpms", - "anat2std_xfm", - "sphere_reg", - "sphere_reg_fsLR", "t1w_fs_aseg", "t1w_fs_aparc", - "cifti_morph", - "cifti_metadata", ] ), name="inputnode", @@ -888,128 +1088,6 @@ def init_anat_second_derivatives_wf( raw_sources = pe.Node(niu.Function(function=_bids_relative), name="raw_sources") raw_sources.inputs.bids_root = bids_root - # Write derivatives in standard spaces specified by --output-spaces - if getattr(spaces, "_cached") is not None and spaces.cached.references: - from niworkflows.interfaces.nibabel import GenerateSamplingReference - - template_iterator_wf = init_template_iterator_wf(spaces=spaces) - - gen_ref = pe.Node(GenerateSamplingReference(), name="gen_ref", mem_gb=0.01) - - # Mask T1w preproc images - mask_t1w = pe.Node(ApplyMask(), name='mask_t1w') - - # Resample T1w-space inputs - anat2std_t1w = pe.Node( - ApplyTransforms( - dimension=3, - default_value=0, - float=True, - interpolation="LanczosWindowedSinc", - ), - name="anat2std_t1w", - ) - - anat2std_mask = pe.Node(ApplyTransforms(interpolation="MultiLabel"), name="anat2std_mask") - anat2std_dseg = pe.Node(ApplyTransforms(interpolation="MultiLabel"), name="anat2std_dseg") - anat2std_tpms = pe.MapNode( - ApplyTransforms(dimension=3, default_value=0, float=True, interpolation="Gaussian"), - iterfield=["input_image"], - name="anat2std_tpms", - ) - - ds_std_t1w = pe.Node( - DerivativesDataSink( - base_directory=output_dir, - desc="preproc", - compress=True, - ), - name="ds_std_t1w", - run_without_submitting=True, - ) - ds_std_t1w.inputs.SkullStripped = True - - ds_std_mask = pe.Node( - DerivativesDataSink( - base_directory=output_dir, desc="brain", suffix="mask", compress=True - ), - name="ds_std_mask", - run_without_submitting=True, - ) - ds_std_mask.inputs.Type = "Brain" - - ds_std_dseg = pe.Node( - DerivativesDataSink(base_directory=output_dir, suffix="dseg", compress=True), - name="ds_std_dseg", - run_without_submitting=True, - ) - - ds_std_tpms = pe.Node( - DerivativesDataSink(base_directory=output_dir, suffix="probseg", compress=True), - name="ds_std_tpms", - run_without_submitting=True, - ) - - # CRITICAL: the sequence of labels here (CSF-GM-WM) is that of the output of FSL-FAST - # (intensity mean, per tissue). This order HAS to be matched also by the ``tpms`` - # output in the data/io_spec.json file. - ds_std_tpms.inputs.label = tpm_labels - # fmt:off - workflow.connect([ - (inputnode, template_iterator_wf, [ - ("anat2std_xfm", "inputnode.anat2std_xfm"), - ("template", "inputnode.template"), - ]), - (inputnode, mask_t1w, [("t1w_preproc", "in_file"), - ("t1w_mask", "in_mask")]), - (mask_t1w, anat2std_t1w, [("out_file", "input_image")]), - (inputnode, anat2std_mask, [("t1w_mask", "input_image")]), - (inputnode, anat2std_dseg, [("t1w_dseg", "input_image")]), - (inputnode, anat2std_tpms, [("t1w_tpms", "input_image")]), - (inputnode, gen_ref, [("t1w_preproc", "moving_image")]), - (template_iterator_wf, gen_ref, [ - ("outputnode.std_t1w", "fixed_image"), - (("outputnode.resolution", _is_native), "keep_native"), - ]), - (anat2std_t1w, ds_std_t1w, [("output_image", "in_file")]), - (anat2std_mask, ds_std_mask, [("output_image", "in_file")]), - (anat2std_dseg, ds_std_dseg, [("output_image", "in_file")]), - (anat2std_tpms, ds_std_tpms, [("output_image", "in_file")]), - (template_iterator_wf, ds_std_mask, [ - (("outputnode.std_mask", _drop_path), "RawSources"), - ]), - ]) - - workflow.connect( - # Connect apply transforms nodes - [ - (gen_ref, n, [('out_file', 'reference_image')]) - for n in (anat2std_t1w, anat2std_mask, anat2std_dseg, anat2std_tpms) - ] - + [ - (template_iterator_wf, n, [('outputnode.anat2std_xfm', 'transforms')]) - for n in (anat2std_t1w, anat2std_mask, anat2std_dseg, anat2std_tpms) - ] - # Connect the source_file input of these datasinks - + [ - (inputnode, n, [('source_files', 'source_file')]) - for n in (ds_std_t1w, ds_std_mask, ds_std_dseg, ds_std_tpms) - ] - # Connect the space input of these datasinks - + [ - (template_iterator_wf, n, [ - ('outputnode.space', 'space'), - ('outputnode.cohort', 'cohort'), - ('outputnode.resolution', 'resolution'), - ]) - for n in (ds_std_t1w, ds_std_mask, ds_std_dseg, ds_std_tpms) - ] - ) - # fmt:on - - if not freesurfer: - return workflow - # Parcellations ds_t1w_fsaseg = pe.Node( DerivativesDataSink(base_directory=output_dir, desc="aseg", suffix="dseg", compress=True), @@ -1033,26 +1111,6 @@ def init_anat_second_derivatives_wf( ]) # fmt:on - if cifti_output: - ds_cifti_morph = pe.MapNode( - DerivativesDataSink( - base_directory=output_dir, - density=cifti_output, - suffix=['curv', 'sulc', 'thickness'], - compress=False, - space='fsLR', - ), - name='ds_cifti_morph', - run_without_submitting=True, - iterfield=["in_file", "meta_dict", "suffix"], - ) - # fmt:off - workflow.connect([ - (inputnode, ds_cifti_morph, [('cifti_morph', 'in_file'), - ('source_files', 'source_file'), - (('cifti_metadata', _read_jsons), 'meta_dict')]) - ]) - # fmt:on return workflow @@ -1241,8 +1299,8 @@ def _combine_cohort(in_template): return [_combine_cohort(v) for v in in_template] -def _read_jsons(in_file): +def _read_json(in_file): from json import loads from pathlib import Path - return [loads(Path(f).read_text()) for f in in_file] + return loads(Path(in_file).read_text()) diff --git a/smriprep/workflows/surfaces.py b/smriprep/workflows/surfaces.py index 53e6f64550..71fe9d3557 100644 --- a/smriprep/workflows/surfaces.py +++ b/smriprep/workflows/surfaces.py @@ -28,28 +28,32 @@ """ import typing as ty -from nipype.pipeline import engine as pe + +from nipype.interfaces import freesurfer as fs +from nipype.interfaces import io as nio +from nipype.interfaces import utility as niu from nipype.interfaces.base import Undefined -from nipype.interfaces import ( - io as nio, - utility as niu, - freesurfer as fs, - workbench as wb, +from nipype.pipeline import engine as pe +from niworkflows.engine.workflows import LiterateWorkflow as Workflow +from niworkflows.interfaces.freesurfer import FSDetectInputs, FSInjectBrainExtracted +from niworkflows.interfaces.freesurfer import PatchedRobustRegister as RobustRegister +from niworkflows.interfaces.freesurfer import RefineBrainMask +from niworkflows.interfaces.nitransforms import ConcatenateXFMs +from niworkflows.interfaces.utility import KeySelect +from niworkflows.interfaces.workbench import ( + MetricDilate, + MetricFillHoles, + MetricMask, + MetricRemoveIslands, + MetricResample, ) from smriprep.interfaces.surf import MakeRibbon +from smriprep.interfaces.workbench import SurfaceResample from ..data import load_resource -from ..interfaces.freesurfer import ReconAll, MakeMidthickness - -from niworkflows.engine.workflows import LiterateWorkflow as Workflow -from niworkflows.interfaces.freesurfer import ( - FSDetectInputs, - FSInjectBrainExtracted, - PatchedRobustRegister as RobustRegister, - RefineBrainMask, -) -from niworkflows.interfaces.nitransforms import ConcatenateXFMs +from ..interfaces.freesurfer import MakeMidthickness, ReconAll +from ..interfaces.gifti import MetricMath from ..interfaces.workbench import CreateSignedDistanceVolume @@ -637,8 +641,6 @@ def init_surface_derivatives_wf( "curv", "out_aseg", "out_aparc", - "cifti_morph", - "cifti_metadata", ] ), name="outputnode", @@ -682,21 +684,6 @@ def init_surface_derivatives_wf( ]) # fmt:on - if cifti_output: - morph_grayords_wf = init_morph_grayords_wf(grayord_density=cifti_output) - # fmt:off - workflow.connect([ - (inputnode, morph_grayords_wf, [ - ('subject_id', 'inputnode.subject_id'), - ('subjects_dir', 'inputnode.subjects_dir'), - ]), - (morph_grayords_wf, outputnode, [ - ("outputnode.cifti_morph", "cifti_morph"), - ("outputnode.cifti_metadata", "cifti_metadata"), - ]), - ]) - # fmt:on - return workflow @@ -742,7 +729,6 @@ def init_fsLR_reg_wf(*, name="fsLR_reg_wf"): def init_msm_sulc_wf(*, sloppy: bool = False, name: str = 'msm_sulc_wf'): """Run MSMSulc registration to fsLR surfaces, per hemisphere.""" - from ..interfaces.gifti import InvertShape from ..interfaces.msm import MSM from ..interfaces.workbench import ( SurfaceAffineRegression, @@ -789,8 +775,8 @@ def init_msm_sulc_wf(*, sloppy: bool = False, name: str = 'msm_sulc_wf'): # 2) Invert sulc # wb_command -metric-math "-1 * var" ... invert_sulc = pe.MapNode( - InvertShape(shape='sulc'), - iterfield=['shape_file', 'hemisphere'], + MetricMath(metric='sulc', operation='invert'), + iterfield=['metric_file', 'hemisphere'], name='invert_sulc', ) invert_sulc.inputs.hemisphere = ['L', 'R'] @@ -823,11 +809,11 @@ def init_msm_sulc_wf(*, sloppy: bool = False, name: str = 'msm_sulc_wf'): ('sphere_reg_fsLR', 'target_surface'), ]), (inputnode, apply_surface_affine, [('sphere', 'in_surface')]), - (inputnode, invert_sulc, [('sulc', 'shape_file')]), + (inputnode, invert_sulc, [('sulc', 'metric_file')]), (regress_affine, apply_surface_affine, [('out_affine', 'in_affine')]), (apply_surface_affine, modify_sphere, [('out_surface', 'in_surface')]), (modify_sphere, msmsulc, [('out_surface', 'in_mesh')]), - (invert_sulc, msmsulc, [('shape_file', 'in_data')]), + (invert_sulc, msmsulc, [('metric_file', 'in_data')]), (msmsulc, outputnode, [('warped_mesh', 'sphere_reg_fsLR')]), ]) # fmt:skip return workflow @@ -1024,6 +1010,149 @@ def init_gifti_morphometrics_wf( return workflow +def init_hcp_morphometrics_wf( + *, + omp_nthreads: int, + name: str = "hcp_morphometrics_wf", +) -> Workflow: + """Convert FreeSurfer-style morphometrics to HCP-style. + + The HCP uses different conventions for curvature and sulcal depth from FreeSurfer, + and performs some geometric preprocessing steps to get final metrics and a + subject-specific cortical ROI. + + Workflow Graph + + .. workflow:: + + from smriprep.workflows.surfaces import init_hcp_morphometrics_wf + wf = init_hcp_morphometrics_wf(omp_nthreads=1) + + Parameters + ---------- + omp_nthreads + Maximum number of threads an individual process may use + + Inputs + ------ + subject_id + FreeSurfer subject ID + thickness + FreeSurfer thickness file in GIFTI format + curv + FreeSurfer curvature file in GIFTI format + sulc + FreeSurfer sulcal depth file in GIFTI format + + Outputs + ------- + thickness + HCP-style thickness file in GIFTI format + curv + HCP-style curvature file in GIFTI format + sulc + HCP-style sulcal depth file in GIFTI format + roi + HCP-style cortical ROI file in GIFTI format + """ + DEFAULT_MEMORY_MIN_GB = 0.01 + + workflow = Workflow(name=name) + + inputnode = pe.Node( + niu.IdentityInterface(fields=['subject_id', 'thickness', 'curv', 'sulc', 'midthickness']), + name='inputnode', + ) + + itersource = pe.Node( + niu.IdentityInterface(fields=['hemi']), + name='itersource', + iterables=[('hemi', ['L', 'R'])], + ) + + outputnode = pe.JoinNode( + niu.IdentityInterface(fields=["thickness", "curv", "sulc", "roi"]), + name="outputnode", + joinsource="itersource", + ) + + select_surfaces = pe.Node( + KeySelect( + fields=[ + "thickness", + "curv", + "sulc", + "midthickness", + ], + keys=["L", "R"], + ), + name="select_surfaces", + run_without_submitting=True, + ) + + # HCP inverts curvature and sulcal depth relative to FreeSurfer + invert_curv = pe.Node(MetricMath(metric='curv', operation='invert'), name='invert_curv') + invert_sulc = pe.Node(MetricMath(metric='sulc', operation='invert'), name='invert_sulc') + # Thickness is presumably already positive, but HCP uses abs(-thickness) + abs_thickness = pe.Node(MetricMath(metric='thickness', operation='abs'), name='abs_thickness') + + # Native ROI is thickness > 0, with holes and islands filled + initial_roi = pe.Node(MetricMath(metric='roi', operation='bin'), name='initial_roi') + fill_holes = pe.Node(MetricFillHoles(), name="fill_holes", mem_gb=DEFAULT_MEMORY_MIN_GB) + native_roi = pe.Node(MetricRemoveIslands(), name="native_roi", mem_gb=DEFAULT_MEMORY_MIN_GB) + + # Dilation happens separately from ROI creation + dilate_curv = pe.Node( + MetricDilate(distance=10, nearest=True), + name="dilate_curv", + n_procs=omp_nthreads, + mem_gb=DEFAULT_MEMORY_MIN_GB, + ) + dilate_thickness = pe.Node( + MetricDilate(distance=10, nearest=True), + name="dilate_thickness", + n_procs=omp_nthreads, + mem_gb=DEFAULT_MEMORY_MIN_GB, + ) + + workflow.connect([ + (inputnode, select_surfaces, [ + ('thickness', 'thickness'), + ('curv', 'curv'), + ('sulc', 'sulc'), + ('midthickness', 'midthickness'), + ]), + (inputnode, invert_curv, [('subject_id', 'subject_id')]), + (inputnode, invert_sulc, [('subject_id', 'subject_id')]), + (inputnode, abs_thickness, [('subject_id', 'subject_id')]), + (itersource, select_surfaces, [('hemi', 'key')]), + (itersource, invert_curv, [('hemi', 'hemisphere')]), + (itersource, invert_sulc, [('hemi', 'hemisphere')]), + (itersource, abs_thickness, [('hemi', 'hemisphere')]), + (select_surfaces, invert_curv, [('curv', 'metric_file')]), + (select_surfaces, invert_sulc, [('sulc', 'metric_file')]), + (select_surfaces, abs_thickness, [('thickness', 'metric_file')]), + (select_surfaces, dilate_curv, [('midthickness', 'surf_file')]), + (select_surfaces, dilate_thickness, [('midthickness', 'surf_file')]), + (invert_curv, dilate_curv, [('metric_file', 'in_file')]), + (abs_thickness, dilate_thickness, [('metric_file', 'in_file')]), + (dilate_curv, outputnode, [('out_file', 'curv')]), + (dilate_thickness, outputnode, [('out_file', 'thickness')]), + (invert_sulc, outputnode, [('metric_file', 'sulc')]), + # Native ROI file from thickness + (inputnode, initial_roi, [('subject_id', 'subject_id')]), + (itersource, initial_roi, [('hemi', 'hemisphere')]), + (abs_thickness, initial_roi, [('metric_file', 'metric_file')]), + (select_surfaces, fill_holes, [('midthickness', 'surface_file')]), + (select_surfaces, native_roi, [('midthickness', 'surface_file')]), + (initial_roi, fill_holes, [('metric_file', 'metric_file')]), + (fill_holes, native_roi, [('out_file', 'metric_file')]), + (native_roi, outputnode, [('out_file', 'roi')]), + ]) # fmt:skip + + return workflow + + def init_segs_to_native_wf(*, name="segs_to_native", segmentation="aseg"): """ Get a segmentation from FreeSurfer conformed space into native T1w space. @@ -1180,8 +1309,87 @@ def init_anat_ribbon_wf(name="anat_ribbon_wf"): return workflow +def init_resample_midthickness_wf( + grayord_density: ty.Literal['91k', '170k'], + name: str = "resample_midthickness_wf", +): + """ + Resample subject midthickness surface to specified density. + + Workflow Graph + .. workflow:: + :graph2use: colored + :simple_form: yes + + from smriprep.workflows.surfaces import init_resample_midthickness_wf + wf = init_resample_midthickness_wf(grayord_density="91k") + + Parameters + ---------- + grayord_density : :obj:`str` + Either `91k` or `170k`, representing the total of vertices or *grayordinates*. + name : :obj:`str` + Unique name for the subworkflow (default: ``"resample_midthickness_wf"``) + + Inputs + ------ + midthickness + GIFTI surface mesh corresponding to the midthickness surface + sphere_reg_fsLR + GIFTI surface mesh corresponding to the subject's fsLR registration sphere + + Outputs + ------- + midthickness + GIFTI surface mesh corresponding to the midthickness surface, resampled to fsLR + """ + import templateflow.api as tf + from niworkflows.engine.workflows import LiterateWorkflow as Workflow + + workflow = Workflow(name=name) + + fslr_density = "32k" if grayord_density == "91k" else "59k" + + inputnode = pe.Node( + niu.IdentityInterface(fields=["midthickness", "sphere_reg_fsLR"]), + name="inputnode", + ) + + outputnode = pe.Node(niu.IdentityInterface(fields=["midthickness_fsLR"]), name="outputnode") + + resampler = pe.MapNode( + SurfaceResample(method='BARYCENTRIC'), + iterfield=['surface_in', 'current_sphere', 'new_sphere'], + name="resampler", + ) + resampler.inputs.new_sphere = [ + str( + tf.get( + template='fsLR', + density=fslr_density, + suffix='sphere', + hemi=hemi, + space=None, + extension='.surf.gii', + ) + ) + for hemi in ['L', 'R'] + ] + + workflow.connect([ + (inputnode, resampler, [ + ('midthickness', 'surface_in'), + ('sphere_reg_fsLR', 'current_sphere'), + ]), + (resampler, outputnode, [('surface_out', 'midthickness_fsLR')]), + ]) # fmt:skip + + return workflow + + def init_morph_grayords_wf( grayord_density: ty.Literal['91k', '170k'], + omp_nthreads: int, name: str = "morph_grayords_wf", ): """ @@ -1195,7 +1403,7 @@ def init_morph_grayords_wf( :simple_form: yes from smriprep.workflows.surfaces import init_morph_grayords_wf - wf = init_morph_grayords_wf(grayord_density="91k") + wf = init_morph_grayords_wf(grayord_density="91k", omp_nthreads=1) Parameters ---------- @@ -1206,163 +1414,168 @@ def init_morph_grayords_wf( Inputs ------ - subject_id : :obj:`str` - FreeSurfer subject ID - subjects_dir : :obj:`str` - FreeSurfer SUBJECTS_DIR + curv + HCP-style curvature file in GIFTI format + sulc + HCP-style sulcal depth file in GIFTI format + thickness + HCP-style thickness file in GIFTI format + roi + HCP-style cortical ROI file in GIFTI format + midthickness + GIFTI surface mesh corresponding to the midthickness surface + sphere_reg_fsLR + GIFTI surface mesh corresponding to the subject's fsLR registration sphere Outputs ------- - cifti_morph : :obj:`list` of :obj:`str` - Paths of CIFTI dscalar files - cifti_metadata : :obj:`list` of :obj:`str` - Paths to JSON files containing metadata corresponding to ``cifti_morph`` - + curv_fsLR + HCP-style curvature file in CIFTI-2 format, resampled to fsLR + curv_metadata + Path to JSON file containing metadata corresponding to ``curv_fsLR`` + sulc_fsLR + HCP-style sulcal depth file in CIFTI-2 format, resampled to fsLR + sulc_metadata + Path to JSON file containing metadata corresponding to ``sulc_fsLR`` + thickness_fsLR + HCP-style thickness file in CIFTI-2 format, resampled to fsLR """ import templateflow.api as tf from niworkflows.engine.workflows import LiterateWorkflow as Workflow + from smriprep.interfaces.cifti import GenerateDScalar workflow = Workflow(name=name) workflow.__desc__ = f"""\ -*Grayordinate* "dscalar" files [@hcppipelines] containing {grayord_density} samples were -also generated using the highest-resolution ``fsaverage`` as an intermediate standardized -surface space. +*Grayordinate* "dscalar" files containing {grayord_density} samples were +resampled onto fsLR using the Connectome Workbench [@hcppipelines]. """ fslr_density = "32k" if grayord_density == "91k" else "59k" inputnode = pe.Node( - niu.IdentityInterface(fields=["subject_id", "subjects_dir"]), + niu.IdentityInterface( + fields=[ + "curv", + "sulc", + "thickness", + "roi", + "midthickness", + "midthickness_fsLR", + "sphere_reg_fsLR", + ] + ), name="inputnode", ) + # Note we have to use a different name than itersource to + # avoid duplicates in the workflow graph, even though the + # previous was already Joined + hemisource = pe.Node( + niu.IdentityInterface(fields=['hemi']), + name='hemisource', + iterables=[('hemi', ['L', 'R'])], + ) + outputnode = pe.Node( - niu.IdentityInterface(fields=["cifti_morph", "cifti_metadata"]), + niu.IdentityInterface( + fields=[ + "curv_fsLR", + "curv_metadata", + "sulc_fsLR", + "sulc_metadata", + "thickness_fsLR", + "thickness_metadata", + ] + ), name="outputnode", ) - get_surfaces = pe.Node(nio.FreeSurferSource(), name="get_surfaces") - - surfmorph_list = pe.Node( - niu.Merge(3, ravel_inputs=True), - name="surfmorph_list", + atlases = load_resource('atlases') + select_surfaces = pe.Node( + KeySelect( + fields=[ + "curv", + "sulc", + "thickness", + "roi", + "midthickness", + "midthickness_fsLR", + "sphere_reg_fsLR", + "template_sphere", + "template_roi", + ], + keys=["L", "R"], + ), + name="select_surfaces", run_without_submitting=True, ) - - surf2surf = pe.MapNode( - fs.SurfaceTransform(target_subject="fsaverage", target_type="gii"), - iterfield=["source_file", "hemi"], - name="surf2surf", - mem_gb=0.01, - ) - surf2surf.inputs.hemi = ["lh", "rh"] * 3 - - # Setup Workbench command. LR ordering for hemi can be assumed, as it is imposed - # by the iterfield of the MapNode in the surface sampling workflow above. - resample = pe.MapNode( - wb.MetricResample(method="ADAP_BARY_AREA", area_metrics=True), - name="resample", - iterfield=[ - "in_file", - "out_file", - "new_sphere", - "new_area", - "current_sphere", - "current_area", - ], - ) - resample.inputs.current_sphere = [ + select_surfaces.inputs.template_sphere = [ str( tf.get( - "fsaverage", - hemi=hemi, - density="164k", - desc="std", - suffix="sphere", - extension=".surf.gii", - ) - ) - for hemi in "LR" - ] * 3 - resample.inputs.current_area = [ - str( - tf.get( - "fsaverage", - hemi=hemi, - density="164k", - desc="vaavg", - suffix="midthickness", - extension=".shape.gii", - ) - ) - for hemi in "LR" - ] * 3 - resample.inputs.new_sphere = [ - str( - tf.get( - "fsLR", - space="fsaverage", - hemi=hemi, + template='fsLR', density=fslr_density, - suffix="sphere", - extension=".surf.gii", - ) - ) - for hemi in "LR" - ] * 3 - resample.inputs.new_area = [ - str( - tf.get( - "fsLR", + suffix='sphere', hemi=hemi, - density=fslr_density, - desc="vaavg", - suffix="midthickness", - extension=".shape.gii", + space=None, + extension='.surf.gii', ) ) - for hemi in "LR" - ] * 3 - resample.inputs.out_file = [ - f"space-fsLR_hemi-{h}_den-{grayord_density}_{morph}.shape.gii" - # Order: curv-L, curv-R, sulc-L, sulc-R, thickness-L, thickness-R - for morph in ('curv', 'sulc', 'thickness') - for h in "LR" + for hemi in ['L', 'R'] + ] + select_surfaces.inputs.template_roi = [ + str(atlases / f'L.atlasroi.{fslr_density}_fs_LR.shape.gii'), + str(atlases / f'R.atlasroi.{fslr_density}_fs_LR.shape.gii'), ] - gen_cifti = pe.MapNode( - GenerateDScalar( - grayordinates=grayord_density, - ), - iterfield=['scalar_name', 'scalar_surfs'], - name="gen_cifti", - ) - gen_cifti.inputs.scalar_name = ['curv', 'sulc', 'thickness'] - - # fmt: off workflow.connect([ - (inputnode, get_surfaces, [ - ('subject_id', 'subject_id'), - ('subjects_dir', 'subjects_dir'), - ]), - (inputnode, surf2surf, [ - ('subject_id', 'source_subject'), - ('subjects_dir', 'subjects_dir'), + (inputnode, select_surfaces, [ + ('curv', 'curv'), + ('sulc', 'sulc'), + ('thickness', 'thickness'), + ('roi', 'roi'), + ('midthickness', 'midthickness'), + ('midthickness_fsLR', 'midthickness_fsLR'), + ('sphere_reg_fsLR', 'sphere_reg_fsLR'), ]), - (get_surfaces, surfmorph_list, [ - (('curv', _sorted_by_basename), 'in1'), - (('sulc', _sorted_by_basename), 'in2'), - (('thickness', _sorted_by_basename), 'in3'), - ]), - (surfmorph_list, surf2surf, [('out', 'source_file')]), - (surf2surf, resample, [('out_file', 'in_file')]), - (resample, gen_cifti, [ - (("out_file", _collate), "scalar_surfs")]), - (gen_cifti, outputnode, [("out_file", "cifti_morph"), - ("out_metadata", "cifti_metadata")]), - ]) - # fmt: on + (hemisource, select_surfaces, [('hemi', 'key')]), + ]) # fmt:skip + + for metric in ('curv', 'sulc', 'thickness'): + resampler = pe.Node( + MetricResample(method='ADAP_BARY_AREA', area_surfs=True), + name=f"resample_{metric}", + n_procs=omp_nthreads, + ) + mask_fsLR = pe.Node( + MetricMask(), + name=f"mask_{metric}", + n_procs=omp_nthreads, + ) + cifti_metric = pe.JoinNode( + GenerateDScalar(grayordinates=grayord_density, scalar_name=metric), + name=f"cifti_{metric}", + joinfield=['scalar_surfs'], + joinsource="hemisource", + ) + + workflow.connect([ + (select_surfaces, resampler, [ + (metric, 'in_file'), + ('sphere_reg_fsLR', 'current_sphere'), + ('template_sphere', 'new_sphere'), + ('midthickness', 'current_area'), + ('midthickness_fsLR', 'new_area'), + ('roi', 'roi_metric'), + ]), + (select_surfaces, mask_fsLR, [('template_roi', 'mask')]), + (resampler, mask_fsLR, [('out_file', 'in_file')]), + (mask_fsLR, cifti_metric, [('out_file', 'scalar_surfs')]), + (cifti_metric, outputnode, [ + ('out_file', f'{metric}_fsLR'), + ('out_metadata', f'{metric}_metadata'), + ]), + ]) # fmt:skip return workflow