Skip to content

Commit 715784f

Browse files
vferatpre-commit-ci[bot]larsoner
authored
Add sourcespace to Report (#12848)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Larson <larson.eric.d@gmail.com>
1 parent d970efb commit 715784f

File tree

12 files changed

+235
-28
lines changed

12 files changed

+235
-28
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add source space(s) visualization(s) in :func:`mne.Report.add_forward`, by `Victor Ferat`_.

mne/report/report.py

Lines changed: 134 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,17 @@ def _fig_to_img(
475475

476476

477477
def _get_bem_contour_figs_as_arrays(
478-
*, sl, n_jobs, mri_fname, surfaces, orientation, src, show, show_orientation, width
478+
*,
479+
sl,
480+
n_jobs,
481+
mri_fname,
482+
surfaces,
483+
orientation,
484+
src,
485+
trans,
486+
show,
487+
show_orientation,
488+
width,
479489
):
480490
"""Render BEM surface contours on MRI slices.
481491
@@ -494,6 +504,7 @@ def _get_bem_contour_figs_as_arrays(
494504
surfaces=surfaces,
495505
orientation=orientation,
496506
src=src,
507+
trans=trans,
497508
show=show,
498509
show_orientation=show_orientation,
499510
width=width,
@@ -507,6 +518,21 @@ def _get_bem_contour_figs_as_arrays(
507518
return out
508519

509520

521+
def _iterate_alignment_views(function, alpha, **kwargs):
522+
"""Auxiliary function to iterate over views in trans fig."""
523+
from ..viz.backends.renderer import MNE_3D_BACKEND_TESTING
524+
525+
# TODO: Eventually maybe we should expose the size option?
526+
size = (80, 80) if MNE_3D_BACKEND_TESTING else (800, 800)
527+
fig = create_3d_figure(size, bgcolor=(0.5, 0.5, 0.5))
528+
from ..viz.backends.renderer import backend
529+
530+
try:
531+
return _itv(function, fig, **kwargs)
532+
finally:
533+
backend._close_3d_figure(fig)
534+
535+
510536
def _iterate_trans_views(function, alpha, **kwargs):
511537
"""Auxiliary function to iterate over views in trans fig."""
512538
from ..viz.backends.renderer import MNE_3D_BACKEND_TESTING
@@ -531,7 +557,18 @@ def _itv(function, fig, *, max_width=MAX_IMG_WIDTH, max_res=MAX_IMG_RES, **kwarg
531557

532558
function(fig=fig, **kwargs)
533559

534-
views = ("frontal", "lateral", "medial", "axial", "rostral", "coronal")
560+
views = (
561+
"right_lateral",
562+
"right_anterolateral",
563+
"anterior",
564+
"left_anterolateral",
565+
"left_lateral",
566+
"superior",
567+
"right_posterolateral",
568+
"posterior",
569+
"left_posterolateral",
570+
"inferior",
571+
)
535572

536573
images = []
537574
for view in views:
@@ -544,7 +581,7 @@ def _itv(function, fig, *, max_width=MAX_IMG_WIDTH, max_res=MAX_IMG_RES, **kwarg
544581
images.append(im)
545582

546583
images = np.concatenate(
547-
[np.concatenate(images[:3], axis=1), np.concatenate(images[3:], axis=1)], axis=0
584+
[np.concatenate(images[:5], axis=1), np.concatenate(images[5:], axis=1)], axis=0
548585
)
549586

550587
try:
@@ -2655,6 +2692,8 @@ def add_bem(
26552692
self._add_bem(
26562693
subject=subject,
26572694
subjects_dir=subjects_dir,
2695+
src=None,
2696+
trans=None,
26582697
decim=decim,
26592698
n_jobs=n_jobs,
26602699
width=width,
@@ -3245,6 +3284,8 @@ def _render_one_bem_axis(
32453284
surfaces,
32463285
image_format,
32473286
orientation,
3287+
src=None,
3288+
trans=None,
32483289
decim=2,
32493290
n_jobs=None,
32503291
width=512,
@@ -3265,7 +3306,8 @@ def _render_one_bem_axis(
32653306
mri_fname=mri_fname,
32663307
surfaces=surfaces,
32673308
orientation=orientation,
3268-
src=None,
3309+
src=src,
3310+
trans=trans,
32693311
show=False,
32703312
show_orientation="always",
32713313
width=width,
@@ -3574,6 +3616,89 @@ def _add_forward(
35743616
replace=replace,
35753617
)
35763618

3619+
if subject:
3620+
src = forward["src"]
3621+
trans = forward["mri_head_t"]
3622+
# Alignment
3623+
kwargs = dict(
3624+
info=forward["info"],
3625+
trans=trans,
3626+
src=src,
3627+
subject=subject,
3628+
subjects_dir=subjects_dir,
3629+
meg=["helmet", "sensors"],
3630+
show_axes=True,
3631+
eeg=dict(original=0.2, projected=0.8),
3632+
coord_frame="mri",
3633+
)
3634+
img, _ = _iterate_trans_views(
3635+
function=plot_alignment,
3636+
alpha=0.5,
3637+
max_width=self.img_max_width,
3638+
max_res=self.img_max_res,
3639+
**kwargs,
3640+
)
3641+
self._add_image(
3642+
img=img,
3643+
title="Alignment",
3644+
section=section,
3645+
caption=None,
3646+
image_format="png",
3647+
tags=tags,
3648+
replace=replace,
3649+
)
3650+
# Source space
3651+
kwargs = dict(
3652+
trans=trans,
3653+
subjects_dir=subjects_dir,
3654+
)
3655+
3656+
self._add_bem(
3657+
subject=subject,
3658+
subjects_dir=subjects_dir,
3659+
src=src,
3660+
trans=trans,
3661+
decim=1,
3662+
n_jobs=1,
3663+
width=512,
3664+
image_format=image_format,
3665+
title="Source space(s) (BEM view)",
3666+
section=section,
3667+
tags=tags,
3668+
replace=replace,
3669+
)
3670+
3671+
if src.kind == "surface" or src.kind == "mixed":
3672+
surfaces = dict(head=0.1, white=0.5)
3673+
else:
3674+
surfaces = dict(head=0.1)
3675+
3676+
kwargs = dict(
3677+
trans=trans,
3678+
src=src,
3679+
subject=subject,
3680+
subjects_dir=subjects_dir,
3681+
show_axes=False,
3682+
coord_frame="mri",
3683+
surfaces=surfaces,
3684+
)
3685+
img, _ = _iterate_alignment_views(
3686+
function=plot_alignment,
3687+
alpha=0.5,
3688+
max_width=self.img_max_width,
3689+
max_res=self.img_max_res,
3690+
**kwargs,
3691+
)
3692+
self._add_image(
3693+
img=img,
3694+
title="Source space(s) (3D view)",
3695+
section=section,
3696+
caption=None,
3697+
image_format="png",
3698+
tags=tags,
3699+
replace=replace,
3700+
)
3701+
35773702
def _add_inverse_operator(
35783703
self,
35793704
*,
@@ -4440,13 +4565,15 @@ def _add_bem(
44404565
*,
44414566
subject,
44424567
subjects_dir,
4568+
src,
4569+
trans,
44434570
decim,
44444571
n_jobs,
44454572
width=512,
44464573
image_format,
44474574
title,
4448-
tags,
44494575
section,
4576+
tags,
44504577
replace,
44514578
):
44524579
"""Render mri+bem (only PNG)."""
@@ -4472,6 +4599,8 @@ def _add_bem(
44724599
mri_fname=mri_fname,
44734600
surfaces=surfaces,
44744601
orientation=orientation,
4602+
src=src,
4603+
trans=trans,
44754604
decim=decim,
44764605
n_jobs=n_jobs,
44774606
width=width,

mne/report/tests/test_report.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,29 @@ def test_add_bem_n_jobs(n_jobs, monkeypatch):
514514
assert 0.778 < corr < 0.80
515515

516516

517+
@pytest.mark.filterwarnings("ignore:Distances could not be calculated.*:RuntimeWarning")
518+
@pytest.mark.slowtest
519+
@testing.requires_testing_data
520+
def test_add_forward(renderer_interactive_pyvistaqt):
521+
"""Test add_forward."""
522+
report = Report(subjects_dir=subjects_dir, image_format="png")
523+
report.add_forward(
524+
forward=fwd_fname,
525+
subject="sample",
526+
subjects_dir=subjects_dir,
527+
title="Forward solution",
528+
)
529+
assert len(report.html) == 4
530+
531+
report = Report(subjects_dir=subjects_dir, image_format="png")
532+
report.add_forward(
533+
forward=fwd_fname,
534+
subjects_dir=subjects_dir,
535+
title="Forward solution",
536+
)
537+
assert len(report.html) == 1
538+
539+
517540
@testing.requires_testing_data
518541
def test_render_mri_without_bem(tmp_path):
519542
"""Test rendering MRI without BEM for mne report."""
@@ -882,7 +905,8 @@ def test_survive_pickle(tmp_path):
882905

883906
@pytest.mark.slowtest # ~30 s on Azure Windows
884907
@testing.requires_testing_data
885-
def test_manual_report_2d(tmp_path, invisible_fig):
908+
@pytest.mark.filterwarnings("ignore:Distances could not be calculated.*:RuntimeWarning")
909+
def test_manual_report_2d(tmp_path, invisible_fig, renderer_pyvistaqt):
886910
"""Simulate user manually creating report by adding one file at a time."""
887911
pytest.importorskip("sklearn")
888912
pytest.importorskip("pandas")

mne/source_space/_source_space.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,8 @@ def plot(
329329
skull=None,
330330
subjects_dir=None,
331331
trans=None,
332+
*,
333+
fig=None,
332334
verbose=None,
333335
):
334336
"""Plot the source space.
@@ -358,6 +360,11 @@ def plot(
358360
produced during coregistration. If trans is None, an identity
359361
matrix is assumed. This is only needed when the source space is in
360362
head coordinates.
363+
fig : Figure3D | None
364+
PyVista scene in which to plot the alignment.
365+
If ``None``, creates a new 600x600 pixel figure with black background.
366+
367+
.. versionadded:: 1.10
361368
%(verbose)s
362369
363370
Returns
@@ -427,6 +434,7 @@ def plot(
427434
ecog=False,
428435
bem=bem,
429436
src=self,
437+
fig=fig,
430438
)
431439

432440
def __getitem__(self, *args, **kwargs):

mne/tests/test_epochs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,7 @@ def my_reject_2(epoch_data):
591591
for kwarg in ("reject", "flat"):
592592
with pytest.raises(
593593
TypeError,
594-
match=r".* must be an instance of .* got <class '.*'> instead.",
594+
match=r".* must be an instance of .* got .* instead.",
595595
):
596596
epochs = Epochs(
597597
raw,

mne/transforms.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ def translation(x=0, y=0, z=0):
439439
return m
440440

441441

442-
def _ensure_trans(trans, fro="mri", to="head"):
442+
def _ensure_trans(trans, fro="mri", to="head", *, extra=""):
443443
"""Ensure we have the proper transform."""
444444
if isinstance(fro, str):
445445
from_str = fro
@@ -455,7 +455,8 @@ def _ensure_trans(trans, fro="mri", to="head"):
455455
to_str = _frame_to_str[to]
456456
to_const = to
457457
del to
458-
err_str = f"trans must be a Transform between {from_str}<->{to_str}, got"
458+
extra = f" {extra}" if extra else ""
459+
err_str = f"trans must be a Transform between {from_str}<->{to_str}{extra}, got"
459460
if not isinstance(trans, list | tuple):
460461
trans = [trans]
461462
# Ensure that we have exactly one match
@@ -481,12 +482,12 @@ def _ensure_trans(trans, fro="mri", to="head"):
481482
return trans
482483

483484

484-
def _get_trans(trans, fro="mri", to="head", allow_none=True):
485+
def _get_trans(trans, fro="mri", to="head", allow_none=True, *, extra=""):
485486
"""Get mri_head_t (from=mri, to=head) from mri filename."""
486487
types = (Transform, "path-like")
487488
if allow_none:
488489
types += (None,)
489-
_validate_type(trans, types, "trans")
490+
_validate_type(trans, types, "trans", extra=extra)
490491
if _path_like(trans):
491492
if trans == "fsaverage":
492493
trans = Path(__file__).parent / "data" / "fsaverage" / "fsaverage-trans.fif"
@@ -510,7 +511,7 @@ def _get_trans(trans, fro="mri", to="head", allow_none=True):
510511
fro_to_t = Transform(fro, to)
511512
trans = "identity"
512513
# it's usually a head->MRI transform, so we probably need to invert it
513-
fro_to_t = _ensure_trans(fro_to_t, fro, to)
514+
fro_to_t = _ensure_trans(fro_to_t, fro, to, extra=extra)
514515
return fro_to_t, trans
515516

516517

mne/utils/check.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -641,9 +641,10 @@ def _validate_type(item, types=None, item_name=None, type_name=None, *, extra=""
641641
type_name[-1] = "or " + type_name[-1]
642642
type_name = ", ".join(type_name)
643643
_item_name = "Item" if item_name is None else item_name
644+
_item_type = type(item) if item is not None else item
644645
raise TypeError(
645646
f"{_item_name} must be an instance of {type_name}{extra}, "
646-
f"got {type(item)} instead."
647+
f"got {_item_type} instead."
647648
)
648649

649650

mne/viz/_brain/view.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,40 @@
4949
rh_views_dict["flat"] = dict(
5050
azimuth=0, elevation=0, focalpoint=ORIGIN, roll=0, distance=DIST
5151
)
52+
53+
both_views_dict = lh_views_dict.copy()
54+
both_views_dict["right_lateral"] = dict(
55+
azimuth=180.0, elevation=90.0, focalpoint=ORIGIN, distance=DIST
56+
)
57+
both_views_dict["right_anterolateral"] = dict(
58+
azimuth=120.0, elevation=90.0, focalpoint=ORIGIN, distance=DIST
59+
)
60+
both_views_dict["anterior"] = dict(
61+
azimuth=90.0, elevation=90.0, focalpoint=ORIGIN, distance=DIST
62+
)
63+
both_views_dict["left_anterolateral"] = dict(
64+
azimuth=60.0, elevation=90.0, focalpoint=ORIGIN, distance=DIST
65+
)
66+
both_views_dict["left_lateral"] = dict(
67+
azimuth=180.0, elevation=-90.0, focalpoint=ORIGIN, distance=DIST
68+
)
69+
both_views_dict["right_posterolateral"] = dict(
70+
azimuth=-120.0, elevation=90.0, focalpoint=ORIGIN, distance=DIST
71+
)
72+
both_views_dict["posterior"] = dict(
73+
azimuth=90.0, elevation=-90.0, focalpoint=ORIGIN, distance=DIST
74+
)
75+
both_views_dict["left_posterolateral"] = dict(
76+
azimuth=-60.0, elevation=90.0, focalpoint=ORIGIN, distance=DIST
77+
)
78+
both_views_dict["superior"] = dict(
79+
azimuth=180.0, elevation=0.0, focalpoint=ORIGIN, distance=DIST
80+
)
81+
both_views_dict["inferior"] = dict(
82+
azimuth=180.0, elevation=180.0, focalpoint=ORIGIN, distance=DIST
83+
)
84+
85+
5286
views_dicts = dict(
53-
lh=lh_views_dict, vol=lh_views_dict, both=lh_views_dict, rh=rh_views_dict
87+
lh=lh_views_dict, vol=lh_views_dict, both=both_views_dict, rh=rh_views_dict
5488
)

0 commit comments

Comments
 (0)