diff --git a/examples/visualization/plot_mne_helmet.py b/examples/visualization/plot_mne_helmet.py new file mode 100644 index 00000000000..1267f46ff67 --- /dev/null +++ b/examples/visualization/plot_mne_helmet.py @@ -0,0 +1,32 @@ +""" +Plot the MNE brain and helmet +============================= + +This tutorial shows how to make the MNE helmet + brain image. +""" + +import os.path as op +import mne + +sample_path = mne.datasets.sample.data_path() +subjects_dir = op.join(sample_path, 'subjects') +fname_evoked = op.join(sample_path, 'MEG', 'sample', 'sample_audvis-ave.fif') +fname_inv = op.join(sample_path, 'MEG', 'sample', + 'sample_audvis-meg-oct-6-meg-inv.fif') +fname_trans = op.join(sample_path, 'MEG', 'sample', + 'sample_audvis_raw-trans.fif') +inv = mne.minimum_norm.read_inverse_operator(fname_inv) +evoked = mne.read_evokeds(fname_evoked, baseline=(None, 0), + proj=True, verbose=False, condition='Left Auditory') +maps = mne.make_field_map(evoked, trans=fname_trans, ch_type='meg', + subject='sample', subjects_dir=subjects_dir) +time = 0.083 +fig = mne.viz.create_3d_figure((256, 256)) +mne.viz.plot_alignment( + evoked.info, subject='sample', subjects_dir=subjects_dir, fig=fig, + trans=fname_trans, meg='sensors', eeg=False, surfaces='pial', + coord_frame='mri') +evoked.plot_field(maps, time=time, fig=fig, time_label=None, vmax=5e-13) +mne.viz.set_3d_view( + fig, azimuth=40, elevation=87, focalpoint=(0., -0.01, 0.04), roll=-100, + distance=0.48) diff --git a/mne/evoked.py b/mne/evoked.py index d3393a547a9..2e965195df7 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -336,10 +336,11 @@ def plot_topomap(self, times="auto", ch_type=None, vmin=None, @copy_function_doc_to_method_doc(plot_evoked_field) def plot_field(self, surf_maps, time=None, time_label='t = %0.0f ms', - n_jobs=1, fig=None, verbose=None): + n_jobs=1, fig=None, vmax=None, n_contours=21, verbose=None): return plot_evoked_field(self, surf_maps, time=time, time_label=time_label, n_jobs=n_jobs, - fig=fig, verbose=verbose) + fig=fig, vmax=vmax, n_contours=n_contours, + verbose=verbose) @copy_function_doc_to_method_doc(plot_evoked_white) def plot_white(self, noise_cov, show=True, rank=None, time_unit='s', diff --git a/mne/icons/mne_icon.png b/mne/icons/mne_icon.png new file mode 100644 index 00000000000..66320cabb37 Binary files /dev/null and b/mne/icons/mne_icon.png differ diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index ee0b704bbd8..76bc8689c2c 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -304,7 +304,8 @@ def _set_aspect_equal(ax): @verbose def plot_evoked_field(evoked, surf_maps, time=None, time_label='t = %0.0f ms', - n_jobs=1, fig=None, verbose=None): + n_jobs=1, fig=None, vmax=None, n_contours=21, + verbose=None): """Plot MEG/EEG fields on head surface and helmet in 3D. Parameters @@ -316,7 +317,7 @@ def plot_evoked_field(evoked, surf_maps, time=None, time_label='t = %0.0f ms', 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 + time_label : str | None How to print info about the time instant visualized. %(n_jobs)s fig : instance of mayavi.core.api.Scene | None @@ -324,6 +325,14 @@ def plot_evoked_field(evoked, surf_maps, time=None, time_label='t = %0.0f ms', plot into the given figure. .. versionadded:: 0.20 + vmax : float | None + Maximum intensity. Can be None to use the max(abs(data)). + + .. versionadded:: 0.21 + n_contours : int + The number of contours. + + .. versionadded:: 0.21 %(verbose)s Returns @@ -334,6 +343,8 @@ def plot_evoked_field(evoked, surf_maps, time=None, time_label='t = %0.0f ms', # Update the backend from .backends.renderer import _get_renderer types = [t for t in ['eeg', 'grad', 'mag'] if t in evoked] + _validate_type(vmax, (None, 'numeric'), 'vmax') + n_contours = _ensure_int(n_contours, 'n_contours') time_idx = None if time is None: @@ -382,23 +393,27 @@ def plot_evoked_field(evoked, surf_maps, time=None, time_label='t = %0.0f ms', data = np.dot(map_data, evoked.data[pick, time_idx]) # Make a solid surface - vlim = np.max(np.abs(data)) + if vmax is None: + vmax = np.max(np.abs(data)) + vmax = float(vmax) alpha = alphas[ii] renderer.surface(surface=surf, color=colors[ii], opacity=alpha) # Now show our field pattern - renderer.surface(surface=surf, vmin=-vlim, vmax=vlim, - scalars=data, colormap=colormap) + renderer.surface(surface=surf, vmin=-vmax, vmax=vmax, + scalars=data, colormap=colormap, + polygon_offset=-1) # And the field lines on top - renderer.contour(surface=surf, scalars=data, contours=21, - vmin=-vlim, vmax=vlim, opacity=alpha, + renderer.contour(surface=surf, scalars=data, contours=n_contours, + vmin=-vmax, vmax=vmax, opacity=alpha, colormap=colormap_lines) - if '%' in time_label: - time_label %= (1e3 * evoked.times[time_idx]) - renderer.text2d(x_window=0.01, y_window=0.01, text=time_label) + if time_label is not None: + if '%' in time_label: + time_label %= (1e3 * evoked.times[time_idx]) + renderer.text2d(x_window=0.01, y_window=0.01, text=time_label) renderer.set_camera(azimuth=10, elevation=60) renderer.show() return renderer.scene() diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 49afb196ac6..58410918653 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -632,13 +632,13 @@ def _add_surface_data(self, hemi): z=self.geo[hemi].coords[:, 2], triangles=self.geo[hemi].faces, normals=self.geo[hemi].nn, + polygon_offset=-2, **kwargs, ) if isinstance(mesh_data, tuple): actor, mesh = mesh_data # add metadata to the mesh for picking mesh._hemi = hemi - self.resolve_coincident_topology(actor) else: actor, mesh = mesh_data, None return actor, mesh @@ -865,10 +865,8 @@ def add_label(self, label, color=None, alpha=1, scalar_thresh=None, color=None, colormap=ctable, backface_culling=False, + polygon_offset=-2, ) - if isinstance(mesh_data, tuple): - actor, _ = mesh_data - self.resolve_coincident_topology(actor) self._label_data.append(mesh_data) self._renderer.set_camera(**views_dicts[hemi][v]) @@ -1077,6 +1075,7 @@ def add_annotation(self, annot, borders=True, alpha=1, hemi=None, vmax=np.max(ids), scalars=ids, interpolate_before_map=False, + polygon_offset=-2, ) if isinstance(mesh_data, tuple): from ..backends._pyvista import _set_colormap_range @@ -1085,17 +1084,9 @@ def add_annotation(self, annot, borders=True, alpha=1, hemi=None, mesh._hemi = hemi _set_colormap_range(actor, cmap.astype(np.uint8), None) - self.resolve_coincident_topology(actor) self._update() - def resolve_coincident_topology(self, actor): - """Resolve z-fighting of overlapping surfaces.""" - mapper = actor.GetMapper() - mapper.SetResolveCoincidentTopologyToPolygonOffset() - mapper.SetRelativeCoincidentTopologyPolygonOffsetParameters( - -1., -1.) - def close(self): """Close all figures and cleanup data structure.""" self._closed = True diff --git a/mne/viz/backends/_pysurfer_mayavi.py b/mne/viz/backends/_pysurfer_mayavi.py index 8f40f86ec71..5077a81dee5 100644 --- a/mne/viz/backends/_pysurfer_mayavi.py +++ b/mne/viz/backends/_pysurfer_mayavi.py @@ -99,8 +99,11 @@ def mesh(self, x, y, z, triangles, color, opacity=1.0, shading=False, backface_culling=False, scalars=None, colormap=None, vmin=None, vmax=None, interpolate_before_map=True, representation='surface', line_width=1., normals=None, - pickable=None, **kwargs): + polygon_offset=None, **kwargs): # normals and pickable are unused + kwargs.pop('pickable', None) + del normals + if color is not None: color = _check_color(color) if color is not None and isinstance(color, np.ndarray) \ @@ -162,7 +165,7 @@ def contour(self, surface, scalars, contours, width=1.0, opacity=1.0, def surface(self, surface, color=None, opacity=1.0, vmin=None, vmax=None, colormap=None, normalized_colormap=False, scalars=None, - backface_culling=False): + backface_culling=False, polygon_offset=None): if color is not None: color = _check_color(color) if normalized_colormap: diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index d3b28e89fdd..cff85dfaeb9 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -292,7 +292,8 @@ def set_interaction(self, interaction): def polydata(self, mesh, color=None, opacity=1.0, normals=None, backface_culling=False, scalars=None, colormap=None, vmin=None, vmax=None, interpolate_before_map=True, - representation='surface', line_width=1., **kwargs): + representation='surface', line_width=1., + polygon_offset=None, **kwargs): with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) rgba = False @@ -325,12 +326,19 @@ def polydata(self, mesh, color=None, opacity=1.0, normals=None, style=representation, line_width=line_width, **kwargs, ) + if polygon_offset is not None: + mapper = actor.GetMapper() + mapper.SetResolveCoincidentTopologyToPolygonOffset() + mapper.SetRelativeCoincidentTopologyPolygonOffsetParameters( + polygon_offset, polygon_offset) + return actor, mesh def mesh(self, x, y, z, triangles, color, opacity=1.0, shading=False, backface_culling=False, scalars=None, colormap=None, vmin=None, vmax=None, interpolate_before_map=True, - representation='surface', line_width=1., normals=None, **kwargs): + representation='surface', line_width=1., normals=None, + polygon_offset=None, **kwargs): with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) vertices = np.c_[x, y, z] @@ -349,6 +357,7 @@ def mesh(self, x, y, z, triangles, color, opacity=1.0, shading=False, interpolate_before_map=interpolate_before_map, representation=representation, line_width=line_width, + polygon_offset=polygon_offset, **kwargs, ) @@ -366,7 +375,7 @@ def contour(self, surface, scalars, contours, width=1.0, opacity=1.0, triangles = np.c_[np.full(n_triangles, 3), triangles] mesh = PolyData(vertices, triangles) mesh.point_arrays['scalars'] = scalars - contour = mesh.contour(isosurfaces=contours, rng=(vmin, vmax)) + contour = mesh.contour(isosurfaces=contours) line_width = width if kind == 'tube': contour = contour.tube(radius=width, n_sides=self.tube_n_sides) @@ -377,6 +386,7 @@ def contour(self, surface, scalars, contours, width=1.0, opacity=1.0, show_scalar_bar=False, line_width=line_width, color=color, + rng=[vmin, vmax], cmap=colormap, opacity=opacity, smooth_shading=self.figure.smooth_shading @@ -386,7 +396,7 @@ def contour(self, surface, scalars, contours, width=1.0, opacity=1.0, def surface(self, surface, color=None, opacity=1.0, vmin=None, vmax=None, colormap=None, normalized_colormap=False, scalars=None, - backface_culling=False): + backface_culling=False, polygon_offset=None): with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) normals = surface.get('nn', None) @@ -407,6 +417,7 @@ def surface(self, surface, color=None, opacity=1.0, colormap=colormap, vmin=vmin, vmax=vmax, + polygon_offset=polygon_offset, ) def sphere(self, center, color, scale, opacity=1.0, @@ -743,8 +754,6 @@ def _set_3d_view(figure, azimuth, elevation, focalpoint, distance, roll=None): phi = _deg2rad(azimuth) if elevation is not None: theta = _deg2rad(elevation) - if roll is not None: - roll = _deg2rad(roll) renderer = figure.plotter.renderer bounds = np.array(renderer.ComputeVisiblePropBounds()) diff --git a/mne/viz/backends/base_renderer.py b/mne/viz/backends/base_renderer.py index 94ab73ff5df..f79f81352bf 100644 --- a/mne/viz/backends/base_renderer.py +++ b/mne/viz/backends/base_renderer.py @@ -36,7 +36,8 @@ def set_interaction(self, interaction): def mesh(self, x, y, z, triangles, color, opacity=1.0, shading=False, backface_culling=False, scalars=None, colormap=None, vmin=None, vmax=None, interpolate_before_map=True, - representation='surface', line_width=1., normals=None, **kwargs): + representation='surface', line_width=1., normals=None, + polygon_offset=None, **kwargs): """Add a mesh in the scene. Parameters @@ -79,6 +80,8 @@ def mesh(self, x, y, z, triangles, color, opacity=1.0, shading=False, The width of the lines when representation='wireframe'. normals: array, shape (n_vertices, 3) The array containing the normal of each vertex. + polygon_offset: float + If not None, the factor used to resolve coincident topology. kwargs: args The arguments to pass to triangular_mesh @@ -128,8 +131,9 @@ def contour(self, surface, scalars, contours, width=1.0, opacity=1.0, @abstractclassmethod def surface(self, surface, color=None, opacity=1.0, - vmin=None, vmax=None, colormap=None, scalars=None, - backface_culling=False): + vmin=None, vmax=None, colormap=None, + normalized_colormap=False, scalars=None, + backface_culling=False, polygon_offset=None): """Add a surface in the scene. Parameters @@ -154,6 +158,8 @@ def surface(self, surface, color=None, opacity=1.0, The scalar valued associated to the vertices. backface_culling: bool If True, enable backface culling on the surface. + polygon_offset: float + If not None, the factor used to resolve coincident topology. """ pass diff --git a/mne/viz/backends/renderer.py b/mne/viz/backends/renderer.py index 6bf058deaf4..66863a001bc 100644 --- a/mne/viz/backends/renderer.py +++ b/mne/viz/backends/renderer.py @@ -200,7 +200,7 @@ def _use_test_3d_backend(backend_name, interactive=False): def set_3d_view(figure, azimuth=None, elevation=None, - focalpoint=None, distance=None): + focalpoint=None, distance=None, roll=None): """Configure the view of the given scene. Parameters @@ -215,10 +215,12 @@ def set_3d_view(figure, azimuth=None, elevation=None, The focal point of the view: (x, y, z). distance : float The distance to the focal point. + roll : float + The view roll. """ backend._set_3d_view(figure=figure, azimuth=azimuth, elevation=elevation, focalpoint=focalpoint, - distance=distance) + distance=distance, roll=roll) def set_3d_title(figure, title, size=40):