From d236c1cdb8bc66cfacb042e56595ef3eae89f301 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 7 Jan 2021 15:31:16 +0100 Subject: [PATCH 01/36] Add screenshot button --- mne/viz/_brain/_brain.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 142c0320192..1de5d182585 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1215,7 +1215,10 @@ def _save_movie_noname(self): return self.save_movie(None) def _screenshot(self): - if not self.notebook: + if self.notebook: + filename = self.actions.get("screenshot_field", "screenshot.png").value + self.plotter.screenshot(filename) + else: self.plotter._qt_screenshot() def _initialize_actions(self): @@ -1229,7 +1232,7 @@ def _add_action(self, name, desc, func, icon_name, qt_icon_name=None, if not notebook: return from ipywidgets import Button - self.actions[name] = Button(description=desc, icon=icon_name) + self.actions[name] = Button(tooltip=desc, icon=icon_name) self.actions[name].on_click(lambda x: func()) else: qt_icon_name = name if qt_icon_name is None else qt_icon_name @@ -1239,14 +1242,24 @@ def _add_action(self, name, desc, func, icon_name, qt_icon_name=None, func, ) + def _add_text(self, name, value, placeholder): + if not self.notebook: + return + from ipywidgets import Text + self.actions[name] = Text(value=value, placeholder=placeholder) + def _configure_tool_bar(self): self._initialize_actions() self._add_action( name="screenshot", desc="Take a screenshot", func=self._screenshot, - icon_name=None, - notebook=False, + icon_name="camera", + ) + self._add_text( + name="screenshot_field", + value="screenshot.png", + placeholder="Type file name", ) self._add_action( name="movie", From 95ab3ec11b1c0bd2f527db473879bace161fa01d Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 7 Jan 2021 15:34:51 +0100 Subject: [PATCH 02/36] Ensure valid filename --- 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 1de5d182585..2eb8f1d6e21 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1216,7 +1216,8 @@ def _save_movie_noname(self): def _screenshot(self): if self.notebook: - filename = self.actions.get("screenshot_field", "screenshot.png").value + filename = self.actions.get("screenshot_field").value + filename = "screenshot.png" if len(filename) == 0 else filename self.plotter.screenshot(filename) else: self.plotter._qt_screenshot() From 82076aa9ecda40585ecb960e94782b8e13daf035 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 7 Jan 2021 15:37:58 +0100 Subject: [PATCH 03/36] Make the name shorter --- mne/viz/_brain/_brain.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 2eb8f1d6e21..346a18f5e42 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -516,6 +516,7 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): "src": ["mean_flip", "pca_flip", "auto"], } self.default_trace_modes = ('vertex', 'label') + self.default_screenshot_name = "screenshot.png" self.annot = None self.label_extract_mode = None all_keys = ('lh', 'rh', 'vol') @@ -1216,9 +1217,9 @@ def _save_movie_noname(self): def _screenshot(self): if self.notebook: - filename = self.actions.get("screenshot_field").value - filename = "screenshot.png" if len(filename) == 0 else filename - self.plotter.screenshot(filename) + fname = self.actions.get("screenshot_field").value + fname = self.default_screenshot_name if len(fname) == 0 else fname + self.plotter.screenshot(fname) else: self.plotter._qt_screenshot() @@ -1259,7 +1260,7 @@ def _configure_tool_bar(self): ) self._add_text( name="screenshot_field", - value="screenshot.png", + value=self.default_screenshot_name, placeholder="Type file name", ) self._add_action( From 07be3c7bfd808c08725cb9f334a6dfd5bb1696f5 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 7 Jan 2021 16:02:05 +0100 Subject: [PATCH 04/36] Use Brain screenshot --- mne/viz/_brain/_brain.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 346a18f5e42..99866d14591 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1217,9 +1217,11 @@ def _save_movie_noname(self): def _screenshot(self): if self.notebook: + from PIL import Image fname = self.actions.get("screenshot_field").value fname = self.default_screenshot_name if len(fname) == 0 else fname - self.plotter.screenshot(fname) + img = self.screenshot(fname, time_viewer=True) + Image.fromarray(img).save(fname) else: self.plotter._qt_screenshot() From a5cbed032fec737151a2d45b26bdac2c979dcef2 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 8 Jan 2021 10:44:01 +0100 Subject: [PATCH 05/36] Click on all buttons --- mne/viz/_brain/tests/test.ipynb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mne/viz/_brain/tests/test.ipynb b/mne/viz/_brain/tests/test.ipynb index 80a8bec809e..8121d5ebfca 100644 --- a/mne/viz/_brain/tests/test.ipynb +++ b/mne/viz/_brain/tests/test.ipynb @@ -29,6 +29,7 @@ "source": [ "import os\n", "import mne\n", + "from ipywidgets import Button\n", "import matplotlib.pyplot as plt\n", "from mne.datasets import testing\n", "data_path = testing.data_path()\n", @@ -49,6 +50,9 @@ " assert brain.notebook\n", " assert brain._renderer.figure.display is not None\n", " brain._update()\n", + " for action in brain.actions.values():\n", + " if isinstance(action, Button):\n", + " action.click()\n", " brain.close()" ] }, From a284f43acaef8a6d583ad83942d4992f8559aff6 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 8 Jan 2021 11:08:19 +0100 Subject: [PATCH 06/36] Count the buttons too --- mne/viz/_brain/tests/test.ipynb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mne/viz/_brain/tests/test.ipynb b/mne/viz/_brain/tests/test.ipynb index 8121d5ebfca..3514bb78873 100644 --- a/mne/viz/_brain/tests/test.ipynb +++ b/mne/viz/_brain/tests/test.ipynb @@ -50,9 +50,13 @@ " assert brain.notebook\n", " assert brain._renderer.figure.display is not None\n", " brain._update()\n", + " total_number_of_buttons = len([k for k in brain.actions.keys() if '_field' not in k])\n", + " number_of_buttons = 0\n", " for action in brain.actions.values():\n", " if isinstance(action, Button):\n", " action.click()\n", + " number_of_buttons += 1\n", + " assert number_of_buttons == total_number_of_buttons\n", " brain.close()" ] }, From e3198f70012de7a7a3eb23433045010d117dd4bf Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 8 Jan 2021 11:44:09 +0100 Subject: [PATCH 07/36] Add a tool bar to the standard _Renderer --- mne/viz/_brain/_brain.py | 39 +++++++++++++++---------------- mne/viz/backends/_notebook.py | 43 +++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 21 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 99866d14591..86270ef81f8 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -594,6 +594,7 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): self._configure_picking() self._configure_tool_bar() if self.notebook: + self._renderer._set_tool_bar(state=False) self.show() self._configure_trace_mode() self.toggle_interface() @@ -1230,14 +1231,13 @@ def _initialize_actions(self): self._load_icons() self.tool_bar = self.window.addToolBar("toolbar") - def _add_action(self, name, desc, func, icon_name, qt_icon_name=None, + def _add_button(self, name, desc, func, icon_name, qt_icon_name=None, notebook=True): if self.notebook: if not notebook: return - from ipywidgets import Button - self.actions[name] = Button(tooltip=desc, icon=icon_name) - self.actions[name].on_click(lambda x: func()) + self.actions[name] = self._renderer._add_button( + desc, func, icon_name) else: qt_icon_name = name if qt_icon_name is None else qt_icon_name self.actions[name] = self.tool_bar.addAction( @@ -1246,71 +1246,71 @@ def _add_action(self, name, desc, func, icon_name, qt_icon_name=None, func, ) - def _add_text(self, name, value, placeholder): + def _add_text_field(self, name, value, placeholder): if not self.notebook: return - from ipywidgets import Text - self.actions[name] = Text(value=value, placeholder=placeholder) + self.actions[name] = self._renderer._add_text_field( + value, placeholder) def _configure_tool_bar(self): self._initialize_actions() - self._add_action( + self._add_button( name="screenshot", desc="Take a screenshot", func=self._screenshot, icon_name="camera", ) - self._add_text( + self._add_text_field( name="screenshot_field", value=self.default_screenshot_name, placeholder="Type file name", ) - self._add_action( + self._add_button( name="movie", desc="Save movie...", func=self._save_movie_noname, icon_name=None, notebook=False, ) - self._add_action( + self._add_button( name="visibility", desc="Toggle Visibility", func=self.toggle_interface, icon_name="eye", qt_icon_name="visibility_on", ) - self._add_action( + self._add_button( name="play", desc="Play/Pause", func=self.toggle_playback, icon_name=None, notebook=False, ) - self._add_action( + self._add_button( name="reset", desc="Reset", func=self.reset, icon_name="history", ) - self._add_action( + self._add_button( name="scale", desc="Auto-Scale", func=self.apply_auto_scaling, icon_name="magic", ) - self._add_action( + self._add_button( name="restore", desc="Restore scaling", func=self.restore_user_scaling, icon_name="reply", ) - self._add_action( + self._add_button( name="clear", desc="Clear traces", func=self.clear_glyphs, icon_name="trash", ) - self._add_action( + self._add_button( name="help", desc="Help", func=self.help, @@ -1319,10 +1319,7 @@ def _configure_tool_bar(self): ) if self.notebook: - from IPython import display - from ipywidgets import HBox - self.tool_bar = HBox(tuple(self.actions.values())) - display.display(self.tool_bar) + self.tool_bar = self._renderer._show_tool_bar(self.actions) else: # Qt shortcuts self.actions["movie"].setShortcut("ctrl+shift+s") diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index 761f0b8a60f..feca388e812 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -10,11 +10,54 @@ class _Renderer(_PyVistaRenderer): def __init__(self, *args, **kwargs): + self.default_screenshot_name = "screenshot.png" + self.tool_bar_state = True + self.tool_bar = None + self.actions = dict() kwargs["notebook"] = True super().__init__(*args, **kwargs) + def _screenshot(self): + fname = self.actions.get("screenshot_field").value + fname = self.default_screenshot_name if len(fname) == 0 else fname + self.screenshot(filename=fname) + + def _set_tool_bar(self, state): + self.tool_bar_state = state + + def _add_button(self, desc, func, icon_name): + from ipywidgets import Button + button = Button(tooltip=desc, icon=icon_name) + button.on_click(lambda x: func()) + return button + + def _add_text_field(self, value, placeholder): + from ipywidgets import Text + return Text(value=value, placeholder=placeholder) + + def _show_tool_bar(self, actions): + from IPython import display + from ipywidgets import HBox + tool_bar = HBox(tuple(actions.values())) + display.display(tool_bar) + return tool_bar + + def _configure_tool_bar(self): + self.actions["screenshot"] = self._add_button( + desc="Take a screenshot", + func=self._screenshot, + icon_name="camera", + ) + self.actions["screenshot_field"] = self._add_text_field( + value="screenshot.png", + placeholder="Type file name", + ) + self.tool_bar = self._show_tool_bar(self.actions) + def show(self): from IPython.display import display + if self.tool_bar_state: + self._configure_tool_bar() self.figure.display = self.plotter.show(use_ipyvtk=True, return_viewer=True) self.figure.display.layout.width = None # unlock the fixed layout From ed67a416273717d03794ec7699971800b05f1121 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 8 Jan 2021 11:51:04 +0100 Subject: [PATCH 08/36] Improve testing of standard _Renderer --- mne/viz/_brain/tests/test.ipynb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mne/viz/_brain/tests/test.ipynb b/mne/viz/_brain/tests/test.ipynb index 3514bb78873..c8cebc52b23 100644 --- a/mne/viz/_brain/tests/test.ipynb +++ b/mne/viz/_brain/tests/test.ipynb @@ -74,6 +74,13 @@ "mne.viz.set_3d_view(fig, 200, 70, focalpoint=[0, 0, 0])\n", "assert fig.display is None\n", "rend.show()\n", + "total_number_of_buttons = len([k for k in rend.actions.keys() if '_field' not in k])\n", + "number_of_buttons = 0\n", + "for action in rend.actions.values():\n", + " if isinstance(action, Button):\n", + " action.click()\n", + " number_of_buttons += 1\n", + "assert number_of_buttons == total_number_of_buttons\n", "assert fig.display is not None" ] } From 1a13ea73619cf87a0dc8a9248815c18216406160 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 8 Jan 2021 12:03:49 +0100 Subject: [PATCH 09/36] DRY a little bit --- mne/viz/_brain/_brain.py | 6 +++--- mne/viz/backends/_notebook.py | 3 +-- mne/viz/backends/_pyvista.py | 1 + 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 86270ef81f8..4af75157ab9 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -516,7 +516,6 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): "src": ["mean_flip", "pca_flip", "auto"], } self.default_trace_modes = ('vertex', 'label') - self.default_screenshot_name = "screenshot.png" self.annot = None self.label_extract_mode = None all_keys = ('lh', 'rh', 'vol') @@ -1220,7 +1219,8 @@ def _screenshot(self): if self.notebook: from PIL import Image fname = self.actions.get("screenshot_field").value - fname = self.default_screenshot_name if len(fname) == 0 else fname + fname = self._renderer.screenshot_filename \ + if len(fname) == 0 else fname img = self.screenshot(fname, time_viewer=True) Image.fromarray(img).save(fname) else: @@ -1262,7 +1262,7 @@ def _configure_tool_bar(self): ) self._add_text_field( name="screenshot_field", - value=self.default_screenshot_name, + value=self._renderer.screenshot_filename, placeholder="Type file name", ) self._add_button( diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index feca388e812..269f9751386 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -10,7 +10,6 @@ class _Renderer(_PyVistaRenderer): def __init__(self, *args, **kwargs): - self.default_screenshot_name = "screenshot.png" self.tool_bar_state = True self.tool_bar = None self.actions = dict() @@ -19,7 +18,7 @@ def __init__(self, *args, **kwargs): def _screenshot(self): fname = self.actions.get("screenshot_field").value - fname = self.default_screenshot_name if len(fname) == 0 else fname + fname = self.screenshot_filename if len(fname) == 0 else fname self.screenshot(filename=fname) def _set_tool_bar(self, state): diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 1b340d2a974..b904857537d 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -170,6 +170,7 @@ def __init__(self, fig=None, size=(600, 600), bgcolor='black', self.font_family = "arial" self.tube_n_sides = 20 self.shape = shape + self.screenshot_filename = "screenshot.png" antialias = _get_3d_option('antialias') self.antialias = antialias and not MNE_3D_BACKEND_TESTING if isinstance(fig, int): From 0fd9744ae4e2cd8cc2d7d04e06e9196c9741b40e Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 8 Jan 2021 16:49:47 +0100 Subject: [PATCH 10/36] Use concatenate_images --- mne/viz/__init__.py | 2 +- mne/viz/_brain/_brain.py | 3 ++- mne/viz/utils.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/mne/viz/__init__.py b/mne/viz/__init__.py index 9b54741afe7..a64bb4efdb9 100644 --- a/mne/viz/__init__.py +++ b/mne/viz/__init__.py @@ -6,7 +6,7 @@ from .topo import plot_topo_image_epochs, iter_topography from .utils import (tight_layout, mne_analyze_colormap, compare_fiff, ClickableImage, add_background_image, plot_sensors, - centers_to_edges) + centers_to_edges, concatenate_images) from ._3d import (plot_sparse_source_estimates, plot_source_estimates, plot_vector_source_estimates, plot_evoked_field, plot_dipole_locations, snapshot_brain_montage, diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 4af75157ab9..98420164880 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -2602,6 +2602,7 @@ def screenshot(self, mode='rgb', time_viewer=False): screenshot : array Image pixel values. """ + from ...viz import concatenate_images img = self._renderer.screenshot(mode) if time_viewer and self.time_viewer and \ self.show_traces and \ @@ -2633,7 +2634,7 @@ def screenshot(self, mode='rgb', time_viewer=False): if delta > 0: start = delta // 2 trace_img = trace_img[:, start:start + img.shape[1]] - img = np.concatenate([img, trace_img], axis=0) + img = concatenate_images([img, trace_img], bgcolor=self._bg_color) return img @fill_doc diff --git a/mne/viz/utils.py b/mne/viz/utils.py index fa07a911714..22a845448b5 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -2270,3 +2270,35 @@ def centers_to_edges(*arrays): arr[:-1] + arr_diff, [arr[-1] + arr_diff[-1]]])) return out + + +def concatenate_images(images, axis=0, bgcolor='black', centered=True): + from matplotlib.colors import colorConverter + if isinstance(bgcolor, str): + bgcolor = colorConverter.to_rgb(bgcolor) + bgcolor = np.asarray(bgcolor) * 255 + if not isinstance(images, list): + images = list(images) + if axis == 0: + ret_height = np.sum([image.shape[0] for image in images]) + ret_width = np.max([image.shape[1] for image in images]) + else: + ret_height = np.max([image.shape[0] for image in images]) + ret_width = np.sum([image.shape[1] for image in images]) + S = np.zeros((ret_height, ret_width, 3), dtype=np.uint8) + S[:, :, :] = bgcolor + if axis == 0: + h_p = 0 + for image in images: + h, w, _ = image.shape + d_width = (ret_width - w) // 2 if centered else 0 + S[h_p:h_p + h, d_width:w + d_width, :] = image + h_p += h + else: + w_p = 0 + for image in images: + h, w, _ = image.shape + d_height = (ret_height - h) // 2 if centered else 0 + S[d_height:h + d_height, w_p:w_p + w, :] = image + w_p += w + return S From 4dd9fc9f4810ba3d8e9c172e2bafab44033b675b Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 8 Jan 2021 17:36:17 +0100 Subject: [PATCH 11/36] Make it shorter and more complicated --- mne/viz/utils.py | 42 ++++++++++++++++-------------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/mne/viz/utils.py b/mne/viz/utils.py index 22a845448b5..f55466b33c2 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -2272,33 +2272,23 @@ def centers_to_edges(*arrays): return out -def concatenate_images(images, axis=0, bgcolor='black', centered=True): +def concatenate_images(images, axis=0, bgcolor='black'): from matplotlib.colors import colorConverter if isinstance(bgcolor, str): bgcolor = colorConverter.to_rgb(bgcolor) bgcolor = np.asarray(bgcolor) * 255 - if not isinstance(images, list): - images = list(images) - if axis == 0: - ret_height = np.sum([image.shape[0] for image in images]) - ret_width = np.max([image.shape[1] for image in images]) - else: - ret_height = np.max([image.shape[0] for image in images]) - ret_width = np.sum([image.shape[1] for image in images]) - S = np.zeros((ret_height, ret_width, 3), dtype=np.uint8) - S[:, :, :] = bgcolor - if axis == 0: - h_p = 0 - for image in images: - h, w, _ = image.shape - d_width = (ret_width - w) // 2 if centered else 0 - S[h_p:h_p + h, d_width:w + d_width, :] = image - h_p += h - else: - w_p = 0 - for image in images: - h, w, _ = image.shape - d_height = (ret_height - h) // 2 if centered else 0 - S[d_height:h + d_height, w_p:w_p + w, :] = image - w_p += w - return S + funcs = [np.sum, np.max] + ret_shape = np.asarray([ + funcs[axis]([image.shape[0] for image in images]), + funcs[1 - axis]([image.shape[1] for image in images]), + ]) + ret = np.zeros((ret_shape[0], ret_shape[1], 3), dtype=np.uint8) + ret[:, :, :] = bgcolor + ptr = np.array([0, 0]) + sec = np.array([0 == axis, 1 == axis]).astype(np.int) + for image in images: + shape = image.shape[:-1] + dec = ptr + ((ret_shape - shape) // 2) * (1 - sec) + ret[dec[0]:dec[0] + shape[0], dec[1]:dec[1] + shape[1], :] = image + ptr += shape * sec + return ret From 4073c1d1e0cd76a2e99cfb1762c91933349edc4b Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 8 Jan 2021 17:56:52 +0100 Subject: [PATCH 12/36] Fix style --- mne/viz/utils.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/mne/viz/utils.py b/mne/viz/utils.py index f55466b33c2..bf1c085a0fc 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -2273,6 +2273,23 @@ def centers_to_edges(*arrays): def concatenate_images(images, axis=0, bgcolor='black'): + """Concatenate a list of images. + + Parameters + ---------- + images : list of ndarray + The list of images to concatenate. + axis : 0 or 1 + The images are concatenated horizontally if 0 and vertically otherwise. + bgcolor : str | list + The color of the background. The name of the color is accepted + (e.g 'red') or a list of RGB values between 0 and 1. + + Returns + ------- + img : ndarray + The concatenated image. + """ from matplotlib.colors import colorConverter if isinstance(bgcolor, str): bgcolor = colorConverter.to_rgb(bgcolor) From 11663a24ec48fb63e5600c60ea03d9dd3717e8c2 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 8 Jan 2021 18:09:22 +0100 Subject: [PATCH 13/36] Add centered parameter --- mne/viz/utils.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/mne/viz/utils.py b/mne/viz/utils.py index bf1c085a0fc..b3740b4ff7d 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -2272,7 +2272,7 @@ def centers_to_edges(*arrays): return out -def concatenate_images(images, axis=0, bgcolor='black'): +def concatenate_images(images, axis=0, bgcolor='black', centered=True): """Concatenate a list of images. Parameters @@ -2281,9 +2281,13 @@ def concatenate_images(images, axis=0, bgcolor='black'): The list of images to concatenate. axis : 0 or 1 The images are concatenated horizontally if 0 and vertically otherwise. + The default orientation is horizontal. bgcolor : str | list The color of the background. The name of the color is accepted - (e.g 'red') or a list of RGB values between 0 and 1. + (e.g 'red') or a list of RGB values between 0 and 1. Defaults to + 'black'. + centered : bool + If True, the images are centered. Defaults to True. Returns ------- @@ -2305,7 +2309,8 @@ def concatenate_images(images, axis=0, bgcolor='black'): sec = np.array([0 == axis, 1 == axis]).astype(np.int) for image in images: shape = image.shape[:-1] - dec = ptr + ((ret_shape - shape) // 2) * (1 - sec) + dec = ptr + dec += ((ret_shape - shape) // 2) * (1 - sec) if centered else 0 ret[dec[0]:dec[0] + shape[0], dec[1]:dec[1] + shape[1], :] = image ptr += shape * sec return ret From 9101cb28e5e7c574f68479fdbc955e62eea9817d Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 14 Jan 2021 16:42:07 +0100 Subject: [PATCH 14/36] Comment slicing --- mne/viz/_brain/_brain.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 98420164880..45695b7df98 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -2630,10 +2630,10 @@ def screenshot(self, mode='rgb', time_viewer=False): canvas.renderer._renderer).take([0, 1, 2], axis=2) # need to slice into trace_img because generally it's a bit # smaller - delta = trace_img.shape[1] - img.shape[1] - if delta > 0: - start = delta // 2 - trace_img = trace_img[:, start:start + img.shape[1]] + # delta = trace_img.shape[1] - img.shape[1] + # if delta > 0: + # start = delta // 2 + # trace_img = trace_img[:, start:start + img.shape[1]] img = concatenate_images([img, trace_img], bgcolor=self._bg_color) return img From 12a729bf3ce02b88d494122272f589621edb5a21 Mon Sep 17 00:00:00 2001 From: Alexandre Gramfort Date: Fri, 15 Jan 2021 11:52:29 +0100 Subject: [PATCH 15/36] make it work on mac --- mne/viz/_brain/_brain.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 45695b7df98..970ef505a66 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -15,6 +15,7 @@ import time import traceback import warnings +from io import BytesIO import numpy as np from scipy import sparse @@ -2609,6 +2610,22 @@ def screenshot(self, mode='rgb', time_viewer=False): not self.separate_canvas: canvas = self.mpl_canvas.fig.canvas canvas.draw_idle() + + # Here we need to save to a buffer without taking into + # account the dpi to keep the size used on creation + # of the canvas which is used for the 3D renderer. + # For some reason doing + # canvas.get_width_height() overestimates the size + # of the image while doing fig.savefig('fname.png') + # returns the right number of pixels. + fig = self.mpl_canvas.fig + output = BytesIO() + fig.savefig(output, format='raw') + output.seek(0) + trace_img = np.reshape(np.frombuffer(output.getvalue(), + dtype=np.uint8), + newshape=(-1, img.shape[1], 4))[:, :, :3] + output.close() # In theory, one of these should work: # # trace_img = np.frombuffer( @@ -2626,14 +2643,18 @@ def screenshot(self, mode='rgb', time_viewer=False): # renderer tostring_rgb() size. So let's directly use what # matplotlib does in lib/matplotlib/backends/backend_agg.py # before calling tobytes(): - trace_img = np.asarray( - canvas.renderer._renderer).take([0, 1, 2], axis=2) + # trace_img = np.asarray( + # canvas.renderer._renderer).take([0, 1, 2], axis=2) # need to slice into trace_img because generally it's a bit # smaller # delta = trace_img.shape[1] - img.shape[1] # if delta > 0: # start = delta // 2 # trace_img = trace_img[:, start:start + img.shape[1]] + # if trace_img.shape[1] != img.shape[1]: + # scale = img.shape[1] / trace_img.shape[1] + # trace_img = ndimage.zoom(trace_img, (scale, scale, 1)) + # # import ipdb; ipdb.set_trace() img = concatenate_images([img, trace_img], bgcolor=self._bg_color) return img From e2667afce076f9b116d9a2d75be8ffbf91d54ce0 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 15 Jan 2021 14:56:07 +0100 Subject: [PATCH 16/36] Remove cruft --- mne/viz/_brain/_brain.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 970ef505a66..a488f39128a 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -2651,10 +2651,6 @@ def screenshot(self, mode='rgb', time_viewer=False): # if delta > 0: # start = delta // 2 # trace_img = trace_img[:, start:start + img.shape[1]] - # if trace_img.shape[1] != img.shape[1]: - # scale = img.shape[1] / trace_img.shape[1] - # trace_img = ndimage.zoom(trace_img, (scale, scale, 1)) - # # import ipdb; ipdb.set_trace() img = concatenate_images([img, trace_img], bgcolor=self._bg_color) return img From 395bda12709c28bfbc9408bb50fc2f06627d2cd7 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 15 Jan 2021 15:01:52 +0100 Subject: [PATCH 17/36] Update comments --- mne/viz/_brain/_brain.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index a488f39128a..4304124ad61 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -2643,10 +2643,13 @@ def screenshot(self, mode='rgb', time_viewer=False): # renderer tostring_rgb() size. So let's directly use what # matplotlib does in lib/matplotlib/backends/backend_agg.py # before calling tobytes(): + # # trace_img = np.asarray( # canvas.renderer._renderer).take([0, 1, 2], axis=2) + # # need to slice into trace_img because generally it's a bit - # smaller + # smaller: + # # delta = trace_img.shape[1] - img.shape[1] # if delta > 0: # start = delta // 2 From 9e18f6fadb4d163c1fdb51456acef5965b431c93 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 15 Jan 2021 15:12:32 +0100 Subject: [PATCH 18/36] Remove more comments --- mne/viz/_brain/_brain.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 4304124ad61..2a5beed731b 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -2626,34 +2626,6 @@ def screenshot(self, mode='rgb', time_viewer=False): dtype=np.uint8), newshape=(-1, img.shape[1], 4))[:, :, :3] output.close() - # In theory, one of these should work: - # - # trace_img = np.frombuffer( - # canvas.tostring_rgb(), dtype=np.uint8) - # trace_img.shape = canvas.get_width_height()[::-1] + (3,) - # - # or - # - # trace_img = np.frombuffer( - # canvas.tostring_rgb(), dtype=np.uint8) - # size = time_viewer.mpl_canvas.getSize() - # trace_img.shape = (size.height(), size.width(), 3) - # - # But in practice, sometimes the sizes does not match the - # renderer tostring_rgb() size. So let's directly use what - # matplotlib does in lib/matplotlib/backends/backend_agg.py - # before calling tobytes(): - # - # trace_img = np.asarray( - # canvas.renderer._renderer).take([0, 1, 2], axis=2) - # - # need to slice into trace_img because generally it's a bit - # smaller: - # - # delta = trace_img.shape[1] - img.shape[1] - # if delta > 0: - # start = delta // 2 - # trace_img = trace_img[:, start:start + img.shape[1]] img = concatenate_images([img, trace_img], bgcolor=self._bg_color) return img From 3c2f5c026cecdaf32cc01aeb8cecb46387ef8fc8 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 15 Jan 2021 15:52:10 +0100 Subject: [PATCH 19/36] Generate screenshot filename --- mne/viz/_brain/_brain.py | 6 +++--- mne/viz/backends/_notebook.py | 6 +++--- mne/viz/backends/_pyvista.py | 7 ++++++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 2a5beed731b..9af315f40ff 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1220,7 +1220,7 @@ def _screenshot(self): if self.notebook: from PIL import Image fname = self.actions.get("screenshot_field").value - fname = self._renderer.screenshot_filename \ + fname = self._renderer._get_screenshot_filename() \ if len(fname) == 0 else fname img = self.screenshot(fname, time_viewer=True) Image.fromarray(img).save(fname) @@ -1263,8 +1263,8 @@ def _configure_tool_bar(self): ) self._add_text_field( name="screenshot_field", - value=self._renderer.screenshot_filename, - placeholder="Type file name", + value=None, + placeholder="Type a file name", ) self._add_button( name="movie", diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index 269f9751386..e8bda5436d0 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -18,7 +18,7 @@ def __init__(self, *args, **kwargs): def _screenshot(self): fname = self.actions.get("screenshot_field").value - fname = self.screenshot_filename if len(fname) == 0 else fname + fname = self._get_screenshot_filename() if len(fname) == 0 else fname self.screenshot(filename=fname) def _set_tool_bar(self, state): @@ -48,8 +48,8 @@ def _configure_tool_bar(self): icon_name="camera", ) self.actions["screenshot_field"] = self._add_text_field( - value="screenshot.png", - placeholder="Type file name", + value=None, + placeholder="Type a file name", ) self.tool_bar = self._show_tool_bar(self.actions) diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index b904857537d..df43e3071df 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -170,7 +170,6 @@ def __init__(self, fig=None, size=(600, 600), bgcolor='black', self.font_family = "arial" self.tube_n_sides = 20 self.shape = shape - self.screenshot_filename = "screenshot.png" antialias = _get_3d_option('antialias') self.antialias = antialias and not MNE_3D_BACKEND_TESTING if isinstance(fig, int): @@ -213,6 +212,12 @@ def __init__(self, fig=None, size=(600, 600), bgcolor='black', self.update_lighting() + def _get_screenshot_filename(self): + from datetime import datetime + now = datetime.now() + dt_string = now.strftime("_%Y-%m-%d_%H-%M-%S") + return "MNE" + dt_string + ".png" + @contextmanager def ensure_minimum_sizes(self): sz = self.figure.store['window_size'] From a22f5b3ce135c82c3973c76eb567f96a8d493d0d Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 18 Jan 2021 11:38:10 +0100 Subject: [PATCH 20/36] Start over and test --- mne/viz/_brain/_brain.py | 45 +++++++++++++++++------------- mne/viz/_brain/tests/test_brain.py | 14 ++++++++++ 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 9af315f40ff..5787b82196b 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -15,7 +15,6 @@ import time import traceback import warnings -from io import BytesIO import numpy as np from scipy import sparse @@ -2603,30 +2602,38 @@ def screenshot(self, mode='rgb', time_viewer=False): screenshot : array Image pixel values. """ - from ...viz import concatenate_images img = self._renderer.screenshot(mode) if time_viewer and self.time_viewer and \ self.show_traces and \ not self.separate_canvas: canvas = self.mpl_canvas.fig.canvas canvas.draw_idle() - - # Here we need to save to a buffer without taking into - # account the dpi to keep the size used on creation - # of the canvas which is used for the 3D renderer. - # For some reason doing - # canvas.get_width_height() overestimates the size - # of the image while doing fig.savefig('fname.png') - # returns the right number of pixels. - fig = self.mpl_canvas.fig - output = BytesIO() - fig.savefig(output, format='raw') - output.seek(0) - trace_img = np.reshape(np.frombuffer(output.getvalue(), - dtype=np.uint8), - newshape=(-1, img.shape[1], 4))[:, :, :3] - output.close() - img = concatenate_images([img, trace_img], bgcolor=self._bg_color) + # In theory, one of these should work: + # + # trace_img = np.frombuffer( + # canvas.tostring_rgb(), dtype=np.uint8) + # trace_img.shape = canvas.get_width_height()[::-1] + (3,) + # + # or + # + # trace_img = np.frombuffer( + # canvas.tostring_rgb(), dtype=np.uint8) + # size = time_viewer.mpl_canvas.getSize() + # trace_img.shape = (size.height(), size.width(), 3) + # + # But in practice, sometimes the sizes does not match the + # renderer tostring_rgb() size. So let's directly use what + # matplotlib does in lib/matplotlib/backends/backend_agg.py + # before calling tobytes(): + trace_img = np.asarray( + canvas.renderer._renderer).take([0, 1, 2], axis=2) + # need to slice into trace_img because generally it's a bit + # smaller + delta = trace_img.shape[1] - img.shape[1] + if delta > 0: + start = delta // 2 + trace_img = trace_img[:, start:start + img.shape[1]] + img = np.concatenate([img, trace_img], axis=0) return img @fill_doc diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 6a0897bf12b..56cde46428e 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -369,6 +369,20 @@ def test_brain_save_movie(tmpdir, renderer, brain_gc): brain.close() +@pytest.mark.parametrize('time_viewer', [True, False]) +@testing.requires_testing_data +@pytest.mark.slowtest +def test_brain_screenshot(renderer_interactive, time_viewer, brain_gc): + if renderer_interactive._get_3d_backend() != 'pyvista': + pytest.skip('TimeViewer tests only supported on PyVista') + brain = _create_testing_brain( + hemi='both', surf='white', initial_time=0, + volume_options=None, # for speed, don't upsample + ) + brain.screenshot(time_viewer=time_viewer) + brain.close() + + @testing.requires_testing_data @pytest.mark.slowtest def test_brain_time_viewer(renderer_interactive, pixel_ratio, brain_gc): From a52f8e930617aaabbf5d81e7074b2525f0d0c294 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 18 Jan 2021 11:44:42 +0100 Subject: [PATCH 21/36] Test both qt and notebook --- mne/viz/_brain/tests/test.ipynb | 2 ++ mne/viz/_brain/tests/test_brain.py | 6 ++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mne/viz/_brain/tests/test.ipynb b/mne/viz/_brain/tests/test.ipynb index c8cebc52b23..ae95f8d619e 100644 --- a/mne/viz/_brain/tests/test.ipynb +++ b/mne/viz/_brain/tests/test.ipynb @@ -45,6 +45,7 @@ " brain = stc.plot(subjects_dir=subjects_dir, initial_time=initial_time,\n", " clim=dict(kind='value', pos_lims=[3, 6, 9]),\n", " time_viewer=True,\n", + " show_traces=True,\n", " hemi='split')\n", " assert isinstance(brain, brain_class)\n", " assert brain.notebook\n", @@ -57,6 +58,7 @@ " action.click()\n", " number_of_buttons += 1\n", " assert number_of_buttons == total_number_of_buttons\n", + " brain.screenshot(time_viewer=True)\n", " brain.close()" ] }, diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 56cde46428e..5c6a8081779 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -373,12 +373,10 @@ def test_brain_save_movie(tmpdir, renderer, brain_gc): @testing.requires_testing_data @pytest.mark.slowtest def test_brain_screenshot(renderer_interactive, time_viewer, brain_gc): + """Test time viewer screenshot.""" if renderer_interactive._get_3d_backend() != 'pyvista': pytest.skip('TimeViewer tests only supported on PyVista') - brain = _create_testing_brain( - hemi='both', surf='white', initial_time=0, - volume_options=None, # for speed, don't upsample - ) + brain = _create_testing_brain(hemi='both') brain.screenshot(time_viewer=time_viewer) brain.close() From 69069d73d9c2e005e1995773640ad7592c70e72f Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 18 Jan 2021 16:19:05 +0100 Subject: [PATCH 22/36] The pragmatic approach --- mne/viz/_brain/_brain.py | 56 ++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 5787b82196b..e4a5ed7da3d 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -2608,25 +2608,43 @@ def screenshot(self, mode='rgb', time_viewer=False): not self.separate_canvas: canvas = self.mpl_canvas.fig.canvas canvas.draw_idle() - # In theory, one of these should work: - # - # trace_img = np.frombuffer( - # canvas.tostring_rgb(), dtype=np.uint8) - # trace_img.shape = canvas.get_width_height()[::-1] + (3,) - # - # or - # - # trace_img = np.frombuffer( - # canvas.tostring_rgb(), dtype=np.uint8) - # size = time_viewer.mpl_canvas.getSize() - # trace_img.shape = (size.height(), size.width(), 3) - # - # But in practice, sometimes the sizes does not match the - # renderer tostring_rgb() size. So let's directly use what - # matplotlib does in lib/matplotlib/backends/backend_agg.py - # before calling tobytes(): - trace_img = np.asarray( - canvas.renderer._renderer).take([0, 1, 2], axis=2) + if self.notebook: + from io import BytesIO + # Here we need to save to a buffer without taking into + # account the dpi to keep the size used on creation + # of the canvas which is used for the 3D renderer. + # For some reason doing + # canvas.get_width_height() overestimates the size + # of the image while doing fig.savefig('fname.png') + # returns the right number of pixels. + fig = self.mpl_canvas.fig + output = BytesIO() + fig.savefig(output, format='raw') + output.seek(0) + trace_img = np.reshape( + np.frombuffer(output.getvalue(), dtype=np.uint8), + newshape=(-1, img.shape[1], 4))[:, :, :3] + output.close() + else: + # In theory, one of these should work: + # + # trace_img = np.frombuffer( + # canvas.tostring_rgb(), dtype=np.uint8) + # trace_img.shape = canvas.get_width_height()[::-1] + (3,) + # + # or + # + # trace_img = np.frombuffer( + # canvas.tostring_rgb(), dtype=np.uint8) + # size = time_viewer.mpl_canvas.getSize() + # trace_img.shape = (size.height(), size.width(), 3) + # + # But in practice, sometimes the sizes does not match the + # renderer tostring_rgb() size. So let's directly use what + # matplotlib does in lib/matplotlib/backends/backend_agg.py + # before calling tobytes(): + trace_img = np.asarray( + canvas.renderer._renderer).take([0, 1, 2], axis=2) # need to slice into trace_img because generally it's a bit # smaller delta = trace_img.shape[1] - img.shape[1] From ab544b725fb0abcb45953a0a707629bb07aaf016 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 18 Jan 2021 16:25:04 +0100 Subject: [PATCH 23/36] Improve testing --- mne/viz/_brain/tests/test.ipynb | 4 +++- mne/viz/_brain/tests/test_brain.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/mne/viz/_brain/tests/test.ipynb b/mne/viz/_brain/tests/test.ipynb index ae95f8d619e..415d11b5748 100644 --- a/mne/viz/_brain/tests/test.ipynb +++ b/mne/viz/_brain/tests/test.ipynb @@ -58,7 +58,9 @@ " action.click()\n", " number_of_buttons += 1\n", " assert number_of_buttons == total_number_of_buttons\n", - " brain.screenshot(time_viewer=True)\n", + " img1 = brain.screenshot(time_viewer=False)\n", + " img2 = brain.screenshot(time_viewer=True)\n", + " assert img1.shape[0] < img2.shape[0]\n", " brain.close()" ] }, diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 5c6a8081779..618f5de4ba1 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -369,7 +369,6 @@ def test_brain_save_movie(tmpdir, renderer, brain_gc): brain.close() -@pytest.mark.parametrize('time_viewer', [True, False]) @testing.requires_testing_data @pytest.mark.slowtest def test_brain_screenshot(renderer_interactive, time_viewer, brain_gc): @@ -377,7 +376,9 @@ def test_brain_screenshot(renderer_interactive, time_viewer, brain_gc): if renderer_interactive._get_3d_backend() != 'pyvista': pytest.skip('TimeViewer tests only supported on PyVista') brain = _create_testing_brain(hemi='both') - brain.screenshot(time_viewer=time_viewer) + img1 = brain.screenshot(time_viewer=False) + img2 = brain.screenshot(time_viewer=True) + assert img1.shape[0] < img2.shape[0] brain.close() From d22c1c3eb21ef35c4636aeb3fc407ae82ed43e68 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 18 Jan 2021 18:30:23 +0100 Subject: [PATCH 24/36] Fix test --- mne/viz/_brain/tests/test_brain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 618f5de4ba1..109bb848dc8 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -371,7 +371,7 @@ def test_brain_save_movie(tmpdir, renderer, brain_gc): @testing.requires_testing_data @pytest.mark.slowtest -def test_brain_screenshot(renderer_interactive, time_viewer, brain_gc): +def test_brain_screenshot(renderer_interactive, brain_gc): """Test time viewer screenshot.""" if renderer_interactive._get_3d_backend() != 'pyvista': pytest.skip('TimeViewer tests only supported on PyVista') From 7456b410d1252b56630a1c243f85784201aa958a Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 19 Jan 2021 13:11:58 -0500 Subject: [PATCH 25/36] ENH: Faster test --- mne/datasets/utils.py | 2 +- mne/label.py | 4 ++- mne/viz/_brain/_brain.py | 29 +++++++++++-------- mne/viz/_brain/tests/test_brain.py | 45 ++++++++++++++++++++++++------ 4 files changed, 58 insertions(+), 22 deletions(-) diff --git a/mne/datasets/utils.py b/mne/datasets/utils.py index d796d0d0898..d0e68e06129 100644 --- a/mne/datasets/utils.py +++ b/mne/datasets/utils.py @@ -384,7 +384,7 @@ def _data_path(path=None, force_update=False, update_path=True, download=True, want_version = _FAKE_VERSION if name == 'fake' else want_version if not need_download and want_version is not None: data_version = _dataset_version(folder_path[0], name) - need_download = data_version != want_version + need_download = LooseVersion(data_version) < LooseVersion(want_version) if need_download: logger.info(f'Dataset {name} version {data_version} out of date, ' f'latest version is {want_version}') diff --git a/mne/label.py b/mne/label.py index e0e6a8ccddd..e596b68c7b3 100644 --- a/mne/label.py +++ b/mne/label.py @@ -1874,9 +1874,11 @@ def _cortex_parcellation(subject, n_parcel, hemis, vertices_, graphs, return labels -def _read_annot_cands(dir_name): +def _read_annot_cands(dir_name, raise_error=True): """List the candidate parcellations.""" if not op.isdir(dir_name): + if not raise_error: + return list() raise IOError('Directory for annotation does not exist: %s', dir_name) cands = os.listdir(dir_name) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index e4a5ed7da3d..1d2d1691c1e 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1146,15 +1146,16 @@ def _set_annot(annot): from PyQt5.QtWidgets import QComboBox, QLabel dir_name = op.join(self._subjects_dir, self._subject_id, 'label') - cands = _read_annot_cands(dir_name) + cands = _read_annot_cands(dir_name, raise_error=False) self.tool_bar.addSeparator() self.tool_bar.addWidget(QLabel("Annotation")) self._annot_cands_widget = QComboBox() self.tool_bar.addWidget(self._annot_cands_widget) - self._annot_cands_widget.addItem('None') + cands = ['None'] + cands for cand in cands: self._annot_cands_widget.addItem(cand) self.annot = cands[0] + del cands # setup label extraction parameters def _set_label_mode(mode): @@ -1607,6 +1608,7 @@ def plot_time_course(self, hemi, vertex_id, color): if self.mpl_canvas is None: return time = self._data['time'].copy() # avoid circular ref + mni = None if hemi == 'vol': hemi_str = 'V' xfm = read_talxfm( @@ -1619,15 +1621,20 @@ def plot_time_course(self, hemi, vertex_id, color): mni = apply_trans(np.dot(xfm['trans'], src_mri_t), ijk) else: hemi_str = 'L' if hemi == 'lh' else 'R' - mni = vertex_to_mni( - vertices=vertex_id, - hemis=0 if hemi == 'lh' else 1, - subject=self._subject_id, - subjects_dir=self._subjects_dir - ) - label = "{}:{} MNI: {}".format( - hemi_str, str(vertex_id).ljust(6), - ', '.join('%5.1f' % m for m in mni)) + try: + mni = vertex_to_mni( + vertices=vertex_id, + hemis=0 if hemi == 'lh' else 1, + subject=self._subject_id, + subjects_dir=self._subjects_dir + ) + except Exception: + mni = None + if mni is not None: + mni = ' MNI: ' + ', '.join('%5.1f' % m for m in mni) + else: + mni = '' + label = "{}:{}{}".format(hemi_str, str(vertex_id).ljust(6), mni) act_data, smooth = self.act_data_smooth[hemi] if smooth is not None: act_data = smooth[vertex_id].dot(act_data)[0] diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 109bb848dc8..827fce98220 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -17,7 +17,7 @@ from mne import (read_source_estimate, read_evokeds, read_cov, read_forward_solution, pick_types_forward, - SourceEstimate, MixedSourceEstimate, + SourceEstimate, MixedSourceEstimate, write_surface, VolSourceEstimate) from mne.minimum_norm import apply_inverse, make_inverse_operator from mne.source_space import (read_source_spaces, vertex_to_mni, @@ -369,17 +369,44 @@ def test_brain_save_movie(tmpdir, renderer, brain_gc): brain.close() -@testing.requires_testing_data -@pytest.mark.slowtest -def test_brain_screenshot(renderer_interactive, brain_gc): - """Test time viewer screenshot.""" +@pytest.fixture() +def tiny(tmpdir, request): + """Create a tiny fake brain.""" + renderer_interactive = request.getfixturevalue('renderer_interactive') if renderer_interactive._get_3d_backend() != 'pyvista': pytest.skip('TimeViewer tests only supported on PyVista') - brain = _create_testing_brain(hemi='both') - img1 = brain.screenshot(time_viewer=False) - img2 = brain.screenshot(time_viewer=True) + # This is a minimal version of what we need for our viz-with-timeviewer + # support currently + subject = 'test' + subject_dir = tmpdir.mkdir(subject) + surf_dir = subject_dir.mkdir('surf') + rng = np.random.RandomState(0) + rr = rng.randn(4, 3) + tris = np.array([[0, 1, 2], [2, 1, 3]]) + curv = rng.randn(len(rr)) + with open(surf_dir.join('lh.curv'), 'wb') as fid: + fid.write(np.array([255, 255, 255], dtype=np.uint8)) + fid.write(np.array([len(rr), 0, 1], dtype='>i4')) + fid.write(curv.astype('>f4')) + write_surface(surf_dir.join('lh.white'), rr, tris) + write_surface(surf_dir.join('rh.white'), rr, tris) # needed for vertex tc + vertices = [np.arange(len(rr)), []] + data = rng.randn(len(rr), 10) + stc = SourceEstimate(data, vertices, 0, 1, subject) + brain = stc.plot(subjects_dir=tmpdir, hemi='lh', surface='white', + size=300) + ratio = brain.mpl_canvas.canvas.window().devicePixelRatio() + return brain, ratio + + +def test_brain_screenshot(renderer_interactive, brain_gc, tiny): + """Test time viewer screenshot.""" + tiny_brain, ratio = tiny + img1 = tiny_brain.screenshot(time_viewer=False) + img2 = tiny_brain.screenshot(time_viewer=True) assert img1.shape[0] < img2.shape[0] - brain.close() + assert img1.shape[1] == 300 * ratio + tiny_brain.close() @testing.requires_testing_data From 031d4dc107c2bfeb8e001f5cf93de0eae397fb5e Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 19 Jan 2021 13:45:24 -0500 Subject: [PATCH 26/36] BUG: More explicit height --- mne/viz/_brain/tests/test_brain.py | 10 ++++++---- mne/viz/backends/_pyvista.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 827fce98220..3af1e66d038 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -402,10 +402,12 @@ def tiny(tmpdir, request): def test_brain_screenshot(renderer_interactive, brain_gc, tiny): """Test time viewer screenshot.""" tiny_brain, ratio = tiny - img1 = tiny_brain.screenshot(time_viewer=False) - img2 = tiny_brain.screenshot(time_viewer=True) - assert img1.shape[0] < img2.shape[0] - assert img1.shape[1] == 300 * ratio + img_nv = tiny_brain.screenshot(time_viewer=False) + want = (300 * ratio, 300 * ratio, 3) + assert img_nv.shape == want + img_v = tiny_brain.screenshot(time_viewer=True) + assert img_v.shape[1:] == want[1:] + assert_allclose(img_v.shape[0], want[0] * 4 / 3, atol=3) # some slop tiny_brain.close() diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index df43e3071df..2026ec0e4e7 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -12,6 +12,7 @@ # License: Simplified BSD from contextlib import contextmanager +from datetime import datetime from distutils.version import LooseVersion import os import sys @@ -213,7 +214,6 @@ def __init__(self, fig=None, size=(600, 600), bgcolor='black', self.update_lighting() def _get_screenshot_filename(self): - from datetime import datetime now = datetime.now() dt_string = now.strftime("_%Y-%m-%d_%H-%M-%S") return "MNE" + dt_string + ".png" From 54fe4383747a059c1ee58ceef36f3ec015664e9d Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 20 Jan 2021 10:41:53 +0100 Subject: [PATCH 27/36] Fix dangling objects issue --- mne/viz/_brain/tests/test_brain.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 3af1e66d038..2e3aaac52ac 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -369,12 +369,8 @@ def test_brain_save_movie(tmpdir, renderer, brain_gc): brain.close() -@pytest.fixture() -def tiny(tmpdir, request): +def tiny(tmpdir): """Create a tiny fake brain.""" - renderer_interactive = request.getfixturevalue('renderer_interactive') - if renderer_interactive._get_3d_backend() != 'pyvista': - pytest.skip('TimeViewer tests only supported on PyVista') # This is a minimal version of what we need for our viz-with-timeviewer # support currently subject = 'test' @@ -399,9 +395,11 @@ def tiny(tmpdir, request): return brain, ratio -def test_brain_screenshot(renderer_interactive, brain_gc, tiny): +def test_brain_screenshot(renderer_interactive, tmpdir, brain_gc): """Test time viewer screenshot.""" - tiny_brain, ratio = tiny + if renderer_interactive._get_3d_backend() != 'pyvista': + pytest.skip('TimeViewer tests only supported on PyVista') + tiny_brain, ratio = tiny(tmpdir) img_nv = tiny_brain.screenshot(time_viewer=False) want = (300 * ratio, 300 * ratio, 3) assert img_nv.shape == want From 972b8a8967c062d0279e88cfac3a165379214fd0 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 20 Jan 2021 12:10:28 +0100 Subject: [PATCH 28/36] Change order --- 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 1d2d1691c1e..385febac7b8 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1151,7 +1151,7 @@ def _set_annot(annot): self.tool_bar.addWidget(QLabel("Annotation")) self._annot_cands_widget = QComboBox() self.tool_bar.addWidget(self._annot_cands_widget) - cands = ['None'] + cands + cands = cands + ['None'] for cand in cands: self._annot_cands_widget.addItem(cand) self.annot = cands[0] From 930fdcfff8fe0aa78d22535343feff65ba098f82 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 20 Jan 2021 15:58:55 +0100 Subject: [PATCH 29/36] Try #8082 --- mne/viz/_brain/_brain.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 385febac7b8..648a7f2679e 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -656,9 +656,20 @@ def ensure_minimum_sizes(self): yield finally: self.splitter.setSizes([sz[1], mpl_h]) + # 1. Process events _process_events(self.plotter) _process_events(self.plotter) - self.mpl_canvas.canvas.setMinimumSize(0, 0) + # 2. Get the window size that accommodates the size + sz = self.plotter.app_window.size() + # 3. Call app_window.setBaseSize and resize (in pyvistaqt) + self.plotter.window_size = (sz.width(), sz.height()) + # 4. Undo the min size setting and process events + self.plotter.interactor.setMinimumSize(0, 0) + _process_events(self.plotter) + _process_events(self.plotter) + # 5. Resize the window (again!) to the correct size + # (not sure why, but this is required on macOS at least) + self.plotter.window_size = (sz.width(), sz.height()) _process_events(self.plotter) _process_events(self.plotter) # sizes could change, update views From 3c0b083d367970177d18dd842c351a1b79d578a4 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 20 Jan 2021 12:43:28 -0500 Subject: [PATCH 30/36] FIX: Fix sizing --- mne/viz/backends/_pyvista.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 2026ec0e4e7..51e2492777d 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -233,17 +233,17 @@ def ensure_minimum_sizes(self): # 1. Process events _process_events(self.plotter) _process_events(self.plotter) - # 2. Get the window size that accommodates the size - sz = self.plotter.app_window.size() - # 3. Call app_window.setBaseSize and resize (in pyvistaqt) - self.plotter.window_size = (sz.width(), sz.height()) - # 4. Undo the min size setting and process events + # 2. Get the window and interactor sizes that work + win_sz = self.plotter.app_window.size() + ren_sz = self.plotter.interactor.size() + # 3. Undo the min size setting and process events self.plotter.interactor.setMinimumSize(0, 0) _process_events(self.plotter) _process_events(self.plotter) - # 5. Resize the window (again!) to the correct size + # 4. Resize the window and interactor to the correct size # (not sure why, but this is required on macOS at least) - self.plotter.window_size = (sz.width(), sz.height()) + self.plotter.window_size = (win_sz.width(), win_sz.height()) + self.plotter.interactor.resize(ren_sz.width(), ren_sz.height()) _process_events(self.plotter) _process_events(self.plotter) From 2c1754779864fd1c9a5fca4fb3405f5689adef79 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 20 Jan 2021 14:52:47 -0500 Subject: [PATCH 31/36] FIX: Use concatenate_images --- mne/viz/_brain/_brain.py | 50 ++++++++-------------------------------- 1 file changed, 9 insertions(+), 41 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 648a7f2679e..d304c63bf22 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -9,6 +9,7 @@ import contextlib from functools import partial +from io import BytesIO import os import os.path as op import sys @@ -27,7 +28,7 @@ from .callback import (ShowView, IntSlider, TimeSlider, SmartSlider, BumpColorbarPoints, UpdateColorbarScale) -from ..utils import _show_help, _get_color_list +from ..utils import _show_help, _get_color_list, concatenate_images from .._3d import _process_clim, _handle_time, _check_views from ...externals.decorator import decorator @@ -2626,50 +2627,17 @@ def screenshot(self, mode='rgb', time_viewer=False): not self.separate_canvas: canvas = self.mpl_canvas.fig.canvas canvas.draw_idle() - if self.notebook: - from io import BytesIO - # Here we need to save to a buffer without taking into - # account the dpi to keep the size used on creation - # of the canvas which is used for the 3D renderer. - # For some reason doing - # canvas.get_width_height() overestimates the size - # of the image while doing fig.savefig('fname.png') - # returns the right number of pixels. - fig = self.mpl_canvas.fig - output = BytesIO() - fig.savefig(output, format='raw') + fig = self.mpl_canvas.fig + with BytesIO() as output: + # Need to pass dpi here so it uses the physical (HiDPI) DPI + # rather than logical DPI when saving + fig.savefig(output, dpi=fig.get_dpi(), format='raw') output.seek(0) trace_img = np.reshape( np.frombuffer(output.getvalue(), dtype=np.uint8), newshape=(-1, img.shape[1], 4))[:, :, :3] - output.close() - else: - # In theory, one of these should work: - # - # trace_img = np.frombuffer( - # canvas.tostring_rgb(), dtype=np.uint8) - # trace_img.shape = canvas.get_width_height()[::-1] + (3,) - # - # or - # - # trace_img = np.frombuffer( - # canvas.tostring_rgb(), dtype=np.uint8) - # size = time_viewer.mpl_canvas.getSize() - # trace_img.shape = (size.height(), size.width(), 3) - # - # But in practice, sometimes the sizes does not match the - # renderer tostring_rgb() size. So let's directly use what - # matplotlib does in lib/matplotlib/backends/backend_agg.py - # before calling tobytes(): - trace_img = np.asarray( - canvas.renderer._renderer).take([0, 1, 2], axis=2) - # need to slice into trace_img because generally it's a bit - # smaller - delta = trace_img.shape[1] - img.shape[1] - if delta > 0: - start = delta // 2 - trace_img = trace_img[:, start:start + img.shape[1]] - img = np.concatenate([img, trace_img], axis=0) + img = concatenate_images( + [img, trace_img], bgcolor=self._brain_color[:3]) return img @fill_doc From 34c804c2bb1cede26ff270d77567b0fa080febb9 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 20 Jan 2021 14:54:53 -0500 Subject: [PATCH 32/36] FIX: dtype --- mne/viz/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/utils.py b/mne/viz/utils.py index cec24e97d4d..4e2ca18471e 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -2323,7 +2323,7 @@ def concatenate_images(images, axis=0, bgcolor='black', centered=True): ret = np.zeros((ret_shape[0], ret_shape[1], 3), dtype=np.uint8) ret[:, :, :] = bgcolor ptr = np.array([0, 0]) - sec = np.array([0 == axis, 1 == axis]).astype(np.int) + sec = np.array([0 == axis, 1 == axis]).astype(int) for image in images: shape = image.shape[:-1] dec = ptr From 8e927e7673c44ed4c90a1231bc6c11891141d9e0 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 20 Jan 2021 15:18:59 -0500 Subject: [PATCH 33/36] MAINT: Notebook test --- mne/viz/_brain/tests/test.ipynb | 31 ++++++++++++++++++++------- mne/viz/_brain/tests/test_notebook.py | 1 - 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/mne/viz/_brain/tests/test.ipynb b/mne/viz/_brain/tests/test.ipynb index 415d11b5748..722efb1bb9d 100644 --- a/mne/viz/_brain/tests/test.ipynb +++ b/mne/viz/_brain/tests/test.ipynb @@ -27,10 +27,12 @@ "metadata": {}, "outputs": [], "source": [ + "from contextlib import contextmanager\n", "import os\n", - "import mne\n", + "from numpy.testing import assert_allclose\n", "from ipywidgets import Button\n", "import matplotlib.pyplot as plt\n", + "import mne\n", "from mne.datasets import testing\n", "data_path = testing.data_path()\n", "sample_dir = os.path.join(data_path, 'MEG', 'sample')\n", @@ -40,13 +42,23 @@ "initial_time = 0.13\n", "mne.viz.set_3d_backend('notebook')\n", "brain_class = mne.viz.get_brain_class()\n", - "for interactive_state in (False, True):\n", - " plt.interactive(interactive_state)\n", + "\n", + "\n", + "@contextmanager\n", + "def interactive(on):\n", + " old = plt.isinteractive()\n", + " plt.interactive(on)\n", + " try:\n", + " yield\n", + " finally:\n", + " plt.interactive(old)\n", + "\n", + "with interactive(False):\n", " brain = stc.plot(subjects_dir=subjects_dir, initial_time=initial_time,\n", " clim=dict(kind='value', pos_lims=[3, 6, 9]),\n", " time_viewer=True,\n", " show_traces=True,\n", - " hemi='split')\n", + " hemi='split', size=300)\n", " assert isinstance(brain, brain_class)\n", " assert brain.notebook\n", " assert brain._renderer.figure.display is not None\n", @@ -58,9 +70,12 @@ " action.click()\n", " number_of_buttons += 1\n", " assert number_of_buttons == total_number_of_buttons\n", - " img1 = brain.screenshot(time_viewer=False)\n", - " img2 = brain.screenshot(time_viewer=True)\n", - " assert img1.shape[0] < img2.shape[0]\n", + " img_nv = brain.screenshot()\n", + " assert img_nv.shape == (300, 300, 3), img_nv.shape\n", + " img_v = brain.screenshot(time_viewer=True)\n", + " assert img_v.shape[1:] == (300, 3), img_v.shape\n", + " # XXX This rtol is not very good, ideally would be zero\n", + " assert_allclose(img_v.shape[0], img_nv.shape[0] * 1.25, err_msg=img_nv.shape, rtol=0.1)\n", " brain.close()" ] }, @@ -105,4 +120,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/mne/viz/_brain/tests/test_notebook.py b/mne/viz/_brain/tests/test_notebook.py index 48c65c2d066..63f0f10ffb0 100644 --- a/mne/viz/_brain/tests/test_notebook.py +++ b/mne/viz/_brain/tests/test_notebook.py @@ -7,7 +7,6 @@ PATH = os.path.dirname(os.path.realpath(__file__)) -@pytest.mark.slowtest @testing.requires_testing_data @requires_version('nbformat') @requires_version('nbclient') From 5d34669c5ea051b1c08dfda19cb1ddf0ba727945 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 20 Jan 2021 15:35:46 -0500 Subject: [PATCH 34/36] FIX: Flake --- mne/viz/_brain/tests/test_notebook.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mne/viz/_brain/tests/test_notebook.py b/mne/viz/_brain/tests/test_notebook.py index 63f0f10ffb0..7c159326b74 100644 --- a/mne/viz/_brain/tests/test_notebook.py +++ b/mne/viz/_brain/tests/test_notebook.py @@ -1,5 +1,4 @@ import os -import pytest from mne.datasets import testing from mne.utils import requires_version From 1e28b738f7b2f0b4e41e40e417236bc8b2c4c002 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 21 Jan 2021 10:24:40 +0100 Subject: [PATCH 35/36] Speed up test.ipynb --- mne/viz/_brain/tests/test.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mne/viz/_brain/tests/test.ipynb b/mne/viz/_brain/tests/test.ipynb index 722efb1bb9d..66b7844d1fa 100644 --- a/mne/viz/_brain/tests/test.ipynb +++ b/mne/viz/_brain/tests/test.ipynb @@ -58,7 +58,7 @@ " clim=dict(kind='value', pos_lims=[3, 6, 9]),\n", " time_viewer=True,\n", " show_traces=True,\n", - " hemi='split', size=300)\n", + " hemi='lh', size=300)\n", " assert isinstance(brain, brain_class)\n", " assert brain.notebook\n", " assert brain._renderer.figure.display is not None\n", @@ -120,4 +120,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} From 2a96cdd6322017dae9107fee73bbc6c687babdee Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 21 Jan 2021 08:38:28 -0500 Subject: [PATCH 36/36] FIX: Bad Qt/VTK combo --- mne/viz/_brain/_brain.py | 10 ++++++++-- mne/viz/_brain/tests/test.ipynb | 2 +- mne/viz/_brain/tests/test_brain.py | 17 ++++++++++++++--- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index d304c63bf22..a3bff001192 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -2630,8 +2630,14 @@ def screenshot(self, mode='rgb', time_viewer=False): fig = self.mpl_canvas.fig with BytesIO() as output: # Need to pass dpi here so it uses the physical (HiDPI) DPI - # rather than logical DPI when saving - fig.savefig(output, dpi=fig.get_dpi(), format='raw') + # rather than logical DPI when saving in most cases. + # But when matplotlib uses HiDPI and VTK doesn't + # (e.g., macOS w/Qt 5.14+ and VTK9) then things won't work, + # so let's just calculate the DPI we need to get + # the correct size output based on the widths being equal + dpi = img.shape[1] / fig.get_size_inches()[0] + fig.savefig(output, dpi=dpi, format='raw', + facecolor=self._bg_color, edgecolor='none') output.seek(0) trace_img = np.reshape( np.frombuffer(output.getvalue(), dtype=np.uint8), diff --git a/mne/viz/_brain/tests/test.ipynb b/mne/viz/_brain/tests/test.ipynb index 66b7844d1fa..ec7bfc13e60 100644 --- a/mne/viz/_brain/tests/test.ipynb +++ b/mne/viz/_brain/tests/test.ipynb @@ -120,4 +120,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 2e3aaac52ac..e206983d211 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -369,6 +369,9 @@ def test_brain_save_movie(tmpdir, renderer, brain_gc): brain.close() +_TINY_SIZE = (300, 250) + + def tiny(tmpdir): """Create a tiny fake brain.""" # This is a minimal version of what we need for our viz-with-timeviewer @@ -390,8 +393,16 @@ def tiny(tmpdir): data = rng.randn(len(rr), 10) stc = SourceEstimate(data, vertices, 0, 1, subject) brain = stc.plot(subjects_dir=tmpdir, hemi='lh', surface='white', - size=300) - ratio = brain.mpl_canvas.canvas.window().devicePixelRatio() + size=_TINY_SIZE) + # in principle this should be sufficient: + # + # ratio = brain.mpl_canvas.canvas.window().devicePixelRatio() + # + # but in practice VTK can mess up sizes, so let's just calculate it. + sz = brain.plotter.size() + sz = (sz.width(), sz.height()) + sz_ren = brain.plotter.renderer.GetSize() + ratio = np.median(np.array(sz_ren) / np.array(sz)) return brain, ratio @@ -401,7 +412,7 @@ def test_brain_screenshot(renderer_interactive, tmpdir, brain_gc): pytest.skip('TimeViewer tests only supported on PyVista') tiny_brain, ratio = tiny(tmpdir) img_nv = tiny_brain.screenshot(time_viewer=False) - want = (300 * ratio, 300 * ratio, 3) + want = (_TINY_SIZE[1] * ratio, _TINY_SIZE[0] * ratio, 3) assert img_nv.shape == want img_v = tiny_brain.screenshot(time_viewer=True) assert img_v.shape[1:] == want[1:]