Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TST: Reproduce paintEvent failures #7294

Merged
merged 2 commits into from
Mar 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ For full functionality, some functions require:
- Picard >= 0.3
- CuPy >= 4.0 (for NVIDIA CUDA acceleration)
- DIPY >= 0.10.1
- Imageio >= 2.6.1
- PyVista >= 0.23.1

Contributing to MNE-Python
Expand Down
2 changes: 0 additions & 2 deletions doc/changes/latest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,6 @@ Changelog

- Expose the number of ICA iterations during the fitting procedure via the ``n_iter_`` attribute of :class:`mne.preprocessing.ICA` by `Richard Höchenberger`_

- Support for saving movies of source time courses (STCs) with ``brain.save_movie`` method and from graphical user interface by `Guillaume Favelier`_

- :func:`mne.grand_average` now produces a warning when only a single dataset was passed, instead of raising an error by `Richard Höchenberger`_

Bug
Expand Down
1 change: 0 additions & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ dependencies:
- traits>=4.6.0
- pyface>=6
- traitsui>=6
- imageio>=2.6.1
- vtk
- pip:
- mne
Expand Down
6 changes: 0 additions & 6 deletions mne/utils/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,12 +728,6 @@
If True, use a linear transparency between fmin and fmid.
None will choose automatically based on colormap type.
"""
docdict["brain_time_interpolation"] = """
interpolation : str | None
Interpolation method (:func:`scipy.interpolate.interp1d` parameter).
Must be one of 'linear', 'nearest', 'zero', 'slinear', 'quadratic',
or 'cubic'.
"""

# STC label time course
docdict['eltc_labels'] = """
Expand Down
242 changes: 44 additions & 198 deletions mne/viz/_brain/_brain.py

Large diffs are not rendered by default.

150 changes: 9 additions & 141 deletions mne/viz/_brain/_timeviewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,9 @@
# License: Simplified BSD

from itertools import cycle
from functools import partial
import os
import time
import traceback
import numpy as np

from . import _Brain
from ..utils import _check_option, _show_help, _get_color_list, tight_layout
from ...utils import warn, copy_doc
from ...source_space import vertex_to_mni


Expand Down Expand Up @@ -320,12 +314,8 @@ def __init__(self, brain, show_traces=False):
# Direct access parameters:
self.brain = brain
self.brain.time_viewer = self
self.brain._save_movie = self.brain.save_movie
self.brain.save_movie = self.save_movie
self.plotter = brain._renderer.plotter
self.main_menu = self.plotter.main_menu
self.window = self.plotter.app_window
self.status_bar = self.window.statusBar()
self.interactor = self.plotter
self.interactor.keyPressEvent = self.keyPressEvent

Expand All @@ -345,18 +335,14 @@ def __init__(self, brain, show_traces=False):
self.configure_playback()
self.configure_point_picking()
self.configure_menu()
self.configure_status_bar()

def keyPressEvent(self, event):
callback = self.key_bindings.get(event.text())
if callback is not None:
callback()

def toggle_interface(self, value=None):
if value is None:
self.visibility = not self.visibility
else:
self.visibility = value
def toggle_interface(self):
self.visibility = not self.visibility

# manage sliders
for slider in self.plotter.slider_widgets:
Expand All @@ -372,6 +358,7 @@ def toggle_interface(self, value=None):
if self.visibility:
self.time_actor.VisibilityOff()
else:
self.time_actor.SetInput(time_label(self.brain._current_time))
self.time_actor.VisibilityOn()

def apply_auto_scaling(self):
Expand All @@ -392,7 +379,7 @@ def toggle_playback(self):
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(0) # first index
self.brain.set_time_point(np.min(time_data))
self._last_tick = time.time()

def set_playback_speed(self, speed):
Expand All @@ -417,82 +404,6 @@ def play(self):
self.playback = False
self.plotter.update() # critical for smooth animation

def _save_movie(self, filename, **kwargs):
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QCursor

def frame_callback(frame, n_frames):
if frame == n_frames:
# On the ImageIO step
self.status_msg.setText(
_sanitize("💾 Saving with ImageIO: %s"
% filename)
)
self.status_msg.show()
self.status_progress.hide()
self.status_bar.layout().update()
else:
self.status_msg.setText(
_sanitize("📽 Rendering images (frame %d / %d) ..."
% (frame + 1, n_frames))
)
self.status_msg.show()
self.status_progress.show()
self.status_progress.setRange(0, n_frames - 1)
self.status_progress.setValue(frame)
self.status_progress.update()
self.status_progress.repaint()
self.status_msg.update()
self.status_msg.parent().update()
self.status_msg.repaint()

# temporarily hide interface
default_visibility = self.visibility
self.toggle_interface(value=False)
# set cursor to busy
default_cursor = self.interactor.cursor()
self.interactor.setCursor(QCursor(Qt.WaitCursor))

try:
self.brain._save_movie(
filename=filename,
time_dilation=(1. / self.playback_speed),
callback=frame_callback,
**kwargs
)
except (Exception, KeyboardInterrupt):
warn('Movie saving aborted:\n' + traceback.format_exc())

# restore visibility
self.toggle_interface(value=default_visibility)
# restore cursor
self.interactor.setCursor(default_cursor)

@copy_doc(_Brain.save_movie)
def save_movie(self, filename=None, **kwargs):
from pyvista.plotting.qt_plotting import FileDialog

if filename is None:
self.status_msg.setText(_sanitize("📁 Choose movie path ..."))
self.status_msg.show()
self.status_progress.setValue(0)

def _clean(unused):
del unused
self.status_msg.hide()
self.status_progress.hide()

dialog = FileDialog(
self.plotter.app_window,
callback=partial(self._save_movie, **kwargs)
)
dialog.setDirectory(os.getcwd())
dialog.finished.connect(_clean)
return dialog
else:
self._save_movie(filename=filename, **kwargs)
return

def set_slider_style(self, slider, show_label=True):
if slider is not None:
slider_rep = slider.GetRepresentation()
Expand Down Expand Up @@ -759,51 +670,19 @@ def configure_point_picking(self):
self.on_pick
)

def configure_status_bar(self):
from PyQt5.QtWidgets import QLabel, QProgressBar
self.status_msg = QLabel()
self.status_progress = QProgressBar()
self.status_bar.layout().addWidget(self.status_msg, 1)
self.status_bar.layout().addWidget(self.status_progress, 0)
self.status_msg.hide()
self.status_progress.hide()

# display help message for 3 seconds
self.status_bar.showMessage("Press ? for help", 3000)

def configure_menu(self):
main_menu = self.plotter.main_menu
file_menu = None

# add help menu
help_menu = main_menu.addMenu('Help')
help_menu.addAction('Show MNE key bindings', self.help, '?')

# remove default picking menu
to_remove = list()
for action in self.main_menu.actions():
if action.text() == "Tools":
to_remove.append(action)
elif action.text() == "File":
file_menu = action.menu()
for action in to_remove:
main_menu.removeAction(action)
to_remove.clear()

# order the file menu
if file_menu is not None:
for action in file_menu.actions():
if action.text() == "Take Screenshot":
movie_action = file_menu.addAction(
'Save movie...',
self.save_movie,
"ctrl+shift+s"
)
# insert at the right place
file_menu.insertAction(action, movie_action)
break
self.main_menu.removeAction(action)

# add help menu
menu = self.main_menu.addMenu('Help')
menu.addAction('Show MNE key bindings\t?', self.help)

def on_mouse_move(self, vtk_picker, event):
if self._mouse_no_mvt:
self._mouse_no_mvt -= 1
Expand Down Expand Up @@ -944,7 +823,6 @@ def help(self):
('s', 'Apply auto-scaling'),
('r', 'Restore original clim'),
('c', 'Clear all traces'),
('ctrl+shift+s', 'Save movie'),
('Space', 'Start/Pause playback'),
]
text1, text2 = zip(*pairs)
Expand Down Expand Up @@ -1010,19 +888,9 @@ def link_sliders(self, name, callback, event_type):


def _get_range(brain):
data = [brain._data.get(hemi, {}).get('array') for hemi in ('lh', 'rh')]
data = np.concatenate([d for d in data if d is not None])
val = np.abs(data)
val = np.abs(brain._data['array'])
return [np.min(val), np.max(val)]


def _normalize(point, shape):
return (point[0] / shape[1], point[1] / shape[0])


def _sanitize(text):
from PyQt5.Qt import PYQT_VERSION_STR
from distutils.version import LooseVersion
if LooseVersion(PYQT_VERSION_STR) < LooseVersion('5.12'):
text = text[2:]
return text
28 changes: 1 addition & 27 deletions mne/viz/_brain/tests/test_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,20 +160,7 @@ def test_brain_add_text(renderer):


@testing.requires_testing_data
def test_brain_save_movie(tmpdir, renderer):
"""Test saving a movie of a _Brain instance."""
if renderer.get_3d_backend() == "mayavi":
pytest.skip()
brain_data = _create_testing_brain(hemi='lh')
filename = str(path.join(tmpdir, "brain_test.gif"))
brain_data.save_movie(filename, time_dilation=1,
interpolation='nearest')
assert path.isfile(filename)
brain_data.close()


@testing.requires_testing_data
def test_brain_timeviewer(tmpdir, renderer_interactive):
def test_brain_timeviewer(renderer_interactive):
"""Test _TimeViewer primitives."""
brain_data = _create_testing_brain(hemi='both')

Expand All @@ -193,19 +180,6 @@ def test_brain_timeviewer(tmpdir, renderer_interactive):
time_viewer.apply_auto_scaling()
time_viewer.restore_user_scaling()

filename = str(path.join(tmpdir, "timeviewer_test.gif"))

def _check_result():
assert path.isfile(filename)

dialog = time_viewer.save_movie(
tmin=0.1,
tmax=0.15,
)
dialog.selectFile(filename)
dialog.accepted.connect(_check_result)
dialog.accept()


@testing.requires_testing_data
@pytest.mark.parametrize('hemi', ['lh', 'rh', 'split', 'both'])
Expand Down
4 changes: 2 additions & 2 deletions mne/viz/backends/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,14 @@ def set_3d_backend(backend_name, verbose=None):
+--------------------------------------+--------+---------+
| Subplotting | ✓ | ✓ |
+--------------------------------------+--------+---------+
| Save offline movie | ✓ | ✓ |
+--------------------------------------+--------+---------+
| Point picking | | ✓ |
+--------------------------------------+--------+---------+
| Linked cameras | | |
+--------------------------------------+--------+---------+
| Eye-dome lighting | | |
+--------------------------------------+--------+---------+
| Exports to movie/GIF | | |
+--------------------------------------+--------+---------+
"""
global MNE_3D_BACKEND
try:
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,4 @@ xlrd
pydocstyle
flake8
https://api.github.com/repos/mcmtroffaes/sphinxcontrib-bibtex/zipball/29694f215b39d64a31b845aafd9ff2ae9329494f
imageio>=2.6.1
pyvista>=0.23.1
4 changes: 1 addition & 3 deletions tutorials/source-modeling/plot_visualize_stc.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,7 @@
###############################################################################
#
# Note that here we used ``initial_time=0.1``, but we can also browse through
# time using ``time_viewer=True``. It's also possible to produce a movie by
# selecting in the menu: ``File > Save movie`` or alternatively with:
# brain.save_movie("movie.mp4")
# time using ``time_viewer=True``.
#
# In case ``mayavi`` is not available, we also offer a ``matplotlib``
# backend. Here we use verbose='error' to ignore a warning that not all
Expand Down