Stage 10: FreeSurfer anatomical reconstruction

Author: Hadi Zaatiti <hadi.zaatiti@nyu.edu>

What & why

Stage 10 of the KIT general pipeline is the first anatomy stage. For every selected subject with a T1-weighted MRI it runs FreeSurfer’s recon-all -all (skull stripping, white and pial surfaces, cortical parcellation, subcortical segmentation) and then builds the scalp surface surf/lh.seghead with FreeSurfer’s mkheadsurf, the same call MNE’s make_scalp_surfaces makes, and the BEM surfaces (brain, inner skull, outer skull, outer skin) with mri_watershed, the call MNE’s make_watershed_bem makes.

Both products are needed later: the manual MEG-MRI coregistration of stage 12 fits the digitised head shape onto the scalp surface, and the forward model of stage 14 builds the BEM and the source space on the FreeSurfer surfaces. Stage 11 checks every reconstruction before stage 12 may use it.

The stage runs on Linux (workstation, HPC node) or in WSL on Windows: FreeSurfer is a Linux package. On Windows, the Python entry point is started with the neurowaves interpreter and forwards the whole run into the WSL distribution, where only python3 and PyYAML are needed. The MNE environment is not used by this stage.

Subjects without a T1w image are not an error: they are recorded as template_fsaverage (default policy) so that stage 12 coregisters them to the fsaverage template, or reported as an error when freesurfer.missing_anat: error.

What you need to provide

  • One T1w image per subject in the BIDS dataset: sub-<id>/anat/sub-<id>_T1w.nii[.gz] or sub-<id>/ses-<id>/anat/sub-<id>_ses-<id>_T1w.nii[.gz]. Exactly one image is used per subject; several candidates are reported so you can choose (the stage does not pick for you).

  • FreeSurfer 7.x installed on the Linux host or in the WSL distribution, with its license (FS_LICENSE in the environment or freesurfer.license_file in the YAML). See Optional: FreeSurfer for stage 10 on the setup page.

  • Disk space of roughly 1 GB per subject and several hours of CPU time per subject (recon-all takes 4 to 12 hours per subject on one core; the stage runs subjects in parallel when the machine allows it).

Configuration file

freesurfer:
  enabled: true
  missing_anat: fsaverage   # fsaverage = template in stage 12 for subjects without a T1w; error = stop
  subjects_dir: null        # null -> <bids_root>/derivatives/10-freesurfer_reconstruction/subjects
  freesurfer_home: null     # null -> environment or auto-discovery of a Linux install
  license_file: null        # null -> FS_LICENSE
  jobs: null                # null -> auto-plan from CPUs and RAM; a number overrides
  threads_per_subject: null # null -> auto-plan; 1 disables OpenMP
  force: false              # rerun verified subjects
  dry_run: false            # validate discovery and resources, launch nothing
  auto_cw256: false         # retry a field of view > 256 with -cw256 (stage 11 checks the crop)
  scalp_surface: true       # mkheadsurf -> surf/lh.seghead after recon-all
  scalp_threshold: 20       # mkheadsurf -thresh1/-thresh2 (MNE's default)

project and subjects select the BIDS root and the subjects as in every stage; an empty subjects.include processes every BIDS subject and subjects.exclude is always applied.

Resource planning

With jobs and threads_per_subject both null, the stage measures the CPUs and the memory available to the process at launch (Linux affinity and cgroup limits, Slurm allocations) and plans on every machine and run: one CPU and at least 10 % of the RAM (or 2 GiB) are left free; 8 GiB are budgeted for the first concurrent subject and 4 GiB for each further one (FreeSurfer’s system requirements); the remaining CPUs are spread across as many memory-safe subject jobs as possible, and the threads of each job go to recon-all -openmp. A single subject therefore uses most of the machine; a cohort runs several subjects at once. --jobs / --threads or the YAML values override the plan.

How to run

On Linux or HPC, from the repository root (the stage does not import the pipeline package, so it also runs by file path):

python3 pipeline/mne_pipelines/kit_general_pipelines/10-freesurfer_reconstruction.py \
    -c pipeline/mne_pipelines/kit_general_pipelines/pipeline_config_files/<project>_config.yml

On Windows, run the same command with the neurowaves interpreter, or use the VS Code launch entries <project> 10 FreeSurfer reconstruction (T1w, runs in WSL), ... dry run and ... FORCE rerun. The entry point translates the Windows paths (C:\... to /mnt/c/...), starts the run inside the distribution named by FREESURFER_WSL_DISTRO (default Ubuntu-22.04) and streams its output. MEG_DATA must be set in the environment before VS Code starts.

Options:

  • --dry-run: discover the T1w images, plan the resources, write the audit files and launch nothing. Run this first on a new dataset.

  • --force: rerun subjects whose reconstruction is verified complete, without deleting anything (recon-all resumes over the existing folder).

  • --jobs N and --threads N: override the plan.

  • --subjects-dir, --freesurfer-home, --license-file, --bids-root: override the YAML values.

What the script does

  1. Resolve the BIDS root and the subjects, find the T1w of each subject, and classify it: T1w found, no T1w (template_fsaverage or error), several T1w (reported).

  2. Check the existing SUBJECTS_DIR entry of each subject. A subject is complete only when scripts/recon-all.done exists, recon-all.error does not, the last recon-all.log records a successful run, and the required MRI, surface, annotation and statistics files are non-empty. Complete subjects are skipped_complete unless --force; incomplete ones are resumed (recon-all continues from its last finished step).

  3. Plan jobs and threads, then run recon-all -all for the pending subjects, several at a time. The live FreeSurfer output is printed with the subject as prefix and saved in full to the run’s subject log; a quiet job prints a heartbeat every 30 seconds.

  4. If recon-all fails because the field of view exceeds 256 and auto_cw256 is true, back up the oversized mri/orig.mgz under scripts/nw_cw256_backups/ and retry with -cw256 (FreeSurfer crops the conformed image to 256 mm; stage 11 later measures whether the scalp touches the faces of the cropped volume).

  5. After every verified reconstruction, including skipped ones, create surf/lh.seghead with mkheadsurf -subjid <subject> -srcvol T1.mgz -thresh1 20 -thresh2 20 when it is missing or older than mri/T1.mgz. Then, when missing or older than mri/T1.mgz (freesurfer.bem_surfaces: true), run mri_watershed -useSRAS -surf bem/watershed/<subject> mri/T1.mgz bem/watershed/ws.mgz. The last step of MNE’s make_watershed_bem (the T1 volume header written into each surface and the copy to bem/<name>.surf) needs MNE, which the FreeSurfer environment does not have; stage 14 does it the first time it needs bem/inner_skull.surf.

  6. Record each subject in the run summary with the FreeSurfer version of the run and the build stamps found in its recon-all.log, so a subject reconstructed with an older version keeps its true version history; copy the run summary to the stage root; exit with code 1 when any subject failed (the console lists them with the reason).

Outputs

Under <bids_root>/derivatives/10-freesurfer_reconstruction/:

subjects/                            # SUBJECTS_DIR
  sub-<id>/
    mri/, surf/, label/, stats/, scripts/   # recon-all
    surf/lh.seghead                         # scalp surface (mkheadsurf)
    bem/watershed/sub-<id>_{brain,inner_skull,outer_skull,outer_skin}_surface   # mri_watershed
    bem/{brain,inner_skull,outer_skull,outer_skin}.surf   # finalized by stage 14
    bem/sub-<id>-head-{dense,medium,sparse}.fif   # written later by stage 11
freesurfer_summary.csv               # latest run, one row per subject
runs/<run-id>/
  freesurfer_summary.csv
  run_metadata.txt                   # command line, host, Slurm allocation, FreeSurfer version
  config.yml                         # resolved config of the run
  logs/sub-<id>.log                  # full recon-all output per subject

freesurfer_summary.csv has one row per subject: run_id, subject, mri_source (individual | template_fsaverage), t1_path, freesurfer_subject_dir, status (success | skipped_complete | failed | template_fsaverage | missing_t1 | ambiguous_t1 | dry_run), action, exit_code, elapsed_seconds, the log paths, the command, parallel_jobs, threads_per_subject, resource_plan, hostname, slurm_job_id, freesurfer_version, reconstruction_versions, and the scalp surface columns (head_surface, head_surface_status, head_surface_message).

Report

The report’s stage-10 section shows the summary table with the resource plan of the latest run, the FreeSurfer version of each reconstruction, and the subjects that will use the template. On a workstation, re-render the report after a run on a compute node with the report_generation.refresh command of the setup page (the compute node has no Quarto).

Edge cases & safeguards

  • A present T1w with a failed reconstruction is an incomplete individual reconstruction, never a template subject: fix it and rerun stage 10. The template is used only when there is no T1w at all (or when stage 11 gives a use_fsaverage verdict).

  • Interrupted run: start the stage again; recon-all resumes and the summary records the resume.

  • Several T1w images for a subject: ambiguous_t1, the stage lists them and processes nothing for that subject until one is kept (or renamed out of the anat folder).

  • Field of view above 256: recon-all fails with a message about cw256; set auto_cw256: true and rerun, then look at the coverage figure of stage 11 for that subject.

  • Old folder name: reconstructions made before the 2026-10 renumbering in 7-freesurfer_reconstruction/ or 8-freesurfer_reconstruction_script/ are still found by stages 11 to 15 when the new folder does not exist. Rename them with rename_legacy_derivatives.py -c <config> --apply once no recon-all is running.

Troubleshooting

  • ``No module named yaml`` inside WSL: sudo apt install python3-yaml in the distribution.

  • FreeSurfer not found: set freesurfer.freesurfer_home or source SetUpFreeSurfer.sh in the shell that starts the stage; in WSL the stage auto-discovers /usr/local/freesurfer/<version>.

  • License error: set FS_LICENSE or freesurfer.license_file to the license.txt obtained from the FreeSurfer website.

  • Out of memory: lower jobs (each concurrent subject needs several GB) or set threads_per_subject: 1.

  • Slow on a syncing drive: a SUBJECTS_DIR under Box or OneDrive is slow and may corrupt files; set freesurfer.subjects_dir to a local disk and copy the results afterwards, or exclude the folder from syncing.