Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 111 additions & 41 deletions mne/viz/_brain/_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ class Brain:
Add support for non-string arguments.
alpha : float in [0, 1]
Alpha level to control opacity of the cortical surface.

See :meth:`set_cortex_alpha` to change this after creation.
size : int | array-like, shape (2,)
The size of the window, in pixels. can be one number to specify
a square window, or a length-2 sequence to specify (width, height).
Expand Down Expand Up @@ -180,6 +182,9 @@ class Brain:

.. versionchanged:: 1.13
The default ``decimate`` value changed from ``0.9`` to ``"ico5"``.

See :meth:`set_silhouette_line_width` to change the line width (or
show/hide the silhouette) after creation.
%(theme_3d)s
show : bool
Display the window as soon as it is ready. Defaults to True.
Expand Down Expand Up @@ -397,6 +402,7 @@ def __init__(
self.silhouette = True
else:
self.silhouette = silhouette
self._silhouette_actors = []
self._scalar_bar = None
# for now only one time label can be added
# since it is the same for all figures
Expand Down Expand Up @@ -471,35 +477,11 @@ def __init__(
else:
actor = self.layered_meshes[h]._actor
self._renderer.plotter.add_actor(actor, render=False)
if self.silhouette:
mesh = self.layered_meshes[h]
decimate = self._silhouette["decimate"]
if isinstance(decimate, str):
import pyvista as pv

vertno, tris = _decimate_surface_ico_oct(
self._subject,
self._subjects_dir,
h,
self.geo[h].surf,
decimate,
)
sil_mesh = pv.PolyData(
self.geo[h].coords[vertno],
np.c_[np.full(len(tris), 3), tris],
)
decimate = None # already decimated
else:
sil_mesh = mesh._polydata
self._renderer._silhouette(
mesh=sil_mesh,
color=self._silhouette["color"],
line_width=self._silhouette["line_width"],
alpha=self._silhouette["alpha"],
decimate=decimate,
)
self._set_camera(**views_dicts[h][v])

if self.silhouette:
self._add_silhouette()

self.interaction = interaction
self._closed = False
if show:
Expand Down Expand Up @@ -652,6 +634,9 @@ def _clean(self):
# clear init actors
for hemi in self.layered_meshes:
self.layered_meshes[hemi]._clean()
for actor in getattr(self, "_silhouette_actors", None) or ():
self.plotter.remove_actor(actor)
self._silhouette_actors = None
self._clear_callbacks()
self._clear_widgets()
if getattr(self, "mpl_canvas", None) is not None:
Expand Down Expand Up @@ -810,7 +795,7 @@ def current_time_func():
del current_time

def _configure_dock_orientation_widget(self, name):
layout = self._renderer._dock_add_group_box(name)
layout = self._renderer._dock_add_group_box(name, collapse=True)
# Renderer widget
rends = [str(i) for i in range(len(self._renderer._all_renderers))]
if len(rends) > 1:
Expand Down Expand Up @@ -866,13 +851,31 @@ def set_orientation(value, orientation_data=orientation_data):
layout=layout,
)

def _configure_dock_surface_widget(self, name):
layout = self._renderer._dock_add_group_box(name, collapse=True)
self.widgets["cortex_alpha"] = self._renderer._dock_add_slider(
name="Alpha",
value=self._alpha,
rng=[0.0, 1.0],
callback=self.set_cortex_alpha,
double=True,
layout=layout,
)
self.widgets["silhouette"] = self._renderer._dock_add_spin_box(
name="Silhouette",
value=self._silhouette["line_width"] if self.silhouette else 0.0,
rng=[0.0, 10.0],
callback=self.set_silhouette_line_width,
layout=layout,
)

def _configure_dock_colormap_widget(self, name):
self._active_data_key = next(iter(self._all_data))
fmax, fscale, fscale_power = _get_range(self)
rng = [0, fmax * fscale]
self._data["fscale"] = fscale

layout = self._renderer._dock_add_group_box(name)
layout = self._renderer._dock_add_group_box(name, collapse=False)

@_auto_weakref
def select_data_key(value):
Expand Down Expand Up @@ -959,6 +962,16 @@ def fplus():
style="toolbutton",
)
self._renderer._layout_add_widget(layout, hlayout)

self.widgets["smoothing"] = self._renderer._dock_add_spin_box(
name="Smoothing",
value=self._data["smoothing_steps"],
rng=self.default_smoothing_range,
callback=self.set_data_smoothing,
double=False,
layout=layout,
)

self._update_colormap_range()

def _refresh_colormap_widgets(self):
Expand Down Expand Up @@ -996,7 +1009,7 @@ def _configure_dock_trace_widget(self, name):
self._configure_vertex_time_course()
return

layout = self._renderer._dock_add_group_box(name)
layout = self._renderer._dock_add_group_box(name, collapse=True)

# setup candidate annots
@_auto_weakref
Expand Down Expand Up @@ -1068,19 +1081,10 @@ def _set_label_mode(mode):
def _configure_dock(self):
self._renderer._dock_initialize()
self._configure_dock_playback_widget(name="Playback")
self._configure_dock_orientation_widget(name="Orientation")
self._configure_dock_colormap_widget(name="Color Limits")
self._configure_dock_orientation_widget(name="Orientation")
self._configure_dock_surface_widget(name="Surface")
self._configure_dock_trace_widget(name="Trace")

# Smoothing widget
self.widgets["smoothing"] = self._renderer._dock_add_spin_box(
name="Smoothing",
value=self._data["smoothing_steps"],
rng=self.default_smoothing_range,
callback=self.set_data_smoothing,
double=False,
)

self._renderer._dock_finalize()

def _configure_mplcanvas(self):
Expand Down Expand Up @@ -3526,6 +3530,72 @@ def screenshot(self, mode="rgb", time_viewer=False):
)
return img

def set_cortex_alpha(self, alpha):
"""Set the opacity of the cortical surface.

Parameters
----------
alpha : float
The opacity of the cortical surface, between 0 and 1.
"""
self._alpha = float(alpha)
for hemi in self._hemis:
self.layered_meshes[hemi].update_overlay("curv", opacity=self._alpha)
self._renderer._update()

def _add_silhouette(self):
self._silhouette_actors = []
for h in self._hemis:
mesh = self.layered_meshes[h]
for _, _, v in self._iter_views(h):
self._set_camera(**views_dicts[h][v])
decimate = self._silhouette["decimate"]
if isinstance(decimate, str):
import pyvista as pv

vertno, tris = _decimate_surface_ico_oct(
self._subject, self._subjects_dir, h, self.geo[h].surf, decimate
)
sil_mesh = pv.PolyData(
self.geo[h].coords[vertno],
np.c_[np.full(len(tris), 3), tris],
)
decimate = None # already decimated
else:
sil_mesh = mesh._polydata
actor = self._renderer._silhouette(
mesh=sil_mesh,
color=self._silhouette["color"],
line_width=self._silhouette["line_width"],
alpha=self._silhouette["alpha"],
decimate=decimate,
)
self._silhouette_actors.append(actor)

def set_silhouette_line_width(self, line_width):
"""Set the width of the cortical surface silhouette outline.

Parameters
----------
line_width : float
The silhouette line width. A value of ``0`` hides the
silhouette entirely.
"""
line_width = float(line_width)
self._silhouette["line_width"] = line_width
if line_width <= 0:
self.silhouette = False
for actor in self._silhouette_actors:
actor.SetVisibility(False)
else:
self.silhouette = True
if not self._silhouette_actors:
self._add_silhouette()
for actor in self._silhouette_actors:
actor.GetProperty().SetLineWidth(line_width)
actor.SetVisibility(True)
self._renderer._update()

@fill_doc
def update_lut(self, fmin=None, fmid=None, fmax=None, alpha=None):
"""Update the range of the color map.
Expand Down
26 changes: 26 additions & 0 deletions mne/viz/_brain/tests/test_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,32 @@ def __init__(self):
brain.close()


@testing.requires_testing_data
def test_surface_controls(renderer_interactive_pyvistaqt, brain_gc):
"""Test live cortex alpha and silhouette line width."""
brain = _create_testing_brain(hemi="lh", show_traces=False)

brain.set_cortex_alpha(0.5)
assert brain._alpha == 0.5

assert not brain.silhouette
brain.set_silhouette_line_width(3.0)
assert brain.silhouette
actors = brain._silhouette_actors
assert len(actors) > 0
assert all(a.GetVisibility() for a in actors)

brain.set_silhouette_line_width(0.0)
assert not brain.silhouette
assert all(not a.GetVisibility() for a in actors)

brain.set_silhouette_line_width(5.0)
assert brain._silhouette_actors is actors
assert all(a.GetVisibility() for a in actors)

brain.close()


def test_add_annotation(renderer_interactive_pyvistaqt, brain_gc):
"""Test add_annotation."""
annots = [
Expand Down
50 changes: 48 additions & 2 deletions mne/viz/backends/_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -944,13 +944,18 @@ def _dock_initialize(
self._dock, self._dock_layout = _create_dock_widget(
window, name, qt_area, max_width=max_width
)
self._dock_collapsibles = []
if area == "left":
window.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)
else:
window.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea)

def _dock_finalize(self):
for content, _ in self._dock_collapsibles:
content.setVisible(True)
self._dock.setMinimumSize(self._dock.sizeHint().width(), 0)
for content, expanded in self._dock_collapsibles:
content.setVisible(expanded)
self._dock_add_stretch(self._dock_layout)

def _dock_show(self):
Expand Down Expand Up @@ -1122,8 +1127,49 @@ def func(button):
def _dock_add_group_box(self, name, *, collapse=None, layout=None):
layout = self._dock_layout if layout is None else layout
hlayout = QVBoxLayout()
widget = QGroupBox(name)
widget.setLayout(hlayout)
if collapse is None:
widget = QGroupBox(name)
widget.setLayout(hlayout)
widget.setStyleSheet(
"QGroupBox::title { font-size: 11pt; font-weight: bold; }"
)
self._layout_add_widget(layout, widget)
return hlayout

assert isinstance(collapse, bool)
content = QGroupBox()
content.setLayout(hlayout)
content.setVisible(not collapse)
self._dock_collapsibles.append((content, not collapse))

toggle = QToolButton()
toggle.setText(f"{'▾' if not collapse else '▸'} {name}")
toggle.setCheckable(True)
toggle.setChecked(not collapse)
toggle.setCursor(Qt.PointingHandCursor)
toggle.setStyleSheet(
"QToolButton {"
" border: none;"
" font-size: 13pt;"
" font-weight: 600;"
" color: palette(mid);"
" }"
"QToolButton:hover { color: palette(text); }"
)

def _toggle_visibility(checked, content=content, toggle=toggle, name=name):
content.setVisible(checked)
toggle.setText(f"{'▾' if checked else '▸'} {name}")

toggle.toggled.connect(_toggle_visibility)

outer = QVBoxLayout()
outer.setContentsMargins(0, 0, 0, 0)
outer.setSpacing(0)
outer.addWidget(toggle)
outer.addWidget(content)
widget = QWidget()
widget.setLayout(outer)
self._layout_add_widget(layout, widget)
return hlayout

Expand Down
2 changes: 1 addition & 1 deletion mne/viz/backends/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ def publish_time_change(time_index):
TimeChange(time=np.interp(time_index, np.arange(len(times)), times)),
)

layout = self._dock_add_group_box("")
layout = self._dock_add_group_box("Playback", collapse=False)
self._widgets["time_slider"] = self._dock_add_slider(
name="Time (s)",
value=np.interp(current_time_func(), times, np.arange(len(times))),
Expand Down
Loading