def init_brain_extraction_wf(tpl_target_path, tpl_mask_path, tpl_regmask_path, name='brain_extraction_wf', template_spec=None, use_float=True, normalization_quality='precise', omp_nthreads=None, mem_gb=3.0, bids_suffix='T1w', atropos_refine=True, atropos_use_random_seed=True, atropos_model=None, use_laplacian=True, bspline_fitting_distance=200): """ A Nipype implementation of the official ANTs' ``antsBrainExtraction.sh`` workflow (only for 3D images). The official workflow is built as follows (and this implementation follows the same organization): 1. Step 1 performs several clerical tasks (adding padding, calculating the Laplacian of inputs, affine initialization) and the core spatial normalization. 2. Maps the brain mask into target space using the normalization calculated in 1. 3. Superstep 1b: smart binarization of the brain mask 4. Superstep 6: apply ATROPOS and massage its outputs 5. Superstep 7: use results from 4 to refine the brain mask .. workflow:: :graph2use: orig :simple_form: yes from niworkflows.anat import init_brain_extraction_wf wf = init_brain_extraction_wf() **Parameters** in_template : str Name of the skull-stripping template ('OASIS30ANTs', 'NKI', or path). The brain template from which regions will be projected Anatomical template created using e.g. LPBA40 data set with ``buildtemplateparallel.sh`` in ANTs. The workflow will automatically search for a brain probability mask created using e.g. LPBA40 data set which have brain masks defined, and warped to anatomical template and averaged resulting in a probability image. use_float : bool Whether single precision should be used normalization_quality : str Use more precise or faster registration parameters (default: ``precise``, other possible values: ``testing``) omp_nthreads : int Maximum number of threads an individual process may use mem_gb : float Estimated peak memory consumption of the most hungry nodes in the workflow bids_suffix : str Sequence type of the first input image. For a list of acceptable values see https://bids-specification.readthedocs.io/en/latest/\ 04-modality-specific-files/01-magnetic-resonance-imaging-data.html#anatomy-imaging-data atropos_refine : bool Enables or disables the whole ATROPOS sub-workflow atropos_use_random_seed : bool Whether ATROPOS should generate a random seed based on the system's clock atropos_model : tuple or None Allows to specify a particular segmentation model, overwriting the defaults based on ``bids_suffix`` use_laplacian : bool Enables or disables alignment of the Laplacian as an additional criterion for image registration quality (default: True) bspline_fitting_distance : float The size of the b-spline mesh grid elements, in mm (default: 200) name : str, optional Workflow name (default: antsBrainExtraction) **Inputs** in_files List of input anatomical images to be brain-extracted, typically T1-weighted. If a list of anatomical images is provided, subsequently specified images are used during the segmentation process. However, only the first image is used in the registration of priors. Our suggestion would be to specify the T1w as the first image. in_mask (optional) Mask used for registration to limit the metric computation to a specific region. **Outputs** out_file Skull-stripped and :abbr:`INU (intensity non-uniformity)`-corrected ``in_files`` out_mask Calculated brain mask bias_corrected The ``in_files`` input images, after :abbr:`INU (intensity non-uniformity)` correction, before skull-stripping. bias_image The :abbr:`INU (intensity non-uniformity)` field estimated for each input in ``in_files`` out_segm Output segmentation by ATROPOS out_tpms Output :abbr:`TPMs (tissue probability maps)` by ATROPOS """ # from templateflow.api import get as get_template wf = pe.Workflow(name) template_spec = template_spec or {} # suffix passed via spec takes precedence template_spec['suffix'] = template_spec.get('suffix', bids_suffix) # # Get probabilistic brain mask if available inputnode = pe.Node(niu.IdentityInterface(fields=['in_files', 'in_mask']), name='inputnode') # # Try to find a registration mask, set if available if tpl_regmask_path: inputnode.inputs.in_mask = str(tpl_regmask_path) outputnode = pe.Node(niu.IdentityInterface(fields=[ 'out_file', 'out_mask', 'bias_corrected', 'bias_image', 'out_segm', 'out_tpms' ]), name='outputnode') copy_xform = pe.Node(CopyXForm( fields=['out_file', 'out_mask', 'bias_corrected', 'bias_image']), name='copy_xform', run_without_submitting=True, mem_gb=2.5) trunc = pe.MapNode(ImageMath(operation='TruncateImageIntensity', op2='0.01 0.999 256'), name='truncate_images', iterfield=['op1']) inu_n4 = pe.MapNode(N4BiasFieldCorrection( dimension=3, save_bias=False, copy_header=True, n_iterations=[50] * 4, convergence_threshold=1e-7, shrink_factor=4, bspline_fitting_distance=bspline_fitting_distance), n_procs=omp_nthreads, name='inu_n4', iterfield=['input_image']) res_tmpl = pe.Node(ResampleImageBySpacing(out_spacing=(4, 4, 4), apply_smoothing=True), name='res_tmpl') res_tmpl.inputs.input_image = tpl_target_path res_target = pe.Node(ResampleImageBySpacing(out_spacing=(4, 4, 4), apply_smoothing=True), name='res_target') lap_tmpl = pe.Node(ImageMath(operation='Laplacian', op2='1.5 1'), name='lap_tmpl') lap_tmpl.inputs.op1 = tpl_target_path lap_target = pe.Node(ImageMath(operation='Laplacian', op2='1.5 1'), name='lap_target') mrg_tmpl = pe.Node(niu.Merge(2), name='mrg_tmpl') mrg_tmpl.inputs.in1 = tpl_target_path mrg_target = pe.Node(niu.Merge(2), name='mrg_target') # Initialize transforms with antsAI 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), verbose=True), name='init_aff', n_procs=omp_nthreads) # Tolerate missing ANTs at construction time _ants_version = Registration().version if _ants_version and parseversion(_ants_version) >= Version('2.3.0'): init_aff.inputs.search_grid = (40, (0, 40, 40)) # Set up spatial normalization settings_file = 'antsBrainExtraction_%s.json' if use_laplacian \ else 'antsBrainExtractionNoLaplacian_%s.json' norm = pe.Node(Registration( from_file=pkgr_fn('CPAC.anat_preproc', 'data/' + settings_file % normalization_quality)), name='norm', n_procs=omp_nthreads, mem_gb=mem_gb) norm.inputs.float = use_float fixed_mask_trait = 'fixed_image_mask' if _ants_version and parseversion(_ants_version) >= Version('2.2.0'): fixed_mask_trait += 's' map_brainmask = pe.Node(ApplyTransforms(interpolation='Gaussian', float=True), name='map_brainmask', mem_gb=1) map_brainmask.inputs.input_image = str(tpl_mask_path) thr_brainmask = pe.Node(ThresholdImage(dimension=3, th_low=0.5, th_high=1.0, inside_value=1, outside_value=0), name='thr_brainmask') # Morphological dilation, radius=2 dil_brainmask = pe.Node(ImageMath(operation='MD', op2='2'), name='dil_brainmask') # Get largest connected component get_brainmask = pe.Node(ImageMath(operation='GetLargestComponent'), name='get_brainmask') # Refine INU correction inu_n4_final = pe.MapNode(N4BiasFieldCorrection( dimension=3, save_bias=True, copy_header=True, n_iterations=[50] * 5, convergence_threshold=1e-7, shrink_factor=4, bspline_fitting_distance=bspline_fitting_distance), n_procs=omp_nthreads, name='inu_n4_final', iterfield=['input_image']) # Apply mask apply_mask = pe.MapNode(ApplyMask(), iterfield=['in_file'], name='apply_mask') wf.connect([ (inputnode, trunc, [('in_files', 'op1')]), (inputnode, copy_xform, [(('in_files', _pop), 'hdr_file')]), (inputnode, inu_n4_final, [('in_files', 'input_image')]), (inputnode, init_aff, [('in_mask', 'fixed_image_mask')]), (inputnode, norm, [('in_mask', fixed_mask_trait)]), (inputnode, map_brainmask, [(('in_files', _pop), 'reference_image')]), (trunc, inu_n4, [('output_image', 'input_image')]), (inu_n4, res_target, [(('output_image', _pop), 'input_image')]), (res_tmpl, init_aff, [('output_image', 'fixed_image')]), (res_target, init_aff, [('output_image', 'moving_image')]), (init_aff, norm, [('output_transform', 'initial_moving_transform')]), (norm, map_brainmask, [('reverse_transforms', 'transforms'), ('reverse_invert_flags', 'invert_transform_flags')]), (map_brainmask, thr_brainmask, [('output_image', 'input_image')]), (thr_brainmask, dil_brainmask, [('output_image', 'op1')]), (dil_brainmask, get_brainmask, [('output_image', 'op1')]), (inu_n4_final, apply_mask, [('output_image', 'in_file')]), (get_brainmask, apply_mask, [('output_image', 'mask_file')]), (get_brainmask, copy_xform, [('output_image', 'out_mask')]), (apply_mask, copy_xform, [('out_file', 'out_file')]), (inu_n4_final, copy_xform, [('output_image', 'bias_corrected'), ('bias_image', 'bias_image')]), (copy_xform, outputnode, [('out_file', 'out_file'), ('out_mask', 'out_mask'), ('bias_corrected', 'bias_corrected'), ('bias_image', 'bias_image')]), ]) if use_laplacian: lap_tmpl = pe.Node(ImageMath(operation='Laplacian', op2='1.5 1'), name='lap_tmpl') lap_tmpl.inputs.op1 = tpl_target_path lap_target = pe.Node(ImageMath(operation='Laplacian', op2='1.5 1'), name='lap_target') mrg_tmpl = pe.Node(niu.Merge(2), name='mrg_tmpl') mrg_tmpl.inputs.in1 = tpl_target_path mrg_target = pe.Node(niu.Merge(2), name='mrg_target') wf.connect([ (inu_n4, lap_target, [(('output_image', _pop), 'op1')]), (lap_tmpl, mrg_tmpl, [('output_image', 'in2')]), (inu_n4, mrg_target, [('output_image', 'in1')]), (lap_target, mrg_target, [('output_image', 'in2')]), (mrg_tmpl, norm, [('out', 'fixed_image')]), (mrg_target, norm, [('out', 'moving_image')]), ]) else: norm.inputs.fixed_image = tpl_target_path wf.connect([ (inu_n4, norm, [(('output_image', _pop), 'moving_image')]), ]) if atropos_refine: atropos_model = atropos_model or list( ATROPOS_MODELS[bids_suffix].values()) atropos_wf = init_atropos_wf( use_random_seed=atropos_use_random_seed, omp_nthreads=omp_nthreads, mem_gb=mem_gb, in_segmentation_model=atropos_model, ) sel_wm = pe.Node(niu.Select(index=atropos_model[-1] - 1), name='sel_wm', run_without_submitting=True) wf.disconnect([ (get_brainmask, apply_mask, [('output_image', 'mask_file')]), (copy_xform, outputnode, [('out_mask', 'out_mask')]), ]) wf.connect([ (inu_n4, atropos_wf, [('output_image', 'inputnode.in_files')]), (thr_brainmask, atropos_wf, [('output_image', 'inputnode.in_mask') ]), (get_brainmask, atropos_wf, [('output_image', 'inputnode.in_mask_dilated')]), (atropos_wf, sel_wm, [('outputnode.out_tpms', 'inlist')]), (sel_wm, inu_n4_final, [('out', 'weight_image')]), (atropos_wf, apply_mask, [('outputnode.out_mask', 'mask_file')]), (atropos_wf, outputnode, [('outputnode.out_mask', 'out_mask'), ('outputnode.out_segm', 'out_segm'), ('outputnode.out_tpms', 'out_tpms')]), ]) return wf
def init_enhance_and_skullstrip_bold_wf(name='enhance_and_skullstrip_bold_wf', pre_mask=False, omp_nthreads=1): """ This workflow takes in a :abbr:`BOLD (blood-oxygen level-dependant)` :abbr:`fMRI (functional MRI)` average/summary (e.g., a reference image averaging non-steady-state timepoints), 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. Calculate a tentative mask by registering (9-parameters) to *fMRIPrep*'s :abbr:`EPI (echo-planar imaging)` -*boldref* template, which is in MNI space. The tentative mask is obtained by resampling the MNI template's brainmask into *boldref*-space. 2. Binary dilation of the tentative mask with a sphere of 3mm diameter. 3. Run ANTs' ``N4BiasFieldCorrection`` on the input :abbr:`BOLD (blood-oxygen level-dependant)` average, using the mask generated in 1) instead of the internal Otsu thresholding. 4. Calculate a loose mask using FSL's ``bet``, with one mathematical morphology dilation of one iteration and a sphere of 6mm as structuring element. 5. 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. 6. Calculate a mask using AFNI's ``3dAutomask`` after the contrast enhancement of 4). 7. Calculate a final mask as the intersection of 4) and 6). 8. Apply final mask on the enhanced reference. Step 1 can be skipped if the ``pre_mask`` argument is set to ``True`` and a tentative mask is passed in to the workflow throught the ``pre_mask`` Nipype input. .. workflow :: :graph2use: orig :simple_form: yes from niworkflows.func.util import init_enhance_and_skullstrip_bold_wf wf = init_enhance_and_skullstrip_bold_wf(omp_nthreads=1) **Parameters** name : str Name of workflow (default: ``enhance_and_skullstrip_bold_wf``) pre_mask : bool Indicates whether the ``pre_mask`` input will be set (and thus, step 1 should be skipped). omp_nthreads : int number of threads available to parallel nodes **Inputs** in_file BOLD image (single volume) pre_mask : bool A tentative brain mask to initialize the workflow (requires ``pre_mask`` parameter set ``True``). **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 .. _N4BiasFieldCorrection: https://hdl.handle.net/10380/3053 """ 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') # Dilate pre_mask pre_dilate = pe.Node(fsl.DilateImage(operation='max', kernel_shape='sphere', kernel_size=3.0, internal_datatype='char'), name='pre_mask_dilate') # Ensure mask's header matches reference's check_hdr = pe.Node(MatchHeader(), name='check_hdr', run_without_submitting=True) # Run N4 normally, force num_threads=1 for stability (images are small, no need for >1) n4_correct = pe.Node(ants.N4BiasFieldCorrection( dimension=3, copy_header=True, bspline_fitting_distance=200), name='n4_correct', n_procs=1) # 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 constrast & fix header unifize = pe.Node( afni.Unifize( t2=True, outputtype='NIFTI_GZ', # Default -clfrac is 0.1, 0.4 was too conservative # -rbt because I'm a Jedi AFNI Master (see 3dUnifize's documentation) 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 ANFI'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') # Compute masked brain apply_mask = pe.Node(fsl.ApplyMask(), name='apply_mask') if not pre_mask: bold_template = get_template('MNI152NLin2009cAsym', resolution=2, desc='fMRIPrep', suffix='boldref') brain_mask = get_template('MNI152NLin2009cAsym', resolution=2, desc='brain', suffix='mask') # Initialize transforms with antsAI init_aff = pe.Node(AI(fixed_image=str(bold_template), fixed_image_mask=str(brain_mask), metric=('Mattes', 32, 'Regular', 0.2), transform=('Affine', 0.1), search_factor=(20, 0.12), principal_axes=False, convergence=(10, 1e-6, 10), verbose=True), name='init_aff', n_procs=omp_nthreads) # Registration().version may be None if parseversion(Registration().version or '0.0.0') > Version('2.2.0'): init_aff.inputs.search_grid = (40, (0, 40, 40)) # Set up spatial normalization norm = pe.Node(Registration(from_file=pkgr_fn( 'fmriprep.data', 'epi_atlasbased_brainmask.json')), name='norm', n_procs=omp_nthreads) norm.inputs.fixed_image = str(bold_template) map_brainmask = pe.Node(ApplyTransforms(interpolation='MultiLabel', float=True, input_image=str(brain_mask)), name='map_brainmask') workflow.connect([ (inputnode, init_aff, [('in_file', 'moving_image')]), (inputnode, map_brainmask, [('in_file', 'reference_image')]), (inputnode, norm, [('in_file', 'moving_image')]), (init_aff, norm, [('output_transform', 'initial_moving_transform') ]), (norm, map_brainmask, [('reverse_invert_flags', 'invert_transform_flags'), ('reverse_transforms', 'transforms')]), (map_brainmask, pre_dilate, [('output_image', 'in_file')]), ]) else: workflow.connect([ (inputnode, pre_dilate, [('pre_mask', 'in_file')]), ]) workflow.connect([ (inputnode, check_hdr, [('in_file', 'reference')]), (pre_dilate, check_hdr, [('out_file', 'in_file')]), (check_hdr, n4_correct, [('out_file', 'mask_image')]), (inputnode, n4_correct, [('in_file', 'input_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')]), (fixhdr_unifize, apply_mask, [('out_file', 'in_file')]), (combine_masks, apply_mask, [('out_file', 'mask_file')]), (combine_masks, outputnode, [('out_file', 'mask_file')]), (apply_mask, outputnode, [('out_file', 'skull_stripped_file')]), (n4_correct, outputnode, [('output_image', 'bias_corrected_file')]), ]) return workflow
def init_brain_extraction_wf(name='brain_extraction_wf', in_template='OASIS', use_float=True, normalization_quality='precise', omp_nthreads=None, mem_gb=3.0, modality='T1', atropos_refine=True, atropos_use_random_seed=True, atropos_model=None): """ A Nipype implementation of the official ANTs' ``antsBrainExtraction.sh`` workflow (only for 3D images). The official workflow is built as follows (and this implementation follows the same organization): 1. Step 1 performs several clerical tasks (adding padding, calculating the Laplacian of inputs, affine initialization) and the core spatial normalization. 2. Maps the brain mask into target space using the normalization calculated in 1. 3. Superstep 1b: smart binarization of the brain mask 4. Superstep 6: apply ATROPOS and massage its outputs 5. Superstep 7: use results from 4 to refine the brain mask .. workflow:: :graph2use: orig :simple_form: yes from niworkflows.anat import init_brain_extraction_wf wf = init_brain_extraction_wf() **Parameters** in_template : str Name of the skull-stripping template ('OASIS', 'NKI', or path). The brain template from which regions will be projected Anatomical template created using e.g. LPBA40 data set with ``buildtemplateparallel.sh`` in ANTs. The workflow will automatically search for a brain probability mask created using e.g. LPBA40 data set which have brain masks defined, and warped to anatomical template and averaged resulting in a probability image. use_float : bool Whether single precision should be used normalization_quality : str Use more precise or faster registration parameters (default: ``precise``, other possible values: ``testing``) omp_nthreads : int Maximum number of threads an individual process may use mem_gb : float Estimated peak memory consumption of the most hungry nodes in the workflow modality : str Sequence type of the first input image ('T1', 'T2', or 'FLAIR') atropos_refine : bool Enables or disables the whole ATROPOS sub-workflow atropos_use_random_seed : bool Whether ATROPOS should generate a random seed based on the system's clock atropos_model : tuple or None Allows to specify a particular segmentation model, overwriting the defaults based on ``modality`` name : str, optional Workflow name (default: antsBrainExtraction) **Inputs** in_files List of input anatomical images to be brain-extracted, typically T1-weighted. If a list of anatomical images is provided, subsequently specified images are used during the segmentation process. However, only the first image is used in the registration of priors. Our suggestion would be to specify the T1w as the first image. in_mask (optional) Mask used for registration to limit the metric computation to a specific region. **Outputs** bias_corrected The ``in_files`` input images, after :abbr:`INU (intensity non-uniformity)` correction. out_mask Calculated brain mask bias_image The :abbr:`INU (intensity non-uniformity)` field estimated for each input in ``in_files`` out_segm Output segmentation by ATROPOS out_tpms Output :abbr:`TPMs (tissue probability maps)` by ATROPOS """ wf = pe.Workflow(name) template_path = None if in_template in TEMPLATE_MAP: template_path = get_dataset(in_template) else: template_path = in_template mod = ('%sw' % modality[:2].upper() if modality.upper().startswith('T') else modality.upper()) # Append template modality potential_targets = list(Path(template_path).glob('*_%s.nii.gz' % mod)) if not potential_targets: raise ValueError('No %s template was found under "%s".' % (mod, template_path)) tpl_target_path = str(potential_targets[0]) target_basename = '_'.join(tpl_target_path.split('_')[:-1]) # Get probabilistic brain mask if available tpl_mask_path = '%s_class-brainmask_probtissue.nii.gz' % target_basename # Fall-back to a binary mask just in case if not os.path.exists(tpl_mask_path): tpl_mask_path = '%s_brainmask.nii.gz' % target_basename if not os.path.exists(tpl_mask_path): raise ValueError( 'Probability map for the brain mask associated to this template ' '"%s" not found.' % tpl_mask_path) if omp_nthreads is None or omp_nthreads < 1: omp_nthreads = cpu_count() inputnode = pe.Node(niu.IdentityInterface(fields=['in_files', 'in_mask']), name='inputnode') # Try to find a registration mask, set if available tpl_regmask_path = '%s_label-BrainCerebellumRegistration_roi.nii.gz' % target_basename if os.path.exists(tpl_regmask_path): inputnode.inputs.in_mask = tpl_regmask_path outputnode = pe.Node(niu.IdentityInterface( fields=['bias_corrected', 'out_mask', 'bias_image', 'out_segm']), name='outputnode') trunc = pe.MapNode(ImageMath(operation='TruncateImageIntensity', op2='0.01 0.999 256'), name='truncate_images', iterfield=['op1']) inu_n4 = pe.MapNode(N4BiasFieldCorrection(dimension=3, save_bias=True, copy_header=True, n_iterations=[50] * 4, convergence_threshold=1e-7, shrink_factor=4, bspline_fitting_distance=200), n_procs=omp_nthreads, name='inu_n4', iterfield=['input_image']) res_tmpl = pe.Node(ResampleImageBySpacing(out_spacing=(4, 4, 4), apply_smoothing=True), name='res_tmpl') res_tmpl.inputs.input_image = tpl_target_path res_target = pe.Node(ResampleImageBySpacing(out_spacing=(4, 4, 4), apply_smoothing=True), name='res_target') lap_tmpl = pe.Node(ImageMath(operation='Laplacian', op2='1.5 1'), name='lap_tmpl') lap_tmpl.inputs.op1 = tpl_target_path lap_target = pe.Node(ImageMath(operation='Laplacian', op2='1.5 1'), name='lap_target') mrg_tmpl = pe.Node(niu.Merge(2), name='mrg_tmpl') mrg_tmpl.inputs.in1 = tpl_target_path mrg_target = pe.Node(niu.Merge(2), name='mrg_target') # Initialize transforms with antsAI init_aff = pe.Node(AI(metric=('Mattes', 32, 'Regular', 0.2), transform=('Affine', 0.1), search_factor=(20, 0.12), principal_axes=False, convergence=(10, 1e-6, 10), verbose=True), name='init_aff', n_procs=omp_nthreads) if parseversion(Registration().version) > Version('2.2.0'): init_aff.inputs.search_grid = (40, (0, 40, 40)) # Set up spatial normalization norm = pe.Node(Registration(from_file=pkgr_fn( 'niworkflows.data', 'antsBrainExtraction_%s.json' % normalization_quality)), name='norm', n_procs=omp_nthreads, mem_gb=mem_gb) norm.inputs.float = use_float fixed_mask_trait = 'fixed_image_mask' if parseversion(Registration().version) >= Version('2.2.0'): fixed_mask_trait += 's' map_brainmask = pe.Node(ApplyTransforms(interpolation='Gaussian', float=True), name='map_brainmask', mem_gb=1) map_brainmask.inputs.input_image = tpl_mask_path thr_brainmask = pe.Node(ThresholdImage(dimension=3, th_low=0.5, th_high=1.0, inside_value=1, outside_value=0), name='thr_brainmask') # Morphological dilation, radius=2 dil_brainmask = pe.Node(ImageMath(operation='MD', op2='2'), name='dil_brainmask') # Get largest connected component get_brainmask = pe.Node(ImageMath(operation='GetLargestComponent'), name='get_brainmask') # Apply mask apply_mask = pe.MapNode(ApplyMask(), iterfield=['in_file'], name='apply_mask') wf.connect([ (inputnode, trunc, [('in_files', 'op1')]), (inputnode, init_aff, [('in_mask', 'fixed_image_mask')]), (inputnode, norm, [('in_mask', fixed_mask_trait)]), (inputnode, map_brainmask, [(('in_files', _pop), 'reference_image')]), (trunc, inu_n4, [('output_image', 'input_image')]), (inu_n4, res_target, [(('output_image', _pop), 'input_image')]), (inu_n4, lap_target, [(('output_image', _pop), 'op1')]), (res_tmpl, init_aff, [('output_image', 'fixed_image')]), (res_target, init_aff, [('output_image', 'moving_image')]), (inu_n4, mrg_target, [('output_image', 'in1')]), (lap_tmpl, mrg_tmpl, [('output_image', 'in2')]), (lap_target, mrg_target, [('output_image', 'in2')]), (init_aff, norm, [('output_transform', 'initial_moving_transform')]), (mrg_tmpl, norm, [('out', 'fixed_image')]), (mrg_target, norm, [('out', 'moving_image')]), (norm, map_brainmask, [('reverse_invert_flags', 'invert_transform_flags'), ('reverse_transforms', 'transforms')]), (map_brainmask, thr_brainmask, [('output_image', 'input_image')]), (thr_brainmask, dil_brainmask, [('output_image', 'op1')]), (dil_brainmask, get_brainmask, [('output_image', 'op1')]), (inu_n4, apply_mask, [('output_image', 'in_file')]), (get_brainmask, apply_mask, [('output_image', 'mask_file')]), (get_brainmask, outputnode, [('output_image', 'out_mask')]), (apply_mask, outputnode, [('out_file', 'bias_corrected')]), (inu_n4, outputnode, [('bias_image', 'bias_image')]), ]) if atropos_refine: atropos_wf = init_atropos_wf( use_random_seed=atropos_use_random_seed, omp_nthreads=omp_nthreads, mem_gb=mem_gb, in_segmentation_model=atropos_model or list(ATROPOS_MODELS[modality].values())) wf.disconnect([ (get_brainmask, outputnode, [('output_image', 'out_mask')]), (get_brainmask, apply_mask, [('output_image', 'mask_file')]), ]) wf.connect([ (inu_n4, atropos_wf, [('output_image', 'inputnode.in_files')]), (get_brainmask, atropos_wf, [('output_image', 'inputnode.in_mask') ]), (atropos_wf, outputnode, [('outputnode.out_mask', 'out_mask')]), (atropos_wf, apply_mask, [('outputnode.out_mask', 'mask_file')]), (atropos_wf, outputnode, [('outputnode.out_segm', 'out_segm'), ('outputnode.out_tpms', 'out_tpms')]) ]) return wf
def init_brain_extraction_wf( name="brain_extraction_wf", in_template="OASIS30ANTs", template_spec=None, use_float=True, normalization_quality="precise", omp_nthreads=None, mem_gb=3.0, bids_suffix="T1w", atropos_refine=True, atropos_use_random_seed=True, atropos_model=None, use_laplacian=True, bspline_fitting_distance=200, ): """ Build a workflow for atlas-based brain extraction on anatomical MRI data. This is a Nipype implementation of atlas-based brain extraction inspired by the official ANTs' ``antsBrainExtraction.sh`` workflow (only for 3D images). The workflow follows the following structure: 1. Step 1 performs several clerical tasks (preliminary INU correction, calculating the Laplacian of inputs, affine initialization) and the core spatial normalization. 2. Maps the brain mask into target space using the normalization calculated in 1. 3. Superstep 1b: binarization of the brain mask 4. Maps the WM (white matter) probability map from the template, if such prior exists. Combines the BS (brainstem) probability map before mapping if the WM and BS are given separately (as it is the case for ``OASIS30ANTs``.) 5. Run a second N4 INU correction round, using the prior mapped into individual step in step 4 if available. 6. Superstep 6: apply ATROPOS on the INU-corrected result of step 5, and massage its outputs 7. Superstep 7: use results from 4 to refine the brain mask 8. If exist, use priors from step 4, calculate the overlap of the posteriors estimated in step 4 to select that overlapping the most with the WM+BS prior from the template. Combine that posterior with the refined brain mask and pass it on to the next step. 9. Apply a final N4 using the refined brain mask (or the map calculated in step 8 if priors were found) as weights map for the algorithm. Workflow Graph .. workflow:: :graph2use: orig :simple_form: yes from niworkflows.anat.ants import init_brain_extraction_wf wf = init_brain_extraction_wf() Parameters ---------- in_template : str Name of the skull-stripping template ('OASIS30ANTs', 'NKI', or path). The brain template from which regions will be projected Anatomical template created using e.g. LPBA40 data set with ``buildtemplateparallel.sh`` in ANTs. The workflow will automatically search for a brain probability mask created using e.g. LPBA40 data set which have brain masks defined, and warped to anatomical template and averaged resulting in a probability image. use_float : bool Whether single precision should be used normalization_quality : str Use more precise or faster registration parameters (default: ``precise``, other possible values: ``testing``) omp_nthreads : int Maximum number of threads an individual process may use mem_gb : float Estimated peak memory consumption of the most hungry nodes in the workflow bids_suffix : str Sequence type of the first input image. For a list of acceptable values see https://bids-specification.readthedocs.io/en/latest/\ 04-modality-specific-files/01-magnetic-resonance-imaging-data.html#anatomy-imaging-data atropos_refine : bool Enables or disables the whole ATROPOS sub-workflow atropos_use_random_seed : bool Whether ATROPOS should generate a random seed based on the system's clock atropos_model : tuple or None Allows to specify a particular segmentation model, overwriting the defaults based on ``bids_suffix`` use_laplacian : bool Enables or disables alignment of the Laplacian as an additional criterion for image registration quality (default: True) bspline_fitting_distance : float The size of the b-spline mesh grid elements, in mm (default: 200) name : str, optional Workflow name (default: antsBrainExtraction) Inputs ------ in_files : list List of input anatomical images to be brain-extracted, typically T1-weighted. If a list of anatomical images is provided, subsequently specified images are used during the segmentation process. However, only the first image is used in the registration of priors. Our suggestion would be to specify the T1w as the first image. in_mask : list, optional Mask used for registration to limit the metric computation to a specific region. Outputs ------- out_file : str Skull-stripped and :abbr:`INU (intensity non-uniformity)`-corrected ``in_files`` out_mask : str Calculated brain mask bias_corrected : str The ``in_files`` input images, after :abbr:`INU (intensity non-uniformity)` correction, before skull-stripping. bias_image : str The :abbr:`INU (intensity non-uniformity)` field estimated for each input in ``in_files`` out_segm : str Output segmentation by ATROPOS out_tpms : str Output :abbr:`TPMs (tissue probability maps)` by ATROPOS """ from packaging.version import parse as parseversion, Version from templateflow.api import get as get_template wf = pe.Workflow(name) template_spec = template_spec or {} # suffix passed via spec takes precedence template_spec["suffix"] = template_spec.get("suffix", bids_suffix) tpl_target_path, common_spec = get_template_specs( in_template, template_spec=template_spec) # Get probabilistic brain mask if available tpl_mask_path = get_template( in_template, label="brain", suffix="probseg", ** common_spec) or get_template( in_template, desc="brain", suffix="mask", **common_spec) if omp_nthreads is None or omp_nthreads < 1: omp_nthreads = cpu_count() inputnode = pe.Node(niu.IdentityInterface(fields=["in_files", "in_mask"]), name="inputnode") # Try to find a registration mask, set if available tpl_regmask_path = get_template(in_template, desc="BrainCerebellumExtraction", suffix="mask", **common_spec) if tpl_regmask_path: inputnode.inputs.in_mask = str(tpl_regmask_path) outputnode = pe.Node( niu.IdentityInterface(fields=[ "out_file", "out_mask", "bias_corrected", "bias_image", "out_segm", "out_tpms", ]), name="outputnode", ) trunc = pe.MapNode( ImageMath(operation="TruncateImageIntensity", op2="0.01 0.999 256", copy_header=True), name="truncate_images", iterfield=["op1"], ) inu_n4 = pe.MapNode( N4BiasFieldCorrection( dimension=3, save_bias=False, copy_header=True, n_iterations=[50] * 4, convergence_threshold=1e-7, shrink_factor=4, bspline_fitting_distance=bspline_fitting_distance, ), n_procs=omp_nthreads, name="inu_n4", iterfield=["input_image"], ) res_tmpl = pe.Node( RegridToZooms(in_file=tpl_target_path, zooms=(4, 4, 4), smooth=True), name="res_tmpl", ) res_target = pe.Node(RegridToZooms(zooms=(4, 4, 4), smooth=True), name="res_target") lap_tmpl = pe.Node(ImageMath(operation="Laplacian", op2="1.5 1", copy_header=True), name="lap_tmpl") lap_tmpl.inputs.op1 = tpl_target_path lap_target = pe.Node( ImageMath(operation="Laplacian", op2="1.5 1", copy_header=True), name="lap_target", ) mrg_tmpl = pe.Node(niu.Merge(2), name="mrg_tmpl") mrg_tmpl.inputs.in1 = tpl_target_path mrg_target = pe.Node(niu.Merge(2), name="mrg_target") # Initialize transforms with antsAI 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), verbose=True, ), name="init_aff", n_procs=omp_nthreads, ) # Tolerate missing ANTs at construction time try: init_aff.inputs.search_grid = (40, (0, 40, 40)) except ValueError: warn("antsAI's option --search-grid was added in ANTS 2.3.0 " f"({init_aff.interface.version} found.)") # Set up spatial normalization settings_file = ("antsBrainExtraction_%s.json" if use_laplacian else "antsBrainExtractionNoLaplacian_%s.json") norm = pe.Node( Registration(from_file=pkgr_fn("niworkflows.data", settings_file % normalization_quality)), name="norm", n_procs=omp_nthreads, mem_gb=mem_gb, ) norm.inputs.float = use_float fixed_mask_trait = "fixed_image_mask" if norm.interface.version and parseversion( norm.interface.version) >= Version("2.2.0"): fixed_mask_trait += "s" map_brainmask = pe.Node( ApplyTransforms(interpolation="Gaussian"), name="map_brainmask", mem_gb=1, ) map_brainmask.inputs.input_image = str(tpl_mask_path) thr_brainmask = pe.Node( ThresholdImage( dimension=3, th_low=0.5, th_high=1.0, inside_value=1, outside_value=0, copy_header=True, ), name="thr_brainmask", ) # Refine INU correction inu_n4_final = pe.MapNode( N4BiasFieldCorrection( dimension=3, save_bias=True, copy_header=True, n_iterations=[50] * 5, convergence_threshold=1e-7, shrink_factor=4, bspline_fitting_distance=bspline_fitting_distance, ), n_procs=omp_nthreads, name="inu_n4_final", iterfield=["input_image"], ) try: inu_n4_final.inputs.rescale_intensities = True except ValueError: warn( "N4BiasFieldCorrection's --rescale-intensities option was added in ANTS 2.1.0 " f"({inu_n4_final.interface.version} found.) Please consider upgrading.", UserWarning, ) # Apply mask apply_mask = pe.MapNode(ApplyMask(), iterfield=["in_file"], name="apply_mask") # fmt: off wf.connect([ (inputnode, trunc, [("in_files", "op1")]), (inputnode, inu_n4_final, [("in_files", "input_image")]), (inputnode, init_aff, [("in_mask", "fixed_image_mask")]), (inputnode, norm, [("in_mask", fixed_mask_trait)]), (inputnode, map_brainmask, [(("in_files", _pop), "reference_image")]), (trunc, inu_n4, [("output_image", "input_image")]), (inu_n4, res_target, [(("output_image", _pop), "in_file")]), (res_tmpl, init_aff, [("out_file", "fixed_image")]), (res_target, init_aff, [("out_file", "moving_image")]), (init_aff, norm, [("output_transform", "initial_moving_transform")]), (norm, map_brainmask, [ ("reverse_transforms", "transforms"), ("reverse_invert_flags", "invert_transform_flags"), ]), (map_brainmask, thr_brainmask, [("output_image", "input_image")]), (map_brainmask, inu_n4_final, [("output_image", "weight_image")]), (inu_n4_final, apply_mask, [("output_image", "in_file")]), (thr_brainmask, apply_mask, [("output_image", "in_mask")]), (thr_brainmask, outputnode, [("output_image", "out_mask")]), (inu_n4_final, outputnode, [("output_image", "bias_corrected"), ("bias_image", "bias_image")]), (apply_mask, outputnode, [("out_file", "out_file")]), ]) # fmt: on wm_tpm = (get_template( in_template, label="WM", suffix="probseg", **common_spec) or None) if wm_tpm: map_wmmask = pe.Node( ApplyTransforms(interpolation="Gaussian"), name="map_wmmask", mem_gb=1, ) # Add the brain stem if it is found. bstem_tpm = (get_template( in_template, label="BS", suffix="probseg", **common_spec) or None) if bstem_tpm: full_wm = pe.Node(niu.Function(function=_imsum), name="full_wm") full_wm.inputs.op1 = str(wm_tpm) full_wm.inputs.op2 = str(bstem_tpm) # fmt: off wf.connect([(full_wm, map_wmmask, [("out", "input_image")])]) # fmt: on else: map_wmmask.inputs.input_image = str(wm_tpm) # fmt: off wf.disconnect([ (map_brainmask, inu_n4_final, [("output_image", "weight_image")]), ]) wf.connect([ (inputnode, map_wmmask, [(("in_files", _pop), "reference_image")]), (norm, map_wmmask, [ ("reverse_transforms", "transforms"), ("reverse_invert_flags", "invert_transform_flags"), ]), (map_wmmask, inu_n4_final, [("output_image", "weight_image")]), ]) # fmt: on if use_laplacian: lap_tmpl = pe.Node( ImageMath(operation="Laplacian", op2="1.5 1", copy_header=True), name="lap_tmpl", ) lap_tmpl.inputs.op1 = tpl_target_path lap_target = pe.Node( ImageMath(operation="Laplacian", op2="1.5 1", copy_header=True), name="lap_target", ) mrg_tmpl = pe.Node(niu.Merge(2), name="mrg_tmpl") mrg_tmpl.inputs.in1 = tpl_target_path mrg_target = pe.Node(niu.Merge(2), name="mrg_target") # fmt: off wf.connect([ (inu_n4, lap_target, [(("output_image", _pop), "op1")]), (lap_tmpl, mrg_tmpl, [("output_image", "in2")]), (inu_n4, mrg_target, [("output_image", "in1")]), (lap_target, mrg_target, [("output_image", "in2")]), (mrg_tmpl, norm, [("out", "fixed_image")]), (mrg_target, norm, [("out", "moving_image")]), ]) # fmt: on else: norm.inputs.fixed_image = tpl_target_path # fmt: off wf.connect([ (inu_n4, norm, [(("output_image", _pop), "moving_image")]), ]) # fmt: on if atropos_refine: atropos_model = atropos_model or list( ATROPOS_MODELS[bids_suffix].values()) atropos_wf = init_atropos_wf( use_random_seed=atropos_use_random_seed, omp_nthreads=omp_nthreads, mem_gb=mem_gb, in_segmentation_model=atropos_model, bspline_fitting_distance=bspline_fitting_distance, wm_prior=bool(wm_tpm), ) # fmt: off wf.disconnect([ (thr_brainmask, outputnode, [("output_image", "out_mask")]), (inu_n4_final, outputnode, [("output_image", "bias_corrected"), ("bias_image", "bias_image")]), (apply_mask, outputnode, [("out_file", "out_file")]), ]) wf.connect([ (inputnode, atropos_wf, [("in_files", "inputnode.in_files")]), (inu_n4_final, atropos_wf, [("output_image", "inputnode.in_corrected")]), (thr_brainmask, atropos_wf, [("output_image", "inputnode.in_mask") ]), (atropos_wf, outputnode, [ ("outputnode.out_file", "out_file"), ("outputnode.bias_corrected", "bias_corrected"), ("outputnode.bias_image", "bias_image"), ("outputnode.out_mask", "out_mask"), ("outputnode.out_segm", "out_segm"), ("outputnode.out_tpms", "out_tpms"), ]), ]) # fmt: on if wm_tpm: # fmt: off wf.connect([ (map_wmmask, atropos_wf, [("output_image", "inputnode.wm_prior")]), ]) # fmt: on return wf
def init_enhance_and_skullstrip_bold_wf( brainmask_thresh=0.5, name="enhance_and_skullstrip_bold_wf", omp_nthreads=1, pre_mask=False, ): """ Enhance and run brain extraction on a BOLD EPI image. This workflow takes in a :abbr:`BOLD (blood-oxygen level-dependant)` :abbr:`fMRI (functional MRI)` average/summary (e.g., a reference image averaging non-steady-state timepoints), 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. Calculate a tentative mask by registering (9-parameters) to *fMRIPrep*'s :abbr:`EPI (echo-planar imaging)` -*boldref* template, which is in MNI space. The tentative mask is obtained by resampling the MNI template's brainmask into *boldref*-space. 2. Binary dilation of the tentative mask with a sphere of 3mm diameter. 3. Run ANTs' ``N4BiasFieldCorrection`` on the input :abbr:`BOLD (blood-oxygen level-dependant)` average, using the mask generated in 1) instead of the internal Otsu thresholding. 4. Calculate a loose mask using FSL's ``bet``, with one mathematical morphology dilation of one iteration and a sphere of 6mm as structuring element. 5. 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. 6. Calculate a mask using AFNI's ``3dAutomask`` after the contrast enhancement of 4). 7. Calculate a final mask as the intersection of 4) and 6). 8. Apply final mask on the enhanced reference. Step 1 can be skipped if the ``pre_mask`` argument is set to ``True`` and a tentative mask is passed in to the workflow throught the ``pre_mask`` Nipype input. Workflow graph .. workflow :: :graph2use: orig :simple_form: yes from niworkflows.func.util import init_enhance_and_skullstrip_bold_wf wf = init_enhance_and_skullstrip_bold_wf(omp_nthreads=1) .. _N4BiasFieldCorrection: https://hdl.handle.net/10380/3053 Parameters ---------- brainmask_thresh: :obj:`float` Lower threshold for the probabilistic brainmask to obtain the final binary mask (default: 0.5). name : str Name of workflow (default: ``enhance_and_skullstrip_bold_wf``) omp_nthreads : int number of threads available to parallel nodes pre_mask : bool Indicates whether the ``pre_mask`` input will be set (and thus, step 1 should be skipped). Inputs ------ in_file : str BOLD image (single volume) pre_mask : bool A tentative brain mask to initialize the workflow (requires ``pre_mask`` parameter set ``True``). Outputs ------- bias_corrected_file : str the ``in_file`` after `N4BiasFieldCorrection`_ skull_stripped_file : str the ``bias_corrected_file`` after skull-stripping mask_file : str mask of the skull-stripped input file out_report : str reportlet for the skull-stripping """ 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", ) # Dilate pre_mask pre_dilate = pe.Node( fsl.DilateImage( operation="max", kernel_shape="sphere", kernel_size=3.0, internal_datatype="char", ), name="pre_mask_dilate", ) # Ensure mask's header matches reference's check_hdr = pe.Node(MatchHeader(), name="check_hdr", run_without_submitting=True) # 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 constrast & fix header unifize = pe.Node( afni.Unifize( t2=True, outputtype="NIFTI_GZ", # Default -clfrac is 0.1, 0.4 was too conservative # -rbt because I'm a Jedi AFNI Master (see 3dUnifize's documentation) 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 ANFI'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") # Compute masked brain apply_mask = pe.Node(fsl.ApplyMask(), name="apply_mask") if not pre_mask: from ..interfaces.nibabel import Binarize bold_template = get_template("MNI152NLin2009cAsym", resolution=2, desc="fMRIPrep", suffix="boldref") brain_mask = get_template("MNI152NLin2009cAsym", resolution=2, desc="brain", suffix="mask") # Initialize transforms with antsAI init_aff = pe.Node( AI( fixed_image=str(bold_template), fixed_image_mask=str(brain_mask), metric=("Mattes", 32, "Regular", 0.2), transform=("Affine", 0.1), search_factor=(20, 0.12), principal_axes=False, convergence=(10, 1e-6, 10), verbose=True, ), name="init_aff", n_procs=omp_nthreads, ) # Registration().version may be None if parseversion(Registration().version or "0.0.0") > Version("2.2.0"): init_aff.inputs.search_grid = (40, (0, 40, 40)) # Set up spatial normalization norm = pe.Node( Registration(from_file=pkgr_fn("niworkflows.data", "epi_atlasbased_brainmask.json")), name="norm", n_procs=omp_nthreads, ) norm.inputs.fixed_image = str(bold_template) map_brainmask = pe.Node( ApplyTransforms( interpolation="BSpline", float=True, # Use the higher resolution and probseg for numerical stability in rounding input_image=str( get_template( "MNI152NLin2009cAsym", resolution=1, label="brain", suffix="probseg", )), ), name="map_brainmask", ) binarize_mask = pe.Node(Binarize(thresh_low=brainmask_thresh), name="binarize_mask") # fmt: off workflow.connect([ (inputnode, init_aff, [("in_file", "moving_image")]), (inputnode, map_brainmask, [("in_file", "reference_image")]), (inputnode, norm, [("in_file", "moving_image")]), (init_aff, norm, [("output_transform", "initial_moving_transform") ]), (norm, map_brainmask, [ ("reverse_invert_flags", "invert_transform_flags"), ("reverse_transforms", "transforms"), ]), (map_brainmask, binarize_mask, [("output_image", "in_file")]), (binarize_mask, pre_dilate, [("out_mask", "in_file")]), ]) # fmt: on else: # fmt: off workflow.connect([ (inputnode, pre_dilate, [("pre_mask", "in_file")]), ]) # fmt: on # fmt: off workflow.connect([ (inputnode, check_hdr, [("in_file", "reference")]), (pre_dilate, check_hdr, [("out_file", "in_file")]), (check_hdr, n4_correct, [("out_file", "mask_image")]), (inputnode, n4_correct, [("in_file", "input_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")]), (fixhdr_unifize, apply_mask, [("out_file", "in_file")]), (combine_masks, apply_mask, [("out_file", "mask_file")]), (combine_masks, outputnode, [("out_file", "mask_file")]), (apply_mask, outputnode, [("out_file", "skull_stripped_file")]), (n4_correct, outputnode, [("output_image", "bias_corrected_file")]), ]) # fmt: on return workflow
def init_brain_extraction_wf(name='brain_extraction_wf', in_template='OASIS30ANTs', use_float=True, normalization_quality='precise', omp_nthreads=None, mem_gb=3.0, bids_suffix='T1w', atropos_refine=True, atropos_use_random_seed=True, atropos_model=None, use_laplacian=True, bspline_fitting_distance=200): """ A Nipype implementation of the official ANTs' ``antsBrainExtraction.sh`` workflow (only for 3D images). The official workflow is built as follows (and this implementation follows the same organization): 1. Step 1 performs several clerical tasks (adding padding, calculating the Laplacian of inputs, affine initialization) and the core spatial normalization. 2. Maps the brain mask into target space using the normalization calculated in 1. 3. Superstep 1b: smart binarization of the brain mask 4. Superstep 6: apply ATROPOS and massage its outputs 5. Superstep 7: use results from 4 to refine the brain mask .. workflow:: :graph2use: orig :simple_form: yes from niworkflows.anat import init_brain_extraction_wf wf = init_brain_extraction_wf() **Parameters** in_template : str Name of the skull-stripping template ('OASIS30ANTs', 'NKI', or path). The brain template from which regions will be projected Anatomical template created using e.g. LPBA40 data set with ``buildtemplateparallel.sh`` in ANTs. The workflow will automatically search for a brain probability mask created using e.g. LPBA40 data set which have brain masks defined, and warped to anatomical template and averaged resulting in a probability image. use_float : bool Whether single precision should be used normalization_quality : str Use more precise or faster registration parameters (default: ``precise``, other possible values: ``testing``) omp_nthreads : int Maximum number of threads an individual process may use mem_gb : float Estimated peak memory consumption of the most hungry nodes in the workflow bids_suffix : str Sequence type of the first input image. For a list of acceptable values see https://bids-specification.readthedocs.io/en/latest/\ 04-modality-specific-files/01-magnetic-resonance-imaging-data.html#anatomy-imaging-data atropos_refine : bool Enables or disables the whole ATROPOS sub-workflow atropos_use_random_seed : bool Whether ATROPOS should generate a random seed based on the system's clock atropos_model : tuple or None Allows to specify a particular segmentation model, overwriting the defaults based on ``bids_suffix`` use_laplacian : bool Enables or disables alignment of the Laplacian as an additional criterion for image registration quality (default: True) bspline_fitting_distance : float The size of the b-spline mesh grid elements, in mm (default: 200) name : str, optional Workflow name (default: antsBrainExtraction) **Inputs** in_files List of input anatomical images to be brain-extracted, typically T1-weighted. If a list of anatomical images is provided, subsequently specified images are used during the segmentation process. However, only the first image is used in the registration of priors. Our suggestion would be to specify the T1w as the first image. in_mask (optional) Mask used for registration to limit the metric computation to a specific region. **Outputs** out_file Skull-stripped and :abbr:`INU (intensity non-uniformity)`-corrected ``in_files`` out_mask Calculated brain mask bias_corrected The ``in_files`` input images, after :abbr:`INU (intensity non-uniformity)` correction, before skull-stripping. bias_image The :abbr:`INU (intensity non-uniformity)` field estimated for each input in ``in_files`` out_segm Output segmentation by ATROPOS out_tpms Output :abbr:`TPMs (tissue probability maps)` by ATROPOS """ from templateflow.api import get as get_template wf = pe.Workflow(name) tpl_target_path = str( get_template(in_template, desc=None, resolution=1, suffix=bids_suffix)) # Get probabilistic brain mask if available tpl_mask_path = get_template( in_template, resolution=1, label='brain', suffix='probseg') or \ get_template(in_template, resolution=1, desc='brain', suffix='mask') if omp_nthreads is None or omp_nthreads < 1: omp_nthreads = cpu_count() inputnode = pe.Node(niu.IdentityInterface(fields=['in_files', 'in_mask']), name='inputnode') # Try to find a registration mask, set if available tpl_regmask_path = get_template( in_template, resolution=1, desc='BrainCerebellumExtraction', suffix='mask') if tpl_regmask_path: inputnode.inputs.in_mask = str(tpl_regmask_path) outputnode = pe.Node(niu.IdentityInterface( fields=['out_file', 'out_mask', 'bias_corrected', 'bias_image', 'out_segm']), name='outputnode') trunc = pe.MapNode(ImageMath(operation='TruncateImageIntensity', op2='0.01 0.999 256'), name='truncate_images', iterfield=['op1']) inu_n4 = pe.MapNode( N4BiasFieldCorrection( dimension=3, save_bias=False, copy_header=True, n_iterations=[50] * 4, convergence_threshold=1e-7, shrink_factor=4, bspline_fitting_distance=bspline_fitting_distance), n_procs=omp_nthreads, name='inu_n4', iterfield=['input_image']) res_tmpl = pe.Node(ResampleImageBySpacing( out_spacing=(4, 4, 4), apply_smoothing=True), name='res_tmpl') res_tmpl.inputs.input_image = tpl_target_path res_target = pe.Node(ResampleImageBySpacing( out_spacing=(4, 4, 4), apply_smoothing=True), name='res_target') lap_tmpl = pe.Node(ImageMath(operation='Laplacian', op2='1.5 1'), name='lap_tmpl') lap_tmpl.inputs.op1 = tpl_target_path lap_target = pe.Node(ImageMath(operation='Laplacian', op2='1.5 1'), name='lap_target') mrg_tmpl = pe.Node(niu.Merge(2), name='mrg_tmpl') mrg_tmpl.inputs.in1 = tpl_target_path mrg_target = pe.Node(niu.Merge(2), name='mrg_target') # Initialize transforms with antsAI 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), verbose=True), name='init_aff', n_procs=omp_nthreads) if parseversion(Registration().version) > Version('2.2.0'): init_aff.inputs.search_grid = (40, (0, 40, 40)) # Set up spatial normalization settings_file = 'antsBrainExtraction_%s.json' if use_laplacian \ else 'antsBrainExtractionNoLaplacian_%s.json' norm = pe.Node(Registration(from_file=pkgr_fn( 'niworkflows.data', settings_file % normalization_quality)), name='norm', n_procs=omp_nthreads, mem_gb=mem_gb) norm.inputs.float = use_float fixed_mask_trait = 'fixed_image_mask' if parseversion(Registration().version) >= Version('2.2.0'): fixed_mask_trait += 's' map_brainmask = pe.Node( ApplyTransforms(interpolation='Gaussian', float=True), name='map_brainmask', mem_gb=1 ) map_brainmask.inputs.input_image = str(tpl_mask_path) thr_brainmask = pe.Node(ThresholdImage( dimension=3, th_low=0.5, th_high=1.0, inside_value=1, outside_value=0), name='thr_brainmask') # Morphological dilation, radius=2 dil_brainmask = pe.Node(ImageMath(operation='MD', op2='2'), name='dil_brainmask') # Get largest connected component get_brainmask = pe.Node(ImageMath(operation='GetLargestComponent'), name='get_brainmask') # Refine INU correction inu_n4_final = pe.MapNode( N4BiasFieldCorrection( dimension=3, save_bias=True, copy_header=True, n_iterations=[50] * 5, convergence_threshold=1e-7, shrink_factor=4, bspline_fitting_distance=bspline_fitting_distance), n_procs=omp_nthreads, name='inu_n4_final', iterfield=['input_image']) # Apply mask apply_mask = pe.MapNode(ApplyMask(), iterfield=['in_file'], name='apply_mask') wf.connect([ (inputnode, trunc, [('in_files', 'op1')]), (inputnode, inu_n4_final, [('in_files', 'input_image')]), (inputnode, init_aff, [('in_mask', 'fixed_image_mask')]), (inputnode, norm, [('in_mask', fixed_mask_trait)]), (inputnode, map_brainmask, [(('in_files', _pop), 'reference_image')]), (trunc, inu_n4, [('output_image', 'input_image')]), (inu_n4, res_target, [ (('output_image', _pop), 'input_image')]), (res_tmpl, init_aff, [('output_image', 'fixed_image')]), (res_target, init_aff, [('output_image', 'moving_image')]), (init_aff, norm, [('output_transform', 'initial_moving_transform')]), (norm, map_brainmask, [ ('reverse_transforms', 'transforms'), ('reverse_invert_flags', 'invert_transform_flags')]), (map_brainmask, thr_brainmask, [('output_image', 'input_image')]), (thr_brainmask, dil_brainmask, [('output_image', 'op1')]), (dil_brainmask, get_brainmask, [('output_image', 'op1')]), (inu_n4_final, apply_mask, [('output_image', 'in_file')]), (get_brainmask, apply_mask, [('output_image', 'mask_file')]), (get_brainmask, outputnode, [('output_image', 'out_mask')]), (apply_mask, outputnode, [('out_file', 'out_file')]), (inu_n4_final, outputnode, [('output_image', 'bias_corrected'), ('bias_image', 'bias_image')]), ]) if use_laplacian: lap_tmpl = pe.Node(ImageMath(operation='Laplacian', op2='1.5 1'), name='lap_tmpl') lap_tmpl.inputs.op1 = tpl_target_path lap_target = pe.Node(ImageMath(operation='Laplacian', op2='1.5 1'), name='lap_target') mrg_tmpl = pe.Node(niu.Merge(2), name='mrg_tmpl') mrg_tmpl.inputs.in1 = tpl_target_path mrg_target = pe.Node(niu.Merge(2), name='mrg_target') wf.connect([ (inu_n4, lap_target, [ (('output_image', _pop), 'op1')]), (lap_tmpl, mrg_tmpl, [('output_image', 'in2')]), (inu_n4, mrg_target, [('output_image', 'in1')]), (lap_target, mrg_target, [('output_image', 'in2')]), (mrg_tmpl, norm, [('out', 'fixed_image')]), (mrg_target, norm, [('out', 'moving_image')]), ]) else: norm.inputs.fixed_image = tpl_target_path wf.connect([ (inu_n4, norm, [ (('output_image', _pop), 'moving_image')]), ]) if atropos_refine: atropos_model = atropos_model or list(ATROPOS_MODELS[bids_suffix].values()) atropos_wf = init_atropos_wf( use_random_seed=atropos_use_random_seed, omp_nthreads=omp_nthreads, mem_gb=mem_gb, in_segmentation_model=atropos_model, ) sel_wm = pe.Node(niu.Select(index=atropos_model[-1] - 1), name='sel_wm', run_without_submitting=True) wf.disconnect([ (get_brainmask, outputnode, [('output_image', 'out_mask')]), (get_brainmask, apply_mask, [('output_image', 'mask_file')]), ]) wf.connect([ (inu_n4, atropos_wf, [ ('output_image', 'inputnode.in_files')]), (thr_brainmask, atropos_wf, [ ('output_image', 'inputnode.in_mask')]), (get_brainmask, atropos_wf, [ ('output_image', 'inputnode.in_mask_dilated')]), (atropos_wf, sel_wm, [('outputnode.out_tpms', 'inlist')]), (sel_wm, inu_n4_final, [('out', 'weight_image')]), (atropos_wf, outputnode, [ ('outputnode.out_mask', 'out_mask')]), (atropos_wf, apply_mask, [ ('outputnode.out_mask', 'mask_file')]), (atropos_wf, outputnode, [ ('outputnode.out_segm', 'out_segm'), ('outputnode.out_tpms', 'out_tpms')]) ]) return wf
def init_syn_sdc_wf( *, atlas_threshold=3, sloppy=False, debug=False, name="syn_sdc_wf", omp_nthreads=1, ): """ Build the *fieldmap-less* susceptibility-distortion estimation workflow. SyN deformation is restricted to the phase-encoding (PE) direction. If no PE direction is specified, anterior-posterior PE is assumed. SyN deformation is also restricted to regions that are expected to have a >3mm (approximately 1 voxel) warp, based on the fieldmap atlas. 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 ---------- atlas_threshold : :obj:`float` Exclude from the registration metric computation areas with average distortions below this threshold (in mm). sloppy : :obj:`bool` Whether a fast (less accurate) configuration of the workflow should be applied. debug : :obj:`bool` Run in debug mode name : :obj:`str` Name for this workflow omp_nthreads : :obj:`int` Parallelize internal tasks across the number of CPUs given by this option. Inputs ------ 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. epi_mask : :obj:`str` A path to a brain mask corresponding to ``epi_ref``. anat_ref : :obj:`str` A preprocessed, skull-stripped anatomical (T1w or T2w) image resampled 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 Outputs ------- fmap : :obj:`str` The path of the estimated fieldmap. fmap_ref : :obj:`str` The path of an unwarped conversion of files in ``epi_ref``. fmap_coeff : :obj:`str` or :obj:`list` of :obj:`str` The path(s) of the B-Spline coefficients supporting the fieldmap. out_warp : :obj:`str` The path of the corresponding displacements field transform to unwarp susceptibility distortions. method: :obj:`str` Short description of the estimation method that was run. """ from pkg_resources import resource_filename as pkgrf from packaging.version import parse as parseversion, Version from nipype.interfaces.ants import ImageMath from niworkflows.interfaces.fixes import ( FixHeaderApplyTransforms as ApplyTransforms, FixHeaderRegistration as Registration, ) from niworkflows.interfaces.nibabel import ( Binarize, IntensityClip, RegridToZooms, ) from ...utils.misc import front as _pop, last as _pull from ...interfaces.epi import GetReadoutTime from ...interfaces.fmap import DisplacementsField2Fieldmap from ...interfaces.bspline import ( ApplyCoeffsField, BSplineApprox, DEFAULT_LF_ZOOMS_MM, DEFAULT_HF_ZOOMS_MM, DEFAULT_ZOOMS_MM, ) from ...interfaces.brainmask import BinaryDilation, Union ants_version = Registration().version if ants_version and parseversion(ants_version) < Version("2.2.0"): raise RuntimeError( f"Please upgrade ANTs to 2.2 or older ({ants_version} found)." ) workflow = Workflow(name=name) workflow.__desc__ = f"""\ A deformation field to correct for susceptibility distortions was estimated based on *fMRIPrep*'s *fieldmap-less* approach. The deformation field is that resulting from co-registering the EPI reference to the same-subject T1w-reference with its intensity inverted [@fieldmapless1; @fieldmapless2]. Registration is performed with `antsRegistration` (ANTs {ants_version or "-- version unknown"}), and the process regularized by constraining deformation to be nonzero only along the phase-encoding direction, and modulated with an average fieldmap template [@fieldmapless3]. """ inputnode = pe.Node(niu.IdentityInterface(INPUT_FIELDS), name="inputnode") outputnode = pe.Node( niu.IdentityInterface( ["fmap", "fmap_ref", "fmap_coeff", "fmap_mask", "out_warp", "method"] ), name="outputnode", ) outputnode.inputs.method = 'FLB ("fieldmap-less", SyN-based)' readout_time = pe.Node( GetReadoutTime(), name="readout_time", run_without_submitting=True, ) warp_dir = pe.Node( niu.Function(function=_warp_dir), run_without_submitting=True, name="warp_dir", ) warp_dir.inputs.nlevels = 2 atlas_msk = pe.Node(Binarize(thresh_low=atlas_threshold), name="atlas_msk") anat_dilmsk = pe.Node(BinaryDilation(), name="anat_dilmsk") amask2epi = pe.Node( ApplyTransforms(interpolation="MultiLabel", transforms="identity"), name="amask2epi", ) # Calculate laplacian maps lap_anat = pe.Node( ImageMath(operation="Laplacian", op2="1.5 1", copy_header=True), name="lap_anat" ) lap_anat_norm = pe.Node(niu.Function(function=_norm_lap), name="lap_anat_norm") anat_merge = pe.Node( niu.Merge(2), name="anat_merge", run_without_submitting=True, ) clip_epi = pe.Node(IntensityClip(p_min=35.0, p_max=99.9), name="clip_epi") lap_epi = pe.Node( ImageMath(operation="Laplacian", op2="1.5 1", copy_header=True), name="lap_epi" ) lap_epi_norm = pe.Node(niu.Function(function=_norm_lap), name="lap_epi_norm") epi_merge = pe.Node( niu.Merge(2), name="epi_merge", run_without_submitting=True, ) epi_umask = pe.Node(Union(), name="epi_umask") moving_masks = pe.Node( niu.Merge(3), name="moving_masks", run_without_submitting=True, ) fixed_masks = pe.Node( niu.Merge(3), name="fixed_masks", mem_gb=DEFAULT_MEMORY_MIN_GB, run_without_submitting=True, ) # Set a manageable size for the epi reference find_zooms = pe.Node(niu.Function(function=_adjust_zooms), name="find_zooms") zooms_epi = pe.Node(RegridToZooms(), name="zooms_epi") # SyN Registration Core syn = pe.Node( Registration( from_file=pkgrf("sdcflows", f"data/sd_syn{'_sloppy' * sloppy}.json") ), name="syn", n_procs=omp_nthreads, ) syn.inputs.output_warped_image = debug syn.inputs.output_inverse_warped_image = debug if debug: syn.inputs.args = "--write-interval-volumes 2" # Extract the corresponding fieldmap in Hz extract_field = pe.Node( DisplacementsField2Fieldmap(demean=True), name="extract_field" ) unwarp = pe.Node(ApplyCoeffsField(), name="unwarp") # Check zooms (avoid very expensive B-Splines fitting) zooms_field = pe.Node( ApplyTransforms( interpolation="BSpline", transforms="identity", args="-u float" ), name="zooms_field", ) zooms_bmask = pe.Node( ApplyTransforms( interpolation="MultiLabel", transforms="identity", args="-u uchar" ), name="zooms_bmask", ) # Regularize with B-Splines bs_filter = pe.Node(BSplineApprox(), n_procs=omp_nthreads, name="bs_filter") bs_filter.interface._always_run = debug bs_filter.inputs.bs_spacing = ( [DEFAULT_LF_ZOOMS_MM, DEFAULT_HF_ZOOMS_MM] if not sloppy else [DEFAULT_ZOOMS_MM] ) bs_filter.inputs.extrapolate = not debug # fmt: off workflow.connect([ (inputnode, readout_time, [(("epi_ref", _pop), "in_file"), (("epi_ref", _pull), "metadata")]), (inputnode, atlas_msk, [("sd_prior", "in_file")]), (inputnode, clip_epi, [(("epi_ref", _pop), "in_file")]), (inputnode, unwarp, [(("epi_ref", _pop), "in_data")]), (inputnode, amask2epi, [("epi_mask", "reference_image")]), (inputnode, zooms_bmask, [("anat_mask", "input_image")]), (inputnode, fixed_masks, [("anat_mask", "in1"), ("anat_mask", "in2")]), (inputnode, anat_dilmsk, [("anat_mask", "in_file")]), (inputnode, warp_dir, [("anat_ref", "fixed_image")]), (inputnode, anat_merge, [("anat_ref", "in1")]), (inputnode, lap_anat, [("anat_ref", "op1")]), (inputnode, find_zooms, [("anat_ref", "in_anat"), (("epi_ref", _pop), "in_epi")]), (inputnode, zooms_field, [(("epi_ref", _pop), "reference_image")]), (inputnode, epi_umask, [("epi_mask", "in1")]), (lap_anat, lap_anat_norm, [("output_image", "in_file")]), (lap_anat_norm, anat_merge, [("out", "in2")]), (epi_umask, moving_masks, [("out_file", "in1"), ("out_file", "in2"), ("out_file", "in3")]), (clip_epi, epi_merge, [("out_file", "in1")]), (clip_epi, lap_epi, [("out_file", "op1")]), (clip_epi, zooms_epi, [("out_file", "in_file")]), (lap_epi, lap_epi_norm, [("output_image", "in_file")]), (lap_epi_norm, epi_merge, [("out", "in2")]), (find_zooms, zooms_epi, [("out", "zooms")]), (atlas_msk, fixed_masks, [("out_mask", "in3")]), (anat_dilmsk, amask2epi, [("out_file", "input_image")]), (amask2epi, epi_umask, [("output_image", "in2")]), (readout_time, warp_dir, [("pe_direction", "pe_dir")]), (warp_dir, syn, [("out", "restrict_deformation")]), (anat_merge, syn, [("out", "fixed_image")]), (fixed_masks, syn, [("out", "fixed_image_masks")]), (epi_merge, syn, [("out", "moving_image")]), (moving_masks, syn, [("out", "moving_image_masks")]), (syn, extract_field, [(("forward_transforms", _pop), "transform")]), (readout_time, extract_field, [("readout_time", "ro_time"), ("pe_direction", "pe_dir")]), (extract_field, zooms_field, [("out_file", "input_image")]), (zooms_field, zooms_bmask, [("output_image", "reference_image")]), (zooms_field, bs_filter, [("output_image", "in_data")]), # Setting a mask ends up over-fitting the field # - it's better to have all those ~zero around. # (zooms_bmask, bs_filter, [("output_image", "in_mask")]), (bs_filter, unwarp, [("out_coeff", "in_coeff")]), (readout_time, unwarp, [("readout_time", "ro_time"), ("pe_direction", "pe_dir")]), (zooms_bmask, outputnode, [("output_image", "fmap_mask")]), (bs_filter, outputnode, [("out_coeff", "fmap_coeff")]), (unwarp, outputnode, [("out_corrected", "fmap_ref"), ("out_field", "fmap"), ("out_warp", "out_warp")]), ]) # fmt: on return workflow
def init_syn_sdc_wf( *, atlas_threshold=3, debug=False, name="syn_sdc_wf", omp_nthreads=1, ): """ Build the *fieldmap-less* susceptibility-distortion estimation workflow. SyN deformation is restricted to the phase-encoding (PE) direction. If no PE direction is specified, anterior-posterior PE is assumed. SyN deformation is also restricted to regions that are expected to have a >3mm (approximately 1 voxel) warp, based on the fieldmap atlas. 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 ---------- atlas_threshold : :obj:`float` Exclude from the registration metric computation areas with average distortions below this threshold (in mm). 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. Inputs ------ 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. epi_mask : :obj:`str` A path to a brain mask corresponding to ``epi_ref``. anat_brain : :obj:`str` A preprocessed, skull-stripped anatomical (T1w or T2w) image. std2anat_xfm : :obj:`str` inverse registration transform of T1w image to MNI template anat2epi_xfm : :obj:`str` transform mapping coordinates from the EPI space to the anatomical space (i.e., the transform to resample anatomical info into EPI space.) Outputs ------- fmap : :obj:`str` The path of the estimated fieldmap. fmap_ref : :obj:`str` The path of an unwarped conversion of files in ``epi_ref``. fmap_coeff : :obj:`str` or :obj:`list` of :obj:`str` The path(s) of the B-Spline coefficients supporting the fieldmap. """ from pkg_resources import resource_filename as pkgrf from packaging.version import parse as parseversion, Version from nipype.interfaces.image import Rescale from niworkflows.interfaces.fixes import ( FixHeaderApplyTransforms as ApplyTransforms, FixHeaderRegistration as Registration, ) from niworkflows.interfaces.nibabel import Binarize from ...utils.misc import front as _pop from ...interfaces.utils import Deoblique, Reoblique from ...interfaces.bspline import ( BSplineApprox, DEFAULT_LF_ZOOMS_MM, DEFAULT_HF_ZOOMS_MM, DEFAULT_ZOOMS_MM, ) from ..ancillary import init_brainextraction_wf ants_version = Registration().version if ants_version and parseversion(ants_version) < Version("2.2.0"): raise RuntimeError( f"Please upgrade ANTs to 2.2 or older ({ants_version} found).") workflow = Workflow(name=name) workflow.__desc__ = f"""\ A deformation field to correct for susceptibility distortions was estimated based on *fMRIPrep*'s *fieldmap-less* approach. The deformation field is that resulting from co-registering the EPI reference to the same-subject T1w-reference with its intensity inverted [@fieldmapless1; @fieldmapless2]. Registration is performed with `antsRegistration` (ANTs {ants_version or "-- version unknown"}), and the process regularized by constraining deformation to be nonzero only along the phase-encoding direction, and modulated with an average fieldmap template [@fieldmapless3]. """ inputnode = pe.Node( niu.IdentityInterface([ "epi_ref", "epi_mask", "anat_brain", "std2anat_xfm", "anat2epi_xfm" ]), name="inputnode", ) outputnode = pe.Node( niu.IdentityInterface(["fmap", "fmap_ref", "fmap_coeff", "fmap_mask"]), name="outputnode", ) invert_t1w = pe.Node(Rescale(invert=True), name="invert_t1w", mem_gb=0.3) anat2epi = pe.Node(ApplyTransforms(interpolation="BSpline"), name="anat2epi", n_procs=omp_nthreads) # Mapping & preparing prior knowledge # Concatenate transform files: # 1) anat -> EPI; 2) MNI -> anat; 3) ATLAS -> MNI transform_list = pe.Node(niu.Merge(3), name="transform_list", mem_gb=DEFAULT_MEMORY_MIN_GB) transform_list.inputs.in3 = pkgrf( "sdcflows", "data/fmap_atlas_2_MNI152NLin2009cAsym_affine.mat") prior2epi = pe.Node( ApplyTransforms( input_image=pkgrf("sdcflows", "data/fmap_atlas.nii.gz")), name="prior2epi", n_procs=omp_nthreads, mem_gb=0.3, ) atlas_msk = pe.Node(Binarize(thresh_low=atlas_threshold), name="atlas_msk") deoblique = pe.Node(Deoblique(), name="deoblique") reoblique = pe.Node(Reoblique(), name="reoblique") # SyN Registration Core syn = pe.Node( Registration( from_file=pkgrf("sdcflows", "data/susceptibility_syn.json")), name="syn", n_procs=omp_nthreads, ) unwarp_ref = pe.Node( ApplyTransforms(interpolation="BSpline"), name="unwarp_ref", ) brainextraction_wf = init_brainextraction_wf() # Extract nonzero component extract_field = pe.Node(niu.Function(function=_extract_field), name="extract_field") # Regularize with B-Splines bs_filter = pe.Node(BSplineApprox(), n_procs=omp_nthreads, name="bs_filter") bs_filter.interface._always_run = debug bs_filter.inputs.bs_spacing = ([DEFAULT_LF_ZOOMS_MM, DEFAULT_HF_ZOOMS_MM] if not debug else [DEFAULT_ZOOMS_MM]) bs_filter.inputs.extrapolate = not debug # fmt: off workflow.connect([ (inputnode, transform_list, [("anat2epi_xfm", "in1"), ("std2anat_xfm", "in2")]), (inputnode, invert_t1w, [("anat_brain", "in_file"), (("epi_ref", _pop), "ref_file")]), (inputnode, anat2epi, [(("epi_ref", _pop), "reference_image"), ("anat2epi_xfm", "transforms")]), (inputnode, deoblique, [(("epi_ref", _pop), "in_epi"), ("epi_mask", "mask_epi")]), (inputnode, reoblique, [(("epi_ref", _pop), "in_epi")]), (inputnode, syn, [(("epi_ref", _warp_dir), "restrict_deformation")]), (inputnode, unwarp_ref, [(("epi_ref", _pop), "reference_image"), (("epi_ref", _pop), "input_image")]), (inputnode, prior2epi, [(("epi_ref", _pop), "reference_image")]), (inputnode, extract_field, [("epi_ref", "epi_meta")]), (invert_t1w, anat2epi, [("out_file", "input_image")]), (transform_list, prior2epi, [("out", "transforms")]), (prior2epi, atlas_msk, [("output_image", "in_file")]), (anat2epi, deoblique, [("output_image", "in_anat")]), (atlas_msk, deoblique, [("out_mask", "mask_anat")]), (deoblique, syn, [("out_epi", "moving_image"), ("out_anat", "fixed_image"), ("mask_epi", "moving_image_masks"), (("mask_anat", _fixed_masks_arg), "fixed_image_masks")]), (syn, extract_field, [("forward_transforms", "in_file")]), (syn, unwarp_ref, [("forward_transforms", "transforms")]), (unwarp_ref, reoblique, [("output_image", "in_plumb")]), (reoblique, brainextraction_wf, [("out_epi", "inputnode.in_file")]), (extract_field, reoblique, [("out", "in_field")]), (reoblique, bs_filter, [("out_field", "in_data")]), (brainextraction_wf, bs_filter, [("outputnode.out_mask", "in_mask")]), (reoblique, outputnode, [("out_epi", "fmap_ref")]), (brainextraction_wf, outputnode, [("outputnode.out_mask", "fmap_mask") ]), (bs_filter, outputnode, [("out_extrapolated" if not debug else "out_field", "fmap"), ("out_coeff", "fmap_coeff")]), ]) # fmt: on return workflow
def init_enhance_and_skullstrip_bold_wf( name='enhance_and_skullstrip_bold_wf', pre_mask=False, omp_nthreads=1): """ This workflow takes in a :abbr:`BOLD (blood-oxygen level-dependant)` :abbr:`fMRI (functional MRI)` average/summary (e.g. a reference image averaging non-steady-state timepoints), 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. Calculate a tentative mask by registering (9-parameters) to *fMRIPrep*'s :abbr:`EPI (echo-planar imaging)` -*boldref* template, which is in MNI space. The tentative mask is obtained by resampling the MNI template's brainmask into *boldref*-space. 2. Binary dilation of the tentative mask with a sphere of 3mm diameter. 3. Run ANTs' ``N4BiasFieldCorrection`` on the input :abbr:`BOLD (blood-oxygen level-dependant)` average, using the mask generated in 1) instead of the internal Otsu thresholding. 4. Calculate a loose mask using FSL's ``bet``, with one mathematical morphology dilation of one iteration and a sphere of 6mm as structuring element. 5. 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. 6. Calculate a mask using AFNI's ``3dAutomask`` after the contrast enhancement of 4). 7. Calculate a final mask as the intersection of 4) and 6). 8. Apply final mask on the enhanced reference. Step 1 can be skipped if the ``pre_mask`` argument is set to ``True`` and a tentative mask is passed in to the workflow throught the ``pre_mask`` Nipype input. .. workflow :: :graph2use: orig :simple_form: yes from fmriprep.workflows.bold.util import init_enhance_and_skullstrip_bold_wf wf = init_enhance_and_skullstrip_bold_wf(omp_nthreads=1) **Parameters** name : str Name of workflow (default: ``enhance_and_skullstrip_bold_wf``) pre_mask : bool Indicates whether the ``pre_mask`` input will be set (and thus, step 1 should be skipped). omp_nthreads : int number of threads available to parallel nodes **Inputs** in_file BOLD image (single volume) pre_mask : bool A tentative brain mask to initialize the workflow (requires ``pre_mask`` parameter set ``True``). **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 .. _N4BiasFieldCorrection: https://hdl.handle.net/10380/3053 """ 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') # Dilate pre_mask pre_dilate = pe.Node(fsl.DilateImage( operation='max', kernel_shape='sphere', kernel_size=3.0, internal_datatype='char'), name='pre_mask_dilate') # Ensure mask's header matches reference's check_hdr = pe.Node(MatchHeader(), name='check_hdr', run_without_submitting=True) # Run N4 normally, force num_threads=1 for stability (images are small, no need for >1) n4_correct = pe.Node(ants.N4BiasFieldCorrection(dimension=3, copy_header=True), name='n4_correct', n_procs=1) # 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 constrast & fix header unifize = pe.Node(afni.Unifize( t2=True, outputtype='NIFTI_GZ', # Default -clfrac is 0.1, 0.4 was too conservative # -rbt because I'm a Jedi AFNI Master (see 3dUnifize's documentation) 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 ANFI'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') # Compute masked brain apply_mask = pe.Node(fsl.ApplyMask(), name='apply_mask') if not pre_mask: bold_template = get_template('fMRIPrep') / 'tpl-fMRIPrep_space-MNI_res-02_boldref.nii.gz' brain_mask = get_template('MNI152NLin2009cAsym') / \ 'tpl-MNI152NLin2009cAsym_space-MNI_res-02_brainmask.nii.gz' # Initialize transforms with antsAI init_aff = pe.Node(AI( fixed_image=str(bold_template), fixed_image_mask=str(brain_mask), metric=('Mattes', 32, 'Regular', 0.2), transform=('Affine', 0.1), search_factor=(20, 0.12), principal_axes=False, convergence=(10, 1e-6, 10), verbose=True), name='init_aff', n_procs=omp_nthreads) # Registration().version may be None if parseversion(Registration().version or '0.0.0') > Version('2.2.0'): init_aff.inputs.search_grid = (40, (0, 40, 40)) # Set up spatial normalization norm = pe.Node(Registration( from_file=pkgr_fn( 'fmriprep.data', 'epi_atlasbased_brainmask.json')), name='norm', n_procs=omp_nthreads) norm.inputs.fixed_image = str(bold_template) map_brainmask = pe.Node( ApplyTransforms(interpolation='MultiLabel', float=True, input_image=str(brain_mask)), name='map_brainmask' ) workflow.connect([ (inputnode, init_aff, [('in_file', 'moving_image')]), (inputnode, map_brainmask, [('in_file', 'reference_image')]), (inputnode, norm, [('in_file', 'moving_image')]), (init_aff, norm, [('output_transform', 'initial_moving_transform')]), (norm, map_brainmask, [ ('reverse_invert_flags', 'invert_transform_flags'), ('reverse_transforms', 'transforms')]), (map_brainmask, pre_dilate, [('output_image', 'in_file')]), ]) else: workflow.connect([ (inputnode, pre_dilate, [('pre_mask', 'in_file')]), ]) workflow.connect([ (inputnode, check_hdr, [('in_file', 'reference')]), (pre_dilate, check_hdr, [('out_file', 'in_file')]), (check_hdr, n4_correct, [('out_file', 'mask_image')]), (inputnode, n4_correct, [('in_file', 'input_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')]), (fixhdr_unifize, apply_mask, [('out_file', 'in_file')]), (combine_masks, apply_mask, [('out_file', 'mask_file')]), (combine_masks, outputnode, [('out_file', 'mask_file')]), (apply_mask, outputnode, [('out_file', 'skull_stripped_file')]), (n4_correct, outputnode, [('output_image', 'bias_corrected_file')]), ]) return workflow
def init_coeff2epi_wf( omp_nthreads, debug=False, write_coeff=False, name="coeff2epi_wf", ): """ Move the field coefficients on to the target (distorted) EPI space. Workflow Graph .. workflow:: :graph2use: orig :simple_form: yes from sdcflows.workflows.apply.registration import init_coeff2epi_wf wf = init_coeff2epi_wf(omp_nthreads=2) Parameters ---------- omp_nthreads : :obj:`int` Maximum number of threads an individual process may use. debug : :obj:`bool` Run fast configurations of registrations. name : :obj:`str` Unique name of this workflow. write_coeff : :obj:`bool` Map coefficients file Inputs ------ target_ref the target EPI reference image target_mask the reference image (skull-stripped) fmap_ref the reference (anatomical) image corresponding to ``fmap`` fmap_mask a brain mask corresponding to ``fmap`` fmap_coeff fieldmap coefficients Outputs ------- fmap_coeff fieldmap coefficients in the space of the target reference EPI target_ref the target reference EPI resampled into the fieldmap reference for quality control purposes. """ from packaging.version import parse as parseversion, Version from niworkflows.interfaces.fixes import FixHeaderRegistration as Registration from ...interfaces.bspline import TransformCoefficients from ...utils.misc import front as _pop workflow = Workflow(name=name) workflow.__desc__ = """\ The estimated *fieldmap* was then aligned with rigid-registration to the target EPI (echo-planar imaging) reference run. The field coefficients were mapped on to the reference EPI using the transform. """ inputnode = pe.Node( niu.IdentityInterface(fields=[ "target_ref", "target_mask", "fmap_ref", "fmap_mask", "fmap_coeff" ]), name="inputnode", ) outputnode = pe.Node( niu.IdentityInterface(fields=["target_ref", "fmap_coeff"]), name="outputnode") # Register the reference of the fieldmap to the reference # of the target image (the one that shall be corrected) ants_settings = pkgrf( "sdcflows", f"data/fmap-any_registration{'_testing' * debug}.json") coregister = pe.Node( Registration( from_file=ants_settings, output_warped_image=True, ), name="coregister", n_procs=omp_nthreads, ) ver = coregister.interface.version or "2.2.0" mask_trait_s = "s" if parseversion(ver) >= Version("2.2.0") else "" # fmt: off workflow.connect([ (inputnode, coregister, [ ("target_ref", "moving_image"), ("fmap_ref", "fixed_image"), ("target_mask", f"moving_image_mask{mask_trait_s}"), ("fmap_mask", f"fixed_image_mask{mask_trait_s}"), ]), (coregister, outputnode, [("warped_image", "target_ref")]), ]) # fmt: on if not write_coeff: return workflow # Map the coefficients into the EPI space map_coeff = pe.Node(TransformCoefficients(), name="map_coeff") map_coeff.interface._always_run = debug # fmt: off workflow.connect([ (inputnode, map_coeff, [("fmap_coeff", "in_coeff"), ("fmap_ref", "fmap_ref")]), (coregister, map_coeff, [(("forward_transforms", _pop), "transform")]), (map_coeff, outputnode, [("out_coeff", "fmap_coeff")]), ]) # fmt: on return workflow