Passing in precomputed brain mask affects T1 preprocessing in fMRIPrep

Summary of what happened:

For my dataset I have found that ANTspyNET consistently produces a good/clean brain mask compared to fmriprep (antsBrainExtraction x Freesurfeer method). The default fmriprep method often outputs brain mask/segmentations that include the skull/non-brain tissues. As a result, I have been feeding my ANTspyNET mask as input via the --derivatives flag. While this works and the resulting outputs look good (see attached screenshots: decent tissue segmentations, good BOLD-T1 coreg etc), I noticed that the preprocessed anatomical T1 gets skull stripped in the process. Is this a sign of something going wrong under the hood?

Would greatly appreciate your thoughts, thank you!

Command used (and if a helper script was used, a link to the helper script or the command generated):

CMD="apptainer run --cleanenv \
      -B /path_to_multiband_rsfMRI_data:/data \
      ${CONTAINER} \
      /data/Outputs/BIDS \
      /data/Outputs/fmriprep \
      participant \
      --participant-label 01 \
      --session-label 01 \
      --fs-license-file /data/Software/license.txt \
      --force no-bbr \
      --ignore slicetiming \
      --output-spaces func anat MNI152NLin2009cAsym:res-native \
      --skull-strip-t1w skip \
      --derivatives fmriprep=/data/Outputs/fmriprep \
      --use-syn-sdc \
      -w /data/Outputs/fmriprep -vvv"

Version:

25.2.3

Environment (Docker, Singularity / Apptainer, custom installation):

Apptainer

Data formatted according to a validatable standard? Please provide the output of the validator:

251125-09:27:55,505 cli INFO:
	 Telemetry system to collect crashes and errors is enabled - thanks for your feedback!. Use option ``--notrack`` to opt out.
251125-09:27:59,596 cli DEBUG:
	 Initializing BIDS Layout
251125-09:28:45,171 nipype.workflow IMPORTANT:
	 Running fMRIPrep version 25.2.3

         License NOTICE ##################################################
         fMRIPrep 25.2.3
         Copyright The NiPreps Developers.

Ok, I think I might have misunderstood how fmriprep handles precomputed masks - please correct me if I’m wrong here :pleading_face:

According to smriprep.workflows.anatomical — smriprep 0.18.1.dev76 documentation

The source code implies that if a precomputed mask is passed in, the apply_mask node/wf will multiply this mask against the denoised T1 scan, producing a skull stripped brain (native space). This seems intentional and not a bug? (please correct me)

    # Stage 2: INU correction and masking
    # We always need to generate t1w_brain; how to do that depends on whether we have
    # a pre-corrected T1w or precomputed mask, or are given an already masked image
    if not have_mask:
        LOGGER.info('ANAT Stage 2: Preparing brain extraction workflow')
        if skull_strip_mode == 'auto':
            run_skull_strip = not all(_is_skull_stripped(img) for img in t1w)
        else:
            run_skull_strip = {'force': True, 'skip': False}[skull_strip_mode]

        # Brain extraction
        …….
        ……………………..
        ……………………..
        ……………………..
        ……………………..
        …………………….. #### [nobrain note: HERE —> this block is where it picks up my mask and uses it to preprocess my t1 (bias correct plus skull strip via apply_mask ?) 
    else:
        LOGGER.info('ANAT Found brain mask')
        desc += """\
A pre-computed brain mask was provided as input and used throughout the workflow.
"""
        t1w_buffer.inputs.t1w_mask = precomputed['t1w_mask']
        # If we have a mask, always apply it
        apply_mask = pe.Node(ApplyMask(in_mask=precomputed['t1w_mask']), name='apply_mask')
        workflow.connect([(anat_validate, apply_mask, [('out_file', 'in_file')])])
        # Run N4 if it hasn't been pre-run
        if not have_t1w:
            LOGGER.info('ANAT Skipping skull-strip, INU-correction only')
            n4_only_wf = init_n4_only_wf(
                omp_nthreads=omp_nthreads,
                atropos_use_random_seed=not skull_strip_fixed_seed,
            )
            workflow.connect([
                (apply_mask, n4_only_wf, [('out_file', 'inputnode.in_files')]),
                (n4_only_wf, t1w_buffer, [
                    (('outputnode.bias_corrected', _pop), 't1w_preproc'),
                    (('outputnode.out_file', _pop), 't1w_brain'),
                ]),
            ])  # fmt:skip
        else:
            LOGGER.info('ANAT Skipping Stage 2')
            workflow.connect([(apply_mask, t1w_buffer, [('out_file', 't1w_brain')])])
        workflow.connect([(refined_buffer, outputnode, [('t1w_mask', 't1w_mask')])])

When I checked the report.rst file in my anat_fit_wf/apply_mask/_report directory I saw this

Node: sub_01_ses_01_wf (anat_fit_wf (apply_mask (nibabel)
======================================================================


 Hierarchy : fmriprep_25_2_wf.sub_01_ses_01_wf.anat_fit_wf.apply_mask
 Exec ID : apply_mask


Original Inputs
---------------


* in_file : /data/Outputs/fmriprep/fmriprep_25_2_wf/sub_01_ses_01_wf/anat_fit_wf/anat_template_wf/denoise/mapflow/_denoise0/sub-01_ses-01_T1w_noise_corrected.nii
* in_mask : /data/Outputs/fmriprep/sub-01/ses-01/anat/sub-01_ses-01_desc-brain_mask.nii.gz
* threshold : 0.5


Execution Inputs
----------------


* in_file : /data/Outputs/fmriprep/fmriprep_25_2_wf/sub_01_ses_01_wf/anat_fit_wf/anat_template_wf/denoise/mapflow/_denoise0/sub-01_ses-01_T1w_noise_corrected.nii
* in_mask : /data/Outputs/fmriprep/sub-01/ses-01/anat/sub-01_ses-01_desc-brain_mask.nii.gz
* threshold : 0.5

Execution Outputs
-----------------

* out_file : /data/Outputs/fmriprep/fmriprep_25_2_wf/sub_01_ses_01_wf/anat_fit_wf/apply_mask/sub-01_ses-01_T1w_noise_corrected_masked.nii <— this guy is skull stripped and used for all downstream steps
  • Is my understanding correct?