Skip to content

Commit a5bf268

Browse files
vferatpre-commit-ci[bot]wmvanvlietlarsoner
authored
Fixes make_scalp_surfaces (#13024)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Marijn van Vliet <w.m.vanvliet@gmail.com> Co-authored-by: Eric Larson <larson.eric.d@gmail.com>
1 parent b515c20 commit a5bf268

5 files changed

Lines changed: 197 additions & 56 deletions

File tree

doc/changes/dev/13024.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix bug where outdated ``seghead.mgz`` files were reused in :func:`mne.bem.make_scalp_surfaces`. Add a new parameter ``reuse_seghead`` to control whether to reuse existing ``seghead.mgz`` files, by `Victor Ferat`_.

mne/bem.py

Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2349,6 +2349,7 @@ def make_scalp_surfaces(
23492349
force=True,
23502350
overwrite=False,
23512351
no_decimate=False,
2352+
reuse_seghead=False,
23522353
*,
23532354
threshold=20,
23542355
mri="T1.mgz",
@@ -2373,6 +2374,12 @@ def make_scalp_surfaces(
23732374
Disable the "medium" and "sparse" decimations. In this case, only
23742375
a "dense" surface will be generated. Defaults to ``False``, i.e.,
23752376
create surfaces for all three types of decimations.
2377+
reuse_seghead : bool
2378+
Whether to reuse existing head segmentation files. If ``True``,
2379+
the existing files will be used if they exist. If ``False``
2380+
(default), the head segmentation will be recomputed.
2381+
2382+
.. versionadded:: 1.12
23762383
threshold : int
23772384
The threshold to use with the MRI in the call to ``mkheadsurf``.
23782385
The default is ``20``.
@@ -2397,30 +2404,58 @@ def make_scalp_surfaces(
23972404
if mri == "T1.mgz":
23982405
mri = mri if (subj_path / "mri" / mri).exists() else "T1"
23992406

2400-
logger.info("1. Creating a dense scalp tessellation with mkheadsurf...")
2407+
threshold = _ensure_int(threshold, "threshold")
24012408

2402-
def check_seghead(surf_path=subj_path / "surf"):
2403-
surf = None
2404-
for k in ["lh.seghead", "lh.smseghead"]:
2405-
this_surf = surf_path / k
2406-
if this_surf.exists():
2407-
surf = this_surf
2408-
break
2409-
return surf
2409+
# Check for existing files
2410+
seghead_mgz_path = subj_path / "mri" / "seghead.mgz"
2411+
seghead_surf_path = subj_path / "surf" / "lh.seghead"
2412+
smseghead_surf_path = subj_path / "surf" / "lh.smseghead"
24102413

2411-
my_seghead = check_seghead()
2412-
threshold = _ensure_int(threshold, "threshold")
2413-
if my_seghead is None:
2414+
bem_dir = subjects_dir / subject / "bem"
2415+
fname_template = bem_dir / (f"{subject}-head-{{}}.fif")
2416+
dense_fname = str(fname_template).format("dense")
2417+
_check_file(dense_fname, overwrite)
2418+
2419+
for level in _tri_levels:
2420+
dec_fname = str(fname_template).format(level)
2421+
if overwrite:
2422+
if os.path.exists(dec_fname):
2423+
logger.info(f"Removing previously existing {dec_fname}.")
2424+
os.remove(dec_fname)
2425+
else:
2426+
if no_decimate:
2427+
if os.path.exists(dec_fname):
2428+
raise OSError(
2429+
f"Trying to generate new scalp surfaces"
2430+
f"but {dec_fname} already exists."
2431+
f"To avoid mixing different scalp surface solutions, "
2432+
f"delete this file or use overwrite to automatically delete it."
2433+
)
2434+
else:
2435+
_check_file(dec_fname, overwrite)
2436+
2437+
_check_freesurfer_home()
2438+
2439+
if reuse_seghead:
2440+
if seghead_surf_path.exists():
2441+
surf = seghead_surf_path
2442+
elif smseghead_surf_path.exists():
2443+
surf = smseghead_surf_path
2444+
else:
2445+
raise ValueError(
2446+
"No existing scalp surface found. Please check your subject's surf "
2447+
"folder or set reuse_seghead to False to recompute the surfaces."
2448+
)
2449+
logger.info(f"1. Using existing scalp tessellation {surf} ...")
2450+
else:
2451+
_check_file(seghead_mgz_path, overwrite)
2452+
_check_file(seghead_surf_path, overwrite)
2453+
_check_file(smseghead_surf_path, overwrite)
2454+
logger.info("1. Creating a dense scalp tessellation with mkheadsurf...")
24142455
this_env = deepcopy(os.environ)
24152456
this_env["SUBJECTS_DIR"] = str(subjects_dir)
24162457
this_env["SUBJECT"] = subject
24172458
this_env["subjdir"] = str(subj_path)
2418-
if "FREESURFER_HOME" not in this_env:
2419-
raise RuntimeError(
2420-
"The FreeSurfer environment needs to be set up to use "
2421-
"make_scalp_surfaces to create the outer skin surface "
2422-
"lh.seghead"
2423-
)
24242459
run_subprocess(
24252460
[
24262461
"mkheadsurf",
@@ -2435,18 +2470,16 @@ def check_seghead(surf_path=subj_path / "surf"):
24352470
],
24362471
env=this_env,
24372472
)
2473+
if os.path.exists(seghead_surf_path):
2474+
surf = seghead_surf_path
2475+
elif os.path.exists(smseghead_surf_path):
2476+
surf = smseghead_surf_path
2477+
else:
2478+
raise ValueError("mkheadsurf did not produce the standard output file.")
24382479

2439-
surf = check_seghead()
2440-
if surf is None:
2441-
raise RuntimeError("mkheadsurf did not produce the standard output file.")
2442-
2443-
bem_dir = subjects_dir / subject / "bem"
2444-
if not bem_dir.is_dir():
2445-
os.mkdir(bem_dir)
2446-
fname_template = bem_dir / (f"{subject}-head-{{}}.fif")
2447-
dense_fname = str(fname_template).format("dense")
24482480
logger.info(f"2. Creating {dense_fname} ...")
2449-
_check_file(dense_fname, overwrite)
2481+
bem_dir.mkdir(exist_ok=True)
2482+
24502483
# Helpful message if we get a topology error
24512484
msg = (
24522485
"\n\nConsider using pymeshfix directly to fix the mesh, or --force "
@@ -2471,7 +2504,6 @@ def check_seghead(surf_path=subj_path / "surf"):
24712504
)
24722505
dec_fname = str(fname_template).format(level)
24732506
logger.info(f"{ii}.2 Creating {dec_fname}")
2474-
_check_file(dec_fname, overwrite)
24752507
dec_surf = _surfaces_to_bem(
24762508
[dict(rr=points, tris=tris)],
24772509
[FIFF.FIFFV_BEM_SURF_ID_HEAD],

mne/commands/mne_make_scalp_surfaces.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ def run():
7373
help="Disable medium and sparse decimations (dense only)",
7474
action="store_true",
7575
)
76+
parser.add_option(
77+
"-r",
78+
"--reuse-seghead",
79+
dest="reuse_seghead",
80+
action="store_true",
81+
help="Whether to reuse existing head segmentation files.",
82+
)
7683
_add_verbose_flag(parser)
7784
options, args = parser.parse_args()
7885

@@ -89,6 +96,7 @@ def run():
8996
no_decimate=options.no_decimate,
9097
threshold=options.threshold,
9198
mri=options.mri,
99+
reuse_seghead=options.reuse_seghead,
92100
verbose=options.verbose,
93101
)
94102

mne/commands/tests/test_commands.py

Lines changed: 102 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -170,51 +170,129 @@ def test_kit2fiff():
170170
check_usage(mne_kit2fiff, force_help=True)
171171

172172

173-
@pytest.mark.slowtest
174173
@pytest.mark.ultraslowtest
175174
@testing.requires_testing_data
175+
@requires_freesurfer("mkheadsurf")
176176
def test_make_scalp_surfaces(tmp_path, monkeypatch):
177177
"""Test mne make_scalp_surfaces."""
178178
pytest.importorskip("nibabel")
179179
pytest.importorskip("pyvista")
180180
check_usage(mne_make_scalp_surfaces)
181181
has = "SUBJECTS_DIR" in os.environ
182-
# Copy necessary files to avoid FreeSurfer call
183-
tempdir = str(tmp_path)
184-
surf_path = op.join(subjects_dir, "sample", "surf")
185-
surf_path_new = op.join(tempdir, "sample", "surf")
186-
os.mkdir(op.join(tempdir, "sample"))
187-
os.mkdir(surf_path_new)
188-
subj_dir = op.join(tempdir, "sample", "bem")
189-
os.mkdir(subj_dir)
182+
freesurfer_home = os.environ.get("FREESURFER_HOME")
190183

191-
cmd = ("-s", "sample", "--subjects-dir", tempdir)
192184
monkeypatch.setattr(
193185
mne.bem,
194186
"decimate_surface",
195187
lambda points, triangles, n_triangles: (points, triangles),
196188
)
197-
dense_fname = op.join(subj_dir, "sample-head-dense.fif")
198-
medium_fname = op.join(subj_dir, "sample-head-medium.fif")
189+
190+
tempdir = str(tmp_path)
191+
t1_path = op.join(subjects_dir, "sample", "mri", "T1.mgz")
192+
t1_path_new = op.join(tempdir, "sample", "mri", "T1.mgz")
193+
194+
headseg_path = op.join(subjects_dir, "sample", "mri", "seghead.mgz")
195+
headseg_path_new = op.join(tempdir, "sample", "mri", "seghead.mgz")
196+
197+
surf_path = op.join(subjects_dir, "sample", "surf", "lh.seghead")
198+
surf_path_new = op.join(tempdir, "sample", "surf", "lh.seghead")
199+
200+
dense_fname = op.join(tempdir, "sample", "bem", "sample-head-dense.fif")
201+
medium_fname = op.join(tempdir, "sample", "bem", "sample-head-medium.fif")
202+
sparse_fname = op.join(tempdir, "sample", "bem", "sample-head-sparse.fif")
203+
204+
os.makedirs(op.join(tempdir, "sample", "mri"), exist_ok=True)
205+
os.makedirs(op.join(tempdir, "sample", "surf"), exist_ok=True)
206+
207+
shutil.copy(t1_path, t1_path_new)
208+
shutil.copy(headseg_path, headseg_path_new)
209+
shutil.copy(surf_path, surf_path_new)
210+
211+
cmd = (
212+
"-s",
213+
"sample",
214+
"--subjects-dir",
215+
tempdir,
216+
"--no-decimate",
217+
"--reuse-seghead",
218+
)
199219
with ArgvSetter(cmd, disable_stdout=False, disable_stderr=False):
200220
monkeypatch.delenv("FREESURFER_HOME", raising=False)
201-
with pytest.raises(RuntimeError, match="The FreeSurfer environ"):
221+
with pytest.raises(RuntimeError, match="The FREESURFER_HOME environment"):
202222
mne_make_scalp_surfaces.run()
203-
shutil.copy(op.join(surf_path, "lh.seghead"), surf_path_new)
204-
monkeypatch.setenv("FREESURFER_HOME", tempdir)
223+
224+
monkeypatch.setenv("FREESURFER_HOME", freesurfer_home)
205225
mne_make_scalp_surfaces.run()
226+
227+
assert op.isfile(headseg_path_new)
228+
assert op.isfile(surf_path_new)
229+
assert op.isfile(dense_fname)
230+
assert not op.isfile(medium_fname)
231+
assert not op.isfile(sparse_fname)
232+
233+
# actually check the outputs
234+
head_py = read_bem_surfaces(dense_fname)
235+
assert_equal(len(head_py), 1)
236+
head_py = head_py[0]
237+
head_c = read_bem_surfaces(
238+
op.join(subjects_dir, "sample", "bem", "sample-head-dense.fif")
239+
)[0]
240+
assert_allclose(head_py["rr"], head_c["rr"])
241+
242+
cmd = ("-s", "sample", "--subjects-dir", tempdir)
243+
with ArgvSetter(cmd, disable_stdout=False, disable_stderr=False):
244+
with pytest.raises(OSError, match="use --overwrite to overwrite it"):
245+
mne_make_scalp_surfaces.run()
246+
247+
cmd = ("-s", "sample", "--subjects-dir", tempdir, "--overwrite")
248+
with ArgvSetter(cmd, disable_stdout=False, disable_stderr=False):
249+
mne_make_scalp_surfaces.run()
250+
assert op.isfile(headseg_path_new)
251+
assert op.isfile(surf_path_new)
206252
assert op.isfile(dense_fname)
207253
assert op.isfile(medium_fname)
208-
with pytest.raises(OSError, match="overwrite"):
254+
assert op.isfile(sparse_fname)
255+
256+
os.remove(headseg_path_new)
257+
os.remove(surf_path_new)
258+
os.remove(dense_fname)
259+
cmd = ("-s", "sample", "--subjects-dir", tempdir, "--no-decimate")
260+
with ArgvSetter(cmd, disable_stdout=False, disable_stderr=False):
261+
with pytest.raises(OSError, match="Trying to generate new scalp surfaces"):
262+
mne_make_scalp_surfaces.run()
263+
264+
os.remove(medium_fname)
265+
os.remove(sparse_fname)
266+
cmd = (
267+
"-s",
268+
"sample",
269+
"--subjects-dir",
270+
tempdir,
271+
"--no-decimate",
272+
"--reuse-seghead",
273+
)
274+
with ArgvSetter(cmd, disable_stdout=False, disable_stderr=False):
275+
with pytest.raises(ValueError, match="No existing scalp surface found"):
209276
mne_make_scalp_surfaces.run()
210-
# actually check the outputs
211-
head_py = read_bem_surfaces(dense_fname)
212-
assert_equal(len(head_py), 1)
213-
head_py = head_py[0]
214-
head_c = read_bem_surfaces(
215-
op.join(subjects_dir, "sample", "bem", "sample-head-dense.fif")
216-
)[0]
217-
assert_allclose(head_py["rr"], head_c["rr"])
277+
278+
cmd = ("-s", "sample", "--subjects-dir", tempdir, "--no-decimate", "--overwrite")
279+
with ArgvSetter(cmd, disable_stdout=False, disable_stderr=False):
280+
mne_make_scalp_surfaces.run()
281+
assert op.isfile(headseg_path_new)
282+
assert op.isfile(surf_path_new)
283+
assert op.isfile(dense_fname)
284+
assert not op.isfile(medium_fname)
285+
assert not op.isfile(sparse_fname)
286+
287+
cmd = ("-s", "sample", "--subjects-dir", tempdir, "--no-decimate", "--overwrite")
288+
with ArgvSetter(cmd, disable_stdout=False, disable_stderr=False):
289+
mne_make_scalp_surfaces.run()
290+
assert op.isfile(headseg_path_new)
291+
assert op.isfile(surf_path_new)
292+
assert op.isfile(dense_fname)
293+
assert not op.isfile(medium_fname)
294+
assert not op.isfile(sparse_fname)
295+
218296
if not has:
219297
assert "SUBJECTS_DIR" not in os.environ
220298

mne/tests/test_bem.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@
4343
from mne.io import read_info
4444
from mne.surface import _get_ico_surface, read_surface
4545
from mne.transforms import translation
46-
from mne.utils import _record_warnings, catch_logging, check_version
46+
from mne.utils import (
47+
_record_warnings,
48+
catch_logging,
49+
check_version,
50+
requires_freesurfer,
51+
)
4752

4853
fname_raw = Path(__file__).parents[1] / "io" / "tests" / "data" / "test_raw.fif"
4954
subjects_dir = testing.data_path(download=False) / "subjects"
@@ -518,11 +523,27 @@ def test_io_head_bem(tmp_path):
518523
assert np.allclose(head["tris"], head_defect["tris"])
519524

520525

521-
@pytest.mark.slowtest # ~4 s locally
526+
@pytest.mark.slowtest
527+
@requires_freesurfer("mkheadsurf")
528+
@testing.requires_testing_data
522529
def test_make_scalp_surfaces_topology(tmp_path, monkeypatch):
523-
"""Test topology checks for make_scalp_surfaces."""
530+
"""Test make_scalp_surfaces and topology checks."""
524531
pytest.importorskip("pyvista")
525532
pytest.importorskip("nibabel")
533+
534+
# tests on 'sample'
535+
subject = "sample"
536+
subjects_dir = testing.data_path(download=False)
537+
with pytest.raises(OSError, match="use --overwrite to overwrite it"):
538+
make_scalp_surfaces(
539+
subject, subjects_dir, force=False, verbose=True, overwrite=False
540+
)
541+
542+
make_scalp_surfaces(
543+
subject, subjects_dir, force=False, verbose=True, overwrite=True
544+
)
545+
546+
# tests on custom surface
526547
subjects_dir = tmp_path
527548
subject = "test"
528549
surf_dir = subjects_dir / subject / "surf"
@@ -550,6 +571,7 @@ def _decimate_surface(points, triangles, n_triangles):
550571
make_scalp_surfaces(
551572
subject, subjects_dir, force=False, verbose=True, overwrite=True
552573
)
574+
553575
bem_dir = subjects_dir / subject / "bem"
554576
sparse_path = bem_dir / f"{subject}-head-sparse.fif"
555577
assert not sparse_path.is_file()

0 commit comments

Comments
 (0)