From f1a1c240a5dfe9ed226c231196c34438ffa3b4b1 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 2 Sep 2019 18:17:16 +0200 Subject: [PATCH 01/88] Add cortex variable --- mne/viz/_3d.py | 3 ++- mne/viz/_brain/_brain.py | 27 ++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index 1f9f5247a52..db582fa547f 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -1705,7 +1705,8 @@ def plot_source_estimates(stc, subject=None, surface='inflated', hemi='lh', time_unit=time_unit, background=background, spacing=spacing, time_viewer=time_viewer, colorbar=colorbar, transparent=transparent) - from surfer import Brain, TimeViewer + from ._brain import _Brain as Brain + from surfer import TimeViewer _check_option('hemi', hemi, ['lh', 'rh', 'split', 'both']) time_label, times = _handle_time(time_label, time_unit, stc.times) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index ae961ba84bf..a06678037f4 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -36,6 +36,28 @@ class _Brain(object): freesurfer surface mesh name (ie 'white', 'inflated', etc.). title : str Title for the window. + cortex : str, tuple, dict, or None + Specifies how the cortical surface is rendered. Options: + + 1. The name of one of the preset cortex styles: + ``'classic'`` (default), ``'high_contrast'``, + ``'low_contrast'``, or ``'bone'``. + 2. A color-like argument to render the cortex as a single + color, e.g. ``'red'`` or ``(0.1, 0.4, 1.)``. Setting + this to ``None`` is equivalent to ``(0.5, 0.5, 0.5)``. + 3. The name of a colormap used to render binarized + curvature values, e.g., ``Grays``. + 4. A list of colors used to render binarized curvature + values. Only the first and last colors are used. E.g., + ['red', 'blue'] or [(1, 0, 0), (0, 0, 1)]. + 5. A container with four entries for colormap (string + specifiying the name of a colormap), vmin (float + specifying the minimum value for the colormap), vmax + (float specifying the maximum value for the colormap), + and reverse (bool specifying whether the colormap + should be reversed. E.g., ``('Greys', -1, 2, False)``. + 6. A dict of keyword arguments that is passed on to the + call to surface. alpha : float in [0, 1] Alpha level to control opacity of the cortical surface. size : float | tuple(float, float) @@ -186,13 +208,16 @@ class _Brain(object): """ def __init__(self, subject_id, hemi, surf, title=None, - alpha=1.0, size=800, background=(0, 0, 0), + cortex=None, alpha=1.0, size=800, background=(0, 0, 0), foreground=None, figure=None, subjects_dir=None, views=['lateral'], offset=True, show_toolbar=False, offscreen=False, interaction=None, units='mm'): if hemi == 'split': raise ValueError('Option hemi="split" is not supported yet.') + if cortex is not None: + raise ValueError('cortex parameter is not supported yet.') + if figure is not None: raise ValueError('figure parameter is not supported yet.') From 3fd096c0257613268d7da844af6692c9089a4b2d Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 2 Sep 2019 18:21:32 +0200 Subject: [PATCH 02/88] Fix typo --- mne/viz/_brain/_brain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index a06678037f4..e6331f6ab3a 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -51,7 +51,7 @@ class _Brain(object): values. Only the first and last colors are used. E.g., ['red', 'blue'] or [(1, 0, 0), (0, 0, 1)]. 5. A container with four entries for colormap (string - specifiying the name of a colormap), vmin (float + specifying the name of a colormap), vmin (float specifying the minimum value for the colormap), vmax (float specifying the maximum value for the colormap), and reverse (bool specifying whether the colormap From 4eb2c685ff79104093e9a216d9138e504cb9b68a Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 2 Sep 2019 18:48:33 +0200 Subject: [PATCH 03/88] Add cortex support --- mne/viz/_brain/_brain.py | 112 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 5 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index e6331f6ab3a..8d6616e2e04 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -208,16 +208,13 @@ class _Brain(object): """ def __init__(self, subject_id, hemi, surf, title=None, - cortex=None, alpha=1.0, size=800, background=(0, 0, 0), + cortex=None, alpha=1.0, size=800, background="black", foreground=None, figure=None, subjects_dir=None, views=['lateral'], offset=True, show_toolbar=False, offscreen=False, interaction=None, units='mm'): if hemi == 'split': raise ValueError('Option hemi="split" is not supported yet.') - if cortex is not None: - raise ValueError('cortex parameter is not supported yet.') - if figure is not None: raise ValueError('figure parameter is not supported yet.') @@ -225,6 +222,12 @@ def __init__(self, subject_id, hemi, surf, title=None, raise ValueError('"interaction" parameter is not supported.') from ..backends.renderer import _Renderer + from matplotlib.colors import colorConverter + + if isinstance(background, str): + background = colorConverter.to_rgb(background) + if isinstance(foreground, str): + foreground = colorConverter.to_rgb(foreground) self._foreground = foreground self._hemi = hemi @@ -257,7 +260,7 @@ def __init__(self, subject_id, hemi, surf, title=None, fig_size = size else: raise ValueError('"size" parameter must be int or tuple.') - + geo_kwargs, geo_reverse, geo_curv = self._get_geo_params(cortex, alpha) for h in self._hemis: # Initialize a Surface object as the geometry geo = Surface(subject_id, h, surf, subjects_dir, offset, @@ -267,6 +270,8 @@ def __init__(self, subject_id, hemi, surf, title=None, geo.load_curvature() self.geo[h] = geo + if isinstance(views, str): + views = [views] for ri, v in enumerate(views): renderer = _Renderer(size=fig_size, bgcolor=background) self._renderers[ri].append(renderer) @@ -553,6 +558,103 @@ def _check_hemi(self, hemi): extra + ", got " + str(hemi)) return hemi + def _get_geo_params(self, cortex, alpha=1.0): + """Return keyword arguments and other parameters for surface + rendering. + + Parameters + ---------- + cortex : {str, tuple, dict, None} + Can be set to: (1) the name of one of the preset cortex + styles ('classic', 'high_contrast', 'low_contrast', or + 'bone'), (2) the name of a colormap, (3) a tuple with + four entries for (colormap, vmin, vmax, reverse) + indicating the name of the colormap, the min and max + values respectively and whether or not the colormap should + be reversed, (4) a valid color specification (such as a + 3-tuple with RGB values or a valid color name), or (5) a + dictionary of keyword arguments that is passed on to the + call to surface. If set to None, color is set to (0.5, + 0.5, 0.5). + alpha : float in [0, 1] + Alpha level to control opacity of the cortical surface. + + Returns + ------- + kwargs : dict + Dictionary with keyword arguments to be used for surface + rendering. For colormaps, keys are ['colormap', 'vmin', + 'vmax', 'alpha'] to specify the name, minimum, maximum, + and alpha transparency of the colormap respectively. For + colors, keys are ['color', 'alpha'] to specify the name + and alpha transparency of the color respectively. + reverse : boolean + Boolean indicating whether a colormap should be + reversed. Set to False if a color (rather than a colormap) + is specified. + curv : boolean + Boolean indicating whether curv file is loaded and binary + curvature is displayed. + + """ + from matplotlib.colors import colorConverter + colormap_map = dict(classic=(dict(colormap="Greys", + vmin=-1, vmax=2, + opacity=alpha), False, True), + high_contrast=(dict(colormap="Greys", + vmin=-.1, vmax=1.3, + opacity=alpha), False, True), + low_contrast=(dict(colormap="Greys", + vmin=-5, vmax=5, + opacity=alpha), False, True), + bone=(dict(colormap="bone", + vmin=-.2, vmax=2, + opacity=alpha), True, True)) + if isinstance(cortex, dict): + if 'opacity' not in cortex: + cortex['opacity'] = alpha + if 'colormap' in cortex: + if 'vmin' not in cortex: + cortex['vmin'] = -1 + if 'vmax' not in cortex: + cortex['vmax'] = 2 + geo_params = cortex, False, True + elif isinstance(cortex, str): + if cortex in colormap_map: + geo_params = colormap_map[cortex] + elif cortex in lut_manager.lut_mode_list(): + geo_params = dict(colormap=cortex, vmin=-1, vmax=2, + opacity=alpha), False, True + else: + try: + color = colorConverter.to_rgb(cortex) + geo_params = dict(color=color, opacity=alpha), False, False + except ValueError: + geo_params = cortex, False, True + # check for None before checking len: + elif cortex is None: + geo_params = dict(color=(0.5, 0.5, 0.5), + opacity=alpha), False, False + # Test for 4-tuple specifying colormap parameters. Need to + # avoid 4 letter strings and 4-tuples not specifying a + # colormap name in the first position (color can be specified + # as RGBA tuple, but the A value will be dropped by to_rgb()): + elif (len(cortex) == 4) and (isinstance(cortex[0], string_types)): + geo_params = dict(colormap=cortex[0], vmin=cortex[1], + vmax=cortex[2], opacity=alpha), cortex[3], True + else: + try: # check if it's a non-string color specification + color = colorConverter.to_rgb(cortex) + geo_params = dict(color=color, opacity=alpha), False, False + except ValueError: + try: + lut = create_color_lut(cortex) + geo_params = dict(colormap="Greys", opacity=alpha, + lut=lut), False, True + except ValueError: + geo_params = cortex, False, True + return geo_params + def _update_limits(fmin, fmid, fmax, center, array): if center is None: From 83339e018d2d0069018a4ee2214d2a85049c359a Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 2 Sep 2019 19:13:30 +0200 Subject: [PATCH 04/88] Fix kwargs --- mne/viz/_3d.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index db582fa547f..013f29536e0 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -1727,10 +1727,7 @@ def plot_source_estimates(stc, subject=None, surface='inflated', hemi='lh', background=background, foreground=foreground, figure=figure, subjects_dir=subjects_dir, views=views) - - ad_kwargs, sd_kwargs = _get_ps_kwargs( - initial_time, diverging, scale_pts[1], transparent) - del initial_time, transparent + center = 0. if diverging else None for hemi in hemis: hemi_idx = 0 if hemi == 'lh' else 1 data = getattr(stc, hemi + '_data') @@ -1741,10 +1738,9 @@ def plot_source_estimates(stc, subject=None, surface='inflated', hemi='lh', smoothing_steps=smoothing_steps, time=times, time_label=time_label, alpha=alpha, hemi=hemi, colorbar=colorbar, - min=scale_pts[0], max=scale_pts[2], **ad_kwargs) - if 'mid' not in ad_kwargs: # PySurfer < 0.9 - brain.scale_data_colormap(fmin=scale_pts[0], fmid=scale_pts[1], - fmax=scale_pts[2], **sd_kwargs) + fmin=scale_pts[0], fmid=scale_pts[1], + fmax=scale_pts[2], transparent=transparent, + center=center, verbose=False) if time_viewer: TimeViewer(brain) return brain From 3517e2717b3c7cd92ed09cab871690c8f1990e4d Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 3 Sep 2019 14:38:02 +0200 Subject: [PATCH 05/88] Reduce get_geo argument to style, valid color name or None --- mne/viz/_brain/_brain.py | 77 ++++++++-------------------------------- 1 file changed, 14 insertions(+), 63 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 8d6616e2e04..a130831cba8 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -36,28 +36,12 @@ class _Brain(object): freesurfer surface mesh name (ie 'white', 'inflated', etc.). title : str Title for the window. - cortex : str, tuple, dict, or None - Specifies how the cortical surface is rendered. Options: - - 1. The name of one of the preset cortex styles: - ``'classic'`` (default), ``'high_contrast'``, - ``'low_contrast'``, or ``'bone'``. - 2. A color-like argument to render the cortex as a single - color, e.g. ``'red'`` or ``(0.1, 0.4, 1.)``. Setting - this to ``None`` is equivalent to ``(0.5, 0.5, 0.5)``. - 3. The name of a colormap used to render binarized - curvature values, e.g., ``Grays``. - 4. A list of colors used to render binarized curvature - values. Only the first and last colors are used. E.g., - ['red', 'blue'] or [(1, 0, 0), (0, 0, 1)]. - 5. A container with four entries for colormap (string - specifying the name of a colormap), vmin (float - specifying the minimum value for the colormap), vmax - (float specifying the maximum value for the colormap), - and reverse (bool specifying whether the colormap - should be reversed. E.g., ``('Greys', -1, 2, False)``. - 6. A dict of keyword arguments that is passed on to the - call to surface. + cortex : str or None + Specifies how the cortical surface is rendered. + The name of one of the preset cortex styles can be: + ``'classic'`` (default), ``'high_contrast'``, + ``'low_contrast'``, or ``'bone'`` or a valid color name. + Setting this to ``None`` is equivalent to ``(0.5, 0.5, 0.5)`` alpha : float in [0, 1] Alpha level to control opacity of the cortical surface. size : float | tuple(float, float) @@ -260,6 +244,7 @@ def __init__(self, subject_id, hemi, surf, title=None, fig_size = size else: raise ValueError('"size" parameter must be int or tuple.') + geo_kwargs, geo_reverse, geo_curv = self._get_geo_params(cortex, alpha) for h in self._hemis: # Initialize a Surface object as the geometry @@ -564,18 +549,11 @@ def _get_geo_params(self, cortex, alpha=1.0): Parameters ---------- - cortex : {str, tuple, dict, None} - Can be set to: (1) the name of one of the preset cortex + cortex : str or None + The name of one of the preset cortex styles ('classic', 'high_contrast', 'low_contrast', or - 'bone'), (2) the name of a colormap, (3) a tuple with - four entries for (colormap, vmin, vmax, reverse) - indicating the name of the colormap, the min and max - values respectively and whether or not the colormap should - be reversed, (4) a valid color specification (such as a - 3-tuple with RGB values or a valid color name), or (5) a - dictionary of keyword arguments that is passed on to the - call to surface. If set to None, color is set to (0.5, - 0.5, 0.5). + 'bone') or a valid color name. If set to None, + color is set to (0.5, 0.5, 0.5). alpha : float in [0, 1] Alpha level to control opacity of the cortical surface. @@ -610,21 +588,9 @@ def _get_geo_params(self, cortex, alpha=1.0): bone=(dict(colormap="bone", vmin=-.2, vmax=2, opacity=alpha), True, True)) - if isinstance(cortex, dict): - if 'opacity' not in cortex: - cortex['opacity'] = alpha - if 'colormap' in cortex: - if 'vmin' not in cortex: - cortex['vmin'] = -1 - if 'vmax' not in cortex: - cortex['vmax'] = 2 - geo_params = cortex, False, True - elif isinstance(cortex, str): + if isinstance(cortex, str): if cortex in colormap_map: geo_params = colormap_map[cortex] - elif cortex in lut_manager.lut_mode_list(): - geo_params = dict(colormap=cortex, vmin=-1, vmax=2, - opacity=alpha), False, True else: try: color = colorConverter.to_rgb(cortex) @@ -635,24 +601,9 @@ def _get_geo_params(self, cortex, alpha=1.0): elif cortex is None: geo_params = dict(color=(0.5, 0.5, 0.5), opacity=alpha), False, False - # Test for 4-tuple specifying colormap parameters. Need to - # avoid 4 letter strings and 4-tuples not specifying a - # colormap name in the first position (color can be specified - # as RGBA tuple, but the A value will be dropped by to_rgb()): - elif (len(cortex) == 4) and (isinstance(cortex[0], string_types)): - geo_params = dict(colormap=cortex[0], vmin=cortex[1], - vmax=cortex[2], opacity=alpha), cortex[3], True else: - try: # check if it's a non-string color specification - color = colorConverter.to_rgb(cortex) - geo_params = dict(color=color, opacity=alpha), False, False - except ValueError: - try: - lut = create_color_lut(cortex) - geo_params = dict(colormap="Greys", opacity=alpha, - lut=lut), False, True - except ValueError: - geo_params = cortex, False, True + raise TypeError("Expected type is `str` or `NoneType`, " + "{} is given.".format(type(cortex))) return geo_params From 528b86c720ab23de1b2d08766a06dd70a084fdc3 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 3 Sep 2019 14:53:55 +0200 Subject: [PATCH 06/88] Add transparent and verbose variables --- mne/viz/_brain/_brain.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index a130831cba8..4a157473a04 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -41,7 +41,7 @@ class _Brain(object): The name of one of the preset cortex styles can be: ``'classic'`` (default), ``'high_contrast'``, ``'low_contrast'``, or ``'bone'`` or a valid color name. - Setting this to ``None`` is equivalent to ``(0.5, 0.5, 0.5)`` + Setting this to ``None`` is equivalent to ``(0.5, 0.5, 0.5)``. alpha : float in [0, 1] Alpha level to control opacity of the cortical surface. size : float | tuple(float, float) @@ -212,6 +212,8 @@ def __init__(self, subject_id, hemi, surf, title=None, background = colorConverter.to_rgb(background) if isinstance(foreground, str): foreground = colorConverter.to_rgb(foreground) + if isinstance(views, str): + views = [views] self._foreground = foreground self._hemi = hemi @@ -255,8 +257,6 @@ def __init__(self, subject_id, hemi, surf, title=None, geo.load_curvature() self.geo[h] = geo - if isinstance(views, str): - views = [views] for ri, v in enumerate(views): renderer = _Renderer(size=fig_size, bgcolor=background) self._renderers[ri].append(renderer) @@ -324,7 +324,6 @@ def add_data(self, array, fmin=None, fmid=None, fmax=None, if not None, center of a divergent colormap, changes the meaning of fmin, fmax and fmid. transparent : bool - Not supported yet. if True: use a linear transparency between fmin and fmid and make values below fmin fully transparent (symmetrically for divergent colormaps) @@ -389,14 +388,15 @@ def add_data(self, array, fmin=None, fmid=None, fmax=None, if len(array.shape) == 3: raise ValueError('Vector values in "array" are not supported.') + _check_option('transparent', type(transparent), [bool]) + _check_option('verbose', type(verbose), [bool]) + # those parameters are not supported yet, only None is allowed _check_option('thresh', thresh, [None]) - _check_option('transparent', transparent, [None]) _check_option('remove_existing', remove_existing, [None]) _check_option('time_label_size', time_label_size, [None]) _check_option('scale_factor', scale_factor, [None]) _check_option('vector_alpha', vector_alpha, [None]) - _check_option('verbose', verbose, [None]) from surfer.utils import mesh_edges, smoothing_matrix @@ -450,7 +450,7 @@ def add_data(self, array, fmin=None, fmid=None, fmax=None, # data mapping into [0, 1] interval dt_max = fmax - dt_min = fmin if center is None else -1 * max + dt_min = fmin if center is None else -1 * fmax k = 1 / (dt_max - dt_min) b = 1 - k * dt_max act_data = k * act_data + b From b42b0bb7fdacfa124af8feee6a32b3866206f870 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 4 Sep 2019 14:40:46 +0200 Subject: [PATCH 07/88] Fix initial_time --- mne/viz/_3d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index 013f29536e0..6beba744a9d 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -1737,7 +1737,7 @@ def plot_source_estimates(stc, subject=None, surface='inflated', hemi='lh', brain.add_data(data, colormap=colormap, vertices=vertices, smoothing_steps=smoothing_steps, time=times, time_label=time_label, alpha=alpha, hemi=hemi, - colorbar=colorbar, + colorbar=colorbar, initial_time=initial_time, fmin=scale_pts[0], fmid=scale_pts[1], fmax=scale_pts[2], transparent=transparent, center=center, verbose=False) From b7b3a05a6ccb9bc5ad9ec572aeb430de01e01d73 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 4 Sep 2019 15:36:04 +0200 Subject: [PATCH 08/88] Add text and colorbar --- mne/viz/_brain/_brain.py | 9 ++++----- mne/viz/backends/_pyvista.py | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 4a157473a04..5c1374a3b8d 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -472,13 +472,12 @@ def add_data(self, array, fmin=None, fmid=None, fmax=None, z=self.geo[hemi].coords[:, 2], triangles=self.geo[hemi].faces, color=act_color) + if array.ndim >= 2: + renderer.text2d(x=0.95, y=0.05, text='time={}'.format(time[0]), + width=time_label_size) + renderer.scalarbar(source=None, n_labels=8) self._overlays[hemi + '_' + v] = mesh - # How can we make this bit universal as well??? - # if colorbar and not self._colorbar_added: - # ColorBar(self) - # self._colorbar_added = True - def show(self): u"""Display widget.""" try: diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 9975cd091fa..31cbe15f8c2 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -320,10 +320,11 @@ def quiver3d(self, x, y, z, u, v, w, color, scale, mode, resolution=8, smooth_shading) def text2d(self, x, y, text, width, color=(1.0, 1.0, 1.0)): + width = int(width * 100) if isinstance(width, float) else width with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) self.plotter.add_text(text, position=(x, y), - font_size=int(width * 100), + font_size=width, color=color) def text3d(self, x, y, z, text, scale, color=(1.0, 1.0, 1.0)): From d1c46badd447671bed93a31b6cfbcea017f5cb26 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 4 Sep 2019 16:53:57 +0200 Subject: [PATCH 09/88] Add scalars and colormap in mesh() for the pyvista backend --- mne/viz/_brain/_brain.py | 6 ++++-- mne/viz/backends/_pyvista.py | 7 +++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 5c1374a3b8d..69ead274b76 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -471,11 +471,13 @@ def add_data(self, array, fmin=None, fmid=None, fmax=None, y=self.geo[hemi].coords[:, 1], z=self.geo[hemi].coords[:, 2], triangles=self.geo[hemi].faces, - color=act_color) + color=act_color, + colormap=colormap, + scalars=act_data) if array.ndim >= 2: renderer.text2d(x=0.95, y=0.05, text='time={}'.format(time[0]), width=time_label_size) - renderer.scalarbar(source=None, n_labels=8) + renderer.scalarbar(source=mesh, n_labels=8) self._overlays[hemi + '_' + v] = mesh def show(self): diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 31cbe15f8c2..da31a7aeadc 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -123,7 +123,7 @@ def set_interactive(self): self.plotter.enable_terrain_style() def mesh(self, x, y, z, triangles, color, opacity=1.0, shading=False, - backface_culling=False, **kwargs): + backface_culling=False, scalars=None, colormap=None, **kwargs): with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) from pyvista import PolyData @@ -132,6 +132,8 @@ def mesh(self, x, y, z, triangles, color, opacity=1.0, shading=False, n_vertices = len(vertices) triangles = np.c_[np.full(len(triangles), 3), triangles] pd = PolyData(vertices, triangles) + if scalars is not None: + pd.point_arrays['scalars'] = scalars if len(color) == n_vertices: if color.shape[1] == 3: scalars = np.c_[color, np.ones(n_vertices)] @@ -149,7 +151,7 @@ def mesh(self, x, y, z, triangles, color, opacity=1.0, shading=False, rgba = False self.plotter.add_mesh(mesh=pd, color=color, scalars=scalars, - rgba=rgba, opacity=opacity, + rgba=rgba, opacity=opacity, cmap=colormap, backface_culling=backface_culling, smooth_shading=smooth_shading) @@ -341,6 +343,7 @@ def scalarbar(self, source, title=None, n_labels=4): with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) self.plotter.add_scalar_bar(title=title, n_labels=n_labels, + use_opacity=False, position_x=0.15, width=0.7) def show(self): From ea4d7eecbf6f1b69f6526bb0a3c38479a2d5acd1 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 5 Sep 2019 11:25:39 +0200 Subject: [PATCH 10/88] Import mesh_edges and smoothing_matrix --- mne/viz/_brain/_brain.py | 3 +- mne/viz/_brain/utils.py | 92 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 mne/viz/_brain/utils.py diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 69ead274b76..587fe26714e 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -12,6 +12,7 @@ from .colormap import _calculate_lut from .view import views_dict from .surface import Surface +from .utils import mesh_edges, smoothing_matrix from ..utils import _check_option, logger @@ -398,8 +399,6 @@ def add_data(self, array, fmin=None, fmid=None, fmax=None, _check_option('scale_factor', scale_factor, [None]) _check_option('vector_alpha', vector_alpha, [None]) - from surfer.utils import mesh_edges, smoothing_matrix - hemi = self._check_hemi(hemi) array = np.asarray(array) diff --git a/mne/viz/_brain/utils.py b/mne/viz/_brain/utils.py new file mode 100644 index 00000000000..1ce5f05b0fa --- /dev/null +++ b/mne/viz/_brain/utils.py @@ -0,0 +1,92 @@ +from ..utils import logger +import numpy as np +from scipy import sparse + + +def mesh_edges(faces): + """Returns sparse matrix with edges as an adjacency matrix + + Parameters + ---------- + faces : array of shape [n_triangles x 3] + The mesh faces + + Returns + ------- + edges : sparse matrix + The adjacency matrix + """ + npoints = np.max(faces) + 1 + nfaces = len(faces) + a, b, c = faces.T + edges = sparse.coo_matrix((np.ones(nfaces), (a, b)), + shape=(npoints, npoints)) + edges = edges + sparse.coo_matrix((np.ones(nfaces), (b, c)), + shape=(npoints, npoints)) + edges = edges + sparse.coo_matrix((np.ones(nfaces), (c, a)), + shape=(npoints, npoints)) + edges = edges + edges.T + edges = edges.tocoo() + return edges + + +def smoothing_matrix(vertices, adj_mat, smoothing_steps=20, verbose=None): + """Create a smoothing matrix which can be used to interpolate data defined + for a subset of vertices onto mesh with an adjancency matrix given by + adj_mat. + + If smoothing_steps is None, as many smoothing steps are applied until + the whole mesh is filled with with non-zeros. Only use this option if + the vertices correspond to a subsampled version of the mesh. + + Parameters + ---------- + vertices : 1d array + vertex indices + adj_mat : sparse matrix + N x N adjacency matrix of the full mesh + smoothing_steps : int or None + number of smoothing steps (Default: 20) + verbose : bool, str, int, or None + If not None, override default verbose level (see surfer.verbose). + + Returns + ------- + smooth_mat : sparse matrix + smoothing matrix with size N x len(vertices) + """ + from scipy import sparse + + logger.info("Updating smoothing matrix, be patient..") + + e = adj_mat.copy() + e.data[e.data == 2] = 1 + n_vertices = e.shape[0] + e = e + sparse.eye(n_vertices, n_vertices) + idx_use = vertices + smooth_mat = 1.0 + n_iter = smoothing_steps if smoothing_steps is not None else 1000 + for k in range(n_iter): + e_use = e[:, idx_use] + + data1 = e_use * np.ones(len(idx_use)) + idx_use = np.where(data1)[0] + scale_mat = sparse.dia_matrix((1 / data1[idx_use], 0), + shape=(len(idx_use), len(idx_use))) + + smooth_mat = scale_mat * e_use[idx_use, :] * smooth_mat + + logger.info("Smoothing matrix creation, step %d" % (k + 1)) + if smoothing_steps is None and len(idx_use) >= n_vertices: + break + + # Make sure the smoothing matrix has the right number of rows + # and is in COO format + smooth_mat = smooth_mat.tocoo() + smooth_mat = sparse.coo_matrix((smooth_mat.data, + (idx_use[smooth_mat.row], + smooth_mat.col)), + shape=(n_vertices, + len(vertices))) + + return smooth_mat From 3851e0ab46e1cdd2322933ba0f2f3126f682a98c Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 5 Sep 2019 11:54:59 +0200 Subject: [PATCH 11/88] Fix time_idx and text_label --- mne/viz/_brain/_brain.py | 87 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 5 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 587fe26714e..dce6656b14b 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -222,6 +222,7 @@ def __init__(self, subject_id, hemi, surf, title=None, self._title = title self._subject_id = subject_id self._views = views + self._n_times = None # for now only one color bar can be added # since it is the same for all figures self._colorbar_added = False @@ -402,10 +403,46 @@ def add_data(self, array, fmin=None, fmid=None, fmax=None, hemi = self._check_hemi(hemi) array = np.asarray(array) - if initial_time is None: + # Create time array and add label if > 1D + if array.ndim <= 1: time_idx = 0 else: - time_idx = np.argmin(abs(time - initial_time)) + # check time array + if time is None: + time = np.arange(array.shape[-1]) + else: + time = np.asarray(time) + if time.shape != (array.shape[-1],): + raise ValueError('time has shape %s, but need shape %s ' + '(array.shape[-1])' % + (time.shape, (array.shape[-1],))) + + if self._n_times is None: + self._n_times = len(time) + self._times = time + elif len(time) != self._n_times: + raise ValueError("New n_times is different from previous " + "n_times") + elif not np.array_equal(time, self._times): + raise ValueError("Not all time values are consistent with " + "previously set times.") + + # initial time + if initial_time is None: + time_idx = 0 + else: + time_idx = self.index_for_time(initial_time) + + # time label + if isinstance(time_label, str): + time_label_fmt = time_label + + def time_label(x): + return time_label_fmt % x + self._data["time_label"] = time_label + self._data["time"] = time + self._data["time_idx"] = 0 + y_txt = 0.05 + 0.05 * bool(colorbar) if time is not None and len(array.shape) == 2: # we have scalar_data with time dimension @@ -473,12 +510,52 @@ def add_data(self, array, fmin=None, fmid=None, fmax=None, color=act_color, colormap=colormap, scalars=act_data) - if array.ndim >= 2: - renderer.text2d(x=0.95, y=0.05, text='time={}'.format(time[0]), - width=time_label_size) + if array.ndim >= 2 and time_label is not None: + renderer.text2d(x=0.95, y=y_txt, width=time_label_size, + text=time_label(time[time_idx])) renderer.scalarbar(source=mesh, n_labels=8) self._overlays[hemi + '_' + v] = mesh + def index_for_time(self, time, rounding='closest'): + """Find the data time index closest to a specific time point. + + Parameters + ---------- + time : scalar + Time. + rounding : 'closest' | 'up' | 'down' + How to round if the exact time point is not an index. + + Returns + ------- + index : int + Data time index closest to time. + """ + if self._n_times is None: + raise RuntimeError("Brain has no time axis") + times = self._times + + # Check that time is in range + tmin = np.min(times) + tmax = np.max(times) + max_diff = (tmax - tmin) / (len(times) - 1) / 2 + if time < tmin - max_diff or time > tmax + max_diff: + err = ("time = %s lies outside of the time axis " + "[%s, %s]" % (time, tmin, tmax)) + raise ValueError(err) + + if rounding == 'closest': + idx = np.argmin(np.abs(times - time)) + elif rounding == 'up': + idx = np.nonzero(times >= time)[0][0] + elif rounding == 'down': + idx = np.nonzero(times <= time)[0][-1] + else: + err = "Invalid rounding parameter: %s" % repr(rounding) + raise ValueError(err) + + return idx + def show(self): u"""Display widget.""" try: From 0b749b92c62e52d44624a49c4008dbf18e71d3a4 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 5 Sep 2019 12:13:56 +0200 Subject: [PATCH 12/88] Fix some doc --- mne/viz/_brain/_brain.py | 3 +-- mne/viz/_brain/utils.py | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index dce6656b14b..5dddd2a190b 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -621,8 +621,7 @@ def _check_hemi(self, hemi): return hemi def _get_geo_params(self, cortex, alpha=1.0): - """Return keyword arguments and other parameters for surface - rendering. + """Return kwargs and other parameters for surface rendering. Parameters ---------- diff --git a/mne/viz/_brain/utils.py b/mne/viz/_brain/utils.py index 1ce5f05b0fa..6fb629702b9 100644 --- a/mne/viz/_brain/utils.py +++ b/mne/viz/_brain/utils.py @@ -4,7 +4,7 @@ def mesh_edges(faces): - """Returns sparse matrix with edges as an adjacency matrix + """Return sparse matrix with edges as an adjacency matrix. Parameters ---------- @@ -31,7 +31,9 @@ def mesh_edges(faces): def smoothing_matrix(vertices, adj_mat, smoothing_steps=20, verbose=None): - """Create a smoothing matrix which can be used to interpolate data defined + """Create a smoothing matrix. + + This smoothing matrix can be used to interpolate data defined for a subset of vertices onto mesh with an adjancency matrix given by adj_mat. From 2ffb5067b39c3b97fe4cca89a7f65edef4bcf17b Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 5 Sep 2019 13:32:43 +0200 Subject: [PATCH 13/88] Postpone use of geo_ variables --- mne/viz/_brain/_brain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 5dddd2a190b..8415a1d1ef0 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -249,7 +249,8 @@ def __init__(self, subject_id, hemi, surf, title=None, else: raise ValueError('"size" parameter must be int or tuple.') - geo_kwargs, geo_reverse, geo_curv = self._get_geo_params(cortex, alpha) + # XXX: use the geo_ variables + # geo_kwargs, geo_reverse, geo_curv = self._get_geo_params(cortex, alpha) for h in self._hemis: # Initialize a Surface object as the geometry geo = Surface(subject_id, h, surf, subjects_dir, offset, From beb6c30d1b7d5afa634b686305e90e9552253a77 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 5 Sep 2019 13:35:12 +0200 Subject: [PATCH 14/88] Fix some doc --- mne/viz/_brain/_brain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 8415a1d1ef0..ba1e4133073 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -250,7 +250,8 @@ def __init__(self, subject_id, hemi, surf, title=None, raise ValueError('"size" parameter must be int or tuple.') # XXX: use the geo_ variables - # geo_kwargs, geo_reverse, geo_curv = self._get_geo_params(cortex, alpha) + # geo_kwargs, geo_reverse, geo_curv = \ + # self._get_geo_params(cortex, alpha) for h in self._hemis: # Initialize a Surface object as the geometry geo = Surface(subject_id, h, surf, subjects_dir, offset, From 9b4f23067167d466a3e248c2d25e1534719b9961 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 5 Sep 2019 18:28:14 +0200 Subject: [PATCH 15/88] Rework colorbar colorbar --- mne/viz/_brain/_brain.py | 23 +++++++++++++++++++---- mne/viz/_brain/colormap.py | 3 ++- mne/viz/backends/_pyvista.py | 12 +++++------- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index ba1e4133073..6513c1550a7 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -499,6 +499,19 @@ def time_label(x): self._data['k'] = k self._data['b'] = b + scalars = array[:, time_idx] + scalars = smooth_mat.dot(scalars) + + # rework of colormap + from matplotlib.colors import ListedColormap + table = self.table * 255 + bgcolor = (127, 127, 127, 0) # XXX: get bgcolor + alphas = table[:, -1][:, np.newaxis] / 255. + use_table = table.copy() + use_table[:, -1] = 255. + vals = (use_table * alphas) + bgcolor * (1 - alphas) + table = ListedColormap(vals / 255.) + for ri, v in enumerate(self._views): if self._hemi != 'split': ci = 0 @@ -510,7 +523,9 @@ def time_label(x): z=self.geo[hemi].coords[:, 2], triangles=self.geo[hemi].faces, color=act_color, - colormap=colormap, + colormap=table, + vmin=dt_min, + vmax=dt_max, scalars=act_data) if array.ndim >= 2 and time_label is not None: renderer.text2d(x=0.95, y=y_txt, width=time_label_size, @@ -585,9 +600,9 @@ def update_lut(self, fmin=None, fmid=None, fmax=None): fmid = self._data['fmid'] if fmid is None else fmid fmax = self._data['fmax'] if fmax is None else fmax - self._data['lut'] = _calculate_lut(colormap, alpha=alpha, - fmin=fmin, fmid=fmid, - fmax=fmax, center=center) + self._data['lut'], self.table = _calculate_lut(colormap, alpha=alpha, + fmin=fmin, fmid=fmid, + fmax=fmax, center=center) return self._data['lut'] diff --git a/mne/viz/_brain/colormap.py b/mne/viz/_brain/colormap.py index 268065c9786..f03f3c20675 100644 --- a/mne/viz/_brain/colormap.py +++ b/mne/viz/_brain/colormap.py @@ -103,6 +103,7 @@ def _calculate_lut(lim_cmap, alpha, fmin, fmid, fmax, center=None): alphas *= alpha np.clip(alphas, 0, 1) cmap[:, -1] = alphas + table = cmap cmap = ListedColormap(cmap) - return cmap + return cmap, table diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index da31a7aeadc..c776287e4e1 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -123,7 +123,8 @@ def set_interactive(self): self.plotter.enable_terrain_style() def mesh(self, x, y, z, triangles, color, opacity=1.0, shading=False, - backface_culling=False, scalars=None, colormap=None, **kwargs): + backface_culling=False, scalars=None, colormap=None, + vmin=None, vmax=None, **kwargs): with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) from pyvista import PolyData @@ -132,9 +133,8 @@ def mesh(self, x, y, z, triangles, color, opacity=1.0, shading=False, n_vertices = len(vertices) triangles = np.c_[np.full(len(triangles), 3), triangles] pd = PolyData(vertices, triangles) - if scalars is not None: - pd.point_arrays['scalars'] = scalars - if len(color) == n_vertices: + rgba = False + if color is not None and len(color) == n_vertices: if color.shape[1] == 3: scalars = np.c_[color, np.ones(n_vertices)] else: @@ -146,13 +146,11 @@ def mesh(self, x, y, z, triangles, color, opacity=1.0, shading=False, # https://github.com/pyvista/pyvista-support/issues/15 smooth_shading = False rgba = True - else: - scalars = None - rgba = False self.plotter.add_mesh(mesh=pd, color=color, scalars=scalars, rgba=rgba, opacity=opacity, cmap=colormap, backface_culling=backface_culling, + rng=[vmin, vmax], show_scalar_bar=False, smooth_shading=smooth_shading) def contour(self, surface, scalars, contours, line_width=1.0, opacity=1.0, From ccfeaa3ed76cbfc572b54b936c81ad0b8879ea01 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 6 Sep 2019 15:35:43 +0200 Subject: [PATCH 16/88] Try new api for scalarbar --- mne/viz/_brain/_brain.py | 36 +++++++----------------------------- mne/viz/backends/_pyvista.py | 7 ++++--- 2 files changed, 11 insertions(+), 32 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 6513c1550a7..508929b802b 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -486,31 +486,8 @@ def time_label(x): act_data = smooth_mat.dot(act_data) self._data[hemi + '_smooth_mat'] = smooth_mat - # data mapping into [0, 1] interval dt_max = fmax dt_min = fmin if center is None else -1 * fmax - k = 1 / (dt_max - dt_min) - b = 1 - k * dt_max - act_data = k * act_data + b - act_data = np.clip(act_data, 0, 1) - - act_color = lut(act_data) - - self._data['k'] = k - self._data['b'] = b - - scalars = array[:, time_idx] - scalars = smooth_mat.dot(scalars) - - # rework of colormap - from matplotlib.colors import ListedColormap - table = self.table * 255 - bgcolor = (127, 127, 127, 0) # XXX: get bgcolor - alphas = table[:, -1][:, np.newaxis] / 255. - use_table = table.copy() - use_table[:, -1] = 255. - vals = (use_table * alphas) + bgcolor * (1 - alphas) - table = ListedColormap(vals / 255.) for ri, v in enumerate(self._views): if self._hemi != 'split': @@ -522,15 +499,16 @@ def time_label(x): y=self.geo[hemi].coords[:, 1], z=self.geo[hemi].coords[:, 2], triangles=self.geo[hemi].faces, - color=act_color, - colormap=table, + color=None, + colormap=lut, vmin=dt_min, vmax=dt_max, scalars=act_data) if array.ndim >= 2 and time_label is not None: renderer.text2d(x=0.95, y=y_txt, width=time_label_size, text=time_label(time[time_idx])) - renderer.scalarbar(source=mesh, n_labels=8) + renderer.scalarbar(source=mesh, n_labels=8, + bgcolor=(0.5, 0.5, 0.5)) self._overlays[hemi + '_' + v] = mesh def index_for_time(self, time, rounding='closest'): @@ -600,9 +578,9 @@ def update_lut(self, fmin=None, fmid=None, fmax=None): fmid = self._data['fmid'] if fmid is None else fmid fmax = self._data['fmax'] if fmax is None else fmax - self._data['lut'], self.table = _calculate_lut(colormap, alpha=alpha, - fmin=fmin, fmid=fmid, - fmax=fmax, center=center) + self._data['lut'], self._ctable = \ + _calculate_lut(colormap, alpha=alpha, fmin=fmin, fmid=fmid, + fmax=fmax, center=center) return self._data['lut'] diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index c776287e4e1..1c15e511d07 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -337,12 +337,13 @@ def text3d(self, x, y, z, text, scale, color=(1.0, 1.0, 1.0)): name=text, shape_opacity=0) - def scalarbar(self, source, title=None, n_labels=4): + def scalarbar(self, source, title=None, n_labels=4, bgcolor=None): with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) self.plotter.add_scalar_bar(title=title, n_labels=n_labels, - use_opacity=False, - position_x=0.15, width=0.7) + use_opacity=False, n_colors=256, + position_x=0.15, width=0.7, + background_color=bgcolor) def show(self): self.figure.display = self.plotter.show() From e2b3e3e8fe0ec64585e3e1e24d7dbc367b2303c2 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 6 Sep 2019 17:04:05 +0200 Subject: [PATCH 17/88] Update mayavi backend --- mne/viz/_brain/_brain.py | 18 ++++++++---------- mne/viz/_brain/colormap.py | 5 +---- mne/viz/backends/_pysurfer_mayavi.py | 15 +++++++++++---- mne/viz/backends/_pyvista.py | 3 +++ 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 508929b802b..9ec1a66f881 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -286,12 +286,12 @@ def __init__(self, subject_id, hemi, surf, title=None, self._hemi_meshes[h + '_' + v] = mesh def add_data(self, array, fmin=None, fmid=None, fmax=None, - thresh=None, center=None, transparent=None, colormap="auto", + thresh=None, center=None, transparent=False, colormap="auto", alpha=1, vertices=None, smoothing_steps=None, time=None, time_label="time index=%d", colorbar=True, hemi=None, remove_existing=None, time_label_size=None, initial_time=None, scale_factor=None, vector_alpha=None, - verbose=None): + verbose=False): u"""Display data from a numpy array on the surface. This provides a similar interface to @@ -373,9 +373,8 @@ def add_data(self, array, fmin=None, fmid=None, fmax=None, Not supported yet. alpha level to control opacity of the arrows. Only used for vector-valued data. If None (default), ``alpha`` is used. - verbose : bool, str, int, or None - Not supported yet. - If not None, override default verbose level (see surfer.verbose). + verbose : bool + Set the default verbose level. Notes ----- @@ -471,8 +470,6 @@ def time_label(x): self._data['fmid'] = fmid self._data['fmax'] = fmax - lut = self.update_lut() - # Create smoothing matrix if necessary if len(act_data) < self.geo[hemi].x.shape[0]: if vertices is None: @@ -488,6 +485,7 @@ def time_label(x): dt_max = fmax dt_min = fmin if center is None else -1 * fmax + ctable = self.update_lut() for ri, v in enumerate(self._views): if self._hemi != 'split': @@ -500,7 +498,7 @@ def time_label(x): z=self.geo[hemi].coords[:, 2], triangles=self.geo[hemi].faces, color=None, - colormap=lut, + colormap=ctable, vmin=dt_min, vmax=dt_max, scalars=act_data) @@ -578,11 +576,11 @@ def update_lut(self, fmin=None, fmid=None, fmax=None): fmid = self._data['fmid'] if fmid is None else fmid fmax = self._data['fmax'] if fmax is None else fmax - self._data['lut'], self._ctable = \ + self._data['ctable'] = \ _calculate_lut(colormap, alpha=alpha, fmin=fmin, fmid=fmid, fmax=fmax, center=center) - return self._data['lut'] + return self._data['ctable'] @property def overlays(self): diff --git a/mne/viz/_brain/colormap.py b/mne/viz/_brain/colormap.py index f03f3c20675..d9636df7731 100644 --- a/mne/viz/_brain/colormap.py +++ b/mne/viz/_brain/colormap.py @@ -52,7 +52,6 @@ def _calculate_lut(lim_cmap, alpha, fmin, fmid, fmax, center=None): Color map with transparency channel. """ from matplotlib import cm - from matplotlib.colors import ListedColormap if center is None: # 'hot' or another linear color map @@ -103,7 +102,5 @@ def _calculate_lut(lim_cmap, alpha, fmin, fmid, fmax, center=None): alphas *= alpha np.clip(alphas, 0, 1) cmap[:, -1] = alphas - table = cmap - cmap = ListedColormap(cmap) - return cmap, table + return cmap diff --git a/mne/viz/backends/_pysurfer_mayavi.py b/mne/viz/backends/_pysurfer_mayavi.py index e84843507e4..254131abdb1 100644 --- a/mne/viz/backends/_pysurfer_mayavi.py +++ b/mne/viz/backends/_pysurfer_mayavi.py @@ -77,8 +77,10 @@ def set_interactive(self): tvtk.InteractorStyleTerrain() def mesh(self, x, y, z, triangles, color, opacity=1.0, shading=False, - backface_culling=False, **kwargs): - if isinstance(color, np.ndarray) and color.ndim > 1: + backface_culling=False, scalars=None, colormap=None, + vmin=None, vmax=None, **kwargs): + if color is not None and isinstance(color, np.ndarray) \ + and color.ndim > 1: if color.shape[1] == 3: vertex_color = np.c_[color, np.ones(len(color))] * 255.0 else: @@ -87,7 +89,6 @@ def mesh(self, x, y, z, triangles, color, opacity=1.0, shading=False, scalars = np.arange(len(color)) color = None else: - scalars = None vertex_color = None with warnings.catch_warnings(record=True): # traits surface = self.mlab.triangular_mesh(x, y, z, triangles, @@ -95,7 +96,13 @@ def mesh(self, x, y, z, triangles, color, opacity=1.0, shading=False, scalars=scalars, opacity=opacity, figure=self.fig, + vmin=vmin, + vmax=vmax, **kwargs) + lut = colormap + if lut is not None: + l_m = surface.module_manager.scalar_lut_manager + l_m.load_lut_from_list(lut) if vertex_color is not None: surface.module_manager.scalar_lut_manager.lut.table = \ vertex_color @@ -203,7 +210,7 @@ def text3d(self, x, y, z, text, scale, color=(1.0, 1.0, 1.0)): self.mlab.text3d(x, y, z, text, scale=scale, color=color, figure=self.fig) - def scalarbar(self, source, title=None, n_labels=4): + def scalarbar(self, source, title=None, n_labels=4, bgcolor=None): with warnings.catch_warnings(record=True): # traits self.mlab.scalarbar(source, title=title, nb_labels=n_labels) diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 1c15e511d07..80e63152717 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -146,6 +146,9 @@ def mesh(self, x, y, z, triangles, color, opacity=1.0, shading=False, # https://github.com/pyvista/pyvista-support/issues/15 smooth_shading = False rgba = True + if colormap is not None and isinstance(colormap, np.ndarray): + from matplotlib.colors import ListedColormap + colormap = ListedColormap(colormap) self.plotter.add_mesh(mesh=pd, color=color, scalars=scalars, rgba=rgba, opacity=opacity, cmap=colormap, From c78dfaf1ac3adc50b372163c1de9fe753c32e9a9 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 6 Sep 2019 17:05:47 +0200 Subject: [PATCH 18/88] Upgrade pyvista to master --- environment.yml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index 906c93f2c9f..27d62344d10 100644 --- a/environment.yml +++ b/environment.yml @@ -34,7 +34,7 @@ dependencies: - pip: - mne - vtk - - pyvista>=0.21.3 + - https://github.com/pyvista/pyvista/zipball/master - mayavi - PySurfer[save_movie] - dipy --only-binary dipy diff --git a/requirements.txt b/requirements.txt index 97230d8b995..42f3b7f1cf2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,5 +29,5 @@ neo xlrd pydocstyle flake8 -pyvista>=0.21.3 +https://github.com/pyvista/pyvista/zipball/master panel From 2835dd67b13c47a934550ce3a692326cc123a59b Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 6 Sep 2019 17:53:15 +0200 Subject: [PATCH 19/88] Add basic support for figure --- mne/viz/_brain/_brain.py | 14 +++++++------- mne/viz/_brain/tests/test_brain.py | 4 ++-- mne/viz/backends/_pysurfer_mayavi.py | 6 ++++++ mne/viz/backends/_pyvista.py | 5 +++++ mne/viz/backends/renderer.py | 2 +- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 9ec1a66f881..419a9a9301f 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -55,7 +55,6 @@ class _Brain(object): None (default) will use black or white depending on the value of ``background``. figure : list of Figure | None | int - Not supported yet. If None (default), a new window will be created with the appropriate views. For single view plots, the figure can be specified as int to retrieve the corresponding Mayavi window. @@ -200,15 +199,15 @@ def __init__(self, subject_id, hemi, surf, title=None, if hemi == 'split': raise ValueError('Option hemi="split" is not supported yet.') - if figure is not None: - raise ValueError('figure parameter is not supported yet.') - if interaction is not None: raise ValueError('"interaction" parameter is not supported.') - from ..backends.renderer import _Renderer + from ..backends.renderer import _Renderer, _check_figure from matplotlib.colors import colorConverter + if figure is not None: + _check_figure(figure) + if isinstance(background, str): background = colorConverter.to_rgb(background) if isinstance(foreground, str): @@ -262,7 +261,7 @@ def __init__(self, subject_id, hemi, surf, title=None, self.geo[h] = geo for ri, v in enumerate(views): - renderer = _Renderer(size=fig_size, bgcolor=background) + renderer = _Renderer(size=fig_size, bgcolor=background, fig=figure) self._renderers[ri].append(renderer) renderer.set_camera(azimuth=views_dict[v].azim, elevation=views_dict[v].elev, @@ -271,7 +270,8 @@ def __init__(self, subject_id, hemi, surf, title=None, for ci, h in enumerate(self._hemis): if ci == 1 and hemi == 'split': # create a separate figure for right hemisphere - renderer = _Renderer(size=fig_size, bgcolor=background) + renderer = _Renderer(size=fig_size, bgcolor=background, + fig=figure) self._renderers[ri].append(renderer) renderer.set_camera(azimuth=views_dict[v].azim, elevation=views_dict[v].elev, diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 23bc3d4c8f7..14c7a3a9de6 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -33,8 +33,8 @@ def test_brain_init(renderer): with pytest.raises(ValueError, match='hemi'): _Brain(subject_id=subject_id, hemi="split", surf=surf) - with pytest.raises(ValueError, match='figure'): - _Brain(subject_id=subject_id, hemi=hemi, surf=surf, figure=0) + with pytest.raises(TypeError, match='figure'): + _Brain(subject_id=subject_id, hemi=hemi, surf=surf, figure='foo') with pytest.raises(ValueError, match='interaction'): _Brain(subject_id=subject_id, hemi=hemi, surf=surf, interaction=0) with pytest.raises(KeyError): diff --git a/mne/viz/backends/_pysurfer_mayavi.py b/mne/viz/backends/_pysurfer_mayavi.py index 254131abdb1..1e54508f431 100644 --- a/mne/viz/backends/_pysurfer_mayavi.py +++ b/mne/viz/backends/_pysurfer_mayavi.py @@ -368,3 +368,9 @@ def _set_3d_title(figure, title, size=40): text.property.vertical_justification = 'top' text.property.font_size = size mlab.draw(figure) + + +def _check_figure(figure): + from mayavi.core.scene import Scene + if not all(isinstance(f, Scene) for f in figure): + raise TypeError('figure must be a mayavi scene or list of scenes') diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 80e63152717..a2258fb0c56 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -493,3 +493,8 @@ def _set_3d_title(figure, title, size=40): with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) figure.plotter.add_text(title, font_size=32, color=(1.0, 1.0, 1.0)) + + +def _check_figure(figure): + if not isinstance(figure, _Figure): + raise TypeError('figure must be an instance of _Figure') diff --git a/mne/viz/backends/renderer.py b/mne/viz/backends/renderer.py index 5adec91ca5e..d7bc3719983 100644 --- a/mne/viz/backends/renderer.py +++ b/mne/viz/backends/renderer.py @@ -24,7 +24,7 @@ logger.info('Using %s 3d backend.\n' % MNE_3D_BACKEND) -_fromlist = ('_Renderer', '_Projection', '_close_all') +_fromlist = ('_Renderer', '_Projection', '_close_all', '_check_figure') _name_map = dict(mayavi='_pysurfer_mayavi', pyvista='_pyvista') if MNE_3D_BACKEND in VALID_3D_BACKENDS: # This is (hopefully) the equivalent to: From c2350d2be34cb60408356d0dd9188b3686a38ded Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 10 Sep 2019 16:16:48 +0200 Subject: [PATCH 20/88] Fix mayavi backend colorbar --- mne/viz/backends/_pysurfer_mayavi.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/mne/viz/backends/_pysurfer_mayavi.py b/mne/viz/backends/_pysurfer_mayavi.py index 1e54508f431..dd1d86f1a1f 100644 --- a/mne/viz/backends/_pysurfer_mayavi.py +++ b/mne/viz/backends/_pysurfer_mayavi.py @@ -213,6 +213,21 @@ def text3d(self, x, y, z, text, scale, color=(1.0, 1.0, 1.0)): def scalarbar(self, source, title=None, n_labels=4, bgcolor=None): with warnings.catch_warnings(record=True): # traits self.mlab.scalarbar(source, title=title, nb_labels=n_labels) + if bgcolor is not None: + from tvtk.api import tvtk + bgcolor = np.asarray(bgcolor) + bgcolor = np.append(bgcolor, 1.0) * 255. + cmap = source.module_manager.scalar_lut_manager + lut = cmap.lut + ctable = lut.table.to_array() + cbar_lut = tvtk.LookupTable() + cbar_lut.deep_copy(lut) + alphas = ctable[:, -1][:, np.newaxis] / 255. + use_lut = ctable.copy() + use_lut[:, -1] = 255. + vals = (use_lut * alphas) + bgcolor * (1 - alphas) + cbar_lut.table.from_array(vals) + cmap.scalar_bar.lookup_table = cbar_lut def show(self): if self.fig is not None: From 38e7333943d0d34c2348246c972dd4043ad36c29 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 10 Sep 2019 17:32:16 +0200 Subject: [PATCH 21/88] Fix text size and position --- mne/viz/_brain/_brain.py | 2 +- mne/viz/backends/_pysurfer_mayavi.py | 9 ++++++--- mne/viz/backends/_pyvista.py | 11 +++++++---- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 419a9a9301f..de7d8de3373 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -443,7 +443,7 @@ def time_label(x): self._data["time_label"] = time_label self._data["time"] = time self._data["time_idx"] = 0 - y_txt = 0.05 + 0.05 * bool(colorbar) + y_txt = 0.05 + 0.1 * bool(colorbar) if time is not None and len(array.shape) == 2: # we have scalar_data with time dimension diff --git a/mne/viz/backends/_pysurfer_mayavi.py b/mne/viz/backends/_pysurfer_mayavi.py index dd1d86f1a1f..0785cc7ee35 100644 --- a/mne/viz/backends/_pysurfer_mayavi.py +++ b/mne/viz/backends/_pysurfer_mayavi.py @@ -200,10 +200,13 @@ def quiver3d(self, x, y, z, u, v, w, color, scale, mode, resolution=8, glyph_resolution quiv.actor.property.backface_culling = backface_culling - def text2d(self, x, y, text, width, color=(1.0, 1.0, 1.0)): + def text2d(self, x, y, text, width=14, color=(1.0, 1.0, 1.0)): with warnings.catch_warnings(record=True): # traits - self.mlab.text(x, y, text, width=width, color=color, - figure=self.fig) + text = self.mlab.text(x, y, text, color=color, figure=self.fig) + width = 14 if width is None else width + text.property.font_size = width + text.actor.text_scale_mode = 'viewport' + text.property.justification = 'right' def text3d(self, x, y, z, text, scale, color=(1.0, 1.0, 1.0)): with warnings.catch_warnings(record=True): # traits diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index a2258fb0c56..ae872eef574 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -322,13 +322,16 @@ def quiver3d(self, x, y, z, u, v, w, color, scale, mode, resolution=8, smooth_shading=self.figure. smooth_shading) - def text2d(self, x, y, text, width, color=(1.0, 1.0, 1.0)): + def text2d(self, x, y, text, width=14, color=(1.0, 1.0, 1.0)): width = int(width * 100) if isinstance(width, float) else width + position = (x*self.figure.store['window_size'][0], + y*self.figure.store['window_size'][1]) with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) - self.plotter.add_text(text, position=(x, y), - font_size=width, - color=color) + actor = self.plotter.add_text(text, position=position, + font_size=width, + color=color) + actor.GetTextProperty().SetJustificationToRight() def text3d(self, x, y, z, text, scale, color=(1.0, 1.0, 1.0)): with warnings.catch_warnings(): From bf8c7bb4b234adb935c7bb679527b80499e7419d Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 11 Sep 2019 11:47:12 +0200 Subject: [PATCH 22/88] Update size parameter in text2d() --- mne/viz/_3d.py | 2 +- mne/viz/backends/_pysurfer_mayavi.py | 6 +++--- mne/viz/backends/_pyvista.py | 10 +++++----- mne/viz/backends/base_renderer.py | 6 +++--- mne/viz/backends/tests/test_renderer.py | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index 6beba744a9d..a79fb242a80 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -406,7 +406,7 @@ def plot_evoked_field(evoked, surf_maps, time=None, time_label='t = %0.0f ms', if '%' in time_label: time_label %= (1e3 * evoked.times[time_idx]) - renderer.text2d(x=0.01, y=0.01, text=time_label, width=0.4) + renderer.text2d(x=0.01, y=0.01, text=time_label) renderer.set_camera(azimuth=10, elevation=60) renderer.show() return renderer.scene() diff --git a/mne/viz/backends/_pysurfer_mayavi.py b/mne/viz/backends/_pysurfer_mayavi.py index 0785cc7ee35..adaaf0900f1 100644 --- a/mne/viz/backends/_pysurfer_mayavi.py +++ b/mne/viz/backends/_pysurfer_mayavi.py @@ -200,11 +200,11 @@ def quiver3d(self, x, y, z, u, v, w, color, scale, mode, resolution=8, glyph_resolution quiv.actor.property.backface_culling = backface_culling - def text2d(self, x, y, text, width=14, color=(1.0, 1.0, 1.0)): + def text2d(self, x, y, text, size=14, color=(1.0, 1.0, 1.0)): + size = 14 if size is None else size with warnings.catch_warnings(record=True): # traits text = self.mlab.text(x, y, text, color=color, figure=self.fig) - width = 14 if width is None else width - text.property.font_size = width + text.property.font_size = size text.actor.text_scale_mode = 'viewport' text.property.justification = 'right' diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index ae872eef574..0680b398108 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -322,14 +322,14 @@ def quiver3d(self, x, y, z, u, v, w, color, scale, mode, resolution=8, smooth_shading=self.figure. smooth_shading) - def text2d(self, x, y, text, width=14, color=(1.0, 1.0, 1.0)): - width = int(width * 100) if isinstance(width, float) else width - position = (x*self.figure.store['window_size'][0], - y*self.figure.store['window_size'][1]) + def text2d(self, x, y, text, size=14, color=(1.0, 1.0, 1.0)): + size = 14 if size is None else size + position = (x * self.figure.store['window_size'][0], + y * self.figure.store['window_size'][1]) with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) actor = self.plotter.add_text(text, position=position, - font_size=width, + font_size=size, color=color) actor.GetTextProperty().SetJustificationToRight() diff --git a/mne/viz/backends/base_renderer.py b/mne/viz/backends/base_renderer.py index 733aee4fb37..7d2cd627ccf 100644 --- a/mne/viz/backends/base_renderer.py +++ b/mne/viz/backends/base_renderer.py @@ -227,7 +227,7 @@ def quiver3d(self, x, y, z, u, v, w, color, scale, mode, resolution=8, pass @abstractclassmethod - def text2d(self, x, y, text, width, color=(1.0, 1.0, 1.0)): + def text2d(self, x, y, text, size=14, color=(1.0, 1.0, 1.0)): """Add 2d text in the scene. Parameters @@ -238,8 +238,8 @@ def text2d(self, x, y, text, width, color=(1.0, 1.0, 1.0)): The Y component to use as position of the text. text: str The content of the text. - width: float - The width of the text. + size: int + The size of the font. color: tuple The color of the text. """ diff --git a/mne/viz/backends/tests/test_renderer.py b/mne/viz/backends/tests/test_renderer.py index d617a6c9830..9a99aee8813 100644 --- a/mne/viz/backends/tests/test_renderer.py +++ b/mne/viz/backends/tests/test_renderer.py @@ -86,7 +86,7 @@ def test_3d_backend(renderer): txt_x = 0.0 txt_y = 0.0 txt_text = "renderer" - txt_width = 1.0 + txt_size = 14 cam_distance = 5 * tet_size @@ -131,7 +131,7 @@ def test_3d_backend(renderer): rend.scalarbar(source=tube, title="Scalar Bar") # use text - rend.text2d(x=txt_x, y=txt_y, text=txt_text, width=txt_width) + rend.text2d(x=txt_x, y=txt_y, text=txt_text, size=txt_size) rend.text3d(x=0, y=0, z=0, text=txt_text, scale=1.0) rend.set_camera(azimuth=180.0, elevation=90.0, distance=cam_distance, From a441b6fed7f6ec30e6e25656fa66af5da917b0cd Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 11 Sep 2019 12:02:00 +0200 Subject: [PATCH 23/88] Update text2d() size in _Brain --- mne/viz/_brain/_brain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index de7d8de3373..d47a3990665 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -503,7 +503,7 @@ def time_label(x): vmax=dt_max, scalars=act_data) if array.ndim >= 2 and time_label is not None: - renderer.text2d(x=0.95, y=y_txt, width=time_label_size, + renderer.text2d(x=0.95, y=y_txt, size=time_label_size, text=time_label(time[time_idx])) renderer.scalarbar(source=mesh, n_labels=8, bgcolor=(0.5, 0.5, 0.5)) From 430ae9465dfeec09c9db41e12e9c5eb2c42914d8 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 11 Sep 2019 17:17:59 +0200 Subject: [PATCH 24/88] Improve figure management --- mne/viz/_brain/_brain.py | 37 +++++++++++++++++++--------- mne/viz/backends/_pysurfer_mayavi.py | 2 ++ mne/viz/backends/_pyvista.py | 12 ++++++++- mne/viz/backends/renderer.py | 6 +++-- 4 files changed, 43 insertions(+), 14 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index d47a3990665..2eaac6befe9 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -202,18 +202,40 @@ def __init__(self, subject_id, hemi, surf, title=None, if interaction is not None: raise ValueError('"interaction" parameter is not supported.') - from ..backends.renderer import _Renderer, _check_figure + from ..backends.renderer import _Renderer, create_3d_figure from matplotlib.colors import colorConverter - if figure is not None: - _check_figure(figure) - if isinstance(background, str): background = colorConverter.to_rgb(background) if isinstance(foreground, str): foreground = colorConverter.to_rgb(foreground) if isinstance(views, str): views = [views] + n_row = len(views) + col_dict = dict(lh=1, rh=1, both=1, split=2) + n_col = col_dict[hemi] + + if isinstance(size, int): + fig_size = (size, size) + elif isinstance(size, tuple): + fig_size = size + else: + raise ValueError('"size" parameter must be int or tuple.') + + if figure is None: + figures = [[create_3d_figure(size=fig_size, + bgcolor=background)]] + else: + if isinstance(figure, int): + figures = [[create_3d_figure(size=fig_size, + bgcolor=background, + handle=figure)]] + elif not isinstance(figure, list): + raise TypeError('Expected type for `figure` is scene, ' + 'list, int or None: ' + '{} was given'.format(type(figure))) + figures = [figure[slice(ri * n_col, (ri + 1) * n_col)] + for ri in range(n_row)] self._foreground = foreground self._hemi = hemi @@ -241,13 +263,6 @@ def __init__(self, subject_id, hemi, surf, title=None, raise KeyError('hemi has to be either "lh", "rh", "split", ' 'or "both"') - if isinstance(size, int): - fig_size = (size, size) - elif isinstance(size, tuple): - fig_size = size - else: - raise ValueError('"size" parameter must be int or tuple.') - # XXX: use the geo_ variables # geo_kwargs, geo_reverse, geo_curv = \ # self._get_geo_params(cortex, alpha) diff --git a/mne/viz/backends/_pysurfer_mayavi.py b/mne/viz/backends/_pysurfer_mayavi.py index adaaf0900f1..aaa294f815f 100644 --- a/mne/viz/backends/_pysurfer_mayavi.py +++ b/mne/viz/backends/_pysurfer_mayavi.py @@ -62,6 +62,8 @@ def __init__(self, fig=None, size=(600, 600), bgcolor=(0., 0., 0.), self.mlab = _import_mlab() if fig is None: self.fig = _mlab_figure(figure=name, bgcolor=bgcolor, size=size) + elif isinstance(fig, int): + self.fig = _mlab_figure(figure=fig, bgcolor=bgcolor, size=size) else: self.fig = fig if show is False: diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 0680b398108..3c4c3c8724a 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -18,6 +18,8 @@ from ._utils import _get_colormap_from_array from ...utils import copy_base_doc_to_subclass_doc +_FIGURES = dict() + class _Figure(object): def __init__(self, plotter=None, @@ -99,7 +101,15 @@ class _Renderer(_BaseRenderer): def __init__(self, fig=None, size=(600, 600), bgcolor=(0., 0., 0.), name="PyVista Scene", show=False): from mne.viz.backends.renderer import MNE_3D_BACKEND_TEST_DATA - if fig is None: + if isinstance(fig, int): + if _FIGURES.get(fig) is None: + self.figure = _Figure(title=name, size=size, + background_color=bgcolor, + notebook=_check_notebook()) + _FIGURES[fig] = self.figure + else: + self.figure = _FIGURES.get(fig) + elif fig is None: self.figure = _Figure(title=name, size=size, background_color=bgcolor, notebook=_check_notebook()) diff --git a/mne/viz/backends/renderer.py b/mne/viz/backends/renderer.py index d7bc3719983..5b1a2018cae 100644 --- a/mne/viz/backends/renderer.py +++ b/mne/viz/backends/renderer.py @@ -181,7 +181,7 @@ def set_3d_title(figure, title, size=40): _mod._set_3d_title(figure=figure, title=title, size=size) -def create_3d_figure(size, bgcolor=(0, 0, 0)): +def create_3d_figure(size, bgcolor=(0, 0, 0), handle=None): """Return an empty figure based on the current 3d backend. Parameters @@ -190,11 +190,13 @@ def create_3d_figure(size, bgcolor=(0, 0, 0)): The dimensions of the 3d figure (width, height). bgcolor: tuple The color of the background. + handle: int | None + The figure identifier. Returns ------- figure: The requested empty scene. """ - renderer = _mod._Renderer(size=size, bgcolor=bgcolor) + renderer = _mod._Renderer(fig=handle, size=size, bgcolor=bgcolor) return renderer.scene() From 4747496b2d510e2750b06fde55e064e1ae078fde Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 11 Sep 2019 17:29:33 +0200 Subject: [PATCH 25/88] Improve Figure testing --- mne/viz/_brain/_brain.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 2eaac6befe9..4d39a17ac8a 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -227,13 +227,19 @@ def __init__(self, subject_id, hemi, surf, title=None, bgcolor=background)]] else: if isinstance(figure, int): - figures = [[create_3d_figure(size=fig_size, - bgcolor=background, - handle=figure)]] + figure = [create_3d_figure(size=fig_size, + bgcolor=background, + handle=figure)] elif not isinstance(figure, list): raise TypeError('Expected type for `figure` is scene, ' 'list, int or None: ' '{} was given'.format(type(figure))) + + if not len(figure) == n_row * n_col: + raise ValueError('For the requested view, figure must be a ' + 'list or tuple with exactly %i elements, ' + 'not %i' % (n_row * n_col, len(figure))) + figures = [figure[slice(ri * n_col, (ri + 1) * n_col)] for ri in range(n_row)] @@ -276,21 +282,13 @@ def __init__(self, subject_id, hemi, surf, title=None, self.geo[h] = geo for ri, v in enumerate(views): - renderer = _Renderer(size=fig_size, bgcolor=background, fig=figure) - self._renderers[ri].append(renderer) - renderer.set_camera(azimuth=views_dict[v].azim, - elevation=views_dict[v].elev, - distance=490.0) - for ci, h in enumerate(self._hemis): - if ci == 1 and hemi == 'split': - # create a separate figure for right hemisphere - renderer = _Renderer(size=fig_size, bgcolor=background, - fig=figure) - self._renderers[ri].append(renderer) - renderer.set_camera(azimuth=views_dict[v].azim, - elevation=views_dict[v].elev, - distance=490.0) + renderer = _Renderer(size=fig_size, bgcolor=background, + fig=figures[ri][ci]) + self._renderers[ri].append(renderer) + renderer.set_camera(azimuth=views_dict[v].azim, + elevation=views_dict[v].elev, + distance=490.0) mesh = renderer.mesh(x=self.geo[h].coords[:, 0], y=self.geo[h].coords[:, 1], From b396bf301c8f5b8f79d872cdead6eba109658ec2 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 11 Sep 2019 17:42:02 +0200 Subject: [PATCH 26/88] Fix IndexError with hi, ci --- mne/viz/_brain/_brain.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 4d39a17ac8a..3da8c51242b 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -283,6 +283,7 @@ def __init__(self, subject_id, hemi, surf, title=None, for ri, v in enumerate(views): for ci, h in enumerate(self._hemis): + ci = hi if hemi == 'split' else 0 renderer = _Renderer(size=fig_size, bgcolor=background, fig=figures[ri][ci]) self._renderers[ri].append(renderer) From 2475315330765b70bab1c8deb4e246a2618b0692 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 11 Sep 2019 17:47:00 +0200 Subject: [PATCH 27/88] Fix variable name --- mne/viz/_brain/_brain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 3da8c51242b..bdf81567074 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -283,7 +283,7 @@ def __init__(self, subject_id, hemi, surf, title=None, for ri, v in enumerate(views): for ci, h in enumerate(self._hemis): - ci = hi if hemi == 'split' else 0 + ci = ri if hemi == 'split' else 0 renderer = _Renderer(size=fig_size, bgcolor=background, fig=figures[ri][ci]) self._renderers[ri].append(renderer) From d2075293dbd3ac6cdbe7f7fb6e5d261490561155 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 12 Sep 2019 14:31:58 +0200 Subject: [PATCH 28/88] Fix callable alert --- mne/viz/_brain/_brain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 5074c2c9b3f..bf2000cfda1 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -516,7 +516,7 @@ def time_label(x): vmin=dt_min, vmax=dt_max, scalars=act_data) - if array.ndim >= 2 and time_label is not None: + if array.ndim >= 2 and callable(time_label): renderer.text2d(x=0.95, y=y_txt, size=time_label_size, text=time_label(time[time_idx])) renderer.scalarbar(source=mesh, n_labels=8, From dad68069217491b5fd89ea09c56fd300495fb5e0 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 12 Sep 2019 14:54:03 +0200 Subject: [PATCH 29/88] Add function signatures --- mne/viz/_brain/_brain.py | 58 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index bf2000cfda1..1cca9d0e6dd 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -523,6 +523,60 @@ def time_label(x): bgcolor=(0.5, 0.5, 0.5)) self._overlays[hemi + '_' + v] = mesh + def add_label(self, label, color=None, alpha=1, scalar_thresh=None, + borders=False, hemi=None, subdir=None): + """Add an ROI label to the image. + + Parameters + ---------- + label : str | instance of Label + label filepath or name. Can also be an instance of + an object with attributes "hemi", "vertices", "name", and + optionally "color" and "values" (if scalar_thresh is not None). + color : matplotlib-style color | None + anything matplotlib accepts: string, RGB, hex, etc. (default + "crimson") + alpha : float in [0, 1] + alpha level to control opacity + scalar_thresh : None or number + threshold the label ids using this value in the label + file's scalar field (i.e. label only vertices with + scalar >= thresh) + borders : bool | int + Show only label borders. If int, specify the number of steps + (away from the true border) along the cortical mesh to include + as part of the border definition. + hemi : str | None + If None, it is assumed to belong to the hemipshere being + shown. If two hemispheres are being shown, an error will + be thrown. + subdir : None | str + If a label is specified as name, subdir can be used to indicate + that the label file is in a sub-directory of the subject's + label directory rather than in the label directory itself (e.g. + for ``$SUBJECTS_DIR/$SUBJECT/label/aparc/lh.cuneus.label`` + ``brain.add_label('cuneus', subdir='aparc')``). + + Notes + ----- + To remove previously added labels, run Brain.remove_labels(). + """ + pass + + def remove_labels(self, labels=None, hemi=None): + """Remove one or more previously added labels from the image. + + Parameters + ---------- + labels : None | str | list of str + Labels to remove. Can be a string naming a single label, or None to + remove all labels. Possible names can be found in the Brain.labels + attribute. + hemi : None + Deprecated parameter, do not use. + """ + pass + def index_for_time(self, time, rounding='closest'): """Find the data time index closest to a specific time point. @@ -570,6 +624,10 @@ def show(self): except RuntimeError: logger.info("No active/running renderer available.") + def show_view(self, view=None, roll=None, distance=None): + """Orient camera to display view""" + pass + def update_lut(self, fmin=None, fmid=None, fmax=None): u"""Update color map. From a698cba663e84ce5724a43cb55f59c10ef70f9d3 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 12 Sep 2019 15:04:39 +0200 Subject: [PATCH 30/88] Update overview table --- mne/viz/_brain/_brain.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 1cca9d0e6dd..2e066dbc6c1 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -106,7 +106,7 @@ class _Brain(object): +---------------------------+--------------+-----------------------+ | add_foci | ✓ | | +---------------------------+--------------+-----------------------+ - | add_label | ✓ | | + | add_label | ✓ | - | +---------------------------+--------------+-----------------------+ | add_morphometry | ✓ | | +---------------------------+--------------+-----------------------+ @@ -146,7 +146,7 @@ class _Brain(object): +---------------------------+--------------+-----------------------+ | remove_foci | ✓ | | +---------------------------+--------------+-----------------------+ - | remove_labels | ✓ | | + | remove_labels | ✓ | - | +---------------------------+--------------+-----------------------+ | reset_view | ✓ | | +---------------------------+--------------+-----------------------+ @@ -180,7 +180,7 @@ class _Brain(object): +---------------------------+--------------+-----------------------+ | show_colorbar | ✓ | | +---------------------------+--------------+-----------------------+ - | show_view | ✓ | | + | show_view | ✓ | - | +---------------------------+--------------+-----------------------+ | texts | ✓ | | +---------------------------+--------------+-----------------------+ From 7438b646d4ee632f01a1e83376de13477e06e123 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 12 Sep 2019 15:13:42 +0200 Subject: [PATCH 31/88] TST: Produce Circle atifact for plot_xhemi example --- examples/visualization/plot_xhemi.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/visualization/plot_xhemi.py b/examples/visualization/plot_xhemi.py index 6d6ce1bec52..4f1668b733e 100644 --- a/examples/visualization/plot_xhemi.py +++ b/examples/visualization/plot_xhemi.py @@ -18,7 +18,6 @@ data_dir = mne.datasets.sample.data_path() subjects_dir = data_dir + '/subjects' stc_path = data_dir + '/MEG/sample/sample_audvis-meg-eeg' - stc = mne.read_source_estimate(stc_path, 'sample') # First, morph the data to fsaverage_sym, for which we have left_right From 84f4898a0962133181547396d42a9d95ee58b63f Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 12 Sep 2019 16:31:33 +0200 Subject: [PATCH 32/88] Fill up add_label() to start somewhere --- mne/viz/_brain/_brain.py | 78 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 2e066dbc6c1..a49356d9978 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -8,7 +8,9 @@ # License: Simplified BSD import numpy as np - +import os +import nibabel as nib +from os.path import join as pjoin from .colormap import _calculate_lut from .view import views_dict from .surface import Surface @@ -561,7 +563,77 @@ def add_label(self, label, color=None, alpha=1, scalar_thresh=None, ----- To remove previously added labels, run Brain.remove_labels(). """ - pass + from matplotlib.colors import colorConverter + if isinstance(label, str): + hemi = self._check_hemi(hemi) + if color is None: + color = "crimson" + + if os.path.isfile(label): + filepath = label + label_name = os.path.basename(filepath).split('.')[1] + else: + label_name = label + label_fname = ".".join([hemi, label_name, 'label']) + if subdir is None: + filepath = pjoin(self.subjects_dir, self.subject_id, + 'label', label_fname) + else: + filepath = pjoin(self.subjects_dir, self.subject_id, + 'label', subdir, label_fname) + if not os.path.exists(filepath): + raise ValueError('Label file %s does not exist' + % filepath) + # Load the label data and create binary overlay + if scalar_thresh is None: + ids = nib.freesurfer.read_label(filepath) + else: + ids, scalars = nib.freesurfer.read_label(filepath, + read_scalars=True) + ids = ids[scalars >= scalar_thresh] + else: + # try to extract parameters from label instance + try: + hemi = label.hemi + ids = label.vertices + if label.name is None: + label_name = 'unnamed' + else: + label_name = str(label.name) + + if color is None: + if hasattr(label, 'color') and label.color is not None: + color = label.color + else: + color = "crimson" + + if scalar_thresh is not None: + scalars = label.values + except Exception: + raise ValueError('Label was not a filename (str), and could ' + 'not be understood as a class. The class ' + 'must have attributes "hemi", "vertices", ' + '"name", and (if scalar_thresh is not None)' + '"values"') + hemi = self._check_hemi(hemi) + + if scalar_thresh is not None: + ids = ids[scalars >= scalar_thresh] + + label = np.zeros((self.geo[hemi].coords.shape[0], 4)) + label[ids] = colorConverter.to_rgba(color, alpha) + + for ri, v in enumerate(self._views): + if self._hemi != 'split': + ci = 0 + else: + ci = 0 if hemi == 'lh' else 1 + renderer = self._renderers[ri][ci] + renderer.mesh(x=self.geo[hemi].coords[:, 0], + y=self.geo[hemi].coords[:, 1], + z=self.geo[hemi].coords[:, 2], + triangles=self.geo[hemi].faces, + color=label) def remove_labels(self, labels=None, hemi=None): """Remove one or more previously added labels from the image. @@ -625,7 +697,7 @@ def show(self): logger.info("No active/running renderer available.") def show_view(self, view=None, roll=None, distance=None): - """Orient camera to display view""" + """Orient camera to display view.""" pass def update_lut(self, fmin=None, fmid=None, fmax=None): From 7a9ec7a9e3a7178669e24e90cd56abbd896b0976 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 16 Sep 2019 11:24:48 +0200 Subject: [PATCH 33/88] Fix overview table --- mne/viz/_brain/_brain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index a49356d9978..7636d7e3401 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -182,7 +182,7 @@ class _Brain(object): +---------------------------+--------------+-----------------------+ | show_colorbar | ✓ | | +---------------------------+--------------+-----------------------+ - | show_view | ✓ | - | + | show_view | ✓ | - | +---------------------------+--------------+-----------------------+ | texts | ✓ | | +---------------------------+--------------+-----------------------+ From 50476e1fa33792d96413c4494966fe64b31e317d Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 16 Sep 2019 15:52:55 +0200 Subject: [PATCH 34/88] Use binary mask for label and custom colormap --- mne/viz/_brain/_brain.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 7636d7e3401..433fdb9b62b 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -621,7 +621,9 @@ def add_label(self, label, color=None, alpha=1, scalar_thresh=None, ids = ids[scalars >= scalar_thresh] label = np.zeros((self.geo[hemi].coords.shape[0], 4)) - label[ids] = colorConverter.to_rgba(color, alpha) + label[ids] = 1 + color = colorConverter.to_rgba(color, alpha) + cmap = np.array([(0, 0, 0, 0,), color]) for ri, v in enumerate(self._views): if self._hemi != 'split': @@ -633,7 +635,9 @@ def add_label(self, label, color=None, alpha=1, scalar_thresh=None, y=self.geo[hemi].coords[:, 1], z=self.geo[hemi].coords[:, 2], triangles=self.geo[hemi].faces, - color=label) + scalars=label, + color=None, + colormap=cmap) def remove_labels(self, labels=None, hemi=None): """Remove one or more previously added labels from the image. From 782fba6d7b5b01fda2115ee07ba536f7c8a9bcb4 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 16 Sep 2019 17:05:35 +0200 Subject: [PATCH 35/88] Fix label management for mayavi --- mne/viz/_brain/_brain.py | 6 ++++-- mne/viz/backends/_pysurfer_mayavi.py | 14 ++++++++++---- mne/viz/backends/_pyvista.py | 4 +++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 433fdb9b62b..72bbc306fb8 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -620,10 +620,11 @@ def add_label(self, label, color=None, alpha=1, scalar_thresh=None, if scalar_thresh is not None: ids = ids[scalars >= scalar_thresh] - label = np.zeros((self.geo[hemi].coords.shape[0], 4)) + label = np.zeros(self.geo[hemi].coords.shape[0]) label[ids] = 1 color = colorConverter.to_rgba(color, alpha) cmap = np.array([(0, 0, 0, 0,), color]) + ctable = np.round(cmap * 255).astype(np.uint8) for ri, v in enumerate(self._views): if self._hemi != 'split': @@ -637,7 +638,8 @@ def add_label(self, label, color=None, alpha=1, scalar_thresh=None, triangles=self.geo[hemi].faces, scalars=label, color=None, - colormap=cmap) + colormap=ctable, + backface_culling=False) def remove_labels(self, labels=None, hemi=None): """Remove one or more previously added labels from the image. diff --git a/mne/viz/backends/_pysurfer_mayavi.py b/mne/viz/backends/_pysurfer_mayavi.py index 07ab16a3992..3a8e7a7116a 100644 --- a/mne/viz/backends/_pysurfer_mayavi.py +++ b/mne/viz/backends/_pysurfer_mayavi.py @@ -101,13 +101,19 @@ def mesh(self, x, y, z, triangles, color, opacity=1.0, shading=False, vmin=vmin, vmax=vmax, **kwargs) - lut = colormap - if lut is not None: - l_m = surface.module_manager.scalar_lut_manager - l_m.load_lut_from_list(lut) if vertex_color is not None: surface.module_manager.scalar_lut_manager.lut.table = \ vertex_color + elif isinstance(colormap, np.ndarray): + l_m = surface.module_manager.scalar_lut_manager + if colormap.dtype == np.uint8: + l_m.lut.table = colormap + elif colormap.dtype == np.float: + l_m.load_lut_from_list(colormap) + else: + raise TypeError('Expected type for colormap values are' + ' np.float or np.uint8: ' + '{} was given'.format(colormap.dtype)) surface.actor.property.shading = shading surface.actor.property.backface_culling = backface_culling return surface diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index c8673bd631a..292d9740ce5 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -156,7 +156,9 @@ def mesh(self, x, y, z, triangles, color, opacity=1.0, shading=False, # https://github.com/pyvista/pyvista-support/issues/15 smooth_shading = False rgba = True - if colormap is not None and isinstance(colormap, np.ndarray): + if isinstance(colormap, np.ndarray): + if colormap.dtype == np.uint8: + colormap = colormap.astype(np.float) / 255. from matplotlib.colors import ListedColormap colormap = ListedColormap(colormap) From 4d885df565245e97274a857ccb32c5ed27ee1106 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 17 Sep 2019 15:47:11 +0200 Subject: [PATCH 36/88] Add screenshot() function --- mne/viz/_brain/_brain.py | 19 ++++++++++++++++++- mne/viz/backends/_pysurfer_mayavi.py | 4 ++-- mne/viz/backends/_pyvista.py | 7 +++++-- mne/viz/backends/base_renderer.py | 10 ++++++++-- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 72bbc306fb8..feece95b111 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -259,6 +259,7 @@ def __init__(self, subject_id, hemi, surf, title=None, self._data = {} self.geo, self._hemi_meshes, self._overlays = {}, {}, {} self._renderers = [[] for _ in views] + self._renderer = None # load geometry for one or both hemispheres as necessary offset = None if (not offset or hemi != 'both') else 0.0 @@ -289,6 +290,7 @@ def __init__(self, subject_id, hemi, surf, title=None, renderer = _Renderer(size=fig_size, bgcolor=background, fig=figures[ri][ci]) self._renderers[ri].append(renderer) + self._renderer = renderer renderer.set_camera(azimuth=views_dict[v].azim, elevation=views_dict[v].elev, distance=490.0) @@ -698,7 +700,7 @@ def index_for_time(self, time, rounding='closest'): def show(self): u"""Display widget.""" try: - return self._renderers[0][0].show() + return self._renderer.show() except RuntimeError: logger.info("No active/running renderer available.") @@ -706,6 +708,21 @@ def show_view(self, view=None, roll=None, distance=None): """Orient camera to display view.""" pass + def screenshot(self, mode='rgb'): + """Generate a screenshot of current view. + + Parameters + ---------- + mode : string + Either 'rgb' or 'rgba' for values to return. + + Returns + ------- + screenshot : array + Image pixel values. + """ + return self._renderer.screenshot(mode) + def update_lut(self, fmin=None, fmid=None, fmax=None): u"""Update color map. diff --git a/mne/viz/backends/_pysurfer_mayavi.py b/mne/viz/backends/_pysurfer_mayavi.py index 3a8e7a7116a..d4c428f95a9 100644 --- a/mne/viz/backends/_pysurfer_mayavi.py +++ b/mne/viz/backends/_pysurfer_mayavi.py @@ -253,9 +253,9 @@ def set_camera(self, azimuth=None, elevation=None, distance=None, elevation=elevation, distance=distance, focalpoint=focalpoint) - def screenshot(self): + def screenshot(self, mode='rgba'): with warnings.catch_warnings(record=True): # traits - return self.mlab.screenshot(self.fig) + return self.mlab.screenshot(self.fig, mode=mode) def project(self, xyz, ch_names): xy = _3d_to_2d(self.fig, xyz) diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 292d9740ce5..724f122c3e3 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -375,8 +375,11 @@ def set_camera(self, azimuth=None, elevation=None, distance=None, _set_3d_view(self.figure, azimuth=azimuth, elevation=elevation, distance=distance, focalpoint=focalpoint) - def screenshot(self): - return self.plotter.screenshot() + def screenshot(self, mode='rgba'): + img = self.plotter.screenshot() # default mode is rgba + if mode == 'rgb': + img = img[:, :3] + return img def project(self, xyz, ch_names): xy = _3d_to_2d(self.plotter, xyz) diff --git a/mne/viz/backends/base_renderer.py b/mne/viz/backends/base_renderer.py index 5b99214511c..fd5e00fa73e 100644 --- a/mne/viz/backends/base_renderer.py +++ b/mne/viz/backends/base_renderer.py @@ -310,8 +310,14 @@ def set_camera(self, azimuth=None, elevation=None, distance=None, pass @abstractclassmethod - def screenshot(self): - """Take a screenshot of the scene.""" + def screenshot(self, mode='rgba'): + """Take a screenshot of the scene. + + Parameters + ---------- + mode: str + Either 'rgb' or 'rgba' for values to return. + """ pass @abstractclassmethod From ae7575afdb3b204f19aca80b5d90666e2ab0b327 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 17 Sep 2019 15:54:29 +0200 Subject: [PATCH 37/88] Fix default parameter for screenshot --- mne/viz/backends/_pysurfer_mayavi.py | 2 +- mne/viz/backends/_pyvista.py | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/mne/viz/backends/_pysurfer_mayavi.py b/mne/viz/backends/_pysurfer_mayavi.py index d4c428f95a9..90d0921e49b 100644 --- a/mne/viz/backends/_pysurfer_mayavi.py +++ b/mne/viz/backends/_pysurfer_mayavi.py @@ -253,7 +253,7 @@ def set_camera(self, azimuth=None, elevation=None, distance=None, elevation=elevation, distance=distance, focalpoint=focalpoint) - def screenshot(self, mode='rgba'): + def screenshot(self, mode='rgb'): with warnings.catch_warnings(record=True): # traits return self.mlab.screenshot(self.fig, mode=mode) diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 724f122c3e3..b5de759ca0f 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -375,11 +375,8 @@ def set_camera(self, azimuth=None, elevation=None, distance=None, _set_3d_view(self.figure, azimuth=azimuth, elevation=elevation, distance=distance, focalpoint=focalpoint) - def screenshot(self, mode='rgba'): - img = self.plotter.screenshot() # default mode is rgba - if mode == 'rgb': - img = img[:, :3] - return img + def screenshot(self, mode='rgb'): + return self.plotter.screenshot(transparent_background=(mode == 'rgba')) def project(self, xyz, ch_names): xy = _3d_to_2d(self.plotter, xyz) From d5289270b0d9a9c6ecac90d526348ca4aee5748d Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 17 Sep 2019 16:21:53 +0200 Subject: [PATCH 38/88] Rescale brain overview table, add close() and test screenshot() --- mne/viz/_brain/_brain.py | 75 ++++------------------------ mne/viz/_brain/tests/test_brain.py | 18 ++++--- mne/viz/backends/_pysurfer_mayavi.py | 9 +++- mne/viz/backends/renderer.py | 3 +- 4 files changed, 30 insertions(+), 75 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index feece95b111..f892ae14bf1 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -100,49 +100,25 @@ class _Brain(object): +---------------------------+--------------+-----------------------+ | 3D function: | surfer.Brain | mne.viz._brain._Brain | +===========================+==============+=======================+ - | add_annotation | ✓ | | - +---------------------------+--------------+-----------------------+ - | add_contour_overlay | ✓ | | - +---------------------------+--------------+-----------------------+ | add_data | ✓ | - | +---------------------------+--------------+-----------------------+ | add_foci | ✓ | | +---------------------------+--------------+-----------------------+ | add_label | ✓ | - | +---------------------------+--------------+-----------------------+ - | add_morphometry | ✓ | | - +---------------------------+--------------+-----------------------+ - | add_overlay | ✓ | | - +---------------------------+--------------+-----------------------+ - | add_text | ✓ | | - +---------------------------+--------------+-----------------------+ - | animate | ✓ | | - +---------------------------+--------------+-----------------------+ - | annot | ✓ | | - +---------------------------+--------------+-----------------------+ - | close | ✓ | | - +---------------------------+--------------+-----------------------+ - | contour | ✓ | | + | close | ✓ | ✓ | +---------------------------+--------------+-----------------------+ | data | ✓ | ✓ | +---------------------------+--------------+-----------------------+ - | data_dict | ✓ | | - +---------------------------+--------------+-----------------------+ - | data_time_index | ✓ | | - +---------------------------+--------------+-----------------------+ | foci | ✓ | | +---------------------------+--------------+-----------------------+ - | get_data_properties | ✓ | | - +---------------------------+--------------+-----------------------+ - | hide_colorbar | ✓ | | - +---------------------------+--------------+-----------------------+ - | index_for_time | ✓ | | + | index_for_time | ✓ | ✓ | +---------------------------+--------------+-----------------------+ | labels | ✓ | | +---------------------------+--------------+-----------------------+ | labels_dict | ✓ | | +---------------------------+--------------+-----------------------+ - | overlays | ✓ | | + | overlays | ✓ | - | +---------------------------+--------------+-----------------------+ | remove_data | ✓ | | +---------------------------+--------------+-----------------------+ @@ -150,46 +126,10 @@ class _Brain(object): +---------------------------+--------------+-----------------------+ | remove_labels | ✓ | - | +---------------------------+--------------+-----------------------+ - | reset_view | ✓ | | - +---------------------------+--------------+-----------------------+ - | save_image | ✓ | | - +---------------------------+--------------+-----------------------+ - | save_image_sequence | ✓ | | - +---------------------------+--------------+-----------------------+ - | save_imageset | ✓ | | - +---------------------------+--------------+-----------------------+ - | save_montage | ✓ | | - +---------------------------+--------------+-----------------------+ - | save_movie | ✓ | | - +---------------------------+--------------+-----------------------+ - | save_single_image | ✓ | | - +---------------------------+--------------+-----------------------+ - | scale_data_colormap | ✓ | | - +---------------------------+--------------+-----------------------+ - | screenshot | ✓ | | - +---------------------------+--------------+-----------------------+ - | screenshot_single | ✓ | | - +---------------------------+--------------+-----------------------+ - | set_data_smoothing_steps | ✓ | | - +---------------------------+--------------+-----------------------+ - | set_data_time_index | ✓ | | - +---------------------------+--------------+-----------------------+ - | set_distance | ✓ | | - +---------------------------+--------------+-----------------------+ - | set_surf | ✓ | | - +---------------------------+--------------+-----------------------+ - | set_time | ✓ | | - +---------------------------+--------------+-----------------------+ - | show_colorbar | ✓ | | + | screenshot | ✓ | ✓ | +---------------------------+--------------+-----------------------+ | show_view | ✓ | - | +---------------------------+--------------+-----------------------+ - | texts | ✓ | | - +---------------------------+--------------+-----------------------+ - | toggle_toolbars | ✓ | | - +---------------------------+--------------+-----------------------+ - | update_text | ✓ | | - +---------------------------+--------------+-----------------------+ """ @@ -697,6 +637,13 @@ def index_for_time(self, time, rounding='closest'): return idx + def close(self): + """Close all figures and cleanup data structure.""" + for renderer in self._renderers: + if renderer is not None: + renderer.close() + self._renderers = [] + def show(self): u"""Display widget.""" try: diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index f9596d389fe..9c2048285b5 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -28,7 +28,6 @@ @testing.requires_testing_data def test_brain_init(renderer): """Test initialization of the _Brain instance.""" - backend_name = renderer.get_3d_backend() hemi = 'both' with pytest.raises(ValueError, match='hemi'): @@ -41,14 +40,21 @@ def test_brain_init(renderer): _Brain(subject_id=subject_id, hemi="foo", surf=surf) brain = _Brain(subject_id, hemi, surf, subjects_dir=subjects_dir) - if backend_name != 'mayavi': - brain.show() + brain.show() + + +@testing.requires_testing_data +def test_brain_screenshot(renderer): + brain = _Brain(subject_id, hemi='both', + surf=surf, subjects_dir=subjects_dir) + img = brain.screenshot(mode='rgba') + brain.show() + # assert(img.shape == [600, 600, 4]) @testing.requires_testing_data def test_brain_add_data(renderer): """Test adding data in _Brain instance.""" - backend_name = renderer.get_3d_backend() stc = read_source_estimate(fname_stc) hemi = 'lh' @@ -69,9 +75,7 @@ def test_brain_add_data(renderer): brain_data.add_data(hemi_data, fmin=fmin, hemi=hemi, fmax=fmax, colormap='hot', vertices=hemi_vertices, colorbar=False) - - if backend_name != 'mayavi': - brain_data.show() + brain_data.show() def test_brain_colormap(): diff --git a/mne/viz/backends/_pysurfer_mayavi.py b/mne/viz/backends/_pysurfer_mayavi.py index 90d0921e49b..b6f6bc25b39 100644 --- a/mne/viz/backends/_pysurfer_mayavi.py +++ b/mne/viz/backends/_pysurfer_mayavi.py @@ -60,6 +60,7 @@ class _Renderer(_BaseRenderer): def __init__(self, fig=None, size=(600, 600), bgcolor=(0., 0., 0.), name=None, show=False): self.mlab = _import_mlab() + self.window_size = size if fig is None: self.fig = _mlab_figure(figure=name, bgcolor=bgcolor, size=size) elif isinstance(fig, int): @@ -254,8 +255,12 @@ def set_camera(self, azimuth=None, elevation=None, distance=None, focalpoint=focalpoint) def screenshot(self, mode='rgb'): - with warnings.catch_warnings(record=True): # traits - return self.mlab.screenshot(self.fig, mode=mode) + from mne.viz.backends.renderer import MNE_3D_BACKEND_TEST_DATA + if MNE_3D_BACKEND_TEST_DATA: + return np.zeros(tuple(self.window_size) + (3,), np.uint8) + else: + with warnings.catch_warnings(record=True): # traits + return self.mlab.screenshot(self.fig, mode=mode) def project(self, xyz, ch_names): xy = _3d_to_2d(self.fig, xyz) diff --git a/mne/viz/backends/renderer.py b/mne/viz/backends/renderer.py index 58ef0c6250a..c780ca16909 100644 --- a/mne/viz/backends/renderer.py +++ b/mne/viz/backends/renderer.py @@ -139,8 +139,7 @@ def _use_test_3d_backend(backend_name): """ with use_3d_backend(backend_name): global MNE_3D_BACKEND_TEST_DATA - if backend_name == 'pyvista': - MNE_3D_BACKEND_TEST_DATA = True + MNE_3D_BACKEND_TEST_DATA = True yield From ad27d4a560944e1d172ac455e5f7042d17449143 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 17 Sep 2019 16:37:09 +0200 Subject: [PATCH 39/88] Fix test_screenshot() --- mne/viz/_brain/tests/test_brain.py | 2 +- mne/viz/backends/_pysurfer_mayavi.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 9c2048285b5..20e73906f5b 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -49,7 +49,7 @@ def test_brain_screenshot(renderer): surf=surf, subjects_dir=subjects_dir) img = brain.screenshot(mode='rgba') brain.show() - # assert(img.shape == [600, 600, 4]) + assert(img.shape == (800, 800, 4)) @testing.requires_testing_data diff --git a/mne/viz/backends/_pysurfer_mayavi.py b/mne/viz/backends/_pysurfer_mayavi.py index b6f6bc25b39..87eb3fa5d0f 100644 --- a/mne/viz/backends/_pysurfer_mayavi.py +++ b/mne/viz/backends/_pysurfer_mayavi.py @@ -257,7 +257,8 @@ def set_camera(self, azimuth=None, elevation=None, distance=None, def screenshot(self, mode='rgb'): from mne.viz.backends.renderer import MNE_3D_BACKEND_TEST_DATA if MNE_3D_BACKEND_TEST_DATA: - return np.zeros(tuple(self.window_size) + (3,), np.uint8) + ndim = 3 if mode == 'rgb' else 4 + return np.zeros(tuple(self.window_size) + (ndim,), np.uint8) else: with warnings.catch_warnings(record=True): # traits return self.mlab.screenshot(self.fig, mode=mode) From bb86df03fee85709e48c79f8e6d83359676340df Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 17 Sep 2019 16:45:30 +0200 Subject: [PATCH 40/88] Fix default mode for screenshot --- mne/viz/backends/base_renderer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mne/viz/backends/base_renderer.py b/mne/viz/backends/base_renderer.py index fd5e00fa73e..59c3d71cc96 100644 --- a/mne/viz/backends/base_renderer.py +++ b/mne/viz/backends/base_renderer.py @@ -310,13 +310,14 @@ def set_camera(self, azimuth=None, elevation=None, distance=None, pass @abstractclassmethod - def screenshot(self, mode='rgba'): + def screenshot(self, mode='rgb'): """Take a screenshot of the scene. Parameters ---------- mode: str Either 'rgb' or 'rgba' for values to return. + Default is 'rgb'. """ pass From e4fa6c2581fb8a2d808b652afbfb6063ffa29bea Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 17 Sep 2019 17:39:38 +0200 Subject: [PATCH 41/88] TST: check appveyor with smaller size --- mne/viz/_brain/tests/test_brain.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 20e73906f5b..3c4ed9adfd5 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -45,11 +45,12 @@ def test_brain_init(renderer): @testing.requires_testing_data def test_brain_screenshot(renderer): - brain = _Brain(subject_id, hemi='both', + """Test screenshot of a _Brain instance.""" + brain = _Brain(subject_id, hemi='both', size=600, surf=surf, subjects_dir=subjects_dir) img = brain.screenshot(mode='rgba') brain.show() - assert(img.shape == (800, 800, 4)) + assert(img.shape == (600, 600, 4)) @testing.requires_testing_data From 53668ea8063b8ed648f4764ea1e8fbdb4aa9d917 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 17 Sep 2019 17:42:26 +0200 Subject: [PATCH 42/88] Postpone 3 lgtm alerts for label_name --- mne/viz/_brain/_brain.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index f892ae14bf1..935039f538b 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -562,6 +562,9 @@ def add_label(self, label, color=None, alpha=1, scalar_thresh=None, if scalar_thresh is not None: ids = ids[scalars >= scalar_thresh] + # XXX: add support for label_name + self._label_name = label_name + label = np.zeros(self.geo[hemi].coords.shape[0]) label[ids] = 1 color = colorConverter.to_rgba(color, alpha) From 4237b42ed178876bddd9baa26a88035504d2e0fc Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 18 Sep 2019 11:06:33 +0200 Subject: [PATCH 43/88] Add add_foci() function signature --- mne/viz/_brain/_brain.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 935039f538b..81fe43b6491 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -102,7 +102,7 @@ class _Brain(object): +===========================+==============+=======================+ | add_data | ✓ | - | +---------------------------+--------------+-----------------------+ - | add_foci | ✓ | | + | add_foci | ✓ | - | +---------------------------+--------------+-----------------------+ | add_label | ✓ | - | +---------------------------+--------------+-----------------------+ @@ -586,6 +586,41 @@ def add_label(self, label, color=None, alpha=1, scalar_thresh=None, colormap=ctable, backface_culling=False) + def add_foci(self, coords, coords_as_verts=False, map_surface=None, + scale_factor=1, color="white", alpha=1, name=None, + hemi=None): + """Add spherical foci, possibly mapping to displayed surf. + + The foci spheres can be displayed at the coordinates given, or + mapped through a surface geometry. In other words, coordinates + from a volume-based analysis in MNI space can be displayed on an + inflated average surface by finding the closest vertex on the + white surface and mapping to that vertex on the inflated mesh. + + Parameters + ---------- + coords : numpy array + x, y, z coordinates in stereotaxic space (default) or array of + vertex ids (with ``coord_as_verts=True``) + coords_as_verts : bool + whether the coords parameter should be interpreted as vertex ids + map_surface : Freesurfer surf or None + surface to map coordinates through, or None to use raw coords + scale_factor : float + Controls the size of the foci spheres (relative to 1cm). + color : matplotlib color code + HTML name, RBG tuple, or hex code + alpha : float in [0, 1] + opacity of focus gylphs + name : str + internal name to use + hemi : str | None + If None, it is assumed to belong to the hemipshere being + shown. If two hemispheres are being shown, an error will + be thrown. + """ + pass + def remove_labels(self, labels=None, hemi=None): """Remove one or more previously added labels from the image. From 78b85fcff7531203d4cd28188f8ad0fa251ee883 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 18 Sep 2019 11:40:43 +0200 Subject: [PATCH 44/88] Fill up add_foci() and add add_text() signature --- mne/viz/_brain/_brain.py | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 81fe43b6491..826b640f079 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -106,6 +106,8 @@ class _Brain(object): +---------------------------+--------------+-----------------------+ | add_label | ✓ | - | +---------------------------+--------------+-----------------------+ + | add_text | ✓ | - | + +---------------------------+--------------+-----------------------+ | close | ✓ | ✓ | +---------------------------+--------------+-----------------------+ | data | ✓ | ✓ | @@ -619,6 +621,56 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, shown. If two hemispheres are being shown, an error will be thrown. """ + from matplotlib.colors import colorConverter + hemi = self._check_hemi(hemi) + + # those parameters are not supported yet, only None is allowed + _check_option('map_surface', map_surface, [None]) + + # Figure out how to interpret the first parameter + if coords_as_verts: + coords = self.geo[hemi].coords[coords] + + # Convert the color code + if not isinstance(color, tuple): + color = colorConverter.to_rgb(color) + + if self._units == 'm': + scale_factor = scale_factor / 1000. + for ri, v in enumerate(self._views): + if self._hemi != 'split': + ci = 0 + else: + ci = 0 if hemi == 'lh' else 1 + renderer = self._renderers[ri][ci] + renderer.sphere(center=coords, color=color, + scale=(10. * scale_factor), opacity=alpha) + + def add_text(self, x, y, text, name, color=None, opacity=1.0, + row=-1, col=-1, font_size=None, justification=None): + """ Add a text to the visualization + + Parameters + ---------- + x : Float + x coordinate + y : Float + y coordinate + text : str + Text to add + name : str + Name of the text (text label can be updated using update_text()) + color : Tuple + Color of the text. Default is the foreground color set during + initialization (default is black or white depending on the + background color). + opacity : Float + Opacity of the text. Default: 1.0 + row : int + Row index of which brain to use + col : int + Column index of which brain to use + """ pass def remove_labels(self, labels=None, hemi=None): From 2d8d0db16e55f1f42f296c08be46b45d2b492f50 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 19 Sep 2019 11:41:49 +0200 Subject: [PATCH 45/88] Fix variable names --- mne/viz/_brain/_brain.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 826b640f079..73d412ef548 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -192,6 +192,7 @@ def __init__(self, subject_id, hemi, surf, title=None, self._units = units self._title = title self._subject_id = subject_id + self._subjects_dir = subjects_dir self._views = views self._n_times = None # for now only one color bar can be added @@ -520,10 +521,10 @@ def add_label(self, label, color=None, alpha=1, scalar_thresh=None, label_name = label label_fname = ".".join([hemi, label_name, 'label']) if subdir is None: - filepath = pjoin(self.subjects_dir, self.subject_id, + filepath = pjoin(self._subjects_dir, self._subject_id, 'label', label_fname) else: - filepath = pjoin(self.subjects_dir, self.subject_id, + filepath = pjoin(self._subjects_dir, self._subject_id, 'label', subdir, label_fname) if not os.path.exists(filepath): raise ValueError('Label file %s does not exist' From fb8e0293f6266528fc9f8e09acd582c588bcebd5 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 19 Sep 2019 11:59:43 +0200 Subject: [PATCH 46/88] Fix close() function --- mne/viz/_brain/_brain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 73d412ef548..f3dd3bf7fd5 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -730,7 +730,8 @@ def index_for_time(self, time, rounding='closest'): def close(self): """Close all figures and cleanup data structure.""" - for renderer in self._renderers: + import itertools + for renderer in list(itertools.chain(*self._renderers)): if renderer is not None: renderer.close() self._renderers = [] From 452205fb7c6e49f4e482dd604458d8bd6105d303 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 19 Sep 2019 14:07:31 +0200 Subject: [PATCH 47/88] Fix dependency to nibabel --- mne/viz/_brain/_brain.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index f3dd3bf7fd5..c002fe4baf9 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -9,8 +9,8 @@ import numpy as np import os -import nibabel as nib from os.path import join as pjoin +from ...label import read_label from .colormap import _calculate_lut from .view import views_dict from .surface import Surface @@ -529,13 +529,8 @@ def add_label(self, label, color=None, alpha=1, scalar_thresh=None, if not os.path.exists(filepath): raise ValueError('Label file %s does not exist' % filepath) - # Load the label data and create binary overlay - if scalar_thresh is None: - ids = nib.freesurfer.read_label(filepath) - else: - ids, scalars = nib.freesurfer.read_label(filepath, - read_scalars=True) - ids = ids[scalars >= scalar_thresh] + label = read_label(filepath) + ids = label.vertices else: # try to extract parameters from label instance try: @@ -649,7 +644,7 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, def add_text(self, x, y, text, name, color=None, opacity=1.0, row=-1, col=-1, font_size=None, justification=None): - """ Add a text to the visualization + """Add a text to the visualization. Parameters ---------- From 856cb8e764453270c662aee338844116b0c53beb Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 19 Sep 2019 14:48:48 +0200 Subject: [PATCH 48/88] Try subplot system --- mne/viz/_brain/_brain.py | 134 +++++++++++------------------- mne/viz/backends/_pyvista.py | 5 +- mne/viz/backends/base_renderer.py | 2 +- 3 files changed, 55 insertions(+), 86 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index c002fe4baf9..83c5b515216 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -146,7 +146,7 @@ def __init__(self, subject_id, hemi, surf, title=None, if interaction is not None: raise ValueError('"interaction" parameter is not supported.') - from ..backends.renderer import _Renderer, create_3d_figure + from ..backends.renderer import _Renderer from matplotlib.colors import colorConverter if isinstance(background, str): @@ -166,26 +166,8 @@ def __init__(self, subject_id, hemi, surf, title=None, else: raise ValueError('"size" parameter must be int or tuple.') - if figure is None: - figures = [[create_3d_figure(size=fig_size, - bgcolor=background)]] - else: - if isinstance(figure, int): - figure = [create_3d_figure(size=fig_size, - bgcolor=background, - handle=figure)] - elif not isinstance(figure, list): - raise TypeError('Expected type for `figure` is scene, ' - 'list, int or None: ' - '{} was given'.format(type(figure))) - - if not len(figure) == n_row * n_col: - raise ValueError('For the requested view, figure must be a ' - 'list or tuple with exactly %i elements, ' - 'not %i' % (n_row * n_col, len(figure))) - - figures = [figure[slice(ri * n_col, (ri + 1) * n_col)] - for ri in range(n_row)] + self._renderer = _Renderer(size=fig_size, bgcolor=background, + shape=(n_row, n_col), fig=figure) self._foreground = foreground self._hemi = hemi @@ -201,8 +183,6 @@ def __init__(self, subject_id, hemi, surf, title=None, # array of data used by TimeViewer self._data = {} self.geo, self._hemi_meshes, self._overlays = {}, {}, {} - self._renderers = [[] for _ in views] - self._renderer = None # load geometry for one or both hemispheres as necessary offset = None if (not offset or hemi != 'both') else 0.0 @@ -229,22 +209,17 @@ def __init__(self, subject_id, hemi, surf, title=None, for ri, v in enumerate(views): for ci, h in enumerate(self._hemis): - ci = ri if hemi == 'split' else 0 - renderer = _Renderer(size=fig_size, bgcolor=background, - fig=figures[ri][ci]) - self._renderers[ri].append(renderer) - self._renderer = renderer - renderer.set_camera(azimuth=views_dict[v].azim, - elevation=views_dict[v].elev, - distance=490.0) - - mesh = renderer.mesh(x=self.geo[h].coords[:, 0], - y=self.geo[h].coords[:, 1], - z=self.geo[h].coords[:, 2], - triangles=self.geo[h].faces, - color=self.geo[h].grey_curv) - - self._hemi_meshes[h + '_' + v] = mesh + ci = 1 if hemi == 'split' else 0 + self._renderer.subplot(ri, ci) + self._renderer.set_camera(azimuth=views_dict[v].azim, + elevation=views_dict[v].elev, + distance=490.0) + + self._renderer.mesh(x=self.geo[h].coords[:, 0], + y=self.geo[h].coords[:, 1], + z=self.geo[h].coords[:, 2], + triangles=self.geo[h].faces, + color=self.geo[h].grey_curv) def add_data(self, array, fmin=None, fmid=None, fmax=None, thresh=None, center=None, transparent=False, colormap="auto", @@ -449,26 +424,24 @@ def time_label(x): ctable = self.update_lut() for ri, v in enumerate(self._views): - if self._hemi != 'split': - ci = 0 - else: - ci = 0 if hemi == 'lh' else 1 - renderer = self._renderers[ri][ci] - mesh = renderer.mesh(x=self.geo[hemi].coords[:, 0], - y=self.geo[hemi].coords[:, 1], - z=self.geo[hemi].coords[:, 2], - triangles=self.geo[hemi].faces, - color=None, - colormap=ctable, - vmin=dt_min, - vmax=dt_max, - scalars=act_data) - if array.ndim >= 2 and callable(time_label): - renderer.text2d(x=0.95, y=y_txt, size=time_label_size, - text=time_label(time[time_idx])) - renderer.scalarbar(source=mesh, n_labels=8, - bgcolor=(0.5, 0.5, 0.5)) - self._overlays[hemi + '_' + v] = mesh + for ci, h in enumerate(self._hemis): + ci = 1 if hemi == 'split' else 0 + self._renderer.subplot(ri, ci) + mesh = self._renderer.mesh(x=self.geo[hemi].coords[:, 0], + y=self.geo[hemi].coords[:, 1], + z=self.geo[hemi].coords[:, 2], + triangles=self.geo[hemi].faces, + color=None, + colormap=ctable, + vmin=dt_min, + vmax=dt_max, + scalars=act_data) + if array.ndim >= 2 and callable(time_label): + self._renderer.text2d(x=0.95, y=y_txt, + size=time_label_size, + text=time_label(time[time_idx])) + self._renderer.scalarbar(source=mesh, n_labels=8, + bgcolor=(0.5, 0.5, 0.5)) def add_label(self, label, color=None, alpha=1, scalar_thresh=None, borders=False, hemi=None, subdir=None): @@ -570,19 +543,17 @@ def add_label(self, label, color=None, alpha=1, scalar_thresh=None, ctable = np.round(cmap * 255).astype(np.uint8) for ri, v in enumerate(self._views): - if self._hemi != 'split': - ci = 0 - else: - ci = 0 if hemi == 'lh' else 1 - renderer = self._renderers[ri][ci] - renderer.mesh(x=self.geo[hemi].coords[:, 0], - y=self.geo[hemi].coords[:, 1], - z=self.geo[hemi].coords[:, 2], - triangles=self.geo[hemi].faces, - scalars=label, - color=None, - colormap=ctable, - backface_culling=False) + for ci, h in enumerate(self._hemis): + ci = 1 if hemi == 'split' else 0 + self._renderer.subplot(ri, ci) + self._renderer.mesh(x=self.geo[hemi].coords[:, 0], + y=self.geo[hemi].coords[:, 1], + z=self.geo[hemi].coords[:, 2], + triangles=self.geo[hemi].faces, + scalars=label, + color=None, + colormap=ctable, + backface_culling=False) def add_foci(self, coords, coords_as_verts=False, map_surface=None, scale_factor=1, color="white", alpha=1, name=None, @@ -634,13 +605,12 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, if self._units == 'm': scale_factor = scale_factor / 1000. for ri, v in enumerate(self._views): - if self._hemi != 'split': - ci = 0 - else: - ci = 0 if hemi == 'lh' else 1 - renderer = self._renderers[ri][ci] - renderer.sphere(center=coords, color=color, - scale=(10. * scale_factor), opacity=alpha) + for ci, h in enumerate(self._hemis): + ci = 1 if hemi == 'split' else 0 + self._renderer.subplot(ri, ci) + self._renderer.sphere(center=coords, color=color, + scale=(10. * scale_factor), + opacity=alpha) def add_text(self, x, y, text, name, color=None, opacity=1.0, row=-1, col=-1, font_size=None, justification=None): @@ -725,11 +695,7 @@ def index_for_time(self, time, rounding='closest'): def close(self): """Close all figures and cleanup data structure.""" - import itertools - for renderer in list(itertools.chain(*self._renderers)): - if renderer is not None: - renderer.close() - self._renderers = [] + self._renderer.close() def show(self): u"""Display widget.""" diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index b5de759ca0f..57f95dded03 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -99,7 +99,7 @@ class _Renderer(_BaseRenderer): """ def __init__(self, fig=None, size=(600, 600), bgcolor=(0., 0., 0.), - name="PyVista Scene", show=False): + name="PyVista Scene", show=False, shape=(1, 1)): from mne.viz.backends.renderer import MNE_3D_BACKEND_TEST_DATA if isinstance(fig, int): if _FIGURES.get(fig) is None: @@ -126,6 +126,9 @@ def __init__(self, fig=None, size=(600, 600), bgcolor=(0., 0., 0.), self.plotter = self.figure.build() self.plotter.hide_axes() + def subplot(self, x, y): + self.plotter.subplot(x, y) + def scene(self): return self.figure diff --git a/mne/viz/backends/base_renderer.py b/mne/viz/backends/base_renderer.py index 59c3d71cc96..90539ff90f2 100644 --- a/mne/viz/backends/base_renderer.py +++ b/mne/viz/backends/base_renderer.py @@ -13,7 +13,7 @@ class _BaseRenderer(metaclass=ABCMeta): @abstractclassmethod def __init__(self, fig=None, size=(600, 600), bgcolor=(0., 0., 0.), - name=None, show=False): + name=None, show=False, shape=(1, 1)): """Set up the scene.""" pass From 5b9188677d78d37d243b6494ad06cc54e033f458 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 19 Sep 2019 14:58:59 +0200 Subject: [PATCH 49/88] Fix shape integration in pyvista backend --- mne/viz/backends/_pyvista.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 57f95dded03..4079baff5cb 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -27,6 +27,7 @@ def __init__(self, plotter=None, display=None, title='PyVista Scene', size=(600, 600), + shape=(1, 1), background_color=(0., 0., 0.), smooth_shading=True, off_screen=False, @@ -41,6 +42,7 @@ def __init__(self, plotter=None, self.store = dict() self.store['title'] = title self.store['window_size'] = size + self.store['shape'] = shape self.store['off_screen'] = off_screen def build(self): @@ -104,6 +106,7 @@ def __init__(self, fig=None, size=(600, 600), bgcolor=(0., 0., 0.), if isinstance(fig, int): if _FIGURES.get(fig) is None: self.figure = _Figure(title=name, size=size, + shape=shape, background_color=bgcolor, notebook=_check_notebook()) _FIGURES[fig] = self.figure @@ -111,6 +114,7 @@ def __init__(self, fig=None, size=(600, 600), bgcolor=(0., 0., 0.), self.figure = _FIGURES.get(fig) elif fig is None: self.figure = _Figure(title=name, size=size, + shape=shape, background_color=bgcolor, notebook=_check_notebook()) else: From a32cac082da4948828c4d89daef60ceda0111128 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 19 Sep 2019 15:04:37 +0200 Subject: [PATCH 50/88] Restore shape coords --- mne/viz/_brain/_brain.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 83c5b515216..291755d5935 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -209,7 +209,10 @@ def __init__(self, subject_id, hemi, surf, title=None, for ri, v in enumerate(views): for ci, h in enumerate(self._hemis): - ci = 1 if hemi == 'split' else 0 + if self._hemi != 'split': + ci = 0 + else: + ci = 0 if hemi == 'lh' else 1 self._renderer.subplot(ri, ci) self._renderer.set_camera(azimuth=views_dict[v].azim, elevation=views_dict[v].elev, @@ -425,7 +428,10 @@ def time_label(x): for ri, v in enumerate(self._views): for ci, h in enumerate(self._hemis): - ci = 1 if hemi == 'split' else 0 + if self._hemi != 'split': + ci = 0 + else: + ci = 0 if hemi == 'lh' else 1 self._renderer.subplot(ri, ci) mesh = self._renderer.mesh(x=self.geo[hemi].coords[:, 0], y=self.geo[hemi].coords[:, 1], @@ -544,7 +550,10 @@ def add_label(self, label, color=None, alpha=1, scalar_thresh=None, for ri, v in enumerate(self._views): for ci, h in enumerate(self._hemis): - ci = 1 if hemi == 'split' else 0 + if self._hemi != 'split': + ci = 0 + else: + ci = 0 if hemi == 'lh' else 1 self._renderer.subplot(ri, ci) self._renderer.mesh(x=self.geo[hemi].coords[:, 0], y=self.geo[hemi].coords[:, 1], @@ -606,7 +615,10 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, scale_factor = scale_factor / 1000. for ri, v in enumerate(self._views): for ci, h in enumerate(self._hemis): - ci = 1 if hemi == 'split' else 0 + if self._hemi != 'split': + ci = 0 + else: + ci = 0 if hemi == 'lh' else 1 self._renderer.subplot(ri, ci) self._renderer.sphere(center=coords, color=color, scale=(10. * scale_factor), From 9b50ed6f4a570bc09621b16a3a0a52adf00f46b6 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 19 Sep 2019 17:34:28 +0200 Subject: [PATCH 51/88] Add basic support for hemi='split' for the pyvista backend --- mne/viz/_brain/_brain.py | 116 ++++++++++++++++++--------------------- 1 file changed, 54 insertions(+), 62 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 291755d5935..f4b16378834 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -140,9 +140,6 @@ def __init__(self, subject_id, hemi, surf, title=None, foreground=None, figure=None, subjects_dir=None, views=['lateral'], offset=True, show_toolbar=False, offscreen=False, interaction=None, units='mm'): - if hemi == 'split': - raise ValueError('Option hemi="split" is not supported yet.') - if interaction is not None: raise ValueError('"interaction" parameter is not supported.') @@ -208,21 +205,19 @@ def __init__(self, subject_id, hemi, surf, title=None, self.geo[h] = geo for ri, v in enumerate(views): - for ci, h in enumerate(self._hemis): - if self._hemi != 'split': - ci = 0 - else: - ci = 0 if hemi == 'lh' else 1 - self._renderer.subplot(ri, ci) - self._renderer.set_camera(azimuth=views_dict[v].azim, - elevation=views_dict[v].elev, - distance=490.0) - - self._renderer.mesh(x=self.geo[h].coords[:, 0], - y=self.geo[h].coords[:, 1], - z=self.geo[h].coords[:, 2], - triangles=self.geo[h].faces, - color=self.geo[h].grey_curv) + for hi, h in enumerate(['lh', 'rh']): + if not (hemi in ['lh', 'rh'] and h != hemi): + ci = hi if hemi == 'split' else 0 + self._renderer.subplot(ri, ci) + self._renderer.set_camera(azimuth=views_dict[v].azim, + elevation=views_dict[v].elev, + distance=490.0) + + self._renderer.mesh(x=self.geo[h].coords[:, 0], + y=self.geo[h].coords[:, 1], + z=self.geo[h].coords[:, 2], + triangles=self.geo[h].faces, + color=self.geo[h].grey_curv) def add_data(self, array, fmin=None, fmid=None, fmax=None, thresh=None, center=None, transparent=False, colormap="auto", @@ -427,27 +422,26 @@ def time_label(x): ctable = self.update_lut() for ri, v in enumerate(self._views): - for ci, h in enumerate(self._hemis): - if self._hemi != 'split': - ci = 0 - else: - ci = 0 if hemi == 'lh' else 1 - self._renderer.subplot(ri, ci) - mesh = self._renderer.mesh(x=self.geo[hemi].coords[:, 0], - y=self.geo[hemi].coords[:, 1], - z=self.geo[hemi].coords[:, 2], - triangles=self.geo[hemi].faces, - color=None, - colormap=ctable, - vmin=dt_min, - vmax=dt_max, - scalars=act_data) - if array.ndim >= 2 and callable(time_label): - self._renderer.text2d(x=0.95, y=y_txt, - size=time_label_size, - text=time_label(time[time_idx])) - self._renderer.scalarbar(source=mesh, n_labels=8, - bgcolor=(0.5, 0.5, 0.5)) + if self._hemi != 'split': + ci = 0 + else: + ci = 0 if hemi == 'lh' else 1 + self._renderer.subplot(ri, ci) + mesh = self._renderer.mesh(x=self.geo[hemi].coords[:, 0], + y=self.geo[hemi].coords[:, 1], + z=self.geo[hemi].coords[:, 2], + triangles=self.geo[hemi].faces, + color=None, + colormap=ctable, + vmin=dt_min, + vmax=dt_max, + scalars=act_data) + if array.ndim >= 2 and callable(time_label): + self._renderer.text2d(x=0.95, y=y_txt, + size=time_label_size, + text=time_label(time[time_idx])) + self._renderer.scalarbar(source=mesh, n_labels=8, + bgcolor=(0.5, 0.5, 0.5)) def add_label(self, label, color=None, alpha=1, scalar_thresh=None, borders=False, hemi=None, subdir=None): @@ -549,20 +543,19 @@ def add_label(self, label, color=None, alpha=1, scalar_thresh=None, ctable = np.round(cmap * 255).astype(np.uint8) for ri, v in enumerate(self._views): - for ci, h in enumerate(self._hemis): - if self._hemi != 'split': - ci = 0 - else: - ci = 0 if hemi == 'lh' else 1 - self._renderer.subplot(ri, ci) - self._renderer.mesh(x=self.geo[hemi].coords[:, 0], - y=self.geo[hemi].coords[:, 1], - z=self.geo[hemi].coords[:, 2], - triangles=self.geo[hemi].faces, - scalars=label, - color=None, - colormap=ctable, - backface_culling=False) + if self._hemi != 'split': + ci = 0 + else: + ci = 0 if hemi == 'lh' else 1 + self._renderer.subplot(ri, ci) + self._renderer.mesh(x=self.geo[hemi].coords[:, 0], + y=self.geo[hemi].coords[:, 1], + z=self.geo[hemi].coords[:, 2], + triangles=self.geo[hemi].faces, + scalars=label, + color=None, + colormap=ctable, + backface_culling=False) def add_foci(self, coords, coords_as_verts=False, map_surface=None, scale_factor=1, color="white", alpha=1, name=None, @@ -614,15 +607,14 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, if self._units == 'm': scale_factor = scale_factor / 1000. for ri, v in enumerate(self._views): - for ci, h in enumerate(self._hemis): - if self._hemi != 'split': - ci = 0 - else: - ci = 0 if hemi == 'lh' else 1 - self._renderer.subplot(ri, ci) - self._renderer.sphere(center=coords, color=color, - scale=(10. * scale_factor), - opacity=alpha) + if self._hemi != 'split': + ci = 0 + else: + ci = 0 if hemi == 'lh' else 1 + self._renderer.subplot(ri, ci) + self._renderer.sphere(center=coords, color=color, + scale=(10. * scale_factor), + opacity=alpha) def add_text(self, x, y, text, name, color=None, opacity=1.0, row=-1, col=-1, font_size=None, justification=None): From d04b363dd792d8b433b462b8f19c32fc656307f0 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 19 Sep 2019 18:23:10 +0200 Subject: [PATCH 52/88] Add small code refactor and fixes --- mne/viz/_brain/_brain.py | 10 ++++++---- mne/viz/_brain/tests/test_brain.py | 2 -- mne/viz/backends/_pysurfer_mayavi.py | 9 ++++++--- mne/viz/backends/_pyvista.py | 10 +++++++--- mne/viz/backends/base_renderer.py | 5 +++++ mne/viz/tests/test_3d.py | 10 ++++------ 6 files changed, 28 insertions(+), 18 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index f4b16378834..79f7d1e99cc 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -143,7 +143,7 @@ def __init__(self, subject_id, hemi, surf, title=None, if interaction is not None: raise ValueError('"interaction" parameter is not supported.') - from ..backends.renderer import _Renderer + from ..backends.renderer import _Renderer, _check_figure from matplotlib.colors import colorConverter if isinstance(background, str): @@ -163,9 +163,6 @@ def __init__(self, subject_id, hemi, surf, title=None, else: raise ValueError('"size" parameter must be int or tuple.') - self._renderer = _Renderer(size=fig_size, bgcolor=background, - shape=(n_row, n_col), fig=figure) - self._foreground = foreground self._hemi = hemi self._units = units @@ -192,6 +189,11 @@ def __init__(self, subject_id, hemi, surf, title=None, raise KeyError('hemi has to be either "lh", "rh", "split", ' 'or "both"') + if figure is not None and not isinstance(figure, int): + _check_figure(figure) + self._renderer = _Renderer(size=fig_size, bgcolor=background, + shape=(n_row, n_col), fig=figure) + # XXX: use the geo_ variables # geo_kwargs, geo_reverse, geo_curv = \ # self._get_geo_params(cortex, alpha) diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 3c4ed9adfd5..24bcbd95ce4 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -30,8 +30,6 @@ def test_brain_init(renderer): """Test initialization of the _Brain instance.""" hemi = 'both' - with pytest.raises(ValueError, match='hemi'): - _Brain(subject_id=subject_id, hemi="split", surf=surf) with pytest.raises(TypeError, match='figure'): _Brain(subject_id=subject_id, hemi=hemi, surf=surf, figure='foo') with pytest.raises(ValueError, match='interaction'): diff --git a/mne/viz/backends/_pysurfer_mayavi.py b/mne/viz/backends/_pysurfer_mayavi.py index 87eb3fa5d0f..7faf7a7c325 100644 --- a/mne/viz/backends/_pysurfer_mayavi.py +++ b/mne/viz/backends/_pysurfer_mayavi.py @@ -58,7 +58,7 @@ class _Renderer(_BaseRenderer): """ def __init__(self, fig=None, size=(600, 600), bgcolor=(0., 0., 0.), - name=None, show=False): + name=None, show=False, shape=(1, 1)): self.mlab = _import_mlab() self.window_size = size if fig is None: @@ -70,6 +70,9 @@ def __init__(self, fig=None, size=(600, 600), bgcolor=(0., 0., 0.), if show is False: _toggle_mlab_render(self.fig, False) + def subplot(self, x, y): + pass + def scene(self): return self.fig @@ -404,5 +407,5 @@ def _set_3d_title(figure, title, size=40): def _check_figure(figure): from mayavi.core.scene import Scene - if not all(isinstance(f, Scene) for f in figure): - raise TypeError('figure must be a mayavi scene or list of scenes') + if not isinstance(figure, Scene): + raise TypeError('figure must be a mayavi scene') diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 4079baff5cb..9b8d04c40c6 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -127,11 +127,15 @@ def __init__(self, fig=None, size=(600, 600), bgcolor=(0., 0., 0.), self.figure.plotter_class = Plotter self.figure.store['off_screen'] = True - self.plotter = self.figure.build() - self.plotter.hide_axes() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=FutureWarning) + self.plotter = self.figure.build() + self.plotter.hide_axes() def subplot(self, x, y): - self.plotter.subplot(x, y) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=FutureWarning) + self.plotter.subplot(x, y) def scene(self): return self.figure diff --git a/mne/viz/backends/base_renderer.py b/mne/viz/backends/base_renderer.py index 90539ff90f2..90435450f94 100644 --- a/mne/viz/backends/base_renderer.py +++ b/mne/viz/backends/base_renderer.py @@ -17,6 +17,11 @@ def __init__(self, fig=None, size=(600, 600), bgcolor=(0., 0., 0.), """Set up the scene.""" pass + @abstractclassmethod + def subplot(self, x, y): + """Set the active subplot.""" + pass + @abstractclassmethod def scene(self): """Return scene handle.""" diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index b940810fbe6..6e42588c4ca 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -31,7 +31,7 @@ plot_sensors_connectivity, plot_brain_colorbar) from mne.viz.utils import _fake_click from mne.utils import (requires_mayavi, requires_pysurfer, run_tests_if_main, - _import_mlab, requires_nibabel, check_version, + requires_nibabel, check_version, traits_test, requires_version, catch_logging) from mne.datasets import testing from mne.source_space import read_source_spaces @@ -335,7 +335,7 @@ def test_plot_alignment(tmpdir, renderer): @requires_pysurfer @requires_mayavi @traits_test -def test_limits_to_control_points(): +def test_limits_to_control_points(renderer): """Test functionality for determining control points.""" sample_src = read_source_spaces(src_fname) kwargs = dict(subjects_dir=subjects_dir, smoothing_steps=1) @@ -348,14 +348,12 @@ def test_limits_to_control_points(): stc = SourceEstimate(stc_data, vertices, 1, 1, 'sample') # Test for simple use cases - mlab = _import_mlab() stc.plot(**kwargs) stc.plot(clim=dict(pos_lims=(10, 50, 90)), **kwargs) stc.plot(colormap='hot', clim='auto', **kwargs) stc.plot(colormap='mne', clim='auto', **kwargs) - figs = [mlab.figure(), mlab.figure()] stc.plot(clim=dict(kind='value', lims=(10, 50, 90)), figure=99, **kwargs) - pytest.raises(ValueError, stc.plot, clim='auto', figure=figs, **kwargs) + pytest.raises(TypeError, stc.plot, clim='auto', figure=[], **kwargs) # Test for correct clim values with pytest.raises(ValueError, match='monotonically'): @@ -378,7 +376,7 @@ def test_limits_to_control_points(): stc._data.fill(0.) with pytest.warns(RuntimeWarning, match='All data were zero'): plot_source_estimates(stc, **kwargs) - mlab.close(all=True) + renderer._close_all() @testing.requires_testing_data From 8cb29a23ec3bb2359d9f5dc65839b3436a597ba2 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 20 Sep 2019 11:11:12 +0200 Subject: [PATCH 53/88] TST: first try build doc with _Brain (default: pyvista) --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index ac1adb010ca..7b6dbd0d6c5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -16,6 +16,7 @@ jobs: echo "export OPENBLAS_NUM_THREADS=4" >> $BASH_ENV export MNE_ROOT=${PWD}/minimal_cmds echo "export MNE_ROOT=${PWD}/minimal_cmds" >> $BASH_ENV + echo "export MNE_3D_BACKEND=pyvista" >> $BASH_ENV echo "export PATH=~/.local/bin/:${MNE_ROOT}/bin:$PATH" >> $BASH_ENV curl https://staff.washington.edu/larsoner/minimal_cmds.tar.gz | tar xz echo "export LD_LIBRARY_PATH=${MNE_ROOT}/lib:$LD_LIBRARY_PATH" >> $BASH_ENV From 29bd17ed06ba21f93e14d5d7290f6399152c522d Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 20 Sep 2019 11:14:31 +0200 Subject: [PATCH 54/88] TST: Trigger circleci [circle full] From 0440a358b6ef8fb2a5a2f58d50b1cfcceed80bbf Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 20 Sep 2019 13:36:25 +0200 Subject: [PATCH 55/88] Fix _Brain screenshot() in sphinx-gallery --- mne/viz/_brain/_brain.py | 16 +++++++++------- mne/viz/_brain/tests/test_brain.py | 9 +++------ mne/viz/backends/_pyvista.py | 11 +++++------ 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 79f7d1e99cc..6382ad4ed18 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -220,6 +220,8 @@ def __init__(self, subject_id, hemi, surf, title=None, z=self.geo[h].coords[:, 2], triangles=self.geo[h].faces, color=self.geo[h].grey_curv) + # Force rendering + self._renderer.show() def add_data(self, array, fmin=None, fmid=None, fmax=None, thresh=None, center=None, transparent=False, colormap="auto", @@ -703,13 +705,6 @@ def close(self): """Close all figures and cleanup data structure.""" self._renderer.close() - def show(self): - u"""Display widget.""" - try: - return self._renderer.show() - except RuntimeError: - logger.info("No active/running renderer available.") - def show_view(self, view=None, roll=None, distance=None): """Orient camera to display view.""" pass @@ -772,6 +767,13 @@ def views(self): def hemis(self): return self._hemis + def _show(self): + """Request rendering of the window.""" + try: + return self._renderer.show() + except RuntimeError: + logger.info("No active/running renderer available.") + def _check_hemi(self, hemi): u"""Check for safe single-hemi input, returns str.""" if hemi is None: diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 24bcbd95ce4..6882d5b5de8 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -37,8 +37,7 @@ def test_brain_init(renderer): with pytest.raises(KeyError): _Brain(subject_id=subject_id, hemi="foo", surf=surf) - brain = _Brain(subject_id, hemi, surf, subjects_dir=subjects_dir) - brain.show() + _Brain(subject_id, hemi, surf, subjects_dir=subjects_dir) @testing.requires_testing_data @@ -46,9 +45,8 @@ def test_brain_screenshot(renderer): """Test screenshot of a _Brain instance.""" brain = _Brain(subject_id, hemi='both', size=600, surf=surf, subjects_dir=subjects_dir) - img = brain.screenshot(mode='rgba') - brain.show() - assert(img.shape == (600, 600, 4)) + img = brain.screenshot(mode='rgb') + assert(img.shape == (600, 600, 3)) @testing.requires_testing_data @@ -74,7 +72,6 @@ def test_brain_add_data(renderer): brain_data.add_data(hemi_data, fmin=fmin, hemi=hemi, fmax=fmax, colormap='hot', vertices=hemi_vertices, colorbar=False) - brain_data.show() def test_brain_colormap(): diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 9b8d04c40c6..d81860b8aa9 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -57,8 +57,6 @@ def build(self): if self.plotter_class == Plotter: self.store.pop('title', None) - elif self.plotter_class == BackgroundPlotter: - self.store.pop('off_screen', None) if self.plotter is None: plotter = self.plotter_class(**self.store) @@ -120,10 +118,11 @@ def __init__(self, fig=None, size=(600, 600), bgcolor=(0., 0., 0.), else: self.figure = fig - if MNE_3D_BACKEND_TEST_DATA: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - from pyvista import Plotter + # Enable off_screen if sphinx-gallery or testing + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=FutureWarning) + from pyvista import Plotter, OFF_SCREEN + if OFF_SCREEN or MNE_3D_BACKEND_TEST_DATA: self.figure.plotter_class = Plotter self.figure.store['off_screen'] = True From 24858d7d30fdb43e4c257fdf04538c3c9cd193ec Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 23 Sep 2019 14:32:53 +0200 Subject: [PATCH 56/88] Refactor colormap management --- mne/viz/_brain/_brain.py | 7 +- mne/viz/_brain/colormap.py | 157 ++++++++++++++++++++++++------------- 2 files changed, 108 insertions(+), 56 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 6382ad4ed18..c3bdbcc25cd 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -11,7 +11,7 @@ import os from os.path import join as pjoin from ...label import read_label -from .colormap import _calculate_lut +from .colormap import calculate_lut from .view import views_dict from .surface import Surface from .utils import mesh_edges, smoothing_matrix @@ -423,6 +423,7 @@ def time_label(x): dt_max = fmax dt_min = fmin if center is None else -1 * fmax + ctable = self.update_lut() for ri, v in enumerate(self._views): @@ -745,8 +746,8 @@ def update_lut(self, fmin=None, fmid=None, fmax=None): fmax = self._data['fmax'] if fmax is None else fmax self._data['ctable'] = \ - _calculate_lut(colormap, alpha=alpha, fmin=fmin, fmid=fmid, - fmax=fmax, center=center) + calculate_lut(colormap, alpha=alpha, fmin=fmin, fmid=fmid, + fmax=fmax, center=center) return self._data['ctable'] diff --git a/mne/viz/_brain/colormap.py b/mne/viz/_brain/colormap.py index 19fe068d40b..89b71f3a6bc 100644 --- a/mne/viz/_brain/colormap.py +++ b/mne/viz/_brain/colormap.py @@ -8,7 +8,57 @@ import numpy as np -def _calculate_lut(lim_cmap, alpha, fmin, fmid, fmax, center=None): +def create_lut(cmap, n_colors=256, center=None): + from matplotlib import cm + cmap = cm.get_cmap(cmap) + lut = (cmap(np.linspace(0, 1, n_colors)) * 255.0).astype(np.int) + return lut + + +def scale_sequential_lut(lut_table, fmin, fmid, fmax): + """Scale a sequential colormap.""" + + lut_table_new = lut_table.copy() + n_colors = lut_table.shape[0] + n_colors2 = n_colors // 2 + + fmid_idx = int(np.round(n_colors * ((fmid - fmin) / + (fmax - fmin))) - 1) + + for i in range(4): + part1 = np.interp(np.linspace(0, n_colors2 - 1, fmid_idx + 1), + np.arange(n_colors), + lut_table[:, i]) + lut_table_new[:fmid_idx + 1, i] = part1 + part2 = np.interp(np.linspace(n_colors2, n_colors - 1, + n_colors - fmid_idx - 1), + np.arange(n_colors), + lut_table[:, i]) + lut_table_new[fmid_idx + 1:, i] = part2 + + return lut_table_new + + +def get_fill_colors(cols, n_fill): + steps = np.linalg.norm(np.diff(cols[:, :3].astype(float), axis=0), + axis=1) + + ind = np.flatnonzero(steps[1:-1] > steps[[0, -1]].mean() * 3) + if ind.size > 0: + # choose the two colors between which there is the large step + ind = ind[0] + 1 + fillcols = np.r_[np.tile(cols[ind, :], (n_fill / 2, 1)), + np.tile(cols[ind + 1, :], + (n_fill - n_fill / 2, 1))] + else: + # choose a color from the middle of the colormap + fillcols = np.tile(cols[int(cols.shape[0] / 2), :], (n_fill, 1)) + + return fillcols + + +def calculate_lut(lut_table, alpha, fmin, fmid, fmax, center=None, + transparent=True): u"""Transparent color map calculation. A colormap may be sequential or divergent. When the colormap is @@ -45,62 +95,63 @@ def _calculate_lut(lim_cmap, alpha, fmin, fmid, fmax, center=None): center : float or None If not None, center of a divergent colormap, changes the meaning of fmin, fmax and fmid. + transparent : boolean + if True: use a linear transparency between fmin and fmid and make + values below fmin fully transparent (symmetrically for divergent + colormaps) Returns ------- cmap : matplotlib.ListedColormap Color map with transparency channel. """ - from matplotlib import cm - - if center is None: - # 'hot' or another linear color map - ctrl_pts = (fmin, fmid, fmax) - scale_pts = ctrl_pts - rgb_cmap = cm.get_cmap(lim_cmap) - # take 60% of hot color map, so it will be consistent - # with mayavi plots - cmap_size = int(rgb_cmap.N * 0.6) - cmap = rgb_cmap(np.arange(rgb_cmap.N))[rgb_cmap.N - cmap_size:, :] - alphas = np.ones(cmap_size) - step = 2 * (scale_pts[-1] - scale_pts[0]) / rgb_cmap.N - # coefficients for linear mapping - # from [ctrl_pts[0], ctrl_pts[1]) interval into [0, 1] - k = 1 / (ctrl_pts[1] - ctrl_pts[0]) - b = - ctrl_pts[0] * k - - for i in range(0, cmap_size): - curr_pos = i * step + scale_pts[0] - - if (curr_pos < ctrl_pts[0]): - alphas[i] = 0 - elif (curr_pos < ctrl_pts[1]): - alphas[i] = k * curr_pos + b + lut_table = create_lut(lut_table) + divergent = center is not None + n_colors = lut_table.shape[0] + + # Add transparency if needed + if transparent: + if divergent: + N4 = np.full(4, n_colors / 4, dtype=int) + N4[:np.mod(n_colors, 4)] += 1 + assert N4.sum() == n_colors + lut_table[:, -1] = np.r_[255 * np.ones(N4[0]), + np.linspace(255, 0, N4[2]), + np.linspace(0, 255, N4[3]), + 255 * np.ones(N4[1])] + else: + n_colors2 = int(n_colors / 2) + lut_table[:n_colors2, -1] = np.linspace(0, 255, n_colors2) + lut_table[n_colors2:, -1] = 255 * np.ones(n_colors - n_colors2) + + alpha = float(alpha) + if alpha < 1.0: + lut_table[:, -1] = lut_table[:, -1] * alpha + + if divergent: + n_colors2 = int(n_colors / 2) + n_fill = int(round(fmin * n_colors2 / (fmax - fmin))) * 2 + lut_table = np.r_[ + scale_sequential_lut(lut_table[:n_colors2, :], + center - fmax, center - fmid, + center - fmin), + get_fill_colors( + lut_table[n_colors2 - 3:n_colors2 + 3, :], n_fill), + scale_sequential_lut(lut_table[n_colors2:, :], + center + fmin, center + fmid, + center + fmax)] else: - # 'mne' or another divergent color map - ctrl_pts = (center + fmin, center + fmid, center + fmax) - scale_pts = (center - fmax, center, center + fmax) - rgb_cmap = lim_cmap - cmap = rgb_cmap(np.arange(rgb_cmap.N)) - alphas = np.ones(rgb_cmap.N) - step = (scale_pts[-1] - scale_pts[0]) / rgb_cmap.N - # coefficients for linear mapping into [0, 1] - k_pos = 1 / (ctrl_pts[1] - ctrl_pts[0]) - k_neg = -k_pos - b = - ctrl_pts[0] * k_pos - - for i in range(0, rgb_cmap.N): - curr_pos = i * step + scale_pts[0] - - if -ctrl_pts[0] < curr_pos < ctrl_pts[0]: - alphas[i] = 0 - elif ctrl_pts[0] <= curr_pos < ctrl_pts[1]: - alphas[i] = k_pos * curr_pos + b - elif -ctrl_pts[1] < curr_pos <= -ctrl_pts[0]: - alphas[i] = k_neg * curr_pos + b - - alphas *= alpha - np.clip(alphas, 0, 1) - cmap[:, -1] = alphas - - return cmap + lut_table = scale_sequential_lut(lut_table, fmin, fmid, fmax) + + n_colors = lut_table.shape[0] + if n_colors != 256: + lut = np.zeros((256, 4)) + x = np.linspace(1, n_colors, 256) + for chan in range(4): + lut[:, chan] = np.interp(x, + np.arange(1, n_colors + 1), + lut_table[:, chan]) + lut_table = lut + + lut_table = lut_table.astype(np.float) / 255.0 + return lut_table From 777165bdbdfab4daa7bbe530bdccdb30ad782d1b Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 23 Sep 2019 14:48:30 +0200 Subject: [PATCH 57/88] Fix minor bugs --- mne/viz/_brain/colormap.py | 3 ++- mne/viz/_brain/tests/test_brain.py | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/mne/viz/_brain/colormap.py b/mne/viz/_brain/colormap.py index 89b71f3a6bc..e68b683fff2 100644 --- a/mne/viz/_brain/colormap.py +++ b/mne/viz/_brain/colormap.py @@ -9,6 +9,7 @@ def create_lut(cmap, n_colors=256, center=None): + """Return a colormap suitable for setting as a LUT.""" from matplotlib import cm cmap = cm.get_cmap(cmap) lut = (cmap(np.linspace(0, 1, n_colors)) * 255.0).astype(np.int) @@ -17,7 +18,6 @@ def create_lut(cmap, n_colors=256, center=None): def scale_sequential_lut(lut_table, fmin, fmid, fmax): """Scale a sequential colormap.""" - lut_table_new = lut_table.copy() n_colors = lut_table.shape[0] n_colors2 = n_colors // 2 @@ -40,6 +40,7 @@ def scale_sequential_lut(lut_table, fmin, fmid, fmax): def get_fill_colors(cols, n_fill): + """Get the fill colors for the middle of divergent colormaps.""" steps = np.linalg.norm(np.diff(cols[:, :3].astype(float), axis=0), axis=1) diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 6882d5b5de8..2c09d0fb31c 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -14,7 +14,7 @@ from mne import read_source_estimate from mne.datasets import testing from mne.viz._brain import _Brain -from mne.viz._brain.colormap import _calculate_lut +from mne.viz._brain.colormap import calculate_lut from matplotlib import cm @@ -82,9 +82,9 @@ def test_brain_colormap(): fmid = 0.5 fmax = 1.0 center = None - _calculate_lut(colormap, alpha=alpha, fmin=fmin, - fmid=fmid, fmax=fmax, center=center) + calculate_lut(colormap, alpha=alpha, fmin=fmin, + fmid=fmid, fmax=fmax, center=center) center = 0.0 colormap = cm.get_cmap(colormap) - _calculate_lut(colormap, alpha=alpha, fmin=fmin, - fmid=fmid, fmax=fmax, center=center) + calculate_lut(colormap, alpha=alpha, fmin=fmin, + fmid=fmid, fmax=fmax, center=center) From 0ea585dca19017f7bea4ccf8e0758885a7854baf Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 23 Sep 2019 14:58:21 +0200 Subject: [PATCH 58/88] Switch to tagged version 0.22.2 of PyVista --- environment.yml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index 27d62344d10..2f188f0bbdb 100644 --- a/environment.yml +++ b/environment.yml @@ -34,7 +34,7 @@ dependencies: - pip: - mne - vtk - - https://github.com/pyvista/pyvista/zipball/master + - pyvista>=0.22.2 - mayavi - PySurfer[save_movie] - dipy --only-binary dipy diff --git a/requirements.txt b/requirements.txt index 42f3b7f1cf2..007cd0c26f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,5 +29,5 @@ neo xlrd pydocstyle flake8 -https://github.com/pyvista/pyvista/zipball/master +pyvista>=0.22.2 panel From f7efe90779d83f396f0b49f26ce1d9d8f2561fe3 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 23 Sep 2019 15:45:27 +0200 Subject: [PATCH 59/88] Add support for transparent in add_data() --- mne/viz/_brain/_brain.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index c3bdbcc25cd..11706aa6693 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -424,7 +424,7 @@ def time_label(x): dt_max = fmax dt_min = fmin if center is None else -1 * fmax - ctable = self.update_lut() + ctable = self.update_lut(transparent=transparent) for ri, v in enumerate(self._views): if self._hemi != 'split': @@ -725,7 +725,7 @@ def screenshot(self, mode='rgb'): """ return self._renderer.screenshot(mode) - def update_lut(self, fmin=None, fmid=None, fmax=None): + def update_lut(self, fmin=None, fmid=None, fmax=None, transparent=True): u"""Update color map. Parameters @@ -747,7 +747,7 @@ def update_lut(self, fmin=None, fmid=None, fmax=None): self._data['ctable'] = \ calculate_lut(colormap, alpha=alpha, fmin=fmin, fmid=fmid, - fmax=fmax, center=center) + fmax=fmax, center=center, transparent=transparent) return self._data['ctable'] From 645d4658d4d800ea491955c056a50b201123a6cf Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 23 Sep 2019 16:48:05 +0200 Subject: [PATCH 60/88] Add subplot system to Mayavi backend --- mne/viz/backends/_pysurfer_mayavi.py | 75 ++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/mne/viz/backends/_pysurfer_mayavi.py b/mne/viz/backends/_pysurfer_mayavi.py index 7faf7a7c325..1875c9a879e 100644 --- a/mne/viz/backends/_pysurfer_mayavi.py +++ b/mne/viz/backends/_pysurfer_mayavi.py @@ -17,12 +17,66 @@ import warnings import numpy as np +from mayavi.tools.mlab_scene_model import MlabSceneModel +from mayavi.core.ui.api import SceneEditor +from mayavi.core.ui.mayavi_scene import MayaviScene +from traits.api import (HasTraits, Instance) +from traitsui.api import View, Item, VGroup, HGroup from .base_renderer import _BaseRenderer from ...surface import _normalize_vectors from ...utils import (_import_mlab, _validate_type, SilenceStdout, copy_base_doc_to_subclass_doc) +class _MlabGenerator(HasTraits): + """TraitsUI mlab figure generator""" + view = Instance(View) + + def __init__(self, n_row, n_col, width, height, bgcolor, title, **traits): + HasTraits.__init__(self, **traits) + self.mlab_names = [] + self.n_row = n_row + self.n_col = n_col + self.width = width + self.height = height + self.bgcolor = bgcolor + for fi in range(n_row * n_col): + name = 'mlab_view%03g' % fi + self.mlab_names.append(name) + self.add_trait(name, Instance(MlabSceneModel, ())) + self.view = self._get_gen_view() + self._v = self.edit_traits(view=self.view) + self._v.title = title + + def _get_figs_view(self): + figures = [] + ind = 0 + for ri in range(self.n_row): + rfigs = [] + for ci in range(self.n_col): + x = getattr(self, self.mlab_names[ind]) + x.scene.background = self.bgcolor + rfigs.append(x.mayavi_scene) + ind += 1 + figures.append(rfigs) + return figures, self._v + + def _get_gen_view(self): + ind = 0 + va = [] + for ri in range(self.n_row): + ha = [] + for ci in range(self.n_col): + ha += [Item(name=self.mlab_names[ind], style='custom', + resizable=True, show_label=False, + editor=SceneEditor(scene_class=MayaviScene))] + ind += 1 + va += [HGroup(*ha)] + view = View(VGroup(*va), resizable=True, + height=self.height, width=self.width) + return view + + class _Projection(object): """Class storing projection information. @@ -61,17 +115,30 @@ def __init__(self, fig=None, size=(600, 600), bgcolor=(0., 0., 0.), name=None, show=False, shape=(1, 1)): self.mlab = _import_mlab() self.window_size = size + self.shape = shape if fig is None: - self.fig = _mlab_figure(figure=name, bgcolor=bgcolor, size=size) + if shape[0] == 1 and shape[1] == 1: + self.fig = _mlab_figure(figure=name, bgcolor=bgcolor, + size=size) + _toggle_mlab_render(self.fig, show) + else: + self.window = _MlabGenerator(shape[0], shape[1], + size[0], size[1], + bgcolor=(0., 0., 0.), + title='TEST') + self.figs, _v = self.window._get_figs_view() + for ri in range(shape[0]): + for ci in range(shape[1]): + _toggle_mlab_render(self.figs[ri][ci], show) elif isinstance(fig, int): self.fig = _mlab_figure(figure=fig, bgcolor=bgcolor, size=size) + _toggle_mlab_render(self.fig, show) else: self.fig = fig - if show is False: - _toggle_mlab_render(self.fig, False) + _toggle_mlab_render(self.fig, show) def subplot(self, x, y): - pass + self.fig = self.figs[x][y] def scene(self): return self.fig From 649aa0e02b7f2f71505ded05ad2752626a581711 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 24 Sep 2019 14:28:03 +0200 Subject: [PATCH 61/88] Use PySurfer from now on for the mayavi backend --- mne/viz/_3d.py | 33 +++++++++---- mne/viz/backends/_pysurfer_mayavi.py | 73 ++-------------------------- mne/viz/tests/test_3d.py | 2 +- 3 files changed, 28 insertions(+), 80 deletions(-) diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index bdaf3c52b42..dad80dda4c8 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -1639,6 +1639,7 @@ def plot_source_estimates(stc, subject=None, surface='inflated', hemi='lh', An instance of :class:`surfer.Brain` from PySurfer or matplotlib figure. """ # noqa: E501 + from .backends.renderer import get_3d_backend # import here to avoid circular import problem from ..source_estimate import SourceEstimate _validate_type(stc, SourceEstimate, "stc", "Surface Source Estimate") @@ -1666,8 +1667,10 @@ def plot_source_estimates(stc, subject=None, surface='inflated', hemi='lh', time_unit=time_unit, background=background, spacing=spacing, time_viewer=time_viewer, colorbar=colorbar, transparent=transparent) - from ._brain import _Brain as Brain - from surfer import TimeViewer + if get_3d_backend() == "mayavi": + from surfer import Brain, TimeViewer + else: + from ._brain import _Brain as Brain _check_option('hemi', hemi, ['lh', 'rh', 'split', 'both']) time_label, times = _handle_time(time_label, time_unit, stc.times) @@ -1694,14 +1697,26 @@ def plot_source_estimates(stc, subject=None, surface='inflated', hemi='lh', data = getattr(stc, hemi + '_data') vertices = stc.vertices[hemi_idx] if len(data) > 0: + kwargs = { + "array": data, "colormap": colormap, + "vertices": vertices, + "smoothing_steps": smoothing_steps, + "time": times, "time_label": time_label, + "alpha": alpha, "hemi": hemi, + "colorbar": colorbar, "initial_time": initial_time, + "transparent": transparent, "center": center, + "verbose": False + } + if get_3d_backend() == "mayavi": + kwargs["min"] = scale_pts[0] + kwargs["mid"] = scale_pts[1] + kwargs["max"] = scale_pts[2] + else: + kwargs["fmin"] = scale_pts[0] + kwargs["fmid"] = scale_pts[1] + kwargs["fmax"] = scale_pts[2] with warnings.catch_warnings(record=True): # traits warnings - brain.add_data(data, colormap=colormap, vertices=vertices, - smoothing_steps=smoothing_steps, time=times, - time_label=time_label, alpha=alpha, hemi=hemi, - colorbar=colorbar, initial_time=initial_time, - fmin=scale_pts[0], fmid=scale_pts[1], - fmax=scale_pts[2], transparent=transparent, - center=center, verbose=False) + brain.add_data(**kwargs) if time_viewer: TimeViewer(brain) return brain diff --git a/mne/viz/backends/_pysurfer_mayavi.py b/mne/viz/backends/_pysurfer_mayavi.py index 1875c9a879e..0a7b9d5d848 100644 --- a/mne/viz/backends/_pysurfer_mayavi.py +++ b/mne/viz/backends/_pysurfer_mayavi.py @@ -17,66 +17,12 @@ import warnings import numpy as np -from mayavi.tools.mlab_scene_model import MlabSceneModel -from mayavi.core.ui.api import SceneEditor -from mayavi.core.ui.mayavi_scene import MayaviScene -from traits.api import (HasTraits, Instance) -from traitsui.api import View, Item, VGroup, HGroup from .base_renderer import _BaseRenderer from ...surface import _normalize_vectors from ...utils import (_import_mlab, _validate_type, SilenceStdout, copy_base_doc_to_subclass_doc) -class _MlabGenerator(HasTraits): - """TraitsUI mlab figure generator""" - view = Instance(View) - - def __init__(self, n_row, n_col, width, height, bgcolor, title, **traits): - HasTraits.__init__(self, **traits) - self.mlab_names = [] - self.n_row = n_row - self.n_col = n_col - self.width = width - self.height = height - self.bgcolor = bgcolor - for fi in range(n_row * n_col): - name = 'mlab_view%03g' % fi - self.mlab_names.append(name) - self.add_trait(name, Instance(MlabSceneModel, ())) - self.view = self._get_gen_view() - self._v = self.edit_traits(view=self.view) - self._v.title = title - - def _get_figs_view(self): - figures = [] - ind = 0 - for ri in range(self.n_row): - rfigs = [] - for ci in range(self.n_col): - x = getattr(self, self.mlab_names[ind]) - x.scene.background = self.bgcolor - rfigs.append(x.mayavi_scene) - ind += 1 - figures.append(rfigs) - return figures, self._v - - def _get_gen_view(self): - ind = 0 - va = [] - for ri in range(self.n_row): - ha = [] - for ci in range(self.n_col): - ha += [Item(name=self.mlab_names[ind], style='custom', - resizable=True, show_label=False, - editor=SceneEditor(scene_class=MayaviScene))] - ind += 1 - va += [HGroup(*ha)] - view = View(VGroup(*va), resizable=True, - height=self.height, width=self.width) - return view - - class _Projection(object): """Class storing projection information. @@ -117,28 +63,15 @@ def __init__(self, fig=None, size=(600, 600), bgcolor=(0., 0., 0.), self.window_size = size self.shape = shape if fig is None: - if shape[0] == 1 and shape[1] == 1: - self.fig = _mlab_figure(figure=name, bgcolor=bgcolor, - size=size) - _toggle_mlab_render(self.fig, show) - else: - self.window = _MlabGenerator(shape[0], shape[1], - size[0], size[1], - bgcolor=(0., 0., 0.), - title='TEST') - self.figs, _v = self.window._get_figs_view() - for ri in range(shape[0]): - for ci in range(shape[1]): - _toggle_mlab_render(self.figs[ri][ci], show) + self.fig = _mlab_figure(figure=name, bgcolor=bgcolor, size=size) elif isinstance(fig, int): self.fig = _mlab_figure(figure=fig, bgcolor=bgcolor, size=size) - _toggle_mlab_render(self.fig, show) else: self.fig = fig - _toggle_mlab_render(self.fig, show) + _toggle_mlab_render(self.fig, show) def subplot(self, x, y): - self.fig = self.figs[x][y] + pass def scene(self): return self.fig diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index 6e42588c4ca..fb573fc7b0f 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -353,7 +353,7 @@ def test_limits_to_control_points(renderer): stc.plot(colormap='hot', clim='auto', **kwargs) stc.plot(colormap='mne', clim='auto', **kwargs) stc.plot(clim=dict(kind='value', lims=(10, 50, 90)), figure=99, **kwargs) - pytest.raises(TypeError, stc.plot, clim='auto', figure=[], **kwargs) + pytest.raises(TypeError, stc.plot, clim='auto', figure=[0], **kwargs) # Test for correct clim values with pytest.raises(ValueError, match='monotonically'): From e1ce006ca314c84b6250a2510cb7f3a5125da829 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 24 Sep 2019 16:50:07 +0200 Subject: [PATCH 62/88] Improve camera settings --- mne/viz/_brain/_brain.py | 17 ++++++++++++----- mne/viz/_brain/view.py | 16 ++++++++-------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 11706aa6693..cde944a0ecd 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -211,15 +211,13 @@ def __init__(self, subject_id, hemi, surf, title=None, if not (hemi in ['lh', 'rh'] and h != hemi): ci = hi if hemi == 'split' else 0 self._renderer.subplot(ri, ci) - self._renderer.set_camera(azimuth=views_dict[v].azim, - elevation=views_dict[v].elev, - distance=490.0) - self._renderer.mesh(x=self.geo[h].coords[:, 0], y=self.geo[h].coords[:, 1], z=self.geo[h].coords[:, 2], triangles=self.geo[h].faces, color=self.geo[h].grey_curv) + self._renderer.set_camera(azimuth=views_dict[v].azim, + elevation=views_dict[v].elev) # Force rendering self._renderer.show() @@ -447,6 +445,8 @@ def time_label(x): text=time_label(time[time_idx])) self._renderer.scalarbar(source=mesh, n_labels=8, bgcolor=(0.5, 0.5, 0.5)) + self._renderer.set_camera(azimuth=views_dict[v].azim, + elevation=views_dict[v].elev) def add_label(self, label, color=None, alpha=1, scalar_thresh=None, borders=False, hemi=None, subdir=None): @@ -561,6 +561,8 @@ def add_label(self, label, color=None, alpha=1, scalar_thresh=None, color=None, colormap=ctable, backface_culling=False) + self._renderer.set_camera(azimuth=0., + elevation=90.) def add_foci(self, coords, coords_as_verts=False, map_surface=None, scale_factor=1, color="white", alpha=1, name=None, @@ -620,6 +622,8 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, self._renderer.sphere(center=coords, color=color, scale=(10. * scale_factor), opacity=alpha) + self._renderer.set_camera(azimuth=views_dict[v].azim, + elevation=views_dict[v].elev) def add_text(self, x, y, text, name, color=None, opacity=1.0, row=-1, col=-1, font_size=None, justification=None): @@ -708,7 +712,10 @@ def close(self): def show_view(self, view=None, roll=None, distance=None): """Orient camera to display view.""" - pass + if isinstance(view, str): + view = views_dict.get(view) + self._renderer.set_camera(azimuth=view.azim, + elevation=view.elev) def screenshot(self, mode='rgb'): """Generate a screenshot of current view. diff --git a/mne/viz/_brain/view.py b/mne/viz/_brain/view.py index 9fefe63a51e..5043d1327e8 100644 --- a/mne/viz/_brain/view.py +++ b/mne/viz/_brain/view.py @@ -11,14 +11,14 @@ View = namedtuple('View', 'elev azim') -views_dict = {'lateral': View(elev=5, azim=0), - 'medial': View(elev=5, azim=180), - 'rostral': View(elev=5, azim=90), - 'caudal': View(elev=5, azim=-90), - 'dorsal': View(elev=90, azim=0), - 'ventral': View(elev=-90, azim=0), - 'frontal': View(elev=5, azim=110), - 'parietal': View(elev=5, azim=-110)} +views_dict = {'lateral': View(azim=180., elev=90.), + 'medial': View(azim=0., elev=90.0), + 'rostral': View(azim=90., elev=90.), + 'caudal': View(azim=270., elev=90.), + 'dorsal': View(azim=180., elev=0.), + 'ventral': View(azim=180., elev=180.), + 'frontal': View(azim=120., elev=80.), + 'parietal': View(azim=-120., elev=60.)} # add short-size version entries into the dict _views_dict = dict() for k, v in views_dict.items(): From 20a40eafac36fa7d7c4eb7d64f898c88b07ada84 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 24 Sep 2019 17:11:30 +0200 Subject: [PATCH 63/88] Add support for view + hemi=split --- mne/viz/_brain/_brain.py | 6 +++++- mne/viz/_brain/view.py | 37 +++++++++++++++++++++++++------------ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index cde944a0ecd..a497aca20c8 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -12,7 +12,7 @@ from os.path import join as pjoin from ...label import read_label from .colormap import calculate_lut -from .view import views_dict +from .view import lh_views_dict, rh_views_dict from .surface import Surface from .utils import mesh_edges, smoothing_matrix from ..utils import _check_option, logger @@ -208,6 +208,7 @@ def __init__(self, subject_id, hemi, surf, title=None, for ri, v in enumerate(views): for hi, h in enumerate(['lh', 'rh']): + views_dict = lh_views_dict if hemi == 'lh' else rh_views_dict if not (hemi in ['lh', 'rh'] and h != hemi): ci = hi if hemi == 'split' else 0 self._renderer.subplot(ri, ci) @@ -425,6 +426,7 @@ def time_label(x): ctable = self.update_lut(transparent=transparent) for ri, v in enumerate(self._views): + views_dict = lh_views_dict if hemi == 'lh' else rh_views_dict if self._hemi != 'split': ci = 0 else: @@ -614,6 +616,7 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, if self._units == 'm': scale_factor = scale_factor / 1000. for ri, v in enumerate(self._views): + views_dict = lh_views_dict if hemi == 'lh' else rh_views_dict if self._hemi != 'split': ci = 0 else: @@ -712,6 +715,7 @@ def close(self): def show_view(self, view=None, roll=None, distance=None): """Orient camera to display view.""" + views_dict = lh_views_dict if self._hemi == 'lh' else rh_views_dict if isinstance(view, str): view = views_dict.get(view) self._renderer.set_camera(azimuth=view.azim, diff --git a/mne/viz/_brain/view.py b/mne/viz/_brain/view.py index 5043d1327e8..1be4e9ed76b 100644 --- a/mne/viz/_brain/view.py +++ b/mne/viz/_brain/view.py @@ -11,16 +11,29 @@ View = namedtuple('View', 'elev azim') -views_dict = {'lateral': View(azim=180., elev=90.), - 'medial': View(azim=0., elev=90.0), - 'rostral': View(azim=90., elev=90.), - 'caudal': View(azim=270., elev=90.), - 'dorsal': View(azim=180., elev=0.), - 'ventral': View(azim=180., elev=180.), - 'frontal': View(azim=120., elev=80.), - 'parietal': View(azim=-120., elev=60.)} +lh_views_dict = {'lateral': View(azim=180., elev=90.), + 'medial': View(azim=0., elev=90.0), + 'rostral': View(azim=90., elev=90.), + 'caudal': View(azim=270., elev=90.), + 'dorsal': View(azim=180., elev=0.), + 'ventral': View(azim=180., elev=180.), + 'frontal': View(azim=120., elev=80.), + 'parietal': View(azim=-120., elev=60.)} +rh_views_dict = {'lateral': View(azim=180., elev=-90.), + 'medial': View(azim=0., elev=-90.0), + 'rostral': View(azim=-90., elev=-90.), + 'caudal': View(azim=90., elev=-90.), + 'dorsal': View(azim=180., elev=0.), + 'ventral': View(azim=180., elev=180.), + 'frontal': View(azim=60., elev=80.), + 'parietal': View(azim=-60., elev=60.)} # add short-size version entries into the dict -_views_dict = dict() -for k, v in views_dict.items(): - _views_dict[k[:3]] = v -views_dict.update(_views_dict) +_lh_views_dict = dict() +for k, v in lh_views_dict.items(): + _lh_views_dict[k[:3]] = v +lh_views_dict.update(_lh_views_dict) + +_rh_views_dict = dict() +for k, v in rh_views_dict.items(): + _rh_views_dict[k[:3]] = v +rh_views_dict.update(_rh_views_dict) From 9c0aab821751b704fe7cef3fe540b20da8359d31 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 25 Sep 2019 14:02:13 +0200 Subject: [PATCH 64/88] Add add_text() function --- mne/viz/_brain/_brain.py | 6 ++++-- mne/viz/backends/_pysurfer_mayavi.py | 6 ++++-- mne/viz/backends/_pyvista.py | 15 +++++++++++++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index a497aca20c8..66d2fdca0a0 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -444,7 +444,8 @@ def time_label(x): if array.ndim >= 2 and callable(time_label): self._renderer.text2d(x=0.95, y=y_txt, size=time_label_size, - text=time_label(time[time_idx])) + text=time_label(time[time_idx]), + justification='right') self._renderer.scalarbar(source=mesh, n_labels=8, bgcolor=(0.5, 0.5, 0.5)) self._renderer.set_camera(azimuth=views_dict[v].azim, @@ -653,7 +654,8 @@ def add_text(self, x, y, text, name, color=None, opacity=1.0, col : int Column index of which brain to use """ - pass + self._renderer.text2d(x=x, y=y, text=text, color=color, + size=font_size, justification=justification) def remove_labels(self, labels=None, hemi=None): """Remove one or more previously added labels from the image. diff --git a/mne/viz/backends/_pysurfer_mayavi.py b/mne/viz/backends/_pysurfer_mayavi.py index 0a7b9d5d848..80ae6ad169f 100644 --- a/mne/viz/backends/_pysurfer_mayavi.py +++ b/mne/viz/backends/_pysurfer_mayavi.py @@ -212,13 +212,15 @@ def quiver3d(self, x, y, z, u, v, w, color, scale, mode, resolution=8, glyph_resolution quiv.actor.property.backface_culling = backface_culling - def text2d(self, x, y, text, size=14, color=(1.0, 1.0, 1.0)): + def text2d(self, x, y, text, size=14, color=(1.0, 1.0, 1.0), + justification=None): size = 14 if size is None else size with warnings.catch_warnings(record=True): # traits text = self.mlab.text(x, y, text, color=color, figure=self.fig) text.property.font_size = size text.actor.text_scale_mode = 'viewport' - text.property.justification = 'right' + if isinstance(justification, str): + text.property.justification = justification def text3d(self, x, y, z, text, scale, color=(1.0, 1.0, 1.0)): with warnings.catch_warnings(record=True): # traits diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index d81860b8aa9..825cfc61115 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -344,7 +344,8 @@ def quiver3d(self, x, y, z, u, v, w, color, scale, mode, resolution=8, smooth_shading=self.figure. smooth_shading) - def text2d(self, x, y, text, size=14, color=(1.0, 1.0, 1.0)): + def text2d(self, x, y, text, size=14, color=(1.0, 1.0, 1.0), + justification=None): size = 14 if size is None else size position = (x * self.figure.store['window_size'][0], y * self.figure.store['window_size'][1]) @@ -353,7 +354,17 @@ def text2d(self, x, y, text, size=14, color=(1.0, 1.0, 1.0)): actor = self.plotter.add_text(text, position=position, font_size=size, color=color) - actor.GetTextProperty().SetJustificationToRight() + if isinstance(justification, str): + if justification == 'left': + actor.GetTextProperty().SetJustificationToLeft() + elif justification == 'center': + actor.GetTextProperty().SetJustificationToCentered() + elif justification == 'right': + actor.GetTextProperty().SetJustificationToRight() + else: + raise ValueError('Expected values for `justification`' + 'are `left`, `center` or `right` but ' + 'got {} instead.'.format(justification)) def text3d(self, x, y, z, text, scale, color=(1.0, 1.0, 1.0)): with warnings.catch_warnings(): From f340d0894821413d8f19557c511d6781c9f243b3 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 25 Sep 2019 15:21:59 +0200 Subject: [PATCH 65/88] Add some tests --- mne/viz/_brain/_brain.py | 20 ++++++------- mne/viz/_brain/tests/test_brain.py | 37 +++++++++++++++++++++++-- mne/viz/backends/tests/test_renderer.py | 12 +++++++- 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 66d2fdca0a0..5676c8d2940 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -140,11 +140,19 @@ def __init__(self, subject_id, hemi, surf, title=None, foreground=None, figure=None, subjects_dir=None, views=['lateral'], offset=True, show_toolbar=False, offscreen=False, interaction=None, units='mm'): + from ..backends.renderer import _Renderer, _check_figure + from matplotlib.colors import colorConverter + if interaction is not None: raise ValueError('"interaction" parameter is not supported.') - from ..backends.renderer import _Renderer, _check_figure - from matplotlib.colors import colorConverter + if hemi in ('both', 'split'): + self._hemis = ('lh', 'rh') + elif hemi in ('lh', 'rh'): + self._hemis = (hemi, ) + else: + raise KeyError('hemi has to be either "lh", "rh", "split", ' + 'or "both"') if isinstance(background, str): background = colorConverter.to_rgb(background) @@ -181,14 +189,6 @@ def __init__(self, subject_id, hemi, surf, title=None, # load geometry for one or both hemispheres as necessary offset = None if (not offset or hemi != 'both') else 0.0 - if hemi in ('both', 'split'): - self._hemis = ('lh', 'rh') - elif hemi in ('lh', 'rh'): - self._hemis = (hemi, ) - else: - raise KeyError('hemi has to be either "lh", "rh", "split", ' - 'or "both"') - if figure is not None and not isinstance(figure, int): _check_figure(figure) self._renderer = _Renderer(size=fig_size, bgcolor=background, diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 2c09d0fb31c..fdebdd448e2 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -30,14 +30,17 @@ def test_brain_init(renderer): """Test initialization of the _Brain instance.""" hemi = 'both' + with pytest.raises(ValueError, match='size'): + _Brain(subject_id=subject_id, hemi=hemi, surf=surf, size=0.5) with pytest.raises(TypeError, match='figure'): _Brain(subject_id=subject_id, hemi=hemi, surf=surf, figure='foo') with pytest.raises(ValueError, match='interaction'): _Brain(subject_id=subject_id, hemi=hemi, surf=surf, interaction=0) with pytest.raises(KeyError): - _Brain(subject_id=subject_id, hemi="foo", surf=surf) + _Brain(subject_id=subject_id, hemi='foo', surf=surf) - _Brain(subject_id, hemi, surf, subjects_dir=subjects_dir) + _Brain(subject_id, hemi, surf, size=(300, 300), + subjects_dir=subjects_dir) @testing.requires_testing_data @@ -63,6 +66,16 @@ def test_brain_add_data(renderer): brain_data = _Brain(subject_id, hemi, surf, size=300, subjects_dir=subjects_dir) + with pytest.raises(ValueError, match='thresh'): + brain_data.add_data(hemi_data, thresh=-1) + with pytest.raises(ValueError, match='remove_existing'): + brain_data.add_data(hemi_data, remove_existing=-1) + with pytest.raises(ValueError, match='time_label_size'): + brain_data.add_data(hemi_data, time_label_size=-1) + with pytest.raises(ValueError, match='scale_factor'): + brain_data.add_data(hemi_data, scale_factor=-1) + with pytest.raises(ValueError, match='vector_alpha'): + brain_data.add_data(hemi_data, vector_alpha=-1) with pytest.raises(ValueError): brain_data.add_data(array=np.array([0, 1, 2])) with pytest.raises(ValueError): @@ -71,7 +84,25 @@ def test_brain_add_data(renderer): brain_data.add_data(hemi_data, fmin=fmin, hemi=hemi, fmax=fmax, colormap='hot', vertices=hemi_vertices, - colorbar=False) + colorbar=False, time=None) + + +@testing.requires_testing_data +def test_brain_add_label(renderer): + """Test adding data in _Brain instance.""" + pass + + +@testing.requires_testing_data +def test_brain_add_foci(renderer): + """Test adding foci in _Brain instance.""" + pass + + +@testing.requires_testing_data +def test_brain_add_text(renderer): + """Test adding text in _Brain instance.""" + pass def test_brain_colormap(): diff --git a/mne/viz/backends/tests/test_renderer.py b/mne/viz/backends/tests/test_renderer.py index 782cd0f9096..2d09db2b11e 100644 --- a/mne/viz/backends/tests/test_renderer.py +++ b/mne/viz/backends/tests/test_renderer.py @@ -44,6 +44,15 @@ def test_backend_environment_setup(backend, backend_mocker, monkeypatch): assert get_3d_backend() == backend +def test_3d_functions(renderer): + """Test figure management functions.""" + fig = renderer.create_3d_figure((300, 300)) + renderer._check_figure(fig) + renderer.set_3d_view(figure=fig) + renderer.set_3d_title(figure=fig, title='foo') + renderer._close_all() + + def test_3d_backend(renderer): """Test default plot.""" # set data @@ -131,7 +140,8 @@ def test_3d_backend(renderer): rend.scalarbar(source=tube, title="Scalar Bar") # use text - rend.text2d(x=txt_x, y=txt_y, text=txt_text, size=txt_size) + rend.text2d(x=txt_x, y=txt_y, text=txt_text, + size=txt_size, justification='right') rend.text3d(x=0, y=0, z=0, text=txt_text, scale=1.0) rend.set_camera(azimuth=180.0, elevation=90.0, distance=cam_distance, From dcfb3f4ad719ee4b6b484f7be3d807b90ac9317c Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 25 Sep 2019 16:07:52 +0200 Subject: [PATCH 66/88] Add more tests --- mne/viz/_brain/_brain.py | 5 ++++- mne/viz/_brain/tests/test_brain.py | 17 ++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 5676c8d2940..bcf26f9d280 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -629,7 +629,7 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, self._renderer.set_camera(azimuth=views_dict[v].azim, elevation=views_dict[v].elev) - def add_text(self, x, y, text, name, color=None, opacity=1.0, + def add_text(self, x, y, text, name=None, color=None, opacity=1.0, row=-1, col=-1, font_size=None, justification=None): """Add a text to the visualization. @@ -654,6 +654,9 @@ def add_text(self, x, y, text, name, color=None, opacity=1.0, col : int Column index of which brain to use """ + # those parameters are not supported yet, only None is allowed + _check_option('name', name, [None]) + self._renderer.text2d(x=x, y=y, text=text, color=color, size=font_size, justification=justification) diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index fdebdd448e2..e56724052ca 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -22,6 +22,7 @@ subject_id = 'sample' subjects_dir = path.join(data_path, 'subjects') fname_stc = path.join(data_path, 'MEG/sample/sample_audvis_trunc-meg') +fname_label = path.join(data_path, 'MEG/sample/labels/Vis-lh.label') surf = 'inflated' @@ -90,19 +91,29 @@ def test_brain_add_data(renderer): @testing.requires_testing_data def test_brain_add_label(renderer): """Test adding data in _Brain instance.""" - pass + from mne.label import read_label + brain = _Brain(subject_id, hemi='lh', size=500, + surf=surf, subjects_dir=subjects_dir) + label = read_label(fname_label) + brain.add_label(fname_label) + brain.add_label(label) @testing.requires_testing_data def test_brain_add_foci(renderer): """Test adding foci in _Brain instance.""" - pass + brain = _Brain(subject_id, hemi='lh', size=500, + surf=surf, subjects_dir=subjects_dir) + brain.add_foci([0], coords_as_verts=True, + hemi='lh', color='blue') @testing.requires_testing_data def test_brain_add_text(renderer): """Test adding text in _Brain instance.""" - pass + brain = _Brain(subject_id, hemi='lh', size=250, + surf=surf, subjects_dir=subjects_dir) + brain.add_text(x=0, y=0, text='foo') def test_brain_colormap(): From 93f5e7881394fdfe49bab798006d8d263f08a216 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 25 Sep 2019 16:57:28 +0200 Subject: [PATCH 67/88] TST: Build documentation [circle full] From 2deee165ad523ee9c840688ec8a5d4b69d1f9c43 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 25 Sep 2019 18:17:11 +0200 Subject: [PATCH 68/88] Check how CIs react to hotfix --- mne/viz/_brain/_brain.py | 10 +++++++--- mne/viz/backends/_pyvista.py | 9 +++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index bcf26f9d280..90baf3df463 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -12,7 +12,7 @@ from os.path import join as pjoin from ...label import read_label from .colormap import calculate_lut -from .view import lh_views_dict, rh_views_dict +from .view import lh_views_dict, rh_views_dict, View from .surface import Surface from .utils import mesh_edges, smoothing_matrix from ..utils import _check_option, logger @@ -654,8 +654,9 @@ def add_text(self, x, y, text, name=None, color=None, opacity=1.0, col : int Column index of which brain to use """ - # those parameters are not supported yet, only None is allowed - _check_option('name', name, [None]) + # XXX: support `name` should be added when update_text/remove_text + # are implemented + # _check_option('name', name, [None]) self._renderer.text2d(x=x, y=y, text=text, color=color, size=font_size, justification=justification) @@ -723,6 +724,9 @@ def show_view(self, view=None, roll=None, distance=None): views_dict = lh_views_dict if self._hemi == 'lh' else rh_views_dict if isinstance(view, str): view = views_dict.get(view) + elif isinstance(view, dict): + view = View(azim=view['azimuth'], + elev=view['elevation']) self._renderer.set_camera(azimuth=view.azim, elevation=view.elev) diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 825cfc61115..727a5965d38 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -100,6 +100,7 @@ class _Renderer(_BaseRenderer): def __init__(self, fig=None, size=(600, 600), bgcolor=(0., 0., 0.), name="PyVista Scene", show=False, shape=(1, 1)): + from pyvista import OFF_SCREEN from mne.viz.backends.renderer import MNE_3D_BACKEND_TEST_DATA if isinstance(fig, int): if _FIGURES.get(fig) is None: @@ -119,15 +120,15 @@ def __init__(self, fig=None, size=(600, 600), bgcolor=(0., 0., 0.), self.figure = fig # Enable off_screen if sphinx-gallery or testing - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - from pyvista import Plotter, OFF_SCREEN if OFF_SCREEN or MNE_3D_BACKEND_TEST_DATA: - self.figure.plotter_class = Plotter self.figure.store['off_screen'] = True with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) + if MNE_3D_BACKEND_TEST_DATA: + from pyvista import Plotter + self.figure.plotter_class = Plotter + self.plotter = self.figure.build() self.plotter.hide_axes() From cb380ab67bb3d3ac3441245cddaa68a035d215cd Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 26 Sep 2019 12:05:19 +0200 Subject: [PATCH 69/88] TST: Build doc [circle full] From 007d2e1f3f8e6b24938261b792aab4ce6133bb6c Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 27 Sep 2019 13:48:55 +0200 Subject: [PATCH 70/88] Add a save_image() function --- mne/viz/_brain/_brain.py | 14 ++++++++++++++ mne/viz/backends/_pysurfer_mayavi.py | 16 ++++++++++++++-- mne/viz/backends/_pyvista.py | 5 +++-- mne/viz/backends/base_renderer.py | 4 +++- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 90baf3df463..4adaeaa17f6 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -128,6 +128,8 @@ class _Brain(object): +---------------------------+--------------+-----------------------+ | remove_labels | ✓ | - | +---------------------------+--------------+-----------------------+ + | save_image | ✓ | ✓ | + +---------------------------+--------------+-----------------------+ | screenshot | ✓ | ✓ | +---------------------------+--------------+-----------------------+ | show_view | ✓ | - | @@ -730,6 +732,18 @@ def show_view(self, view=None, roll=None, distance=None): self._renderer.set_camera(azimuth=view.azim, elevation=view.elev) + def save_image(self, filename, mode='rgb'): + """Save view from all panels to disk. + + Parameters + ---------- + filename: string + path to new image file + mode : string + Either 'rgb' or 'rgba' for values to return. + """ + self._renderer.screenshot(mode=mode, filename=filename) + def screenshot(self, mode='rgb'): """Generate a screenshot of current view. diff --git a/mne/viz/backends/_pysurfer_mayavi.py b/mne/viz/backends/_pysurfer_mayavi.py index 80ae6ad169f..d50f01c0eb7 100644 --- a/mne/viz/backends/_pysurfer_mayavi.py +++ b/mne/viz/backends/_pysurfer_mayavi.py @@ -259,14 +259,17 @@ def set_camera(self, azimuth=None, elevation=None, distance=None, elevation=elevation, distance=distance, focalpoint=focalpoint) - def screenshot(self, mode='rgb'): + def screenshot(self, mode='rgb', filename=None): from mne.viz.backends.renderer import MNE_3D_BACKEND_TEST_DATA if MNE_3D_BACKEND_TEST_DATA: ndim = 3 if mode == 'rgb' else 4 return np.zeros(tuple(self.window_size) + (ndim,), np.uint8) else: with warnings.catch_warnings(record=True): # traits - return self.mlab.screenshot(self.fig, mode=mode) + img = self.mlab.screenshot(self.fig, mode=mode) + if isinstance(filename, str): + _save_figure(img, filename) + return img def project(self, xyz, ch_names): xy = _3d_to_2d(self.fig, xyz) @@ -411,3 +414,12 @@ def _check_figure(figure): from mayavi.core.scene import Scene if not isinstance(figure, Scene): raise TypeError('figure must be a mayavi scene') + + +def _save_figure(img, filename): + from matplotlib.backends.backend_agg import FigureCanvasAgg + from matplotlib.figure import Figure + fig = Figure(frameon=False) + FigureCanvasAgg(fig) + fig.figimage(img, resize=True) + fig.savefig(filename) diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 727a5965d38..cf6bfad40e7 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -397,8 +397,9 @@ def set_camera(self, azimuth=None, elevation=None, distance=None, _set_3d_view(self.figure, azimuth=azimuth, elevation=elevation, distance=distance, focalpoint=focalpoint) - def screenshot(self, mode='rgb'): - return self.plotter.screenshot(transparent_background=(mode == 'rgba')) + def screenshot(self, mode='rgb', filename=None): + return self.plotter.screenshot(transparent_background=(mode == 'rgba'), + filename=filename) def project(self, xyz, ch_names): xy = _3d_to_2d(self.plotter, xyz) diff --git a/mne/viz/backends/base_renderer.py b/mne/viz/backends/base_renderer.py index 90435450f94..ffaf424ff14 100644 --- a/mne/viz/backends/base_renderer.py +++ b/mne/viz/backends/base_renderer.py @@ -315,7 +315,7 @@ def set_camera(self, azimuth=None, elevation=None, distance=None, pass @abstractclassmethod - def screenshot(self, mode='rgb'): + def screenshot(self, mode='rgb', filename=None): """Take a screenshot of the scene. Parameters @@ -323,6 +323,8 @@ def screenshot(self, mode='rgb'): mode: str Either 'rgb' or 'rgba' for values to return. Default is 'rgb'. + filename: str | None + If not None, save the figure to the disk. """ pass From 9a02f7ee06b9f6a386dbcca4be358892c0def756 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 27 Sep 2019 14:14:02 +0200 Subject: [PATCH 71/88] TST: Try on sphinx-patch [circle full][skip azp][skip travis] --- environment.yml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index 2f188f0bbdb..6375d77a2ee 100644 --- a/environment.yml +++ b/environment.yml @@ -34,7 +34,7 @@ dependencies: - pip: - mne - vtk - - pyvista>=0.22.2 + - https://github.com/pyvista/pyvista/zipball/sphinx-patch - mayavi - PySurfer[save_movie] - dipy --only-binary dipy diff --git a/requirements.txt b/requirements.txt index 007cd0c26f8..ac1cd8053d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,5 +29,5 @@ neo xlrd pydocstyle flake8 -pyvista>=0.22.2 +https://github.com/pyvista/pyvista/zipball/sphinx-patch panel From c18b5a1afaba18924663d4846059ad2edfc06fa9 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 27 Sep 2019 16:27:05 +0200 Subject: [PATCH 72/88] TST: Hotfix for figure naming by int id [circle full][skip travis][skip azp] --- mne/viz/backends/_pyvista.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index cf6bfad40e7..3063373de86 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -64,6 +64,11 @@ def build(self): self.plotter = plotter return self.plotter + def is_active(self): + if self.plotter is None: + return False + return hasattr(self.plotter, 'ren_win') + class _Projection(object): """Class storing projection information. @@ -102,20 +107,18 @@ def __init__(self, fig=None, size=(600, 600), bgcolor=(0., 0., 0.), name="PyVista Scene", show=False, shape=(1, 1)): from pyvista import OFF_SCREEN from mne.viz.backends.renderer import MNE_3D_BACKEND_TEST_DATA + figure = _Figure(title=name, size=size, shape=shape, + background_color=bgcolor, notebook=_check_notebook()) if isinstance(fig, int): - if _FIGURES.get(fig) is None: - self.figure = _Figure(title=name, size=size, - shape=shape, - background_color=bgcolor, - notebook=_check_notebook()) - _FIGURES[fig] = self.figure + saved_fig = _FIGURES.get(fig) + # Restore only active plotter + if saved_fig is not None and saved_fig.is_active(): + self.figure = saved_fig else: - self.figure = _FIGURES.get(fig) + self.figure = figure + _FIGURES[fig] = self.figure elif fig is None: - self.figure = _Figure(title=name, size=size, - shape=shape, - background_color=bgcolor, - notebook=_check_notebook()) + self.figure = figure else: self.figure = fig From 720f2b53a4018aba0bfe01094312298d95b7d202 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 30 Sep 2019 14:23:26 +0200 Subject: [PATCH 73/88] Allow only one active scalar bar --- mne/viz/_brain/_brain.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 4adaeaa17f6..86a5393e942 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -181,6 +181,7 @@ def __init__(self, subject_id, hemi, surf, title=None, self._subjects_dir = subjects_dir self._views = views self._n_times = None + self._scalarbar = False # for now only one color bar can be added # since it is the same for all figures self._colorbar_added = False @@ -448,8 +449,10 @@ def time_label(x): size=time_label_size, text=time_label(time[time_idx]), justification='right') - self._renderer.scalarbar(source=mesh, n_labels=8, - bgcolor=(0.5, 0.5, 0.5)) + if not self._scalarbar: + self._renderer.scalarbar(source=mesh, n_labels=8, + bgcolor=(0.5, 0.5, 0.5)) + self._scalarbar = True self._renderer.set_camera(azimuth=views_dict[v].azim, elevation=views_dict[v].elev) From 23fc522753b0f00f7060cb0ae4aeb494b3b847ab Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 30 Sep 2019 14:32:15 +0200 Subject: [PATCH 74/88] Change font and increase tick label size --- mne/viz/backends/_pyvista.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 3063373de86..57bb072fcbc 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -109,6 +109,7 @@ def __init__(self, fig=None, size=(600, 600), bgcolor=(0., 0., 0.), from mne.viz.backends.renderer import MNE_3D_BACKEND_TEST_DATA figure = _Figure(title=name, size=size, shape=shape, background_color=bgcolor, notebook=_check_notebook()) + self.font_family = "arial" if isinstance(fig, int): saved_fig = _FIGURES.get(fig) # Restore only active plotter @@ -357,6 +358,7 @@ def text2d(self, x, y, text, size=14, color=(1.0, 1.0, 1.0), warnings.filterwarnings("ignore", category=FutureWarning) actor = self.plotter.add_text(text, position=position, font_size=size, + font=self.font_family, color=color) if isinstance(justification, str): if justification == 'left': @@ -377,6 +379,7 @@ def text3d(self, x, y, z, text, scale, color=(1.0, 1.0, 1.0)): labels=[text], point_size=scale, text_color=color, + font_family=self.font_family, name=text, shape_opacity=0) @@ -386,6 +389,8 @@ def scalarbar(self, source, title=None, n_labels=4, bgcolor=None): self.plotter.add_scalar_bar(title=title, n_labels=n_labels, use_opacity=False, n_colors=256, position_x=0.15, width=0.7, + label_font_size=28, + font_family=self.font_family, background_color=bgcolor) def show(self): From 15a7cc37401458e1ef3a02c6eef078f6bdd0b5b7 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 30 Sep 2019 14:38:17 +0200 Subject: [PATCH 75/88] Tweak the tick label size --- mne/viz/backends/_pyvista.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 57bb072fcbc..a29209ddf76 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -389,7 +389,7 @@ def scalarbar(self, source, title=None, n_labels=4, bgcolor=None): self.plotter.add_scalar_bar(title=title, n_labels=n_labels, use_opacity=False, n_colors=256, position_x=0.15, width=0.7, - label_font_size=28, + label_font_size=22, font_family=self.font_family, background_color=bgcolor) From 1d5670a64a5eea96ad2940d87e5a4fe00c1c5731 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 30 Sep 2019 16:59:03 +0200 Subject: [PATCH 76/88] Work on coverage --- mne/viz/_brain/_brain.py | 65 ------------------------------ mne/viz/_brain/tests/test_brain.py | 2 +- 2 files changed, 1 insertion(+), 66 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 86a5393e942..ed167ad1811 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -197,9 +197,6 @@ def __init__(self, subject_id, hemi, surf, title=None, self._renderer = _Renderer(size=fig_size, bgcolor=background, shape=(n_row, n_col), fig=figure) - # XXX: use the geo_ variables - # geo_kwargs, geo_reverse, geo_curv = \ - # self._get_geo_params(cortex, alpha) for h in self._hemis: # Initialize a Surface object as the geometry geo = Surface(subject_id, h, surf, subjects_dir, offset, @@ -826,68 +823,6 @@ def _check_hemi(self, hemi): extra + ", got " + str(hemi)) return hemi - def _get_geo_params(self, cortex, alpha=1.0): - """Return kwargs and other parameters for surface rendering. - - Parameters - ---------- - cortex : str or None - The name of one of the preset cortex - styles ('classic', 'high_contrast', 'low_contrast', or - 'bone') or a valid color name. If set to None, - color is set to (0.5, 0.5, 0.5). - alpha : float in [0, 1] - Alpha level to control opacity of the cortical surface. - - Returns - ------- - kwargs : dict - Dictionary with keyword arguments to be used for surface - rendering. For colormaps, keys are ['colormap', 'vmin', - 'vmax', 'alpha'] to specify the name, minimum, maximum, - and alpha transparency of the colormap respectively. For - colors, keys are ['color', 'alpha'] to specify the name - and alpha transparency of the color respectively. - reverse : boolean - Boolean indicating whether a colormap should be - reversed. Set to False if a color (rather than a colormap) - is specified. - curv : boolean - Boolean indicating whether curv file is loaded and binary - curvature is displayed. - - """ - from matplotlib.colors import colorConverter - colormap_map = dict(classic=(dict(colormap="Greys", - vmin=-1, vmax=2, - opacity=alpha), False, True), - high_contrast=(dict(colormap="Greys", - vmin=-.1, vmax=1.3, - opacity=alpha), False, True), - low_contrast=(dict(colormap="Greys", - vmin=-5, vmax=5, - opacity=alpha), False, True), - bone=(dict(colormap="bone", - vmin=-.2, vmax=2, - opacity=alpha), True, True)) - if isinstance(cortex, str): - if cortex in colormap_map: - geo_params = colormap_map[cortex] - else: - try: - color = colorConverter.to_rgb(cortex) - geo_params = dict(color=color, opacity=alpha), False, False - except ValueError: - geo_params = cortex, False, True - # check for None before checking len: - elif cortex is None: - geo_params = dict(color=(0.5, 0.5, 0.5), - opacity=alpha), False, False - else: - raise TypeError("Expected type is `str` or `NoneType`, " - "{} is given.".format(type(cortex))) - return geo_params - def _update_limits(fmin, fmid, fmax, center, array): if center is None: diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index e56724052ca..5dd84d028f0 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -85,7 +85,7 @@ def test_brain_add_data(renderer): brain_data.add_data(hemi_data, fmin=fmin, hemi=hemi, fmax=fmax, colormap='hot', vertices=hemi_vertices, - colorbar=False, time=None) + initial_time=0., colorbar=False, time=None) @testing.requires_testing_data From 4ee3181406d9a51f1bd4e96eef9f901aa092b620 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 1 Oct 2019 16:49:38 +0200 Subject: [PATCH 77/88] Add support for colorbar parameter --- mne/viz/_brain/_brain.py | 7 ++----- mne/viz/_brain/tests/test_brain.py | 5 ++++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index ed167ad1811..565c984604a 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -325,9 +325,6 @@ def add_data(self, array, fmin=None, fmid=None, fmax=None, Due to a Mayavi (or VTK) alpha rendering bug, ``vector_alpha`` is clamped to be strictly < 1. """ - if len(array.shape) == 3: - raise ValueError('Vector values in "array" are not supported.') - _check_option('transparent', type(transparent), [bool]) _check_option('verbose', type(verbose), [bool]) @@ -446,10 +443,10 @@ def time_label(x): size=time_label_size, text=time_label(time[time_idx]), justification='right') - if not self._scalarbar: + if colorbar and not self._colorbar_added: self._renderer.scalarbar(source=mesh, n_labels=8, bgcolor=(0.5, 0.5, 0.5)) - self._scalarbar = True + self._colorbar_added = True self._renderer.set_camera(azimuth=views_dict[v].azim, elevation=views_dict[v].elev) diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 5dd84d028f0..152a9528440 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -85,7 +85,10 @@ def test_brain_add_data(renderer): brain_data.add_data(hemi_data, fmin=fmin, hemi=hemi, fmax=fmax, colormap='hot', vertices=hemi_vertices, - initial_time=0., colorbar=False, time=None) + colorbar=False, time=None) + brain_data.add_data(hemi_data, fmin=fmin, hemi=hemi, fmax=fmax, + colormap='hot', vertices=hemi_vertices, + initial_time=0., colorbar=True, time=None) @testing.requires_testing_data From 726e07cfa876524f9408656dfd6eb236ad208432 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 3 Oct 2019 11:07:41 +0200 Subject: [PATCH 78/88] Switching to master branch --- environment.yml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index 6375d77a2ee..27d62344d10 100644 --- a/environment.yml +++ b/environment.yml @@ -34,7 +34,7 @@ dependencies: - pip: - mne - vtk - - https://github.com/pyvista/pyvista/zipball/sphinx-patch + - https://github.com/pyvista/pyvista/zipball/master - mayavi - PySurfer[save_movie] - dipy --only-binary dipy diff --git a/requirements.txt b/requirements.txt index ac1cd8053d4..42f3b7f1cf2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,5 +29,5 @@ neo xlrd pydocstyle flake8 -https://github.com/pyvista/pyvista/zipball/sphinx-patch +https://github.com/pyvista/pyvista/zipball/master panel From bed57eed1c9140a1bad389f663ffe442b5666737 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 3 Oct 2019 11:15:17 +0200 Subject: [PATCH 79/88] TST: Build doc [circle full][skip azp][skip travis] From e93322a1d5307c0e46b93f3b3bea62be14ddfcba Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 3 Oct 2019 14:09:34 +0200 Subject: [PATCH 80/88] TST: Check CIs without skipping this time From d7d51391d6d0c6a5912333645214d6df4c06a5b5 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 3 Oct 2019 15:34:46 +0200 Subject: [PATCH 81/88] Rework verbose parameter --- mne/viz/_brain/_brain.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 565c984604a..10bb181c0c5 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -15,7 +15,7 @@ from .view import lh_views_dict, rh_views_dict, View from .surface import Surface from .utils import mesh_edges, smoothing_matrix -from ..utils import _check_option, logger +from ..utils import _check_option, logger, verbose class _Brain(object): @@ -222,13 +222,14 @@ def __init__(self, subject_id, hemi, surf, title=None, # Force rendering self._renderer.show() + @verbose def add_data(self, array, fmin=None, fmid=None, fmax=None, thresh=None, center=None, transparent=False, colormap="auto", alpha=1, vertices=None, smoothing_steps=None, time=None, time_label="time index=%d", colorbar=True, hemi=None, remove_existing=None, time_label_size=None, initial_time=None, scale_factor=None, vector_alpha=None, - verbose=False): + verbose=None): u"""Display data from a numpy array on the surface. This provides a similar interface to @@ -310,8 +311,7 @@ def add_data(self, array, fmin=None, fmid=None, fmax=None, Not supported yet. alpha level to control opacity of the arrows. Only used for vector-valued data. If None (default), ``alpha`` is used. - verbose : bool - Set the default verbose level. + %(verbose)s Notes ----- @@ -326,7 +326,6 @@ def add_data(self, array, fmin=None, fmid=None, fmax=None, clamped to be strictly < 1. """ _check_option('transparent', type(transparent), [bool]) - _check_option('verbose', type(verbose), [bool]) # those parameters are not supported yet, only None is allowed _check_option('thresh', thresh, [None]) @@ -660,7 +659,7 @@ def add_text(self, x, y, text, name=None, color=None, opacity=1.0, self._renderer.text2d(x=x, y=y, text=text, color=color, size=font_size, justification=justification) - def remove_labels(self, labels=None, hemi=None): + def remove_labels(self, labels=None): """Remove one or more previously added labels from the image. Parameters @@ -669,8 +668,6 @@ def remove_labels(self, labels=None, hemi=None): Labels to remove. Can be a string naming a single label, or None to remove all labels. Possible names can be found in the Brain.labels attribute. - hemi : None - Deprecated parameter, do not use. """ pass From 00c617887316d1c35e9669e0a08d2e0b5afb9596 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 3 Oct 2019 15:54:35 +0200 Subject: [PATCH 82/88] Rework verbose parameter in util --- mne/viz/_brain/utils.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/mne/viz/_brain/utils.py b/mne/viz/_brain/utils.py index 6fb629702b9..f2e7cf60cd4 100644 --- a/mne/viz/_brain/utils.py +++ b/mne/viz/_brain/utils.py @@ -1,4 +1,4 @@ -from ..utils import logger +from ..utils import logger, verbose import numpy as np from scipy import sparse @@ -8,7 +8,7 @@ def mesh_edges(faces): Parameters ---------- - faces : array of shape [n_triangles x 3] + faces : array, shape (n_triangles, 3) The mesh faces Returns @@ -30,16 +30,17 @@ def mesh_edges(faces): return edges +@verbose def smoothing_matrix(vertices, adj_mat, smoothing_steps=20, verbose=None): """Create a smoothing matrix. - This smoothing matrix can be used to interpolate data defined - for a subset of vertices onto mesh with an adjancency matrix given by - adj_mat. + This smoothing matrix can be used to interpolate data defined + for a subset of vertices onto mesh with an adjancency matrix given by + adj_mat. - If smoothing_steps is None, as many smoothing steps are applied until - the whole mesh is filled with with non-zeros. Only use this option if - the vertices correspond to a subsampled version of the mesh. + If smoothing_steps is None, as many smoothing steps are applied until + the whole mesh is filled with with non-zeros. Only use this option if + the vertices correspond to a subsampled version of the mesh. Parameters ---------- @@ -49,8 +50,7 @@ def smoothing_matrix(vertices, adj_mat, smoothing_steps=20, verbose=None): N x N adjacency matrix of the full mesh smoothing_steps : int or None number of smoothing steps (Default: 20) - verbose : bool, str, int, or None - If not None, override default verbose level (see surfer.verbose). + %(verbose)s Returns ------- From a667d0efebf9d888da7341b1ef620bb31b9f100d Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 4 Oct 2019 12:10:44 +0200 Subject: [PATCH 83/88] Add magic number 0.15 for default cylinder glyph radius --- mne/viz/backends/_pyvista.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index a29209ddf76..4f538e3c705 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -325,7 +325,7 @@ def quiver3d(self, x, y, z, u, v, w, color, scale, mode, resolution=8, elif mode == "cylinder": cylinder = vtk.vtkCylinderSource() cylinder.SetHeight(glyph_height) - cylinder.SetRadius(glyph_height) + cylinder.SetRadius(0.15) cylinder.SetCenter(glyph_center) cylinder.SetResolution(glyph_resolution) cylinder.Update() From 5ab129b4e51a70f480052299c2c936c3d5d3c670 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 4 Oct 2019 16:02:20 +0200 Subject: [PATCH 84/88] Fix text position --- mne/viz/backends/_pyvista.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 4f538e3c705..f6c69bd3583 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -352,8 +352,7 @@ def quiver3d(self, x, y, z, u, v, w, color, scale, mode, resolution=8, def text2d(self, x, y, text, size=14, color=(1.0, 1.0, 1.0), justification=None): size = 14 if size is None else size - position = (x * self.figure.store['window_size'][0], - y * self.figure.store['window_size'][1]) + position = (x, y) with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) actor = self.plotter.add_text(text, position=position, From da4c42af7310518bb10e2272c54741c3bfb6d9f2 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 4 Oct 2019 16:18:26 +0200 Subject: [PATCH 85/88] TST: Try normalized viewport coordinates --- environment.yml | 2 +- mne/viz/backends/_pyvista.py | 3 ++- requirements.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/environment.yml b/environment.yml index 27d62344d10..a9c2c45ff59 100644 --- a/environment.yml +++ b/environment.yml @@ -34,7 +34,7 @@ dependencies: - pip: - mne - vtk - - https://github.com/pyvista/pyvista/zipball/master + - https://github.com/pyvista/pyvista/zipball/add_text_normalized_viewport - mayavi - PySurfer[save_movie] - dipy --only-binary dipy diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index f6c69bd3583..dde302481a6 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -358,7 +358,8 @@ def text2d(self, x, y, text, size=14, color=(1.0, 1.0, 1.0), actor = self.plotter.add_text(text, position=position, font_size=size, font=self.font_family, - color=color) + color=color, + viewport=True) if isinstance(justification, str): if justification == 'left': actor.GetTextProperty().SetJustificationToLeft() diff --git a/requirements.txt b/requirements.txt index 42f3b7f1cf2..c05d74ff8ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,5 +29,5 @@ neo xlrd pydocstyle flake8 -https://github.com/pyvista/pyvista/zipball/master +https://github.com/pyvista/pyvista/zipball/add_text_normalized_viewport panel From 469baea46e356b49b798c41919d31efab359a076 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 4 Oct 2019 22:06:12 +0200 Subject: [PATCH 86/88] Switch back to master branch of pyvista --- environment.yml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index a9c2c45ff59..27d62344d10 100644 --- a/environment.yml +++ b/environment.yml @@ -34,7 +34,7 @@ dependencies: - pip: - mne - vtk - - https://github.com/pyvista/pyvista/zipball/add_text_normalized_viewport + - https://github.com/pyvista/pyvista/zipball/master - mayavi - PySurfer[save_movie] - dipy --only-binary dipy diff --git a/requirements.txt b/requirements.txt index c05d74ff8ab..42f3b7f1cf2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,5 +29,5 @@ neo xlrd pydocstyle flake8 -https://github.com/pyvista/pyvista/zipball/add_text_normalized_viewport +https://github.com/pyvista/pyvista/zipball/master panel From d0c631561a87576b553410e1cf501771bbc90211 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 4 Oct 2019 22:07:40 +0200 Subject: [PATCH 87/88] TST: Build doc [circle full] From 518439448862620c3f7cfa046b73a0af918520d4 Mon Sep 17 00:00:00 2001 From: Alexandre Gramfort Date: Tue, 8 Oct 2019 13:03:08 +0200 Subject: [PATCH 88/88] fix import order + add authors --- mne/viz/_brain/utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mne/viz/_brain/utils.py b/mne/viz/_brain/utils.py index f2e7cf60cd4..d1f94eaa241 100644 --- a/mne/viz/_brain/utils.py +++ b/mne/viz/_brain/utils.py @@ -1,7 +1,13 @@ -from ..utils import logger, verbose +# Authors: Guillaume Favelier +# +# License: Simplified BSD + + import numpy as np from scipy import sparse +from ..utils import logger, verbose + def mesh_edges(faces): """Return sparse matrix with edges as an adjacency matrix.