diff --git a/.circleci/config.yml b/.circleci/config.yml index 30749074d..9a7a329bb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -742,14 +742,22 @@ workflows: - get_regression_data: filters: branches: - ignore: /docs?\/.*/ + ignore: + - /docs?\/.*/ + - /ds005\/.*/ + - /ds054\/.*/ + - /ds210\/.*/ - build_docs: requires: - build filters: branches: - ignore: /tests?\/.*/ + ignore: + - /tests?\/.*/ + - /ds005\/.*/ + - /ds054\/.*/ + - /ds210\/.*/ tags: only: /.*/ @@ -770,7 +778,11 @@ workflows: - get_regression_data filters: branches: - ignore: /docs?\/.*/ + ignore: + - /docs?\/.*/ + - /ds005\/.*/ + - /ds054\/.*/ + - /ds210\/.*/ tags: only: /.*/ @@ -783,6 +795,8 @@ workflows: ignore: - /docs?\/.*/ - /tests?\/.*/ + - /ds054\/.*/ + - /ds210\/.*/ tags: only: /.*/ @@ -795,6 +809,8 @@ workflows: ignore: - /docs?\/.*/ - /tests?\/.*/ + - /ds005\/.*/ + - /ds210\/.*/ tags: only: /.*/ @@ -808,6 +824,8 @@ workflows: ignore: - /docs?\/.*/ - /tests?\/.*/ + - /ds005\/.*/ + - /ds054\/.*/ tags: only: /.*/ diff --git a/.circleci/ds005_outputs.txt b/.circleci/ds005_outputs.txt index 6e9dc34c4..6e5b1a141 100644 --- a/.circleci/ds005_outputs.txt +++ b/.circleci/ds005_outputs.txt @@ -36,8 +36,8 @@ fmriprep/sub-01/func fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_AROMAnoiseICs.csv fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-confounds_regressors.tsv fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-MELODIC_mixing.tsv -fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-preproc_bold.dtseries.json -fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-preproc_bold.dtseries.nii +fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_bold.dtseries.json +fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_bold.dtseries.nii fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_space-fsaverage5_hemi-L.func.gii fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_space-fsaverage5_hemi-R.func.gii fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_space-MNI152NLin2009cAsym_boldref.nii.gz @@ -52,8 +52,8 @@ fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_space-T1w_label-aseg_ds fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-02_AROMAnoiseICs.csv fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-confounds_regressors.tsv fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-MELODIC_mixing.tsv -fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-preproc_bold.dtseries.json -fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-preproc_bold.dtseries.nii +fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-02_bold.dtseries.json +fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-02_bold.dtseries.nii fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-02_space-fsaverage5_hemi-L.func.gii fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-02_space-fsaverage5_hemi-R.func.gii fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-02_space-MNI152NLin2009cAsym_boldref.nii.gz diff --git a/.circleci/ds005_partial_outputs.txt b/.circleci/ds005_partial_outputs.txt index 56cae3e9f..6afee6b3b 100644 --- a/.circleci/ds005_partial_outputs.txt +++ b/.circleci/ds005_partial_outputs.txt @@ -36,8 +36,8 @@ fmriprep/sub-01/func fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_AROMAnoiseICs.csv fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-confounds_regressors.tsv fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-MELODIC_mixing.tsv -fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-preproc_bold.dtseries.json -fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-preproc_bold.dtseries.nii +fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_bold.dtseries.json +fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_bold.dtseries.nii fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_space-fsaverage5_hemi-L.func.gii fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_space-fsaverage5_hemi-R.func.gii fmriprep/sub-01/func/sub-01_task-mixedgamblestask_run-01_space-MNI152NLin2009cAsym_boldref.nii.gz diff --git a/docker/scripts/get_templates.sh b/docker/scripts/get_templates.sh index bd3663668..c55f60e9d 100755 --- a/docker/scripts/get_templates.sh +++ b/docker/scripts/get_templates.sh @@ -2,14 +2,18 @@ MNI_TEMPLATE="https://files.osf.io/v1/resources/fvuh8/providers/osfstorage/580705eb594d9001ed622649" MNI_SHA256="608b1d609255424d51300e189feacd5ec74b04e244628303e802a6c0b0f9d9db" -ASYM_09C_TEMPLATE="https://files.osf.io/v1/resources/fvuh8/providers/osfstorage/580705089ad5a101f17944a9" -ASYM_09C_SHA256="a24699ba0d13f72d0f8934cc211cb80bfd9c9a077b481d9b64295cf5275235a9" +ASYM_09C_TEMPLATE_OLD="https://files.osf.io/v1/resources/fvuh8/providers/osfstorage/580705089ad5a101f17944a9" +ASYM_09C_SHA256_OLD="a24699ba0d13f72d0f8934cc211cb80bfd9c9a077b481d9b64295cf5275235a9" +ASYM_09C_TEMPLATE="https://files.osf.io/v1/resources/fvuh8/providers/osfstorage/5b0dbce20f461a000db8fa3d" +ASYM_09C_SHA256="2851302474359c2c48995155aadb48b861e5dcf87aefda71af8010f671e8ed66" OASIS_TEMPLATE="https://files.osf.io/v1/resources/fvuh8/providers/osfstorage/584123a29ad5a1020913609d" OASIS_SHA256="d87300e91346c16f55baf6f54f5f990bc020b61e8d5df9bcc3abb0cc4b943113" -NKI_TEMPLATE="https://files.osf.io/v1/resources/fvuh8/providers/osfstorage/59cd90f46c613b02b3d79782" -NKI_SHA256="4bba067f6675d15be96b205cb227e18a540673fd7e4577e13feedcef3a6f0ec5" +NKI_TEMPLATE="https://files.osf.io/v1/resources/fvuh8/providers/osfstorage/5bc3fad82aa873001bc5a553" +NKI_SHA256="9c08713d067bcf13baa61b01a9495e526b55d1f148d951da01e082679f076fa9" OASIS_DKT31_TEMPLATE="https://files.osf.io/v1/resources/fvuh8/providers/osfstorage/5b16f17aeca4a80012bd7542" OASIS_DKT31_SHA256="623fa7141712b1a7263331dba16eb069a4443e9640f52556c89d461611478145" +EPI_TEMPLATE="https://files.osf.io/v1/resources/fvuh8/providers/osfstorage/5bc12155ac011000176bff82" +EPI_SHA256="fcd6980ef98c9d7622c6dc2a7747ff51ba3909d98e2a740df9a8265d50920d1b" GET(){ URL=$1; SHA256=$2; @@ -28,6 +32,8 @@ GET(){ set -e echo "Getting MNI template" GET "$MNI_TEMPLATE" "$MNI_SHA256" +echo "Getting (deprecated version of) MNI152NLin2009cAsym template" +GET "$ASYM_09C_TEMPLATE_OLD" "$ASYM_09C_SHA256_OLD" echo "Getting MNI152NLin2009cAsym template" GET "$ASYM_09C_TEMPLATE" "$ASYM_09C_SHA256" echo "Getting OASIS template" @@ -36,4 +42,6 @@ echo "Getting NKI template" GET "$NKI_TEMPLATE" "$NKI_SHA256" echo "Getting OASIS DKT31 template" GET "$OASIS_DKT31_TEMPLATE" "$OASIS_DKT31_SHA256" +echo "Getting fMRIPrep's BOLDref template" +GET "$EPI_TEMPLATE" "$EPI_SHA256" echo "Done!" diff --git a/docs/environment.yml b/docs/environment.yml index 2c92a97dc..24d91fbba 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -29,4 +29,4 @@ dependencies: - svgutils - nitime - nilearn - - niworkflows>=0.4.2 + - niworkflows==0.4.4 diff --git a/fmriprep/__about__.py b/fmriprep/__about__.py index bae53895e..81590f460 100644 --- a/fmriprep/__about__.py +++ b/fmriprep/__about__.py @@ -98,7 +98,7 @@ 'pybids==0.6.5', 'nitime', 'nipype>=1.1.3', - 'niworkflows>=0.4.4', + 'niworkflows==0.4.4', 'statsmodels', 'seaborn', 'indexed_gzip>=0.8.2', diff --git a/fmriprep/data/epi_atlasbased_brainmask.json b/fmriprep/data/epi_atlasbased_brainmask.json new file mode 100644 index 000000000..31fed8bd2 --- /dev/null +++ b/fmriprep/data/epi_atlasbased_brainmask.json @@ -0,0 +1,20 @@ +{ + "winsorize_upper_quantile": 0.98, + "winsorize_lower_quantile": 0.05, + "float": true, + "metric": ["Mattes"], + "metric_weight": [1], + "radius_or_number_of_bins": [64], + "transforms": ["Affine"], + "transform_parameters": [[0.1]], + "number_of_iterations": [[200]], + "convergence_window_size": [10], + "convergence_threshold": [1e-09], + "sampling_strategy": ["Random", "Random"], + "smoothing_sigmas": [[2]], + "sigma_units": ["mm", "mm", "mm"], + "shrink_factors": [[2]], + "sampling_percentage": [0.2], + "use_histogram_matching": [true], + "use_estimate_learning_rate_once": [true] +} diff --git a/fmriprep/interfaces/__init__.py b/fmriprep/interfaces/__init__.py index c0f4bc22b..32cdf5d1f 100644 --- a/fmriprep/interfaces/__init__.py +++ b/fmriprep/interfaces/__init__.py @@ -5,7 +5,7 @@ ReadSidecarJSON, DerivativesDataSink, BIDSDataGrabber, BIDSFreeSurferDir, BIDSInfo ) from .images import ( - IntraModalMerge, ValidateImage, TemplateDimensions, Conform + IntraModalMerge, ValidateImage, TemplateDimensions, Conform, MatchHeader ) from .freesurfer import ( StructuralReference, MakeMidthickness, FSInjectBrainExtracted, diff --git a/fmriprep/interfaces/images.py b/fmriprep/interfaces/images.py index d58db6cef..bf667c2f3 100644 --- a/fmriprep/interfaces/images.py +++ b/fmriprep/interfaces/images.py @@ -479,6 +479,53 @@ def _run_interface(self, runtime): return runtime +class MatchHeaderInputSpec(BaseInterfaceInputSpec): + reference = File(exists=True, mandatory=True, + desc='NIfTI file with reference header') + in_file = File(exists=True, mandatory=True, + desc='NIfTI file which header will be checked') + + +class MatchHeaderOutputSpec(TraitedSpec): + out_file = File(exists=True, desc='NIfTI file with fixed header') + + +class MatchHeader(SimpleInterface): + input_spec = MatchHeaderInputSpec + output_spec = MatchHeaderOutputSpec + + def _run_interface(self, runtime): + refhdr = nb.load(self.inputs.reference).header.copy() + imgnii = nb.load(self.inputs.in_file) + imghdr = imgnii.header.copy() + + imghdr['dim_info'] = refhdr['dim_info'] # dim_info is lost sometimes + + # Set qform + qform = refhdr.get_qform() + qcode = int(refhdr['qform_code']) + if not np.allclose(qform, imghdr.get_qform()): + LOGGER.warning( + 'q-forms of reference and mask are substantially different') + imghdr.set_qform(qform, qcode) + + # Set sform + sform = refhdr.get_sform() + scode = int(refhdr['sform_code']) + if not np.allclose(sform, imghdr.get_sform()): + LOGGER.warning( + 's-forms of reference and mask are substantially different') + imghdr.set_sform(sform, scode) + + out_file = fname_presuffix(self.inputs.in_file, suffix='_hdr', + newpath=runtime.cwd) + + imgnii.__class__(imgnii.get_data(), imghdr.get_best_affine(), + imghdr).to_filename(out_file) + self._results['out_file'] = out_file + return runtime + + def reorient(in_file, newpath=None): """Reorient Nifti files to RAS""" out_file = fname_presuffix(in_file, suffix='_ras', newpath=newpath) diff --git a/fmriprep/workflows/bold/base.py b/fmriprep/workflows/bold/base.py index 60b41b5b8..316905514 100755 --- a/fmriprep/workflows/bold/base.py +++ b/fmriprep/workflows/bold/base.py @@ -394,9 +394,8 @@ def init_func_preproc_wf(bold_file, ignore, freesurfer, ]), ]) - # The first reference uses T2 contrast enhancement - bold_reference_wf = init_bold_reference_wf( - omp_nthreads=omp_nthreads, enhance_t2=True) + # Generate a tentative boldref + bold_reference_wf = init_bold_reference_wf(omp_nthreads=omp_nthreads) # Top-level BOLD splitter bold_split = pe.Node(FSLSplit(dimension='t'), name='bold_split', @@ -924,7 +923,7 @@ def init_func_derivatives_wf(output_dir, output_spaces, template, freesurfer, CiftiNameSource(), iterfield=['variant'], name='name_cifti', mem_gb=DEFAULT_MEMORY_MIN_GB, run_without_submitting=True) cifti_bolds = pe.MapNode( - DerivativesDataSink(base_directory=output_dir, desc='preproc', compress=False), + DerivativesDataSink(base_directory=output_dir, compress=False), iterfield=['in_file', 'suffix'], name='cifti_bolds', run_without_submitting=True, mem_gb=DEFAULT_MEMORY_MIN_GB) cifti_key = pe.MapNode(DerivativesDataSink( diff --git a/fmriprep/workflows/bold/registration.py b/fmriprep/workflows/bold/registration.py index 861a3b6f2..55392443a 100644 --- a/fmriprep/workflows/bold/registration.py +++ b/fmriprep/workflows/bold/registration.py @@ -324,7 +324,7 @@ def init_bold_t1_trans_wf(freesurfer, mem_gb, omp_nthreads, use_fieldwarp=False, merge = pe.Node(Merge(compress=use_compression), name='merge', mem_gb=mem_gb) # Generate a reference on the target T1w space - gen_final_ref = init_bold_reference_wf(omp_nthreads) + gen_final_ref = init_bold_reference_wf(omp_nthreads, pre_mask=True) workflow.connect([ (inputnode, merge_xforms, [('itk_bold_to_t1', 'in1')]), @@ -334,6 +334,7 @@ def init_bold_t1_trans_wf(freesurfer, mem_gb, omp_nthreads, use_fieldwarp=False, (gen_ref, bold_to_t1w_transform, [('out_file', 'reference_image')]), (bold_to_t1w_transform, merge, [('out_files', 'in_files')]), (merge, gen_final_ref, [('out_file', 'inputnode.bold_file')]), + (mask_t1w_tfm, gen_final_ref, [('output_image', 'inputnode.bold_mask')]), (merge, outputnode, [('out_file', 'bold_t1')]), (gen_final_ref, outputnode, [('outputnode.ref_image', 'bold_t1_ref')]), ]) diff --git a/fmriprep/workflows/bold/resampling.py b/fmriprep/workflows/bold/resampling.py index 6c19c342e..cc40acfb0 100644 --- a/fmriprep/workflows/bold/resampling.py +++ b/fmriprep/workflows/bold/resampling.py @@ -301,7 +301,8 @@ def _aslist(in_value): mem_gb=mem_gb * 3) # Generate a reference on the target T1w space - gen_final_ref = init_bold_reference_wf(omp_nthreads) + gen_final_ref = init_bold_reference_wf( + omp_nthreads=omp_nthreads, pre_mask=True) workflow.connect([ (inputnode, merge_xforms, [('t1_2_mni_forward_transform', 'in1'), @@ -311,6 +312,7 @@ def _aslist(in_value): (inputnode, bold_to_mni_transform, [('bold_split', 'input_image')]), (bold_to_mni_transform, merge, [('out_files', 'in_files')]), (merge, gen_final_ref, [('out_file', 'inputnode.bold_file')]), + (mask_mni_tfm, gen_final_ref, [('output_image', 'inputnode.bold_mask')]), (merge, outputnode, [('out_file', 'bold_mni')]), (gen_final_ref, outputnode, [('outputnode.ref_image', 'bold_mni_ref')]), ]) diff --git a/fmriprep/workflows/bold/tests/test_util.py b/fmriprep/workflows/bold/tests/test_util.py index ffa742ea3..aca5858e6 100755 --- a/fmriprep/workflows/bold/tests/test_util.py +++ b/fmriprep/workflows/bold/tests/test_util.py @@ -58,7 +58,7 @@ def symmetric_overlap(img1, img2): ) ]) def test_masking(input_fname, expected_fname): - bold_reference_wf = init_bold_reference_wf(omp_nthreads=1, enhance_t2=True) + bold_reference_wf = init_bold_reference_wf(omp_nthreads=1) bold_reference_wf.inputs.inputnode.bold_file = input_fname # Reconstruct base_fname from above diff --git a/fmriprep/workflows/bold/util.py b/fmriprep/workflows/bold/util.py index de7e3238a..652f8f499 100644 --- a/fmriprep/workflows/bold/util.py +++ b/fmriprep/workflows/bold/util.py @@ -10,22 +10,30 @@ .. autofunction:: init_skullstrip_bold_wf """ +from packaging.version import parse as parseversion, Version +from pkg_resources import resource_filename as pkgr_fn + from nipype.pipeline import engine as pe from nipype.interfaces import utility as niu, fsl, afni, ants -from niworkflows.interfaces.utils import CopyXForm +from niworkflows.data import get_template +from niworkflows.interfaces.ants import AI +from niworkflows.interfaces.fixes import ( + FixHeaderRegistration as Registration, + FixHeaderApplyTransforms as ApplyTransforms, +) from niworkflows.interfaces.masks import SimpleShowMaskRPT from niworkflows.interfaces.registration import EstimateReferenceImage +from niworkflows.interfaces.utils import CopyXForm from ...engine import Workflow -from ...interfaces.nilearn import MaskEPI -from ...interfaces import ValidateImage +from ...interfaces import ValidateImage, MatchHeader DEFAULT_MEMORY_MIN_GB = 0.01 -def init_bold_reference_wf(omp_nthreads, bold_file=None, name='bold_reference_wf', - gen_report=False, enhance_t2=False): +def init_bold_reference_wf(omp_nthreads, bold_file=None, pre_mask=False, + name='bold_reference_wf', gen_report=False): """ This workflow generates reference BOLD images for a series @@ -58,6 +66,9 @@ def init_bold_reference_wf(omp_nthreads, bold_file=None, name='bold_reference_wf bold_file BOLD series NIfTI file + bold_mask : bool + A tentative brain mask to initialize the workflow (requires ``pre_mask`` + parameter set ``True``). **Outputs** @@ -87,7 +98,7 @@ def init_bold_reference_wf(omp_nthreads, bold_file=None, name='bold_reference_wf First, a reference volume and its skull-stripped version were generated using a custom methodology of *fMRIPrep*. """ - inputnode = pe.Node(niu.IdentityInterface(fields=['bold_file', 'sbref_file']), + inputnode = pe.Node(niu.IdentityInterface(fields=['bold_file', 'sbref_file', 'bold_mask']), name='inputnode') outputnode = pe.Node( niu.IdentityInterface(fields=['bold_file', 'raw_ref_image', 'skip_vols', 'ref_image', @@ -105,10 +116,11 @@ def init_bold_reference_wf(omp_nthreads, bold_file=None, name='bold_reference_wf mem_gb=1) # OE: 128x128x128x50 * 64 / 8 ~ 900MB. # Re-run validation; no effect if no sbref; otherwise apply same validation to sbref as bold validate_ref = pe.Node(ValidateImage(), name='validate_ref', mem_gb=DEFAULT_MEMORY_MIN_GB) - enhance_and_skullstrip_bold_wf = init_enhance_and_skullstrip_bold_wf(omp_nthreads=omp_nthreads, - enhance_t2=enhance_t2) + enhance_and_skullstrip_bold_wf = init_enhance_and_skullstrip_bold_wf( + omp_nthreads=omp_nthreads, pre_mask=pre_mask) workflow.connect([ + (inputnode, enhance_and_skullstrip_bold_wf, [('bold_mask', 'inputnode.pre_mask')]), (inputnode, validate, [('bold_file', 'in_file')]), (inputnode, gen_ref, [('sbref_file', 'sbref_file')]), (validate, gen_ref, [('out_file', 'in_file')]), @@ -136,8 +148,10 @@ def init_bold_reference_wf(omp_nthreads, bold_file=None, name='bold_reference_wf return workflow -def init_enhance_and_skullstrip_bold_wf(name='enhance_and_skullstrip_bold_wf', - omp_nthreads=1, enhance_t2=False): +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 @@ -148,21 +162,28 @@ def init_enhance_and_skullstrip_bold_wf(name='enhance_and_skullstrip_bold_wf', Steps of this workflow are: - - 1. Calculate a conservative mask using Nilearn's ``create_epi_mask``. - 2. Run ANTs' ``N4BiasFieldCorrection`` on the input + 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. - 3. Calculate a loose mask using FSL's ``bet``, with one mathematical morphology + 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. - 4. Mask the :abbr:`INU (intensity non-uniformity)`-corrected image + 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. - 5. Calculate a mask using AFNI's ``3dAutomask`` after the contrast + 6. Calculate a mask using AFNI's ``3dAutomask`` after the contrast enhancement of 4). - 6. Calculate a final mask as the intersection of 3) and 5). - 7. Apply final mask on the enhanced reference. + 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 :: @@ -175,17 +196,19 @@ def init_enhance_and_skullstrip_bold_wf(name='enhance_and_skullstrip_bold_wf', **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 - enhance_t2 : bool - perform logarithmic transform of input BOLD image to improve contrast - before calculating the preliminary mask - **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** @@ -202,14 +225,19 @@ def init_enhance_and_skullstrip_bold_wf(name='enhance_and_skullstrip_bold_wf', .. _N4BiasFieldCorrection: https://hdl.handle.net/10380/3053 """ workflow = Workflow(name=name) - inputnode = pe.Node(niu.IdentityInterface(fields=['in_file']), + 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') - # Create a loose mask to avoid N4 internal's Otsu mask - n4_mask = pe.Node(MaskEPI(upper_cutoff=0.75, enhance_t2=enhance_t2, opening=1, - no_sanitize=True), name='n4_mask') + # 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), @@ -245,12 +273,61 @@ def init_enhance_and_skullstrip_bold_wf(name='enhance_and_skullstrip_bold_wf', # 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) + + 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( + '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, n4_mask, [('in_file', 'in_files')]), + (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_mask, n4_correct, [('out_mask', 'mask_image')]), (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')]), diff --git a/fmriprep/workflows/fieldmap/unwarp.py b/fmriprep/workflows/fieldmap/unwarp.py index 188572eda..cd0e5fcfb 100644 --- a/fmriprep/workflows/fieldmap/unwarp.py +++ b/fmriprep/workflows/fieldmap/unwarp.py @@ -152,7 +152,8 @@ def init_sdc_unwarp_wf(omp_nthreads, fmap_demean, debug, name='sdc_unwarp_wf'): apply_fov_mask = pe.Node(fsl.ApplyMask(), name="apply_fov_mask") - enhance_and_skullstrip_bold_wf = init_enhance_and_skullstrip_bold_wf(omp_nthreads=omp_nthreads) + enhance_and_skullstrip_bold_wf = init_enhance_and_skullstrip_bold_wf(omp_nthreads=omp_nthreads, + pre_mask=True) workflow.connect([ (inputnode, fmap2ref_reg, [('fmap_ref', 'moving_image')]), @@ -187,6 +188,8 @@ def init_sdc_unwarp_wf(omp_nthreads, fmap_demean, debug, name='sdc_unwarp_wf'): (fmap_fov2ref_apply, apply_fov_mask, [('output_image', 'mask_file')]), (unwarp_reference, apply_fov_mask, [('output_image', 'in_file')]), (apply_fov_mask, enhance_and_skullstrip_bold_wf, [('out_file', 'inputnode.in_file')]), + (fmap_mask2ref_apply, enhance_and_skullstrip_bold_wf, + [('output_image', 'inputnode.pre_mask')]), (apply_fov_mask, outputnode, [('out_file', 'out_reference')]), (enhance_and_skullstrip_bold_wf, outputnode, [ ('outputnode.mask_file', 'out_mask'), diff --git a/requirements.txt b/requirements.txt index c184845fd..317e6899b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -niworkflows>=0.4.4 +niworkflows==0.4.4 grabbit==0.2.3 pybids==0.6.5 versioneer diff --git a/scripts/generate_reference_mask.py b/scripts/generate_reference_mask.py index 4084e9695..9f9745aa0 100755 --- a/scripts/generate_reference_mask.py +++ b/scripts/generate_reference_mask.py @@ -15,8 +15,7 @@ def sink_mask_file(in_file, orig_file, out_dir): def init_main_wf(bold_file, out_dir, base_dir=None, name='main_wf'): - wf = init_bold_reference_wf(enhance_t2=True, - omp_nthreads=4, + wf = init_bold_reference_wf(omp_nthreads=4, name=name) wf.base_dir = base_dir wf.inputs.inputnode.bold_file = bold_file @@ -27,7 +26,7 @@ def init_main_wf(bold_file, out_dir, base_dir=None, name='main_wf'): sink.inputs.orig_file = bold_file wf.connect([ (wf.get_node('outputnode'), sink, [('bold_mask', 'in_file')]), - ]) + ]) return wf