From 024bd525252978357a17dc2bfca8c93076090e33 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 7 Apr 2020 14:20:34 +0200 Subject: [PATCH 1/8] Add imageio dependency --- README.rst | 1 + environment.yml | 1 + requirements.txt | 1 + 3 files changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 502a275b53c..d87f11f5682 100644 --- a/README.rst +++ b/README.rst @@ -96,6 +96,7 @@ 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.24 Contributing to MNE-Python diff --git a/environment.yml b/environment.yml index ff30191a485..31dde958205 100644 --- a/environment.yml +++ b/environment.yml @@ -30,6 +30,7 @@ dependencies: - pyface>=6 - traitsui>=6 - vtk +- imageio>=2.6.1 - pip: - mne - https://github.com/numpy/numpydoc/archive/master.zip diff --git a/requirements.txt b/requirements.txt index fc882f72a2d..c37077b2b1e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,5 +30,6 @@ xlrd pydocstyle flake8 https://github.com/mcmtroffaes/sphinxcontrib-bibtex/archive/29694f215b39d64a31b845aafd9ff2ae9329494f.zip +imageio>=2.6.1 pyvista>=0.24 tqdm From 3e8e9a33137a1f0a24226a9fb2c7a3bed369cb71 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 7 Apr 2020 14:31:22 +0200 Subject: [PATCH 2/8] Add save_movie feature in _Brain --- mne/viz/_brain/_brain.py | 116 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 03db2331d45..29cfd6b56ec 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1111,6 +1111,122 @@ def views(self): def hemis(self): return self._hemis + def save_movie(self, filename, time_dilation=4., tmin=None, tmax=None, + framerate=24, interpolation=None, codec=None, + bitrate=None, callback=None, **kwargs): + """Save a movie (for data with a time axis). + The movie is created through the :mod:`imageio` module. The format is + determined by the extension, and additional options can be specified + through keyword arguments that depend on the format. For available + formats and corresponding parameters see the imageio documentation: + http://imageio.readthedocs.io/en/latest/formats.html#multiple-images + .. Warning:: + This method assumes that time is specified in seconds when adding + data. If time is specified in milliseconds this will result in + movies 1000 times longer than expected. + Parameters + ---------- + filename : str + Path at which to save the movie. The extension determines the + format (e.g., `'*.mov'`, `'*.gif'`, ...; see the :mod:`imageio` + documenttion for available formats). + time_dilation : float + Factor by which to stretch time (default 4). For example, an epoch + from -100 to 600 ms lasts 700 ms. With ``time_dilation=4`` this + would result in a 2.8 s long movie. + tmin : float + First time point to include (default: all data). + tmax : float + Last time point to include (default: all data). + framerate : float + Framerate of the movie (frames per second, default 24). + %(brain_time_interpolation)s + If None, it uses the current ``brain.interpolation``, + which defaults to ``'nearest'``. Defaults to None. + callback : callable | None + A function to call on each iteration. Useful for status message + updates. It will be passed keyword arguments ``frame`` and + ``n_frames``. + **kwargs : + Specify additional options for :mod:`imageio`. + """ + import imageio + from math import floor + + # find imageio FFMPEG parameters + if 'fps' not in kwargs: + kwargs['fps'] = framerate + if codec is not None: + kwargs['codec'] = codec + if bitrate is not None: + kwargs['bitrate'] = bitrate + + # find tmin + if tmin is None: + tmin = self._times[0] + elif tmin < self._times[0]: + raise ValueError("tmin=%r is smaller than the first time point " + "(%r)" % (tmin, self._times[0])) + + # find indexes at which to create frames + if tmax is None: + tmax = self._times[-1] + elif tmax > self._times[-1]: + raise ValueError("tmax=%r is greater than the latest time point " + "(%r)" % (tmax, self._times[-1])) + n_frames = floor((tmax - tmin) * time_dilation * framerate) + times = np.arange(n_frames, dtype=float) + times /= framerate * time_dilation + times += tmin + time_idx = np.interp(times, self._times, np.arange(self._n_times)) + + n_times = len(time_idx) + if n_times == 0: + raise ValueError("No time points selected") + + logger.debug("Save movie for time points/samples\n%s\n%s" + % (times, time_idx)) + # Sometimes the first screenshot is rendered with a different + # resolution on OS X + self.screenshot() + old_mode = self.time_interpolation + if interpolation is not None: + self.set_time_interpolation(interpolation) + try: + images = [ + self.screenshot() for _ in self._iter_time(time_idx, callback)] + finally: + self.set_time_interpolation(old_mode) + if callback is not None: + callback(frame=len(time_idx), n_frames=len(time_idx)) + imageio.mimwrite(filename, images, **kwargs) + + def _iter_time(self, time_idx, callback): + """Iterate through time points, then reset to current time. + Parameters + ---------- + time_idx : array_like + Time point indexes through which to iterate. + callback : callable | None + Callback to call before yielding each frame. + Yields + ------ + idx : int | float + Current index. + Notes + ----- + Used by movie and image sequence saving functions. + """ + current_time_idx = self._data["time_idx"] + for ii, idx in enumerate(time_idx): + self.set_time_point(idx) + if callback is not None: + callback(frame=ii, n_frames=len(time_idx)) + yield idx + + # Restore original time index + self.set_time_point(current_time_idx) + def _show(self): """Request rendering of the window.""" try: From 5855f2e56bdd378233dbbef6001d2eff568c1a4a Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 7 Apr 2020 14:35:25 +0200 Subject: [PATCH 3/8] Update overview tables --- mne/viz/_brain/_brain.py | 12 +++++++----- mne/viz/backends/renderer.py | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 29cfd6b56ec..18f1106d58a 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -106,13 +106,13 @@ class _Brain(object): +---------------------------+--------------+-----------------------+ | 3D function: | surfer.Brain | mne.viz._brain._Brain | +===========================+==============+=======================+ - | add_data | ✓ | - | + | add_data | ✓ | ✓ | +---------------------------+--------------+-----------------------+ - | add_foci | ✓ | - | + | add_foci | ✓ | ✓ | +---------------------------+--------------+-----------------------+ - | add_label | ✓ | - | + | add_label | ✓ | ✓ | +---------------------------+--------------+-----------------------+ - | add_text | ✓ | - | + | add_text | ✓ | ✓ | +---------------------------+--------------+-----------------------+ | close | ✓ | ✓ | +---------------------------+--------------+-----------------------+ @@ -132,9 +132,11 @@ class _Brain(object): +---------------------------+--------------+-----------------------+ | save_image | ✓ | ✓ | +---------------------------+--------------+-----------------------+ + | save_movie | ✓ | ✓ | + +---------------------------+--------------+-----------------------+ | screenshot | ✓ | ✓ | +---------------------------+--------------+-----------------------+ - | show_view | ✓ | - | + | show_view | ✓ | ✓ | +---------------------------+--------------+-----------------------+ | TimeViewer | ✓ | ✓ | +---------------------------+--------------+-----------------------+ diff --git a/mne/viz/backends/renderer.py b/mne/viz/backends/renderer.py index 2d9a57fa588..81c50aad0b1 100644 --- a/mne/viz/backends/renderer.py +++ b/mne/viz/backends/renderer.py @@ -91,14 +91,14 @@ def set_3d_backend(backend_name, verbose=None): +--------------------------------------+--------+---------+ | Subplotting | ✓ | ✓ | +--------------------------------------+--------+---------+ + | Save offline movie | ✓ | ✓ | + +--------------------------------------+--------+---------+ | Point picking | | ✓ | +--------------------------------------+--------+---------+ | Linked cameras | | | +--------------------------------------+--------+---------+ | Eye-dome lighting | | | +--------------------------------------+--------+---------+ - | Exports to movie/GIF | | | - +--------------------------------------+--------+---------+ .. note:: In the case of `plot_vector_source_estimates` with PyVista, the glyph From 2a40c1fbb783c1bc754e7354c4a541d13266af12 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 7 Apr 2020 14:53:40 +0200 Subject: [PATCH 4/8] Add testing for save_movie --- mne/viz/_brain/tests/test_brain.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index cd64d9eb255..a4ce37910ae 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -164,6 +164,19 @@ def test_brain_add_text(renderer): brain.close() +@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(renderer_interactive): """Test _TimeViewer primitives.""" From b63aeffc6fc160d97054fe0a123e4bf2e4ebaef2 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 8 Apr 2020 11:53:26 +0200 Subject: [PATCH 5/8] Fix style --- mne/viz/_brain/_brain.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 18f1106d58a..592bef6762d 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1117,15 +1117,18 @@ def save_movie(self, filename, time_dilation=4., tmin=None, tmax=None, framerate=24, interpolation=None, codec=None, bitrate=None, callback=None, **kwargs): """Save a movie (for data with a time axis). + The movie is created through the :mod:`imageio` module. The format is determined by the extension, and additional options can be specified through keyword arguments that depend on the format. For available formats and corresponding parameters see the imageio documentation: http://imageio.readthedocs.io/en/latest/formats.html#multiple-images + .. Warning:: This method assumes that time is specified in seconds when adding data. If time is specified in milliseconds this will result in movies 1000 times longer than expected. + Parameters ---------- filename : str @@ -1205,16 +1208,19 @@ def save_movie(self, filename, time_dilation=4., tmin=None, tmax=None, def _iter_time(self, time_idx, callback): """Iterate through time points, then reset to current time. + Parameters ---------- time_idx : array_like Time point indexes through which to iterate. callback : callable | None Callback to call before yielding each frame. + Yields ------ idx : int | float Current index. + Notes ----- Used by movie and image sequence saving functions. From 0adffdce5c53a4a32b6944ce38c8e03f011e50c5 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 9 Apr 2020 11:43:55 +0200 Subject: [PATCH 6/8] TST: Trigger CIs From ac5f08ca2e36886b314689081ff0ce82cccf5b4d Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 10 Apr 2020 17:06:17 +0200 Subject: [PATCH 7/8] Update requirements --- environment.yml | 1 + mne/viz/_brain/tests/test_brain.py | 2 +- requirements.txt | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index 2432fe42707..2fc7b674f2d 100644 --- a/environment.yml +++ b/environment.yml @@ -34,6 +34,7 @@ dependencies: - pip: - mne - https://github.com/numpy/numpydoc/archive/master.zip + - imageio-ffmpeg>=0.4.1 - pyvista>=0.24 - mayavi - PySurfer[save_movie] diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index a4ce37910ae..a7e80ecb71e 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -170,7 +170,7 @@ def test_brain_save_movie(tmpdir, renderer): if renderer.get_3d_backend() == "mayavi": pytest.skip() brain_data = _create_testing_brain(hemi='lh') - filename = str(path.join(tmpdir, "brain_test.gif")) + filename = str(path.join(tmpdir, "brain_test.mov")) brain_data.save_movie(filename, time_dilation=1, interpolation='nearest') assert path.isfile(filename) diff --git a/requirements.txt b/requirements.txt index c37077b2b1e..fc87d61bbcc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,5 +31,6 @@ pydocstyle flake8 https://github.com/mcmtroffaes/sphinxcontrib-bibtex/archive/29694f215b39d64a31b845aafd9ff2ae9329494f.zip imageio>=2.6.1 +imageio-ffmpeg>=0.4.1 pyvista>=0.24 tqdm From 47c0bdb0c81f3627710db5c6863122d79030153f Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 13 Apr 2020 11:28:51 +0200 Subject: [PATCH 8/8] Update latest changes --- doc/changes/latest.inc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index 097c3621ba2..454fc203936 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -29,6 +29,8 @@ Changelog - Add function :func:`mne.preprocessing.annotate_muscle_zscore` to annotate periods with muscle artifacts. by `Adonay Nunes`_ +- Support for saving movies of source time courses (STCs) with ``brain.save_movie`` method by `Guillaume Favelier`_ + Bug ~~~