From 5643c0b9c2928b20f5d91d27f64bb38fd49bc6c9 Mon Sep 17 00:00:00 2001 From: payam Date: Sun, 26 Jul 2026 13:40:04 +0200 Subject: [PATCH] add surf and cortex to gui --- mne/viz/_3d_overlay.py | 25 +++++++- mne/viz/_brain/_brain.py | 100 ++++++++++++++++++++++++++++- mne/viz/_brain/tests/test_brain.py | 14 +++- 3 files changed, 133 insertions(+), 6 deletions(-) diff --git a/mne/viz/_3d_overlay.py b/mne/viz/_3d_overlay.py index 7a241969646..d2815a8e35d 100644 --- a/mne/viz/_3d_overlay.py +++ b/mne/viz/_3d_overlay.py @@ -12,13 +12,14 @@ class _Overlay: - def __init__(self, scalars, colormap, rng, opacity, name): + def __init__(self, scalars, colormap, rng, opacity, name, smooth=False): self._scalars = scalars self._colormap = colormap assert rng is not None self._rng = rng self._opacity = opacity self._name = name + self._smooth = smooth def to_colors(self): from matplotlib.colors import Colormap, ListedColormap @@ -204,6 +205,7 @@ def add_overlay(self, scalars, colormap, rng, opacity, name, smooth=False): rng=rng, opacity=opacity, name=name, + smooth=smooth, ) self._overlays[name] = overlay colors = overlay.to_colors() @@ -263,6 +265,22 @@ def _clean(self): self._polydata = None self._renderer = None + def update_geometry(self, vertices, normals): + """Update the mesh's vertex positions and normals in place. + + Parameters + ---------- + vertices : array, shape (n_vertices, 3) + New vertex coordinates. Must match the existing vertex count. + normals : array, shape (n_vertices, 3) + New vertex normals. + """ + self._vertices = vertices + self._normals = normals + self._polydata.points = vertices + self._polydata.point_data["Normals"] = normals + self._polydata.GetPointData().SetActiveNormals("Normals") + def update_overlay( self, name, scalars=None, colormap=None, opacity=None, rng=None, update=True ): @@ -294,7 +312,8 @@ def update_overlay( return if scalars is not None: scalars = np.asarray(scalars) - if self.smooth_mat is not None: + smooth = overlay._smooth and self.smooth_mat is not None + if smooth: expected = self.smooth_mat.shape[1] else: expected = len(overlay._scalars) @@ -302,7 +321,7 @@ def update_overlay( raise ValueError( f"scalars must have shape ({expected},), got {scalars.shape}" ) - if self.smooth_mat is not None: + if smooth: scalars = self.smooth_mat.dot(scalars) overlay._scalars = scalars if colormap is not None: diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 1711333bf76..88c60c330c5 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -92,6 +92,14 @@ from .surface import _Surface from .view import _lh_views_dict, views_dicts +_CORTEX_PRESETS = ("classic", "high_contrast", "low_contrast", "bone") + + +def _resolve_offset(offset, surf, hemi): + if isinstance(offset, str): + offset = surf in ("inflated", "flat") + return None if (not offset or hemi != "both") else 0.0 + @fill_doc class Brain: @@ -328,6 +336,7 @@ def __init__( _validate_type(subject, str, "subject") self._surf = surf + self._offset_request = offset if hemi is None: hemi = "vol" hemi = self._check_hemi(hemi, extras=("both", "split", "vol")) @@ -420,13 +429,13 @@ def __init__( # evaluate at the midpoint of the used colormap val = -geo_kwargs["vmin"] / (geo_kwargs["vmax"] - geo_kwargs["vmin"]) self._brain_color = geo_kwargs["colormap"](val) + self._cortex_preset = cortex if cortex in _CORTEX_PRESETS else "classic" # load geometry for one or both hemispheres as necessary _validate_type(offset, (str, bool), "offset") if isinstance(offset, str): _check_option("offset", offset, ("auto",), extra="when str") - offset = surf in ("inflated", "flat") - offset = None if (not offset or hemi != "both") else 0.0 + offset = _resolve_offset(offset, surf, hemi) logger.debug(f"Hemi offset: {offset}") _validate_type(theme, (str, None), "theme") self._renderer = _get_renderer( @@ -863,6 +872,21 @@ def set_orientation(value, orientation_data=orientation_data): def _configure_dock_surface_widget(self, name): layout = self._renderer._dock_add_group_box(name, collapse=True) + if self._surf in ("pial", "white", "inflated"): + self.widgets["surf"] = self._renderer._dock_add_combo_box( + name="Surf", + value=self._surf, + rng=("pial", "white", "inflated"), + callback=self.set_surf, + layout=layout, + ) + self.widgets["cortex"] = self._renderer._dock_add_combo_box( + name="Cortex", + value=self._cortex_preset, + rng=_CORTEX_PRESETS, + callback=self.set_cortex_colormap, + layout=layout, + ) self.widgets["cortex_alpha"] = self._renderer._dock_add_slider( name="Alpha", value=self._alpha, @@ -3763,6 +3787,78 @@ def set_cortex_alpha(self, alpha): self.layered_meshes[hemi].update_overlay("curv", opacity=self._alpha) self._renderer._update() + def set_cortex_colormap(self, cortex): + """Set the curvature colormap preset of the cortical surface. + + Parameters + ---------- + cortex : str + One of ``'classic'``, ``'high_contrast'``, ``'low_contrast'``, ``'bone'``. + """ + _check_option("cortex", cortex, _CORTEX_PRESETS) + geo_kwargs = self._cortex_colormap(cortex) + val = -geo_kwargs["vmin"] / (geo_kwargs["vmax"] - geo_kwargs["vmin"]) + self._brain_color = geo_kwargs["colormap"](val) + self._cortex_preset = cortex + for hemi in self._hemis: + geo = self.geo[hemi] + mesh = self.layered_meshes[hemi] + scalars = ( + geo.bin_curv + if geo.bin_curv is not None + else mesh._default_scalars[:, 0] + ) + mesh.update_overlay( + "curv", + scalars=scalars, + colormap=geo_kwargs["colormap"], + rng=[geo_kwargs["vmin"], geo_kwargs["vmax"]], + ) + self._renderer._update() + + def set_surf(self, surf): + """Set the cortical surface representation. + + Parameters + ---------- + surf : str + One of ``'pial'``, ``'white'``, ``'inflated'``. To use a flat + surface, close this figure and construct a new one with + ``Brain(..., surf="flat", views="flat")``. + """ + _check_option("surf", surf, ("pial", "white", "inflated")) + if surf == self._surf: + return + if any(self._labels[h] for h in self._hemis) or any( + "foci" in self._foci_data.get(h, {}) for h in self._hemis + ): + warn( + "Foci and label/annotation outlines do not move when the " + "surface representation changes and may now be misaligned." + ) + offset = _resolve_offset(self._offset_request, surf, self._hemi) + for h in self._hemis: + geo = _Surface( + self._subject, + h, + surf, + self._subjects_dir, + offset, + units=self._units, + x_dir=self._rigid[0, :3], + ) + geo.load_geometry() + geo.load_curvature() + self.geo[h] = geo + self.layered_meshes[h].update_geometry(geo.coords, geo.nn) + self._surf = surf + if self.silhouette: + for actor in self._silhouette_actors: + self.plotter.remove_actor(actor) + self._add_silhouette() + self.reset_view() + self._renderer._update() + def _add_silhouette(self): self._silhouette_actors = [] for h in self._hemis: diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 3e94837886a..c0dbb6a4473 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -588,12 +588,24 @@ def __init__(self): @testing.requires_testing_data def test_surface_controls(renderer_interactive_pyvistaqt, brain_gc): - """Test live cortex alpha and silhouette line width.""" + """Test live cortex alpha/colormap, surf switching, and silhouette line width.""" brain = _create_testing_brain(hemi="lh", show_traces=False) brain.set_cortex_alpha(0.5) assert brain._alpha == 0.5 + brain.set_cortex_colormap("bone") + assert brain._cortex_preset == "bone" + + old_coords = brain.geo["lh"].coords.copy() + brain.set_surf("white") + assert brain._surf == "white" + assert not np.allclose(brain.geo["lh"].coords, old_coords) + assert brain.layered_meshes["lh"]._vertices is brain.geo["lh"].coords + + brain.set_surf("white") # no-op, same surf + assert brain._surf == "white" + assert not brain.silhouette brain.set_silhouette_line_width(3.0) assert brain.silhouette