Spaces:
Running
Running
| """Class to draw evoked MEG and EEG fieldlines, with a GUI to control the figure. | |
| author: Marijn van Vliet <w.m.vanvliet@gmail.com> | |
| """ | |
| # Authors: The MNE-Python contributors. | |
| # License: BSD-3-Clause | |
| # Copyright the MNE-Python contributors. | |
| from copy import deepcopy | |
| from functools import partial | |
| import numpy as np | |
| from scipy.interpolate import interp1d | |
| from .._fiff.pick import pick_types | |
| from ..defaults import DEFAULTS | |
| from ..utils import ( | |
| _auto_weakref, | |
| _check_option, | |
| _ensure_int, | |
| _to_rgb, | |
| _validate_type, | |
| fill_doc, | |
| ) | |
| from ._3d_overlay import _LayeredMesh | |
| from .ui_events import ( | |
| ColormapRange, | |
| Contours, | |
| TimeChange, | |
| disable_ui_events, | |
| publish, | |
| subscribe, | |
| ) | |
| from .utils import mne_analyze_colormap | |
| class EvokedField: | |
| """Plot MEG/EEG fields on head surface and helmet in 3D. | |
| Parameters | |
| ---------- | |
| evoked : instance of mne.Evoked | |
| The evoked object. | |
| surf_maps : list | |
| The surface mapping information obtained with make_field_map. | |
| time : float | None | |
| The time point at which the field map shall be displayed. If None, | |
| the average peak latency (across sensor types) is used. | |
| time_label : str | None | |
| How to print info about the time instant visualized. | |
| %(n_jobs)s | |
| fig : instance of Figure3D | None | |
| If None (default), a new figure will be created, otherwise it will | |
| plot into the given figure. | |
| .. versionadded:: 0.20 | |
| vmax : float | dict | None | |
| Maximum intensity. Can be a dictionary with two entries ``"eeg"`` and ``"meg"`` | |
| to specify separate values for EEG and MEG fields respectively. Can be | |
| ``None`` to use the maximum value of the data. | |
| .. versionadded:: 0.21 | |
| .. versionadded:: 1.4 | |
| ``vmax`` can be a dictionary to specify separate values for EEG and | |
| MEG fields. | |
| n_contours : int | |
| The number of contours. | |
| .. versionadded:: 0.21 | |
| show_density : bool | |
| Whether to draw the field density as an overlay on top of the helmet/head | |
| surface. Defaults to ``True``. | |
| alpha : float | dict | None | |
| Opacity of the meshes (between 0 and 1). Can be a dictionary with two | |
| entries ``"eeg"`` and ``"meg"`` to specify separate values for EEG and | |
| MEG fields respectively. Can be ``None`` to use 1.0 when a single field | |
| map is shown, or ``dict(eeg=1.0, meg=0.5)`` when both field maps are shown. | |
| .. versionadded:: 1.4 | |
| %(interpolation_brain_time)s | |
| .. versionadded:: 1.6 | |
| %(interaction_scene)s | |
| Defaults to ``'terrain'``. | |
| .. versionadded:: 1.1 | |
| time_viewer : bool | str | |
| Display time viewer GUI. Can also be ``"auto"``, which will mean | |
| ``True`` if there is more than one time point and ``False`` otherwise. | |
| .. versionadded:: 1.6 | |
| %(verbose)s | |
| Notes | |
| ----- | |
| The figure will publish and subscribe to the following UI events: | |
| * :class:`~mne.viz.ui_events.TimeChange` | |
| * :class:`~mne.viz.ui_events.Contours`, ``kind="field_strength_meg" | "field_strength_eeg"`` | |
| * :class:`~mne.viz.ui_events.ColormapRange`, ``kind="field_strength_meg" | "field_strength_eeg"`` | |
| """ # noqa | |
| def __init__( | |
| self, | |
| evoked, | |
| surf_maps, | |
| *, | |
| time=None, | |
| time_label="t = %0.0f ms", | |
| n_jobs=None, | |
| fig=None, | |
| vmax=None, | |
| n_contours=21, | |
| show_density=True, | |
| alpha=None, | |
| interpolation="nearest", | |
| interaction="terrain", | |
| time_viewer="auto", | |
| verbose=None, | |
| ): | |
| from .backends.renderer import _get_3d_backend, _get_renderer | |
| # Setup figure parameters | |
| self._evoked = evoked | |
| if time is None: | |
| types = [t for t in ["eeg", "grad", "mag"] if t in evoked] | |
| time = np.mean([evoked.get_peak(ch_type=t)[1] for t in types]) | |
| self._current_time = time | |
| if not evoked.times[0] <= time <= evoked.times[-1]: | |
| raise ValueError(f"`time` ({time:0.3f}) must be inside `evoked.times`") | |
| self._time_label = time_label | |
| self._vmax = _validate_type(vmax, (None, "numeric", dict), "vmax") | |
| self._n_contours = _ensure_int(n_contours, "n_contours") | |
| self._time_interpolation = _check_option( | |
| "interpolation", | |
| interpolation, | |
| ("linear", "nearest", "zero", "slinear", "quadratic", "cubic"), | |
| ) | |
| self._interaction = _check_option( | |
| "interaction", interaction, ["trackball", "terrain"] | |
| ) | |
| surf_map_kinds = [surf_map["kind"] for surf_map in surf_maps] | |
| if vmax is None: | |
| self._vmax = {kind: None for kind in surf_map_kinds} | |
| elif isinstance(vmax, dict): | |
| for kind in surf_map_kinds: | |
| if kind not in vmax: | |
| raise ValueError( | |
| f'No entry for "{kind}" found in the vmax dictionary' | |
| ) | |
| self._vmax = vmax | |
| else: # float value | |
| self._vmax = {kind: vmax for kind in surf_map_kinds} | |
| if alpha is None: | |
| self._alpha = { | |
| surf_map["kind"]: val for surf_map, val in zip(surf_maps, [1.0, 0.5]) | |
| } | |
| elif isinstance(alpha, dict): | |
| for kind in surf_map_kinds: | |
| if kind not in alpha: | |
| raise ValueError( | |
| f'No entry for "{kind}" found in the alpha dictionary' | |
| ) | |
| self._alpha = alpha | |
| else: # float value | |
| self._alpha = {kind: alpha for kind in surf_map_kinds} | |
| self._colors = [(0.6, 0.6, 0.6), (1.0, 1.0, 1.0)] | |
| self._colormap = mne_analyze_colormap(format="vtk") | |
| self._colormap_lines = np.concatenate( | |
| [ | |
| np.tile([0.0, 0.0, 255.0, 255.0], (127, 1)), | |
| np.tile([0.0, 0.0, 0.0, 255.0], (2, 1)), | |
| np.tile([255.0, 0.0, 0.0, 255.0], (127, 1)), | |
| ] | |
| ) | |
| self._show_density = show_density | |
| from ._brain import Brain | |
| if isinstance(fig, Brain): | |
| self._renderer = fig._renderer | |
| self._in_brain_figure = True | |
| self._units = fig._units | |
| if _get_3d_backend() == "notebook": | |
| raise NotImplementedError( | |
| "Plotting on top of an existing Brain figure " | |
| "is currently not supported inside a notebook." | |
| ) | |
| else: | |
| self._renderer = _get_renderer( | |
| fig, bgcolor=(0.0, 0.0, 0.0), size=(600, 600) | |
| ) | |
| self._in_brain_figure = False | |
| self._units = "m" | |
| self.plotter = self._renderer.plotter | |
| self.interaction = interaction | |
| # Prepare the surface maps | |
| self._surf_maps = [ | |
| self._prepare_surf_map(surf_map, color, self._alpha[surf_map["kind"]]) | |
| for surf_map, color in zip(surf_maps, self._colors) | |
| ] | |
| # Do we want the time viewer? | |
| if time_viewer == "auto": | |
| time_viewer = len(evoked.times) > 1 | |
| self.time_viewer = time_viewer | |
| # Configure UI events | |
| def current_time_func(): | |
| return self._current_time | |
| self._widgets = dict() | |
| if self.time_viewer: | |
| # Draw widgets only if not inside a figure that already has them. | |
| if ( | |
| not hasattr(self._renderer, "_widgets") | |
| or "time_slider" not in self._renderer._widgets | |
| ): | |
| self._renderer._enable_time_interaction( | |
| self, | |
| current_time_func=current_time_func, | |
| times=evoked.times, | |
| ) | |
| if not self._in_brain_figure or "time_slider" not in fig.widgets: | |
| # Draw the time label | |
| self._time_label = time_label | |
| if time_label is not None: | |
| if "%" in time_label: | |
| time_label = time_label % np.round(1e3 * time) | |
| self._time_label_actor = self._renderer.text2d( | |
| x_window=0.01, y_window=0.01, text=time_label | |
| ) | |
| self._configure_dock() | |
| subscribe(self, "time_change", self._on_time_change) | |
| subscribe(self, "colormap_range", self._on_colormap_range) | |
| subscribe(self, "contours", self._on_contours) | |
| if not self._in_brain_figure: | |
| self._renderer.set_interaction(interaction) | |
| self._renderer.set_camera(azimuth=10, elevation=60, distance="auto") | |
| self._renderer.show() | |
| def _prepare_surf_map(self, surf_map, color, alpha): | |
| """Compute all the data required to render a fieldlines map.""" | |
| if surf_map["kind"] == "eeg": | |
| pick = pick_types(self._evoked.info, meg=False, eeg=True) | |
| else: | |
| pick = pick_types(self._evoked.info, meg=True, eeg=False, ref_meg=False) | |
| evoked_ch_names = set([self._evoked.ch_names[k] for k in pick]) | |
| map_ch_names = set(surf_map["ch_names"]) | |
| if evoked_ch_names != map_ch_names: | |
| message = ["Channels in map and data do not match."] | |
| diff = map_ch_names - evoked_ch_names | |
| if len(diff): | |
| message += [f"{list(diff)} not in data file. "] | |
| diff = evoked_ch_names - map_ch_names | |
| if len(diff): | |
| message += [f"{list(diff)} not in map file."] | |
| raise RuntimeError(" ".join(message)) | |
| data = surf_map["data"] @ self._evoked.data[pick] | |
| data_interp = interp1d( | |
| self._evoked.times, | |
| data, | |
| kind=self._time_interpolation, | |
| assume_sorted=True, | |
| ) | |
| current_data = data_interp(self._current_time) | |
| # Make a solid surface | |
| surf = surf_map["surf"] | |
| if self._units == "mm": | |
| surf = deepcopy(surf) | |
| surf["rr"] *= 1000 | |
| map_vmax = self._vmax.get(surf_map["kind"]) | |
| if map_vmax is None: | |
| map_vmax = float(np.max(current_data)) | |
| mesh = _LayeredMesh( | |
| renderer=self._renderer, | |
| vertices=surf["rr"], | |
| triangles=surf["tris"], | |
| normals=surf["nn"], | |
| ) | |
| mesh.map() | |
| color = _to_rgb(color, alpha=True) | |
| cmap = np.array([(0, 0, 0, 0), color]) | |
| ctable = np.round(cmap * 255).astype(np.uint8) | |
| mesh.add_overlay( | |
| scalars=np.ones(len(current_data)), | |
| colormap=ctable, | |
| rng=[0, 1], | |
| opacity=alpha, | |
| name="surf", | |
| ) | |
| # Show the field density | |
| if self._show_density: | |
| mesh.add_overlay( | |
| scalars=current_data, | |
| colormap=self._colormap, | |
| rng=[-map_vmax, map_vmax], | |
| opacity=1.0, | |
| name="field", | |
| ) | |
| # And the field lines on top | |
| if self._n_contours > 1: | |
| contours = np.linspace(-map_vmax, map_vmax, self._n_contours) | |
| contours_actor, _ = self._renderer.contour( | |
| surface=surf, | |
| scalars=current_data, | |
| contours=contours, | |
| vmin=-map_vmax, | |
| vmax=map_vmax, | |
| colormap=self._colormap_lines, | |
| ) | |
| else: | |
| contours = None # noqa | |
| contours_actor = None | |
| return dict( | |
| pick=pick, | |
| data=data, | |
| data_interp=data_interp, | |
| map_kind=surf_map["kind"], | |
| mesh=mesh, | |
| contours=contours, | |
| contours_actor=contours_actor, | |
| surf=surf, | |
| map_vmax=map_vmax, | |
| ) | |
| def _update(self): | |
| """Update the figure to reflect the current settings.""" | |
| for surf_map in self._surf_maps: | |
| current_data = surf_map["data_interp"](self._current_time) | |
| surf_map["mesh"].update_overlay(name="field", scalars=current_data) | |
| if surf_map["contours"] is not None: | |
| self._renderer.plotter.remove_actor( | |
| surf_map["contours_actor"], render=False | |
| ) | |
| if self._n_contours > 1: | |
| surf_map["contours_actor"], _ = self._renderer.contour( | |
| surface=surf_map["surf"], | |
| scalars=current_data, | |
| contours=surf_map["contours"], | |
| vmin=-surf_map["map_vmax"], | |
| vmax=surf_map["map_vmax"], | |
| colormap=self._colormap_lines, | |
| ) | |
| if self._time_label is not None: | |
| if hasattr(self, "_time_label_actor"): | |
| self._renderer.plotter.remove_actor( | |
| self._time_label_actor, render=False | |
| ) | |
| time_label = self._time_label | |
| if "%" in self._time_label: | |
| time_label = self._time_label % np.round(1e3 * self._current_time) | |
| self._time_label_actor = self._renderer.text2d( | |
| x_window=0.01, y_window=0.01, text=time_label | |
| ) | |
| self._renderer.plotter.update() | |
| def _configure_dock(self): | |
| """Configure the widgets shown in the dock on the left.""" | |
| r = self._renderer | |
| if not hasattr(r, "_dock"): | |
| r._dock_initialize() | |
| # Fieldline configuration | |
| layout = r._dock_add_group_box("Fieldlines") | |
| r._dock_add_label(value="max value", align=True, layout=layout) | |
| def _callback(vmax, kind, scaling): | |
| self.set_vmax(vmax / scaling, kind=kind) | |
| for surf_map in self._surf_maps: | |
| if surf_map["map_kind"] == "meg": | |
| scaling = DEFAULTS["scalings"]["grad"] | |
| else: | |
| scaling = DEFAULTS["scalings"]["eeg"] | |
| rng = [0, np.max(np.abs(surf_map["data"])) * scaling] | |
| hlayout = r._dock_add_layout(vertical=False) | |
| self._widgets[f"vmax_slider_{surf_map['map_kind']}"] = r._dock_add_slider( | |
| name=surf_map["map_kind"].upper(), | |
| value=surf_map["map_vmax"] * scaling, | |
| rng=rng, | |
| callback=partial(_callback, kind=surf_map["map_kind"], scaling=scaling), | |
| double=True, | |
| layout=hlayout, | |
| ) | |
| self._widgets[f"vmax_spin_{surf_map['map_kind']}"] = r._dock_add_spin_box( | |
| name="", | |
| value=surf_map["map_vmax"] * scaling, | |
| rng=rng, | |
| callback=partial(_callback, kind=surf_map["map_kind"], scaling=scaling), | |
| layout=hlayout, | |
| ) | |
| r._layout_add_widget(layout, hlayout) | |
| hlayout = r._dock_add_layout(vertical=False) | |
| r._dock_add_label( | |
| value="Rescale", | |
| align=True, | |
| layout=hlayout, | |
| ) | |
| r._dock_add_button( | |
| name="↺", | |
| callback=self._rescale, | |
| layout=hlayout, | |
| style="toolbutton", | |
| ) | |
| r._layout_add_widget(layout, hlayout) | |
| self._widgets["contours"] = r._dock_add_spin_box( | |
| name="Contour lines", | |
| value=21, | |
| rng=[0, 99], | |
| step=1, | |
| double=False, | |
| callback=self.set_contours, | |
| layout=layout, | |
| ) | |
| r._dock_finalize() | |
| def _on_time_change(self, event): | |
| """Respond to time_change UI event.""" | |
| new_time = np.clip(event.time, self._evoked.times[0], self._evoked.times[-1]) | |
| if new_time == self._current_time: | |
| return | |
| self._current_time = new_time | |
| self._update() | |
| def _on_colormap_range(self, event): | |
| """Response to the colormap_range UI event.""" | |
| if event.kind == "field_strength_meg": | |
| kind = "meg" | |
| elif event.kind == "field_strength_eeg": | |
| kind = "eeg" | |
| else: | |
| return | |
| for surf_map in self._surf_maps: | |
| if surf_map["map_kind"] == kind: | |
| break | |
| else: | |
| # No field map currently shown of the requested type. | |
| return | |
| vmin = event.fmin | |
| vmax = event.fmax | |
| surf_map["contours"] = np.linspace(vmin, vmax, self._n_contours) | |
| if self._show_density: | |
| surf_map["mesh"].update_overlay(name="field", rng=[vmin, vmax]) | |
| # Update the GUI widgets | |
| if kind == "meg": | |
| scaling = DEFAULTS["scalings"]["grad"] | |
| else: | |
| scaling = DEFAULTS["scalings"]["eeg"] | |
| with disable_ui_events(self): | |
| widget = self._widgets.get(f"vmax_slider_{kind}", None) | |
| if widget is not None: | |
| widget.set_value(vmax * scaling) | |
| widget = self._widgets.get(f"vmax_spin_{kind}", None) | |
| if widget is not None: | |
| widget.set_value(vmax * scaling) | |
| self._update() | |
| def _on_contours(self, event): | |
| """Respond to the contours UI event.""" | |
| if event.kind == "field_strength_meg": | |
| kind = "meg" | |
| elif event.kind == "field_strength_eeg": | |
| kind = "eeg" | |
| else: | |
| return | |
| for surf_map in self._surf_maps: | |
| if surf_map["map_kind"] == kind: | |
| break | |
| surf_map["contours"] = event.contours | |
| self._n_contours = len(event.contours) | |
| with disable_ui_events(self): | |
| if "contours" in self._widgets: | |
| self._widgets["contours"].set_value(len(event.contours)) | |
| self._update() | |
| def set_time(self, time): | |
| """Set the time to display (in seconds). | |
| Parameters | |
| ---------- | |
| time : float | |
| The time to show, in seconds. | |
| """ | |
| if self._evoked.times[0] <= time <= self._evoked.times[-1]: | |
| publish(self, TimeChange(time=time)) | |
| else: | |
| raise ValueError( | |
| f"Requested time ({time} s) is outside the range of " | |
| f"available times ({self._evoked.times[0]}-{self._evoked.times[-1]} s)." | |
| ) | |
| def set_contours(self, n_contours): | |
| """Adjust the number of contour lines to use when drawing the fieldlines. | |
| Parameters | |
| ---------- | |
| n_contours : int | |
| The number of contour lines to use. | |
| """ | |
| for surf_map in self._surf_maps: | |
| publish( | |
| self, | |
| Contours( | |
| kind=f"field_strength_{surf_map['map_kind']}", | |
| contours=np.linspace( | |
| -surf_map["map_vmax"], surf_map["map_vmax"], n_contours | |
| ).tolist(), | |
| ), | |
| ) | |
| def set_vmax(self, vmax, kind="meg"): | |
| """Change the color range of the density maps. | |
| Parameters | |
| ---------- | |
| vmax : float | |
| The new maximum value of the color range. | |
| kind : 'meg' | 'eeg' | |
| Which field map to apply the new color range to. | |
| """ | |
| _check_option("type", kind, ["eeg", "meg"]) | |
| for surf_map in self._surf_maps: | |
| if surf_map["map_kind"] == kind: | |
| publish( | |
| self, | |
| ColormapRange( | |
| kind=f"field_strength_{kind}", | |
| fmin=-vmax, | |
| fmax=vmax, | |
| ), | |
| ) | |
| break | |
| else: | |
| raise ValueError(f"No {type.upper()} field map currently shown.") | |
| def _rescale(self): | |
| """Rescale the fieldlines and density maps to the current time point.""" | |
| for surf_map in self._surf_maps: | |
| current_data = surf_map["data_interp"](self._current_time) | |
| vmax = float(np.max(current_data)) | |
| self.set_vmax(vmax, kind=surf_map["map_kind"]) | |