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
25 changes: 22 additions & 3 deletions mne/viz/_3d_overlay.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -204,6 +205,7 @@ def add_overlay(self, scalars, colormap, rng, opacity, name, smooth=False):
rng=rng,
opacity=opacity,
name=name,
smooth=smooth,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a new smooth parameter? We already had controls for the amount of smoothing, which you could set to 1 to turn it off.

)
self._overlays[name] = overlay
colors = overlay.to_colors()
Expand Down Expand Up @@ -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
):
Expand Down Expand Up @@ -294,15 +312,16 @@ 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)
if scalars.shape != (expected,):
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:
Expand Down
100 changes: 98 additions & 2 deletions mne/viz/_brain/_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"))
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
14 changes: 13 additions & 1 deletion mne/viz/_brain/tests/test_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't test whether the new colors are actually shown, like you do below with changing surf


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
Expand Down
Loading