From 6873d6c60cd0617769a0b4c8aa82ee4e1a01a2bb Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Fri, 8 Mar 2024 09:44:31 +0100 Subject: [PATCH 01/52] remove 2d black margins screenshot --- napari/_qt/qt_main_window.py | 46 +++++++++++++++++++++++++++---- napari/components/viewer_model.py | 22 ++++++++++++--- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index a5b5f69ad90..7f4aa31fec9 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1464,7 +1464,12 @@ def _restart(self): self._qt_window.restart() def _screenshot( - self, size=None, scale=None, flash=True, canvas_only=False + self, + size=None, + scale=None, + flash=True, + canvas_only=False, + fit_to_data: bool = True, ) -> 'QImage': """Capture screenshot of the currently displayed viewer. @@ -1484,6 +1489,8 @@ def _screenshot( If True, screenshot shows only the image display canvas, and if False include the napari viewer frame in the screenshot, By default, True. + fit_to_data: bool + Whether to fit a bounding box around the data to prevent margins of showing in the screenshot. Returns ------- @@ -1491,9 +1498,23 @@ def _screenshot( """ from napari._qt.utils import add_flash_animation + canvas = self._qt_viewer.canvas + prev_size = canvas.size + if fit_to_data: + ndisplay = self._qt_viewer.viewer.dims.ndisplay + camera = self._qt_viewer.viewer.camera + old_center = camera.center + old_zoom = camera.zoom + if ndisplay > 2: + raise NotImplementedError + + self._qt_viewer.viewer.reset_view() + canvas.size = self._qt_viewer.viewer.layers.extent.world[1][ + -ndisplay: + ].astype(int) + self._qt_viewer.viewer.reset_view(screenshot=True) + if canvas_only: - canvas = self._qt_viewer.canvas - prev_size = canvas.size if size is not None: if len(size) != 2: raise ValueError( @@ -1517,8 +1538,11 @@ def _screenshot( add_flash_animation(self._qt_viewer._welcome_widget) finally: # make sure we always go back to the right canvas size - if size is not None or scale is not None: + if size is not None or scale is not None or fit_to_data: canvas.size = prev_size + if fit_to_data: + camera.center = old_center + camera.zoom = old_zoom else: img = self._qt_window.grab().toImage() if flash: @@ -1526,7 +1550,13 @@ def _screenshot( return img def screenshot( - self, path=None, size=None, scale=None, flash=True, canvas_only=False + self, + path=None, + size=None, + scale=None, + flash=True, + canvas_only=False, + fit_to_data: bool = True, ): """Take currently displayed viewer and convert to an image array. @@ -1548,6 +1578,8 @@ def screenshot( If True, screenshot shows only the image display canvas, and if False includes the napari viewer frame in the screenshot, By default, True. + fit_to_data : bool + Whether to fit a bounding box around the data to prevent margins of showing in the screenshot. Returns ------- @@ -1556,7 +1588,9 @@ def screenshot( upper-left corner of the rendered region. """ - img = QImg2array(self._screenshot(size, scale, flash, canvas_only)) + img = QImg2array( + self._screenshot(size, scale, flash, canvas_only, fit_to_data) + ) if path is not None: imsave(path, img) return img diff --git a/napari/components/viewer_model.py b/napari/components/viewer_model.py index 0fc05476391..b621569b6ad 100644 --- a/napari/components/viewer_model.py +++ b/napari/components/viewer_model.py @@ -383,8 +383,19 @@ def _sliced_extent_world_augmented(self) -> np.ndarray: ) return self.layers._extent_world_augmented[:, self.dims.displayed] - def reset_view(self) -> None: - """Reset the camera view.""" + def reset_view(self, screenshot=False) -> None: + """ + Reset the camera view. + + This reset has two modes, one for when viewing the data in the viewer and one for when taking a + screenshot with a canvas not showing margins around the data. The two differ in the scaling + factor of the zoom. + + Parameters + ---------- + screenshot: bool + Whether to reset the view in screenshot mode. Default is False. + """ extent = self._sliced_extent_world_augmented scene_size = extent[1] - extent[0] @@ -408,12 +419,15 @@ def reset_view(self) -> None: # zoom is definied as the number of canvas pixels per world pixel # The default value used below will zoom such that the whole field # of view will occupy 95% of the canvas on the most filled axis + + scale_factor = 0.95 if not screenshot else 1 + if np.max(size) == 0: - self.camera.zoom = 0.95 * np.min(self._canvas_size) + self.camera.zoom = scale_factor * np.min(self._canvas_size) else: scale = np.array(size[-2:]) scale[np.isclose(scale, 0)] = 1 - self.camera.zoom = 0.95 * np.min( + self.camera.zoom = scale_factor * np.min( np.array(self._canvas_size) / scale ) self.camera.angles = (0, 0, 90) From 5948051fee864e4e3314235cdbe5a6edbe5b6265 Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Fri, 8 Mar 2024 10:13:42 +0100 Subject: [PATCH 02/52] add param to viewer.screenshot --- napari/viewer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/napari/viewer.py b/napari/viewer.py index fd85c9d915b..da11a525ef5 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -100,6 +100,7 @@ def screenshot( scale=None, canvas_only=True, flash: bool = True, + fit_to_data: bool = True, ): """Take currently displayed screen and convert to an image array. @@ -121,6 +122,8 @@ def screenshot( Flag to indicate whether flash animation should be shown after the screenshot was captured. By default, True. + fit_to_data : bool + Whether to fit a bounding box around the data to prevent margins of showing in the screenshot. Returns ------- @@ -134,6 +137,7 @@ def screenshot( scale=scale, flash=flash, canvas_only=canvas_only, + fit_to_data=fit_to_data, ) def show(self, *, block=False): From 7824f6946c3295181c4f199650d74cfce463564c Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Fri, 22 Mar 2024 09:46:53 +0100 Subject: [PATCH 03/52] scale length of scale bar to canvas size --- napari/_vispy/overlays/scale_bar.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/napari/_vispy/overlays/scale_bar.py b/napari/_vispy/overlays/scale_bar.py index 3f3f3eccb79..10108225ef7 100644 --- a/napari/_vispy/overlays/scale_bar.py +++ b/napari/_vispy/overlays/scale_bar.py @@ -29,6 +29,7 @@ def __init__(self, *, viewer, overlay, parent=None) -> None: self.y_offset = 20 self.y_size = 5 + self.node.events.parent_change.connect(self._on_parent_change) self.overlay.events.box.connect(self._on_box_change) self.overlay.events.box_color.connect(self._on_data_change) self.overlay.events.color.connect(self._on_data_change) @@ -42,6 +43,16 @@ def __init__(self, *, viewer, overlay, parent=None) -> None: self.reset() + def _on_parent_change(self, event): + if event.new and self.node.canvas: + event.new.canvas.events.resize.connect( + self._scale_on_canvas_resize + ) + + def _scale_on_canvas_resize(self, event): + self._target_length = event.source.size[0] / 5 + self._on_zoom_change(force=True) + def _on_unit_change(self): self._unit = get_unit_registry()(self.overlay.unit) self._on_zoom_change(force=True) From cf4590319160b7d05fdb46480b0e372745b1949f Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Sat, 23 Mar 2024 18:48:39 +0100 Subject: [PATCH 04/52] add docstring --- napari/_vispy/overlays/scale_bar.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/napari/_vispy/overlays/scale_bar.py b/napari/_vispy/overlays/scale_bar.py index 10108225ef7..bbd48f734e9 100644 --- a/napari/_vispy/overlays/scale_bar.py +++ b/napari/_vispy/overlays/scale_bar.py @@ -29,7 +29,9 @@ def __init__(self, *, viewer, overlay, parent=None) -> None: self.y_offset = 20 self.y_size = 5 + # In the super().__init__ we see node is scale bar, need to connect its parent, canvas self.node.events.parent_change.connect(self._on_parent_change) + self.overlay.events.box.connect(self._on_box_change) self.overlay.events.box_color.connect(self._on_data_change) self.overlay.events.color.connect(self._on_data_change) @@ -44,6 +46,7 @@ def __init__(self, *, viewer, overlay, parent=None) -> None: self.reset() def _on_parent_change(self, event): + """Connect the canvas resize event to scale bar callback function(s).""" if event.new and self.node.canvas: event.new.canvas.events.resize.connect( self._scale_on_canvas_resize From 998d0b77fc17f1a409b1950a679b3f3b6602b8c9 Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Fri, 22 Mar 2024 09:46:53 +0100 Subject: [PATCH 05/52] scale length of scale bar to canvas size Co-authored-by: olusesan.ajina@gmail.com --- napari/_vispy/overlays/scale_bar.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/napari/_vispy/overlays/scale_bar.py b/napari/_vispy/overlays/scale_bar.py index 3f3f3eccb79..10108225ef7 100644 --- a/napari/_vispy/overlays/scale_bar.py +++ b/napari/_vispy/overlays/scale_bar.py @@ -29,6 +29,7 @@ def __init__(self, *, viewer, overlay, parent=None) -> None: self.y_offset = 20 self.y_size = 5 + self.node.events.parent_change.connect(self._on_parent_change) self.overlay.events.box.connect(self._on_box_change) self.overlay.events.box_color.connect(self._on_data_change) self.overlay.events.color.connect(self._on_data_change) @@ -42,6 +43,16 @@ def __init__(self, *, viewer, overlay, parent=None) -> None: self.reset() + def _on_parent_change(self, event): + if event.new and self.node.canvas: + event.new.canvas.events.resize.connect( + self._scale_on_canvas_resize + ) + + def _scale_on_canvas_resize(self, event): + self._target_length = event.source.size[0] / 5 + self._on_zoom_change(force=True) + def _on_unit_change(self): self._unit = get_unit_registry()(self.overlay.unit) self._on_zoom_change(force=True) From 607f116b15b1c12edb53457c5b245880dde4ca6b Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Sat, 23 Mar 2024 18:48:39 +0100 Subject: [PATCH 06/52] add docstring Co-authored-by: olusesan.ajina@gmail.com --- napari/_vispy/overlays/scale_bar.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/napari/_vispy/overlays/scale_bar.py b/napari/_vispy/overlays/scale_bar.py index 10108225ef7..bbd48f734e9 100644 --- a/napari/_vispy/overlays/scale_bar.py +++ b/napari/_vispy/overlays/scale_bar.py @@ -29,7 +29,9 @@ def __init__(self, *, viewer, overlay, parent=None) -> None: self.y_offset = 20 self.y_size = 5 + # In the super().__init__ we see node is scale bar, need to connect its parent, canvas self.node.events.parent_change.connect(self._on_parent_change) + self.overlay.events.box.connect(self._on_box_change) self.overlay.events.box_color.connect(self._on_data_change) self.overlay.events.color.connect(self._on_data_change) @@ -44,6 +46,7 @@ def __init__(self, *, viewer, overlay, parent=None) -> None: self.reset() def _on_parent_change(self, event): + """Connect the canvas resize event to scale bar callback function(s).""" if event.new and self.node.canvas: event.new.canvas.events.resize.connect( self._scale_on_canvas_resize From 181e1b22b9c45fdab5c5885a75f177f744ee5153 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 28 Apr 2024 17:02:51 +0200 Subject: [PATCH 07/52] adjust font size Co-authored-by: olusesan.ajina@gmail.com --- napari/_vispy/overlays/scale_bar.py | 17 ++++++++++++++--- napari/settings/_experimental.py | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/napari/_vispy/overlays/scale_bar.py b/napari/_vispy/overlays/scale_bar.py index bbd48f734e9..ec102290e33 100644 --- a/napari/_vispy/overlays/scale_bar.py +++ b/napari/_vispy/overlays/scale_bar.py @@ -8,6 +8,7 @@ from napari._vispy.overlays.base import ViewerOverlayMixin, VispyCanvasOverlay from napari._vispy.visuals.scale_bar import ScaleBar +from napari.settings import get_settings from napari.utils._units import PREFERRED_VALUES, get_unit_registry from napari.utils.colormaps.standardize_color import transform_color from napari.utils.theme import get_theme @@ -49,11 +50,21 @@ def _on_parent_change(self, event): """Connect the canvas resize event to scale bar callback function(s).""" if event.new and self.node.canvas: event.new.canvas.events.resize.connect( - self._scale_on_canvas_resize + self._scale_scalebar_on_canvas_resize ) + event.new.canvas.events.resize.connect(self._scale_font_size) - def _scale_on_canvas_resize(self, event): - self._target_length = event.source.size[0] / 5 + def _scale_font_size(self, event): + """Scale the font size in response to a canvas resize""" + self.overlay.font_size = ( + event.source.size[1] + / get_settings().experimental.scale_bar_font_size + ) + + def _scale_scalebar_on_canvas_resize(self, event): + self._target_length = ( + event.source.size[0] / get_settings().experimental.scale_bar_length + ) self._on_zoom_change(force=True) def _on_unit_change(self): diff --git a/napari/settings/_experimental.py b/napari/settings/_experimental.py index 92200918e56..568dac318cb 100644 --- a/napari/settings/_experimental.py +++ b/napari/settings/_experimental.py @@ -45,6 +45,26 @@ class ExperimentalSettings(EventedSettings): gt=0, lt=50, ) + scale_bar_length: int = Field( + 5, + title=trans._('Scale bar length'), + description=trans._( + 'The scale bar length as a fraction of the canvas width.' + ), + type=int, + ge=3, + le=15, + ) + scale_bar_font_size: int = Field( + 30, + title=trans._('Scale bar font size'), + description=trans._( + 'The scale bar font size as a fraction of the canvas height.' + ), + type=int, + ge=25, + le=50, + ) class NapariConfig: # Napari specific configuration From 22c5319e0ce34a933ddf323e96955f1abe5d229d Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Wed, 1 May 2024 22:03:06 +0200 Subject: [PATCH 08/52] adjust bottom offset to canvas size Co-authored-by: olusesan.ajina@gmail.com --- napari/_vispy/overlays/scale_bar.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/napari/_vispy/overlays/scale_bar.py b/napari/_vispy/overlays/scale_bar.py index ec102290e33..c85da6e0fd1 100644 --- a/napari/_vispy/overlays/scale_bar.py +++ b/napari/_vispy/overlays/scale_bar.py @@ -28,6 +28,7 @@ def __init__(self, *, viewer, overlay, parent=None) -> None: self.x_size = 150 # will be updated on zoom anyways # need to change from defaults because the anchor is in the center self.y_offset = 20 + # TODO: perhaps change name as y_size does not indicate bottom offset. self.y_size = 5 # In the super().__init__ we see node is scale bar, need to connect its parent, canvas @@ -65,6 +66,7 @@ def _scale_scalebar_on_canvas_resize(self, event): self._target_length = ( event.source.size[0] / get_settings().experimental.scale_bar_length ) + self.y_size = event.source.size[1] / 40 self._on_zoom_change(force=True) def _on_unit_change(self): From a1e873812a4713a5eb566ed265d7c57307b7f537 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sat, 4 May 2024 20:01:42 +0200 Subject: [PATCH 09/52] adjust bottom offset to canvas size Co-authored-by: olusesan.ajina@gmail.com --- napari/_vispy/overlays/scale_bar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/napari/_vispy/overlays/scale_bar.py b/napari/_vispy/overlays/scale_bar.py index c85da6e0fd1..f4ee5e688eb 100644 --- a/napari/_vispy/overlays/scale_bar.py +++ b/napari/_vispy/overlays/scale_bar.py @@ -57,7 +57,7 @@ def _on_parent_change(self, event): def _scale_font_size(self, event): """Scale the font size in response to a canvas resize""" - self.overlay.font_size = ( + self.node.text.font_size = ( event.source.size[1] / get_settings().experimental.scale_bar_font_size ) From 5fdf661549ae36df62e7012e8e91de9ba723bbb7 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sat, 4 May 2024 20:13:54 +0200 Subject: [PATCH 10/52] adjust width based on canvas height Co-authored-by: olusesan.ajina@gmail.com --- napari/_vispy/overlays/scale_bar.py | 1 + 1 file changed, 1 insertion(+) diff --git a/napari/_vispy/overlays/scale_bar.py b/napari/_vispy/overlays/scale_bar.py index f4ee5e688eb..ee3a75c2152 100644 --- a/napari/_vispy/overlays/scale_bar.py +++ b/napari/_vispy/overlays/scale_bar.py @@ -67,6 +67,7 @@ def _scale_scalebar_on_canvas_resize(self, event): event.source.size[0] / get_settings().experimental.scale_bar_length ) self.y_size = event.source.size[1] / 40 + self.node.line._width = event.source.size[1] / 100 self._on_zoom_change(force=True) def _on_unit_change(self): From 31233671a54ca2119ac4de2491527f9716e00fb0 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sat, 4 May 2024 20:42:53 +0200 Subject: [PATCH 11/52] remove scalebar whiskers Co-authored-by: olusesan.ajina@gmail.com --- napari/_vispy/visuals/scale_bar.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/napari/_vispy/visuals/scale_bar.py b/napari/_vispy/visuals/scale_bar.py index a999e61a849..440301d9da1 100644 --- a/napari/_vispy/visuals/scale_bar.py +++ b/napari/_vispy/visuals/scale_bar.py @@ -8,10 +8,6 @@ def __init__(self) -> None: [ [0, 0], [1, 0], - [0, -5], - [0, 5], - [1, -5], - [1, 5], ] ) @@ -26,7 +22,7 @@ def __init__(self) -> None: anchor_y='top', font_size=10, ), - Line(connect='segments', method='gl', width=3), + Line(connect='strip', method='gl', width=3), ] ) From 4a434fc48445e65071abf7df1a6ff16508da8244 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sat, 4 May 2024 20:57:21 +0200 Subject: [PATCH 12/52] adjust x_size based on canvas size Co-authored-by: olusesan.ajina@gmail.com --- napari/_vispy/overlays/scale_bar.py | 1 + napari/settings/_experimental.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/napari/_vispy/overlays/scale_bar.py b/napari/_vispy/overlays/scale_bar.py index ee3a75c2152..25aebf87114 100644 --- a/napari/_vispy/overlays/scale_bar.py +++ b/napari/_vispy/overlays/scale_bar.py @@ -67,6 +67,7 @@ def _scale_scalebar_on_canvas_resize(self, event): event.source.size[0] / get_settings().experimental.scale_bar_length ) self.y_size = event.source.size[1] / 40 + self.x_size = event.source.size[0] / 20 self.node.line._width = event.source.size[1] / 100 self._on_zoom_change(force=True) diff --git a/napari/settings/_experimental.py b/napari/settings/_experimental.py index 568dac318cb..335fc97d29e 100644 --- a/napari/settings/_experimental.py +++ b/napari/settings/_experimental.py @@ -53,16 +53,16 @@ class ExperimentalSettings(EventedSettings): ), type=int, ge=3, - le=15, + le=10, ) scale_bar_font_size: int = Field( - 30, + 35, title=trans._('Scale bar font size'), description=trans._( 'The scale bar font size as a fraction of the canvas height.' ), type=int, - ge=25, + ge=30, le=50, ) From 4cc0d90e77b2bdadd6cc8d41ab570e86336040b9 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 5 May 2024 19:37:52 +0200 Subject: [PATCH 13/52] fix 1 pixel off Co-authored-by: olusesan.ajina@gmail.com --- napari/_qt/qt_main_window.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index f11849fce81..ce76ad2c52a 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1549,9 +1549,12 @@ def _screenshot( raise NotImplementedError self._qt_viewer.viewer.reset_view() - canvas.size = self._qt_viewer.viewer.layers.extent.world[1][ - -ndisplay: - ].astype(int) + canvas.size = ( + self._qt_viewer.viewer.layers.extent.world[1][ + -ndisplay: + ].astype(int) + + 1 + ) self._qt_viewer.viewer.reset_view(screenshot=True) if canvas_only: From 6364fd0cec068783236659e6a70db1295483b72c Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 5 May 2024 19:37:52 +0200 Subject: [PATCH 14/52] Co-Authored-By: olusesan.ajina@gmail.com --- napari/_qt/qt_main_window.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index f11849fce81..ce76ad2c52a 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1549,9 +1549,12 @@ def _screenshot( raise NotImplementedError self._qt_viewer.viewer.reset_view() - canvas.size = self._qt_viewer.viewer.layers.extent.world[1][ - -ndisplay: - ].astype(int) + canvas.size = ( + self._qt_viewer.viewer.layers.extent.world[1][ + -ndisplay: + ].astype(int) + + 1 + ) self._qt_viewer.viewer.reset_view(screenshot=True) if canvas_only: From d3831bd2b8bd0fa9c475cd82a5efd58cab7c6dcf Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 5 May 2024 19:51:01 +0200 Subject: [PATCH 15/52] add test Co-Authored-By: olusesan.ajina@gmail.com --- napari/_qt/_tests/test_qt_viewer.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py index a0d6cf1f73c..be18bd07ffd 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -259,6 +259,18 @@ def test_screenshot(make_napari_viewer): assert screenshot.ndim == 3 +def test_screenshot_fit_data(make_napari_viewer): + viewer = make_napari_viewer() + + np.random.seed(0) + # Add image + data = np.ones((10, 15)) + viewer.add_image(data) + img = viewer.screenshot(flash=False) + assert img.shape == (10, 15, 4) + assert np.all(img == 255) + + @pytest.mark.skip('new approach') def test_screenshot_dialog(make_napari_viewer, tmpdir): """Test save screenshot functionality.""" From d012489ea19289661ce29bb325b371811b1bcc22 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 5 May 2024 20:23:01 +0200 Subject: [PATCH 16/52] fix tests Co-authored-by: olusesan olusesan.ajina@gmail.com --- napari/_qt/_tests/test_qt_viewer.py | 2 +- napari/_qt/qt_main_window.py | 4 +++- napari/_tests/test_with_screenshot.py | 4 +++- napari/_vispy/_tests/test_vispy_multiscale.py | 4 +++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py index be18bd07ffd..e23323705b8 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -254,7 +254,7 @@ def test_screenshot(make_napari_viewer): # Take screenshot with pytest.warns(FutureWarning): - screenshot = viewer.window.qt_viewer.screenshot(flash=False) + viewer.window.qt_viewer.screenshot(flash=False) screenshot = viewer.window.screenshot(flash=False, canvas_only=True) assert screenshot.ndim == 3 diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index ce76ad2c52a..d8f52adf843 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1546,7 +1546,9 @@ def _screenshot( old_center = camera.center old_zoom = camera.zoom if ndisplay > 2: - raise NotImplementedError + raise NotImplementedError( + 'Fit_to_data is not yet implemented for 3D. Please set fit_to_data to False' + ) self._qt_viewer.viewer.reset_view() canvas.size = ( diff --git a/napari/_tests/test_with_screenshot.py b/napari/_tests/test_with_screenshot.py index 9e61b4ef116..db84218aeae 100644 --- a/napari/_tests/test_with_screenshot.py +++ b/napari/_tests/test_with_screenshot.py @@ -112,7 +112,9 @@ def test_z_order_images_after_ndisplay(make_napari_viewer): # Switch to 3D rendering viewer.dims.ndisplay = 3 - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, fit_to_data=False + ) center = tuple(np.round(np.divide(screenshot.shape[:2], 2)).astype(int)) # Check that blue is still visible np.testing.assert_almost_equal(screenshot[center], [0, 0, 255, 255]) diff --git a/napari/_vispy/_tests/test_vispy_multiscale.py b/napari/_vispy/_tests/test_vispy_multiscale.py index 6312641eeb8..e2c832d7681 100644 --- a/napari/_vispy/_tests/test_vispy_multiscale.py +++ b/napari/_vispy/_tests/test_vispy_multiscale.py @@ -71,7 +71,9 @@ def test_multiscale_screenshot(make_napari_viewer): # Set canvas size to target amount viewer.window._qt_viewer.canvas.view.canvas.size = (800, 600) - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, fit_to_data=False + ) center_coord = np.round(np.array(screenshot.shape[:2]) / 2).astype(int) target_center = np.array([255, 255, 255, 255], dtype='uint8') target_edge = np.array([0, 0, 0, 255], dtype='uint8') From 4d9b73f7cb10cf830a07c220798a2e4f9f42094d Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 5 May 2024 20:27:13 +0200 Subject: [PATCH 17/52] chance parameter name Co-authored-by: olusesan.ajina@gmail.com --- napari/_qt/qt_main_window.py | 18 +++++++++--------- napari/_tests/test_with_screenshot.py | 2 +- napari/_vispy/_tests/test_vispy_multiscale.py | 2 +- napari/viewer.py | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index d8f52adf843..d89b41c3479 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1509,7 +1509,7 @@ def _screenshot( scale=None, flash=True, canvas_only=False, - fit_to_data: bool = True, + margins: bool = True, ) -> 'QImage': """Capture screenshot of the currently displayed viewer. @@ -1529,7 +1529,7 @@ def _screenshot( If True, screenshot shows only the image display canvas, and if False include the napari viewer frame in the screenshot, By default, True. - fit_to_data: bool + margins: bool Whether to fit a bounding box around the data to prevent margins of showing in the screenshot. Returns @@ -1540,14 +1540,14 @@ def _screenshot( canvas = self._qt_viewer.canvas prev_size = canvas.size - if fit_to_data: + if margins: ndisplay = self._qt_viewer.viewer.dims.ndisplay camera = self._qt_viewer.viewer.camera old_center = camera.center old_zoom = camera.zoom if ndisplay > 2: raise NotImplementedError( - 'Fit_to_data is not yet implemented for 3D. Please set fit_to_data to False' + 'margins is not yet implemented for 3D. Please set margins to False' ) self._qt_viewer.viewer.reset_view() @@ -1583,9 +1583,9 @@ def _screenshot( add_flash_animation(self._qt_viewer._welcome_widget) finally: # make sure we always go back to the right canvas size - if size is not None or scale is not None or fit_to_data: + if size is not None or scale is not None or margins: canvas.size = prev_size - if fit_to_data: + if margins: camera.center = old_center camera.zoom = old_zoom else: @@ -1601,7 +1601,7 @@ def screenshot( scale=None, flash=True, canvas_only=False, - fit_to_data: bool = True, + margins: bool = True, ): """Take currently displayed viewer and convert to an image array. @@ -1623,7 +1623,7 @@ def screenshot( If True, screenshot shows only the image display canvas, and if False includes the napari viewer frame in the screenshot, By default, True. - fit_to_data : bool + margins : bool Whether to fit a bounding box around the data to prevent margins of showing in the screenshot. Returns @@ -1634,7 +1634,7 @@ def screenshot( """ img = QImg2array( - self._screenshot(size, scale, flash, canvas_only, fit_to_data) + self._screenshot(size, scale, flash, canvas_only, margins) ) if path is not None: imsave(path, img) diff --git a/napari/_tests/test_with_screenshot.py b/napari/_tests/test_with_screenshot.py index db84218aeae..c5a80ad1f60 100644 --- a/napari/_tests/test_with_screenshot.py +++ b/napari/_tests/test_with_screenshot.py @@ -113,7 +113,7 @@ def test_z_order_images_after_ndisplay(make_napari_viewer): # Switch to 3D rendering viewer.dims.ndisplay = 3 screenshot = viewer.screenshot( - canvas_only=True, flash=False, fit_to_data=False + canvas_only=True, flash=False, margins=False ) center = tuple(np.round(np.divide(screenshot.shape[:2], 2)).astype(int)) # Check that blue is still visible diff --git a/napari/_vispy/_tests/test_vispy_multiscale.py b/napari/_vispy/_tests/test_vispy_multiscale.py index e2c832d7681..5ee1e055410 100644 --- a/napari/_vispy/_tests/test_vispy_multiscale.py +++ b/napari/_vispy/_tests/test_vispy_multiscale.py @@ -72,7 +72,7 @@ def test_multiscale_screenshot(make_napari_viewer): viewer.window._qt_viewer.canvas.view.canvas.size = (800, 600) screenshot = viewer.screenshot( - canvas_only=True, flash=False, fit_to_data=False + canvas_only=True, flash=False, margins=False ) center_coord = np.round(np.array(screenshot.shape[:2]) / 2).astype(int) target_center = np.array([255, 255, 255, 255], dtype='uint8') diff --git a/napari/viewer.py b/napari/viewer.py index 6901cc379b3..6e685da0e80 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -96,7 +96,7 @@ def screenshot( scale=None, canvas_only=True, flash: bool = True, - fit_to_data: bool = True, + margins: bool = True, ): """Take currently displayed screen and convert to an image array. @@ -118,7 +118,7 @@ def screenshot( Flag to indicate whether flash animation should be shown after the screenshot was captured. By default, True. - fit_to_data : bool + margins : bool Whether to fit a bounding box around the data to prevent margins of showing in the screenshot. Returns @@ -133,7 +133,7 @@ def screenshot( scale=scale, flash=flash, canvas_only=canvas_only, - fit_to_data=fit_to_data, + margins=margins, ) def show(self, *, block=False): From 9212a9fdb940a107070c23b257a9ee28a60effcd Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 5 May 2024 20:58:57 +0200 Subject: [PATCH 18/52] fix tests Co-authored-by: olusesan.ajina@gmail.com --- napari/_tests/test_with_screenshot.py | 140 +++++++++++++++++++------- 1 file changed, 105 insertions(+), 35 deletions(-) diff --git a/napari/_tests/test_with_screenshot.py b/napari/_tests/test_with_screenshot.py index c5a80ad1f60..ef50265e897 100644 --- a/napari/_tests/test_with_screenshot.py +++ b/napari/_tests/test_with_screenshot.py @@ -166,24 +166,34 @@ def test_changing_image_colormap(make_napari_viewer): data = np.ones((20, 20, 20)) layer = viewer.add_image(data, contrast_limits=[0, 1]) - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) center = tuple(np.round(np.divide(screenshot.shape[:2], 2)).astype(int)) np.testing.assert_almost_equal(screenshot[center], [255, 255, 255, 255]) layer.colormap = 'red' - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) np.testing.assert_almost_equal(screenshot[center], [255, 0, 0, 255]) viewer.dims.ndisplay = 3 - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) np.testing.assert_almost_equal(screenshot[center], [255, 0, 0, 255]) layer.colormap = 'blue' - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) np.testing.assert_almost_equal(screenshot[center], [0, 0, 255, 255]) viewer.dims.ndisplay = 2 - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) np.testing.assert_almost_equal(screenshot[center], [0, 0, 255, 255]) @@ -196,24 +206,34 @@ def test_changing_image_gamma(make_napari_viewer): data = np.ones((20, 20, 20)) layer = viewer.add_image(data, contrast_limits=[0, 2]) - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) center = tuple(np.round(np.divide(screenshot.shape[:2], 2)).astype(int)) assert 127 <= screenshot[(*center, 0)] <= 129 layer.gamma = 0.1 - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) assert screenshot[(*center, 0)] > 230 viewer.dims.ndisplay = 3 - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) assert screenshot[(*center, 0)] > 230 layer.gamma = 1.9 - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) assert screenshot[(*center, 0)] < 80 viewer.dims.ndisplay = 2 - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) assert screenshot[(*center, 0)] < 80 @@ -235,7 +255,9 @@ def test_grid_mode(make_napari_viewer): np.testing.assert_allclose(translations, expected_translations) # check screenshot - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) center = tuple(np.round(np.divide(screenshot.shape[:2], 2)).astype(int)) np.testing.assert_almost_equal(screenshot[center], [0, 0, 255, 255]) @@ -256,7 +278,9 @@ def test_grid_mode(make_napari_viewer): np.testing.assert_allclose(translations, expected_translations[::-1]) # check screenshot - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) # sample 6 squares of the grid and check they have right colors pos = [ (1 / 3, 1 / 4), @@ -286,7 +310,9 @@ def test_grid_mode(make_napari_viewer): viewer.layers.move(1, 6) # check screenshot - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) # CGRMYB color order color = [ [0, 255, 255, 255], @@ -312,7 +338,9 @@ def test_grid_mode(make_napari_viewer): np.testing.assert_allclose(translations, expected_translations) # check screenshot - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) center = tuple(np.round(np.divide(screenshot.shape[:2], 2)).astype(int)) np.testing.assert_almost_equal(screenshot[center], [0, 255, 255, 255]) @@ -330,19 +358,25 @@ def test_changing_image_attenuation(make_napari_viewer): # normal mip viewer.layers[0].rendering = 'mip' - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) center = tuple(np.round(np.divide(screenshot.shape[:2], 2)).astype(int)) mip_value = screenshot[center][0] # zero attenuation (still attenuated!) viewer.layers[0].rendering = 'attenuated_mip' viewer.layers[0].attenuation = 0.0 - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) zero_att_value = screenshot[center][0] # increase attenuation viewer.layers[0].attenuation = 0.5 - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) more_att_value = screenshot[center][0] # Check that rendering has been attenuated assert zero_att_value < more_att_value < mip_value @@ -358,7 +392,9 @@ def test_labels_painting(make_napari_viewer): viewer.add_labels(data) layer = viewer.layers[0] - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) # Check that no painting has occurred assert layer.data.max() == 0 @@ -403,7 +439,9 @@ def test_labels_painting(make_napari_viewer): ) mouse_press_callbacks(layer, event) - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) # Check that painting has now occurred assert layer.data.max() > 0 assert screenshot[:, :, :2].max() > 0 @@ -417,19 +455,25 @@ def test_welcome(make_napari_viewer): viewer = make_napari_viewer(show=True) # Check something is visible - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) assert len(viewer.layers) == 0 assert screenshot[..., :-1].max() > 0 # Check adding zeros image makes it go away viewer.add_image(np.zeros((1, 1))) - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) assert len(viewer.layers) == 1 assert screenshot[..., :-1].max() == 0 # Remove layer and check something is visible again viewer.layers.pop(0) - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) assert len(viewer.layers) == 0 assert screenshot[..., :-1].max() > 0 @@ -442,18 +486,24 @@ def test_axes_visible(make_napari_viewer): viewer.window._qt_viewer.set_welcome_visible(False) # Check axes are not visible - launch_screenshot = viewer.screenshot(canvas_only=True, flash=False) + launch_screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) assert not viewer.axes.visible # Make axes visible and check something is seen viewer.axes.visible = True - on_screenshot = viewer.screenshot(canvas_only=True, flash=False) + on_screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) assert viewer.axes.visible assert abs(on_screenshot - launch_screenshot).max() > 0 # Make axes not visible and check they are gone viewer.axes.visible = False - off_screenshot = viewer.screenshot(canvas_only=True, flash=False) + off_screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) assert not viewer.axes.visible np.testing.assert_almost_equal(launch_screenshot, off_screenshot) @@ -466,18 +516,24 @@ def test_scale_bar_visible(make_napari_viewer): viewer.window._qt_viewer.set_welcome_visible(False) # Check scale bar is not visible - launch_screenshot = viewer.screenshot(canvas_only=True, flash=False) + launch_screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) assert not viewer.scale_bar.visible # Make scale bar visible and check something is seen viewer.scale_bar.visible = True - on_screenshot = viewer.screenshot(canvas_only=True, flash=False) + on_screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) assert viewer.scale_bar.visible assert abs(on_screenshot - launch_screenshot).max() > 0 # Make scale bar not visible and check it is gone viewer.scale_bar.visible = False - off_screenshot = viewer.screenshot(canvas_only=True, flash=False) + off_screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) assert not viewer.scale_bar.visible np.testing.assert_almost_equal(launch_screenshot, off_screenshot) @@ -492,7 +548,9 @@ def test_screenshot_has_no_border(make_napari_viewer): # Zoom in dramatically to make the screenshot all red. viewer.camera.zoom = 1000 - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) expected = np.broadcast_to([255, 0, 0, 255], screenshot.shape) np.testing.assert_array_equal(screenshot, expected) @@ -516,17 +574,23 @@ def test_blending_modes_with_canvas(make_napari_viewer): # check that additive behaves correctly with black canvas img1_layer.blending = 'additive' img2_layer.blending = 'additive' - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) np.testing.assert_array_equal(screenshot[:, :, 0], img1 + img2) # minimum should not result in black background if canvas is black img1_layer.blending = 'minimum' img2_layer.blending = 'minimum' - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) np.testing.assert_array_equal(screenshot[:, :, 0], np.minimum(img1, img2)) # toggle visibility of bottom layer img1_layer.visible = False - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) np.testing.assert_array_equal(screenshot[:, :, 0], img2) # and canvas should not affect the above results viewer.window._qt_viewer.canvas.bgcolor = 'white' @@ -535,15 +599,21 @@ def test_blending_modes_with_canvas(make_napari_viewer): img1_layer.visible = True img1_layer.blending = 'additive' img2_layer.blending = 'additive' - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) np.testing.assert_array_equal(screenshot[:, :, 0], img1 + img2) # toggle visibility of bottom layer img1_layer.visible = False - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) np.testing.assert_array_equal(screenshot[:, :, 0], img2) # minimum should always work with white canvas bgcolor img1_layer.visible = True img1_layer.blending = 'minimum' img2_layer.blending = 'minimum' - screenshot = viewer.screenshot(canvas_only=True, flash=False) + screenshot = viewer.screenshot( + canvas_only=True, flash=False, margins=False + ) np.testing.assert_array_equal(screenshot[:, :, 0], np.minimum(img1, img2)) From d091e326de17171333d06e036e83126dc32754d9 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 5 May 2024 21:11:43 +0200 Subject: [PATCH 19/52] rename to no_margins Co-authored-by: olusesan.ajina@gmail.com --- napari/_qt/_tests/test_qt_viewer.py | 6 +- napari/_qt/qt_main_window.py | 18 ++--- napari/_tests/test_with_screenshot.py | 72 +++++++++---------- napari/_vispy/_tests/test_vispy_multiscale.py | 2 +- napari/viewer.py | 6 +- 5 files changed, 54 insertions(+), 50 deletions(-) diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py index e23323705b8..8f56b01fe30 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -259,7 +259,7 @@ def test_screenshot(make_napari_viewer): assert screenshot.ndim == 3 -def test_screenshot_fit_data(make_napari_viewer): +def test_screenshot_without_margin(make_napari_viewer): viewer = make_napari_viewer() np.random.seed(0) @@ -270,6 +270,10 @@ def test_screenshot_fit_data(make_napari_viewer): assert img.shape == (10, 15, 4) assert np.all(img == 255) + img = viewer.screenshot(scale=8) + assert img.shape == (80, 120, 4) + assert np.all(img == 255) + @pytest.mark.skip('new approach') def test_screenshot_dialog(make_napari_viewer, tmpdir): diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index d89b41c3479..0be4f72b53e 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1509,7 +1509,7 @@ def _screenshot( scale=None, flash=True, canvas_only=False, - margins: bool = True, + no_margins: bool = True, ) -> 'QImage': """Capture screenshot of the currently displayed viewer. @@ -1529,7 +1529,7 @@ def _screenshot( If True, screenshot shows only the image display canvas, and if False include the napari viewer frame in the screenshot, By default, True. - margins: bool + no_margins: bool Whether to fit a bounding box around the data to prevent margins of showing in the screenshot. Returns @@ -1540,14 +1540,14 @@ def _screenshot( canvas = self._qt_viewer.canvas prev_size = canvas.size - if margins: + if no_margins: ndisplay = self._qt_viewer.viewer.dims.ndisplay camera = self._qt_viewer.viewer.camera old_center = camera.center old_zoom = camera.zoom if ndisplay > 2: raise NotImplementedError( - 'margins is not yet implemented for 3D. Please set margins to False' + 'no_margins is not yet implemented for 3D. Please set no_margins to False' ) self._qt_viewer.viewer.reset_view() @@ -1583,9 +1583,9 @@ def _screenshot( add_flash_animation(self._qt_viewer._welcome_widget) finally: # make sure we always go back to the right canvas size - if size is not None or scale is not None or margins: + if size is not None or scale is not None or no_margins: canvas.size = prev_size - if margins: + if no_margins: camera.center = old_center camera.zoom = old_zoom else: @@ -1601,7 +1601,7 @@ def screenshot( scale=None, flash=True, canvas_only=False, - margins: bool = True, + no_margins: bool = True, ): """Take currently displayed viewer and convert to an image array. @@ -1623,7 +1623,7 @@ def screenshot( If True, screenshot shows only the image display canvas, and if False includes the napari viewer frame in the screenshot, By default, True. - margins : bool + no_margins : bool Whether to fit a bounding box around the data to prevent margins of showing in the screenshot. Returns @@ -1634,7 +1634,7 @@ def screenshot( """ img = QImg2array( - self._screenshot(size, scale, flash, canvas_only, margins) + self._screenshot(size, scale, flash, canvas_only, no_margins) ) if path is not None: imsave(path, img) diff --git a/napari/_tests/test_with_screenshot.py b/napari/_tests/test_with_screenshot.py index ef50265e897..e5019e2027a 100644 --- a/napari/_tests/test_with_screenshot.py +++ b/napari/_tests/test_with_screenshot.py @@ -113,7 +113,7 @@ def test_z_order_images_after_ndisplay(make_napari_viewer): # Switch to 3D rendering viewer.dims.ndisplay = 3 screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) center = tuple(np.round(np.divide(screenshot.shape[:2], 2)).astype(int)) # Check that blue is still visible @@ -167,32 +167,32 @@ def test_changing_image_colormap(make_napari_viewer): layer = viewer.add_image(data, contrast_limits=[0, 1]) screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) center = tuple(np.round(np.divide(screenshot.shape[:2], 2)).astype(int)) np.testing.assert_almost_equal(screenshot[center], [255, 255, 255, 255]) layer.colormap = 'red' screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) np.testing.assert_almost_equal(screenshot[center], [255, 0, 0, 255]) viewer.dims.ndisplay = 3 screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) np.testing.assert_almost_equal(screenshot[center], [255, 0, 0, 255]) layer.colormap = 'blue' screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) np.testing.assert_almost_equal(screenshot[center], [0, 0, 255, 255]) viewer.dims.ndisplay = 2 screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) np.testing.assert_almost_equal(screenshot[center], [0, 0, 255, 255]) @@ -207,32 +207,32 @@ def test_changing_image_gamma(make_napari_viewer): layer = viewer.add_image(data, contrast_limits=[0, 2]) screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) center = tuple(np.round(np.divide(screenshot.shape[:2], 2)).astype(int)) assert 127 <= screenshot[(*center, 0)] <= 129 layer.gamma = 0.1 screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) assert screenshot[(*center, 0)] > 230 viewer.dims.ndisplay = 3 screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) assert screenshot[(*center, 0)] > 230 layer.gamma = 1.9 screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) assert screenshot[(*center, 0)] < 80 viewer.dims.ndisplay = 2 screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) assert screenshot[(*center, 0)] < 80 @@ -256,7 +256,7 @@ def test_grid_mode(make_napari_viewer): # check screenshot screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) center = tuple(np.round(np.divide(screenshot.shape[:2], 2)).astype(int)) np.testing.assert_almost_equal(screenshot[center], [0, 0, 255, 255]) @@ -279,7 +279,7 @@ def test_grid_mode(make_napari_viewer): # check screenshot screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) # sample 6 squares of the grid and check they have right colors pos = [ @@ -311,7 +311,7 @@ def test_grid_mode(make_napari_viewer): # check screenshot screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) # CGRMYB color order color = [ @@ -339,7 +339,7 @@ def test_grid_mode(make_napari_viewer): # check screenshot screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) center = tuple(np.round(np.divide(screenshot.shape[:2], 2)).astype(int)) np.testing.assert_almost_equal(screenshot[center], [0, 255, 255, 255]) @@ -359,7 +359,7 @@ def test_changing_image_attenuation(make_napari_viewer): # normal mip viewer.layers[0].rendering = 'mip' screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) center = tuple(np.round(np.divide(screenshot.shape[:2], 2)).astype(int)) mip_value = screenshot[center][0] @@ -368,14 +368,14 @@ def test_changing_image_attenuation(make_napari_viewer): viewer.layers[0].rendering = 'attenuated_mip' viewer.layers[0].attenuation = 0.0 screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) zero_att_value = screenshot[center][0] # increase attenuation viewer.layers[0].attenuation = 0.5 screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) more_att_value = screenshot[center][0] # Check that rendering has been attenuated @@ -393,7 +393,7 @@ def test_labels_painting(make_napari_viewer): layer = viewer.layers[0] screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) # Check that no painting has occurred @@ -440,7 +440,7 @@ def test_labels_painting(make_napari_viewer): mouse_press_callbacks(layer, event) screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) # Check that painting has now occurred assert layer.data.max() > 0 @@ -456,7 +456,7 @@ def test_welcome(make_napari_viewer): # Check something is visible screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) assert len(viewer.layers) == 0 assert screenshot[..., :-1].max() > 0 @@ -464,7 +464,7 @@ def test_welcome(make_napari_viewer): # Check adding zeros image makes it go away viewer.add_image(np.zeros((1, 1))) screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) assert len(viewer.layers) == 1 assert screenshot[..., :-1].max() == 0 @@ -472,7 +472,7 @@ def test_welcome(make_napari_viewer): # Remove layer and check something is visible again viewer.layers.pop(0) screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) assert len(viewer.layers) == 0 assert screenshot[..., :-1].max() > 0 @@ -487,14 +487,14 @@ def test_axes_visible(make_napari_viewer): # Check axes are not visible launch_screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) assert not viewer.axes.visible # Make axes visible and check something is seen viewer.axes.visible = True on_screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) assert viewer.axes.visible assert abs(on_screenshot - launch_screenshot).max() > 0 @@ -502,7 +502,7 @@ def test_axes_visible(make_napari_viewer): # Make axes not visible and check they are gone viewer.axes.visible = False off_screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) assert not viewer.axes.visible np.testing.assert_almost_equal(launch_screenshot, off_screenshot) @@ -517,14 +517,14 @@ def test_scale_bar_visible(make_napari_viewer): # Check scale bar is not visible launch_screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) assert not viewer.scale_bar.visible # Make scale bar visible and check something is seen viewer.scale_bar.visible = True on_screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) assert viewer.scale_bar.visible assert abs(on_screenshot - launch_screenshot).max() > 0 @@ -532,7 +532,7 @@ def test_scale_bar_visible(make_napari_viewer): # Make scale bar not visible and check it is gone viewer.scale_bar.visible = False off_screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) assert not viewer.scale_bar.visible np.testing.assert_almost_equal(launch_screenshot, off_screenshot) @@ -549,7 +549,7 @@ def test_screenshot_has_no_border(make_napari_viewer): viewer.camera.zoom = 1000 screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) expected = np.broadcast_to([255, 0, 0, 255], screenshot.shape) @@ -575,7 +575,7 @@ def test_blending_modes_with_canvas(make_napari_viewer): img1_layer.blending = 'additive' img2_layer.blending = 'additive' screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) np.testing.assert_array_equal(screenshot[:, :, 0], img1 + img2) @@ -583,13 +583,13 @@ def test_blending_modes_with_canvas(make_napari_viewer): img1_layer.blending = 'minimum' img2_layer.blending = 'minimum' screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) np.testing.assert_array_equal(screenshot[:, :, 0], np.minimum(img1, img2)) # toggle visibility of bottom layer img1_layer.visible = False screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) np.testing.assert_array_equal(screenshot[:, :, 0], img2) # and canvas should not affect the above results @@ -600,13 +600,13 @@ def test_blending_modes_with_canvas(make_napari_viewer): img1_layer.blending = 'additive' img2_layer.blending = 'additive' screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) np.testing.assert_array_equal(screenshot[:, :, 0], img1 + img2) # toggle visibility of bottom layer img1_layer.visible = False screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) np.testing.assert_array_equal(screenshot[:, :, 0], img2) # minimum should always work with white canvas bgcolor @@ -614,6 +614,6 @@ def test_blending_modes_with_canvas(make_napari_viewer): img1_layer.blending = 'minimum' img2_layer.blending = 'minimum' screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) np.testing.assert_array_equal(screenshot[:, :, 0], np.minimum(img1, img2)) diff --git a/napari/_vispy/_tests/test_vispy_multiscale.py b/napari/_vispy/_tests/test_vispy_multiscale.py index 5ee1e055410..f1a945f39f3 100644 --- a/napari/_vispy/_tests/test_vispy_multiscale.py +++ b/napari/_vispy/_tests/test_vispy_multiscale.py @@ -72,7 +72,7 @@ def test_multiscale_screenshot(make_napari_viewer): viewer.window._qt_viewer.canvas.view.canvas.size = (800, 600) screenshot = viewer.screenshot( - canvas_only=True, flash=False, margins=False + canvas_only=True, flash=False, no_margins=False ) center_coord = np.round(np.array(screenshot.shape[:2]) / 2).astype(int) target_center = np.array([255, 255, 255, 255], dtype='uint8') diff --git a/napari/viewer.py b/napari/viewer.py index 6e685da0e80..9fbab33555c 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -96,7 +96,7 @@ def screenshot( scale=None, canvas_only=True, flash: bool = True, - margins: bool = True, + no_margins: bool = True, ): """Take currently displayed screen and convert to an image array. @@ -118,7 +118,7 @@ def screenshot( Flag to indicate whether flash animation should be shown after the screenshot was captured. By default, True. - margins : bool + no_margins : bool Whether to fit a bounding box around the data to prevent margins of showing in the screenshot. Returns @@ -133,7 +133,7 @@ def screenshot( scale=scale, flash=flash, canvas_only=canvas_only, - margins=margins, + no_margins=no_margins, ) def show(self, *, block=False): From 9daa6cb3976daa01fa3045045f3fe97479ae484f Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Mon, 6 May 2024 09:58:54 +0200 Subject: [PATCH 20/52] switch to margins parameter and default old behaviour --- napari/_qt/_tests/test_qt_viewer.py | 4 +- napari/_qt/qt_main_window.py | 17 ++- napari/_tests/test_with_screenshot.py | 144 +++++------------- napari/_vispy/_tests/test_vispy_multiscale.py | 4 +- napari/viewer.py | 7 +- 5 files changed, 52 insertions(+), 124 deletions(-) diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py index 8f56b01fe30..26b72eecf9b 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -266,11 +266,11 @@ def test_screenshot_without_margin(make_napari_viewer): # Add image data = np.ones((10, 15)) viewer.add_image(data) - img = viewer.screenshot(flash=False) + img = viewer.screenshot(flash=False, margins=False) assert img.shape == (10, 15, 4) assert np.all(img == 255) - img = viewer.screenshot(scale=8) + img = viewer.screenshot(margins=False, scale=8) assert img.shape == (80, 120, 4) assert np.all(img == 255) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 0be4f72b53e..624e88cb6c6 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1509,7 +1509,7 @@ def _screenshot( scale=None, flash=True, canvas_only=False, - no_margins: bool = True, + margins: bool = True, ) -> 'QImage': """Capture screenshot of the currently displayed viewer. @@ -1529,8 +1529,9 @@ def _screenshot( If True, screenshot shows only the image display canvas, and if False include the napari viewer frame in the screenshot, By default, True. - no_margins: bool + margins: bool Whether to fit a bounding box around the data to prevent margins of showing in the screenshot. + Currently, if this is False it means a screenshot of the whole data will be generated. Returns ------- @@ -1540,14 +1541,14 @@ def _screenshot( canvas = self._qt_viewer.canvas prev_size = canvas.size - if no_margins: + if not margins: ndisplay = self._qt_viewer.viewer.dims.ndisplay camera = self._qt_viewer.viewer.camera old_center = camera.center old_zoom = camera.zoom if ndisplay > 2: raise NotImplementedError( - 'no_margins is not yet implemented for 3D. Please set no_margins to False' + 'margins equal to False is not yet implemented for 3D. Please set margins to True.' ) self._qt_viewer.viewer.reset_view() @@ -1583,9 +1584,9 @@ def _screenshot( add_flash_animation(self._qt_viewer._welcome_widget) finally: # make sure we always go back to the right canvas size - if size is not None or scale is not None or no_margins: + if size is not None or scale is not None or not margins: canvas.size = prev_size - if no_margins: + if not margins: camera.center = old_center camera.zoom = old_zoom else: @@ -1601,7 +1602,7 @@ def screenshot( scale=None, flash=True, canvas_only=False, - no_margins: bool = True, + margins: bool = True, ): """Take currently displayed viewer and convert to an image array. @@ -1634,7 +1635,7 @@ def screenshot( """ img = QImg2array( - self._screenshot(size, scale, flash, canvas_only, no_margins) + self._screenshot(size, scale, flash, canvas_only, margins) ) if path is not None: imsave(path, img) diff --git a/napari/_tests/test_with_screenshot.py b/napari/_tests/test_with_screenshot.py index e5019e2027a..9e61b4ef116 100644 --- a/napari/_tests/test_with_screenshot.py +++ b/napari/_tests/test_with_screenshot.py @@ -112,9 +112,7 @@ def test_z_order_images_after_ndisplay(make_napari_viewer): # Switch to 3D rendering viewer.dims.ndisplay = 3 - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) center = tuple(np.round(np.divide(screenshot.shape[:2], 2)).astype(int)) # Check that blue is still visible np.testing.assert_almost_equal(screenshot[center], [0, 0, 255, 255]) @@ -166,34 +164,24 @@ def test_changing_image_colormap(make_napari_viewer): data = np.ones((20, 20, 20)) layer = viewer.add_image(data, contrast_limits=[0, 1]) - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) center = tuple(np.round(np.divide(screenshot.shape[:2], 2)).astype(int)) np.testing.assert_almost_equal(screenshot[center], [255, 255, 255, 255]) layer.colormap = 'red' - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) np.testing.assert_almost_equal(screenshot[center], [255, 0, 0, 255]) viewer.dims.ndisplay = 3 - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) np.testing.assert_almost_equal(screenshot[center], [255, 0, 0, 255]) layer.colormap = 'blue' - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) np.testing.assert_almost_equal(screenshot[center], [0, 0, 255, 255]) viewer.dims.ndisplay = 2 - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) np.testing.assert_almost_equal(screenshot[center], [0, 0, 255, 255]) @@ -206,34 +194,24 @@ def test_changing_image_gamma(make_napari_viewer): data = np.ones((20, 20, 20)) layer = viewer.add_image(data, contrast_limits=[0, 2]) - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) center = tuple(np.round(np.divide(screenshot.shape[:2], 2)).astype(int)) assert 127 <= screenshot[(*center, 0)] <= 129 layer.gamma = 0.1 - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) assert screenshot[(*center, 0)] > 230 viewer.dims.ndisplay = 3 - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) assert screenshot[(*center, 0)] > 230 layer.gamma = 1.9 - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) assert screenshot[(*center, 0)] < 80 viewer.dims.ndisplay = 2 - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) assert screenshot[(*center, 0)] < 80 @@ -255,9 +233,7 @@ def test_grid_mode(make_napari_viewer): np.testing.assert_allclose(translations, expected_translations) # check screenshot - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) center = tuple(np.round(np.divide(screenshot.shape[:2], 2)).astype(int)) np.testing.assert_almost_equal(screenshot[center], [0, 0, 255, 255]) @@ -278,9 +254,7 @@ def test_grid_mode(make_napari_viewer): np.testing.assert_allclose(translations, expected_translations[::-1]) # check screenshot - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) # sample 6 squares of the grid and check they have right colors pos = [ (1 / 3, 1 / 4), @@ -310,9 +284,7 @@ def test_grid_mode(make_napari_viewer): viewer.layers.move(1, 6) # check screenshot - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) # CGRMYB color order color = [ [0, 255, 255, 255], @@ -338,9 +310,7 @@ def test_grid_mode(make_napari_viewer): np.testing.assert_allclose(translations, expected_translations) # check screenshot - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) center = tuple(np.round(np.divide(screenshot.shape[:2], 2)).astype(int)) np.testing.assert_almost_equal(screenshot[center], [0, 255, 255, 255]) @@ -358,25 +328,19 @@ def test_changing_image_attenuation(make_napari_viewer): # normal mip viewer.layers[0].rendering = 'mip' - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) center = tuple(np.round(np.divide(screenshot.shape[:2], 2)).astype(int)) mip_value = screenshot[center][0] # zero attenuation (still attenuated!) viewer.layers[0].rendering = 'attenuated_mip' viewer.layers[0].attenuation = 0.0 - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) zero_att_value = screenshot[center][0] # increase attenuation viewer.layers[0].attenuation = 0.5 - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) more_att_value = screenshot[center][0] # Check that rendering has been attenuated assert zero_att_value < more_att_value < mip_value @@ -392,9 +356,7 @@ def test_labels_painting(make_napari_viewer): viewer.add_labels(data) layer = viewer.layers[0] - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) # Check that no painting has occurred assert layer.data.max() == 0 @@ -439,9 +401,7 @@ def test_labels_painting(make_napari_viewer): ) mouse_press_callbacks(layer, event) - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) # Check that painting has now occurred assert layer.data.max() > 0 assert screenshot[:, :, :2].max() > 0 @@ -455,25 +415,19 @@ def test_welcome(make_napari_viewer): viewer = make_napari_viewer(show=True) # Check something is visible - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) assert len(viewer.layers) == 0 assert screenshot[..., :-1].max() > 0 # Check adding zeros image makes it go away viewer.add_image(np.zeros((1, 1))) - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) assert len(viewer.layers) == 1 assert screenshot[..., :-1].max() == 0 # Remove layer and check something is visible again viewer.layers.pop(0) - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) assert len(viewer.layers) == 0 assert screenshot[..., :-1].max() > 0 @@ -486,24 +440,18 @@ def test_axes_visible(make_napari_viewer): viewer.window._qt_viewer.set_welcome_visible(False) # Check axes are not visible - launch_screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + launch_screenshot = viewer.screenshot(canvas_only=True, flash=False) assert not viewer.axes.visible # Make axes visible and check something is seen viewer.axes.visible = True - on_screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + on_screenshot = viewer.screenshot(canvas_only=True, flash=False) assert viewer.axes.visible assert abs(on_screenshot - launch_screenshot).max() > 0 # Make axes not visible and check they are gone viewer.axes.visible = False - off_screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + off_screenshot = viewer.screenshot(canvas_only=True, flash=False) assert not viewer.axes.visible np.testing.assert_almost_equal(launch_screenshot, off_screenshot) @@ -516,24 +464,18 @@ def test_scale_bar_visible(make_napari_viewer): viewer.window._qt_viewer.set_welcome_visible(False) # Check scale bar is not visible - launch_screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + launch_screenshot = viewer.screenshot(canvas_only=True, flash=False) assert not viewer.scale_bar.visible # Make scale bar visible and check something is seen viewer.scale_bar.visible = True - on_screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + on_screenshot = viewer.screenshot(canvas_only=True, flash=False) assert viewer.scale_bar.visible assert abs(on_screenshot - launch_screenshot).max() > 0 # Make scale bar not visible and check it is gone viewer.scale_bar.visible = False - off_screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + off_screenshot = viewer.screenshot(canvas_only=True, flash=False) assert not viewer.scale_bar.visible np.testing.assert_almost_equal(launch_screenshot, off_screenshot) @@ -548,9 +490,7 @@ def test_screenshot_has_no_border(make_napari_viewer): # Zoom in dramatically to make the screenshot all red. viewer.camera.zoom = 1000 - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) expected = np.broadcast_to([255, 0, 0, 255], screenshot.shape) np.testing.assert_array_equal(screenshot, expected) @@ -574,23 +514,17 @@ def test_blending_modes_with_canvas(make_napari_viewer): # check that additive behaves correctly with black canvas img1_layer.blending = 'additive' img2_layer.blending = 'additive' - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) np.testing.assert_array_equal(screenshot[:, :, 0], img1 + img2) # minimum should not result in black background if canvas is black img1_layer.blending = 'minimum' img2_layer.blending = 'minimum' - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) np.testing.assert_array_equal(screenshot[:, :, 0], np.minimum(img1, img2)) # toggle visibility of bottom layer img1_layer.visible = False - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) np.testing.assert_array_equal(screenshot[:, :, 0], img2) # and canvas should not affect the above results viewer.window._qt_viewer.canvas.bgcolor = 'white' @@ -599,21 +533,15 @@ def test_blending_modes_with_canvas(make_napari_viewer): img1_layer.visible = True img1_layer.blending = 'additive' img2_layer.blending = 'additive' - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) np.testing.assert_array_equal(screenshot[:, :, 0], img1 + img2) # toggle visibility of bottom layer img1_layer.visible = False - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) np.testing.assert_array_equal(screenshot[:, :, 0], img2) # minimum should always work with white canvas bgcolor img1_layer.visible = True img1_layer.blending = 'minimum' img2_layer.blending = 'minimum' - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) np.testing.assert_array_equal(screenshot[:, :, 0], np.minimum(img1, img2)) diff --git a/napari/_vispy/_tests/test_vispy_multiscale.py b/napari/_vispy/_tests/test_vispy_multiscale.py index f1a945f39f3..6312641eeb8 100644 --- a/napari/_vispy/_tests/test_vispy_multiscale.py +++ b/napari/_vispy/_tests/test_vispy_multiscale.py @@ -71,9 +71,7 @@ def test_multiscale_screenshot(make_napari_viewer): # Set canvas size to target amount viewer.window._qt_viewer.canvas.view.canvas.size = (800, 600) - screenshot = viewer.screenshot( - canvas_only=True, flash=False, no_margins=False - ) + screenshot = viewer.screenshot(canvas_only=True, flash=False) center_coord = np.round(np.array(screenshot.shape[:2]) / 2).astype(int) target_center = np.array([255, 255, 255, 255], dtype='uint8') target_edge = np.array([0, 0, 0, 255], dtype='uint8') diff --git a/napari/viewer.py b/napari/viewer.py index 9fbab33555c..32374b0af0b 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -96,7 +96,7 @@ def screenshot( scale=None, canvas_only=True, flash: bool = True, - no_margins: bool = True, + margins: bool = True, ): """Take currently displayed screen and convert to an image array. @@ -118,8 +118,9 @@ def screenshot( Flag to indicate whether flash animation should be shown after the screenshot was captured. By default, True. - no_margins : bool + margins : bool Whether to fit a bounding box around the data to prevent margins of showing in the screenshot. + Currently, if this is False it means a screenshot of the whole data will be generated. Returns ------- @@ -133,7 +134,7 @@ def screenshot( scale=scale, flash=flash, canvas_only=canvas_only, - no_margins=no_margins, + margins=margins, ) def show(self, *, block=False): From 3fd75a58066131c475457f2e16bf9c632cbdcbe9 Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Wed, 8 May 2024 17:18:53 +0200 Subject: [PATCH 21/52] back to fit_to_data --- napari/_qt/qt_main_window.py | 4 ++-- napari/viewer.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 7f4aa31fec9..3f56ca78cbf 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1469,7 +1469,7 @@ def _screenshot( scale=None, flash=True, canvas_only=False, - fit_to_data: bool = True, + fit_to_data: bool = False, ) -> 'QImage': """Capture screenshot of the currently displayed viewer. @@ -1556,7 +1556,7 @@ def screenshot( scale=None, flash=True, canvas_only=False, - fit_to_data: bool = True, + fit_to_data: bool = False, ): """Take currently displayed viewer and convert to an image array. diff --git a/napari/viewer.py b/napari/viewer.py index da11a525ef5..bda23bf7534 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -100,7 +100,7 @@ def screenshot( scale=None, canvas_only=True, flash: bool = True, - fit_to_data: bool = True, + fit_to_data: bool = False, ): """Take currently displayed screen and convert to an image array. From 221692c2ae18d672f976dfd1ee39c9619ed39044 Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Sun, 12 May 2024 11:14:20 +0200 Subject: [PATCH 22/52] revert to fit_to_data --- napari/_qt/_tests/test_qt_viewer.py | 19 ++++++++++--------- napari/_qt/qt_main_window.py | 22 ++++++++++++---------- napari/viewer.py | 9 +++++---- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py index 26b72eecf9b..3b5550a65a3 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -259,20 +259,21 @@ def test_screenshot(make_napari_viewer): assert screenshot.ndim == 3 -def test_screenshot_without_margin(make_napari_viewer): +def test_screenshot_fit_to_data(make_napari_viewer): viewer = make_napari_viewer() np.random.seed(0) # Add image - data = np.ones((10, 15)) + data = np.random.randint(150, 250, size=(250, 250)) viewer.add_image(data) - img = viewer.screenshot(flash=False, margins=False) - assert img.shape == (10, 15, 4) - assert np.all(img == 255) - - img = viewer.screenshot(margins=False, scale=8) - assert img.shape == (80, 120, 4) - assert np.all(img == 255) + img = viewer.screenshot(flash=False, fit_to_data=True) + assert img.shape == (250, 250, 4) + assert np.all(img != np.array([0, 0, 0, 1])) + + # TODO: check why this fails in the testing suite but not when testing with scratch script with same example. + img = viewer.screenshot(fit_to_data=True, scale=8) + assert img.shape == (250 * 8, 250 * 8, 4) + assert np.all(img != np.array([0, 0, 0, 1])) @pytest.mark.skip('new approach') diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 624e88cb6c6..69055ed26b0 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1509,7 +1509,7 @@ def _screenshot( scale=None, flash=True, canvas_only=False, - margins: bool = True, + fit_to_data: bool = False, ) -> 'QImage': """Capture screenshot of the currently displayed viewer. @@ -1529,9 +1529,10 @@ def _screenshot( If True, screenshot shows only the image display canvas, and if False include the napari viewer frame in the screenshot, By default, True. - margins: bool + fit_to_data: bool Whether to fit a bounding box around the data to prevent margins of showing in the screenshot. - Currently, if this is False it means a screenshot of the whole data will be generated. + Currently, if this is False it means a screenshot of the current canvas will be generated with + black margins if visible. Returns ------- @@ -1541,7 +1542,7 @@ def _screenshot( canvas = self._qt_viewer.canvas prev_size = canvas.size - if not margins: + if fit_to_data: ndisplay = self._qt_viewer.viewer.dims.ndisplay camera = self._qt_viewer.viewer.camera old_center = camera.center @@ -1584,9 +1585,9 @@ def _screenshot( add_flash_animation(self._qt_viewer._welcome_widget) finally: # make sure we always go back to the right canvas size - if size is not None or scale is not None or not margins: + if size is not None or scale is not None or fit_to_data: canvas.size = prev_size - if not margins: + if fit_to_data: camera.center = old_center camera.zoom = old_zoom else: @@ -1602,7 +1603,7 @@ def screenshot( scale=None, flash=True, canvas_only=False, - margins: bool = True, + fit_to_data: bool = False, ): """Take currently displayed viewer and convert to an image array. @@ -1624,8 +1625,9 @@ def screenshot( If True, screenshot shows only the image display canvas, and if False includes the napari viewer frame in the screenshot, By default, True. - no_margins : bool - Whether to fit a bounding box around the data to prevent margins of showing in the screenshot. + fit_to_data : bool + Whether to fit a bounding box around the data to prevent margins of showing in the screenshot. This fits a + bounding box around all data currently being displayed in the viewer (resets the view). Returns ------- @@ -1635,7 +1637,7 @@ def screenshot( """ img = QImg2array( - self._screenshot(size, scale, flash, canvas_only, margins) + self._screenshot(size, scale, flash, canvas_only, fit_to_data) ) if path is not None: imsave(path, img) diff --git a/napari/viewer.py b/napari/viewer.py index 32374b0af0b..ee6ad3e6f1c 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -96,7 +96,7 @@ def screenshot( scale=None, canvas_only=True, flash: bool = True, - margins: bool = True, + fit_to_data: bool = False, ): """Take currently displayed screen and convert to an image array. @@ -118,9 +118,10 @@ def screenshot( Flag to indicate whether flash animation should be shown after the screenshot was captured. By default, True. - margins : bool + fit_to_data : bool Whether to fit a bounding box around the data to prevent margins of showing in the screenshot. - Currently, if this is False it means a screenshot of the whole data will be generated. + Currently, if this is False it means a screenshot of the whole data will be generated without margins (a + temporary view reset is applied so the canvas has all data within the extent of the canvas). Returns ------- @@ -134,7 +135,7 @@ def screenshot( scale=scale, flash=flash, canvas_only=canvas_only, - margins=margins, + fit_to_data=fit_to_data, ) def show(self, *, block=False): From 41a83264970c86574cee511fd2b4e0153721049c Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sat, 18 May 2024 19:29:50 +0200 Subject: [PATCH 23/52] disallow fit_to_data if canvas_only is False Co-authored-by: olusesan.ajina@gmail.com --- napari/_qt/qt_main_window.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 69055ed26b0..15182d09edf 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1543,6 +1543,10 @@ def _screenshot( canvas = self._qt_viewer.canvas prev_size = canvas.size if fit_to_data: + if not canvas_only: + raise ValueError( + "'fit_to_data' can't be set to True if 'canvas_only' is set to False" + ) ndisplay = self._qt_viewer.viewer.dims.ndisplay camera = self._qt_viewer.viewer.camera old_center = camera.center From 10e86ded4707a7d72a3d4dd21e2932f14fb0162b Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sat, 18 May 2024 20:01:21 +0200 Subject: [PATCH 24/52] adjust test Co-authored-by: olusesan.ajina@gmail.com --- napari/_qt/_tests/test_qt_viewer.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py index 3b5550a65a3..80ea3b39f6f 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -266,14 +266,26 @@ def test_screenshot_fit_to_data(make_napari_viewer): # Add image data = np.random.randint(150, 250, size=(250, 250)) viewer.add_image(data) + + with pytest.raises(ValueError, match="can't be set to True"): + viewer.screenshot(canvas_only=False, fit_to_data=True) + camera_center = viewer.camera.center + camera_zoom = viewer.camera.zoom img = viewer.screenshot(flash=False, fit_to_data=True) + + assert viewer.camera.center == camera_center + assert viewer.camera.zoom == camera_zoom assert img.shape == (250, 250, 4) - assert np.all(img != np.array([0, 0, 0, 1])) + assert np.all(img != np.array([0, 0, 0, 0])) - # TODO: check why this fails in the testing suite but not when testing with scratch script with same example. - img = viewer.screenshot(fit_to_data=True, scale=8) - assert img.shape == (250 * 8, 250 * 8, 4) - assert np.all(img != np.array([0, 0, 0, 1])) + viewer.camera.center = [100, 100] + camera_zoom = viewer.camera.zoom + img = viewer.screenshot(canvas_only=True, fit_to_data=True) + + assert viewer.camera.center == camera_center + assert viewer.camera.zoom == camera_zoom + assert img.shape == (250, 250, 4) + assert np.all(img != np.array([0, 0, 0, 0])) @pytest.mark.skip('new approach') From bffcbb2f07b418543bcc963e5a433b973c031543 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Fri, 24 May 2024 21:08:27 +0200 Subject: [PATCH 25/52] fix test camera center Co-authored-by: olusesan.ajina@gmail.com --- napari/_qt/_tests/test_qt_viewer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py index 80ea3b39f6f..87ce63606e1 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -279,6 +279,7 @@ def test_screenshot_fit_to_data(make_napari_viewer): assert np.all(img != np.array([0, 0, 0, 0])) viewer.camera.center = [100, 100] + camera_center = viewer.camera.center camera_zoom = viewer.camera.zoom img = viewer.screenshot(canvas_only=True, fit_to_data=True) From e0e2498ea645ac0b691a5cab785b83f9174cf387 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 26 May 2024 08:51:26 +0200 Subject: [PATCH 26/52] close viewer prevent dangling animation --- napari/_qt/_tests/test_qt_viewer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py index 87ce63606e1..56bc7827f8e 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -287,6 +287,7 @@ def test_screenshot_fit_to_data(make_napari_viewer): assert viewer.camera.zoom == camera_zoom assert img.shape == (250, 250, 4) assert np.all(img != np.array([0, 0, 0, 0])) + viewer.close() @pytest.mark.skip('new approach') From effce1e3fb5a9257c673d20940cb95f41f2339c4 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 26 May 2024 09:23:28 +0200 Subject: [PATCH 27/52] add fit_to_data to nbscreenshot --- napari/utils/notebook_display.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/napari/utils/notebook_display.py b/napari/utils/notebook_display.py index fd3e27fcadb..512b1b6cbba 100644 --- a/napari/utils/notebook_display.py +++ b/napari/utils/notebook_display.py @@ -52,6 +52,7 @@ def __init__( viewer, *, canvas_only=False, + fit_to_data=False, alt_text=None, ) -> None: """Initialize screenshot object. @@ -64,6 +65,9 @@ def __init__( If False include the napari viewer frame in the screenshot, and if True then take screenshot of just the image display canvas. By default, False. + fit_to_data : bool, optional + Whether to fit a bounding box around the data to prevent margins of showing in the screenshot. This fits a + bounding box around all data currently being displayed in the viewer (resets the view). alt_text : str, optional Image description alternative text, for screenreader accessibility. Good alt-text describes the image and any text within the image @@ -71,6 +75,7 @@ def __init__( """ self.viewer = viewer self.canvas_only = canvas_only + self.fit_to_data = fit_to_data self.image = None self.alt_text = self._clean_alt_text(alt_text) @@ -112,7 +117,9 @@ def _repr_png_(self): get_app().processEvents() self.image = self.viewer.screenshot( - canvas_only=self.canvas_only, flash=False + canvas_only=self.canvas_only, + fit_to_data=self.fit_to_data, + flash=False, ) with BytesIO() as file_obj: imsave_png(file_obj, self.image) From 3a79f081dc7a3367223020f900801bfad789c5b9 Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Mon, 27 May 2024 11:34:30 +0200 Subject: [PATCH 28/52] Update napari/components/viewer_model.py Co-authored-by: Juan Nunez-Iglesias --- napari/components/viewer_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/napari/components/viewer_model.py b/napari/components/viewer_model.py index 90e81575939..9e6efc6320d 100644 --- a/napari/components/viewer_model.py +++ b/napari/components/viewer_model.py @@ -377,7 +377,7 @@ def _sliced_extent_world_augmented(self) -> np.ndarray: ) return self.layers._extent_world_augmented[:, self.dims.displayed] - def reset_view(self, screenshot=False) -> None: + def reset_view(self, *, margin=0.05) -> None: """ Reset the camera view. From 0997e8938df2bd873e2ff4c6cd679a682dde2716 Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Mon, 27 May 2024 11:35:49 +0200 Subject: [PATCH 29/52] change parameter --- napari/_qt/qt_main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 15182d09edf..253c2635816 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1563,7 +1563,7 @@ def _screenshot( ].astype(int) + 1 ) - self._qt_viewer.viewer.reset_view(screenshot=True) + self._qt_viewer.viewer.reset_view(margin=0) if canvas_only: if size is not None: From 33a2a323b729f98ab296a7c6e4f017af47064e0a Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Mon, 27 May 2024 11:41:17 +0200 Subject: [PATCH 30/52] update scale_factor calc --- napari/components/viewer_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/napari/components/viewer_model.py b/napari/components/viewer_model.py index 9e6efc6320d..4fe58f4623d 100644 --- a/napari/components/viewer_model.py +++ b/napari/components/viewer_model.py @@ -414,7 +414,7 @@ def reset_view(self, *, margin=0.05) -> None: # The default value used below will zoom such that the whole field # of view will occupy 95% of the canvas on the most filled axis - scale_factor = 0.95 if not screenshot else 1 + scale_factor = 1 - margin if np.max(size) == 0: self.camera.zoom = scale_factor * np.min(self._canvas_size) From 4bb78548ad9ed7743fe0a508050431b6b7d2ac53 Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Mon, 27 May 2024 13:34:07 +0200 Subject: [PATCH 31/52] Update napari/_qt/qt_main_window.py Co-authored-by: Lorenzo Gaifas --- napari/_qt/qt_main_window.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 253c2635816..fd7371c6bdc 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1530,9 +1530,8 @@ def _screenshot( if False include the napari viewer frame in the screenshot, By default, True. fit_to_data: bool - Whether to fit a bounding box around the data to prevent margins of showing in the screenshot. - Currently, if this is False it means a screenshot of the current canvas will be generated with - black margins if visible. + Tightly fit the canvas around the data to prevent margins of showing in the screenshot. + If False, a screenshot of the whole currently visible canvas will be generated. Returns ------- From e2ebf1b172853e172dd84bbed4ac092cc84d3223 Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Mon, 27 May 2024 13:38:03 +0200 Subject: [PATCH 32/52] update error messages --- napari/_qt/qt_main_window.py | 10 ++++++++-- napari/components/viewer_model.py | 11 +++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 253c2635816..735155c9f1b 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1545,7 +1545,10 @@ def _screenshot( if fit_to_data: if not canvas_only: raise ValueError( - "'fit_to_data' can't be set to True if 'canvas_only' is set to False" + trans._( + "'fit_to_data' can't be set to True if 'canvas_only' is set to False" + ), + deferred=True, ) ndisplay = self._qt_viewer.viewer.dims.ndisplay camera = self._qt_viewer.viewer.camera @@ -1553,7 +1556,10 @@ def _screenshot( old_zoom = camera.zoom if ndisplay > 2: raise NotImplementedError( - 'margins equal to False is not yet implemented for 3D. Please set margins to True.' + trans._( + 'fit_to_data equal to True is not yet implemented for 3D. Please set fit_to_data to False.', + deferred=True, + ) ) self._qt_viewer.viewer.reset_view() diff --git a/napari/components/viewer_model.py b/napari/components/viewer_model.py index 4fe58f4623d..59ae690614b 100644 --- a/napari/components/viewer_model.py +++ b/napari/components/viewer_model.py @@ -414,8 +414,15 @@ def reset_view(self, *, margin=0.05) -> None: # The default value used below will zoom such that the whole field # of view will occupy 95% of the canvas on the most filled axis - scale_factor = 1 - margin - + if 0 <= margin < 1: + scale_factor = 1 - margin + else: + raise ValueError( + trans._( + f'margin must be between 0 and 1; got {margin} instead.', + deferred=True, + ) + ) if np.max(size) == 0: self.camera.zoom = scale_factor * np.min(self._canvas_size) else: From 2f7172875b2d8cfc7df86f518dfb44f51afea39c Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Mon, 27 May 2024 13:40:02 +0200 Subject: [PATCH 33/52] update docstrings --- napari/_qt/qt_main_window.py | 4 ++-- napari/utils/notebook_display.py | 4 ++-- napari/viewer.py | 5 ++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index b3230217c1e..7c9f68a77cd 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1635,8 +1635,8 @@ def screenshot( if False includes the napari viewer frame in the screenshot, By default, True. fit_to_data : bool - Whether to fit a bounding box around the data to prevent margins of showing in the screenshot. This fits a - bounding box around all data currently being displayed in the viewer (resets the view). + Tightly fit the canvas around the data to prevent margins of showing in the screenshot. + If False, a screenshot of the whole currently visible canvas will be generated. Returns ------- diff --git a/napari/utils/notebook_display.py b/napari/utils/notebook_display.py index 512b1b6cbba..13908552054 100644 --- a/napari/utils/notebook_display.py +++ b/napari/utils/notebook_display.py @@ -66,8 +66,8 @@ def __init__( and if True then take screenshot of just the image display canvas. By default, False. fit_to_data : bool, optional - Whether to fit a bounding box around the data to prevent margins of showing in the screenshot. This fits a - bounding box around all data currently being displayed in the viewer (resets the view). + Tightly fit the canvas around the data to prevent margins of showing in the screenshot. + If False, a screenshot of the whole currently visible canvas will be generated. alt_text : str, optional Image description alternative text, for screenreader accessibility. Good alt-text describes the image and any text within the image diff --git a/napari/viewer.py b/napari/viewer.py index ee6ad3e6f1c..fcccee77791 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -119,9 +119,8 @@ def screenshot( the screenshot was captured. By default, True. fit_to_data : bool - Whether to fit a bounding box around the data to prevent margins of showing in the screenshot. - Currently, if this is False it means a screenshot of the whole data will be generated without margins (a - temporary view reset is applied so the canvas has all data within the extent of the canvas). + Tightly fit the canvas around the data to prevent margins of showing in the screenshot. + If False, a screenshot of the whole currently visible canvas will be generated. Returns ------- From 6fcf9be86023ca1a13efa73de826b9a32724da3d Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Mon, 27 May 2024 13:59:50 +0200 Subject: [PATCH 34/52] fix error --- napari/_qt/qt_main_window.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 7c9f68a77cd..e10f85a8f14 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1545,9 +1545,9 @@ def _screenshot( if not canvas_only: raise ValueError( trans._( - "'fit_to_data' can't be set to True if 'canvas_only' is set to False" - ), - deferred=True, + "'fit_to_data' can't be set to True if 'canvas_only' is set to False", + deferred=True, + ) ) ndisplay = self._qt_viewer.viewer.dims.ndisplay camera = self._qt_viewer.viewer.camera From 7635530524c95a0e67cd4df233745e67ca8c349b Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 28 May 2024 00:04:26 +1000 Subject: [PATCH 35/52] minor grammar fix + fit docstring in 80c --- napari/_qt/qt_main_window.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index e10f85a8f14..9afab54a662 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1530,8 +1530,9 @@ def _screenshot( if False include the napari viewer frame in the screenshot, By default, True. fit_to_data: bool - Tightly fit the canvas around the data to prevent margins of showing in the screenshot. - If False, a screenshot of the whole currently visible canvas will be generated. + Tightly fit the canvas around the data to prevent margins from + showing in the screenshot. If False, a screenshot of the whole + currently visible canvas will be generated. Returns ------- From 65729bb9e42bbcac95e421ab3ee93e7987055f38 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 28 May 2024 00:08:25 +1000 Subject: [PATCH 36/52] Make error strings fit in 80c --- napari/_qt/qt_main_window.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 9afab54a662..0b2cff2dd05 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1546,7 +1546,8 @@ def _screenshot( if not canvas_only: raise ValueError( trans._( - "'fit_to_data' can't be set to True if 'canvas_only' is set to False", + "'fit_to_data' cannot be set to True if 'canvas_only' is" + ' set to False', deferred=True, ) ) @@ -1557,7 +1558,8 @@ def _screenshot( if ndisplay > 2: raise NotImplementedError( trans._( - 'fit_to_data equal to True is not yet implemented for 3D. Please set fit_to_data to False.', + 'fit_to_data=True is not yet implemented for 3D. ' + 'Please set fit_to_data to False in 3D view.', deferred=True, ) ) From 1d3140c595f592a7796b2b2e1f22794adba4c71f Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 28 May 2024 00:18:36 +1000 Subject: [PATCH 37/52] Update docstring for screenshot --- napari/_qt/qt_main_window.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 0b2cff2dd05..03766669104 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1637,9 +1637,10 @@ def screenshot( If True, screenshot shows only the image display canvas, and if False includes the napari viewer frame in the screenshot, By default, True. - fit_to_data : bool - Tightly fit the canvas around the data to prevent margins of showing in the screenshot. - If False, a screenshot of the whole currently visible canvas will be generated. + fit_to_data: bool + Tightly fit the canvas around the data to prevent margins from + showing in the screenshot. If False, a screenshot of the whole + currently visible canvas will be generated. Returns ------- From 5d50d644b35c29703e4adaa44b9d1025729c7ba1 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 28 May 2024 00:22:34 +1000 Subject: [PATCH 38/52] Fix outdated docstring for Viewer.reset_view --- napari/components/viewer_model.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/napari/components/viewer_model.py b/napari/components/viewer_model.py index 59ae690614b..d177a200b0f 100644 --- a/napari/components/viewer_model.py +++ b/napari/components/viewer_model.py @@ -378,17 +378,13 @@ def _sliced_extent_world_augmented(self) -> np.ndarray: return self.layers._extent_world_augmented[:, self.dims.displayed] def reset_view(self, *, margin=0.05) -> None: - """ - Reset the camera view. - - This reset has two modes, one for when viewing the data in the viewer and one for when taking a - screenshot with a canvas not showing margins around the data. The two differ in the scaling - factor of the zoom. + """Reset the camera view. Parameters ---------- - screenshot: bool - Whether to reset the view in screenshot mode. Default is False. + margin : float in [0, 1) + Margin as fraction of the canvas, showing blank space around the + data. """ extent = self._sliced_extent_world_augmented From 8e5cba5eb49b582e5e2f286ff9ccd5df3a58ee75 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 28 May 2024 00:26:16 +1000 Subject: [PATCH 39/52] Fix fit-to-data docstring in two more places --- napari/utils/notebook_display.py | 7 ++++--- napari/viewer.py | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/napari/utils/notebook_display.py b/napari/utils/notebook_display.py index 13908552054..a1001c2eda3 100644 --- a/napari/utils/notebook_display.py +++ b/napari/utils/notebook_display.py @@ -65,9 +65,10 @@ def __init__( If False include the napari viewer frame in the screenshot, and if True then take screenshot of just the image display canvas. By default, False. - fit_to_data : bool, optional - Tightly fit the canvas around the data to prevent margins of showing in the screenshot. - If False, a screenshot of the whole currently visible canvas will be generated. + fit_to_data: bool, optional + Tightly fit the canvas around the data to prevent margins from + showing in the screenshot. If False, a screenshot of the whole + currently visible canvas will be generated. alt_text : str, optional Image description alternative text, for screenreader accessibility. Good alt-text describes the image and any text within the image diff --git a/napari/viewer.py b/napari/viewer.py index fcccee77791..0abe1da744a 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -118,9 +118,10 @@ def screenshot( Flag to indicate whether flash animation should be shown after the screenshot was captured. By default, True. - fit_to_data : bool - Tightly fit the canvas around the data to prevent margins of showing in the screenshot. - If False, a screenshot of the whole currently visible canvas will be generated. + fit_to_data: bool, optional + Tightly fit the canvas around the data to prevent margins from + showing in the screenshot. If False, a screenshot of the whole + currently visible canvas will be generated. Returns ------- From 7eabe99edf8deb2bad22e6b64b049575877c2278 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 28 May 2024 00:55:24 +1000 Subject: [PATCH 40/52] Fix test error message match --- napari/_qt/_tests/test_qt_viewer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py index 56bc7827f8e..6716ee7204d 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -267,7 +267,7 @@ def test_screenshot_fit_to_data(make_napari_viewer): data = np.random.randint(150, 250, size=(250, 250)) viewer.add_image(data) - with pytest.raises(ValueError, match="can't be set to True"): + with pytest.raises(ValueError, match='cannot be set to True'): viewer.screenshot(canvas_only=False, fit_to_data=True) camera_center = viewer.camera.center camera_zoom = viewer.camera.zoom From 55d6db914e218b470e8d9685aab181396025f382 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Tue, 28 May 2024 01:23:57 +0200 Subject: [PATCH 41/52] rename parameter --- napari/_qt/_tests/test_qt_viewer.py | 8 ++++---- napari/_qt/qt_main_window.py | 24 +++++++++++++----------- napari/utils/notebook_display.py | 8 ++++---- napari/viewer.py | 6 +++--- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py index 6716ee7204d..eb962907628 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -259,7 +259,7 @@ def test_screenshot(make_napari_viewer): assert screenshot.ndim == 3 -def test_screenshot_fit_to_data(make_napari_viewer): +def test_screenshot_fit_to_data_extent(make_napari_viewer): viewer = make_napari_viewer() np.random.seed(0) @@ -268,10 +268,10 @@ def test_screenshot_fit_to_data(make_napari_viewer): viewer.add_image(data) with pytest.raises(ValueError, match='cannot be set to True'): - viewer.screenshot(canvas_only=False, fit_to_data=True) + viewer.screenshot(canvas_only=False, fit_to_data_extent=True) camera_center = viewer.camera.center camera_zoom = viewer.camera.zoom - img = viewer.screenshot(flash=False, fit_to_data=True) + img = viewer.screenshot(flash=False, fit_to_data_extent=True) assert viewer.camera.center == camera_center assert viewer.camera.zoom == camera_zoom @@ -281,7 +281,7 @@ def test_screenshot_fit_to_data(make_napari_viewer): viewer.camera.center = [100, 100] camera_center = viewer.camera.center camera_zoom = viewer.camera.zoom - img = viewer.screenshot(canvas_only=True, fit_to_data=True) + img = viewer.screenshot(canvas_only=True, fit_to_data_extent=True) assert viewer.camera.center == camera_center assert viewer.camera.zoom == camera_zoom diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 03766669104..4d49a5bf67b 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1509,7 +1509,7 @@ def _screenshot( scale=None, flash=True, canvas_only=False, - fit_to_data: bool = False, + fit_to_data_extent: bool = False, ) -> 'QImage': """Capture screenshot of the currently displayed viewer. @@ -1529,7 +1529,7 @@ def _screenshot( If True, screenshot shows only the image display canvas, and if False include the napari viewer frame in the screenshot, By default, True. - fit_to_data: bool + fit_to_data_extent: bool Tightly fit the canvas around the data to prevent margins from showing in the screenshot. If False, a screenshot of the whole currently visible canvas will be generated. @@ -1542,11 +1542,11 @@ def _screenshot( canvas = self._qt_viewer.canvas prev_size = canvas.size - if fit_to_data: + if fit_to_data_extent: if not canvas_only: raise ValueError( trans._( - "'fit_to_data' cannot be set to True if 'canvas_only' is" + "'fit_to_data_extent' cannot be set to True if 'canvas_only' is" ' set to False', deferred=True, ) @@ -1558,8 +1558,8 @@ def _screenshot( if ndisplay > 2: raise NotImplementedError( trans._( - 'fit_to_data=True is not yet implemented for 3D. ' - 'Please set fit_to_data to False in 3D view.', + 'fit_to_data_extent=True is not yet implemented for 3D. ' + 'Please set fit_to_data_extent to False in 3D view.', deferred=True, ) ) @@ -1597,9 +1597,9 @@ def _screenshot( add_flash_animation(self._qt_viewer._welcome_widget) finally: # make sure we always go back to the right canvas size - if size is not None or scale is not None or fit_to_data: + if size is not None or scale is not None or fit_to_data_extent: canvas.size = prev_size - if fit_to_data: + if fit_to_data_extent: camera.center = old_center camera.zoom = old_zoom else: @@ -1615,7 +1615,7 @@ def screenshot( scale=None, flash=True, canvas_only=False, - fit_to_data: bool = False, + fit_to_data_extent: bool = False, ): """Take currently displayed viewer and convert to an image array. @@ -1637,7 +1637,7 @@ def screenshot( If True, screenshot shows only the image display canvas, and if False includes the napari viewer frame in the screenshot, By default, True. - fit_to_data: bool + fit_to_data_extent: bool Tightly fit the canvas around the data to prevent margins from showing in the screenshot. If False, a screenshot of the whole currently visible canvas will be generated. @@ -1650,7 +1650,9 @@ def screenshot( """ img = QImg2array( - self._screenshot(size, scale, flash, canvas_only, fit_to_data) + self._screenshot( + size, scale, flash, canvas_only, fit_to_data_extent + ) ) if path is not None: imsave(path, img) diff --git a/napari/utils/notebook_display.py b/napari/utils/notebook_display.py index a1001c2eda3..1dbaecd28ed 100644 --- a/napari/utils/notebook_display.py +++ b/napari/utils/notebook_display.py @@ -52,7 +52,7 @@ def __init__( viewer, *, canvas_only=False, - fit_to_data=False, + fit_to_data_extent=False, alt_text=None, ) -> None: """Initialize screenshot object. @@ -65,7 +65,7 @@ def __init__( If False include the napari viewer frame in the screenshot, and if True then take screenshot of just the image display canvas. By default, False. - fit_to_data: bool, optional + fit_to_data_extent: bool, optional Tightly fit the canvas around the data to prevent margins from showing in the screenshot. If False, a screenshot of the whole currently visible canvas will be generated. @@ -76,7 +76,7 @@ def __init__( """ self.viewer = viewer self.canvas_only = canvas_only - self.fit_to_data = fit_to_data + self.fit_to_data_extent = fit_to_data_extent self.image = None self.alt_text = self._clean_alt_text(alt_text) @@ -119,7 +119,7 @@ def _repr_png_(self): get_app().processEvents() self.image = self.viewer.screenshot( canvas_only=self.canvas_only, - fit_to_data=self.fit_to_data, + fit_to_data_extent=self.fit_to_data_extent, flash=False, ) with BytesIO() as file_obj: diff --git a/napari/viewer.py b/napari/viewer.py index 0abe1da744a..34ae8f64337 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -96,7 +96,7 @@ def screenshot( scale=None, canvas_only=True, flash: bool = True, - fit_to_data: bool = False, + fit_to_data_extent: bool = False, ): """Take currently displayed screen and convert to an image array. @@ -118,7 +118,7 @@ def screenshot( Flag to indicate whether flash animation should be shown after the screenshot was captured. By default, True. - fit_to_data: bool, optional + fit_to_data_extent: bool, optional Tightly fit the canvas around the data to prevent margins from showing in the screenshot. If False, a screenshot of the whole currently visible canvas will be generated. @@ -135,7 +135,7 @@ def screenshot( scale=scale, flash=flash, canvas_only=canvas_only, - fit_to_data=fit_to_data, + fit_to_data_extent=fit_to_data_extent, ) def show(self, *, block=False): From 710565814814211c539d1fb5f274fc61a7624dbc Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sat, 1 Jun 2024 20:49:30 +0200 Subject: [PATCH 42/52] change to export_view Co-authored by: olusesan.ajina@gmail.com --- napari/_qt/_tests/test_qt_viewer.py | 6 +- napari/_qt/qt_main_window.py | 49 ++++++++++--- napari/utils/__init__.py | 9 ++- napari/utils/notebook_display.py | 110 +++++++++++++++++++++++++++- napari/viewer.py | 35 ++++++++- 5 files changed, 188 insertions(+), 21 deletions(-) diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py index eb962907628..2c85bdbb7e5 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -267,11 +267,9 @@ def test_screenshot_fit_to_data_extent(make_napari_viewer): data = np.random.randint(150, 250, size=(250, 250)) viewer.add_image(data) - with pytest.raises(ValueError, match='cannot be set to True'): - viewer.screenshot(canvas_only=False, fit_to_data_extent=True) camera_center = viewer.camera.center camera_zoom = viewer.camera.zoom - img = viewer.screenshot(flash=False, fit_to_data_extent=True) + img = viewer.export_view(flash=False) assert viewer.camera.center == camera_center assert viewer.camera.zoom == camera_zoom @@ -281,7 +279,7 @@ def test_screenshot_fit_to_data_extent(make_napari_viewer): viewer.camera.center = [100, 100] camera_center = viewer.camera.center camera_zoom = viewer.camera.zoom - img = viewer.screenshot(canvas_only=True, fit_to_data_extent=True) + img = viewer.export_view() assert viewer.camera.center == camera_center assert viewer.camera.zoom == camera_zoom diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 4d49a5bf67b..427e55700a6 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1608,6 +1608,44 @@ def _screenshot( add_flash_animation(self._qt_window) return img + def export_view( + self, + path=None, + scale=None, + flash=True, + ): + """Take currently displayed canvas, resets the view and create a screenshot without margins around the data. + + Parameters + ---------- + path : str + Filename for saving screenshot image. + scale : float + Scale factor used to increase resolution of canvas for the screenshot. By default, the currently displayed resolution. + Only used if `canvas_only` is True. + flash : bool + Flag to indicate whether flash animation should be shown after + the screenshot was captured. + By default, True. + + Returns + ------- + image : array + Numpy array of type ubyte and shape (h, w, 4). Index [0, 0] is the + upper-left corner of the rendered region. + """ + img = QImg2array( + self._screenshot( + scale=scale, + flash=flash, + canvas_only=True, + fit_to_data_extent=True, + ) + ) + if path is not None: + imsave(path, img) + return img + def screenshot( self, path=None, @@ -1615,7 +1653,6 @@ def screenshot( scale=None, flash=True, canvas_only=False, - fit_to_data_extent: bool = False, ): """Take currently displayed viewer and convert to an image array. @@ -1637,10 +1674,6 @@ def screenshot( If True, screenshot shows only the image display canvas, and if False includes the napari viewer frame in the screenshot, By default, True. - fit_to_data_extent: bool - Tightly fit the canvas around the data to prevent margins from - showing in the screenshot. If False, a screenshot of the whole - currently visible canvas will be generated. Returns ------- @@ -1649,11 +1682,7 @@ def screenshot( upper-left corner of the rendered region. """ - img = QImg2array( - self._screenshot( - size, scale, flash, canvas_only, fit_to_data_extent - ) - ) + img = QImg2array(self._screenshot(size, scale, flash, canvas_only)) if path is not None: imsave(path, img) return img diff --git a/napari/utils/__init__.py b/napari/utils/__init__.py index dbac3fe4fd6..456773979a8 100644 --- a/napari/utils/__init__.py +++ b/napari/utils/__init__.py @@ -5,7 +5,12 @@ DirectLabelColormap, ) from napari.utils.info import citation_text, sys_info -from napari.utils.notebook_display import NotebookScreenshot, nbscreenshot +from napari.utils.notebook_display import ( + ExportView, + NotebookScreenshot, + export_view, + nbscreenshot, +) from napari.utils.progress import cancelable_progress, progrange, progress __all__ = ( @@ -14,6 +19,8 @@ 'CyclicLabelColormap', 'cancelable_progress', 'citation_text', + 'export_view', + 'ExportView', 'nbscreenshot', 'NotebookScreenshot', 'progrange', diff --git a/napari/utils/notebook_display.py b/napari/utils/notebook_display.py index 1dbaecd28ed..6715dad4ec6 100644 --- a/napari/utils/notebook_display.py +++ b/napari/utils/notebook_display.py @@ -14,7 +14,7 @@ from napari.utils.io import imsave_png -__all__ = ['nbscreenshot', 'NotebookScreenshot'] +__all__ = ['nbscreenshot', 'NotebookScreenshot', 'export_view', 'ExportView'] class NotebookScreenshot: @@ -52,7 +52,6 @@ def __init__( viewer, *, canvas_only=False, - fit_to_data_extent=False, alt_text=None, ) -> None: """Initialize screenshot object. @@ -76,7 +75,6 @@ def __init__( """ self.viewer = viewer self.canvas_only = canvas_only - self.fit_to_data_extent = fit_to_data_extent self.image = None self.alt_text = self._clean_alt_text(alt_text) @@ -119,7 +117,6 @@ def _repr_png_(self): get_app().processEvents() self.image = self.viewer.screenshot( canvas_only=self.canvas_only, - fit_to_data_extent=self.fit_to_data_extent, flash=False, ) with BytesIO() as file_obj: @@ -135,4 +132,109 @@ def _repr_html_(self): return f'{_alt}' +class ExportView: + """Display export_view in the jupyter notebook. + + This is equivalent to viewer.export_view in which a screenshot + of just the canvas is taken with a reset view and the margins + removed. + Functions returning an object with a _repr_png_() method + will displayed as a rich image in the jupyter notebook. + + https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html + + Parameters + ---------- + viewer : napari.Viewer + The napari viewer. + + Examples + -------- + + >>> import napari + >>> from napari.utils import export_view + >>> from skimage.data import chelsea + + >>> viewer = napari.view_image(chelsea(), name='chelsea-the-cat') + >>> export_view(viewer) + # screenshot just the canvas with the napari viewer framing it + >>> export_view(viewer) + + """ + + def __init__( + self, + viewer, + *, + alt_text=None, + ) -> None: + """Initialize screenshot object. + + Parameters + ---------- + viewer : napari.Viewer + The napari viewer + alt_text : str, optional + Image description alternative text, for screenreader accessibility. + Good alt-text describes the image and any text within the image + in no more than three short, complete sentences. + """ + self.viewer = viewer + self.image = None + self.alt_text = self._clean_alt_text(alt_text) + + def _clean_alt_text(self, alt_text): + """Clean user input to prevent script injection.""" + if alt_text is not None: + if lxml_unavailable: + warn( + 'The lxml library is not installed, and is required to ' + 'sanitize alt text for napari screenshots. Alt-text ' + 'will be stripped altogether without lxml.' + ) + return None + # cleaner won't recognize escaped script tags, so always unescape + # to be safe + alt_text = html.unescape(str(alt_text)) + cleaner = Cleaner() + try: + doc = document_fromstring(alt_text) + alt_text = cleaner.clean_html(doc).text_content() + except ParserError: + warn( + 'The provided alt text does not constitute valid html, so it was discarded.', + stacklevel=3, + ) + alt_text = '' + if alt_text == '': + alt_text = None + return alt_text + + def _repr_png_(self): + """PNG representation of the viewer object for IPython. + + Returns + ------- + In memory binary stream containing PNG screenshot image. + """ + from napari._qt.qt_event_loop import get_app + + get_app().processEvents() + self.image = self.viewer.export_view( + flash=False, + ) + with BytesIO() as file_obj: + imsave_png(file_obj, self.image) + file_obj.seek(0) + png = file_obj.read() + return png + + def _repr_html_(self): + png = self._repr_png_() + url = 'data:image/png;base64,' + base64.b64encode(png).decode('utf-8') + _alt = html.escape(self.alt_text) if self.alt_text is not None else '' + return f'{_alt}' + + +export_view = ExportView nbscreenshot = NotebookScreenshot diff --git a/napari/viewer.py b/napari/viewer.py index 34ae8f64337..805b727dee1 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -88,6 +88,39 @@ def update_console(self, variables): return self.window._qt_viewer.console.push(variables) + def export_view( + self, + path=None, + *, + scale=None, + flash: bool = True, + ): + """Take currently displayed canvas, resets the view and create a screenshot without margins around the data. + + Parameters + ---------- + path : str + Filename for saving screenshot image. + scale : float + Scale factor used to increase resolution of canvas for the screenshot. By default, the currently displayed resolution. + Only used if `canvas_only` is True. + flash : bool + Flag to indicate whether flash animation should be shown after + the screenshot was captured. + By default, True. + + Returns + ------- + image : array + Numpy array of type ubyte and shape (h, w, 4). Index [0, 0] is the + upper-left corner of the rendered region. + """ + return self.window.export_view( + path=path, + scale=scale, + flash=flash, + ) + def screenshot( self, path=None, @@ -96,7 +129,6 @@ def screenshot( scale=None, canvas_only=True, flash: bool = True, - fit_to_data_extent: bool = False, ): """Take currently displayed screen and convert to an image array. @@ -135,7 +167,6 @@ def screenshot( scale=scale, flash=flash, canvas_only=canvas_only, - fit_to_data_extent=fit_to_data_extent, ) def show(self, *, block=False): From 6afe24b3eac366b376b09bdc26352a9ea2fced3d Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Fri, 14 Jun 2024 21:11:30 +0200 Subject: [PATCH 43/52] address comments --- napari/_qt/qt_main_window.py | 7 +- napari/utils/__init__.py | 4 -- napari/utils/notebook_display.py | 114 +------------------------------ 3 files changed, 3 insertions(+), 122 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 427e55700a6..3cf04ec7b62 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1647,12 +1647,7 @@ def export_view( return img def screenshot( - self, - path=None, - size=None, - scale=None, - flash=True, - canvas_only=False, + self, path=None, size=None, scale=None, flash=True, canvas_only=False ): """Take currently displayed viewer and convert to an image array. diff --git a/napari/utils/__init__.py b/napari/utils/__init__.py index 456773979a8..266f00ae8b1 100644 --- a/napari/utils/__init__.py +++ b/napari/utils/__init__.py @@ -6,9 +6,7 @@ ) from napari.utils.info import citation_text, sys_info from napari.utils.notebook_display import ( - ExportView, NotebookScreenshot, - export_view, nbscreenshot, ) from napari.utils.progress import cancelable_progress, progrange, progress @@ -19,8 +17,6 @@ 'CyclicLabelColormap', 'cancelable_progress', 'citation_text', - 'export_view', - 'ExportView', 'nbscreenshot', 'NotebookScreenshot', 'progrange', diff --git a/napari/utils/notebook_display.py b/napari/utils/notebook_display.py index 6715dad4ec6..fd3e27fcadb 100644 --- a/napari/utils/notebook_display.py +++ b/napari/utils/notebook_display.py @@ -14,7 +14,7 @@ from napari.utils.io import imsave_png -__all__ = ['nbscreenshot', 'NotebookScreenshot', 'export_view', 'ExportView'] +__all__ = ['nbscreenshot', 'NotebookScreenshot'] class NotebookScreenshot: @@ -64,10 +64,6 @@ def __init__( If False include the napari viewer frame in the screenshot, and if True then take screenshot of just the image display canvas. By default, False. - fit_to_data_extent: bool, optional - Tightly fit the canvas around the data to prevent margins from - showing in the screenshot. If False, a screenshot of the whole - currently visible canvas will be generated. alt_text : str, optional Image description alternative text, for screenreader accessibility. Good alt-text describes the image and any text within the image @@ -116,8 +112,7 @@ def _repr_png_(self): get_app().processEvents() self.image = self.viewer.screenshot( - canvas_only=self.canvas_only, - flash=False, + canvas_only=self.canvas_only, flash=False ) with BytesIO() as file_obj: imsave_png(file_obj, self.image) @@ -132,109 +127,4 @@ def _repr_html_(self): return f'{_alt}' -class ExportView: - """Display export_view in the jupyter notebook. - - This is equivalent to viewer.export_view in which a screenshot - of just the canvas is taken with a reset view and the margins - removed. - Functions returning an object with a _repr_png_() method - will displayed as a rich image in the jupyter notebook. - - https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html - - Parameters - ---------- - viewer : napari.Viewer - The napari viewer. - - Examples - -------- - - >>> import napari - >>> from napari.utils import export_view - >>> from skimage.data import chelsea - - >>> viewer = napari.view_image(chelsea(), name='chelsea-the-cat') - >>> export_view(viewer) - # screenshot just the canvas with the napari viewer framing it - >>> export_view(viewer) - - """ - - def __init__( - self, - viewer, - *, - alt_text=None, - ) -> None: - """Initialize screenshot object. - - Parameters - ---------- - viewer : napari.Viewer - The napari viewer - alt_text : str, optional - Image description alternative text, for screenreader accessibility. - Good alt-text describes the image and any text within the image - in no more than three short, complete sentences. - """ - self.viewer = viewer - self.image = None - self.alt_text = self._clean_alt_text(alt_text) - - def _clean_alt_text(self, alt_text): - """Clean user input to prevent script injection.""" - if alt_text is not None: - if lxml_unavailable: - warn( - 'The lxml library is not installed, and is required to ' - 'sanitize alt text for napari screenshots. Alt-text ' - 'will be stripped altogether without lxml.' - ) - return None - # cleaner won't recognize escaped script tags, so always unescape - # to be safe - alt_text = html.unescape(str(alt_text)) - cleaner = Cleaner() - try: - doc = document_fromstring(alt_text) - alt_text = cleaner.clean_html(doc).text_content() - except ParserError: - warn( - 'The provided alt text does not constitute valid html, so it was discarded.', - stacklevel=3, - ) - alt_text = '' - if alt_text == '': - alt_text = None - return alt_text - - def _repr_png_(self): - """PNG representation of the viewer object for IPython. - - Returns - ------- - In memory binary stream containing PNG screenshot image. - """ - from napari._qt.qt_event_loop import get_app - - get_app().processEvents() - self.image = self.viewer.export_view( - flash=False, - ) - with BytesIO() as file_obj: - imsave_png(file_obj, self.image) - file_obj.seek(0) - png = file_obj.read() - return png - - def _repr_html_(self): - png = self._repr_png_() - url = 'data:image/png;base64,' + base64.b64encode(png).decode('utf-8') - _alt = html.escape(self.alt_text) if self.alt_text is not None else '' - return f'{_alt}' - - -export_view = ExportView nbscreenshot = NotebookScreenshot From f62209715ca471be996499f61d2e7dd334024ccf Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Fri, 14 Jun 2024 21:18:05 +0200 Subject: [PATCH 44/52] remove docstring --- napari/viewer.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/napari/viewer.py b/napari/viewer.py index 805b727dee1..732dbb69ac0 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -150,10 +150,6 @@ def screenshot( Flag to indicate whether flash animation should be shown after the screenshot was captured. By default, True. - fit_to_data_extent: bool, optional - Tightly fit the canvas around the data to prevent margins from - showing in the screenshot. If False, a screenshot of the whole - currently visible canvas will be generated. Returns ------- From cb1e9517de8e6b75cdaa5177f69eec56387ed01a Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Fri, 14 Jun 2024 22:10:29 +0200 Subject: [PATCH 45/52] fix test --- napari/utils/migrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/napari/utils/migrations.py b/napari/utils/migrations.py index 435a5695060..480cedd25d7 100644 --- a/napari/utils/migrations.py +++ b/napari/utils/migrations.py @@ -65,7 +65,7 @@ def _update_from_dict(*args, **kwargs): version=version, since_version=since_version, ), - category=FutureWarning, + category=DeprecationWarning, stacklevel=2, ) kwargs = kwargs.copy() From 21feada364da2c5ec97142e3f35ff20b72b68390 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Fri, 14 Jun 2024 22:31:21 +0200 Subject: [PATCH 46/52] revert to FutureWarning --- napari/utils/_tests/test_register.py | 4 ++-- napari/utils/migrations.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/napari/utils/_tests/test_register.py b/napari/utils/_tests/test_register.py index cd42b65ac99..d5ba4b1dc81 100644 --- a/napari/utils/_tests/test_register.py +++ b/napari/utils/_tests/test_register.py @@ -87,9 +87,9 @@ def test_create_func_deprecated(): def test_create_func_renamed(): DummyClass.add_simple_class_renamed = create_func(SimpleClassRenamed) dc = DummyClass() - with pytest.warns(DeprecationWarning, match="Argument 'c' is deprecated"): + with pytest.warns(FutureWarning, match="Argument 'c' is deprecated"): dc.add_simple_class_renamed(c=4) assert dc.layers[0].a == 4 - with pytest.warns(DeprecationWarning, match="Argument 'd' is deprecated"): + with pytest.warns(FutureWarning, match="Argument 'd' is deprecated"): dc.add_simple_class_renamed(d=8) assert dc.layers[1].b == 8 diff --git a/napari/utils/migrations.py b/napari/utils/migrations.py index 480cedd25d7..435a5695060 100644 --- a/napari/utils/migrations.py +++ b/napari/utils/migrations.py @@ -65,7 +65,7 @@ def _update_from_dict(*args, **kwargs): version=version, since_version=since_version, ), - category=DeprecationWarning, + category=FutureWarning, stacklevel=2, ) kwargs = kwargs.copy() From 9fa60ca8a2090c923d21b0027e036211aaf8e97c Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Thu, 20 Jun 2024 15:03:53 +0200 Subject: [PATCH 47/52] change to export_figure --- napari/_qt/_tests/test_qt_viewer.py | 4 ++-- napari/_qt/qt_main_window.py | 2 +- napari/viewer.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py index 648ebfd59c1..a92caf20c3e 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -279,7 +279,7 @@ def test_screenshot_fit_to_data_extent(make_napari_viewer): camera_center = viewer.camera.center camera_zoom = viewer.camera.zoom - img = viewer.export_view(flash=False) + img = viewer.export_figure(flash=False) assert viewer.camera.center == camera_center assert viewer.camera.zoom == camera_zoom @@ -289,7 +289,7 @@ def test_screenshot_fit_to_data_extent(make_napari_viewer): viewer.camera.center = [100, 100] camera_center = viewer.camera.center camera_zoom = viewer.camera.zoom - img = viewer.export_view() + img = viewer.export_figure() assert viewer.camera.center == camera_center assert viewer.camera.zoom == camera_zoom diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 24703429dff..f48c2bd8ac8 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1645,7 +1645,7 @@ def _screenshot( add_flash_animation(self._qt_window) return img - def export_view( + def export_figure( self, path=None, scale=None, diff --git a/napari/viewer.py b/napari/viewer.py index 732dbb69ac0..146d49317c4 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -88,7 +88,7 @@ def update_console(self, variables): return self.window._qt_viewer.console.push(variables) - def export_view( + def export_figure( self, path=None, *, @@ -115,7 +115,7 @@ def export_view( Numpy array of type ubyte and shape (h, w, 4). Index [0, 0] is the upper-left corner of the rendered region. """ - return self.window.export_view( + return self.window.export_figure( path=path, scale=scale, flash=flash, From 6380901ad824d2ebbc5e79d0ff081b2b015c1f7d Mon Sep 17 00:00:00 2001 From: Peter Sobolewski <76622105+psobolewskiPhD@users.noreply.github.com> Date: Mon, 17 Feb 2025 11:41:46 -0500 Subject: [PATCH 48/52] Update README.md to bump the recommended python to 3.11 (#7610) # References and relevant issues Depends on https://github.com/napari/docs/pull/572 # Description This PR updates the recommended python version in the README installation section to match what will be in the docs. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e0151eefc6..e6700268b30 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ We're working on [tutorials](https://napari.org/stable/tutorials/), but you can It is recommended to install napari into a virtual environment, like this: ```sh -conda create -y -n napari-env -c conda-forge python=3.10 +conda create -y -n napari-env -c conda-forge python=3.11 conda activate napari-env python -m pip install "napari[all]" ``` From 5ad360a1c97ffb68b4e645c8be3afd3b0218cdcf Mon Sep 17 00:00:00 2001 From: Lukasz Migas Date: Tue, 18 Feb 2025 00:34:45 +0100 Subject: [PATCH 49/52] Enable creation of custom linear colormaps in layer controls (#7600) - replace QLabel with QPushButton in the image controls # Description This PR replaces the 'colormap' label (which displays the current colormap) with a push button (which also displays the colormap as before). When the button is pressed, it will ask the user to select a color using color picker. If a color was selected, then it creates a new colormap, otherwise nothing happens. https://github.com/user-attachments/assets/81771c13-cba9-4fec-851d-327748fc2517 --------- Co-authored-by: Juan Nunez-Iglesias Co-authored-by: Wouter-Michiel Vierdag --- napari/_qt/_tests/test_qt_utils.py | 54 +++++++++++++++- .../_tests/test_qt_image_base_layer_.py | 18 +++++- .../layer_controls/qt_image_controls_base.py | 21 +++++-- napari/_qt/utils.py | 61 ++++++++++++++++++- 4 files changed, 145 insertions(+), 9 deletions(-) diff --git a/napari/_qt/_tests/test_qt_utils.py b/napari/_qt/_tests/test_qt_utils.py index e5bc37185ec..b850a1167e3 100644 --- a/napari/_qt/_tests/test_qt_utils.py +++ b/napari/_qt/_tests/test_qt_utils.py @@ -1,10 +1,15 @@ +from unittest.mock import patch + +import numpy as np import pytest from qtpy.QtCore import QObject, Signal -from qtpy.QtWidgets import QMainWindow +from qtpy.QtGui import QColor +from qtpy.QtWidgets import QApplication, QColorDialog, QMainWindow from napari._qt.utils import ( QBYTE_FLAG, add_flash_animation, + get_color, is_qbyte, qbytearray_to_str, qt_might_be_rich_text, @@ -117,3 +122,50 @@ class X: with pytest.raises(RuntimeError): f.result() assert x.a == 2 + + +def test_get_color(qtbot): + """Test the get_color utility function.""" + widget = QMainWindow() + qtbot.addWidget(widget) + + with patch.object(QColorDialog, 'exec_') as mock: + mock.return_value = QColorDialog.Accepted + color = get_color(None, 'hex') + assert isinstance(color, str), 'Expected string color' + + with patch.object(QColorDialog, 'exec_') as mock: + mock.return_value = QColorDialog.Accepted + color = get_color('#FF00FF', 'hex') + assert isinstance(color, str), 'Expected string color' + assert color == '#ff00ff', 'Expected color to be #FF00FF' + + with patch.object(QColorDialog, 'exec_') as mock: + mock.return_value = QColorDialog.Accepted + color = get_color(None, 'array') + assert not isinstance(color, str), 'Expected array color' + assert isinstance(color, np.ndarray), 'Expected numpy array color' + + with patch.object(QColorDialog, 'exec_') as mock: + mock.return_value = QColorDialog.Accepted + color = get_color(np.asarray([255, 0, 255]), 'array') + assert not isinstance(color, str), 'Expected array color' + assert isinstance(color, np.ndarray), 'Expected numpy array color' + np.testing.assert_array_equal(color, np.asarray([1, 0, 1])) + + with patch.object(QColorDialog, 'exec_') as mock: + mock.return_value = QColorDialog.Accepted + color = get_color(None, 'qcolor') + assert not isinstance(color, np.ndarray), 'Expected QColor color' + assert isinstance(color, QColor), 'Expected QColor color' + + with patch.object(QColorDialog, 'exec_') as mock: + mock.return_value = QColorDialog.Rejected + color = get_color(None, 'qcolor') + assert color is None, 'Expected None color' + + # close still open popup widgets + for widget in QApplication.topLevelWidgets(): + if isinstance(widget, QColorDialog): + widget.accept() + widget.close() diff --git a/napari/_qt/layer_controls/_tests/test_qt_image_base_layer_.py b/napari/_qt/layer_controls/_tests/test_qt_image_base_layer_.py index 2d8730b4b11..53d02ceb8f7 100644 --- a/napari/_qt/layer_controls/_tests/test_qt_image_base_layer_.py +++ b/napari/_qt/layer_controls/_tests/test_qt_image_base_layer_.py @@ -4,7 +4,7 @@ import numpy as np import pytest from qtpy.QtCore import Qt -from qtpy.QtWidgets import QPushButton +from qtpy.QtWidgets import QApplication, QColorDialog, QPushButton from napari._qt.layer_controls.qt_image_controls_base import ( QContrastLimitsPopup, @@ -161,3 +161,19 @@ def test_blending_opacity_slider(qtbot): layer.blending = 'translucent' assert layer.blending == 'translucent' assert qtctrl.opacitySlider.isEnabled() + + +@pytest.mark.parametrize('layer', [Image(_IMAGE), Surface(_SURF)]) +def test_custom_colormap(qtbot, layer): + """Test whether colormap button does anything.""" + qtctrl = QtBaseImageControls(layer) + + # check widget popup + assert isinstance(qtctrl.colorbarLabel, QPushButton), ( + 'Colorbar button not found' + ) + qtbot.mouseRelease(qtctrl.colorbarLabel, Qt.MouseButton.LeftButton) + # close still open popup widgets + for widget in QApplication.topLevelWidgets(): + if isinstance(widget, QColorDialog): + widget.close() diff --git a/napari/_qt/layer_controls/qt_image_controls_base.py b/napari/_qt/layer_controls/qt_image_controls_base.py index 509e112ce15..c3e9f9ca4ac 100644 --- a/napari/_qt/layer_controls/qt_image_controls_base.py +++ b/napari/_qt/layer_controls/qt_image_controls_base.py @@ -4,10 +4,9 @@ import numpy as np from qtpy.QtCore import Qt -from qtpy.QtGui import QImage, QPixmap +from qtpy.QtGui import QIcon, QImage, QPixmap from qtpy.QtWidgets import ( QHBoxLayout, - QLabel, QPushButton, QWidget, ) @@ -75,8 +74,8 @@ class QtBaseImageControls(QtLayerControls): Button to transform image layer. clim_popup : napari._qt.qt_range_slider_popup.QRangeSliderPopup Popup widget launching the contrast range slider. - colorbarLabel : qtpy.QtWidgets.QLabel - Label text of colorbar widget. + colorbarLabel : qtpy.QtWidgets.QPushButton + Button showing colorbar widget. Also enables selection of custom colormap. colormapComboBox : qtpy.QtWidgets.QComboBox Dropdown widget for selecting the layer colormap. contrastLimitsSlider : superqt.QRangeSlider @@ -146,9 +145,10 @@ def __init__(self, layer: Image) -> None: connect_setattr(sld.valueChanged, self.layer, 'gamma') self.gammaSlider = sld - self.colorbarLabel = QLabel(parent=self) + self.colorbarLabel = QPushButton(parent=self) self.colorbarLabel.setObjectName('colorbar') self.colorbarLabel.setToolTip(trans._('Colorbar')) + self.colorbarLabel.clicked.connect(self._on_make_colormap) self._on_colormap_change() if self.__class__ == QtBaseImageControls: @@ -158,6 +158,15 @@ def __init__(self, layer: Image) -> None: # widgets. self.layout().addRow(self.button_grid) + def _on_make_colormap(self): + """Make new colormap when colorbarLabel (pushbutton) is pressed.""" + from napari._qt.utils import get_color + from napari.utils.colormaps.colormap_utils import ensure_colormap + + color = get_color(self, mode='hex') + if color: + self.layer.colormap = ensure_colormap(color) + def changeColor(self, text): """Change colormap on the layer model. @@ -215,7 +224,7 @@ def _on_colormap_change(self): cbar.shape[0], QImage.Format_RGBA8888, ) - self.colorbarLabel.setPixmap(QPixmap.fromImage(image)) + self.colorbarLabel.setIcon(QIcon(QPixmap.fromImage(image))) def _on_gamma_change(self): """Receive the layer model gamma change event and update the slider.""" diff --git a/napari/_qt/utils.py b/napari/_qt/utils.py index 51237f2b9ac..32ba1feab31 100644 --- a/napari/_qt/utils.py +++ b/napari/_qt/utils.py @@ -6,6 +6,7 @@ import weakref from collections.abc import Iterable, Sequence from contextlib import contextmanager +from enum import auto from functools import partial import numpy as np @@ -20,6 +21,7 @@ ) from qtpy.QtGui import QColor, QCursor, QDrag, QImage, QPainter, QPixmap from qtpy.QtWidgets import ( + QColorDialog, QGraphicsColorizeEffect, QGraphicsOpacityEffect, QHBoxLayout, @@ -30,13 +32,29 @@ from napari.utils.colormaps.standardize_color import transform_color from napari.utils.events.custom_types import Array -from napari.utils.misc import is_sequence +from napari.utils.misc import StringEnum, is_sequence from napari.utils.translations import trans QBYTE_FLAG = '!QBYTE_' RICH_TEXT_PATTERN = re.compile('<[^\n]+>') +class ColorMode(StringEnum): + """Enum fo selecting the color mode to return the color in. + + ColorMode.HEX + Returns color as hex string. + ColorMode.LOOP + Returns color as a numpy array. + ColorMode.QCOLOR + Returns color as a QColor object + """ + + HEX = auto() + ARRAY = auto() + QCOLOR = auto() + + def is_qbyte(string: str) -> bool: """Check if a string is a QByteArray string. @@ -388,3 +406,44 @@ def in_qt_main_thread() -> bool: True if we are in the main thread, False otherwise. """ return QCoreApplication.instance().thread() == QThread.currentThread() + + +def get_color( + color: str | np.ndarray | QColor | None = None, + mode: ColorMode = ColorMode.HEX, +) -> np.ndarray | None: + """ + Helper function to get a color from q QColorDialog. + + Parameters + ---------- + color : str | np.ndarray | QColor | None + Initial color to display in the dialog. Color will be automatically converted to QColor. + mode : ColorMode + Mode to return the color in (hex, array, QColor). + + Returns + ------- + new_color : str | np.ndarray | QColor + New color in the desired format. + """ + + if isinstance(color, str): + color = QColor(color) + elif isinstance(color, np.ndarray): + color = QColor(*color.astype(int)) + + dlg = QColorDialog(color) + new_color: str | np.ndarray | QColor | None = None + if dlg.exec_(): + new_color = dlg.currentColor() + if mode == ColorMode.HEX: + new_color = new_color.name() + elif mode == ColorMode.ARRAY: + new_color = ( + np.asarray( + [new_color.red(), new_color.green(), new_color.blue()] + ) + / 255 + ) + return new_color From b2f722fd16e08d84cdf5f9af9cbd08b81639bf72 Mon Sep 17 00:00:00 2001 From: Carol Willing Date: Mon, 17 Feb 2025 23:35:47 -0800 Subject: [PATCH 50/52] Add link to napari weather report dashboard in README.md (#7609) # References and relevant issues # Description Adds a link in the README doc under the contributing section to the [napari weather report dashboard](https://napari.org/weather-report/) --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e6700268b30..57373680533 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,9 @@ You can see details of [the project roadmap here](https://napari.org/stable/road Contributions are encouraged! Please read our [contributing guide](https://napari.org/dev/developers/contributing/index.html) to get started. Given that we're in an early stage, you may want to reach out on our [GitHub Issues](https://github.com/napari/napari/issues) before jumping in. -If you want to contribute or edit to our documentation, please go to [napari/docs](https://github.com/napari/docs). +If you want to contribute to or edit our documentation, please go to [napari/docs](https://github.com/napari/docs). + +Visit our [project weather report dashboard](https://napari.org/weather-report/) to see metrics and how development is progressing. ## code of conduct From 26f7af4366e41c2c6fba1c07e5bbd908b231ec39 Mon Sep 17 00:00:00 2001 From: Lukasz Migas Date: Tue, 18 Feb 2025 13:28:03 +0100 Subject: [PATCH 51/52] Fix layout issue in image/surface controls (#7618) This fixes a minor layout issue for the image/surface layer controls (which use. the colormapComboBox. I noticed that if the width of the layer controls is changed, the 'colormap' combobox doesn't get resized (because there was a stretch after the control). I should have noticed in #7600 but clearly missed it. before ![image](https://github.com/user-attachments/assets/a5644564-274b-4ba6-b522-c2639a22fed4) after ![image](https://github.com/user-attachments/assets/7ec326c8-3441-4be2-9dbb-3c4e293fef78) # References and relevant issues # Description --- napari/_qt/layer_controls/qt_image_controls.py | 3 +-- napari/_qt/layer_controls/qt_surface_controls.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/napari/_qt/layer_controls/qt_image_controls.py b/napari/_qt/layer_controls/qt_image_controls.py index 776ba7a2da1..92f39474dce 100644 --- a/napari/_qt/layer_controls/qt_image_controls.py +++ b/napari/_qt/layer_controls/qt_image_controls.py @@ -185,8 +185,7 @@ def __init__(self, layer) -> None: self.colorbarLabel.setVisible(False) else: colormap_layout.addWidget(self.colorbarLabel) - colormap_layout.addWidget(self.colormapComboBox) - colormap_layout.addStretch(1) + colormap_layout.addWidget(self.colormapComboBox, stretch=1) self.layout().addRow(self.button_grid) self.layout().addRow(self.opacityLabel, self.opacitySlider) diff --git a/napari/_qt/layer_controls/qt_surface_controls.py b/napari/_qt/layer_controls/qt_surface_controls.py index 22defd6e2d1..f8c88dd8dd9 100644 --- a/napari/_qt/layer_controls/qt_surface_controls.py +++ b/napari/_qt/layer_controls/qt_surface_controls.py @@ -52,8 +52,7 @@ def __init__(self, layer) -> None: colormap_layout = QHBoxLayout() colormap_layout.addWidget(self.colorbarLabel) - colormap_layout.addWidget(self.colormapComboBox) - colormap_layout.addStretch(1) + colormap_layout.addWidget(self.colormapComboBox, stretch=1) shading_comboBox = QComboBox(self) for display_name, shading in SHADING_TRANSLATION.items(): From 3a46b9c85e477e6569153b7796483e58d3055523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Melissa=20Weber=20Mendon=C3=A7a?= Date: Wed, 19 Feb 2025 03:59:02 -0300 Subject: [PATCH 52/52] Add example to LayerList and docstrings for link_layers/unlink_layers (#7410) # References and relevant issues Closes #7226 # Description Adds an example to the LayerList docstring including a note on the "changed" event. Also documents `link_layers` and `unlink_layers` to surface them in the API documentation pages. --------- Co-authored-by: Carol Willing Co-authored-by: Juan Nunez-Iglesias --- napari/components/layerlist.py | 60 ++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/napari/components/layerlist.py b/napari/components/layerlist.py index 7259ce5d0c3..45a1040d497 100644 --- a/napari/components/layerlist.py +++ b/napari/components/layerlist.py @@ -49,9 +49,11 @@ class LayerList(SelectableEventedList[Layer]): moved : (index: int, new_index: int, value: T) emitted after ``value`` is moved from ``index`` to ``new_index`` changed : (index: int, old_value: T, value: T) - emitted when item at ``index`` is changed from ``old_value`` to ``value`` + emitted when item at ``index`` is changed from ``old_value`` to + ``value`` changed : (index: slice, old_value: List[_T], value: List[_T]) - emitted when item at ``index`` is changed from ``old_value`` to ``value`` + emitted when items at ``index``es are changed from ``old_value`` to + ``value`` reordered : (value: self) emitted when the list is reordered (eg. moved/reversed). selection.events.changed : (added: Set[_T], removed: Set[_T]) @@ -62,6 +64,49 @@ class LayerList(SelectableEventedList[Layer]): selection.events._current : (value: _T) emitted when the current item has changed. (Private event) + Notes + ----- + + Note that ``changed`` events are only emitted when an element of the + list changes, *not* when the list itself changes (for example when items + are added or removed). For example, ``layerlist.append(layer)`` will emit + an ``inserted`` event. ``layerlist[idx] = layer`` *will* emit a ``changed`` + event. + + However, the layerlist does not have a way of detecting when an object in + the list is modified in-place. Therefore, although + ``layerlist[idx].scale = [2, 1, 1]`` changes the *value* of the layer at + position ``idx``, a ``changed`` event will not be emitted. + + Examples + -------- + + >>> import napari + >>> from skimage.data import astronaut + >>> viewer = napari.Viewer() + >>> event_list = [] + + Connect to the event list: + + >>> viewer.layers.events.connect(event_list.append) + + + >>> viewer.add_image(astronaut()) + + >>> viewer.add_points() + + >>> viewer.layers + [, ] + + Inspecting the list of events, we see: + + >>> event_list[0].type + 'inserting' + >>> viewer.layers.pop(1) + + >>> event_list[-1].type + 'removed' + """ def __init__(self, data=()) -> None: @@ -396,6 +441,12 @@ def link_layers( layers: Iterable[str | Layer] | None = None, attributes: Iterable[str] = (), ): + """ + Links the selected layers. + + Once layers are linked, any action performed on one layer will be + performed on all linked layers at the same time. + """ return self._link_layers('link_layers', layers, attributes) def unlink_layers( @@ -403,6 +454,11 @@ def unlink_layers( layers: Iterable[str | Layer] | None = None, attributes: Iterable[str] = (), ): + """Unlinks previously linked layers. + + Changes to one of the layer's properties no longer result in the same + changes to the previously linked layers. + """ return self._link_layers('unlink_layers', layers, attributes) def save(