From 6bc6cb0c55caa254e4071793e78a5b4f7c8eedca Mon Sep 17 00:00:00 2001 From: payam Date: Tue, 21 Jul 2026 17:38:40 +0200 Subject: [PATCH 1/4] add cortex alpha and silhouette to controls --- mne/viz/_brain/_brain.py | 151 +++++++++++++++++++++-------- mne/viz/_brain/tests/test_brain.py | 26 +++++ mne/viz/backends/_qt.py | 23 +++++ mne/viz/backends/renderer.py | 2 +- 4 files changed, 161 insertions(+), 41 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 677268d626d..db615ab21b5 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -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). @@ -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. @@ -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 @@ -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: @@ -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: @@ -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=False) # Renderer widget rends = [str(i) for i in range(len(self._renderer._all_renderers))] if len(rends) > 1: @@ -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=False) + 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): @@ -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): @@ -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=False) # setup candidate annots @_auto_weakref @@ -1069,18 +1082,9 @@ 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_surface_widget(name="Surface") self._configure_dock_colormap_widget(name="Color Limits") 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): @@ -3526,6 +3530,73 @@ 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() + else: + 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. diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 27057abf782..0efb53fb201 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -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 = [ diff --git a/mne/viz/backends/_qt.py b/mne/viz/backends/_qt.py index 33f5ab04967..b5ffc5ca310 100644 --- a/mne/viz/backends/_qt.py +++ b/mne/viz/backends/_qt.py @@ -138,6 +138,17 @@ def _get_layout(layout): return layout +def _set_item_visible(item, visible): + widget = item.widget() + if widget is not None: + widget.setVisible(visible) + return + sub_layout = item.layout() + if sub_layout is not None: + for i in range(sub_layout.count()): + _set_item_visible(sub_layout.itemAt(i), visible) + + # ------- # Widgets # ------- @@ -1124,6 +1135,18 @@ def _dock_add_group_box(self, name, *, collapse=None, layout=None): hlayout = QVBoxLayout() widget = QGroupBox(name) widget.setLayout(hlayout) + widget.setStyleSheet("QGroupBox::title { font-size: 11pt; font-weight: bold; }") + if collapse is not None: + assert isinstance(collapse, bool) + + def _toggle_visibility(checked, hlayout=hlayout): + for i in range(hlayout.count()): + _set_item_visible(hlayout.itemAt(i), checked) + + widget.setCheckable(True) + widget.setChecked(not collapse) + widget.toggled.connect(_toggle_visibility) + _toggle_visibility(widget.isChecked()) self._layout_add_widget(layout, widget) return hlayout diff --git a/mne/viz/backends/renderer.py b/mne/viz/backends/renderer.py index a96239a4aed..01f9371f903 100644 --- a/mne/viz/backends/renderer.py +++ b/mne/viz/backends/renderer.py @@ -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))), From 211f07f67f8e6a84a7c7a1a9c21237c2bee07715 Mon Sep 17 00:00:00 2001 From: payam Date: Tue, 21 Jul 2026 18:22:08 +0200 Subject: [PATCH 2/4] fix silhoute visibility --- mne/viz/_brain/_brain.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index db615ab21b5..fdce0857520 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -3591,10 +3591,9 @@ def set_silhouette_line_width(self, line_width): self.silhouette = True if not self._silhouette_actors: self._add_silhouette() - else: - for actor in self._silhouette_actors: - actor.GetProperty().SetLineWidth(line_width) - actor.SetVisibility(True) + for actor in self._silhouette_actors: + actor.GetProperty().SetLineWidth(line_width) + actor.SetVisibility(True) self._renderer._update() @fill_doc From ea280526b7713593a7ee6365ee46801c485c7eaa Mon Sep 17 00:00:00 2001 From: payam Date: Wed, 22 Jul 2026 09:50:32 +0200 Subject: [PATCH 3/4] a bit fancier tool button now --- mne/viz/backends/_qt.py | 73 +++++++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/mne/viz/backends/_qt.py b/mne/viz/backends/_qt.py index b5ffc5ca310..2051a8e38ab 100644 --- a/mne/viz/backends/_qt.py +++ b/mne/viz/backends/_qt.py @@ -138,17 +138,6 @@ def _get_layout(layout): return layout -def _set_item_visible(item, visible): - widget = item.widget() - if widget is not None: - widget.setVisible(visible) - return - sub_layout = item.layout() - if sub_layout is not None: - for i in range(sub_layout.count()): - _set_item_visible(sub_layout.itemAt(i), visible) - - # ------- # Widgets # ------- @@ -955,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): @@ -1133,20 +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) - widget.setStyleSheet("QGroupBox::title { font-size: 11pt; font-weight: bold; }") - if collapse is not None: - assert isinstance(collapse, bool) - - def _toggle_visibility(checked, hlayout=hlayout): - for i in range(hlayout.count()): - _set_item_visible(hlayout.itemAt(i), checked) - - widget.setCheckable(True) - widget.setChecked(not collapse) - widget.toggled.connect(_toggle_visibility) - _toggle_visibility(widget.isChecked()) + 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 From 34865675d9a9e22dea48aacda678c15a97ea751a Mon Sep 17 00:00:00 2001 From: payam Date: Wed, 22 Jul 2026 09:51:16 +0200 Subject: [PATCH 4/4] chnage docks order and default collapses --- mne/viz/_brain/_brain.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index fdce0857520..991619538e1 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -795,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, collapse=False) + 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: @@ -852,7 +852,7 @@ def set_orientation(value, orientation_data=orientation_data): ) def _configure_dock_surface_widget(self, name): - layout = self._renderer._dock_add_group_box(name, collapse=False) + layout = self._renderer._dock_add_group_box(name, collapse=True) self.widgets["cortex_alpha"] = self._renderer._dock_add_slider( name="Alpha", value=self._alpha, @@ -1009,7 +1009,7 @@ def _configure_dock_trace_widget(self, name): self._configure_vertex_time_course() return - layout = self._renderer._dock_add_group_box(name, collapse=False) + layout = self._renderer._dock_add_group_box(name, collapse=True) # setup candidate annots @_auto_weakref @@ -1081,9 +1081,9 @@ def _set_label_mode(mode): def _configure_dock(self): self._renderer._dock_initialize() self._configure_dock_playback_widget(name="Playback") + 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_colormap_widget(name="Color Limits") self._configure_dock_trace_widget(name="Trace") self._renderer._dock_finalize()