def synthstrip_wf(name="synthstrip_wf", omp_nthreads=None): """Create a brain-extraction workflow using SynthStrip.""" from nipype.interfaces.ants import N4BiasFieldCorrection from niworkflows.interfaces.nibabel import IntensityClip, ApplyMask from mriqc.interfaces.synthstrip import SynthStrip inputnode = pe.Node(niu.IdentityInterface(fields=["in_files"]), name="inputnode") outputnode = pe.Node( niu.IdentityInterface( fields=["out_corrected", "out_brain", "bias_image", "out_mask"]), name="outputnode", ) # truncate target intensity for N4 correction pre_clip = pe.Node(IntensityClip(p_min=10, p_max=99.9), name="pre_clip") pre_n4 = pe.Node( N4BiasFieldCorrection( dimension=3, num_threads=omp_nthreads, rescale_intensities=True, copy_header=True, ), name="pre_n4", ) post_n4 = pe.Node( N4BiasFieldCorrection( dimension=3, save_bias=True, num_threads=omp_nthreads, n_iterations=[50] * 4, copy_header=True, ), name="post_n4", ) synthstrip = pe.Node( SynthStrip(), name="synthstrip", ) final_masked = pe.Node(ApplyMask(), name="final_masked") final_inu = pe.Node(niu.Function(function=_apply_bias_correction), name="final_inu") workflow = pe.Workflow(name=name) # fmt: off workflow.connect([ (inputnode, final_inu, [("in_files", "in_file")]), (inputnode, pre_clip, [("in_files", "in_file")]), (pre_clip, pre_n4, [("out_file", "input_image")]), (pre_n4, synthstrip, [("output_image", "in_file")]), (synthstrip, post_n4, [("out_mask", "weight_image")]), (synthstrip, final_masked, [("out_mask", "in_mask")]), (pre_clip, post_n4, [("out_file", "input_image")]), (post_n4, final_inu, [("bias_image", "bias_image")]), (post_n4, final_masked, [("output_image", "in_file")]), (final_masked, outputnode, [("out_file", "out_brain")]), (post_n4, outputnode, [("bias_image", "bias_image")]), (synthstrip, outputnode, [("out_mask", "out_mask")]), (post_n4, outputnode, [("output_image", "out_corrected")]), ]) # fmt: on return workflow
def init_rodent_brain_extraction_wf( ants_affine_init=False, factor=20, arc=0.12, step=4, grid=(0, 4, 4), debug=False, interim_checkpoints=True, mem_gb=3.0, mri_scheme="T2w", name="rodent_brain_extraction_wf", omp_nthreads=None, output_dir=None, template_id="Fischer344", template_specs=None, use_float=True, ): """ Build an atlas-based brain extraction pipeline for rodent T1w and T2w MRI data. Parameters ---------- ants_affine_init : :obj:`bool`, optional Set-up a pre-initialization step with ``antsAI`` to account for mis-oriented images. """ inputnode = pe.Node(niu.IdentityInterface(fields=["in_files", "in_mask"]), name="inputnode") outputnode = pe.Node( niu.IdentityInterface( fields=["out_corrected", "out_brain", "out_mask"]), name="outputnode", ) template_specs = template_specs or {} if template_id == "WHS" and "resolution" not in template_specs: template_specs["resolution"] = 2 # Find a suitable target template in TemplateFlow tpl_target_path = get_template( template_id, suffix=mri_scheme, **template_specs, ) if not tpl_target_path: raise RuntimeError( f"An instance of template <tpl-{template_id}> with MR scheme '{mri_scheme}'" " could not be found.") tpl_brainmask_path = get_template( template_id, atlas=None, hemi=None, desc="brain", suffix="probseg", **template_specs, ) or get_template( template_id, atlas=None, hemi=None, desc="brain", suffix="mask", **template_specs, ) tpl_regmask_path = get_template( template_id, atlas=None, desc="BrainCerebellumExtraction", suffix="mask", **template_specs, ) denoise = pe.Node(DenoiseImage(dimension=3, copy_header=True), name="denoise", n_procs=omp_nthreads) # Resample template to a controlled, isotropic resolution res_tmpl = pe.Node(RegridToZooms(zooms=HIRES_ZOOMS, smooth=True), name="res_tmpl") # Create Laplacian images lap_tmpl = pe.Node(ImageMath(operation="Laplacian", copy_header=True), name="lap_tmpl") tmpl_sigma = pe.Node(niu.Function(function=_lap_sigma), name="tmpl_sigma", run_without_submitting=True) norm_lap_tmpl = pe.Node(niu.Function(function=_norm_lap), name="norm_lap_tmpl") lap_target = pe.Node(ImageMath(operation="Laplacian", copy_header=True), name="lap_target") target_sigma = pe.Node(niu.Function(function=_lap_sigma), name="target_sigma", run_without_submitting=True) norm_lap_target = pe.Node(niu.Function(function=_norm_lap), name="norm_lap_target") # Set up initial spatial normalization ants_params = "testing" if debug else "precise" norm = pe.Node( Registration(from_file=pkgr_fn( "nirodents", f"data/artsBrainExtraction_{ants_params}_{mri_scheme}.json")), name="norm", n_procs=omp_nthreads, mem_gb=mem_gb, ) norm.inputs.float = use_float # main workflow wf = pe.Workflow(name) # truncate target intensity for N4 correction clip_target = pe.Node(IntensityClip(p_min=15, p_max=99.9), name="clip_target") # truncate template intensity to match target clip_tmpl = pe.Node(IntensityClip(p_min=5, p_max=98), name="clip_tmpl") clip_tmpl.inputs.in_file = _pop(tpl_target_path) # set INU bspline grid based on voxel size bspline_grid = pe.Node(niu.Function(function=_bspline_grid), name="bspline_grid") # INU correction of the target image init_n4 = pe.Node( N4BiasFieldCorrection( dimension=3, save_bias=False, copy_header=True, n_iterations=[50] * (4 - debug), convergence_threshold=1e-7, shrink_factor=4, rescale_intensities=True, ), n_procs=omp_nthreads, name="init_n4", ) clip_inu = pe.Node(IntensityClip(p_min=1, p_max=99.8), name="clip_inu") # Create a buffer interface as a cache for the actual inputs to registration buffernode = pe.Node(niu.IdentityInterface(fields=["hires_target"]), name="buffernode") # Merge image nodes mrg_target = pe.Node(niu.Merge(2), name="mrg_target") mrg_tmpl = pe.Node(niu.Merge(2), name="mrg_tmpl") # fmt: off wf.connect([ # Target image massaging (inputnode, denoise, [(("in_files", _pop), "input_image")]), (inputnode, bspline_grid, [(("in_files", _pop), "in_file")]), (bspline_grid, init_n4, [("out", "args")]), (denoise, clip_target, [("output_image", "in_file")]), (clip_target, init_n4, [("out_file", "input_image")]), (init_n4, clip_inu, [("output_image", "in_file")]), (clip_inu, target_sigma, [("out_file", "in_file")]), (clip_inu, buffernode, [("out_file", "hires_target")]), (buffernode, lap_target, [("hires_target", "op1")]), (target_sigma, lap_target, [("out", "op2")]), (lap_target, norm_lap_target, [("output_image", "in_file")]), (buffernode, mrg_target, [("hires_target", "in1")]), (norm_lap_target, mrg_target, [("out", "in2")]), # Template massaging (clip_tmpl, res_tmpl, [("out_file", "in_file")]), (res_tmpl, tmpl_sigma, [("out_file", "in_file")]), (res_tmpl, lap_tmpl, [("out_file", "op1")]), (tmpl_sigma, lap_tmpl, [("out", "op2")]), (lap_tmpl, norm_lap_tmpl, [("output_image", "in_file")]), (res_tmpl, mrg_tmpl, [("out_file", "in1")]), (norm_lap_tmpl, mrg_tmpl, [("out", "in2")]), # Setup inputs to spatial normalization (mrg_target, norm, [("out", "moving_image")]), (mrg_tmpl, norm, [("out", "fixed_image")]), ]) # fmt: on # Graft a template registration-mask if present if tpl_regmask_path: hires_mask = pe.Node( ApplyTransforms( input_image=_pop(tpl_regmask_path), transforms="identity", interpolation="Gaussian", float=True, ), name="hires_mask", mem_gb=1, ) # fmt: off wf.connect([ (res_tmpl, hires_mask, [("out_file", "reference_image")]), (hires_mask, norm, [("output_image", "fixed_image_masks")]), ]) # fmt: on # Finally project brain mask and refine INU correction map_brainmask = pe.Node( ApplyTransforms(interpolation="Gaussian", float=True), name="map_brainmask", mem_gb=1, ) map_brainmask.inputs.input_image = str(tpl_brainmask_path) thr_brainmask = pe.Node(Binarize(thresh_low=0.50), name="thr_brainmask") final_n4 = pe.Node( N4BiasFieldCorrection( dimension=3, save_bias=True, copy_header=True, n_iterations=[50] * 4, convergence_threshold=1e-7, rescale_intensities=True, shrink_factor=4, ), n_procs=omp_nthreads, name="final_n4", ) final_mask = pe.Node(ApplyMask(), name="final_mask") # fmt: off wf.connect([ (inputnode, map_brainmask, [(("in_files", _pop), "reference_image")]), (bspline_grid, final_n4, [("out", "args")]), (denoise, final_n4, [("output_image", "input_image")]), # Project template's brainmask into subject space (norm, map_brainmask, [("reverse_transforms", "transforms"), ("reverse_invert_flags", "invert_transform_flags")]), (map_brainmask, thr_brainmask, [("output_image", "in_file")]), # take a second pass of N4 (map_brainmask, final_n4, [("output_image", "mask_image")]), (final_n4, final_mask, [("output_image", "in_file")]), (thr_brainmask, final_mask, [("out_mask", "in_mask")]), (final_n4, outputnode, [("output_image", "out_corrected")]), (thr_brainmask, outputnode, [("out_mask", "out_mask")]), (final_mask, outputnode, [("out_file", "out_brain")]), ]) # fmt: on if interim_checkpoints: final_apply = pe.Node( ApplyTransforms(interpolation="BSpline", float=True), name="final_apply", mem_gb=1, ) final_report = pe.Node( SimpleBeforeAfter(after_label="target", before_label=f"tpl-{template_id}"), name="final_report", ) # fmt: off wf.connect([ (inputnode, final_apply, [(("in_files", _pop), "reference_image") ]), (res_tmpl, final_apply, [("out_file", "input_image")]), (norm, final_apply, [("reverse_transforms", "transforms"), ("reverse_invert_flags", "invert_transform_flags")]), (final_apply, final_report, [("output_image", "before")]), (outputnode, final_report, [("out_corrected", "after"), ("out_mask", "wm_seg")]), ]) # fmt: on if ants_affine_init: # Initialize transforms with antsAI lowres_tmpl = pe.Node(RegridToZooms(zooms=LOWRES_ZOOMS, smooth=True), name="lowres_tmpl") lowres_trgt = pe.Node(RegridToZooms(zooms=LOWRES_ZOOMS, smooth=True), name="lowres_trgt") init_aff = pe.Node( AI( convergence=(100, 1e-6, 10), metric=("Mattes", 32, "Random", 0.25), principal_axes=False, search_factor=(factor, arc), search_grid=(step, grid), transform=("Affine", 0.1), verbose=True, ), name="init_aff", n_procs=omp_nthreads, ) # fmt: off wf.connect([ (clip_inu, lowres_trgt, [("out_file", "in_file")]), (lowres_trgt, init_aff, [("out_file", "moving_image")]), (clip_tmpl, lowres_tmpl, [("out_file", "in_file")]), (lowres_tmpl, init_aff, [("out_file", "fixed_image")]), (init_aff, norm, [("output_transform", "initial_moving_transform") ]), ]) # fmt: on if tpl_regmask_path: lowres_mask = pe.Node( ApplyTransforms( input_image=_pop(tpl_regmask_path), transforms="identity", interpolation="MultiLabel", ), name="lowres_mask", mem_gb=1, ) # fmt: off wf.connect([ (lowres_tmpl, lowres_mask, [("out_file", "reference_image")]), (lowres_mask, init_aff, [("output_image", "fixed_image_mask") ]), ]) # fmt: on if interim_checkpoints: init_apply = pe.Node( ApplyTransforms(interpolation="BSpline", invert_transform_flags=[True]), name="init_apply", mem_gb=1, ) init_mask = pe.Node( ApplyTransforms(interpolation="Gaussian", invert_transform_flags=[True]), name="init_mask", mem_gb=1, ) init_mask.inputs.input_image = str(tpl_brainmask_path) init_report = pe.Node( SimpleBeforeAfter( out_report="init_report.svg", before_label="target", after_label="template", ), name="init_report", ) # fmt: off wf.connect([ (lowres_trgt, init_apply, [("out_file", "reference_image")]), (lowres_tmpl, init_apply, [("out_file", "input_image")]), (init_aff, init_apply, [("output_transform", "transforms")]), (lowres_trgt, init_report, [("out_file", "before")]), (init_apply, init_report, [("output_image", "after")]), (lowres_trgt, init_mask, [("out_file", "reference_image")]), (init_aff, init_mask, [("output_transform", "transforms")]), (init_mask, init_report, [("output_image", "wm_seg")]), ]) # fmt: on else: norm.inputs.initial_moving_transform_com = 1 if output_dir: ds_final_inu = pe.Node(DerivativesDataSink( base_directory=str(output_dir), desc="preproc", compress=True, ), name="ds_final_inu", run_without_submitting=True) ds_final_msk = pe.Node(DerivativesDataSink( base_directory=str(output_dir), desc="brain", suffix="mask", compress=True, ), name="ds_final_msk", run_without_submitting=True) # fmt: off wf.connect([ (inputnode, ds_final_inu, [("in_files", "source_file")]), (inputnode, ds_final_msk, [("in_files", "source_file")]), (outputnode, ds_final_inu, [("out_corrected", "in_file")]), (outputnode, ds_final_msk, [("out_mask", "in_file")]), ]) # fmt: on if interim_checkpoints: ds_report = pe.Node(DerivativesDataSink( base_directory=str(output_dir), desc="brain", suffix="mask", datatype="figures"), name="ds_report", run_without_submitting=True) # fmt: off wf.connect([ (inputnode, ds_report, [("in_files", "source_file")]), (final_report, ds_report, [("out_report", "in_file")]), ]) # fmt: on if ants_affine_init and interim_checkpoints: ds_report_init = pe.Node(DerivativesDataSink( base_directory=str(output_dir), desc="init", suffix="mask", datatype="figures"), name="ds_report_init", run_without_submitting=True) # fmt: off wf.connect([ (inputnode, ds_report_init, [("in_files", "source_file")]), (init_report, ds_report_init, [("out_report", "in_file")]), ]) # fmt: on return wf
def test_syn_wf(tmpdir, datadir, workdir, outdir): """Build and run an SDC-SyN workflow.""" derivs_path = datadir / "ds000054" / "derivatives" smriprep = derivs_path / "smriprep-0.6" / "sub-100185" / "anat" wf = pe.Workflow(name="syn_test") syn_wf = init_syn_sdc_wf(debug=True, omp_nthreads=4) syn_wf.inputs.inputnode.epi_ref = ( str(derivs_path / "sdcflows-tests" / "sub-100185_task-machinegame_run-1_boldref.nii.gz"), { "PhaseEncodingDirection": "j-", "TotalReadoutTime": 0.005 }, ) syn_wf.inputs.inputnode.epi_mask = str( derivs_path / "sdcflows-tests" / "sub-100185_task-machinegame_run-1_desc-brain_mask.nii.gz") syn_wf.inputs.inputnode.anat2epi_xfm = str(derivs_path / "sdcflows-tests" / "t1w2bold.txt") syn_wf.inputs.inputnode.std2anat_xfm = str( smriprep / "sub-100185_from-MNI152NLin2009cAsym_to-T1w_mode-image_xfm.h5") t1w_mask = pe.Node( ApplyMask( in_file=str(smriprep / "sub-100185_desc-preproc_T1w.nii.gz"), in_mask=str(smriprep / "sub-100185_desc-brain_mask.nii.gz"), ), name="t1w_mask", ) # fmt: off wf.connect([ (t1w_mask, syn_wf, [("out_file", "inputnode.anat_brain")]), ]) # fmt: on if outdir: from ...outputs import init_fmap_derivatives_wf, init_fmap_reports_wf outdir = outdir / "unittests" / "syn_test" fmap_derivatives_wf = init_fmap_derivatives_wf( output_dir=str(outdir), write_coeff=True, bids_fmap_id="sdcsyn", ) fmap_derivatives_wf.inputs.inputnode.source_files = [ str(derivs_path / "sdcflows-tests" / "sub-100185_task-machinegame_run-1_boldref.nii.gz") ] fmap_derivatives_wf.inputs.inputnode.fmap_meta = { "PhaseEncodingDirection": "j-" } fmap_reports_wf = init_fmap_reports_wf( output_dir=str(outdir), fmap_type="sdcsyn", ) fmap_reports_wf.inputs.inputnode.source_files = [ str(derivs_path / "sdcflows-tests" / "sub-100185_task-machinegame_run-1_boldref.nii.gz") ] # fmt: off wf.connect([ (syn_wf, fmap_reports_wf, [("outputnode.fmap", "inputnode.fieldmap"), ("outputnode.fmap_ref", "inputnode.fmap_ref"), ("outputnode.fmap_mask", "inputnode.fmap_mask")]), (syn_wf, fmap_derivatives_wf, [ ("outputnode.fmap", "inputnode.fieldmap"), ("outputnode.fmap_ref", "inputnode.fmap_ref"), ("outputnode.fmap_coeff", "inputnode.fmap_coeff"), ]), ]) # fmt: on if workdir: wf.base_dir = str(workdir) wf.run(plugin="Linear")
def init_bold_confs_wf( mem_gb, metadata, regressors_all_comps, regressors_dvars_th, regressors_fd_th, freesurfer=False, name="bold_confs_wf", ): """ Build a workflow to generate and write out confounding signals. This workflow calculates confounds for a BOLD series, and aggregates them into a :abbr:`TSV (tab-separated value)` file, for use as nuisance regressors in a :abbr:`GLM (general linear model)`. The following confounds are calculated, with column headings in parentheses: #. Region-wise average signal (``csf``, ``white_matter``, ``global_signal``) #. DVARS - original and standardized variants (``dvars``, ``std_dvars``) #. Framewise displacement, based on head-motion parameters (``framewise_displacement``) #. Temporal CompCor (``t_comp_cor_XX``) #. Anatomical CompCor (``a_comp_cor_XX``) #. Cosine basis set for high-pass filtering w/ 0.008 Hz cut-off (``cosine_XX``) #. Non-steady-state volumes (``non_steady_state_XX``) #. Estimated head-motion parameters, in mm and rad (``trans_x``, ``trans_y``, ``trans_z``, ``rot_x``, ``rot_y``, ``rot_z``) Prior to estimating aCompCor and tCompCor, non-steady-state volumes are censored and high-pass filtered using a :abbr:`DCT (discrete cosine transform)` basis. The cosine basis, as well as one regressor per censored volume, are included for convenience. Workflow Graph .. workflow:: :graph2use: orig :simple_form: yes from fmriprep.workflows.bold.confounds import init_bold_confs_wf wf = init_bold_confs_wf( mem_gb=1, metadata={}, regressors_all_comps=False, regressors_dvars_th=1.5, regressors_fd_th=0.5, ) Parameters ---------- mem_gb : :obj:`float` Size of BOLD file in GB - please note that this size should be calculated after resamplings that may extend the FoV metadata : :obj:`dict` BIDS metadata for BOLD file name : :obj:`str` Name of workflow (default: ``bold_confs_wf``) regressors_all_comps : :obj:`bool` Indicates whether CompCor decompositions should return all components instead of the minimal number of components necessary to explain 50 percent of the variance in the decomposition mask. regressors_dvars_th : :obj:`float` Criterion for flagging DVARS outliers regressors_fd_th : :obj:`float` Criterion for flagging framewise displacement outliers Inputs ------ bold BOLD image, after the prescribed corrections (STC, HMC and SDC) when available. bold_mask BOLD series mask movpar_file SPM-formatted motion parameters file rmsd_file Framewise displacement as measured by ``fsl_motion_outliers``. skip_vols number of non steady state volumes t1w_mask Mask of the skull-stripped template image t1w_tpms List of tissue probability maps in T1w space t1_bold_xform Affine matrix that maps the T1w space into alignment with the native BOLD space Outputs ------- confounds_file TSV of all aggregated confounds rois_report Reportlet visualizing white-matter/CSF mask used for aCompCor, the ROI for tCompCor and the BOLD brain mask. confounds_metadata Confounds metadata dictionary. """ from niworkflows.engine.workflows import LiterateWorkflow as Workflow from niworkflows.interfaces.confounds import ExpandModel, SpikeRegressors from niworkflows.interfaces.fixes import FixHeaderApplyTransforms as ApplyTransforms from niworkflows.interfaces.images import SignalExtraction from niworkflows.interfaces.masks import ROIsPlot from niworkflows.interfaces.nibabel import ApplyMask, Binarize from niworkflows.interfaces.patches import ( RobustACompCor as ACompCor, RobustTCompCor as TCompCor, ) from niworkflows.interfaces.plotting import (CompCorVariancePlot, ConfoundsCorrelationPlot) from niworkflows.interfaces.utils import (AddTSVHeader, TSV2JSON, DictMerge) from ...interfaces.confounds import aCompCorMasks gm_desc = ( "dilating a GM mask extracted from the FreeSurfer's *aseg* segmentation" if freesurfer else "thresholding the corresponding partial volume map at 0.05") workflow = Workflow(name=name) workflow.__desc__ = f"""\ Several confounding time-series were calculated based on the *preprocessed BOLD*: framewise displacement (FD), DVARS and three region-wise global signals. FD was computed using two formulations following Power (absolute sum of relative motions, @power_fd_dvars) and Jenkinson (relative root mean square displacement between affines, @mcflirt). FD and DVARS are calculated for each functional run, both using their implementations in *Nipype* [following the definitions by @power_fd_dvars]. The three global signals are extracted within the CSF, the WM, and the whole-brain masks. Additionally, a set of physiological regressors were extracted to allow for component-based noise correction [*CompCor*, @compcor]. Principal components are estimated after high-pass filtering the *preprocessed BOLD* time-series (using a discrete cosine filter with 128s cut-off) for the two *CompCor* variants: temporal (tCompCor) and anatomical (aCompCor). tCompCor components are then calculated from the top 2% variable voxels within the brain mask. For aCompCor, three probabilistic masks (CSF, WM and combined CSF+WM) are generated in anatomical space. The implementation differs from that of Behzadi et al. in that instead of eroding the masks by 2 pixels on BOLD space, the aCompCor masks are subtracted a mask of pixels that likely contain a volume fraction of GM. This mask is obtained by {gm_desc}, and it ensures components are not extracted from voxels containing a minimal fraction of GM. Finally, these masks are resampled into BOLD space and binarized by thresholding at 0.99 (as in the original implementation). Components are also calculated separately within the WM and CSF masks. For each CompCor decomposition, the *k* components with the largest singular values are retained, such that the retained components' time series are sufficient to explain 50 percent of variance across the nuisance mask (CSF, WM, combined, or temporal). The remaining components are dropped from consideration. The head-motion estimates calculated in the correction step were also placed within the corresponding confounds file. The confound time series derived from head motion estimates and global signals were expanded with the inclusion of temporal derivatives and quadratic terms for each [@confounds_satterthwaite_2013]. Frames that exceeded a threshold of {regressors_fd_th} mm FD or {regressors_dvars_th} standardised DVARS were annotated as motion outliers. """ inputnode = pe.Node(niu.IdentityInterface(fields=[ 'bold', 'bold_mask', 'movpar_file', 'rmsd_file', 'skip_vols', 't1w_mask', 't1w_tpms', 't1_bold_xform' ]), name='inputnode') outputnode = pe.Node(niu.IdentityInterface(fields=[ 'confounds_file', 'confounds_metadata', 'acompcor_masks', 'tcompcor_mask' ]), name='outputnode') # DVARS dvars = pe.Node(nac.ComputeDVARS(save_nstd=True, save_std=True, remove_zerovariance=True), name="dvars", mem_gb=mem_gb) # Frame displacement fdisp = pe.Node(nac.FramewiseDisplacement(parameter_source="SPM"), name="fdisp", mem_gb=mem_gb) # Generate aCompCor probseg maps acc_masks = pe.Node(aCompCorMasks(is_aseg=freesurfer), name="acc_masks") # Resample probseg maps in BOLD space via T1w-to-BOLD transform acc_msk_tfm = pe.MapNode(ApplyTransforms(interpolation='Gaussian', float=False), iterfield=["input_image"], name='acc_msk_tfm', mem_gb=0.1) acc_msk_brain = pe.MapNode(ApplyMask(), name="acc_msk_brain", iterfield=["in_file"]) acc_msk_bin = pe.MapNode(Binarize(thresh_low=0.99), name='acc_msk_bin', iterfield=["in_file"]) acompcor = pe.Node(ACompCor(components_file='acompcor.tsv', header_prefix='a_comp_cor_', pre_filter='cosine', save_pre_filter=True, save_metadata=True, mask_names=['CSF', 'WM', 'combined'], merge_method='none', failure_mode='NaN'), name="acompcor", mem_gb=mem_gb) tcompcor = pe.Node(TCompCor(components_file='tcompcor.tsv', header_prefix='t_comp_cor_', pre_filter='cosine', save_pre_filter=True, save_metadata=True, percentile_threshold=.02, failure_mode='NaN'), name="tcompcor", mem_gb=mem_gb) # Set number of components if regressors_all_comps: acompcor.inputs.num_components = 'all' tcompcor.inputs.num_components = 'all' else: acompcor.inputs.variance_threshold = 0.5 tcompcor.inputs.variance_threshold = 0.5 # Set TR if present if 'RepetitionTime' in metadata: tcompcor.inputs.repetition_time = metadata['RepetitionTime'] acompcor.inputs.repetition_time = metadata['RepetitionTime'] # Global and segment regressors signals_class_labels = [ "global_signal", "csf", "white_matter", "csf_wm", "tcompcor", ] merge_rois = pe.Node(niu.Merge(3, ravel_inputs=True), name='merge_rois', run_without_submitting=True) signals = pe.Node(SignalExtraction(class_labels=signals_class_labels), name="signals", mem_gb=mem_gb) # Arrange confounds add_dvars_header = pe.Node(AddTSVHeader(columns=["dvars"]), name="add_dvars_header", mem_gb=0.01, run_without_submitting=True) add_std_dvars_header = pe.Node(AddTSVHeader(columns=["std_dvars"]), name="add_std_dvars_header", mem_gb=0.01, run_without_submitting=True) add_motion_headers = pe.Node(AddTSVHeader( columns=["trans_x", "trans_y", "trans_z", "rot_x", "rot_y", "rot_z"]), name="add_motion_headers", mem_gb=0.01, run_without_submitting=True) add_rmsd_header = pe.Node(AddTSVHeader(columns=["rmsd"]), name="add_rmsd_header", mem_gb=0.01, run_without_submitting=True) concat = pe.Node(GatherConfounds(), name="concat", mem_gb=0.01, run_without_submitting=True) # CompCor metadata tcc_metadata_fmt = pe.Node(TSV2JSON( index_column='component', drop_columns=['mask'], output=None, additional_metadata={'Method': 'tCompCor'}, enforce_case=True), name='tcc_metadata_fmt') acc_metadata_fmt = pe.Node(TSV2JSON( index_column='component', output=None, additional_metadata={'Method': 'aCompCor'}, enforce_case=True), name='acc_metadata_fmt') mrg_conf_metadata = pe.Node(niu.Merge(3), name='merge_confound_metadata', run_without_submitting=True) mrg_conf_metadata.inputs.in3 = { label: { 'Method': 'Mean' } for label in signals_class_labels } mrg_conf_metadata2 = pe.Node(DictMerge(), name='merge_confound_metadata2', run_without_submitting=True) # Expand model to include derivatives and quadratics model_expand = pe.Node( ExpandModel(model_formula='(dd1(rps + wm + csf + gsr))^^2 + others'), name='model_expansion') # Add spike regressors spike_regress = pe.Node(SpikeRegressors(fd_thresh=regressors_fd_th, dvars_thresh=regressors_dvars_th), name='spike_regressors') # Generate reportlet (ROIs) mrg_compcor = pe.Node(niu.Merge(2, ravel_inputs=True), name='mrg_compcor', run_without_submitting=True) rois_plot = pe.Node(ROIsPlot(colors=['b', 'magenta'], generate_report=True), name='rois_plot', mem_gb=mem_gb) ds_report_bold_rois = pe.Node(DerivativesDataSink( desc='rois', datatype="figures", dismiss_entities=("echo", )), name='ds_report_bold_rois', run_without_submitting=True, mem_gb=DEFAULT_MEMORY_MIN_GB) # Generate reportlet (CompCor) mrg_cc_metadata = pe.Node(niu.Merge(2), name='merge_compcor_metadata', run_without_submitting=True) compcor_plot = pe.Node(CompCorVariancePlot( variance_thresholds=(0.5, 0.7, 0.9), metadata_sources=['tCompCor', 'aCompCor']), name='compcor_plot') ds_report_compcor = pe.Node(DerivativesDataSink( desc='compcorvar', datatype="figures", dismiss_entities=("echo", )), name='ds_report_compcor', run_without_submitting=True, mem_gb=DEFAULT_MEMORY_MIN_GB) # Generate reportlet (Confound correlation) conf_corr_plot = pe.Node(ConfoundsCorrelationPlot( reference_column='global_signal', max_dim=20), name='conf_corr_plot') ds_report_conf_corr = pe.Node(DerivativesDataSink( desc='confoundcorr', datatype="figures", dismiss_entities=("echo", )), name='ds_report_conf_corr', run_without_submitting=True, mem_gb=DEFAULT_MEMORY_MIN_GB) def _last(inlist): return inlist[-1] def _select_cols(table): import pandas as pd return [ col for col in pd.read_table(table, nrows=2).columns if not col.startswith(("a_comp_cor_", "t_comp_cor_", "std_dvars")) ] workflow.connect([ # connect inputnode to each non-anatomical confound node (inputnode, dvars, [('bold', 'in_file'), ('bold_mask', 'in_mask')]), (inputnode, fdisp, [('movpar_file', 'in_file')]), # aCompCor (inputnode, acompcor, [("bold", "realigned_file"), ("skip_vols", "ignore_initial_volumes")]), (inputnode, acc_masks, [("t1w_tpms", "in_vfs"), (("bold", _get_zooms), "bold_zooms")]), (inputnode, acc_msk_tfm, [("t1_bold_xform", "transforms"), ("bold_mask", "reference_image")]), (inputnode, acc_msk_brain, [("bold_mask", "in_mask")]), (acc_masks, acc_msk_tfm, [("out_masks", "input_image")]), (acc_msk_tfm, acc_msk_brain, [("output_image", "in_file")]), (acc_msk_brain, acc_msk_bin, [("out_file", "in_file")]), (acc_msk_bin, acompcor, [("out_file", "mask_files")]), # tCompCor (inputnode, tcompcor, [("bold", "realigned_file"), ("skip_vols", "ignore_initial_volumes"), ("bold_mask", "mask_files")]), # Global signals extraction (constrained by anatomy) (inputnode, signals, [('bold', 'in_file')]), (inputnode, merge_rois, [('bold_mask', 'in1')]), (acc_msk_bin, merge_rois, [('out_file', 'in2')]), (tcompcor, merge_rois, [('high_variance_masks', 'in3')]), (merge_rois, signals, [('out', 'label_files')]), # Collate computed confounds together (inputnode, add_motion_headers, [('movpar_file', 'in_file')]), (inputnode, add_rmsd_header, [('rmsd_file', 'in_file')]), (dvars, add_dvars_header, [('out_nstd', 'in_file')]), (dvars, add_std_dvars_header, [('out_std', 'in_file')]), (signals, concat, [('out_file', 'signals')]), (fdisp, concat, [('out_file', 'fd')]), (tcompcor, concat, [('components_file', 'tcompcor'), ('pre_filter_file', 'cos_basis')]), (acompcor, concat, [('components_file', 'acompcor')]), (add_motion_headers, concat, [('out_file', 'motion')]), (add_rmsd_header, concat, [('out_file', 'rmsd')]), (add_dvars_header, concat, [('out_file', 'dvars')]), (add_std_dvars_header, concat, [('out_file', 'std_dvars')]), # Confounds metadata (tcompcor, tcc_metadata_fmt, [('metadata_file', 'in_file')]), (acompcor, acc_metadata_fmt, [('metadata_file', 'in_file')]), (tcc_metadata_fmt, mrg_conf_metadata, [('output', 'in1')]), (acc_metadata_fmt, mrg_conf_metadata, [('output', 'in2')]), (mrg_conf_metadata, mrg_conf_metadata2, [('out', 'in_dicts')]), # Expand the model with derivatives, quadratics, and spikes (concat, model_expand, [('confounds_file', 'confounds_file')]), (model_expand, spike_regress, [('confounds_file', 'confounds_file')]), # Set outputs (spike_regress, outputnode, [('confounds_file', 'confounds_file')]), (mrg_conf_metadata2, outputnode, [('out_dict', 'confounds_metadata')]), (tcompcor, outputnode, [("high_variance_masks", "tcompcor_mask")]), (acc_msk_bin, outputnode, [("out_file", "acompcor_masks")]), (inputnode, rois_plot, [('bold', 'in_file'), ('bold_mask', 'in_mask')]), (tcompcor, mrg_compcor, [('high_variance_masks', 'in1')]), (acc_msk_bin, mrg_compcor, [(('out_file', _last), 'in2')]), (mrg_compcor, rois_plot, [('out', 'in_rois')]), (rois_plot, ds_report_bold_rois, [('out_report', 'in_file')]), (tcompcor, mrg_cc_metadata, [('metadata_file', 'in1')]), (acompcor, mrg_cc_metadata, [('metadata_file', 'in2')]), (mrg_cc_metadata, compcor_plot, [('out', 'metadata_files')]), (compcor_plot, ds_report_compcor, [('out_file', 'in_file')]), (concat, conf_corr_plot, [('confounds_file', 'confounds_file'), (('confounds_file', _select_cols), 'columns') ]), (conf_corr_plot, ds_report_conf_corr, [('out_file', 'in_file')]), ]) return workflow
def init_func_preproc_wf(bold_file): """ This workflow controls the functional preprocessing stages of *fMRIPrep*. Workflow Graph .. workflow:: :graph2use: orig :simple_form: yes from fmriprep.workflows.tests import mock_config from fmriprep import config from fmriprep.workflows.bold.base import init_func_preproc_wf with mock_config(): bold_file = config.execution.bids_dir / 'sub-01' / 'func' \ / 'sub-01_task-mixedgamblestask_run-01_bold.nii.gz' wf = init_func_preproc_wf(str(bold_file)) Parameters ---------- bold_file BOLD series NIfTI file Inputs ------ bold_file BOLD series NIfTI file t1w_preproc Bias-corrected structural template image t1w_mask Mask of the skull-stripped template image t1w_dseg Segmentation of preprocessed structural image, including gray-matter (GM), white-matter (WM) and cerebrospinal fluid (CSF) t1w_asec Segmentation of structural image, done with FreeSurfer. t1w_aparc Parcellation of structural image, done with FreeSurfer. t1w_tpms List of tissue probability maps in T1w space template List of templates to target anat2std_xfm List of transform files, collated with templates std2anat_xfm List of inverse transform files, collated with templates subjects_dir FreeSurfer SUBJECTS_DIR subject_id FreeSurfer subject ID t1w2fsnative_xfm LTA-style affine matrix translating from T1w to FreeSurfer-conformed subject space fsnative2t1w_xfm LTA-style affine matrix translating from FreeSurfer-conformed subject space to T1w Outputs ------- bold_t1 BOLD series, resampled to T1w space bold_mask_t1 BOLD series mask in T1w space bold_std BOLD series, resampled to template space bold_mask_std BOLD series mask in template space confounds TSV of confounds surfaces BOLD series, resampled to FreeSurfer surfaces aroma_noise_ics Noise components identified by ICA-AROMA melodic_mix FSL MELODIC mixing matrix bold_cifti BOLD CIFTI image cifti_variant combination of target spaces for `bold_cifti` See Also -------- * :py:func:`~niworkflows.func.util.init_bold_reference_wf` * :py:func:`~fmriprep.workflows.bold.stc.init_bold_stc_wf` * :py:func:`~fmriprep.workflows.bold.hmc.init_bold_hmc_wf` * :py:func:`~fmriprep.workflows.bold.t2s.init_bold_t2s_wf` * :py:func:`~fmriprep.workflows.bold.registration.init_bold_t1_trans_wf` * :py:func:`~fmriprep.workflows.bold.registration.init_bold_reg_wf` * :py:func:`~fmriprep.workflows.bold.confounds.init_bold_confounds_wf` * :py:func:`~fmriprep.workflows.bold.confounds.init_ica_aroma_wf` * :py:func:`~fmriprep.workflows.bold.resampling.init_bold_std_trans_wf` * :py:func:`~fmriprep.workflows.bold.resampling.init_bold_preproc_trans_wf` * :py:func:`~fmriprep.workflows.bold.resampling.init_bold_surf_wf` * :py:func:`~sdcflows.workflows.fmap.init_fmap_wf` * :py:func:`~sdcflows.workflows.pepolar.init_pepolar_unwarp_wf` * :py:func:`~sdcflows.workflows.phdiff.init_phdiff_wf` * :py:func:`~sdcflows.workflows.syn.init_syn_sdc_wf` * :py:func:`~sdcflows.workflows.unwarp.init_sdc_unwarp_wf` """ from niworkflows.engine.workflows import LiterateWorkflow as Workflow from niworkflows.func.util import init_bold_reference_wf from niworkflows.interfaces.nibabel import ApplyMask from niworkflows.interfaces.utility import KeySelect from niworkflows.interfaces.utils import DictMerge from sdcflows.workflows.base import init_sdc_estimate_wf, fieldmap_wrangler ref_file = bold_file mem_gb = {'filesize': 1, 'resampled': 1, 'largemem': 1} bold_tlen = 10 multiecho = isinstance(bold_file, list) # Have some options handy layout = config.execution.layout omp_nthreads = config.nipype.omp_nthreads freesurfer = config.workflow.run_reconall spaces = config.workflow.spaces if multiecho: tes = [layout.get_metadata(echo)['EchoTime'] for echo in bold_file] ref_file = dict(zip(tes, bold_file))[min(tes)] if os.path.isfile(ref_file): bold_tlen, mem_gb = _create_mem_gb(ref_file) wf_name = _get_wf_name(ref_file) config.loggers.workflow.debug( 'Creating bold processing workflow for "%s" (%.2f GB / %d TRs). ' 'Memory resampled/largemem=%.2f/%.2f GB.', ref_file, mem_gb['filesize'], bold_tlen, mem_gb['resampled'], mem_gb['largemem']) sbref_file = None # Find associated sbref, if possible entities = layout.parse_file_entities(ref_file) entities['suffix'] = 'sbref' entities['extension'] = ['nii', 'nii.gz'] # Overwrite extensions files = layout.get(return_type='file', **entities) refbase = os.path.basename(ref_file) if 'sbref' in config.workflow.ignore: config.loggers.workflow.info("Single-band reference files ignored.") elif files and multiecho: config.loggers.workflow.warning( "Single-band reference found, but not supported in " "multi-echo workflows at this time. Ignoring.") elif files: sbref_file = files[0] sbbase = os.path.basename(sbref_file) if len(files) > 1: config.loggers.workflow.warning( "Multiple single-band reference files found for {}; using " "{}".format(refbase, sbbase)) else: config.loggers.workflow.info( "Using single-band reference file %s.", sbbase) else: config.loggers.workflow.info("No single-band-reference found for %s.", refbase) metadata = layout.get_metadata(ref_file) # Find fieldmaps. Options: (phase1|phase2|phasediff|epi|fieldmap|syn) fmaps = None if 'fieldmaps' not in config.workflow.ignore: fmaps = fieldmap_wrangler(layout, ref_file, use_syn=config.workflow.use_syn, force_syn=config.workflow.force_syn) elif config.workflow.use_syn or config.workflow.force_syn: # If fieldmaps are not enabled, activate SyN-SDC in unforced (False) mode fmaps = {'syn': False} # Short circuits: (True and True and (False or 'TooShort')) == 'TooShort' run_stc = (bool(metadata.get("SliceTiming")) and 'slicetiming' not in config.workflow.ignore and (_get_series_len(ref_file) > 4 or "TooShort")) # Check if MEEPI for T2* coregistration target if config.workflow.t2s_coreg and not multiecho: config.loggers.workflow.warning( "No multiecho BOLD images found for T2* coregistration. " "Using standard EPI-T1 coregistration.") config.workflow.t2s_coreg = False # By default, force-bbr for t2s_coreg unless user specifies otherwise if config.workflow.t2s_coreg and config.workflow.use_bbr is None: config.workflow.use_bbr = True # Build workflow workflow = Workflow(name=wf_name) workflow.__postdesc__ = """\ All resamplings can be performed with *a single interpolation step* by composing all the pertinent transformations (i.e. head-motion transform matrices, susceptibility distortion correction when available, and co-registrations to anatomical and output spaces). Gridded (volumetric) resamplings were performed using `antsApplyTransforms` (ANTs), configured with Lanczos interpolation to minimize the smoothing effects of other kernels [@lanczos]. Non-gridded (surface) resamplings were performed using `mri_vol2surf` (FreeSurfer). """ inputnode = pe.Node(niu.IdentityInterface(fields=[ 'bold_file', 'subjects_dir', 'subject_id', 't1w_preproc', 't1w_mask', 't1w_dseg', 't1w_tpms', 't1w_aseg', 't1w_aparc', 'anat2std_xfm', 'std2anat_xfm', 'template', 't1w2fsnative_xfm', 'fsnative2t1w_xfm' ]), name='inputnode') inputnode.inputs.bold_file = bold_file if sbref_file is not None: from niworkflows.interfaces.images import ValidateImage val_sbref = pe.Node(ValidateImage(in_file=sbref_file), name='val_sbref') outputnode = pe.Node(niu.IdentityInterface(fields=[ 'bold_t1', 'bold_t1_ref', 'bold_mask_t1', 'bold_aseg_t1', 'bold_aparc_t1', 'bold_std', 'bold_std_ref', 'bold_mask_std', 'bold_aseg_std', 'bold_aparc_std', 'bold_native', 'bold_cifti', 'cifti_variant', 'cifti_metadata', 'cifti_density', 'surfaces', 'confounds', 'aroma_noise_ics', 'melodic_mix', 'nonaggr_denoised_file', 'confounds_metadata' ]), name='outputnode') # Generate a brain-masked conversion of the t1w t1w_brain = pe.Node(ApplyMask(), name='t1w_brain') # BOLD buffer: an identity used as a pointer to either the original BOLD # or the STC'ed one for further use. boldbuffer = pe.Node(niu.IdentityInterface(fields=['bold_file']), name='boldbuffer') summary = pe.Node(FunctionalSummary( slice_timing=run_stc, registration=('FSL', 'FreeSurfer')[freesurfer], registration_dof=config.workflow.bold2t1w_dof, registration_init=config.workflow.bold2t1w_init, pe_direction=metadata.get("PhaseEncodingDirection"), tr=metadata.get("RepetitionTime")), name='summary', mem_gb=config.DEFAULT_MEMORY_MIN_GB, run_without_submitting=True) summary.inputs.dummy_scans = config.workflow.dummy_scans func_derivatives_wf = init_func_derivatives_wf( bids_root=layout.root, cifti_output=config.workflow.cifti_output, freesurfer=freesurfer, metadata=metadata, output_dir=str(config.execution.output_dir), spaces=spaces, use_aroma=config.workflow.use_aroma, ) workflow.connect([ (outputnode, func_derivatives_wf, [ ('bold_t1', 'inputnode.bold_t1'), ('bold_t1_ref', 'inputnode.bold_t1_ref'), ('bold_aseg_t1', 'inputnode.bold_aseg_t1'), ('bold_aparc_t1', 'inputnode.bold_aparc_t1'), ('bold_mask_t1', 'inputnode.bold_mask_t1'), ('bold_native', 'inputnode.bold_native'), ('confounds', 'inputnode.confounds'), ('surfaces', 'inputnode.surf_files'), ('aroma_noise_ics', 'inputnode.aroma_noise_ics'), ('melodic_mix', 'inputnode.melodic_mix'), ('nonaggr_denoised_file', 'inputnode.nonaggr_denoised_file'), ('bold_cifti', 'inputnode.bold_cifti'), ('cifti_variant', 'inputnode.cifti_variant'), ('cifti_metadata', 'inputnode.cifti_metadata'), ('cifti_density', 'inputnode.cifti_density'), ('confounds_metadata', 'inputnode.confounds_metadata'), ]), ]) # Generate a tentative boldref bold_reference_wf = init_bold_reference_wf(omp_nthreads=omp_nthreads) bold_reference_wf.inputs.inputnode.dummy_scans = config.workflow.dummy_scans if sbref_file is not None: workflow.connect([ (val_sbref, bold_reference_wf, [('out_file', 'inputnode.sbref_file')]), ]) # Top-level BOLD splitter bold_split = pe.Node(FSLSplit(dimension='t'), name='bold_split', mem_gb=mem_gb['filesize'] * 3) # HMC on the BOLD bold_hmc_wf = init_bold_hmc_wf(name='bold_hmc_wf', mem_gb=mem_gb['filesize'], omp_nthreads=omp_nthreads) # calculate BOLD registration to T1w bold_reg_wf = init_bold_reg_wf(name='bold_reg_wf', freesurfer=freesurfer, use_bbr=config.workflow.use_bbr, bold2t1w_dof=config.workflow.bold2t1w_dof, bold2t1w_init=config.workflow.bold2t1w_init, mem_gb=mem_gb['resampled'], omp_nthreads=omp_nthreads, use_compression=False) # apply BOLD registration to T1w bold_t1_trans_wf = init_bold_t1_trans_wf(name='bold_t1_trans_wf', freesurfer=freesurfer, use_fieldwarp=bool(fmaps), multiecho=multiecho, mem_gb=mem_gb['resampled'], omp_nthreads=omp_nthreads, use_compression=False) # get confounds bold_confounds_wf = init_bold_confs_wf( mem_gb=mem_gb['largemem'], metadata=metadata, regressors_all_comps=config.workflow.regressors_all_comps, regressors_fd_th=config.workflow.regressors_fd_th, regressors_dvars_th=config.workflow.regressors_dvars_th, name='bold_confounds_wf') bold_confounds_wf.get_node('inputnode').inputs.t1_transform_flags = [False] # Apply transforms in 1 shot # Only use uncompressed output if AROMA is to be run bold_bold_trans_wf = init_bold_preproc_trans_wf( mem_gb=mem_gb['resampled'], omp_nthreads=omp_nthreads, use_compression=not config.execution.low_mem, use_fieldwarp=bool(fmaps), name='bold_bold_trans_wf') bold_bold_trans_wf.inputs.inputnode.name_source = ref_file # SLICE-TIME CORRECTION (or bypass) ############################################# if run_stc is True: # bool('TooShort') == True, so check True explicitly bold_stc_wf = init_bold_stc_wf(name='bold_stc_wf', metadata=metadata) workflow.connect([ (bold_reference_wf, bold_stc_wf, [('outputnode.skip_vols', 'inputnode.skip_vols')]), (bold_stc_wf, boldbuffer, [('outputnode.stc_file', 'bold_file')]), ]) if not multiecho: workflow.connect([(bold_reference_wf, bold_stc_wf, [ ('outputnode.bold_file', 'inputnode.bold_file') ])]) else: # for meepi, iterate through stc_wf for all workflows meepi_echos = boldbuffer.clone(name='meepi_echos') meepi_echos.iterables = ('bold_file', bold_file) workflow.connect([(meepi_echos, bold_stc_wf, [('bold_file', 'inputnode.bold_file')])]) elif not multiecho: # STC is too short or False # bypass STC from original BOLD to the splitter through boldbuffer workflow.connect([(bold_reference_wf, boldbuffer, [('outputnode.bold_file', 'bold_file')])]) else: # for meepi, iterate over all meepi echos to boldbuffer boldbuffer.iterables = ('bold_file', bold_file) # SDC (SUSCEPTIBILITY DISTORTION CORRECTION) or bypass ########################## bold_sdc_wf = init_sdc_estimate_wf(fmaps, metadata, omp_nthreads=omp_nthreads, debug=config.execution.debug) # MULTI-ECHO EPI DATA ############################################# if multiecho: from niworkflows.func.util import init_skullstrip_bold_wf skullstrip_bold_wf = init_skullstrip_bold_wf(name='skullstrip_bold_wf') inputnode.inputs.bold_file = ref_file # Replace reference w first echo join_echos = pe.JoinNode( niu.IdentityInterface(fields=['bold_files']), joinsource=('meepi_echos' if run_stc is True else 'boldbuffer'), joinfield=['bold_files'], name='join_echos') # create optimal combination, adaptive T2* map bold_t2s_wf = init_bold_t2s_wf(echo_times=tes, mem_gb=mem_gb['resampled'], omp_nthreads=omp_nthreads, t2s_coreg=config.workflow.t2s_coreg, name='bold_t2smap_wf') workflow.connect([ (skullstrip_bold_wf, join_echos, [('outputnode.skull_stripped_file', 'bold_files')]), (join_echos, bold_t2s_wf, [('bold_files', 'inputnode.bold_file')]), ]) # MAIN WORKFLOW STRUCTURE ####################################################### workflow.connect([ (inputnode, t1w_brain, [('t1w_preproc', 'in_file'), ('t1w_mask', 'in_mask')]), # Generate early reference (inputnode, bold_reference_wf, [('bold_file', 'inputnode.bold_file')]), # BOLD buffer has slice-time corrected if it was run, original otherwise (boldbuffer, bold_split, [('bold_file', 'in_file')]), # HMC (bold_reference_wf, bold_hmc_wf, [('outputnode.raw_ref_image', 'inputnode.raw_ref_image'), ('outputnode.bold_file', 'inputnode.bold_file')]), (bold_reference_wf, summary, [('outputnode.algo_dummy_scans', 'algo_dummy_scans')]), # EPI-T1 registration workflow ( inputnode, bold_reg_wf, [ ('t1w_dseg', 'inputnode.t1w_dseg'), # Undefined if --fs-no-reconall, but this is safe ('subjects_dir', 'inputnode.subjects_dir'), ('subject_id', 'inputnode.subject_id'), ('fsnative2t1w_xfm', 'inputnode.fsnative2t1w_xfm') ]), (t1w_brain, bold_reg_wf, [('out_file', 'inputnode.t1w_brain')]), (inputnode, bold_t1_trans_wf, [('bold_file', 'inputnode.name_source'), ('t1w_mask', 'inputnode.t1w_mask'), ('t1w_aseg', 'inputnode.t1w_aseg'), ('t1w_aparc', 'inputnode.t1w_aparc')]), (t1w_brain, bold_t1_trans_wf, [('out_file', 'inputnode.t1w_brain')]), # unused if multiecho, but this is safe (bold_hmc_wf, bold_t1_trans_wf, [('outputnode.xforms', 'inputnode.hmc_xforms')]), (bold_reg_wf, bold_t1_trans_wf, [('outputnode.itk_bold_to_t1', 'inputnode.itk_bold_to_t1')]), (bold_t1_trans_wf, outputnode, [('outputnode.bold_t1', 'bold_t1'), ('outputnode.bold_t1_ref', 'bold_t1_ref'), ('outputnode.bold_aseg_t1', 'bold_aseg_t1'), ('outputnode.bold_aparc_t1', 'bold_aparc_t1')]), (bold_reg_wf, summary, [('outputnode.fallback', 'fallback')]), # SDC (or pass-through workflow) (t1w_brain, bold_sdc_wf, [('out_file', 'inputnode.t1w_brain')]), (bold_reference_wf, bold_sdc_wf, [('outputnode.ref_image', 'inputnode.epi_file'), ('outputnode.ref_image_brain', 'inputnode.epi_brain'), ('outputnode.bold_mask', 'inputnode.epi_mask')]), (bold_sdc_wf, bold_t1_trans_wf, [('outputnode.out_warp', 'inputnode.fieldwarp')]), (bold_sdc_wf, bold_bold_trans_wf, [('outputnode.out_warp', 'inputnode.fieldwarp'), ('outputnode.epi_mask', 'inputnode.bold_mask')]), (bold_sdc_wf, summary, [('outputnode.method', 'distortion_correction') ]), # Connect bold_confounds_wf (inputnode, bold_confounds_wf, [('t1w_tpms', 'inputnode.t1w_tpms'), ('t1w_mask', 'inputnode.t1w_mask')]), (bold_hmc_wf, bold_confounds_wf, [('outputnode.movpar_file', 'inputnode.movpar_file')]), (bold_reg_wf, bold_confounds_wf, [('outputnode.itk_t1_to_bold', 'inputnode.t1_bold_xform')]), (bold_reference_wf, bold_confounds_wf, [('outputnode.skip_vols', 'inputnode.skip_vols')]), (bold_confounds_wf, outputnode, [ ('outputnode.confounds_file', 'confounds'), ]), (bold_confounds_wf, outputnode, [ ('outputnode.confounds_metadata', 'confounds_metadata'), ]), # Connect bold_bold_trans_wf (bold_split, bold_bold_trans_wf, [('out_files', 'inputnode.bold_file')] ), (bold_hmc_wf, bold_bold_trans_wf, [('outputnode.xforms', 'inputnode.hmc_xforms')]), # Summary (outputnode, summary, [('confounds', 'confounds_file')]), ]) if not config.workflow.t2s_coreg: workflow.connect([ (bold_sdc_wf, bold_reg_wf, [('outputnode.epi_brain', 'inputnode.ref_bold_brain')]), (bold_sdc_wf, bold_t1_trans_wf, [('outputnode.epi_brain', 'inputnode.ref_bold_brain'), ('outputnode.epi_mask', 'inputnode.ref_bold_mask')]), ]) else: workflow.connect([ # For t2s_coreg, replace EPI-to-T1w registration inputs (bold_t2s_wf, bold_reg_wf, [('outputnode.bold_ref_brain', 'inputnode.ref_bold_brain')]), (bold_t2s_wf, bold_t1_trans_wf, [('outputnode.bold_ref_brain', 'inputnode.ref_bold_brain'), ('outputnode.bold_mask', 'inputnode.ref_bold_mask')]), ]) # for standard EPI data, pass along correct file if not multiecho: workflow.connect([ (inputnode, func_derivatives_wf, [('bold_file', 'inputnode.source_file')]), (bold_bold_trans_wf, bold_confounds_wf, [('outputnode.bold', 'inputnode.bold'), ('outputnode.bold_mask', 'inputnode.bold_mask')]), (bold_split, bold_t1_trans_wf, [('out_files', 'inputnode.bold_split')]), ]) else: # for meepi, create and use optimal combination workflow.connect([ # update name source for optimal combination (inputnode, func_derivatives_wf, [(('bold_file', combine_meepi_source), 'inputnode.source_file')]), (bold_bold_trans_wf, skullstrip_bold_wf, [('outputnode.bold', 'inputnode.in_file')]), (bold_t2s_wf, bold_confounds_wf, [('outputnode.bold', 'inputnode.bold'), ('outputnode.bold_mask', 'inputnode.bold_mask')]), (bold_t2s_wf, bold_t1_trans_wf, [('outputnode.bold', 'inputnode.bold_split')]), ]) if fmaps: from sdcflows.workflows.outputs import init_sdc_unwarp_report_wf # Report on BOLD correction fmap_unwarp_report_wf = init_sdc_unwarp_report_wf() workflow.connect([ (inputnode, fmap_unwarp_report_wf, [('t1w_dseg', 'inputnode.in_seg')]), (bold_reference_wf, fmap_unwarp_report_wf, [('outputnode.ref_image', 'inputnode.in_pre')]), (bold_reg_wf, fmap_unwarp_report_wf, [('outputnode.itk_t1_to_bold', 'inputnode.in_xfm')]), (bold_sdc_wf, fmap_unwarp_report_wf, [('outputnode.epi_corrected', 'inputnode.in_post')]), ]) # Overwrite ``out_path_base`` of unwarping DataSinks for node in fmap_unwarp_report_wf.list_node_names(): if node.split('.')[-1].startswith('ds_'): fmap_unwarp_report_wf.get_node( node).interface.out_path_base = 'fmriprep' for node in bold_sdc_wf.list_node_names(): if node.split('.')[-1].startswith('ds_'): bold_sdc_wf.get_node(node).interface.out_path_base = 'fmriprep' if 'syn' in fmaps: sdc_select_std = pe.Node(KeySelect(fields=['std2anat_xfm']), name='sdc_select_std', run_without_submitting=True) sdc_select_std.inputs.key = 'MNI152NLin2009cAsym' workflow.connect([ (inputnode, sdc_select_std, [('std2anat_xfm', 'std2anat_xfm'), ('template', 'keys')]), (sdc_select_std, bold_sdc_wf, [('std2anat_xfm', 'inputnode.std2anat_xfm')]), ]) if fmaps.get('syn') is True: # SyN forced syn_unwarp_report_wf = init_sdc_unwarp_report_wf( name='syn_unwarp_report_wf', forcedsyn=True) workflow.connect([ (inputnode, syn_unwarp_report_wf, [('t1w_dseg', 'inputnode.in_seg')]), (bold_reference_wf, syn_unwarp_report_wf, [('outputnode.ref_image', 'inputnode.in_pre')]), (bold_reg_wf, syn_unwarp_report_wf, [('outputnode.itk_t1_to_bold', 'inputnode.in_xfm')]), (bold_sdc_wf, syn_unwarp_report_wf, [('outputnode.syn_ref', 'inputnode.in_post')]), ]) # Overwrite ``out_path_base`` of unwarping DataSinks for node in syn_unwarp_report_wf.list_node_names(): if node.split('.')[-1].startswith('ds_'): syn_unwarp_report_wf.get_node( node).interface.out_path_base = 'fmriprep' # Map final BOLD mask into T1w space (if required) nonstd_spaces = set(spaces.get_nonstandard()) if nonstd_spaces.intersection(('T1w', 'anat')): from niworkflows.interfaces.fixes import (FixHeaderApplyTransforms as ApplyTransforms) boldmask_to_t1w = pe.Node(ApplyTransforms(interpolation='MultiLabel', float=True), name='boldmask_to_t1w', mem_gb=0.1) workflow.connect([ (bold_reg_wf, boldmask_to_t1w, [('outputnode.itk_bold_to_t1', 'transforms')]), (bold_t1_trans_wf, boldmask_to_t1w, [('outputnode.bold_mask_t1', 'reference_image')]), (bold_bold_trans_wf if not multiecho else bold_t2s_wf, boldmask_to_t1w, [('outputnode.bold_mask', 'input_image')]), (boldmask_to_t1w, outputnode, [('output_image', 'bold_mask_t1')]), ]) if nonstd_spaces.intersection(('func', 'run', 'bold', 'boldref', 'sbref')): workflow.connect([ (bold_bold_trans_wf, outputnode, [('outputnode.bold', 'bold_native')]), (bold_bold_trans_wf, func_derivatives_wf, [('outputnode.bold_ref', 'inputnode.bold_native_ref'), ('outputnode.bold_mask', 'inputnode.bold_mask_native')]), ]) if spaces.get_spaces(nonstandard=False, dim=(3, )): # Apply transforms in 1 shot # Only use uncompressed output if AROMA is to be run bold_std_trans_wf = init_bold_std_trans_wf( freesurfer=freesurfer, mem_gb=mem_gb['resampled'], omp_nthreads=omp_nthreads, spaces=spaces, name='bold_std_trans_wf', use_compression=not config.execution.low_mem, use_fieldwarp=bool(fmaps), ) workflow.connect([ (inputnode, bold_std_trans_wf, [('template', 'inputnode.templates'), ('anat2std_xfm', 'inputnode.anat2std_xfm'), ('bold_file', 'inputnode.name_source'), ('t1w_aseg', 'inputnode.bold_aseg'), ('t1w_aparc', 'inputnode.bold_aparc')]), (bold_hmc_wf, bold_std_trans_wf, [('outputnode.xforms', 'inputnode.hmc_xforms')]), (bold_reg_wf, bold_std_trans_wf, [('outputnode.itk_bold_to_t1', 'inputnode.itk_bold_to_t1')]), (bold_bold_trans_wf if not multiecho else bold_t2s_wf, bold_std_trans_wf, [('outputnode.bold_mask', 'inputnode.bold_mask')]), (bold_sdc_wf, bold_std_trans_wf, [('outputnode.out_warp', 'inputnode.fieldwarp')]), (bold_std_trans_wf, outputnode, [('outputnode.bold_std', 'bold_std'), ('outputnode.bold_std_ref', 'bold_std_ref'), ('outputnode.bold_mask_std', 'bold_mask_std')]), ]) if freesurfer: workflow.connect([ (bold_std_trans_wf, func_derivatives_wf, [ ('outputnode.bold_aseg_std', 'inputnode.bold_aseg_std'), ('outputnode.bold_aparc_std', 'inputnode.bold_aparc_std'), ]), (bold_std_trans_wf, outputnode, [('outputnode.bold_aseg_std', 'bold_aseg_std'), ('outputnode.bold_aparc_std', 'bold_aparc_std')]), ]) if not multiecho: workflow.connect([(bold_split, bold_std_trans_wf, [('out_files', 'inputnode.bold_split')])]) else: split_opt_comb = bold_split.clone(name='split_opt_comb') workflow.connect([(bold_t2s_wf, split_opt_comb, [('outputnode.bold', 'in_file')]), (split_opt_comb, bold_std_trans_wf, [('out_files', 'inputnode.bold_split')])]) # func_derivatives_wf internally parametrizes over snapshotted spaces. workflow.connect([ (bold_std_trans_wf, func_derivatives_wf, [ ('outputnode.template', 'inputnode.template'), ('outputnode.spatial_reference', 'inputnode.spatial_reference'), ('outputnode.bold_std_ref', 'inputnode.bold_std_ref'), ('outputnode.bold_std', 'inputnode.bold_std'), ('outputnode.bold_mask_std', 'inputnode.bold_mask_std'), ]), ]) if config.workflow.use_aroma: # ICA-AROMA workflow from .confounds import init_ica_aroma_wf ica_aroma_wf = init_ica_aroma_wf( mem_gb=mem_gb['resampled'], metadata=metadata, omp_nthreads=omp_nthreads, use_fieldwarp=bool(fmaps), err_on_aroma_warn=config.workflow.aroma_err_on_warn, aroma_melodic_dim=config.workflow.aroma_melodic_dim, name='ica_aroma_wf') join = pe.Node(niu.Function(output_names=["out_file"], function=_to_join), name='aroma_confounds') mrg_conf_metadata = pe.Node(niu.Merge(2), name='merge_confound_metadata', run_without_submitting=True) mrg_conf_metadata2 = pe.Node(DictMerge(), name='merge_confound_metadata2', run_without_submitting=True) workflow.disconnect([ (bold_confounds_wf, outputnode, [ ('outputnode.confounds_file', 'confounds'), ]), (bold_confounds_wf, outputnode, [ ('outputnode.confounds_metadata', 'confounds_metadata'), ]), ]) workflow.connect([ (inputnode, ica_aroma_wf, [('bold_file', 'inputnode.name_source')]), (bold_hmc_wf, ica_aroma_wf, [('outputnode.movpar_file', 'inputnode.movpar_file')]), (bold_reference_wf, ica_aroma_wf, [('outputnode.skip_vols', 'inputnode.skip_vols')]), (bold_confounds_wf, join, [('outputnode.confounds_file', 'in_file')]), (bold_confounds_wf, mrg_conf_metadata, [('outputnode.confounds_metadata', 'in1')]), (ica_aroma_wf, join, [('outputnode.aroma_confounds', 'join_file')]), (ica_aroma_wf, mrg_conf_metadata, [('outputnode.aroma_metadata', 'in2')]), (mrg_conf_metadata, mrg_conf_metadata2, [('out', 'in_dicts')]), (ica_aroma_wf, outputnode, [('outputnode.aroma_noise_ics', 'aroma_noise_ics'), ('outputnode.melodic_mix', 'melodic_mix'), ('outputnode.nonaggr_denoised_file', 'nonaggr_denoised_file') ]), (join, outputnode, [('out_file', 'confounds')]), (mrg_conf_metadata2, outputnode, [('out_dict', 'confounds_metadata')]), (bold_std_trans_wf, ica_aroma_wf, [('outputnode.bold_std', 'inputnode.bold_std'), ('outputnode.bold_mask_std', 'inputnode.bold_mask_std'), ('outputnode.spatial_reference', 'inputnode.spatial_reference')]), ]) # SURFACES ################################################################################## # Freesurfer freesurfer_spaces = spaces.get_fs_spaces() if freesurfer and freesurfer_spaces: config.loggers.workflow.debug( 'Creating BOLD surface-sampling workflow.') bold_surf_wf = init_bold_surf_wf( mem_gb=mem_gb['resampled'], surface_spaces=freesurfer_spaces, medial_surface_nan=config.workflow.medial_surface_nan, name='bold_surf_wf') workflow.connect([ (inputnode, bold_surf_wf, [('t1w_preproc', 'inputnode.t1w_preproc'), ('subjects_dir', 'inputnode.subjects_dir'), ('subject_id', 'inputnode.subject_id'), ('t1w2fsnative_xfm', 'inputnode.t1w2fsnative_xfm')]), (bold_t1_trans_wf, bold_surf_wf, [('outputnode.bold_t1', 'inputnode.source_file')]), (bold_surf_wf, outputnode, [('outputnode.surfaces', 'surfaces')]), (bold_surf_wf, func_derivatives_wf, [('outputnode.target', 'inputnode.surf_refs')]), ]) # CIFTI output if config.workflow.cifti_output: from .resampling import init_bold_grayords_wf bold_grayords_wf = init_bold_grayords_wf( grayord_density=config.workflow.cifti_output, mem_gb=mem_gb['resampled'], repetition_time=metadata['RepetitionTime']) workflow.connect([ (inputnode, bold_grayords_wf, [('subjects_dir', 'inputnode.subjects_dir')]), (bold_std_trans_wf, bold_grayords_wf, [('outputnode.bold_std', 'inputnode.bold_std'), ('outputnode.spatial_reference', 'inputnode.spatial_reference')]), (bold_surf_wf, bold_grayords_wf, [ ('outputnode.surfaces', 'inputnode.surf_files'), ('outputnode.target', 'inputnode.surf_refs'), ]), (bold_grayords_wf, outputnode, [('outputnode.cifti_bold', 'bold_cifti'), ('outputnode.cifti_variant', 'cifti_variant'), ('outputnode.cifti_metadata', 'cifti_metadata'), ('outputnode.cifti_density', 'cifti_density')]), ]) if spaces.get_spaces(nonstandard=False, dim=(3, )): carpetplot_wf = init_carpetplot_wf( mem_gb=mem_gb['resampled'], metadata=metadata, cifti_output=config.workflow.cifti_output, name='carpetplot_wf') if config.workflow.cifti_output: workflow.connect(bold_grayords_wf, 'outputnode.cifti_bold', carpetplot_wf, 'inputnode.cifti_bold') else: # Xform to 'MNI152NLin2009cAsym' is always computed. carpetplot_select_std = pe.Node(KeySelect( fields=['std2anat_xfm'], key='MNI152NLin2009cAsym'), name='carpetplot_select_std', run_without_submitting=True) workflow.connect([ (inputnode, carpetplot_select_std, [('std2anat_xfm', 'std2anat_xfm'), ('template', 'keys')]), (carpetplot_select_std, carpetplot_wf, [('std2anat_xfm', 'inputnode.std2anat_xfm')]), (bold_bold_trans_wf if not multiecho else bold_t2s_wf, carpetplot_wf, [('outputnode.bold', 'inputnode.bold'), ('outputnode.bold_mask', 'inputnode.bold_mask')]), (bold_reg_wf, carpetplot_wf, [('outputnode.itk_t1_to_bold', 'inputnode.t1_bold_xform')]), ]) workflow.connect([(bold_confounds_wf, carpetplot_wf, [ ('outputnode.confounds_file', 'inputnode.confounds_file') ])]) # REPORTING ############################################################ reportlets_dir = str(config.execution.work_dir / 'reportlets') ds_report_summary = pe.Node(DerivativesDataSink(desc='summary', keep_dtype=True), name='ds_report_summary', run_without_submitting=True, mem_gb=config.DEFAULT_MEMORY_MIN_GB) ds_report_validation = pe.Node(DerivativesDataSink( base_directory=reportlets_dir, desc='validation', keep_dtype=True), name='ds_report_validation', run_without_submitting=True, mem_gb=config.DEFAULT_MEMORY_MIN_GB) workflow.connect([ (summary, ds_report_summary, [('out_report', 'in_file')]), (bold_reference_wf, ds_report_validation, [('outputnode.validation_report', 'in_file')]), ]) # Fill-in datasinks of reportlets seen so far for node in workflow.list_node_names(): if node.split('.')[-1].startswith('ds_report'): workflow.get_node(node).inputs.base_directory = reportlets_dir workflow.get_node(node).inputs.source_file = ref_file return workflow
def init_syn_preprocessing_wf( *, debug=False, name="syn_preprocessing_wf", omp_nthreads=1, auto_bold_nss=False, t1w_inversion=False, ): """ Prepare EPI references and co-registration to anatomical for SyN. Workflow Graph .. workflow :: :graph2use: orig :simple_form: yes from sdcflows.workflows.fit.syn import init_syn_sdc_wf wf = init_syn_sdc_wf(omp_nthreads=8) Parameters ---------- debug : :obj:`bool` Whether a fast (less accurate) configuration of the workflow should be applied. name : :obj:`str` Name for this workflow omp_nthreads : :obj:`int` Parallelize internal tasks across the number of CPUs given by this option. auto_bold_nss : :obj:`bool` Set up the reference workflow to automatically execute nonsteady states detection of BOLD images. t1w_inversion : :obj:`bool` Run T1w intensity inversion so that it looks more like a T2 contrast. Inputs ------ in_epis : :obj:`list` of :obj:`str` Distorted EPI images that will be merged together to create the EPI reference file. t_masks : :obj:`list` of :obj:`bool` (optional) mask of timepoints for calculating an EPI reference. Not used if ``auto_bold_nss=True``. in_meta : :obj:`list` of :obj:`dict` Metadata dictionaries corresponding to the ``in_epis`` input. in_anat : :obj:`str` A preprocessed anatomical (T1w or T2w) image. mask_anat : :obj:`str` A brainmask corresponding to the anatomical (T1w or T2w) image. std2anat_xfm : :obj:`str` inverse registration transform of T1w image to MNI template. Outputs ------- epi_ref : :obj:`tuple` (:obj:`str`, :obj:`dict`) A tuple, where the first element is the path of the distorted EPI reference map (e.g., an average of *b=0* volumes), and the second element is a dictionary of associated metadata. anat_ref : :obj:`str` Path to the anatomical, skull-stripped reference in EPI space. anat_mask : :obj:`str` Path to the brain mask corresponding to ``anat_ref`` in EPI space. sd_prior : :obj:`str` A template map of areas with strong susceptibility distortions (SD) to regularize the cost function of SyN. """ from pkg_resources import resource_filename as pkgrf from niworkflows.interfaces.nibabel import ( IntensityClip, ApplyMask, GenerateSamplingReference, ) from niworkflows.interfaces.fixes import ( FixHeaderApplyTransforms as ApplyTransforms, FixHeaderRegistration as Registration, ) from niworkflows.workflows.epi.refmap import init_epi_reference_wf from ...interfaces.utils import Deoblique, DenoiseImage from ...interfaces.brainmask import BrainExtraction, BinaryDilation workflow = Workflow(name=name) inputnode = pe.Node( niu.IdentityInterface( fields=[ "in_epis", "t_masks", "in_meta", "in_anat", "mask_anat", "std2anat_xfm", ] ), name="inputnode", ) outputnode = pe.Node( niu.IdentityInterface( fields=["epi_ref", "epi_mask", "anat_ref", "anat_mask", "sd_prior"] ), name="outputnode", ) deob_epi = pe.Node(Deoblique(), name="deob_epi") # Mapping & preparing prior knowledge # Concatenate transform files: # 1) MNI -> anat; 2) ATLAS -> MNI transform_list = pe.Node( niu.Merge(3), name="transform_list", mem_gb=DEFAULT_MEMORY_MIN_GB, run_without_submitting=True, ) transform_list.inputs.in3 = pkgrf( "sdcflows", "data/fmap_atlas_2_MNI152NLin2009cAsym_affine.mat" ) prior2epi = pe.Node( ApplyTransforms( invert_transform_flags=[True, False, False], input_image=pkgrf("sdcflows", "data/fmap_atlas.nii.gz"), ), name="prior2epi", n_procs=omp_nthreads, mem_gb=0.3, ) anat2epi = pe.Node( ApplyTransforms(invert_transform_flags=[True]), name="anat2epi", n_procs=omp_nthreads, mem_gb=0.3, ) mask2epi = pe.Node( ApplyTransforms(invert_transform_flags=[True], interpolation="MultiLabel"), name="mask2epi", n_procs=omp_nthreads, mem_gb=0.3, ) mask_dtype = pe.Node( niu.Function(function=_set_dtype, input_names=["in_file", "dtype"]), name="mask_dtype", ) mask_dtype.inputs.dtype = "uint8" epi_reference_wf = init_epi_reference_wf( omp_nthreads=omp_nthreads, auto_bold_nss=auto_bold_nss, ) epi_brain = pe.Node(BrainExtraction(), name="epi_brain") merge_output = pe.Node( niu.Function(function=_merge_meta), name="merge_output", run_without_submitting=True, ) mask_anat = pe.Node(ApplyMask(), name="mask_anat") clip_anat = pe.Node(IntensityClip(p_min=0.0, p_max=99.8), name="clip_anat") ref_anat = pe.Node( DenoiseImage(copy_header=True), name="ref_anat", n_procs=omp_nthreads ) epi2anat = pe.Node( Registration(from_file=resource_filename("sdcflows", "data/affine.json")), name="epi2anat", n_procs=omp_nthreads, ) epi2anat.inputs.output_warped_image = debug epi2anat.inputs.output_inverse_warped_image = debug if debug: epi2anat.inputs.args = "--write-interval-volumes 5" def _remove_first_mask(in_file): if not isinstance(in_file, list): in_file = [in_file] in_file.insert(0, "NULL") return in_file anat_dilmsk = pe.Node(BinaryDilation(), name="anat_dilmsk") epi_dilmsk = pe.Node(BinaryDilation(), name="epi_dilmsk") sampling_ref = pe.Node(GenerateSamplingReference(), name="sampling_ref") # fmt:off workflow.connect([ (inputnode, transform_list, [("std2anat_xfm", "in2")]), (inputnode, epi_reference_wf, [("in_epis", "inputnode.in_files")]), (inputnode, merge_output, [("in_meta", "meta_list")]), (inputnode, anat_dilmsk, [("mask_anat", "in_file")]), (inputnode, mask_anat, [("in_anat", "in_file"), ("mask_anat", "in_mask")]), (inputnode, mask2epi, [("mask_anat", "input_image")]), (epi_reference_wf, deob_epi, [("outputnode.epi_ref_file", "in_file")]), (deob_epi, merge_output, [("out_file", "epi_ref")]), (mask_anat, clip_anat, [("out_file", "in_file")]), (clip_anat, ref_anat, [("out_file", "input_image")]), (deob_epi, epi_brain, [("out_file", "in_file")]), (epi_brain, epi_dilmsk, [("out_mask", "in_file")]), (ref_anat, epi2anat, [("output_image", "fixed_image")]), (anat_dilmsk, epi2anat, [("out_file", "fixed_image_masks")]), (deob_epi, epi2anat, [("out_file", "moving_image")]), (epi_dilmsk, epi2anat, [ (("out_file", _remove_first_mask), "moving_image_masks")]), (deob_epi, sampling_ref, [("out_file", "fixed_image")]), (epi2anat, transform_list, [("forward_transforms", "in1")]), (transform_list, prior2epi, [("out", "transforms")]), (sampling_ref, prior2epi, [("out_file", "reference_image")]), (ref_anat, anat2epi, [("output_image", "input_image")]), (epi2anat, anat2epi, [("forward_transforms", "transforms")]), (sampling_ref, anat2epi, [("out_file", "reference_image")]), (epi2anat, mask2epi, [("forward_transforms", "transforms")]), (sampling_ref, mask2epi, [("out_file", "reference_image")]), (mask2epi, mask_dtype, [("output_image", "in_file")]), (anat2epi, outputnode, [("output_image", "anat_ref")]), (mask_dtype, outputnode, [("out", "anat_mask")]), (merge_output, outputnode, [("out", "epi_ref")]), (epi_brain, outputnode, [("out_mask", "epi_mask")]), (prior2epi, outputnode, [("output_image", "sd_prior")]), ]) # fmt:on if debug: from niworkflows.interfaces.nibabel import RegridToZooms regrid_anat = pe.Node( RegridToZooms(zooms=(2.0, 2.0, 2.0), smooth=True), name="regrid_anat" ) # fmt:off workflow.connect([ (inputnode, regrid_anat, [("in_anat", "in_file")]), (regrid_anat, sampling_ref, [("out_file", "moving_image")]), ]) # fmt:on else: # fmt:off workflow.connect([ (inputnode, sampling_ref, [("in_anat", "moving_image")]), ]) # fmt:on if not auto_bold_nss: workflow.connect(inputnode, "t_masks", epi_reference_wf, "inputnode.t_masks") return workflow
def init_anat_derivatives_wf( *, bids_root, freesurfer, num_t1w, output_dir, spaces, name="anat_derivatives_wf", tpm_labels=BIDS_TISSUE_ORDER, ): """ Set up a battery of datasinks to store derivatives in the right location. Parameters ---------- bids_root : :obj:`str` Root path of BIDS dataset freesurfer : :obj:`bool` FreeSurfer was enabled num_t1w : :obj:`int` Number of T1w images output_dir : :obj:`str` Directory in which to save derivatives name : :obj:`str` Workflow name (default: anat_derivatives_wf) tpm_labels : :obj:`tuple` Tissue probability maps in order Inputs ------ template Template space and specifications source_files List of input T1w images t1w_ref_xfms List of affine transforms to realign input T1w images t1w_preproc The T1w reference map, which is calculated as the average of bias-corrected and preprocessed T1w images, defining the anatomical space. t1w_mask Mask of the ``t1w_preproc`` t1w_dseg Segmentation in T1w space t1w_tpms Tissue probability maps in T1w space anat2std_xfm Nonlinear spatial transform to resample imaging data given in anatomical space into standard space. std2anat_xfm Inverse transform of ``anat2std_xfm`` std_t1w T1w reference resampled in one or more standard spaces. std_mask Mask of skull-stripped template, in standard space std_dseg Segmentation, resampled into standard space std_tpms Tissue probability maps in standard space t1w2fsnative_xfm LTA-style affine matrix translating from T1w to FreeSurfer-conformed subject space fsnative2t1w_xfm LTA-style affine matrix translating from FreeSurfer-conformed subject space to T1w surfaces GIFTI surfaces (gray/white boundary, midthickness, pial, inflated) t1w_fs_aseg FreeSurfer's aseg segmentation, in native T1w space t1w_fs_aparc FreeSurfer's aparc+aseg segmentation, in native T1w space """ from niworkflows.interfaces.utility import KeySelect workflow = Workflow(name=name) inputnode = pe.Node( niu.IdentityInterface(fields=[ "template", "source_files", "t1w_ref_xfms", "t1w_preproc", "t1w_mask", "t1w_dseg", "t1w_tpms", "anat2std_xfm", "std2anat_xfm", "t1w2fsnative_xfm", "fsnative2t1w_xfm", "surfaces", "t1w_fs_aseg", "t1w_fs_aparc", ]), name="inputnode", ) raw_sources = pe.Node(niu.Function(function=_bids_relative), name="raw_sources") raw_sources.inputs.bids_root = bids_root ds_t1w_preproc = pe.Node( DerivativesDataSink(base_directory=output_dir, desc="preproc", compress=True), name="ds_t1w_preproc", run_without_submitting=True, ) ds_t1w_preproc.inputs.SkullStripped = False ds_t1w_mask = pe.Node( DerivativesDataSink(base_directory=output_dir, desc="brain", suffix="mask", compress=True), name="ds_t1w_mask", run_without_submitting=True, ) ds_t1w_mask.inputs.Type = "Brain" ds_t1w_dseg = pe.Node( DerivativesDataSink(base_directory=output_dir, suffix="dseg", compress=True), name="ds_t1w_dseg", run_without_submitting=True, ) ds_t1w_tpms = pe.Node( DerivativesDataSink(base_directory=output_dir, suffix="probseg", compress=True), name="ds_t1w_tpms", run_without_submitting=True, ) ds_t1w_tpms.inputs.label = tpm_labels # fmt:off workflow.connect([ (inputnode, raw_sources, [('source_files', 'in_files')]), (inputnode, ds_t1w_preproc, [('t1w_preproc', 'in_file'), ('source_files', 'source_file')]), (inputnode, ds_t1w_mask, [('t1w_mask', 'in_file'), ('source_files', 'source_file')]), (inputnode, ds_t1w_tpms, [('t1w_tpms', 'in_file'), ('source_files', 'source_file')]), (inputnode, ds_t1w_dseg, [('t1w_dseg', 'in_file'), ('source_files', 'source_file')]), (raw_sources, ds_t1w_mask, [('out', 'RawSources')]), ]) # fmt:on # Transforms if spaces.get_spaces(nonstandard=False, dim=(3, )): ds_std2t1w_xfm = pe.MapNode( DerivativesDataSink(base_directory=output_dir, to="T1w", mode="image", suffix="xfm"), iterfield=("in_file", "from"), name="ds_std2t1w_xfm", run_without_submitting=True, ) ds_t1w2std_xfm = pe.MapNode( DerivativesDataSink(base_directory=output_dir, mode="image", suffix="xfm", **{"from": "T1w"}), iterfield=("in_file", "to"), name="ds_t1w2std_xfm", run_without_submitting=True, ) # fmt:off workflow.connect([ (inputnode, ds_t1w2std_xfm, [('anat2std_xfm', 'in_file'), (('template', _combine_cohort), 'to'), ('source_files', 'source_file')]), (inputnode, ds_std2t1w_xfm, [('std2anat_xfm', 'in_file'), (('template', _combine_cohort), 'from'), ('source_files', 'source_file')]), ]) # fmt:on if num_t1w > 1: # Please note the dictionary unpacking to provide the from argument. # It is necessary because from is a protected keyword (not allowed as argument name). ds_t1w_ref_xfms = pe.MapNode( DerivativesDataSink( base_directory=output_dir, to="T1w", mode="image", suffix="xfm", extension="txt", **{"from": "orig"}, ), iterfield=["source_file", "in_file"], name="ds_t1w_ref_xfms", run_without_submitting=True, ) # fmt:off workflow.connect([ (inputnode, ds_t1w_ref_xfms, [('source_files', 'source_file'), ('t1w_ref_xfms', 'in_file')]), ]) # fmt:on # Write derivatives in standard spaces specified by --output-spaces if getattr(spaces, "_cached") is not None and spaces.cached.references: from niworkflows.interfaces.space import SpaceDataSource from niworkflows.interfaces.nibabel import GenerateSamplingReference from niworkflows.interfaces.fixes import ( FixHeaderApplyTransforms as ApplyTransforms, ) from ..interfaces.templateflow import TemplateFlowSelect spacesource = pe.Node(SpaceDataSource(), name="spacesource", run_without_submitting=True) spacesource.iterables = ( "in_tuple", [(s.fullname, s.spec) for s in spaces.cached.get_standard(dim=(3, ))], ) gen_tplid = pe.Node( niu.Function(function=_fmt_cohort), name="gen_tplid", run_without_submitting=True, ) select_xfm = pe.Node( KeySelect(fields=["anat2std_xfm"]), name="select_xfm", run_without_submitting=True, ) select_tpl = pe.Node(TemplateFlowSelect(), name="select_tpl", run_without_submitting=True) 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, 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')]), (inputnode, select_xfm, [('anat2std_xfm', 'anat2std_xfm'), ('template', 'keys')]), (spacesource, gen_tplid, [('space', 'template'), ('cohort', 'cohort')]), (gen_tplid, select_xfm, [('out', 'key')]), (spacesource, select_tpl, [('space', 'template'), ('cohort', 'cohort'), (('resolution', _no_native), 'resolution')]), (spacesource, gen_ref, [(('resolution', _is_native), 'keep_native') ]), (select_tpl, gen_ref, [('t1w_file', 'fixed_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')]), (select_tpl, ds_std_mask, [(('brain_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)] + [(select_xfm, n, [('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 + [(spacesource, n, [('space', 'space'), ('cohort', 'cohort'), ('resolution', 'resolution')]) for n in (ds_std_t1w, ds_std_mask, ds_std_dseg, ds_std_tpms)]) # fmt:on if not freesurfer: return workflow from niworkflows.interfaces.nitransforms import ConcatenateXFMs from niworkflows.interfaces.surf import Path2BIDS # FS native space transforms lta2itk_fwd = pe.Node(ConcatenateXFMs(), name="lta2itk_fwd", run_without_submitting=True) lta2itk_inv = pe.Node(ConcatenateXFMs(), name="lta2itk_inv", run_without_submitting=True) ds_t1w_fsnative = pe.Node( DerivativesDataSink( base_directory=output_dir, mode="image", to="fsnative", suffix="xfm", extension="txt", **{"from": "T1w"}, ), name="ds_t1w_fsnative", run_without_submitting=True, ) ds_fsnative_t1w = pe.Node( DerivativesDataSink( base_directory=output_dir, mode="image", to="T1w", suffix="xfm", extension="txt", **{"from": "fsnative"}, ), name="ds_fsnative_t1w", run_without_submitting=True, ) # Surfaces name_surfs = pe.MapNode(Path2BIDS(), iterfield="in_file", name="name_surfs", run_without_submitting=True) ds_surfs = pe.MapNode( DerivativesDataSink(base_directory=output_dir, extension=".surf.gii"), iterfield=["in_file", "hemi", "suffix"], name="ds_surfs", run_without_submitting=True, ) # Parcellations ds_t1w_fsaseg = pe.Node( DerivativesDataSink(base_directory=output_dir, desc="aseg", suffix="dseg", compress=True), name="ds_t1w_fsaseg", run_without_submitting=True, ) ds_t1w_fsparc = pe.Node( DerivativesDataSink(base_directory=output_dir, desc="aparcaseg", suffix="dseg", compress=True), name="ds_t1w_fsparc", run_without_submitting=True, ) # fmt:off workflow.connect([ (inputnode, lta2itk_fwd, [('t1w2fsnative_xfm', 'in_xfms')]), (inputnode, lta2itk_inv, [('fsnative2t1w_xfm', 'in_xfms')]), (inputnode, ds_t1w_fsnative, [('source_files', 'source_file')]), (lta2itk_fwd, ds_t1w_fsnative, [('out_xfm', 'in_file')]), (inputnode, ds_fsnative_t1w, [('source_files', 'source_file')]), (lta2itk_inv, ds_fsnative_t1w, [('out_xfm', 'in_file')]), (inputnode, name_surfs, [('surfaces', 'in_file')]), (inputnode, ds_surfs, [('surfaces', 'in_file'), ('source_files', 'source_file')]), (name_surfs, ds_surfs, [('hemi', 'hemi'), ('suffix', 'suffix')]), (inputnode, ds_t1w_fsaseg, [('t1w_fs_aseg', 'in_file'), ('source_files', 'source_file')]), (inputnode, ds_t1w_fsparc, [('t1w_fs_aparc', 'in_file'), ('source_files', 'source_file')]), ]) # fmt:on return workflow
def init_dwi_preproc_wf(dwi_file, has_fieldmap=False): """ Build a preprocessing workflow for one DWI run. Workflow Graph .. workflow:: :graph2use: orig :simple_form: yes from dmriprep.config.testing import mock_config from dmriprep import config from dmriprep.workflows.dwi.base import init_dwi_preproc_wf with mock_config(): wf = init_dwi_preproc_wf( f"{config.execution.layout.root}/" "sub-THP0005/dwi/sub-THP0005_dwi.nii.gz" ) Parameters ---------- dwi_file : :obj:`os.PathLike` One diffusion MRI dataset to be processed. has_fieldmap : :obj:`bool` Build the workflow with a path to register a fieldmap to the DWI. Inputs ------ dwi_file dwi NIfTI file in_bvec File path of the b-vectors in_bval File path of the b-values fmap File path of the fieldmap fmap_ref File path of the fieldmap reference fmap_coeff File path of the fieldmap coefficients fmap_mask File path of the fieldmap mask fmap_id The BIDS modality label of the fieldmap being used Outputs ------- dwi_reference A 3D :math:`b = 0` reference, before susceptibility distortion correction. dwi_mask A 3D, binary mask of the ``dwi_reference`` above. gradients_rasb A *RASb* (RAS+ coordinates, scaled b-values, normalized b-vectors, BIDS-compatible) gradient table. See Also -------- * :py:func:`~dmriprep.workflows.dwi.util.init_dwi_reference_wf` * :py:func:`~dmriprep.workflows.dwi.outputs.init_dwi_derivatives_wf` * :py:func:`~dmriprep.workflows.dwi.outputs.init_reportlets_wf` """ from niworkflows.interfaces.reportlets.registration import ( SimpleBeforeAfterRPT as SimpleBeforeAfter, ) from ...interfaces.vectors import CheckGradientTable from .util import init_dwi_reference_wf from .outputs import init_dwi_derivatives_wf, init_reportlets_wf from .eddy import init_eddy_wf layout = config.execution.layout dwi_file = Path(dwi_file) config.loggers.workflow.debug( f"Creating DWI preprocessing workflow for <{dwi_file.name}>") if has_fieldmap: import re from sdcflows.fieldmaps import get_identifier dwi_rel = re.sub(r"^sub-[a-zA-Z0-9]*/", "", str(dwi_file.relative_to(layout.root))) estimator_key = get_identifier(dwi_rel) if not estimator_key: has_fieldmap = False config.loggers.workflow.critical( f"None of the available B0 fieldmaps are associated to <{dwi_rel}>" ) # Build workflow workflow = Workflow(name=_get_wf_name(dwi_file.name)) inputnode = pe.Node( niu.IdentityInterface(fields=[ # DWI "dwi_file", "in_bvec", "in_bval", # From SDCFlows "fmap", "fmap_ref", "fmap_coeff", "fmap_mask", "fmap_id", # From anatomical "t1w_preproc", "t1w_mask", "t1w_dseg", "t1w_aseg", "t1w_aparc", "t1w_tpms", "template", "anat2std_xfm", "std2anat_xfm", "subjects_dir", "subject_id", "t1w2fsnative_xfm", "fsnative2t1w_xfm", ]), name="inputnode", ) inputnode.inputs.dwi_file = str(dwi_file.absolute()) inputnode.inputs.in_bvec = str(layout.get_bvec(dwi_file)) inputnode.inputs.in_bval = str(layout.get_bval(dwi_file)) outputnode = pe.Node( niu.IdentityInterface( fields=["dwi_reference", "dwi_mask", "gradients_rasb"]), name="outputnode", ) gradient_table = pe.Node(CheckGradientTable(), name="gradient_table") dwi_reference_wf = init_dwi_reference_wf( mem_gb=config.DEFAULT_MEMORY_MIN_GB, omp_nthreads=config.nipype.omp_nthreads) dwi_derivatives_wf = init_dwi_derivatives_wf( output_dir=str(config.execution.output_dir)) # MAIN WORKFLOW STRUCTURE # fmt: off workflow.connect([ (inputnode, gradient_table, [("dwi_file", "dwi_file"), ("in_bvec", "in_bvec"), ("in_bval", "in_bval")]), (inputnode, dwi_reference_wf, [("dwi_file", "inputnode.dwi_file")]), (inputnode, dwi_derivatives_wf, [("dwi_file", "inputnode.source_file") ]), (gradient_table, dwi_reference_wf, [("b0_ixs", "inputnode.b0_ixs")]), (gradient_table, outputnode, [("out_rasb", "gradients_rasb")]), (outputnode, dwi_derivatives_wf, [ ("dwi_reference", "inputnode.dwi_ref"), ("dwi_mask", "inputnode.dwi_mask"), ]), ]) # fmt: on if config.workflow.run_reconall: from niworkflows.interfaces.nibabel import ApplyMask from niworkflows.anat.coregistration import init_bbreg_wf from ...utils.misc import sub_prefix as _prefix # Mask the T1w t1w_brain = pe.Node(ApplyMask(), name="t1w_brain") bbr_wf = init_bbreg_wf( debug=config.execution.debug, epi2t1w_init=config.workflow.dwi2t1w_init, omp_nthreads=config.nipype.omp_nthreads, ) ds_report_reg = pe.Node( DerivativesDataSink( base_directory=str(config.execution.output_dir), datatype="figures", ), name="ds_report_reg", run_without_submitting=True, ) def _bold_reg_suffix(fallback): return "coreg" if fallback else "bbregister" # fmt: off workflow.connect([ (inputnode, bbr_wf, [ ("fsnative2t1w_xfm", "inputnode.fsnative2t1w_xfm"), (("subject_id", _prefix), "inputnode.subject_id"), ("subjects_dir", "inputnode.subjects_dir"), ]), # T1w Mask (inputnode, t1w_brain, [("t1w_preproc", "in_file"), ("t1w_mask", "in_mask")]), (inputnode, ds_report_reg, [("dwi_file", "source_file")]), # BBRegister (dwi_reference_wf, bbr_wf, [("outputnode.ref_image", "inputnode.in_file")]), (bbr_wf, ds_report_reg, [('outputnode.out_report', 'in_file'), (('outputnode.fallback', _bold_reg_suffix), 'desc')]), ]) # fmt: on if "eddy" not in config.workflow.ignore: # Eddy distortion correction eddy_wf = init_eddy_wf(debug=config.execution.debug) eddy_wf.inputs.inputnode.metadata = layout.get_metadata(str(dwi_file)) ds_report_eddy = pe.Node( DerivativesDataSink( base_directory=str(config.execution.output_dir), desc="eddy", datatype="figures", ), name="ds_report_eddy", run_without_submitting=True, ) eddy_report = pe.Node( SimpleBeforeAfter( before_label="Distorted", after_label="Eddy Corrected", ), name="eddy_report", mem_gb=0.1, ) # fmt:off workflow.connect([ (dwi_reference_wf, eddy_wf, [ ("outputnode.dwi_file", "inputnode.dwi_file"), ("outputnode.dwi_mask", "inputnode.dwi_mask"), ]), (inputnode, eddy_wf, [("in_bvec", "inputnode.in_bvec"), ("in_bval", "inputnode.in_bval")]), (dwi_reference_wf, eddy_report, [("outputnode.ref_image", "before") ]), (eddy_wf, eddy_report, [('outputnode.eddy_ref_image', 'after')]), (dwi_reference_wf, ds_report_eddy, [("outputnode.dwi_file", "source_file")]), (eddy_report, ds_report_eddy, [("out_report", "in_file")]), ]) # fmt:on # REPORTING ############################################################ reportlets_wf = init_reportlets_wf( str(config.execution.output_dir), sdc_report=has_fieldmap, ) # fmt: off workflow.connect([ (inputnode, reportlets_wf, [("dwi_file", "inputnode.source_file")]), (dwi_reference_wf, reportlets_wf, [ ("outputnode.validation_report", "inputnode.validation_report"), ]), (outputnode, reportlets_wf, [ ("dwi_reference", "inputnode.dwi_ref"), ("dwi_mask", "inputnode.dwi_mask"), ]), ]) # fmt: on if not has_fieldmap: # fmt: off workflow.connect([ (dwi_reference_wf, outputnode, [("outputnode.ref_image", "dwi_reference"), ("outputnode.dwi_mask", "dwi_mask")]), ]) # fmt: on return workflow from niworkflows.interfaces.utility import KeySelect from sdcflows.workflows.apply.registration import init_coeff2epi_wf from sdcflows.workflows.apply.correction import init_unwarp_wf coeff2epi_wf = init_coeff2epi_wf( debug=config.execution.debug, omp_nthreads=config.nipype.omp_nthreads, write_coeff=True, ) unwarp_wf = init_unwarp_wf(debug=config.execution.debug, omp_nthreads=config.nipype.omp_nthreads) unwarp_wf.inputs.inputnode.metadata = layout.get_metadata(str(dwi_file)) output_select = pe.Node( KeySelect(fields=["fmap", "fmap_ref", "fmap_coeff", "fmap_mask"]), name="output_select", run_without_submitting=True, ) output_select.inputs.key = estimator_key[0] if len(estimator_key) > 1: config.loggers.workflow.warning( f"Several fieldmaps <{', '.join(estimator_key)}> are " f"'IntendedFor' <{dwi_file}>, using {estimator_key[0]}") sdc_report = pe.Node( SimpleBeforeAfter( before_label="Distorted", after_label="Corrected", ), name="sdc_report", mem_gb=0.1, ) # fmt: off workflow.connect([ (inputnode, output_select, [("fmap", "fmap"), ("fmap_ref", "fmap_ref"), ("fmap_coeff", "fmap_coeff"), ("fmap_mask", "fmap_mask"), ("fmap_id", "keys")]), (output_select, coeff2epi_wf, [("fmap_ref", "inputnode.fmap_ref"), ("fmap_coeff", "inputnode.fmap_coeff"), ("fmap_mask", "inputnode.fmap_mask")]), (dwi_reference_wf, coeff2epi_wf, [("outputnode.ref_image", "inputnode.target_ref"), ("outputnode.dwi_mask", "inputnode.target_mask")]), (dwi_reference_wf, unwarp_wf, [("outputnode.ref_image", "inputnode.distorted")]), (coeff2epi_wf, unwarp_wf, [("outputnode.fmap_coeff", "inputnode.fmap_coeff")]), (dwi_reference_wf, sdc_report, [("outputnode.ref_image", "before")]), (unwarp_wf, sdc_report, [("outputnode.corrected", "after"), ("outputnode.corrected_mask", "wm_seg")]), (sdc_report, reportlets_wf, [("out_report", "inputnode.sdc_report")]), (unwarp_wf, outputnode, [("outputnode.corrected", "dwi_reference"), ("outputnode.corrected_mask", "dwi_mask")]), ]) # fmt: on return workflow
def init_infant_brain_extraction_wf( age_months=None, ants_affine_init=False, bspline_fitting_distance=200, sloppy=False, skull_strip_template="UNCInfant", template_specs=None, mem_gb=3.0, debug=False, name="infant_brain_extraction_wf", omp_nthreads=None, ): """ Build an atlas-based brain extraction pipeline for infant T2w MRI data. Pros/Cons of available templates -------------------------------- * MNIInfant + More cohorts available for finer-grain control + T1w/T2w images available - Template masks are poor * UNCInfant + Accurate masks - No T2w image available Parameters ---------- age_months : :obj:`int` Age of this participant, in months. ants_affine_init : :obj:`bool`, optional Set-up a pre-initialization step with ``antsAI`` to account for mis-oriented images. bspline_fitting_distance : :obj:`float` Distance in mm between B-Spline control points for N4 INU estimation. sloppy : :obj:`bool` Run in *sloppy* mode. skull_strip_template : :obj:`str` A TemplateFlow ID indicating which template will be used as target for atlas-based segmentation. template_specs : :obj:`dict` Additional template specifications (e.g., resolution or cohort) to correctly select the adequate template instance. mem_gb : :obj:`float` Base memory fingerprint unit. name : :obj:`str` This particular workflow's unique name (Nipype requirement). omp_nthreads : :obj:`int` The number of threads for individual processes in this workflow. debug : :obj:`bool` Produce intermediate registration files Inputs ------ in_t2w : :obj:`str` The unprocessed input T2w image. Outputs ------- t2w_preproc : :obj:`str` The preprocessed T2w image (INU and clipping). t2w_brain : :obj:`str` The preprocessed, brain-extracted T2w image. out_mask : :obj:`str` The brainmask projected from the template into the T2w, after binarization. out_probmap : :obj:`str` The same as above, before binarization. """ from nipype.interfaces.ants import N4BiasFieldCorrection, ImageMath # niworkflows from niworkflows.interfaces.nibabel import ApplyMask, Binarize, IntensityClip from niworkflows.interfaces.fixes import ( FixHeaderRegistration as Registration, FixHeaderApplyTransforms as ApplyTransforms, ) from templateflow.api import get as get_template from ...interfaces.nibabel import BinaryDilation from ...utils.misc import cohort_by_months # handle template specifics template_specs = template_specs or {} if skull_strip_template == "MNIInfant": template_specs["resolution"] = 2 if sloppy else 1 if not template_specs.get("cohort"): if age_months is None: raise KeyError( f"Age or cohort for {skull_strip_template} must be provided!") template_specs["cohort"] = cohort_by_months(skull_strip_template, age_months) tpl_target_path = get_template( skull_strip_template, suffix="T1w", # no T2w template desc=None, **template_specs, ) if not tpl_target_path: raise RuntimeError( f"An instance of template <tpl-{skull_strip_template}> with T1w suffix " "could not be found.") tpl_brainmask_path = get_template(skull_strip_template, label="brain", suffix="probseg", **template_specs) or get_template( skull_strip_template, desc="brain", suffix="mask", **template_specs) tpl_regmask_path = get_template( skull_strip_template, label="BrainCerebellumExtraction", suffix="mask", **template_specs, ) # main workflow workflow = pe.Workflow(name) inputnode = pe.Node(niu.IdentityInterface(fields=["in_t2w"]), name="inputnode") outputnode = pe.Node( niu.IdentityInterface( fields=["t2w_preproc", "t2w_brain", "out_mask", "out_probmap"]), name="outputnode", ) # Ensure template comes with a range of intensities ANTs will like clip_tmpl = pe.Node(IntensityClip(p_max=99), name="clip_tmpl") clip_tmpl.inputs.in_file = _pop(tpl_target_path) # Generate laplacian registration targets lap_tmpl = pe.Node(ImageMath(operation="Laplacian", op2="0.4 1"), name="lap_tmpl") lap_t2w = pe.Node(ImageMath(operation="Laplacian", op2="0.4 1"), name="lap_t2w") norm_lap_tmpl = pe.Node(niu.Function(function=_norm_lap), name="norm_lap_tmpl") norm_lap_t2w = pe.Node(niu.Function(function=_norm_lap), name="norm_lap_t2w") # Merge image nodes mrg_tmpl = pe.Node(niu.Merge(2), name="mrg_tmpl", run_without_submitting=True) mrg_t2w = pe.Node(niu.Merge(2), name="mrg_t2w", run_without_submitting=True) bin_regmask = pe.Node(Binarize(thresh_low=0.20), name="bin_regmask") bin_regmask.inputs.in_file = str(tpl_brainmask_path) refine_mask = pe.Node(BinaryDilation(radius=3, iterations=2), name="refine_mask") fixed_masks = pe.Node(niu.Merge(4), name="fixed_masks", run_without_submitting=True) fixed_masks.inputs.in1 = "NULL" fixed_masks.inputs.in2 = "NULL" fixed_masks.inputs.in3 = "NULL" if not tpl_regmask_path else _pop( tpl_regmask_path) # Set up initial spatial normalization ants_params = "testing" if sloppy else "precise" norm = pe.Node( Registration(from_file=pkgr_fn( "nibabies.data", f"antsBrainExtraction_{ants_params}.json")), name="norm", n_procs=omp_nthreads, mem_gb=mem_gb, ) norm.inputs.float = sloppy if debug: norm.inputs.args = "--write-interval-volumes 5" map_mask_t2w = pe.Node( ApplyTransforms(interpolation="Gaussian", float=True), name="map_mask_t2w", mem_gb=1, ) # map template brainmask to t2w space map_mask_t2w.inputs.input_image = str(tpl_brainmask_path) thr_t2w_mask = pe.Node(Binarize(thresh_low=0.80), name="thr_t2w_mask") # Refine INU correction final_n4 = pe.Node( N4BiasFieldCorrection( dimension=3, bspline_fitting_distance=bspline_fitting_distance, save_bias=True, copy_header=True, n_iterations=[50] * 5, convergence_threshold=1e-7, rescale_intensities=True, shrink_factor=4, ), n_procs=omp_nthreads, name="final_n4", ) final_clip = pe.Node(IntensityClip(p_min=5.0, p_max=99.5), name="final_clip") apply_mask = pe.Node(ApplyMask(), name="apply_mask") # fmt:off workflow.connect([ (inputnode, final_n4, [("in_t2w", "input_image")]), # 1. Massage T2w (inputnode, mrg_t2w, [("in_t2w", "in1")]), (inputnode, lap_t2w, [("in_t2w", "op1")]), (inputnode, map_mask_t2w, [("in_t2w", "reference_image")]), (bin_regmask, refine_mask, [("out_file", "in_file")]), (refine_mask, fixed_masks, [("out_file", "in4")]), (lap_t2w, norm_lap_t2w, [("output_image", "in_file")]), (norm_lap_t2w, mrg_t2w, [("out", "in2")]), # 2. Prepare template (clip_tmpl, lap_tmpl, [("out_file", "op1")]), (lap_tmpl, norm_lap_tmpl, [("output_image", "in_file")]), (clip_tmpl, mrg_tmpl, [("out_file", "in1")]), (norm_lap_tmpl, mrg_tmpl, [("out", "in2")]), # 3. Set normalization node inputs (mrg_tmpl, norm, [("out", "fixed_image")]), (mrg_t2w, norm, [("out", "moving_image")]), (fixed_masks, norm, [("out", "fixed_image_masks")]), # 4. Map template brainmask into T2w space (norm, map_mask_t2w, [("reverse_transforms", "transforms"), ("reverse_invert_flags", "invert_transform_flags")]), (map_mask_t2w, thr_t2w_mask, [("output_image", "in_file")]), (thr_t2w_mask, apply_mask, [("out_mask", "in_mask")]), (final_n4, apply_mask, [("output_image", "in_file")]), # 5. Refine T2w INU correction with brain mask (map_mask_t2w, final_n4, [("output_image", "weight_image")]), (final_n4, final_clip, [("output_image", "in_file")]), # 9. Outputs (final_clip, outputnode, [("out_file", "t2w_preproc")]), (map_mask_t2w, outputnode, [("output_image", "out_probmap")]), (thr_t2w_mask, outputnode, [("out_mask", "out_mask")]), (apply_mask, outputnode, [("out_file", "t2w_brain")]), ]) # fmt:on if ants_affine_init: from nipype.interfaces.ants.utils import AI ants_kwargs = dict( metric=("Mattes", 32, "Regular", 0.2), transform=("Affine", 0.1), search_factor=(20, 0.12), principal_axes=False, convergence=(10, 1e-6, 10), search_grid=(40, (0, 40, 40)), verbose=True, ) if ants_affine_init == "random": ants_kwargs["metric"] = ("Mattes", 32, "Random", 0.2) if ants_affine_init == "search": ants_kwargs["search_grid"] = (20, (20, 40, 40)) init_aff = pe.Node( AI(**ants_kwargs), name="init_aff", n_procs=omp_nthreads, ) if tpl_regmask_path: init_aff.inputs.fixed_image_mask = _pop(tpl_regmask_path) # fmt:off workflow.connect([ (clip_tmpl, init_aff, [("out_file", "fixed_image")]), (inputnode, init_aff, [("in_t2w", "moving_image")]), (init_aff, norm, [("output_transform", "initial_moving_transform") ]), ]) # fmt:on return workflow
def init_coregistration_wf( *, bspline_fitting_distance=200, mem_gb=3.0, name="coregistration_wf", omp_nthreads=None, sloppy=False, debug=False, ): """ Set-up a T2w-to-T1w within-baby co-registration framework. See the ANTs' registration config file (under ``nibabies/data``) for further details. The main surprise in it is that, for some participants, accurate registration requires extra degrees of freedom (one affine level and one SyN level) to ensure that the T1w and T2w images align well. I attribute this requirement to the following potential reasons: * The T1w image and the T2w image were acquired in different sessions, apart in time enough for growth to happen. Although this is, in theory possible, it doesn't seem the images we have tested on are acquired on different sessions. * The skull is still so malleable that a change of position of the baby inside the coil made an actual change on the overall shape of their head. * Nonlinear distortions of the T1w and T2w images are, for some reason, more notorious for babies than they are for adults. We would need to look into each sequence's details to confirm this. Parameters ---------- bspline_fitting_distance : :obj:`float` Distance in mm between B-Spline control points for N4 INU estimation. mem_gb : :obj:`float` Base memory fingerprint unit. name : :obj:`str` This particular workflow's unique name (Nipype requirement). omp_nthreads : :obj:`int` The number of threads for individual processes in this workflow. sloppy : :obj:`bool` Run in *sloppy* mode. debug : :obj:`bool` Produce intermediate registration files Inputs ------ in_t1w : :obj:`str` The unprocessed input T1w image. in_t2w_preproc : :obj:`str` The preprocessed input T2w image, from the brain extraction workflow. in_mask : :obj:`str` The brainmask, as obtained in T2w space. in_probmap : :obj:`str` The probabilistic brainmask, as obtained in T2w space. Outputs ------- t1w_preproc : :obj:`str` The preprocessed T1w image (INU and clipping). t2w_preproc : :obj:`str` The preprocessed T2w image (INU and clipping), aligned into the T1w's space. t1w_brain : :obj:`str` The preprocessed, brain-extracted T1w image. t1w_mask : :obj:`str` The binary brainmask projected from the T2w. t1w2t2w_xfm : :obj:`str` The T1w-to-T2w mapping. """ from nipype.interfaces.ants import N4BiasFieldCorrection from niworkflows.interfaces.fixes import ( FixHeaderRegistration as Registration, FixHeaderApplyTransforms as ApplyTransforms, ) from niworkflows.interfaces.nibabel import ApplyMask, Binarize from ...interfaces.nibabel import BinaryDilation workflow = pe.Workflow(name) inputnode = pe.Node( niu.IdentityInterface( fields=["in_t1w", "in_t2w_preproc", "in_mask", "in_probmap"]), name="inputnode", ) outputnode = pe.Node( niu.IdentityInterface(fields=[ "t1w_preproc", "t1w_brain", "t1w_mask", "t1w2t2w_xfm", "t2w_preproc", ]), name="outputnode", ) fixed_masks_arg = pe.Node(niu.Merge(3), name="fixed_masks_arg", run_without_submitting=True) # Dilate t2w mask for easier t1->t2 registration reg_mask = pe.Node(BinaryDilation(radius=8, iterations=3), name="reg_mask") refine_mask = pe.Node(BinaryDilation(radius=8, iterations=1), name="refine_mask") # Set up T2w -> T1w within-subject registration coreg = pe.Node( Registration( from_file=pkgr_fn("nibabies.data", "within_subject_t1t2.json")), name="coreg", n_procs=omp_nthreads, mem_gb=mem_gb, ) coreg.inputs.float = sloppy if debug: coreg.inputs.args = "--write-interval-volumes 5" coreg.inputs.output_inverse_warped_image = sloppy coreg.inputs.output_warped_image = sloppy map_mask = pe.Node(ApplyTransforms(interpolation="Gaussian"), name="map_mask", mem_gb=1) map_t2w = pe.Node(ApplyTransforms(interpolation="BSpline"), name="map_t2w", mem_gb=1) thr_mask = pe.Node(Binarize(thresh_low=0.80), name="thr_mask") final_n4 = pe.Node( N4BiasFieldCorrection( dimension=3, bspline_fitting_distance=bspline_fitting_distance, save_bias=True, copy_header=True, n_iterations=[50] * 5, convergence_threshold=1e-7, rescale_intensities=True, shrink_factor=4, ), n_procs=omp_nthreads, name="final_n4", ) apply_mask = pe.Node(ApplyMask(), name="apply_mask") # fmt:off workflow.connect([ (inputnode, map_mask, [("in_t1w", "reference_image")]), (inputnode, final_n4, [("in_t1w", "input_image")]), (inputnode, coreg, [("in_t1w", "moving_image"), ("in_t2w_preproc", "fixed_image")]), (inputnode, map_mask, [("in_probmap", "input_image")]), (inputnode, reg_mask, [("in_mask", "in_file")]), (inputnode, refine_mask, [("in_mask", "in_file")]), (reg_mask, fixed_masks_arg, [("out_file", "in1")]), (reg_mask, fixed_masks_arg, [("out_file", "in2")]), (refine_mask, fixed_masks_arg, [("out_file", "in3")]), (inputnode, map_t2w, [("in_t1w", "reference_image")]), (inputnode, map_t2w, [("in_t2w_preproc", "input_image")]), (fixed_masks_arg, coreg, [("out", "fixed_image_masks")]), (coreg, map_mask, [ ("reverse_transforms", "transforms"), ("reverse_invert_flags", "invert_transform_flags"), ]), (coreg, map_t2w, [ ("reverse_transforms", "transforms"), ("reverse_invert_flags", "invert_transform_flags"), ]), (map_mask, thr_mask, [("output_image", "in_file")]), (map_mask, final_n4, [("output_image", "weight_image")]), (final_n4, apply_mask, [("output_image", "in_file")]), (thr_mask, apply_mask, [("out_mask", "in_mask")]), (final_n4, outputnode, [("output_image", "t1w_preproc")]), (map_t2w, outputnode, [("output_image", "t2w_preproc")]), (thr_mask, outputnode, [("out_mask", "t1w_mask")]), (apply_mask, outputnode, [("out_file", "t1w_brain")]), (coreg, outputnode, [("forward_transforms", "t1w2t2w_xfm")]), ]) # fmt:on return workflow
def init_enhance_and_skullstrip_dwi_wf( name="enhance_and_skullstrip_dwi_wf", omp_nthreads=1 ): """ Enhance a *b0* reference and perform brain extraction. This workflow takes in a *b0* reference image and sharpens the histogram with the application of the N4 algorithm for removing the :abbr:`INU (intensity non-uniformity)` bias field and calculates a signal mask. Steps of this workflow are: 1. Run ANTs' ``N4BiasFieldCorrection`` on the input dwi reference image and mask. 2. Calculate a loose mask using FSL's ``bet``, with one mathematical morphology dilation of one iteration and a sphere of 6mm as structuring element. 3. Mask the :abbr:`INU (intensity non-uniformity)`-corrected image with the latest mask calculated in 3), then use AFNI's ``3dUnifize`` to *standardize* the T2* contrast distribution. 4. Calculate a mask using AFNI's ``3dAutomask`` after the contrast enhancement of 4). 5. Calculate a final mask as the intersection of 2) and 4). 6. Apply final mask on the enhanced reference. Workflow Graph: .. workflow :: :graph2use: orig :simple_form: yes from dmriprep.workflows.dwi.util import init_enhance_and_skullstrip_dwi_wf wf = init_enhance_and_skullstrip_dwi_wf(omp_nthreads=1) .. _N4BiasFieldCorrection: https://hdl.handle.net/10380/3053 Parameters ---------- name : str Name of workflow (default: ``enhance_and_skullstrip_dwi_wf``) omp_nthreads : int number of threads available to parallel nodes Inputs ------ in_file The *b0* reference (single volume) pre_mask initial mask Outputs ------- bias_corrected_file the ``in_file`` after `N4BiasFieldCorrection`_ skull_stripped_file the ``bias_corrected_file`` after skull-stripping mask_file mask of the skull-stripped input file out_report reportlet for the skull-stripping """ from niworkflows.interfaces.header import CopyXForm from niworkflows.interfaces.fixes import ( FixN4BiasFieldCorrection as N4BiasFieldCorrection, ) from niworkflows.interfaces.nibabel import ApplyMask workflow = Workflow(name=name) inputnode = pe.Node( niu.IdentityInterface(fields=["in_file", "pre_mask"]), name="inputnode" ) outputnode = pe.Node( niu.IdentityInterface( fields=["mask_file", "skull_stripped_file", "bias_corrected_file"] ), name="outputnode", ) # Run N4 normally, force num_threads=1 for stability (images are small, no need for >1) n4_correct = pe.Node( N4BiasFieldCorrection( dimension=3, copy_header=True, bspline_fitting_distance=200 ), shrink_factor=2, name="n4_correct", n_procs=1, ) n4_correct.inputs.rescale_intensities = True # Create a generous BET mask out of the bias-corrected EPI skullstrip_first_pass = pe.Node( fsl.BET(frac=0.2, mask=True), name="skullstrip_first_pass" ) bet_dilate = pe.Node( fsl.DilateImage( operation="max", kernel_shape="sphere", kernel_size=6.0, internal_datatype="char", ), name="skullstrip_first_dilate", ) bet_mask = pe.Node(fsl.ApplyMask(), name="skullstrip_first_mask") # Use AFNI's unifize for T2 contrast & fix header unifize = pe.Node( afni.Unifize( t2=True, outputtype="NIFTI_GZ", args="-clfrac 0.2 -rbt 18.3 65.0 90.0", out_file="uni.nii.gz", ), name="unifize", ) fixhdr_unifize = pe.Node(CopyXForm(), name="fixhdr_unifize", mem_gb=0.1) # Run AFNI's 3dAutomask to extract a refined brain mask skullstrip_second_pass = pe.Node( afni.Automask(dilate=1, outputtype="NIFTI_GZ"), name="skullstrip_second_pass" ) fixhdr_skullstrip2 = pe.Node(CopyXForm(), name="fixhdr_skullstrip2", mem_gb=0.1) # Take intersection of both masks combine_masks = pe.Node(fsl.BinaryMaths(operation="mul"), name="combine_masks") normalize = pe.Node(niu.Function(function=_normalize), name="normalize") # Compute masked brain apply_mask = pe.Node(ApplyMask(), name="apply_mask") # fmt:off workflow.connect([ (inputnode, n4_correct, [("in_file", "input_image"), ("pre_mask", "mask_image")]), (inputnode, fixhdr_unifize, [("in_file", "hdr_file")]), (inputnode, fixhdr_skullstrip2, [("in_file", "hdr_file")]), (n4_correct, skullstrip_first_pass, [("output_image", "in_file")]), (skullstrip_first_pass, bet_dilate, [("mask_file", "in_file")]), (bet_dilate, bet_mask, [("out_file", "mask_file")]), (skullstrip_first_pass, bet_mask, [("out_file", "in_file")]), (bet_mask, unifize, [("out_file", "in_file")]), (unifize, fixhdr_unifize, [("out_file", "in_file")]), (fixhdr_unifize, skullstrip_second_pass, [("out_file", "in_file")]), (skullstrip_first_pass, combine_masks, [("mask_file", "in_file")]), (skullstrip_second_pass, fixhdr_skullstrip2, [("out_file", "in_file")]), (fixhdr_skullstrip2, combine_masks, [("out_file", "operand_file")]), (combine_masks, apply_mask, [("out_file", "in_mask")]), (combine_masks, outputnode, [("out_file", "mask_file")]), (n4_correct, normalize, [("output_image", "in_file")]), (normalize, apply_mask, [("out", "in_file")]), (normalize, outputnode, [("out", "bias_corrected_file")]), (apply_mask, outputnode, [("out_file", "skull_stripped_file")]), ] ) # fmt:on return workflow
def init_func_preproc_wf( workdir=None, name="func_preproc_wf", fmap_type=None, memcalc=MemoryCalculator() ): """ simplified from fmriprep/workflows/bold/base.py """ workflow = pe.Workflow(name=name) inputnode = pe.Node( niu.IdentityInterface( fields=["bold_file", "fmaps", "metadata", *in_attrs_from_anat_preproc_wf] ), name="inputnode", ) outputnode = pe.Node( niu.IdentityInterface( fields=[ *func_preproc_wf_out_attrs, "aroma_confounds", "aroma_metadata", "movpar_file", "rmsd_file", "skip_vols", ] ), name="outputnode", ) def get_repetition_time(dic): return dic.get("RepetitionTime") metadatanode = pe.Node(niu.IdentityInterface(fields=["repetition_time"]), name="metadatanode") workflow.connect( [(inputnode, metadatanode, [(("metadata", get_repetition_time), "repetition_time")],)] ) # Generate a brain-masked conversion of the t1w t1w_brain = pe.Node(ApplyMask(), name="t1w_brain") workflow.connect( [(inputnode, t1w_brain, [("t1w_preproc", "in_file"), ("t1w_mask", "in_mask")])] ) # Generate a tentative boldref bold_reference_wf = init_bold_reference_wf(omp_nthreads=config.nipype.omp_nthreads) bold_reference_wf.get_node("inputnode").inputs.dummy_scans = config.workflow.dummy_scans workflow.connect(inputnode, "bold_file", bold_reference_wf, "inputnode.bold_file") workflow.connect(bold_reference_wf, "outputnode.skip_vols", outputnode, "skip_vols") # SDC (SUSCEPTIBILITY DISTORTION CORRECTION) or bypass ########################## bold_sdc_wf = init_sdc_estimate_wf(fmap_type=fmap_type) workflow.connect( [ ( inputnode, bold_sdc_wf, [("fmaps", "inputnode.fmaps"), ("metadata", "inputnode.metadata")], ), ( bold_reference_wf, bold_sdc_wf, [ ("outputnode.ref_image", "inputnode.epi_file"), ("outputnode.ref_image_brain", "inputnode.epi_brain"), ("outputnode.bold_mask", "inputnode.epi_mask"), ], ), ] ) # Top-level BOLD splitter bold_split = pe.Node(FSLSplit(dimension="t"), name="bold_split", mem_gb=memcalc.series_gb * 3) workflow.connect(inputnode, "bold_file", bold_split, "in_file") # HMC on the BOLD bold_hmc_wf = init_bold_hmc_wf( name="bold_hmc_wf", mem_gb=memcalc.series_gb, omp_nthreads=config.nipype.omp_nthreads, ) workflow.connect( [ ( bold_reference_wf, bold_hmc_wf, [ ("outputnode.raw_ref_image", "inputnode.raw_ref_image"), ("outputnode.bold_file", "inputnode.bold_file"), ], ), ( bold_hmc_wf, outputnode, [("outputnode.movpar_file", "movpar_file"), ("outputnode.rmsd_file", "rmsd_file")], ), ] ) # calculate BOLD registration to T1w bold_reg_wf = init_bold_reg_wf( freesurfer=config.workflow.run_reconall, use_bbr=config.workflow.use_bbr, bold2t1w_dof=config.workflow.bold2t1w_dof, bold2t1w_init=config.workflow.bold2t1w_init, mem_gb=memcalc.series_std_gb, omp_nthreads=config.nipype.omp_nthreads, use_compression=False, ) workflow.connect( [ (inputnode, bold_reg_wf, [("t1w_dseg", "inputnode.t1w_dseg")]), (t1w_brain, bold_reg_wf, [("out_file", "inputnode.t1w_brain")]), (bold_sdc_wf, bold_reg_wf, [("outputnode.epi_brain", "inputnode.ref_bold_brain")],), ] ) # apply BOLD registration to T1w bold_t1_trans_wf = init_bold_t1_trans_wf( name="bold_t1_trans_wf", freesurfer=config.workflow.run_reconall, use_fieldwarp=fmap_type is not None, multiecho=False, mem_gb=memcalc.series_std_gb, omp_nthreads=config.nipype.omp_nthreads, use_compression=False, ) workflow.connect( [ ( inputnode, bold_t1_trans_wf, [("bold_file", "inputnode.name_source"), ("t1w_mask", "inputnode.t1w_mask")], ), ( bold_sdc_wf, bold_t1_trans_wf, [ ("outputnode.epi_brain", "inputnode.ref_bold_brain"), ("outputnode.epi_mask", "inputnode.ref_bold_mask"), ("outputnode.out_warp", "inputnode.fieldwarp"), ], ), (t1w_brain, bold_t1_trans_wf, [("out_file", "inputnode.t1w_brain")]), (bold_split, bold_t1_trans_wf, [("out_files", "inputnode.bold_split")]), (bold_hmc_wf, bold_t1_trans_wf, [("outputnode.xforms", "inputnode.hmc_xforms")],), ( bold_reg_wf, bold_t1_trans_wf, [("outputnode.itk_bold_to_t1", "inputnode.itk_bold_to_t1")], ), ] ) # Apply transforms in 1 shot # Only use uncompressed output if AROMA is to be run bold_bold_trans_wf = init_bold_preproc_trans_wf( mem_gb=memcalc.series_std_gb, omp_nthreads=config.nipype.omp_nthreads, use_compression=not config.execution.low_mem, use_fieldwarp=True, name="bold_bold_trans_wf", ) # bold_bold_trans_wf.inputs.inputnode.name_source = ref_file workflow.connect( [ (inputnode, bold_bold_trans_wf, [("bold_file", "inputnode.name_source")]), (bold_split, bold_bold_trans_wf, [("out_files", "inputnode.bold_file")]), (bold_hmc_wf, bold_bold_trans_wf, [("outputnode.xforms", "inputnode.hmc_xforms")],), ( bold_sdc_wf, bold_bold_trans_wf, [ ("outputnode.out_warp", "inputnode.fieldwarp"), ("outputnode.epi_mask", "inputnode.bold_mask"), ], ), ] ) # Apply transforms in 1 shot # Only use uncompressed output if AROMA is to be run bold_std_trans_wf = init_bold_std_trans_wf( freesurfer=config.workflow.run_reconall, mem_gb=memcalc.series_std_gb, omp_nthreads=config.nipype.omp_nthreads, spaces=config.workflow.spaces, name="bold_std_trans_wf", use_compression=not config.execution.low_mem, use_fieldwarp=fmap_type is not None, ) workflow.connect( [ ( inputnode, bold_std_trans_wf, [ ("template", "inputnode.templates"), ("anat2std_xfm", "inputnode.anat2std_xfm"), ("bold_file", "inputnode.name_source"), ], ), (bold_split, bold_std_trans_wf, [("out_files", "inputnode.bold_split")]), (bold_hmc_wf, bold_std_trans_wf, [("outputnode.xforms", "inputnode.hmc_xforms")],), ( bold_reg_wf, bold_std_trans_wf, [("outputnode.itk_bold_to_t1", "inputnode.itk_bold_to_t1")], ), ( bold_bold_trans_wf, bold_std_trans_wf, [("outputnode.bold_mask", "inputnode.bold_mask")], ), (bold_sdc_wf, bold_std_trans_wf, [("outputnode.out_warp", "inputnode.fieldwarp")],), ( bold_std_trans_wf, outputnode, [ ("outputnode.bold_std", "bold_std"), ("outputnode.bold_std_ref", "bold_std_ref"), ("outputnode.bold_mask_std", "bold_mask_std"), ], ), ] ) ica_aroma_wf = init_ica_aroma_wf( mem_gb=memcalc.series_std_gb, metadata={"RepetitionTime": np.nan}, omp_nthreads=config.nipype.omp_nthreads, use_fieldwarp=True, err_on_aroma_warn=config.workflow.aroma_err_on_warn, aroma_melodic_dim=config.workflow.aroma_melodic_dim, name="ica_aroma_wf", ) ica_aroma_wf.get_node("ica_aroma").inputs.denoise_type = "no" ica_aroma_wf.remove_nodes([ica_aroma_wf.get_node("add_nonsteady")]) workflow.connect( [ (inputnode, ica_aroma_wf, [("bold_file", "inputnode.name_source")]), ( metadatanode, ica_aroma_wf, [("repetition_time", "melodic.tr_sec",), ("repetition_time", "ica_aroma.TR")], ), (bold_hmc_wf, ica_aroma_wf, [("outputnode.movpar_file", "inputnode.movpar_file")]), (bold_reference_wf, ica_aroma_wf, [("outputnode.skip_vols", "inputnode.skip_vols")]), ( bold_std_trans_wf, ica_aroma_wf, [ ("outputnode.bold_std", "inputnode.bold_std"), ("outputnode.bold_mask_std", "inputnode.bold_mask_std"), ("outputnode.spatial_reference", "inputnode.spatial_reference"), ], ), ( ica_aroma_wf, outputnode, [ ("outputnode.aroma_noise_ics", "aroma_noise_ics"), ("outputnode.melodic_mix", "melodic_mix"), ("outputnode.nonaggr_denoised_file", "nonaggr_denoised_file"), ("outputnode.aroma_confounds", "aroma_confounds"), ("outputnode.aroma_metadata", "aroma_metadata"), ], ), ] ) bold_confounds_wf = init_bold_confs_wf( mem_gb=memcalc.series_std_gb, metadata={}, regressors_all_comps=config.workflow.regressors_all_comps, regressors_fd_th=config.workflow.regressors_fd_th, regressors_dvars_th=config.workflow.regressors_dvars_th, name="bold_confounds_wf", ) bold_confounds_wf.get_node("inputnode").inputs.t1_transform_flags = [False] workflow.connect( [ ( inputnode, bold_confounds_wf, [("t1w_tpms", "inputnode.t1w_tpms"), ("t1w_mask", "inputnode.t1w_mask")], ), ( bold_hmc_wf, bold_confounds_wf, [ ("outputnode.movpar_file", "inputnode.movpar_file"), ("outputnode.rmsd_file", "inputnode.rmsd_file"), ], ), ( bold_reg_wf, bold_confounds_wf, [("outputnode.itk_t1_to_bold", "inputnode.t1_bold_xform")], ), ( bold_reference_wf, bold_confounds_wf, [("outputnode.skip_vols", "inputnode.skip_vols")], ), (bold_confounds_wf, outputnode, [("outputnode.confounds_file", "confounds")]), ( bold_confounds_wf, outputnode, [("outputnode.confounds_metadata", "confounds_metadata")], ), ( metadatanode, bold_confounds_wf, [ ("repetition_time", "acompcor.repetition_time"), ("repetition_time", "tcompcor.repetition_time"), ], ), ( bold_bold_trans_wf, bold_confounds_wf, [ ("outputnode.bold", "inputnode.bold"), ("outputnode.bold_mask", "inputnode.bold_mask"), ], ), ] ) carpetplot_wf = init_carpetplot_wf( mem_gb=memcalc.series_std_gb, metadata={"RepetitionTime": np.nan}, cifti_output=config.workflow.cifti_output, name="carpetplot_wf", ) carpetplot_select_std = pe.Node( KeySelect(fields=["std2anat_xfm"], key="MNI152NLin2009cAsym"), name="carpetplot_select_std", run_without_submitting=True, ) workflow.connect( [ ( inputnode, carpetplot_select_std, [("std2anat_xfm", "std2anat_xfm"), ("template", "keys")], ), (carpetplot_select_std, carpetplot_wf, [("std2anat_xfm", "inputnode.std2anat_xfm")],), ( bold_bold_trans_wf, carpetplot_wf, [ ("outputnode.bold", "inputnode.bold"), ("outputnode.bold_mask", "inputnode.bold_mask"), ], ), ( bold_reg_wf, carpetplot_wf, [("outputnode.itk_t1_to_bold", "inputnode.t1_bold_xform")], ), ( bold_confounds_wf, carpetplot_wf, [("outputnode.confounds_file", "inputnode.confounds_file")], ), (metadatanode, carpetplot_wf, [("repetition_time", "conf_plot.tr")]), ] ) # Custom make_reportnode(workflow) assert workdir is not None make_reportnode_datasink(workflow, workdir) return workflow
def init_bold_reference_wf( omp_nthreads, bold_file=None, sbref_files=None, brainmask_thresh=0.85, pre_mask=False, multiecho=False, name="bold_reference_wf", gen_report=False, ): """ Build a workflow that generates reference BOLD images for a series. The raw reference image is the target of :abbr:`HMC (head motion correction)`, and a contrast-enhanced reference is the subject of distortion correction, as well as boundary-based registration to T1w and template spaces. Workflow Graph .. workflow:: :graph2use: orig :simple_form: yes from niworkflows.func.util import init_bold_reference_wf wf = init_bold_reference_wf(omp_nthreads=1) Parameters ---------- omp_nthreads : :obj:`int` Maximum number of threads an individual process may use bold_file : :obj:`str` BOLD series NIfTI file sbref_files : :obj:`list` or :obj:`bool` Single band (as opposed to multi band) reference NIfTI file. If ``True`` is passed, the workflow is built to accommodate SBRefs, but the input is left undefined (i.e., it is left open for connection) brainmask_thresh: :obj:`float` Lower threshold for the probabilistic brainmask to obtain the final binary mask (default: 0.85). pre_mask : :obj:`bool` Indicates whether the ``pre_mask`` input will be set (and thus, step 1 should be skipped). multiecho : :obj:`bool` If multiecho data was supplied, data from the first echo will be selected name : :obj:`str` Name of workflow (default: ``bold_reference_wf``) gen_report : :obj:`bool` Whether a mask report node should be appended in the end Inputs ------ bold_file : str BOLD series NIfTI file bold_mask : bool A tentative brain mask to initialize the workflow (requires ``pre_mask`` parameter set ``True``). dummy_scans : int or None Number of non-steady-state volumes specified by user at beginning of ``bold_file`` sbref_file : str single band (as opposed to multi band) reference NIfTI file Outputs ------- bold_file : str Validated BOLD series NIfTI file raw_ref_image : str Reference image to which BOLD series is motion corrected skip_vols : int Number of non-steady-state volumes selected at beginning of ``bold_file`` algo_dummy_scans : int Number of non-steady-state volumes agorithmically detected at beginning of ``bold_file`` ref_image : str Contrast-enhanced reference image ref_image_brain : str Skull-stripped reference image bold_mask : str Skull-stripping mask of reference image validation_report : str HTML reportlet indicating whether ``bold_file`` had a valid affine Subworkflows * :py:func:`~niworkflows.func.util.init_enhance_and_skullstrip_wf` """ workflow = Workflow(name=name) workflow.__desc__ = f"""\ First, a reference volume and its skull-stripped version were generated {'from the shortest echo of the BOLD run' * multiecho} using a custom methodology of *fMRIPrep*. """ inputnode = pe.Node( niu.IdentityInterface( fields=["bold_file", "bold_mask", "dummy_scans", "sbref_file"] ), name="inputnode", ) outputnode = pe.Node( niu.IdentityInterface( fields=[ "bold_file", "raw_ref_image", "skip_vols", "algo_dummy_scans", "ref_image", "ref_image_brain", "bold_mask", "validation_report", "mask_report", ] ), name="outputnode", ) # Simplify manually setting input image if bold_file is not None: inputnode.inputs.bold_file = bold_file val_bold = pe.MapNode( ValidateImage(), name="val_bold", mem_gb=DEFAULT_MEMORY_MIN_GB, iterfield=["in_file"], ) get_dummy = pe.Node(NonsteadyStatesDetector(), name="get_dummy") gen_avg = pe.Node(RobustAverage(), name="gen_avg", mem_gb=1) calc_dummy_scans = pe.Node( niu.Function(function=_pass_dummy_scans, output_names=["skip_vols_num"]), name="calc_dummy_scans", run_without_submitting=True, mem_gb=DEFAULT_MEMORY_MIN_GB, ) bold_1st = pe.Node( niu.Select(index=[0]), name="bold_1st", run_without_submitting=True ) validate_1st = pe.Node( niu.Select(index=[0]), name="validate_1st", run_without_submitting=True ) # fmt: off workflow.connect([ (inputnode, val_bold, [(("bold_file", listify), "in_file")]), (inputnode, get_dummy, [(("bold_file", pop_file), "in_file")]), (inputnode, calc_dummy_scans, [("dummy_scans", "dummy_scans")]), (val_bold, bold_1st, [(("out_file", listify), "inlist")]), (get_dummy, calc_dummy_scans, [("n_dummy", "algo_dummy_scans")]), (calc_dummy_scans, outputnode, [("skip_vols_num", "skip_vols")]), (gen_avg, outputnode, [("out_file", "raw_ref_image")]), (val_bold, validate_1st, [(("out_report", listify), "inlist")]), (bold_1st, outputnode, [("out", "bold_file")]), (validate_1st, outputnode, [("out", "validation_report")]), (get_dummy, outputnode, [("n_dummy", "algo_dummy_scans")]), ]) # fmt: on if not pre_mask: from nirodents.workflows.brainextraction import init_rodent_brain_extraction_wf brain_extraction_wf = init_rodent_brain_extraction_wf( ants_affine_init=False, debug=config.execution.debug is True ) # fmt: off workflow.connect([ (gen_avg, brain_extraction_wf, [ ("out_file", "inputnode.in_files"), ]), (brain_extraction_wf, outputnode, [ ("outputnode.out_corrected", "ref_image"), ("outputnode.out_mask", "bold_mask"), ("outputnode.out_brain", "ref_image_brain"), ]), ]) # fmt: on else: from niworkflows.interfaces.nibabel import ApplyMask mask_brain = pe.Node(ApplyMask(), name="mask_brain") # fmt: off workflow.connect([ (inputnode, mask_brain, [("bold_mask", "in_mask")]), (inputnode, outputnode, [("bold_mask", "bold_mask")]), (gen_avg, outputnode, [("out_file", "ref_image")]), (gen_avg, mask_brain, [("out_file", "in_file")]), (mask_brain, outputnode, [("out_file", "ref_image_brain")]), ]) # fmt: on if not sbref_files: # fmt: off workflow.connect([ (val_bold, gen_avg, [(("out_file", pop_file), "in_file")]), # pop first echo of ME-EPI (get_dummy, gen_avg, [("t_mask", "t_mask")]), ]) # fmt: on return workflow from niworkflows.interfaces.nibabel import MergeSeries nsbrefs = 0 if sbref_files is not True: # If not boolean, then it is a list-of or pathlike. inputnode.inputs.sbref_file = sbref_files nsbrefs = 1 if isinstance(sbref_files, str) else len(sbref_files) val_sbref = pe.MapNode( ValidateImage(), name="val_sbref", mem_gb=DEFAULT_MEMORY_MIN_GB, iterfield=["in_file"], ) merge_sbrefs = pe.Node(MergeSeries(), name="merge_sbrefs") # fmt: off workflow.connect([ (inputnode, val_sbref, [(("sbref_file", listify), "in_file")]), (val_sbref, merge_sbrefs, [("out_file", "in_files")]), (merge_sbrefs, gen_avg, [("out_file", "in_file")]), ]) # fmt: on # Edit the boilerplate as the SBRef will be the reference workflow.__desc__ = f"""\ First, a reference volume and its skull-stripped version were generated by aligning and averaging{' the first echo of' * multiecho} {nsbrefs or ''} single-band references (SBRefs). """ if gen_report: mask_reportlet = pe.Node(SimpleShowMaskRPT(), name="mask_reportlet") # fmt: off workflow.connect([ (outputnode, mask_reportlet, [ ("ref_image", "background_file"), ("bold_mask", "mask_file"), ]), ]) # fmt: on return workflow
def init_func_preproc_wf(bold_file): """ This workflow controls the functional preprocessing stages of *fMRIPrep*. Workflow Graph .. workflow:: :graph2use: orig :simple_form: yes from fprodents.workflows.tests import mock_config from fprodents import config from fprodents.workflows.bold.base import init_func_preproc_wf with mock_config(): bold_file = config.execution.bids_dir / 'sub-01' / 'func' \ / 'sub-01_task-mixedgamblestask_run-01_bold.nii.gz' wf = init_func_preproc_wf(str(bold_file)) Inputs ------ bold_file BOLD series NIfTI file t1w_preproc Bias-corrected structural template image t1w_mask Mask of the skull-stripped template image anat_dseg Segmentation of preprocessed structural image, including gray-matter (GM), white-matter (WM) and cerebrospinal fluid (CSF) t1w_asec Segmentation of structural image, done with FreeSurfer. t1w_aparc Parcellation of structural image, done with FreeSurfer. anat_tpms List of tissue probability maps in T1w space template List of templates to target anat2std_xfm List of transform files, collated with templates std2anat_xfm List of inverse transform files, collated with templates subjects_dir FreeSurfer SUBJECTS_DIR subject_id FreeSurfer subject ID t1w2fsnative_xfm LTA-style affine matrix translating from T1w to FreeSurfer-conformed subject space fsnative2t1w_xfm LTA-style affine matrix translating from FreeSurfer-conformed subject space to T1w Outputs ------- bold_t1 BOLD series, resampled to T1w space bold_mask_t1 BOLD series mask in T1w space bold_std BOLD series, resampled to template space bold_mask_std BOLD series mask in template space confounds TSV of confounds surfaces BOLD series, resampled to FreeSurfer surfaces aroma_noise_ics Noise components identified by ICA-AROMA melodic_mix FSL MELODIC mixing matrix See Also -------- * :py:func:`~fprodents.workflows.bold.stc.init_bold_stc_wf` * :py:func:`~fprodents.workflows.bold.hmc.init_bold_hmc_wf` * :py:func:`~fprodents.workflows.bold.t2s.init_bold_t2s_wf` * :py:func:`~fprodents.workflows.bold.registration.init_bold_t1_trans_wf` * :py:func:`~fprodents.workflows.bold.registration.init_bold_reg_wf` * :py:func:`~fprodents.workflows.bold.confounds.init_bold_confounds_wf` * :py:func:`~fprodents.workflows.bold.confounds.init_ica_aroma_wf` * :py:func:`~fprodents.workflows.bold.resampling.init_bold_std_trans_wf` * :py:func:`~fprodents.workflows.bold.resampling.init_bold_preproc_trans_wf` * :py:func:`~fprodents.workflows.bold.resampling.init_bold_surf_wf` """ from niworkflows.engine.workflows import LiterateWorkflow as Workflow from niworkflows.interfaces.fixes import FixHeaderApplyTransforms as ApplyTransforms from niworkflows.interfaces.nibabel import ApplyMask from niworkflows.interfaces.utility import KeySelect, DictMerge from nipype.interfaces.freesurfer.utils import LTAConvert from ...patch.utils import extract_entities mem_gb = {"filesize": 1, "resampled": 1, "largemem": 1} bold_tlen = 10 # Have some options handy omp_nthreads = config.nipype.omp_nthreads spaces = config.workflow.spaces output_dir = str(config.execution.output_dir) # Extract BIDS entities and metadata from BOLD file(s) entities = extract_entities(bold_file) layout = config.execution.layout # Take first file as reference ref_file = pop_file(bold_file) metadata = layout.get_metadata(ref_file) echo_idxs = listify(entities.get("echo", [])) multiecho = len(echo_idxs) > 2 if len(echo_idxs) == 1: config.loggers.warning( f"Running a single echo <{ref_file}> from a seemingly multi-echo dataset." ) bold_file = ref_file # Just in case - drop the list if len(echo_idxs) == 2: raise RuntimeError( "Multi-echo processing requires at least three different echos (found two)." ) if multiecho: # Drop echo entity for future queries, have a boolean shorthand entities.pop("echo", None) # reorder echoes from shortest to largest tes, bold_file = zip(*sorted([(layout.get_metadata(bf)["EchoTime"], bf) for bf in bold_file])) ref_file = bold_file[0] # Reset reference to be the shortest TE if os.path.isfile(ref_file): bold_tlen, mem_gb = _create_mem_gb(ref_file) wf_name = _get_wf_name(ref_file) config.loggers.workflow.debug( "Creating bold processing workflow for <%s> (%.2f GB / %d TRs). " "Memory resampled/largemem=%.2f/%.2f GB.", ref_file, mem_gb["filesize"], bold_tlen, mem_gb["resampled"], mem_gb["largemem"], ) # Find associated sbref, if possible entities["suffix"] = "sbref" entities["extension"] = ["nii", "nii.gz"] # Overwrite extensions sbref_files = layout.get(return_type="file", **entities) sbref_msg = f"No single-band-reference found for {os.path.basename(ref_file)}." if sbref_files and "sbref" in config.workflow.ignore: sbref_msg = "Single-band reference file(s) found and ignored." elif sbref_files: sbref_msg = "Using single-band reference file(s) {}.".format(",".join( [os.path.basename(sbf) for sbf in sbref_files])) config.loggers.workflow.info(sbref_msg) # Check whether STC must/can be run run_stc = (bool(metadata.get("SliceTiming")) and 'slicetiming' not in config.workflow.ignore) # Build workflow workflow = Workflow(name=wf_name) workflow.__postdesc__ = """\ All resamplings can be performed with *a single interpolation step* by composing all the pertinent transformations (i.e. head-motion transform matrices, susceptibility distortion correction when available, and co-registrations to anatomical and output spaces). Gridded (volumetric) resamplings were performed using `antsApplyTransforms` (ANTs), configured with Lanczos interpolation to minimize the smoothing effects of other kernels [@lanczos]. Non-gridded (surface) resamplings were performed using `mri_vol2surf` (FreeSurfer). """ inputnode = pe.Node( niu.IdentityInterface(fields=[ "bold_file", "ref_file", "bold_ref_xfm", "n_dummy_scans", "validation_report", "subjects_dir", "subject_id", "anat_preproc", "anat_mask", "anat_dseg", "anat_tpms", "anat2std_xfm", "std2anat_xfm", "template", "anat2fsnative_xfm", "fsnative2anat_xfm", ]), name="inputnode", ) inputnode.inputs.bold_file = bold_file outputnode = pe.Node( niu.IdentityInterface(fields=[ "bold_mask", "bold_t1", "bold_t1_ref", "bold_mask_t1", "bold_std", "bold_std_ref", "bold_mask_std", "bold_native", "surfaces", "confounds", "aroma_noise_ics", "melodic_mix", "nonaggr_denoised_file", "confounds_metadata", ]), name="outputnode", ) # Generate a brain-masked conversion of the t1w and bold reference images t1w_brain = pe.Node(ApplyMask(), name="t1w_brain") lta_convert = pe.Node(LTAConvert(out_fsl=True, out_itk=True), name="lta_convert") # BOLD buffer: an identity used as a pointer to either the original BOLD # or the STC'ed one for further use. boldbuffer = pe.Node(niu.IdentityInterface(fields=["bold_file"]), name="boldbuffer") summary = pe.Node( FunctionalSummary( slice_timing=run_stc, registration="FSL", registration_dof=config.workflow.bold2t1w_dof, registration_init=config.workflow.bold2t1w_init, pe_direction=metadata.get("PhaseEncodingDirection"), echo_idx=echo_idxs, tr=metadata.get("RepetitionTime"), distortion_correction="<not implemented>", ), name="summary", mem_gb=config.DEFAULT_MEMORY_MIN_GB, run_without_submitting=True, ) summary.inputs.dummy_scans = config.workflow.dummy_scans func_derivatives_wf = init_func_derivatives_wf( bids_root=layout.root, metadata=metadata, output_dir=output_dir, spaces=spaces, use_aroma=config.workflow.use_aroma, ) # fmt:off workflow.connect([ (outputnode, func_derivatives_wf, [ ('bold_t1', 'inputnode.bold_t1'), ('bold_t1_ref', 'inputnode.bold_t1_ref'), ('bold_mask_t1', 'inputnode.bold_mask_t1'), ('bold_native', 'inputnode.bold_native'), ('confounds', 'inputnode.confounds'), ('surfaces', 'inputnode.surf_files'), ('aroma_noise_ics', 'inputnode.aroma_noise_ics'), ('melodic_mix', 'inputnode.melodic_mix'), ('nonaggr_denoised_file', 'inputnode.nonaggr_denoised_file'), ('confounds_metadata', 'inputnode.confounds_metadata'), ]), ]) # fmt:on # Top-level BOLD splitter bold_split = pe.Node(FSLSplit(dimension="t"), name="bold_split", mem_gb=mem_gb["filesize"] * 3) # calculate BOLD registration to T1w bold_reg_wf = init_bold_reg_wf( bold2t1w_dof=config.workflow.bold2t1w_dof, bold2t1w_init=config.workflow.bold2t1w_init, mem_gb=mem_gb["resampled"], name="bold_reg_wf", omp_nthreads=omp_nthreads, use_compression=False, ) # apply BOLD registration to T1w bold_t1_trans_wf = init_bold_t1_trans_wf( name="bold_t1_trans_wf", use_fieldwarp=False, multiecho=multiecho, mem_gb=mem_gb["resampled"], omp_nthreads=omp_nthreads, use_compression=False, ) t1w_mask_bold_tfm = pe.Node(ApplyTransforms(interpolation="MultiLabel"), name="t1w_mask_bold_tfm", mem_gb=0.1) # get confounds bold_confounds_wf = init_bold_confs_wf( mem_gb=mem_gb["largemem"], metadata=metadata, regressors_all_comps=config.workflow.regressors_all_comps, regressors_fd_th=config.workflow.regressors_fd_th, regressors_dvars_th=config.workflow.regressors_dvars_th, name="bold_confounds_wf", ) bold_confounds_wf.get_node("inputnode").inputs.t1_transform_flags = [False] # Apply transforms in 1 shot # Only use uncompressed output if AROMA is to be run bold_bold_trans_wf = init_bold_preproc_trans_wf( mem_gb=mem_gb["resampled"], omp_nthreads=omp_nthreads, use_compression=not config.execution.low_mem, use_fieldwarp=False, name="bold_bold_trans_wf", ) bold_bold_trans_wf.inputs.inputnode.name_source = ref_file # SLICE-TIME CORRECTION (or bypass) ############################################# if run_stc: bold_stc_wf = init_bold_stc_wf(name="bold_stc_wf", metadata=metadata) # fmt:off workflow.connect([ (inputnode, bold_stc_wf, [("n_dummy_scans", "inputnode.skip_vols") ]), (bold_stc_wf, boldbuffer, [("outputnode.stc_file", "bold_file")]), ]) # fmt:on if not multiecho: # fmt:off workflow.connect([(inputnode, bold_stc_wf, [('bold_file', 'inputnode.bold_file')])]) # fmt:on else: # for meepi, iterate through stc_wf for all workflows meepi_echos = boldbuffer.clone(name="meepi_echos") meepi_echos.iterables = ("bold_file", bold_file) # fmt:off workflow.connect([(meepi_echos, bold_stc_wf, [('bold_file', 'inputnode.bold_file')])]) # fmt:on elif not multiecho: # STC is too short or False # bypass STC from original BOLD to the splitter through boldbuffer # fmt:off workflow.connect([(inputnode, boldbuffer, [('bold_file', 'bold_file')]) ]) # fmt:on else: # for meepi, iterate over all meepi echos to boldbuffer boldbuffer.iterables = ("bold_file", bold_file) # MULTI-ECHO EPI DATA ############################################# if multiecho: from niworkflows.func.util import init_skullstrip_bold_wf skullstrip_bold_wf = init_skullstrip_bold_wf(name="skullstrip_bold_wf") inputnode.inputs.bold_file = ref_file # Replace reference w first echo join_echos = pe.JoinNode( niu.IdentityInterface(fields=["bold_files"]), joinsource=("meepi_echos" if run_stc is True else "boldbuffer"), joinfield=["bold_files"], name="join_echos", ) # create optimal combination, adaptive T2* map bold_t2s_wf = init_bold_t2s_wf( echo_times=tes, mem_gb=mem_gb["resampled"], omp_nthreads=omp_nthreads, name="bold_t2smap_wf", ) # fmt:off workflow.connect([ (skullstrip_bold_wf, join_echos, [('outputnode.skull_stripped_file', 'bold_files')]), (join_echos, bold_t2s_wf, [('bold_files', 'inputnode.bold_file')]), ]) # fmt:on # MAIN WORKFLOW STRUCTURE ####################################################### # fmt:off workflow.connect([ (inputnode, bold_reg_wf, [('anat_preproc', 'inputnode.t1w_brain'), ('ref_file', 'inputnode.ref_bold_brain')]), (inputnode, t1w_brain, [('anat_preproc', 'in_file'), ('anat_mask', 'in_mask')]), # convert bold reference LTA transform to other formats (inputnode, lta_convert, [('bold_ref_xfm', 'in_lta')]), # BOLD buffer has slice-time corrected if it was run, original otherwise (boldbuffer, bold_split, [('bold_file', 'in_file')]), (inputnode, summary, [('n_dummy_scans', 'algo_dummy_scans')]), # EPI-T1 registration workflow (inputnode, bold_t1_trans_wf, [('bold_file', 'inputnode.name_source'), ('anat_mask', 'inputnode.t1w_mask'), ('ref_file', 'inputnode.ref_bold_brain') ]), (t1w_brain, bold_t1_trans_wf, [('out_file', 'inputnode.t1w_brain')]), (lta_convert, bold_t1_trans_wf, [('out_itk', 'inputnode.hmc_xforms')]), (bold_reg_wf, bold_t1_trans_wf, [('outputnode.bold2anat', 'inputnode.bold2anat')]), (bold_t1_trans_wf, outputnode, [('outputnode.bold_t1', 'bold_t1'), ('outputnode.bold_t1_ref', 'bold_t1_ref')]), # transform T1 mask to BOLD (inputnode, t1w_mask_bold_tfm, [('anat_mask', 'input_image'), ('ref_file', 'reference_image')]), (bold_reg_wf, t1w_mask_bold_tfm, [('outputnode.anat2bold', 'transforms')]), (t1w_mask_bold_tfm, outputnode, [('output_image', 'bold_mask')]), # Connect bold_confounds_wf (inputnode, bold_confounds_wf, [('anat_tpms', 'inputnode.anat_tpms'), ('anat_mask', 'inputnode.t1w_mask')]), (lta_convert, bold_confounds_wf, [('out_fsl', 'inputnode.movpar_file') ]), (bold_reg_wf, bold_confounds_wf, [('outputnode.anat2bold', 'inputnode.anat2bold')]), (inputnode, bold_confounds_wf, [('n_dummy_scans', 'inputnode.skip_vols')]), (t1w_mask_bold_tfm, bold_confounds_wf, [('output_image', 'inputnode.bold_mask')]), (bold_confounds_wf, outputnode, [('outputnode.confounds_file', 'confounds')]), (bold_confounds_wf, outputnode, [('outputnode.confounds_metadata', 'confounds_metadata')]), # Connect bold_bold_trans_wf (inputnode, bold_bold_trans_wf, [('ref_file', 'inputnode.bold_ref')]), (t1w_mask_bold_tfm, bold_bold_trans_wf, [('output_image', 'inputnode.bold_mask')]), (bold_split, bold_bold_trans_wf, [('out_files', 'inputnode.bold_file') ]), (lta_convert, bold_bold_trans_wf, [('out_itk', 'inputnode.hmc_xforms') ]), # Summary (outputnode, summary, [('confounds', 'confounds_file')]), ]) # fmt:on # for standard EPI data, pass along correct file if not multiecho: # fmt:off workflow.connect([ (inputnode, func_derivatives_wf, [('bold_file', 'inputnode.source_file')]), (bold_bold_trans_wf, bold_confounds_wf, [('outputnode.bold', 'inputnode.bold')]), (bold_split, bold_t1_trans_wf, [('out_files', 'inputnode.bold_split')]), ]) # fmt:on else: # for meepi, create and use optimal combination # fmt:off workflow.connect([ # update name source for optimal combination (inputnode, func_derivatives_wf, [(('bold_file', combine_meepi_source), 'inputnode.source_file')]), (bold_bold_trans_wf, skullstrip_bold_wf, [('outputnode.bold', 'inputnode.in_file')]), (bold_t2s_wf, bold_confounds_wf, [('outputnode.bold', 'inputnode.bold')]), (bold_t2s_wf, bold_t1_trans_wf, [('outputnode.bold', 'inputnode.bold_split')]), ]) # fmt:on # Map final BOLD mask into T1w space (if required) nonstd_spaces = set(spaces.get_nonstandard()) if nonstd_spaces.intersection(("T1w", "anat")): from niworkflows.interfaces.fixes import ( FixHeaderApplyTransforms as ApplyTransforms, ) boldmask_to_t1w = pe.Node( ApplyTransforms(interpolation="MultiLabel"), name="boldmask_to_t1w", mem_gb=0.1, ) # fmt:off workflow.connect([ (bold_reg_wf, boldmask_to_t1w, [('outputnode.bold2anat', 'transforms')]), (bold_t1_trans_wf, boldmask_to_t1w, [('outputnode.bold_mask_t1', 'reference_image')]), (t1w_mask_bold_tfm, boldmask_to_t1w, [('output_image', 'input_image')]), (boldmask_to_t1w, outputnode, [('output_image', 'bold_mask_t1')]), ]) # fmt:on if nonstd_spaces.intersection(("func", "run", "bold", "boldref", "sbref")): # fmt:off workflow.connect([ (bold_bold_trans_wf, outputnode, [('outputnode.bold', 'bold_native')]), (bold_bold_trans_wf, func_derivatives_wf, [('outputnode.bold_ref', 'inputnode.bold_native_ref'), ('outputnode.bold_mask', 'inputnode.bold_mask_native')]), ]) # fmt:on if spaces.get_spaces(nonstandard=False, dim=(3, )): # Apply transforms in 1 shot # Only use uncompressed output if AROMA is to be run bold_std_trans_wf = init_bold_std_trans_wf( mem_gb=mem_gb["resampled"], omp_nthreads=omp_nthreads, spaces=spaces, name="bold_std_trans_wf", use_compression=not config.execution.low_mem, use_fieldwarp=False, ) # fmt:off workflow.connect([ (inputnode, bold_std_trans_wf, [('template', 'inputnode.templates'), ('anat2std_xfm', 'inputnode.anat2std_xfm'), ('bold_file', 'inputnode.name_source')]), (t1w_mask_bold_tfm, bold_std_trans_wf, [('output_image', 'inputnode.bold_mask')]), (lta_convert, bold_std_trans_wf, [('out_itk', 'inputnode.hmc_xforms')]), (bold_reg_wf, bold_std_trans_wf, [('outputnode.bold2anat', 'inputnode.bold2anat')]), (bold_std_trans_wf, outputnode, [('outputnode.bold_std', 'bold_std'), ('outputnode.bold_std_ref', 'bold_std_ref'), ('outputnode.bold_mask_std', 'bold_mask_std')]), ]) # fmt:on if not multiecho: # fmt:off workflow.connect([ (bold_split, bold_std_trans_wf, [("out_files", "inputnode.bold_split")]), ]) # fmt:on else: split_opt_comb = bold_split.clone(name="split_opt_comb") # fmt:off workflow.connect([(bold_t2s_wf, split_opt_comb, [('outputnode.bold', 'in_file')]), (split_opt_comb, bold_std_trans_wf, [('out_files', 'inputnode.bold_split')])]) # fmt:on # func_derivatives_wf internally parametrizes over snapshotted spaces. # fmt:off workflow.connect([ (bold_std_trans_wf, func_derivatives_wf, [ ('outputnode.template', 'inputnode.template'), ('outputnode.spatial_reference', 'inputnode.spatial_reference'), ('outputnode.bold_std_ref', 'inputnode.bold_std_ref'), ('outputnode.bold_std', 'inputnode.bold_std'), ('outputnode.bold_mask_std', 'inputnode.bold_mask_std'), ]), ]) # fmt:on if config.workflow.use_aroma: # ICA-AROMA workflow from .confounds import init_ica_aroma_wf ica_aroma_wf = init_ica_aroma_wf( mem_gb=mem_gb["resampled"], metadata=metadata, omp_nthreads=omp_nthreads, use_fieldwarp=False, err_on_aroma_warn=config.workflow.aroma_err_on_warn, aroma_melodic_dim=config.workflow.aroma_melodic_dim, name="ica_aroma_wf", ) join = pe.Node( niu.Function(output_names=["out_file"], function=_to_join), name="aroma_confounds", ) mrg_conf_metadata = pe.Node( niu.Merge(2), name="merge_confound_metadata", run_without_submitting=True, ) mrg_conf_metadata2 = pe.Node( DictMerge(), name="merge_confound_metadata2", run_without_submitting=True, ) # fmt:off workflow.disconnect([ (bold_confounds_wf, outputnode, [ ('outputnode.confounds_file', 'confounds'), ]), (bold_confounds_wf, outputnode, [ ('outputnode.confounds_metadata', 'confounds_metadata'), ]), ]) workflow.connect([ (inputnode, ica_aroma_wf, [('bold_file', 'inputnode.name_source'), ('n_dummy_scans', 'inputnode.skip_vols')]), (lta_convert, ica_aroma_wf, [('out_fsl', 'inputnode.movpar_file')]), (bold_confounds_wf, join, [('outputnode.confounds_file', 'in_file')]), (bold_confounds_wf, mrg_conf_metadata, [('outputnode.confounds_metadata', 'in1')]), (ica_aroma_wf, join, [('outputnode.aroma_confounds', 'join_file')]), (ica_aroma_wf, mrg_conf_metadata, [('outputnode.aroma_metadata', 'in2')]), (mrg_conf_metadata, mrg_conf_metadata2, [('out', 'in_dicts')]), (ica_aroma_wf, outputnode, [('outputnode.aroma_noise_ics', 'aroma_noise_ics'), ('outputnode.melodic_mix', 'melodic_mix'), ('outputnode.nonaggr_denoised_file', 'nonaggr_denoised_file') ]), (join, outputnode, [('out_file', 'confounds')]), (mrg_conf_metadata2, outputnode, [('out_dict', 'confounds_metadata')]), (bold_std_trans_wf, ica_aroma_wf, [('outputnode.bold_std', 'inputnode.bold_std'), ('outputnode.bold_mask_std', 'inputnode.bold_mask_std'), ('outputnode.spatial_reference', 'inputnode.spatial_reference')]), ]) # fmt:on if spaces.get_spaces(nonstandard=False, dim=(3, )): carpetplot_wf = init_carpetplot_wf( mem_gb=mem_gb["resampled"], metadata=metadata, name="carpetplot_wf", ) # Xform to 'Fischer344' is always computed. carpetplot_select_std = pe.Node( KeySelect(fields=["std2anat_xfm"], key="Fischer344"), name="carpetplot_select_std", run_without_submitting=True, ) # fmt:off workflow.connect([ (inputnode, carpetplot_select_std, [('std2anat_xfm', 'std2anat_xfm'), ('template', 'keys')]), (carpetplot_select_std, carpetplot_wf, [('std2anat_xfm', 'inputnode.std2anat_xfm')]), (bold_bold_trans_wf if not multiecho else bold_t2s_wf, carpetplot_wf, [('outputnode.bold', 'inputnode.bold')]), (t1w_mask_bold_tfm, carpetplot_wf, [('output_image', 'inputnode.bold_mask')]), (bold_reg_wf, carpetplot_wf, [('outputnode.anat2bold', 'inputnode.anat2bold')]), (bold_confounds_wf, carpetplot_wf, [('outputnode.confounds_file', 'inputnode.confounds_file')]), ]) # fmt:on # REPORTING ############################################################ ds_report_summary = pe.Node( DerivativesDataSink(desc="summary", datatype="figures", dismiss_entities=("echo", )), name="ds_report_summary", run_without_submitting=True, mem_gb=config.DEFAULT_MEMORY_MIN_GB, ) ds_report_validation = pe.Node( DerivativesDataSink( base_directory=output_dir, desc="validation", datatype="figures", dismiss_entities=("echo", ), ), name="ds_report_validation", run_without_submitting=True, mem_gb=config.DEFAULT_MEMORY_MIN_GB, ) # fmt:off workflow.connect([ (summary, ds_report_summary, [('out_report', 'in_file')]), (inputnode, ds_report_validation, [('validation_report', 'in_file')]), ]) # fmt:on # Fill-in datasinks of reportlets seen so far for node in workflow.list_node_names(): if node.split(".")[-1].startswith("ds_report"): workflow.get_node(node).inputs.base_directory = output_dir workflow.get_node(node).inputs.source_file = ref_file return workflow
def init_infant_brain_extraction_wf( age_months=None, ants_affine_init=False, bspline_fitting_distance=200, sloppy=False, skull_strip_template="UNCInfant", template_specs=None, interim_checkpoints=True, mem_gb=3.0, mri_scheme="T1w", name="infant_brain_extraction_wf", atropos_model=None, omp_nthreads=None, output_dir=None, use_float=True, use_t2w=False, ): """ Build an atlas-based brain extraction pipeline for infant T1w/T2w MRI data. Pros/Cons of available templates -------------------------------- * MNIInfant + More cohorts available for finer-grain control + T1w/T2w images available - Template masks are poor * UNCInfant + Accurate masks - No T2w image available Parameters ---------- ants_affine_init : :obj:`bool`, optional Set-up a pre-initialization step with ``antsAI`` to account for mis-oriented images. """ # handle template specifics template_specs = template_specs or {} if skull_strip_template == 'MNIInfant': template_specs['resolution'] = 2 if sloppy else 1 if not template_specs.get('cohort'): if age_months is None: raise KeyError( f"Age or cohort for {skull_strip_template} must be provided!") template_specs['cohort'] = cohort_by_months(skull_strip_template, age_months) inputnode = pe.Node( niu.IdentityInterface(fields=["t1w", "t2w", "in_mask"]), name="inputnode") outputnode = pe.Node(niu.IdentityInterface( fields=["t1w_corrected", "t1w_corrected_brain", "t1w_mask"]), name="outputnode") if not use_t2w: raise RuntimeError("A T2w image is currently required.") tpl_target_path = get_template( skull_strip_template, suffix='T1w', # no T2w template desc=None, **template_specs, ) if not tpl_target_path: raise RuntimeError( f"An instance of template <tpl-{skull_strip_template}> with MR scheme " f"'{'T1w' or mri_scheme}' could not be found.") tpl_brainmask_path = get_template(skull_strip_template, label="brain", suffix="probseg", **template_specs) or get_template( skull_strip_template, desc="brain", suffix="mask", **template_specs) tpl_regmask_path = get_template(skull_strip_template, label="BrainCerebellumExtraction", suffix="mask", **template_specs) # validate images val_tmpl = pe.Node(ValidateImage(), name='val_tmpl') val_t1w = val_tmpl.clone("val_t1w") val_t2w = val_tmpl.clone("val_t2w") val_tmpl.inputs.in_file = _pop(tpl_target_path) gauss_tmpl = pe.Node(niu.Function(function=_gauss_filter), name="gauss_tmpl") # Spatial normalization step lap_tmpl = pe.Node(ImageMath(operation="Laplacian", op2="0.4 1"), name="lap_tmpl") lap_t1w = lap_tmpl.clone("lap_t1w") lap_t2w = lap_tmpl.clone("lap_t2w") # Merge image nodes mrg_tmpl = pe.Node(niu.Merge(2), name="mrg_tmpl") mrg_t2w = mrg_tmpl.clone("mrg_t2w") mrg_t1w = mrg_tmpl.clone("mrg_t1w") norm_lap_tmpl = pe.Node(niu.Function(function=_trunc), name="norm_lap_tmpl") norm_lap_tmpl.inputs.dtype = "float32" norm_lap_tmpl.inputs.out_max = 1.0 norm_lap_tmpl.inputs.percentile = (0.01, 99.99) norm_lap_tmpl.inputs.clip_max = None norm_lap_t1w = norm_lap_tmpl.clone('norm_lap_t1w') norm_lap_t2w = norm_lap_t1w.clone('norm_lap_t2w') # Set up initial spatial normalization ants_params = "testing" if sloppy else "precise" norm = pe.Node( Registration(from_file=pkgr_fn( "niworkflows.data", f"antsBrainExtraction_{ants_params}.json")), name="norm", n_procs=omp_nthreads, mem_gb=mem_gb, ) norm.inputs.float = use_float if tpl_regmask_path: norm.inputs.fixed_image_masks = tpl_regmask_path # Set up T2w -> T1w within-subject registration norm_subj = pe.Node( Registration( from_file=pkgr_fn("nibabies.data", "within_subject_t1t2.json")), name="norm_subj", n_procs=omp_nthreads, mem_gb=mem_gb, ) norm_subj.inputs.float = use_float # main workflow wf = pe.Workflow(name) # Create a buffer interface as a cache for the actual inputs to registration buffernode = pe.Node( niu.IdentityInterface(fields=["hires_target", "smooth_target"]), name="buffernode") # truncate target intensity for N4 correction clip_tmpl = pe.Node(niu.Function(function=_trunc), name="clip_tmpl") clip_t2w = clip_tmpl.clone('clip_t2w') clip_t1w = clip_tmpl.clone('clip_t1w') # INU correction of the t1w init_t2w_n4 = pe.Node( N4BiasFieldCorrection( dimension=3, save_bias=False, copy_header=True, n_iterations=[50] * (4 - sloppy), convergence_threshold=1e-7, shrink_factor=4, bspline_fitting_distance=bspline_fitting_distance, ), n_procs=omp_nthreads, name="init_t2w_n4", ) init_t1w_n4 = init_t2w_n4.clone("init_t1w_n4") clip_t2w_inu = pe.Node(niu.Function(function=_trunc), name="clip_t2w_inu") clip_t1w_inu = clip_t2w_inu.clone("clip_t1w_inu") map_mask_t2w = pe.Node(ApplyTransforms(interpolation="Gaussian", float=True), name="map_mask_t2w", mem_gb=1) map_mask_t1w = map_mask_t2w.clone("map_mask_t1w") # map template brainmask to t2w space map_mask_t2w.inputs.input_image = str(tpl_brainmask_path) thr_t2w_mask = pe.Node(Binarize(thresh_low=0.80), name="thr_t2w_mask") thr_t1w_mask = thr_t2w_mask.clone('thr_t1w_mask') bspline_grid = pe.Node(niu.Function(function=_bspline_distance), name="bspline_grid") # Refine INU correction final_n4 = pe.Node( N4BiasFieldCorrection( dimension=3, bspline_fitting_distance=bspline_fitting_distance, save_bias=True, copy_header=True, n_iterations=[50] * 5, convergence_threshold=1e-7, rescale_intensities=True, shrink_factor=4, ), n_procs=omp_nthreads, name="final_n4", ) final_mask = pe.Node(ApplyMask(), name="final_mask") if atropos_model is None: atropos_model = tuple(ATROPOS_MODELS[mri_scheme].values()) atropos_wf = init_atropos_wf( use_random_seed=False, omp_nthreads=omp_nthreads, mem_gb=mem_gb, in_segmentation_model=atropos_model, ) # if tpl_regmask_path: # atropos_wf.get_node('inputnode').inputs.in_mask_dilated = tpl_regmask_path sel_wm = pe.Node(niu.Select(index=atropos_model[-1] - 1), name='sel_wm', run_without_submitting=True) wf.connect([ # 1. massage template (val_tmpl, clip_tmpl, [("out_file", "in_file")]), (clip_tmpl, lap_tmpl, [("out", "op1")]), (clip_tmpl, mrg_tmpl, [("out", "in1")]), (lap_tmpl, norm_lap_tmpl, [("output_image", "in_file")]), (norm_lap_tmpl, mrg_tmpl, [("out", "in2")]), # 2. massage T2w (inputnode, val_t2w, [('t2w', 'in_file')]), (val_t2w, clip_t2w, [('out_file', 'in_file')]), (clip_t2w, init_t2w_n4, [('out', 'input_image')]), (init_t2w_n4, clip_t2w_inu, [("output_image", "in_file")]), (clip_t2w_inu, lap_t2w, [('out', 'op1')]), (clip_t2w_inu, mrg_t2w, [('out', 'in1')]), (lap_t2w, norm_lap_t2w, [("output_image", "in_file")]), (norm_lap_t2w, mrg_t2w, [("out", "in2")]), # 3. normalize T2w to target template (UNC) (mrg_t2w, norm, [("out", "moving_image")]), (mrg_tmpl, norm, [("out", "fixed_image")]), # 4. map template brainmask to T2w space (inputnode, map_mask_t2w, [('t2w', 'reference_image')]), (norm, map_mask_t2w, [("reverse_transforms", "transforms"), ("reverse_invert_flags", "invert_transform_flags")]), (map_mask_t2w, thr_t2w_mask, [("output_image", "in_file")]), # 5. massage T1w (inputnode, val_t1w, [("t1w", "in_file")]), (val_t1w, clip_t1w, [("out_file", "in_file")]), (clip_t1w, init_t1w_n4, [("out", "input_image")]), (init_t1w_n4, clip_t1w_inu, [("output_image", "in_file")]), (clip_t1w_inu, lap_t1w, [('out', 'op1')]), (clip_t1w_inu, mrg_t1w, [('out', 'in1')]), (lap_t1w, norm_lap_t1w, [("output_image", "in_file")]), (norm_lap_t1w, mrg_t1w, [("out", "in2")]), # 6. normalize within subject T1w to T2w (mrg_t1w, norm_subj, [("out", "moving_image")]), (mrg_t2w, norm_subj, [("out", "fixed_image")]), (thr_t2w_mask, norm_subj, [("out_mask", "fixed_image_mask")]), # 7. map mask to T1w space (thr_t2w_mask, map_mask_t1w, [("out_mask", "input_image")]), (inputnode, map_mask_t1w, [("t1w", "reference_image")]), (norm_subj, map_mask_t1w, [ ("reverse_transforms", "transforms"), ("reverse_invert_flags", "invert_transform_flags"), ]), (map_mask_t1w, thr_t1w_mask, [("output_image", "in_file")]), # 8. T1w INU (inputnode, final_n4, [("t1w", "input_image")]), (inputnode, bspline_grid, [("t1w", "in_file")]), (bspline_grid, final_n4, [("out", "args")]), (map_mask_t1w, final_n4, [("output_image", "weight_image")]), (final_n4, final_mask, [("output_image", "in_file")]), (thr_t1w_mask, final_mask, [("out_mask", "in_mask")]), # 9. Outputs (final_n4, outputnode, [("output_image", "t1w_corrected")]), (thr_t1w_mask, outputnode, [("out_mask", "t1w_mask")]), (final_mask, outputnode, [("out_file", "t1w_corrected_brain")]), ]) if ants_affine_init: ants_kwargs = dict( metric=("Mattes", 32, "Regular", 0.2), transform=("Affine", 0.1), search_factor=(20, 0.12), principal_axes=False, convergence=(10, 1e-6, 10), search_grid=(40, (0, 40, 40)), verbose=True, ) if ants_affine_init == 'random': ants_kwargs['metric'] = ("Mattes", 32, "Random", 0.2) if ants_affine_init == 'search': ants_kwargs['search_grid'] = (20, (20, 40, 40)) init_aff = pe.Node( AI(**ants_kwargs), name="init_aff", n_procs=omp_nthreads, ) if tpl_regmask_path: init_aff.inputs.fixed_image_mask = _pop(tpl_regmask_path) wf.connect([ (clip_tmpl, init_aff, [("out", "fixed_image")]), (clip_t2w_inu, init_aff, [("out", "moving_image")]), (init_aff, norm, [("output_transform", "initial_moving_transform") ]), ]) return wf
def init_infant_brain_extraction_wf( ants_affine_init=False, bspline_fitting_distance=200, debug=False, in_template="MNIInfant", template_specs=None, interim_checkpoints=True, mem_gb=3.0, mri_scheme="T2w", name="infant_brain_extraction_wf", atropos_model=None, omp_nthreads=None, output_dir=None, use_float=True, ): """ Build an atlas-based brain extraction pipeline for infant T2w MRI data. Parameters ---------- ants_affine_init : :obj:`bool`, optional Set-up a pre-initialization step with ``antsAI`` to account for mis-oriented images. """ inputnode = pe.Node(niu.IdentityInterface(fields=["in_files", "in_mask"]), name="inputnode") outputnode = pe.Node(niu.IdentityInterface( fields=["out_corrected", "out_brain", "out_mask"]), name="outputnode") template_specs = template_specs or {} # Find a suitable target template in TemplateFlow tpl_target_path = get_template(in_template, suffix=mri_scheme, **template_specs) if not tpl_target_path: raise RuntimeError( f"An instance of template <tpl-{in_template}> with MR scheme '{mri_scheme}'" " could not be found.") # tpl_brainmask_path = get_template( # in_template, desc="brain", suffix="probseg", **template_specs # ) # if not tpl_brainmask_path: # ignore probseg for the time being tpl_brainmask_path = get_template(in_template, desc="brain", suffix="mask", **template_specs) tpl_regmask_path = get_template(in_template, desc="BrainCerebellumExtraction", suffix="mask", **template_specs) # validate images val_tmpl = pe.Node(ValidateImage(), name='val_tmpl') val_tmpl.inputs.in_file = _pop(tpl_target_path) val_target = pe.Node(ValidateImage(), name='val_target') # Resample both target and template to a controlled, isotropic resolution res_tmpl = pe.Node(RegridToZooms(zooms=HIRES_ZOOMS), name="res_tmpl") # testing res_target = pe.Node(RegridToZooms(zooms=HIRES_ZOOMS), name="res_target") # testing gauss_tmpl = pe.Node(niu.Function(function=_gauss_filter), name="gauss_tmpl") # Spatial normalization step lap_tmpl = pe.Node(ImageMath(operation="Laplacian", op2="0.4 1"), name="lap_tmpl") lap_target = pe.Node(ImageMath(operation="Laplacian", op2="0.4 1"), name="lap_target") # Merge image nodes mrg_target = pe.Node(niu.Merge(2), name="mrg_target") mrg_tmpl = pe.Node(niu.Merge(2), name="mrg_tmpl") norm_lap_tmpl = pe.Node(niu.Function(function=_trunc), name="norm_lap_tmpl") norm_lap_tmpl.inputs.dtype = "float32" norm_lap_tmpl.inputs.out_max = 1.0 norm_lap_tmpl.inputs.percentile = (0.01, 99.99) norm_lap_tmpl.inputs.clip_max = None norm_lap_target = pe.Node(niu.Function(function=_trunc), name="norm_lap_target") norm_lap_target.inputs.dtype = "float32" norm_lap_target.inputs.out_max = 1.0 norm_lap_target.inputs.percentile = (0.01, 99.99) norm_lap_target.inputs.clip_max = None # Set up initial spatial normalization ants_params = "testing" if debug else "precise" norm = pe.Node( Registration(from_file=pkgr_fn( "niworkflows.data", f"antsBrainExtraction_{ants_params}.json")), name="norm", n_procs=omp_nthreads, mem_gb=mem_gb, ) norm.inputs.float = use_float # main workflow wf = pe.Workflow(name) # Create a buffer interface as a cache for the actual inputs to registration buffernode = pe.Node( niu.IdentityInterface(fields=["hires_target", "smooth_target"]), name="buffernode") # truncate target intensity for N4 correction clip_target = pe.Node( niu.Function(function=_trunc), name="clip_target", ) clip_tmpl = pe.Node( niu.Function(function=_trunc), name="clip_tmpl", ) #clip_tmpl.inputs.in_file = _pop(tpl_target_path) # INU correction of the target image init_n4 = pe.Node( N4BiasFieldCorrection( dimension=3, save_bias=False, copy_header=True, n_iterations=[50] * (4 - debug), convergence_threshold=1e-7, shrink_factor=4, bspline_fitting_distance=bspline_fitting_distance, ), n_procs=omp_nthreads, name="init_n4", ) clip_inu = pe.Node( niu.Function(function=_trunc), name="clip_inu", ) gauss_target = pe.Node(niu.Function(function=_gauss_filter), name="gauss_target") wf.connect([ # truncation, resampling, and initial N4 (inputnode, val_target, [(("in_files", _pop), "in_file")]), # (inputnode, res_target, [(("in_files", _pop), "in_file")]), (val_target, res_target, [("out_file", "in_file")]), (res_target, clip_target, [("out_file", "in_file")]), (val_tmpl, clip_tmpl, [("out_file", "in_file")]), (clip_tmpl, res_tmpl, [("out", "in_file")]), (clip_target, init_n4, [("out", "input_image")]), (init_n4, clip_inu, [("output_image", "in_file")]), (clip_inu, gauss_target, [("out", "in_file")]), (clip_inu, buffernode, [("out", "hires_target")]), (gauss_target, buffernode, [("out", "smooth_target")]), (res_tmpl, gauss_tmpl, [("out_file", "in_file")]), # (clip_tmpl, gauss_tmpl, [("out", "in_file")]), ]) # Graft a template registration-mask if present if tpl_regmask_path: hires_mask = pe.Node(ApplyTransforms( input_image=_pop(tpl_regmask_path), transforms="identity", interpolation="NearestNeighbor", float=True), name="hires_mask", mem_gb=1) wf.connect([ (res_tmpl, hires_mask, [("out_file", "reference_image")]), ]) map_brainmask = pe.Node(ApplyTransforms(interpolation="Gaussian", float=True), name="map_brainmask", mem_gb=1) map_brainmask.inputs.input_image = str(tpl_brainmask_path) thr_brainmask = pe.Node(Binarize(thresh_low=0.80), name="thr_brainmask") bspline_grid = pe.Node(niu.Function(function=_bspline_distance), name="bspline_grid") # Refine INU correction final_n4 = pe.Node( N4BiasFieldCorrection( dimension=3, save_bias=True, copy_header=True, n_iterations=[50] * 5, convergence_threshold=1e-7, rescale_intensities=True, shrink_factor=4, ), n_procs=omp_nthreads, name="final_n4", ) final_mask = pe.Node(ApplyMask(), name="final_mask") if atropos_model is None: atropos_model = tuple(ATROPOS_MODELS[mri_scheme].values()) atropos_wf = init_atropos_wf( use_random_seed=False, omp_nthreads=omp_nthreads, mem_gb=mem_gb, in_segmentation_model=atropos_model, ) # if tpl_regmask_path: # atropos_wf.get_node('inputnode').inputs.in_mask_dilated = tpl_regmask_path sel_wm = pe.Node(niu.Select(index=atropos_model[-1] - 1), name='sel_wm', run_without_submitting=True) wf.connect([ (inputnode, map_brainmask, [(("in_files", _pop), "reference_image")]), (inputnode, final_n4, [(("in_files", _pop), "input_image")]), (inputnode, bspline_grid, [(("in_files", _pop), "in_file")]), # (bspline_grid, final_n4, [("out", "bspline_fitting_distance")]), (bspline_grid, final_n4, [("out", "args")]), # merge laplacian and original images (buffernode, lap_target, [("smooth_target", "op1")]), (buffernode, mrg_target, [("hires_target", "in1")]), (lap_target, norm_lap_target, [("output_image", "in_file")]), (norm_lap_target, mrg_target, [("out", "in2")]), # Template massaging (res_tmpl, lap_tmpl, [("out_file", "op1")]), (res_tmpl, mrg_tmpl, [("out_file", "in1")]), (lap_tmpl, norm_lap_tmpl, [("output_image", "in_file")]), (norm_lap_tmpl, mrg_tmpl, [("out", "in2")]), # spatial normalization (mrg_target, norm, [("out", "moving_image")]), (mrg_tmpl, norm, [("out", "fixed_image")]), (norm, map_brainmask, [("reverse_transforms", "transforms"), ("reverse_invert_flags", "invert_transform_flags")]), (map_brainmask, thr_brainmask, [("output_image", "in_file")]), # take a second pass of N4 (map_brainmask, final_n4, [("output_image", "weight_image")]), (final_n4, final_mask, [("output_image", "in_file")]), (thr_brainmask, final_mask, [("out_mask", "in_mask")]), (final_n4, outputnode, [("output_image", "out_corrected")]), (thr_brainmask, outputnode, [("out_mask", "out_mask")]), (final_mask, outputnode, [("out_file", "out_brain")]), ]) # wf.disconnect([ # (get_brainmask, apply_mask, [('output_image', 'mask_file')]), # (copy_xform, outputnode, [('out_mask', 'out_mask')]), # ]) # wf.connect([ # (init_n4, atropos_wf, [ # ('output_image', 'inputnode.in_files')]), # intensity image # (thr_brainmask, atropos_wf, [ # ('out_mask', 'inputnode.in_mask')]), # (atropos_wf, sel_wm, [('outputnode.out_tpms', 'inlist')]), # (sel_wm, final_n4, [('out', 'weight_image')]), # ]) # wf.connect([ # (atropos_wf, outputnode, [ # ('outputnode.out_mask', 'out_mask'), # ('outputnode.out_segm', 'out_segm'), # ('outputnode.out_tpms', 'out_tpms')]), # ]) if tpl_regmask_path: wf.connect([ (hires_mask, norm, [("output_image", "fixed_image_masks")]), # (hires_mask, atropos_wf, [ # ("output_image", "inputnode.in_mask_dilated")]), ]) if interim_checkpoints: final_apply = pe.Node(ApplyTransforms(interpolation="BSpline", float=True), name="final_apply", mem_gb=1) final_report = pe.Node(SimpleBeforeAfter( before_label=f"tpl-{in_template}", after_label="target", out_report="final_report.svg"), name="final_report") wf.connect([ (inputnode, final_apply, [(("in_files", _pop), "reference_image") ]), (res_tmpl, final_apply, [("out_file", "input_image")]), (norm, final_apply, [("reverse_transforms", "transforms"), ("reverse_invert_flags", "invert_transform_flags")]), (final_apply, final_report, [("output_image", "before")]), (outputnode, final_report, [("out_corrected", "after"), ("out_mask", "wm_seg")]), ]) if output_dir: from nipype.interfaces.io import DataSink ds_final_inu = pe.Node(DataSink(base_directory=str(output_dir.parent)), name="ds_final_inu") ds_final_msk = pe.Node(DataSink(base_directory=str(output_dir.parent)), name="ds_final_msk") ds_report = pe.Node(DataSink(base_directory=str(output_dir.parent)), name="ds_report") wf.connect([ (outputnode, ds_final_inu, [("out_corrected", f"{output_dir.name}.@inu_corrected")]), (outputnode, ds_final_msk, [("out_mask", f"{output_dir.name}.@brainmask")]), (final_report, ds_report, [("out_report", f"{output_dir.name}.@report")]), ]) if not ants_affine_init: return wf # Initialize transforms with antsAI lowres_tmpl = pe.Node(RegridToZooms(zooms=LOWRES_ZOOMS), name="lowres_tmpl") lowres_target = pe.Node(RegridToZooms(zooms=LOWRES_ZOOMS), name="lowres_target") init_aff = pe.Node( AI( metric=("Mattes", 32, "Regular", 0.25), transform=("Affine", 0.1), search_factor=(15, 0.1), principal_axes=False, convergence=(10, 1e-6, 10), search_grid=(40, (0, 40, 40)), verbose=True, ), name="init_aff", n_procs=omp_nthreads, ) wf.connect([ (gauss_tmpl, lowres_tmpl, [("out", "in_file")]), (lowres_tmpl, init_aff, [("out_file", "fixed_image")]), (gauss_target, lowres_target, [("out", "in_file")]), (lowres_target, init_aff, [("out_file", "moving_image")]), (init_aff, norm, [("output_transform", "initial_moving_transform")]), ]) if tpl_regmask_path: lowres_mask = pe.Node(ApplyTransforms( input_image=_pop(tpl_regmask_path), transforms="identity", interpolation="MultiLabel", float=True), name="lowres_mask", mem_gb=1) wf.connect([ (lowres_tmpl, lowres_mask, [("out_file", "reference_image")]), (lowres_mask, init_aff, [("output_image", "fixed_image_mask")]), ]) if interim_checkpoints: init_apply = pe.Node(ApplyTransforms(interpolation="BSpline", float=True), name="init_apply", mem_gb=1) init_report = pe.Node(SimpleBeforeAfter( before_label=f"tpl-{in_template}", after_label="target", out_report="init_report.svg"), name="init_report") wf.connect([ (lowres_target, init_apply, [("out_file", "input_image")]), (res_tmpl, init_apply, [("out_file", "reference_image")]), (init_aff, init_apply, [("output_transform", "transforms")]), (init_apply, init_report, [("output_image", "after")]), (res_tmpl, init_report, [("out_file", "before")]), ]) if output_dir: ds_init_report = pe.Node( DataSink(base_directory=str(output_dir.parent)), name="ds_init_report") wf.connect(init_report, "out_report", ds_init_report, f"{output_dir.name}.@init_report") return wf