From 209b0ebee8df8c4d6518b38a8306f57a1d4c9670 Mon Sep 17 00:00:00 2001 From: Christian Brodbeck Date: Mon, 26 Oct 2026 13:21:12 -0400 Subject: [PATCH 1/2] BUG: Prefer -head.fif over outer_skin.surf for the head surface Both the coregistration GUI and plot_alignment looked for outer_skin.surf before the subject's own -head.fif, so a curated head surface (such as the fsaverage head packaged with MNE) was never used when a watershed outer skin was also present. --- mne/_freesurfer.py | 2 +- mne/coreg.py | 2 +- mne/tests/test_coreg.py | 3 +++ mne/viz/tests/test_3d.py | 2 ++ 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mne/_freesurfer.py b/mne/_freesurfer.py index dc94f09daf3..49d65157217 100644 --- a/mne/_freesurfer.py +++ b/mne/_freesurfer.py @@ -838,10 +838,10 @@ def _get_head_surface(surf, subject, subjects_dir, bem=None, verbose=None): ] else: try_fnames = [ + op.join(subject_dir, "bem", f"{subject}-head.fif"), op.join(subject_dir, "bem", "outer_skin.surf"), op.join(subject_dir, "bem", "flash", "outer_skin.surf"), op.join(subject_dir, "bem", f"{subject}-head-sparse.fif"), - op.join(subject_dir, "bem", f"{subject}-head.fif"), ] for fname in try_fnames: if op.exists(fname): diff --git a/mne/coreg.py b/mne/coreg.py index 8363fb5fea8..f73b197901c 100644 --- a/mne/coreg.py +++ b/mne/coreg.py @@ -91,9 +91,9 @@ fid_fname_general = os.path.join(bem_dirname, "{head}-fiducials.fif") src_fname = os.path.join(bem_dirname, "{subject}-{spacing}-src.fif") _head_fnames = ( + head_bem_fname, os.path.join(bem_dirname, "outer_skin.surf"), head_sparse_fname, - head_bem_fname, ) _high_res_head_fnames = ( os.path.join(bem_dirname, "{subject}-head-dense.fif"), diff --git a/mne/tests/test_coreg.py b/mne/tests/test_coreg.py index 6dbc5bd7ba3..6e712c4c325 100644 --- a/mne/tests/test_coreg.py +++ b/mne/tests/test_coreg.py @@ -21,6 +21,7 @@ from mne.channels import DigMontage from mne.coreg import ( Coregistration, + _find_head_bem, _is_mri_subject, coregister_fiducials, create_default_subject, @@ -443,6 +444,8 @@ def test_coregistration(scale_mode, ref_scale, grow_hair, fiducials, fid_match): coreg = Coregistration( info, subject=subject, subjects_dir=subjects_dir, fiducials=fiducials ) + # the subject's own head file is preferred over the watershed outer_skin.surf + assert _find_head_bem(subject, subjects_dir).endswith("sample-head.fif") assert np.allclose(coreg._last_parameters, coreg._parameters) assert len(coreg.fiducials.dig) == 3 for dig_point in coreg.fiducials.dig: diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index df767f83897..af9d96c8449 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -795,6 +795,8 @@ def test_plot_alignment_eeg(renderer, evoked): assert "ecog: 1" in log assert "seeg: 1" in log assert "eeg: 10" in log + # the subject's own head file is preferred over the watershed outer_skin.surf + assert "Using sample-head.fif for head surface." in log # check number of actors # 4 surfs (both hemis, skin, skull), 1 ECoG, 1 sEEG, # 5 orig EEG + 1 projected EEG From 80fea8836ed11b9757613acf91a62d22729ba222 Mon Sep 17 00:00:00 2001 From: Christian Brodbeck Date: Mon, 26 Oct 2026 13:23:09 -0400 Subject: [PATCH 2/2] ENH: Install the packaged fsaverage head surface in fetch_fsaverage Both fsaverage zip files ship the watershed outer skin as bem/fsaverage-head.fif, so the smoother head surface packaged with MNE (previously only reachable through create_default_subject, which needs a FreeSurfer install) was never used. This modification copies it over the extracted file when fsaverage-head.fif was missing before the downloads; existing files are left alone, consistent with the no-overwrite rule. --- mne/datasets/_fsaverage/base.py | 20 ++++++++++++++++++++ mne/datasets/tests/test_datasets.py | 18 +++++++++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/mne/datasets/_fsaverage/base.py b/mne/datasets/_fsaverage/base.py index 1bbecb3851d..3106cb32355 100644 --- a/mne/datasets/_fsaverage/base.py +++ b/mne/datasets/_fsaverage/base.py @@ -2,6 +2,7 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. +import shutil from pathlib import Path from ...utils import get_subjects_dir, set_config, verbose_static @@ -50,6 +51,15 @@ def fetch_fsaverage(subjects_dir=None, *, verbose=None): the zip file is downloaded and files are updated. No files will be overwritten. + The head surface ``bem/fsaverage-head.fif`` is the smooth head surface + packaged with MNE-Python (the one :func:`mne.create_default_subject` + installs) rather than the watershed outer skin surface from the zip file. + To update an existing copy of fsaverage, delete the file and call this + function again. + + .. versionchanged:: 1.13 + The packaged head surface is used for ``bem/fsaverage-head.fif``. + .. versionadded:: 0.18 """ # Code used to create the BEM (other files taken from MNE-sample-data): @@ -74,6 +84,10 @@ def fetch_fsaverage(subjects_dir=None, *, verbose=None): subjects_dir = subjects_dir.expanduser().absolute() fs_dir = subjects_dir / "fsaverage" fs_dir.mkdir(parents=True, exist_ok=True) + # Both zip files contain the watershed outer skin as bem/fsaverage-head.fif; + # replace it with the packaged head surface unless the file already existed + head_fname = fs_dir / "bem" / "fsaverage-head.fif" + replace_head = not head_fname.is_file() _manifest_check_download( manifest_path=FSAVERAGE_MANIFEST_PATH / "root.txt", destination=subjects_dir, @@ -86,6 +100,12 @@ def fetch_fsaverage(subjects_dir=None, *, verbose=None): url="https://github.com/mne-tools/mne-data/releases/download/fsaverage-1.0/fsaverage-bem.zip", hash_="b31509cdcf7908af6a83dc5ee8f49fb1", ) + if replace_head: + head_fname.parent.mkdir(exist_ok=True) + shutil.copyfile( + Path(__file__).parents[2] / "data" / "fsaverage" / "fsaverage-head.fif", + head_fname, + ) return fs_dir diff --git a/mne/datasets/tests/test_datasets.py b/mne/datasets/tests/test_datasets.py index 3be699d17e5..1a0eb7220c9 100644 --- a/mne/datasets/tests/test_datasets.py +++ b/mne/datasets/tests/test_datasets.py @@ -82,12 +82,24 @@ def test_datasets_basic(tmp_path, monkeypatch): with pytest.raises(FileNotFoundError, match="as specified by MNE_DAT"): testing.data_path(download=False) - def noop(*args, **kwargs): - return + def fake_download(manifest_path, destination, url, hash_): + for name in manifest_path.read_text().split(): + if not (destination / name).is_file(): # like the real downloader + (destination / name).parent.mkdir(parents=True, exist_ok=True) + (destination / name).write_bytes(b"") - monkeypatch.setattr(mne.datasets._fsaverage.base, "_manifest_check_download", noop) + monkeypatch.setattr( + mne.datasets._fsaverage.base, "_manifest_check_download", fake_download + ) sd_2 = datasets.fetch_fsaverage() assert sd / "fsaverage" == sd_2 + # the packaged head surface is installed, but an existing one is left alone + head = sd_2 / "bem" / "fsaverage-head.fif" + packaged = Path(mne.__file__).parent / "data" / "fsaverage" / "fsaverage-head.fif" + assert head.read_bytes() == packaged.read_bytes() + head.write_bytes(b"") + datasets.fetch_fsaverage() + assert head.read_bytes() == b"" def test_erp_core_fetch_file(tmp_path, monkeypatch):