From 9f2a0057fd3f41cc2fbd4cdcfaf761f0a84a5f2f Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 13 Jan 2020 14:20:20 +0100 Subject: [PATCH 01/13] Add basic support for playback --- mne/viz/_brain/_timeviewer.py | 49 +++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/mne/viz/_brain/_timeviewer.py b/mne/viz/_brain/_timeviewer.py index eac09b3ceec..a69a16ef368 100644 --- a/mne/viz/_brain/_timeviewer.py +++ b/mne/viz/_brain/_timeviewer.py @@ -109,15 +109,16 @@ class _TimeViewer(object): """Class to interact with _Brain.""" def __init__(self, brain): + self.brain = brain self.plotter = brain._renderer.plotter # scalar bar if brain._colorbar_added: scalar_bar = self.plotter.scalar_bar scalar_bar.SetOrientationToVertical() - scalar_bar.SetHeight(0.8) + scalar_bar.SetHeight(0.6) scalar_bar.SetWidth(0.05) - scalar_bar.SetPosition(0.05, 0.1) + scalar_bar.SetPosition(0.02, 0.2) # smoothing slider default_smoothing_value = 7 @@ -173,6 +174,23 @@ def __init__(self, brain): pointb=(0.77, 0.1), event_type='always' ) + time_slider.name = "time_slider" + + # playback speed + default_playback_speed = 1 + self.set_playback_speed = IntSlider( + plotter=self.plotter, + callback=self.set_playback_speed, + name="playback_speed" + ) + playback_speed_slider = self.plotter.add_slider_widget( + self.set_playback_speed, + value=default_playback_speed, + rng=[1, 100], title="playback speed", + pointa=(0.02, 0.1), + pointb=(0.18, 0.1) + ) + playback_speed_slider.name = "playback_speed" # colormap slider scaling_limits = [0.2, 2.0] @@ -240,12 +258,19 @@ def __init__(self, brain): _set_slider_style(fmid_slider) _set_slider_style(fmax_slider) _set_slider_style(fscale_slider) + _set_slider_style(playback_speed_slider) _set_slider_style(time_slider, show_label=False) # add toggle to show/hide interface self.visibility = True self.plotter.add_key_event('y', self.toggle_interface) + # add toggle to start/stop playback + self.playback = False + self.playback_speed = 1 + self.plotter.add_key_event('t', self.toggle_playback) + self.plotter.add_callback(self.play) + def toggle_interface(self): self.visibility = not self.visibility for slider in self.plotter.slider_widgets: @@ -254,6 +279,26 @@ def toggle_interface(self): else: slider.Off() + def toggle_playback(self): + self.playback = not self.playback + + def set_playback_speed(self, speed): + self.playback_speed = speed + + def play(self): + if self.playback: + time_idx = self.brain._data['time_idx'] + self.playback_speed + max_time = len(self.brain._data['time']) + if time_idx < max_time: + self.brain.set_time_point(time_idx) + for slider in self.plotter.slider_widgets: + name = getattr(slider, "name", None) + if name == "time_slider": + slider_rep = slider.GetRepresentation() + slider_rep.SetValue(time_idx) + else: + self.playback = False + def _set_slider_style(slider, show_label=True): slider_rep = slider.GetRepresentation() From 3e184708fecdb95dc18424cd6a6d621006971f97 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 13 Jan 2020 18:53:44 +0100 Subject: [PATCH 02/13] Try time dilation --- mne/viz/_brain/_timeviewer.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/mne/viz/_brain/_timeviewer.py b/mne/viz/_brain/_timeviewer.py index a69a16ef368..392d1238656 100644 --- a/mne/viz/_brain/_timeviewer.py +++ b/mne/viz/_brain/_timeviewer.py @@ -268,8 +268,9 @@ def __init__(self, brain): # add toggle to start/stop playback self.playback = False self.playback_speed = 1 + self.time_elapsed = 0 self.plotter.add_key_event('t', self.toggle_playback) - self.plotter.add_callback(self.play) + self.plotter.add_callback(self.play, 10) def toggle_interface(self): self.visibility = not self.visibility @@ -281,23 +282,31 @@ def toggle_interface(self): def toggle_playback(self): self.playback = not self.playback + self.time_elapsed = 0 def set_playback_speed(self, speed): self.playback_speed = speed def play(self): if self.playback: - time_idx = self.brain._data['time_idx'] + self.playback_speed - max_time = len(self.brain._data['time']) - if time_idx < max_time: - self.brain.set_time_point(time_idx) - for slider in self.plotter.slider_widgets: - name = getattr(slider, "name", None) - if name == "time_slider": - slider_rep = slider.GetRepresentation() - slider_rep.SetValue(time_idx) - else: - self.playback = False + self.time_elapsed += 10 + if self.time_elapsed >= self.playback_speed * 10: + times = self.brain._data['time'] + time_idx = self.brain._data['time_idx'] + time = times[time_idx] + 1. / self.playback_speed + idx = np.argmin(np.abs(times - time)) + + max_time = len(self.brain._data['time']) + if time_idx < max_time: + self.brain.set_time_point(idx) + for slider in self.plotter.slider_widgets: + name = getattr(slider, "name", None) + if name == "time_slider": + slider_rep = slider.GetRepresentation() + slider_rep.SetValue(idx) + else: + self.playback = False + self.time_elapsed = 0 def _set_slider_style(slider, show_label=True): From e16bdfeb8f18a40aad87453b08dc25e5104cf1fc Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 14 Jan 2020 13:03:19 +0100 Subject: [PATCH 03/13] Add button widgets --- mne/viz/_brain/_timeviewer.py | 152 +++++++++++++++++++++++++++------- mne/viz/backends/_pyvista.py | 1 + 2 files changed, 124 insertions(+), 29 deletions(-) diff --git a/mne/viz/_brain/_timeviewer.py b/mne/viz/_brain/_timeviewer.py index 392d1238656..6864090ff8e 100644 --- a/mne/viz/_brain/_timeviewer.py +++ b/mne/viz/_brain/_timeviewer.py @@ -5,6 +5,7 @@ # License: Simplified BSD import time +import warnings import numpy as np @@ -118,7 +119,7 @@ def __init__(self, brain): scalar_bar.SetOrientationToVertical() scalar_bar.SetHeight(0.6) scalar_bar.SetWidth(0.05) - scalar_bar.SetPosition(0.02, 0.2) + scalar_bar.SetPosition(0.02, 0.35) # smoothing slider default_smoothing_value = 7 @@ -159,10 +160,10 @@ def __init__(self, brain): # time label for hemi in brain._hemis: - time_actor = brain._data.get(hemi + '_time_actor') - if time_actor is not None: - time_actor.SetPosition(0.5, 0.03) - time_actor.GetTextProperty().SetJustificationToCentered() + self.time_actor = brain._data.get(hemi + '_time_actor') + if self.time_actor is not None: + self.time_actor.SetPosition(0.5, 0.03) + self.time_actor.GetTextProperty().SetJustificationToCentered() # time slider max_time = len(brain._data['time']) - 1 @@ -251,6 +252,47 @@ def __init__(self, brain): pointb=(0.98, 0.10) ) + # add toggle to start/stop playback + self.playback = False + self.playback_speed = 1 + self.time_elapsed = 0 + self.refresh_rate = 16 + self.plotter.add_callback(self.play, self.refresh_rate) + self.plotter.add_callback(self.perform_maintenance) + self.button_size = 40 + self.font_size = 14 + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=FutureWarning) + playback_button = self.plotter.add_checkbox_button_widget( + self.toggle_playback, + value=False, + size=self.button_size, + position=(0, 0) + ) + playback_button.name = "toggle_playback" + self.playback_actor = self.plotter.add_text( + text="Start", + font_size=self.font_size, + position=(0, 0) + ) + + # add toggle to show/hide interface + self.visibility = True + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=FutureWarning) + interface_button = self.plotter.add_checkbox_button_widget( + self.toggle_interface, + value=True, + size=self.button_size, + position=(0, 0) + ) + interface_button.name = "toggle_interface" + self.interface_actor = self.plotter.add_text( + text="Hide", + font_size=self.font_size, + position=(0, 0) + ) + # set the slider style _set_slider_style(smoothing_slider) _set_slider_style(orientation_slider, show_label=False) @@ -261,35 +303,46 @@ def __init__(self, brain): _set_slider_style(playback_speed_slider) _set_slider_style(time_slider, show_label=False) - # add toggle to show/hide interface - self.visibility = True - self.plotter.add_key_event('y', self.toggle_interface) + # set the text style + _set_text_style(self.time_actor) + _set_text_style(self.playback_actor) + _set_text_style(self.interface_actor) - # add toggle to start/stop playback - self.playback = False - self.playback_speed = 1 - self.time_elapsed = 0 - self.plotter.add_key_event('t', self.toggle_playback) - self.plotter.add_callback(self.play, 10) + self.perform_maintenance() - def toggle_interface(self): - self.visibility = not self.visibility + def toggle_interface(self, state): + self.visibility = state for slider in self.plotter.slider_widgets: if self.visibility: slider.On() else: slider.Off() - - def toggle_playback(self): - self.playback = not self.playback + for button in self.plotter.button_widgets: + name = getattr(button, "name", None) + if name != "toggle_interface": + if self.visibility: + button.On() + else: + button.Off() + if self.visibility: + self.interface_actor.SetInput("Hide") + else: + self.interface_actor.SetInput("Show") + + def toggle_playback(self, state): + self.playback = state self.time_elapsed = 0 + if self.playback: + self.playback_actor.SetInput("Stop") + else: + self.playback_actor.SetInput("Start") def set_playback_speed(self, speed): self.playback_speed = speed def play(self): if self.playback: - self.time_elapsed += 10 + self.time_elapsed += self.refresh_rate if self.time_elapsed >= self.playback_speed * 10: times = self.brain._data['time'] time_idx = self.brain._data['time_idx'] @@ -308,17 +361,58 @@ def play(self): self.playback = False self.time_elapsed = 0 + def place_widget(self, position): + if hasattr(self.plotter, 'ren_win'): + window_size = self.plotter.ren_win.GetSize() + position = ( + position[0] * window_size[0], + position[1] * window_size[1] + ) + return position + + def set_bounds(self, position): + bounds = [ + position[0], position[0] + self.button_size, + position[1], position[1] + self.button_size, + 0., 0. + ] + return bounds + + def perform_maintenance(self): + for button in self.plotter.button_widgets: + name = getattr(button, "name", None) + if name == "toggle_playback": + button_rep = button.GetRepresentation() + position = self.place_widget((0.02, 0.17)) + bounds = self.set_bounds(position) + button_rep.PlaceWidget(bounds) + elif name == "toggle_interface": + button_rep = button.GetRepresentation() + position = self.place_widget((0.02, 0.27)) + bounds = self.set_bounds(position) + button_rep.PlaceWidget(bounds) + + self.playback_actor.SetPosition(self.place_widget((0.06, 0.17))) + self.interface_actor.SetPosition(self.place_widget((0.06, 0.27))) + def _set_slider_style(slider, show_label=True): - slider_rep = slider.GetRepresentation() - slider_rep.SetSliderLength(0.02) - slider_rep.SetSliderWidth(0.04) - slider_rep.SetTubeWidth(0.005) - slider_rep.SetEndCapLength(0.01) - slider_rep.SetEndCapWidth(0.02) - slider_rep.GetSliderProperty().SetColor((0.5, 0.5, 0.5)) - if not show_label: - slider_rep.ShowSliderLabelOff() + if slider is not None: + slider_rep = slider.GetRepresentation() + slider_rep.SetSliderLength(0.02) + slider_rep.SetSliderWidth(0.04) + slider_rep.SetTubeWidth(0.005) + slider_rep.SetEndCapLength(0.01) + slider_rep.SetEndCapWidth(0.02) + slider_rep.GetSliderProperty().SetColor((0.5, 0.5, 0.5)) + if not show_label: + slider_rep.ShowSliderLabelOff() + + +def _set_text_style(text_actor): + if text_actor is not None: + prop = text_actor.GetTextProperty() + prop.BoldOn() def _get_range(brain): diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index f6305bc1f23..83aadcccaa9 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -138,6 +138,7 @@ def __init__(self, fig=None, size=(600, 600), bgcolor='black', self.plotter = self.figure.build() self.plotter.hide_axes() + self.plotter.disable_depth_peeling() def subplot(self, x, y): with warnings.catch_warnings(): From fd6bf28716383dbdb51ac94922a7b7e383d5c607 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 14 Jan 2020 14:25:22 +0100 Subject: [PATCH 04/13] Rollback this part --- mne/viz/_brain/_timeviewer.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/mne/viz/_brain/_timeviewer.py b/mne/viz/_brain/_timeviewer.py index 6864090ff8e..960c1ceaa72 100644 --- a/mne/viz/_brain/_timeviewer.py +++ b/mne/viz/_brain/_timeviewer.py @@ -317,13 +317,6 @@ def toggle_interface(self, state): slider.On() else: slider.Off() - for button in self.plotter.button_widgets: - name = getattr(button, "name", None) - if name != "toggle_interface": - if self.visibility: - button.On() - else: - button.Off() if self.visibility: self.interface_actor.SetInput("Hide") else: From b84b775f7655e2659ed8c106a2bb2439b676677b Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 14 Jan 2020 15:50:48 +0100 Subject: [PATCH 05/13] Add linear time interpolation --- mne/viz/_brain/_brain.py | 26 ++++++++++++++++++++------ mne/viz/_brain/_timeviewer.py | 8 +++++++- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 049ccfac6b0..f5a1da01685 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -816,6 +816,7 @@ def set_data_smoothing(self, n_steps): Number of smoothing steps """ from ..backends._pyvista import _set_mesh_scalars + from scipy.interpolate import interp1d for hemi in ['lh', 'rh']: pd = self._data.get(hemi + '_mesh') if pd is not None: @@ -823,10 +824,14 @@ def set_data_smoothing(self, n_steps): vertices = self._data[hemi + '_vertices'] if pd is not None: time_idx = self._data['time_idx'] - if self._data['array'].ndim == 1: - act_data = array - elif self._data['array'].ndim == 2: - act_data = array[:, time_idx] + act_data = array + if self._data['array'].ndim == 2: + if isinstance(time_idx, int): + act_data = act_data[:, time_idx] + else: + times = np.arange(self._n_times) + act_data = interp1d(times, act_data, 'linear', axis=1, + assume_sorted=True)(time_idx) adj_mat = mesh_edges(self.geo[hemi].faces) smooth_mat = smoothing_matrix(vertices, @@ -839,7 +844,7 @@ def set_data_smoothing(self, n_steps): def set_time_point(self, time_idx): """Set the time point shown.""" from ..backends._pyvista import _set_mesh_scalars - time_idx = int(time_idx) + from scipy.interpolate import interp1d for hemi in ['lh', 'rh']: pd = self._data.get(hemi + '_mesh') if pd is not None: @@ -855,13 +860,22 @@ def set_time_point(self, time_idx): if isinstance(time_idx, int): act_data = act_data[:, time_idx] + else: + times = np.arange(self._n_times) + act_data = interp1d(times, act_data, 'linear', axis=1, + assume_sorted=True)(time_idx) smooth_mat = self._data[hemi + '_smooth_mat'] if smooth_mat is not None: act_data = smooth_mat.dot(act_data) _set_mesh_scalars(pd, act_data, 'Data') if callable(time_label) and time_actor is not None: - time_actor.SetInput(time_label(time[time_idx])) + if isinstance(time_idx, float): + ifunc = interp1d(times, self._data['time']) + time = ifunc(time_idx) + time_actor.SetInput(time_label(time)) + else: + time_actor.SetInput(time_label(time[time_idx])) self._data['time_idx'] = time_idx def update_fmax(self, fmax): diff --git a/mne/viz/_brain/_timeviewer.py b/mne/viz/_brain/_timeviewer.py index 960c1ceaa72..c8ecd09973c 100644 --- a/mne/viz/_brain/_timeviewer.py +++ b/mne/viz/_brain/_timeviewer.py @@ -334,12 +334,18 @@ def set_playback_speed(self, speed): self.playback_speed = speed def play(self): + from scipy.interpolate import interp1d if self.playback: self.time_elapsed += self.refresh_rate if self.time_elapsed >= self.playback_speed * 10: times = self.brain._data['time'] time_idx = self.brain._data['time_idx'] - time = times[time_idx] + 1. / self.playback_speed + if isinstance(time_idx, float): + ifunc = interp1d(times, times) + time = ifunc(time_idx) + else: + time = times[time_idx] + time = time + 1. / self.playback_speed idx = np.argmin(np.abs(times - time)) max_time = len(self.brain._data['time']) From 0b8ac3f587f8ee20fcd1f640301ee83b568add76 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 14 Jan 2020 16:02:01 +0100 Subject: [PATCH 06/13] Fix time management --- mne/viz/_brain/_timeviewer.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/mne/viz/_brain/_timeviewer.py b/mne/viz/_brain/_timeviewer.py index c8ecd09973c..a99140d633f 100644 --- a/mne/viz/_brain/_timeviewer.py +++ b/mne/viz/_brain/_timeviewer.py @@ -338,15 +338,12 @@ def play(self): if self.playback: self.time_elapsed += self.refresh_rate if self.time_elapsed >= self.playback_speed * 10: - times = self.brain._data['time'] + time_data = self.brain._data['time'] time_idx = self.brain._data['time_idx'] - if isinstance(time_idx, float): - ifunc = interp1d(times, times) - time = ifunc(time_idx) - else: - time = times[time_idx] - time = time + 1. / self.playback_speed - idx = np.argmin(np.abs(times - time)) + times = np.arange(self.brain._n_times) + ifunc = interp1d(times, time_data) + time_point = ifunc(time_idx) + 1. / self.playback_speed + idx = np.argmin(np.abs(time_data - time_point)) max_time = len(self.brain._data['time']) if time_idx < max_time: From 726cc97326709f580b1d54d89d846e723aa7cfff Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 14 Jan 2020 17:34:29 +0100 Subject: [PATCH 07/13] Fix time dilation --- mne/viz/_brain/_brain.py | 16 +++++++------ mne/viz/_brain/_timeviewer.py | 42 +++++++++++++++++------------------ 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index f5a1da01685..53b5e12ed6b 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -830,8 +830,9 @@ def set_data_smoothing(self, n_steps): act_data = act_data[:, time_idx] else: times = np.arange(self._n_times) - act_data = interp1d(times, act_data, 'linear', axis=1, - assume_sorted=True)(time_idx) + act_data = interp1d( + times, act_data, 'linear', axis=1, + assume_sorted=True)(time_idx) adj_mat = mesh_edges(self.geo[hemi].faces) smooth_mat = smoothing_matrix(vertices, @@ -870,12 +871,13 @@ def set_time_point(self, time_idx): act_data = smooth_mat.dot(act_data) _set_mesh_scalars(pd, act_data, 'Data') if callable(time_label) and time_actor is not None: - if isinstance(time_idx, float): - ifunc = interp1d(times, self._data['time']) - time = ifunc(time_idx) - time_actor.SetInput(time_label(time)) + if isinstance(time_idx, int): + self._current_time = time[time_idx] + time_actor.SetInput(time_label(self._current_time)) else: - time_actor.SetInput(time_label(time[time_idx])) + ifunc = interp1d(times, self._data['time']) + self._current_time = ifunc(time_idx) + time_actor.SetInput(time_label(self._current_time)) self._data['time_idx'] = time_idx def update_fmax(self, fmax): diff --git a/mne/viz/_brain/_timeviewer.py b/mne/viz/_brain/_timeviewer.py index a99140d633f..099bbf047a3 100644 --- a/mne/viz/_brain/_timeviewer.py +++ b/mne/viz/_brain/_timeviewer.py @@ -257,6 +257,7 @@ def __init__(self, brain): self.playback_speed = 1 self.time_elapsed = 0 self.refresh_rate = 16 + self.refresh_rate_ms = self.refresh_rate / 1000. self.plotter.add_callback(self.play, self.refresh_rate) self.plotter.add_callback(self.perform_maintenance) self.button_size = 40 @@ -324,7 +325,6 @@ def toggle_interface(self, state): def toggle_playback(self, state): self.playback = state - self.time_elapsed = 0 if self.playback: self.playback_actor.SetInput("Stop") else: @@ -336,26 +336,26 @@ def set_playback_speed(self, speed): def play(self): from scipy.interpolate import interp1d if self.playback: - self.time_elapsed += self.refresh_rate - if self.time_elapsed >= self.playback_speed * 10: - time_data = self.brain._data['time'] - time_idx = self.brain._data['time_idx'] - times = np.arange(self.brain._n_times) - ifunc = interp1d(times, time_data) - time_point = ifunc(time_idx) + 1. / self.playback_speed - idx = np.argmin(np.abs(time_data - time_point)) - - max_time = len(self.brain._data['time']) - if time_idx < max_time: - self.brain.set_time_point(idx) - for slider in self.plotter.slider_widgets: - name = getattr(slider, "name", None) - if name == "time_slider": - slider_rep = slider.GetRepresentation() - slider_rep.SetValue(idx) - else: - self.playback = False - self.time_elapsed = 0 + time_data = self.brain._data['time'] + times = np.arange(self.brain._n_times) + time_shift = self.refresh_rate_ms / self.playback_speed + time_point = self.brain._current_time + time_shift + if time_point < np.max(time_data): + ifunc = interp1d(time_data, times) + idx = ifunc(time_point) + self.brain.set_time_point(idx) + for slider in self.plotter.slider_widgets: + name = getattr(slider, "name", None) + if name == "time_slider": + slider_rep = slider.GetRepresentation() + slider_rep.SetValue(idx) + else: + self.toggle_playback(False) + for button in self.plotter.button_widgets: + name = getattr(button, "name", None) + if name == "toggle_playback": + button_rep = button.GetRepresentation() + button_rep.SetState(0) def place_widget(self, position): if hasattr(self.plotter, 'ren_win'): From 14840aecbf5bdffb4d75fd4d5732786207a3dbff Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 15 Jan 2020 10:22:23 +0100 Subject: [PATCH 08/13] Update range of playback speed --- mne/viz/_brain/_timeviewer.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/mne/viz/_brain/_timeviewer.py b/mne/viz/_brain/_timeviewer.py index 099bbf047a3..c6af060e4f6 100644 --- a/mne/viz/_brain/_timeviewer.py +++ b/mne/viz/_brain/_timeviewer.py @@ -179,19 +179,13 @@ def __init__(self, brain): # playback speed default_playback_speed = 1 - self.set_playback_speed = IntSlider( - plotter=self.plotter, - callback=self.set_playback_speed, - name="playback_speed" - ) playback_speed_slider = self.plotter.add_slider_widget( self.set_playback_speed, value=default_playback_speed, - rng=[1, 100], title="playback speed", + rng=[0.01, 1], title="playback speed", pointa=(0.02, 0.1), pointb=(0.18, 0.1) ) - playback_speed_slider.name = "playback_speed" # colormap slider scaling_limits = [0.2, 2.0] @@ -338,7 +332,7 @@ def play(self): if self.playback: time_data = self.brain._data['time'] times = np.arange(self.brain._n_times) - time_shift = self.refresh_rate_ms / self.playback_speed + time_shift = self.refresh_rate_ms * self.playback_speed time_point = self.brain._current_time + time_shift if time_point < np.max(time_data): ifunc = interp1d(time_data, times) From e3f9a0cc0ff4fe4f4d5026dff795d651d0ad5c5a Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 15 Jan 2020 10:39:13 +0100 Subject: [PATCH 09/13] Rollback to shortcuts --- mne/viz/_brain/_timeviewer.py | 97 +++-------------------------------- 1 file changed, 8 insertions(+), 89 deletions(-) diff --git a/mne/viz/_brain/_timeviewer.py b/mne/viz/_brain/_timeviewer.py index c6af060e4f6..33c7693ef82 100644 --- a/mne/viz/_brain/_timeviewer.py +++ b/mne/viz/_brain/_timeviewer.py @@ -5,7 +5,6 @@ # License: Simplified BSD import time -import warnings import numpy as np @@ -119,7 +118,7 @@ def __init__(self, brain): scalar_bar.SetOrientationToVertical() scalar_bar.SetHeight(0.6) scalar_bar.SetWidth(0.05) - scalar_bar.SetPosition(0.02, 0.35) + scalar_bar.SetPosition(0.02, 0.2) # smoothing slider default_smoothing_value = 7 @@ -253,40 +252,11 @@ def __init__(self, brain): self.refresh_rate = 16 self.refresh_rate_ms = self.refresh_rate / 1000. self.plotter.add_callback(self.play, self.refresh_rate) - self.plotter.add_callback(self.perform_maintenance) - self.button_size = 40 - self.font_size = 14 - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - playback_button = self.plotter.add_checkbox_button_widget( - self.toggle_playback, - value=False, - size=self.button_size, - position=(0, 0) - ) - playback_button.name = "toggle_playback" - self.playback_actor = self.plotter.add_text( - text="Start", - font_size=self.font_size, - position=(0, 0) - ) + self.plotter.add_key_event('space', self.toggle_playback) # add toggle to show/hide interface self.visibility = True - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - interface_button = self.plotter.add_checkbox_button_widget( - self.toggle_interface, - value=True, - size=self.button_size, - position=(0, 0) - ) - interface_button.name = "toggle_interface" - self.interface_actor = self.plotter.add_text( - text="Hide", - font_size=self.font_size, - position=(0, 0) - ) + self.plotter.add_key_event('y', self.toggle_interface) # set the slider style _set_slider_style(smoothing_slider) @@ -300,29 +270,17 @@ def __init__(self, brain): # set the text style _set_text_style(self.time_actor) - _set_text_style(self.playback_actor) - _set_text_style(self.interface_actor) - - self.perform_maintenance() - def toggle_interface(self, state): - self.visibility = state + def toggle_interface(self): + self.visibility = not self.visibility for slider in self.plotter.slider_widgets: if self.visibility: slider.On() else: slider.Off() - if self.visibility: - self.interface_actor.SetInput("Hide") - else: - self.interface_actor.SetInput("Show") - def toggle_playback(self, state): - self.playback = state - if self.playback: - self.playback_actor.SetInput("Stop") - else: - self.playback_actor.SetInput("Start") + def toggle_playback(self): + self.playback = not self.playback def set_playback_speed(self, speed): self.playback_speed = speed @@ -344,46 +302,7 @@ def play(self): slider_rep = slider.GetRepresentation() slider_rep.SetValue(idx) else: - self.toggle_playback(False) - for button in self.plotter.button_widgets: - name = getattr(button, "name", None) - if name == "toggle_playback": - button_rep = button.GetRepresentation() - button_rep.SetState(0) - - def place_widget(self, position): - if hasattr(self.plotter, 'ren_win'): - window_size = self.plotter.ren_win.GetSize() - position = ( - position[0] * window_size[0], - position[1] * window_size[1] - ) - return position - - def set_bounds(self, position): - bounds = [ - position[0], position[0] + self.button_size, - position[1], position[1] + self.button_size, - 0., 0. - ] - return bounds - - def perform_maintenance(self): - for button in self.plotter.button_widgets: - name = getattr(button, "name", None) - if name == "toggle_playback": - button_rep = button.GetRepresentation() - position = self.place_widget((0.02, 0.17)) - bounds = self.set_bounds(position) - button_rep.PlaceWidget(bounds) - elif name == "toggle_interface": - button_rep = button.GetRepresentation() - position = self.place_widget((0.02, 0.27)) - bounds = self.set_bounds(position) - button_rep.PlaceWidget(bounds) - - self.playback_actor.SetPosition(self.place_widget((0.06, 0.17))) - self.interface_actor.SetPosition(self.place_widget((0.06, 0.27))) + self.playback = False def _set_slider_style(slider, show_label=True): From 50dddef485c0a12d4762931be91039992b2dbc7c Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 15 Jan 2020 10:56:21 +0100 Subject: [PATCH 10/13] Snap fscale back to 1 --- mne/viz/_brain/_brain.py | 3 +++ mne/viz/_brain/_timeviewer.py | 10 +++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 53b5e12ed6b..7bf973f83f8 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -960,6 +960,9 @@ def update_fscale(self, fscale): scalar_bar = None _set_colormap_range(actor, ctable, scalar_bar, rng) self._data['ctable'] = ctable + self._data['fmin'] = fmin + self._data['fmid'] = fmid + self._data['fmax'] = fmax @property def data(self): diff --git a/mne/viz/_brain/_timeviewer.py b/mne/viz/_brain/_timeviewer.py index 33c7693ef82..17c89704a81 100644 --- a/mne/viz/_brain/_timeviewer.py +++ b/mne/viz/_brain/_timeviewer.py @@ -37,9 +37,9 @@ def __init__(self, plotter=None, brain=None): def __call__(self, value): """Update the colorbar sliders.""" self.brain.update_fscale(value) - fmin = self.brain._data['fmin'] * value - fmid = self.brain._data['fmid'] * value - fmax = self.brain._data['fmax'] * value + fmin = self.brain._data['fmin'] + fmid = self.brain._data['fmid'] + fmax = self.brain._data['fmax'] for slider in self.plotter.slider_widgets: name = getattr(slider, "name", None) if name == "fmin": @@ -51,6 +51,9 @@ def __call__(self, value): elif name == "fmax": slider_rep = slider.GetRepresentation() slider_rep.SetValue(fmax) + elif name == "fscale": + slider_rep = slider.GetRepresentation() + slider_rep.SetValue(1.0) class BumpColorbarPoints(object): @@ -244,6 +247,7 @@ def __init__(self, brain): pointa=(0.82, 0.10), pointb=(0.98, 0.10) ) + fscale_slider.name = "fscale" # add toggle to start/stop playback self.playback = False From 926beaa957a623f65707d727aa6bbb85cc0b9a42 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 15 Jan 2020 12:53:11 +0100 Subject: [PATCH 11/13] Improve coverage --- mne/viz/_brain/_timeviewer.py | 4 ++-- mne/viz/_brain/tests/test_brain.py | 25 +++++++++++++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/mne/viz/_brain/_timeviewer.py b/mne/viz/_brain/_timeviewer.py index 17c89704a81..957669f7471 100644 --- a/mne/viz/_brain/_timeviewer.py +++ b/mne/viz/_brain/_timeviewer.py @@ -236,12 +236,12 @@ def __init__(self, brain): event_type="always", ) fmax_slider.name = "fmax" - update_fscale = UpdateColorbarScale( + self.update_fscale = UpdateColorbarScale( plotter=self.plotter, brain=brain, ) fscale_slider = self.plotter.add_slider_widget( - update_fscale, + self.update_fscale, value=1.0, rng=scaling_limits, title="fscale", pointa=(0.82, 0.10), diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index ee5a3e2e957..bc28262abee 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -14,7 +14,8 @@ import numpy as np from numpy.testing import assert_allclose -from mne import read_source_estimate +from mne import SourceEstimate, read_source_estimate +from mne.source_space import read_source_spaces from mne.datasets import testing from mne.viz._brain import _Brain, _TimeViewer from mne.viz._brain.colormap import calculate_lut @@ -26,6 +27,8 @@ 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') +src_fname = path.join(data_path, 'subjects', 'sample', 'bem', + 'sample-oct-6-src.fif') surf = 'inflated' @@ -131,10 +134,21 @@ def test_brain_timeviewer(renderer): # Disable testing to allow interactive window renderer.MNE_3D_BACKEND_TESTING = False - stc = read_source_estimate(fname_stc) + sample_src = read_source_spaces(src_fname) + + # dense version + vertices = [s['vertno'] for s in sample_src] + n_time = 5 + n_verts = sum(len(v) for v in vertices) + stc_data = np.zeros((n_verts * n_time)) + stc_size = stc_data.size + stc_data[(np.random.rand(stc_size // 20) * stc_size).astype(int)] = \ + np.random.RandomState(0).rand(stc_data.size // 20) + stc_data.shape = (n_verts, n_time) + stc = SourceEstimate(stc_data, vertices, 1, 1) hemi = 'lh' - hemi_data = stc.data[:len(stc.vertices[0]), 10] + hemi_data = getattr(stc, hemi + '_data') hemi_vertices = stc.vertices[0] fmin = stc.data.min() fmax = stc.data.max() @@ -144,7 +158,7 @@ def test_brain_timeviewer(renderer): brain_data.add_data(hemi_data, fmin=fmin, hemi=hemi, fmax=fmax, colormap='hot', vertices=hemi_vertices, - colorbar=False, time=[0]) + colorbar=False) brain_data.set_time_point(time_idx=0) @@ -154,6 +168,9 @@ def test_brain_timeviewer(renderer): time_viewer.update_fmax(value=4.0) time_viewer.update_fmid(value=6.0) time_viewer.update_fmid(value=4.0) + time_viewer.update_fscale(value=1.1) + time_viewer.toggle_interface() + time_viewer.toggle_playback() def test_brain_colormap(): From 68ac4f5bca49374d05cac81f7573a13918c8c6c7 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 15 Jan 2020 10:36:22 -0500 Subject: [PATCH 12/13] FIX: Smoother animation --- mne/viz/_brain/_timeviewer.py | 64 +++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/mne/viz/_brain/_timeviewer.py b/mne/viz/_brain/_timeviewer.py index 957669f7471..0b95fdea8b1 100644 --- a/mne/viz/_brain/_timeviewer.py +++ b/mne/viz/_brain/_timeviewer.py @@ -180,7 +180,7 @@ def __init__(self, brain): time_slider.name = "time_slider" # playback speed - default_playback_speed = 1 + default_playback_speed = 0.05 playback_speed_slider = self.plotter.add_slider_widget( self.set_playback_speed, value=default_playback_speed, @@ -191,6 +191,9 @@ def __init__(self, brain): # colormap slider scaling_limits = [0.2, 2.0] + pointa = np.array((0.82, 0.26)) + pointb = np.array((0.98, 0.26)) + shift = np.array([0, 0.08]) fmin = brain._data["fmin"] self.update_fmin = BumpColorbarPoints( plotter=self.plotter, @@ -200,9 +203,9 @@ def __init__(self, brain): fmin_slider = self.plotter.add_slider_widget( self.update_fmin, value=fmin, - rng=_get_range(brain), title="fmin", - pointa=(0.82, 0.26), - pointb=(0.98, 0.26), + rng=_get_range(brain), title="clim", + pointa=pointa, + pointb=pointb, event_type="always", ) fmin_slider.name = "fmin" @@ -215,9 +218,9 @@ def __init__(self, brain): fmid_slider = self.plotter.add_slider_widget( self.update_fmid, value=fmid, - rng=_get_range(brain), title="fmid", - pointa=(0.82, 0.42), - pointb=(0.98, 0.42), + rng=_get_range(brain), title="", + pointa=pointa + shift, + pointb=pointb + shift, event_type="always", ) fmid_slider.name = "fmid" @@ -230,9 +233,9 @@ def __init__(self, brain): fmax_slider = self.plotter.add_slider_widget( self.update_fmax, value=fmax, - rng=_get_range(brain), title="fmax", - pointa=(0.82, 0.58), - pointb=(0.98, 0.58), + rng=_get_range(brain), title="", + pointa=pointa + 2 * shift, + pointb=pointb + 2 * shift, event_type="always", ) fmax_slider.name = "fmax" @@ -251,11 +254,10 @@ def __init__(self, brain): # add toggle to start/stop playback self.playback = False - self.playback_speed = 1 + self.playback_speed = default_playback_speed self.time_elapsed = 0 - self.refresh_rate = 16 - self.refresh_rate_ms = self.refresh_rate / 1000. - self.plotter.add_callback(self.play, self.refresh_rate) + self.refresh_rate_ms = max(int(round(1000. / 60.)), 1) + self.plotter.add_callback(self.play, self.refresh_rate_ms) self.plotter.add_key_event('space', self.toggle_playback) # add toggle to show/hide interface @@ -285,6 +287,12 @@ def toggle_interface(self): def toggle_playback(self): self.playback = not self.playback + if self.playback: + time_data = self.brain._data['time'] + max_time = np.max(time_data) + if self.brain._current_time == max_time: # start over + self.brain.set_time_point(np.min(time_data)) + self._last_tick = time.time() def set_playback_speed(self, speed): self.playback_speed = speed @@ -292,21 +300,25 @@ def set_playback_speed(self, speed): def play(self): from scipy.interpolate import interp1d if self.playback: + this_time = time.time() + delta = this_time - self._last_tick + self._last_tick = time.time() time_data = self.brain._data['time'] times = np.arange(self.brain._n_times) - time_shift = self.refresh_rate_ms * self.playback_speed - time_point = self.brain._current_time + time_shift - if time_point < np.max(time_data): - ifunc = interp1d(time_data, times) - idx = ifunc(time_point) - self.brain.set_time_point(idx) - for slider in self.plotter.slider_widgets: - name = getattr(slider, "name", None) - if name == "time_slider": - slider_rep = slider.GetRepresentation() - slider_rep.SetValue(idx) - else: + time_shift = delta * self.playback_speed + max_time = np.max(time_data) + time_point = min(self.brain._current_time + time_shift, max_time) + ifunc = interp1d(time_data, times) + idx = ifunc(time_point) + self.brain.set_time_point(idx) + for slider in self.plotter.slider_widgets: + name = getattr(slider, "name", None) + if name == "time_slider": + slider_rep = slider.GetRepresentation() + slider_rep.SetValue(idx) + if time_point == max_time: self.playback = False + self.plotter.update() # critical for smooth animation def _set_slider_style(slider, show_label=True): From b3c36228fde669507208b4cede49db2b4a44a187 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 15 Jan 2020 16:58:46 +0100 Subject: [PATCH 13/13] Remove unused variable --- mne/viz/_brain/_timeviewer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mne/viz/_brain/_timeviewer.py b/mne/viz/_brain/_timeviewer.py index 0b95fdea8b1..d28abf52cf0 100644 --- a/mne/viz/_brain/_timeviewer.py +++ b/mne/viz/_brain/_timeviewer.py @@ -255,7 +255,6 @@ def __init__(self, brain): # add toggle to start/stop playback self.playback = False self.playback_speed = default_playback_speed - self.time_elapsed = 0 self.refresh_rate_ms = max(int(round(1000. / 60.)), 1) self.plotter.add_callback(self.play, self.refresh_rate_ms) self.plotter.add_key_event('space', self.toggle_playback)