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/74] 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/74] 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 4cc0d90e77b2bdadd6cc8d41ab570e86336040b9 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 5 May 2024 19:37:52 +0200 Subject: [PATCH 03/74] 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 04/74] 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 05/74] 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 06/74] 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 07/74] 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 08/74] 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 09/74] 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 10/74] 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 221692c2ae18d672f976dfd1ee39c9619ed39044 Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Sun, 12 May 2024 11:14:20 +0200 Subject: [PATCH 11/74] 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 12/74] 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 13/74] 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 14/74] 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 15/74] 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 16/74] 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 17/74] 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 18/74] 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 19/74] 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 20/74] 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 21/74] 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 22/74] 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 23/74] 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 24/74] 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 25/74] 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 26/74] 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 27/74] 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 28/74] 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 29/74] 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 30/74] 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 31/74] 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 32/74] 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 33/74] 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 34/74] 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 35/74] 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 36/74] 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 4b6c429ff242929d90160ead4a1d006ed9617909 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sat, 22 Jun 2024 18:56:26 +1000 Subject: [PATCH 37/74] Match docstring formatting to PEP257 and clarify scale --- napari/_qt/qt_main_window.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index b67ba89cdaa..b6096edd59e 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1645,20 +1645,22 @@ def _screenshot( add_flash_animation(self._qt_window) return img - def export_figure( - self, - path=None, - scale=None, - flash=True, - ): - """Take currently displayed canvas, resets the view and create a screenshot without margins around the data. + def export_figure(self, path=None, scale=None, flash=True): + """Temporarily reset the view and create a screenshot without margins. + + This function finds a tight boundary around the data, resets the view + around that boundary (and, when scale=1, such that 1 captured pixel is + equivalent to one data pixel), takes a screenshot, then restores the + previous zoom and canvas sizes. 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. + Scale factor used to increase resolution of canvas for the + screenshot. By default, the currently displayed resolution. A scale + of 1 corresponds to 1 data pixel per screenshot pixel. Only used if `canvas_only` is True. flash : bool Flag to indicate whether flash animation should be shown after From 21f03cb60677179f21fdac522d5ec6c71511f753 Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Sun, 23 Jun 2024 09:10:48 +0200 Subject: [PATCH 38/74] Update napari/_qt/_tests/test_qt_viewer.py Co-authored-by: Lorenzo Gaifas --- 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 713debd776b..d3486d91afe 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -269,7 +269,7 @@ def test_screenshot(make_napari_viewer): assert screenshot.ndim == 3 -def test_screenshot_fit_to_data_extent(make_napari_viewer): +def test_export_figure(make_napari_viewer): viewer = make_napari_viewer() np.random.seed(0) From a2eee46c83c5e61ba4b154030a547c71a9cae112 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 23 Jun 2024 09:34:59 +0200 Subject: [PATCH 39/74] adjust docstring --- napari/_qt/qt_main_window.py | 12 ++++++++---- napari/viewer.py | 17 ++++++++++++++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index b6096edd59e..1eebe40fc15 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1646,12 +1646,16 @@ def _screenshot( return img def export_figure(self, path=None, scale=None, flash=True): - """Temporarily reset the view and create a screenshot without margins. + """Export an image of the full extent of the displayed layer data. This function finds a tight boundary around the data, resets the view around that boundary (and, when scale=1, such that 1 captured pixel is equivalent to one data pixel), takes a screenshot, then restores the - previous zoom and canvas sizes. + previous zoom and canvas sizes. For example with one image layer with + an image of 256 x 256 pixels, with scale=1 the size of the resulting + figure will be 256 x 256 pixels. If the scale of the layer for both + dimensions is set to 0.5, it will be 128 x 128 pixels. Currently, + only works when 2 dimensions are displayed. Parameters ---------- @@ -1660,8 +1664,8 @@ def export_figure(self, path=None, scale=None, flash=True): scale : float Scale factor used to increase resolution of canvas for the screenshot. By default, the currently displayed resolution. A scale - of 1 corresponds to 1 data pixel per screenshot pixel. - Only used if `canvas_only` is True. + of 1 corresponds to 1 data pixel per screenshot pixel if all displayed + layers have each displayed dimension set to a scale of 1. flash : bool Flag to indicate whether flash animation should be shown after the screenshot was captured. diff --git a/napari/viewer.py b/napari/viewer.py index 146d49317c4..e053deea552 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -95,15 +95,26 @@ def export_figure( scale=None, flash: bool = True, ): - """Take currently displayed canvas, resets the view and create a screenshot without margins around the data. + """Export an image of the full extent of the displayed layer data. + + This function finds a tight boundary around the data, resets the view + around that boundary (and, when scale=1, such that 1 captured pixel is + equivalent to one data pixel), takes a screenshot, then restores the + previous zoom and canvas sizes. For example with one image layer with + an image of 256 x 256 pixels, with scale=1 the size of the resulting + figure will be 256 x 256 pixels. If the scale of the layer for both + dimensions is set to 0.5, it will be 128 x 128 pixels. Currently, + only works when 2 dimensions are displayed. 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. + Scale factor used to increase resolution of canvas for the + screenshot. By default, the currently displayed resolution. A scale + of 1 corresponds to 1 data pixel per screenshot pixel if all displayed + layers have each displayed dimension set to a scale of 1. flash : bool Flag to indicate whether flash animation should be shown after the screenshot was captured. From 90ca6d39e21747202facd1340f85655bccf6d584 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 23 Jun 2024 10:08:52 +0200 Subject: [PATCH 40/74] typehints --- napari/viewer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/napari/viewer.py b/napari/viewer.py index e053deea552..203e9f3b910 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -90,9 +90,9 @@ def update_console(self, variables): def export_figure( self, - path=None, + path: Optional[str] = None, *, - scale=None, + scale: Optional[float] = None, flash: bool = True, ): """Export an image of the full extent of the displayed layer data. From 657eaacc315c5cc1c18dbb8c0f7dec6a756a92e9 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 23 Jun 2024 10:13:25 +0200 Subject: [PATCH 41/74] typehints --- napari/_qt/qt_main_window.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 1eebe40fc15..2c421c7e1cd 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1645,7 +1645,12 @@ def _screenshot( add_flash_animation(self._qt_window) return img - def export_figure(self, path=None, scale=None, flash=True): + def export_figure( + self, + path: Optional[str] = None, + scale: Optional[float] = None, + flash=True, + ): """Export an image of the full extent of the displayed layer data. This function finds a tight boundary around the data, resets the view From 5cb8a5570b728f8129f17320cee8cd7b56f91b06 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 23 Jun 2024 10:23:26 +0200 Subject: [PATCH 42/74] typehints --- napari/_qt/qt_main_window.py | 16 ++++++++-------- napari/viewer.py | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 2c421c7e1cd..40705971193 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1542,10 +1542,10 @@ def _restart(self): def _screenshot( self, - size=None, - scale=None, - flash=True, - canvas_only=False, + size: Optional[tuple[int, int]] = None, + scale: Optional[float] = None, + flash: bool = True, + canvas_only: bool = False, fit_to_data_extent: bool = False, ) -> 'QImage': """Capture screenshot of the currently displayed viewer. @@ -1555,10 +1555,10 @@ def _screenshot( flash : bool Flag to indicate whether flash animation should be shown after the screenshot was captured. - size : tuple (int, int) + size : Optional[int, int] Size (resolution height x width) of the screenshot. By default, the currently displayed size. Only used if `canvas_only` is True. - scale : float + scale : Optional[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. @@ -1664,9 +1664,9 @@ def export_figure( Parameters ---------- - path : str + path : Optional[str] Filename for saving screenshot image. - scale : float + scale : Optional[float] Scale factor used to increase resolution of canvas for the screenshot. By default, the currently displayed resolution. A scale of 1 corresponds to 1 data pixel per screenshot pixel if all displayed diff --git a/napari/viewer.py b/napari/viewer.py index 203e9f3b910..728f2dfcc0f 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -108,9 +108,9 @@ def export_figure( Parameters ---------- - path : str + path : Optional[str] Filename for saving screenshot image. - scale : float + scale : Optional[float] Scale factor used to increase resolution of canvas for the screenshot. By default, the currently displayed resolution. A scale of 1 corresponds to 1 data pixel per screenshot pixel if all displayed From a80e64f0290470729d8e76e4786f22f60a6fbbef Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 23 Jun 2024 10:28:37 +0200 Subject: [PATCH 43/74] typehints --- napari/viewer.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/napari/viewer.py b/napari/viewer.py index 728f2dfcc0f..beaaf8d3889 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -134,23 +134,23 @@ def export_figure( def screenshot( self, - path=None, + path: Optional[str] = None, *, - size=None, - scale=None, - canvas_only=True, + size: Optional[tuple[str, str]] = None, + scale: Optional[float] = None, + canvas_only: bool = True, flash: bool = True, ): """Take currently displayed screen and convert to an image array. Parameters ---------- - path : str + path : Optional[str] Filename for saving screenshot image. - size : tuple (int, int) + size : Optional[tuple[str, str]] Size (resolution height x width) of the screenshot. By default, the currently displayed size. Only used if `canvas_only` is True. - scale : float + scale : Optional[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. canvas_only : bool From b0eed32fc0b28ad72796e996d05bb0e3b495b0b3 Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Sun, 23 Jun 2024 10:30:54 +0200 Subject: [PATCH 44/74] Update napari/components/viewer_model.py Co-authored-by: Grzegorz Bokota --- 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 bc2fe4c875a..9888c3097c5 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, *, margin=0.05) -> None: + def reset_view(self, *, margin: float = 0.05) -> None: """Reset the camera view. Parameters From c142c3c9a38ac37e6d1edd88698a701114b20d7b Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 23 Jun 2024 10:59:31 +0200 Subject: [PATCH 45/74] add example --- examples/export_figure.py | 122 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 examples/export_figure.py diff --git a/examples/export_figure.py b/examples/export_figure.py new file mode 100644 index 00000000000..25703725d6e --- /dev/null +++ b/examples/export_figure.py @@ -0,0 +1,122 @@ +""" +Export Figure +============= + +Display one shapes layer ontop of one image layer using the ``add_shapes`` and +``add_image`` APIs. When the window is closed it will print the coordinates of +your shapes. + +.. tags:: visualization-advanced +""" + +import numpy as np +from skimage import data +from vispy.color import Colormap + +import napari + +# create the viewer and window +viewer = napari.Viewer() + +# add the image +img_layer = viewer.add_image(data.camera(), name='photographer') +img_layer.colormap = 'gray' + +# create a list of polygons +polygons = [ + np.array([[11, 13], [111, 113], [22, 246]]), + np.array( + [ + [505, 60], + [402, 71], + [383, 42], + [251, 95], + [212, 59], + [131, 137], + [126, 187], + [191, 204], + [171, 248], + [211, 260], + [273, 243], + [264, 225], + [430, 173], + [512, 160], + ] + ), + np.array( + [ + [310, 382], + [229, 381], + [209, 401], + [221, 411], + [258, 411], + [300, 412], + [306, 435], + [268, 434], + [265, 454], + [298, 461], + [307, 461], + [307, 507], + [349, 510], + [352, 369], + [330, 366], + [330, 366], + ] + ), +] + +# add polygons +layer = viewer.add_shapes( + polygons, + shape_type='polygon', + edge_width=1, + edge_color='coral', + face_color='royalblue', + name='shapes', +) + +# change some attributes of the layer +layer.selected_data = set(range(layer.nshapes)) +layer.current_edge_width = 5 +layer.opacity = 0.75 +layer.selected_data = set() + +# add an ellipse to the layer +ellipse = np.array([[59, 222], [110, 289], [170, 243], [119, 176]]) +layer.add( + ellipse, + shape_type='ellipse', + edge_width=5, + edge_color='coral', + face_color='purple', +) + +masks = layer.to_masks([512, 512]) +masks_layer = viewer.add_image(masks.astype(float), name='masks') +masks_layer.opacity = 0.7 +masks_layer.colormap = Colormap([[0.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 1.0]]) + +labels = layer.to_labels([512, 512]) +labels_layer = viewer.add_labels(labels, name='labels') + +points = np.array([[100, 100], [200, 200], [333, 111]]) +size = np.array([10, 20, 20]) +viewer.add_points(points, size=size) + +# Export figure and change theme before and after exporting to show that the background canvas margins +# are not in the exported figure. +viewer.theme = "light" +# Optionally for saving the exported figure: viewer.export_figure(path="export_figure.png") +export_figure = viewer.export_figure() +scaled_export_figure = viewer.export_figure(scale=5) +viewer.theme = "dark" + +viewer.add_image(export_figure, rgb=True, name='exported_figure') +viewer.add_image(scaled_export_figure, rgb=True, name='scaled_exported_figure') +viewer.reset_view() + +# from skimage.io import imsave +# imsave('screenshot.png', screenshot) + +if __name__ == '__main__': + napari.run() From 5cccde9d841893d3348ae0ada25678157cdd3838 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 23 Jun 2024 13:47:36 +0200 Subject: [PATCH 46/74] set default scale to 1, adjust docstring --- napari/_qt/qt_main_window.py | 6 +++--- napari/viewer.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 40705971193..df58971727c 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1648,7 +1648,7 @@ def _screenshot( def export_figure( self, path: Optional[str] = None, - scale: Optional[float] = None, + scale: float = 1, flash=True, ): """Export an image of the full extent of the displayed layer data. @@ -1658,7 +1658,7 @@ def export_figure( equivalent to one data pixel), takes a screenshot, then restores the previous zoom and canvas sizes. For example with one image layer with an image of 256 x 256 pixels, with scale=1 the size of the resulting - figure will be 256 x 256 pixels. If the scale of the layer for both + figure will be 256 x 256 pixels. If layer.scale for both dimensions is set to 0.5, it will be 128 x 128 pixels. Currently, only works when 2 dimensions are displayed. @@ -1668,7 +1668,7 @@ def export_figure( Filename for saving screenshot image. scale : Optional[float] Scale factor used to increase resolution of canvas for the - screenshot. By default, the currently displayed resolution. A scale + screenshot. By default, a scale of 1. A scale of 1 corresponds to 1 data pixel per screenshot pixel if all displayed layers have each displayed dimension set to a scale of 1. flash : bool diff --git a/napari/viewer.py b/napari/viewer.py index beaaf8d3889..d4a01ef1162 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -92,7 +92,7 @@ def export_figure( self, path: Optional[str] = None, *, - scale: Optional[float] = None, + scale: float = 1, flash: bool = True, ): """Export an image of the full extent of the displayed layer data. @@ -102,7 +102,7 @@ def export_figure( equivalent to one data pixel), takes a screenshot, then restores the previous zoom and canvas sizes. For example with one image layer with an image of 256 x 256 pixels, with scale=1 the size of the resulting - figure will be 256 x 256 pixels. If the scale of the layer for both + figure will be 256 x 256 pixels. If layer.scale for both dimensions is set to 0.5, it will be 128 x 128 pixels. Currently, only works when 2 dimensions are displayed. @@ -110,9 +110,9 @@ def export_figure( ---------- path : Optional[str] Filename for saving screenshot image. - scale : Optional[float] + scale : float Scale factor used to increase resolution of canvas for the - screenshot. By default, the currently displayed resolution. A scale + screenshot. By default, a scale of 1. A scale of 1 corresponds to 1 data pixel per screenshot pixel if all displayed layers have each displayed dimension set to a scale of 1. flash : bool From fbe7c5817ab6a7716215dd8469f9c1a9b2721508 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 23 Jun 2024 13:54:20 +0200 Subject: [PATCH 47/74] adjust example and docstrings --- examples/export_figure.py | 12 ------------ napari/_qt/qt_main_window.py | 4 ++-- napari/viewer.py | 2 +- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/examples/export_figure.py b/examples/export_figure.py index 25703725d6e..8dd54a5f1c3 100644 --- a/examples/export_figure.py +++ b/examples/export_figure.py @@ -11,7 +11,6 @@ import numpy as np from skimage import data -from vispy.color import Colormap import napari @@ -75,12 +74,6 @@ name='shapes', ) -# change some attributes of the layer -layer.selected_data = set(range(layer.nshapes)) -layer.current_edge_width = 5 -layer.opacity = 0.75 -layer.selected_data = set() - # add an ellipse to the layer ellipse = np.array([[59, 222], [110, 289], [170, 243], [119, 176]]) layer.add( @@ -91,11 +84,6 @@ face_color='purple', ) -masks = layer.to_masks([512, 512]) -masks_layer = viewer.add_image(masks.astype(float), name='masks') -masks_layer.opacity = 0.7 -masks_layer.colormap = Colormap([[0.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 1.0]]) - labels = layer.to_labels([512, 512]) labels_layer = viewer.add_labels(labels, name='labels') diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index df58971727c..70af3a11397 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1666,11 +1666,11 @@ def export_figure( ---------- path : Optional[str] Filename for saving screenshot image. - scale : Optional[float] + scale : float Scale factor used to increase resolution of canvas for the screenshot. By default, a scale of 1. A scale of 1 corresponds to 1 data pixel per screenshot pixel if all displayed - layers have each displayed dimension set to a scale of 1. + layers have layer.scale for each displayed dimension set to 1. flash : bool Flag to indicate whether flash animation should be shown after the screenshot was captured. diff --git a/napari/viewer.py b/napari/viewer.py index d4a01ef1162..64ca447acb2 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -114,7 +114,7 @@ def export_figure( Scale factor used to increase resolution of canvas for the screenshot. By default, a scale of 1. A scale of 1 corresponds to 1 data pixel per screenshot pixel if all displayed - layers have each displayed dimension set to a scale of 1. + layers have layer.scale for each displayed dimension set to 1. flash : bool Flag to indicate whether flash animation should be shown after the screenshot was captured. From 74aa2621e95d89430a9f30ea975ca09786f62124 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 23 Jun 2024 13:58:06 +0200 Subject: [PATCH 48/74] adjust explanation docstring --- napari/_qt/qt_main_window.py | 6 +++--- napari/viewer.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 70af3a11397..aa5862b42d3 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1657,9 +1657,9 @@ def export_figure( around that boundary (and, when scale=1, such that 1 captured pixel is equivalent to one data pixel), takes a screenshot, then restores the previous zoom and canvas sizes. For example with one image layer with - an image of 256 x 256 pixels, with scale=1 the size of the resulting - figure will be 256 x 256 pixels. If layer.scale for both - dimensions is set to 0.5, it will be 128 x 128 pixels. Currently, + an image of 256 x 256 pixels and layer.scale=[1,1], with scale=1 the + size of the resulting figure will be 256 x 256 pixels. If layer.scale + for both dimensions is set to 0.5, it will be 128 x 128 pixels. Currently, only works when 2 dimensions are displayed. Parameters diff --git a/napari/viewer.py b/napari/viewer.py index 64ca447acb2..e5b0e775ebf 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -101,9 +101,9 @@ def export_figure( around that boundary (and, when scale=1, such that 1 captured pixel is equivalent to one data pixel), takes a screenshot, then restores the previous zoom and canvas sizes. For example with one image layer with - an image of 256 x 256 pixels, with scale=1 the size of the resulting - figure will be 256 x 256 pixels. If layer.scale for both - dimensions is set to 0.5, it will be 128 x 128 pixels. Currently, + an image of 256 x 256 pixels and layer.scale=[1,1], with scale=1 the + size of the resulting figure will be 256 x 256 pixels. If layer.scale + for both dimensions is set to 0.5, it will be 128 x 128 pixels. Currently, only works when 2 dimensions are displayed. Parameters From e28e9d3bf3b45023395c3106530dc38fcc5e4023 Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 23 Jun 2024 21:13:44 +0200 Subject: [PATCH 49/74] Only allow float or int --- napari/_qt/qt_main_window.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index aa5862b42d3..23eb6193b44 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1682,6 +1682,13 @@ def export_figure( Numpy array of type ubyte and shape (h, w, 4). Index [0, 0] is the upper-left corner of the rendered region. """ + if not isinstance(scale, float): + raise TypeError( + trans._( + 'Scale must be a float or an int.', + deferred=True, + ) + ) img = QImg2array( self._screenshot( scale=scale, From b511309d67ce2ede66309653dce2abad6a9323eb Mon Sep 17 00:00:00 2001 From: wmv_hpomen Date: Sun, 23 Jun 2024 21:40:36 +0200 Subject: [PATCH 50/74] fix isinstance --- 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 23eb6193b44..b268b39c2dc 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1682,7 +1682,7 @@ def export_figure( Numpy array of type ubyte and shape (h, w, 4). Index [0, 0] is the upper-left corner of the rendered region. """ - if not isinstance(scale, float): + if not isinstance(scale, (float, int)): raise TypeError( trans._( 'Scale must be a float or an int.', From 3b1fad74ba5015e5e57289929ced4e5bed259df2 Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Mon, 1 Jul 2024 11:36:54 +0200 Subject: [PATCH 51/74] Update napari/_qt/qt_main_window.py Co-authored-by: Grzegorz Bokota --- 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 b268b39c2dc..f7ec9d19cae 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1555,7 +1555,7 @@ def _screenshot( flash : bool Flag to indicate whether flash animation should be shown after the screenshot was captured. - size : Optional[int, int] + size : tuple of two ints, optional Size (resolution height x width) of the screenshot. By default, the currently displayed size. Only used if `canvas_only` is True. scale : Optional[float] From 343182a365fc7b5f7afcde35015b907b17ad3863 Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Mon, 1 Jul 2024 11:51:47 +0200 Subject: [PATCH 52/74] Apply suggestions from code review Co-authored-by: Grzegorz Bokota --- napari/_qt/_tests/test_qt_viewer.py | 6 ++++-- napari/_qt/qt_main_window.py | 17 ++++++++--------- napari/viewer.py | 6 +++--- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py index adff59e05c3..79a36ef414c 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -269,7 +269,7 @@ def test_screenshot(make_napari_viewer): assert screenshot.ndim == 3 -def test_export_figure(make_napari_viewer): +def test_export_figure(make_napari_viewer, tmp_path): viewer = make_napari_viewer() np.random.seed(0) @@ -279,12 +279,14 @@ def test_export_figure(make_napari_viewer): camera_center = viewer.camera.center camera_zoom = viewer.camera.zoom - img = viewer.export_figure(flash=False) + img = viewer.export_figure(flash=False, path=str(tmp_path / "img.png")) 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])) + + assert (tmp_path / "img.png").exist() viewer.camera.center = [100, 100] camera_center = viewer.camera.center diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index f7ec9d19cae..9592c74dcdf 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1558,7 +1558,7 @@ def _screenshot( size : tuple of two ints, optional Size (resolution height x width) of the screenshot. By default, the currently displayed size. Only used if `canvas_only` is True. - scale : Optional[float] + scale : float, optional Scale factor used to increase resolution of canvas for the screenshot. By default, the currently displayed resolution. Only used if `canvas_only` is True. @@ -1579,6 +1579,9 @@ def _screenshot( canvas = self._qt_viewer.canvas prev_size = canvas.size + camera = self._qt_viewer.viewer.camera + old_center = camera.center + old_zoom = camera.zoom if fit_to_data_extent: if not canvas_only: raise ValueError( @@ -1589,9 +1592,6 @@ def _screenshot( ) ) 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( trans._( @@ -1636,9 +1636,8 @@ def _screenshot( # 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_extent: canvas.size = prev_size - if fit_to_data_extent: - camera.center = old_center - camera.zoom = old_zoom + camera.center = old_center + camera.zoom = old_zoom else: img = self._qt_window.grab().toImage() if flash: @@ -1650,7 +1649,7 @@ def export_figure( path: Optional[str] = None, scale: float = 1, flash=True, - ): + ) -> np.ndarray: """Export an image of the full extent of the displayed layer data. This function finds a tight boundary around the data, resets the view @@ -1664,7 +1663,7 @@ def export_figure( Parameters ---------- - path : Optional[str] + path : str, optional Filename for saving screenshot image. scale : float Scale factor used to increase resolution of canvas for the diff --git a/napari/viewer.py b/napari/viewer.py index e5b0e775ebf..0c7fd8307ea 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -145,12 +145,12 @@ def screenshot( Parameters ---------- - path : Optional[str] + path : str, optional Filename for saving screenshot image. - size : Optional[tuple[str, str]] + size : tuple of two ints, optional Size (resolution height x width) of the screenshot. By default, the currently displayed size. Only used if `canvas_only` is True. - scale : Optional[float] + scale : float, optional Scale factor used to increase resolution of canvas for the screenshot. By default, the currently displayed resolution. Only used if `canvas_only` is True. canvas_only : bool From 04a2ba2dd13f68f030d3dd047295568f0998f438 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 09:52:35 +0000 Subject: [PATCH 53/74] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- napari/_qt/_tests/test_qt_viewer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py index 79a36ef414c..b4f428c801f 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -279,14 +279,14 @@ def test_export_figure(make_napari_viewer, tmp_path): camera_center = viewer.camera.center camera_zoom = viewer.camera.zoom - img = viewer.export_figure(flash=False, path=str(tmp_path / "img.png")) + img = viewer.export_figure(flash=False, path=str(tmp_path / 'img.png')) 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])) - - assert (tmp_path / "img.png").exist() + + assert (tmp_path / 'img.png').exist() viewer.camera.center = [100, 100] camera_center = viewer.camera.center From 667795f9e87670a4e1ff1c72b80b241dd73656b4 Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Mon, 1 Jul 2024 12:14:29 +0200 Subject: [PATCH 54/74] add missed import --- napari/_qt/qt_main_window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 9592c74dcdf..7c9738a5b34 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -21,6 +21,7 @@ ) from weakref import WeakValueDictionary +import numpy as np from qtpy.QtCore import ( QEvent, QEventLoop, From 9137b276fa3e099af8316fa33e8e417603d4d930 Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Mon, 1 Jul 2024 12:27:23 +0200 Subject: [PATCH 55/74] Update napari/_qt/_tests/test_qt_viewer.py --- 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 b4f428c801f..dba60f14f83 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -286,7 +286,7 @@ def test_export_figure(make_napari_viewer, tmp_path): assert img.shape == (250, 250, 4) assert np.all(img != np.array([0, 0, 0, 0])) - assert (tmp_path / 'img.png').exist() + assert (tmp_path / 'img.png').exists() viewer.camera.center = [100, 100] camera_center = viewer.camera.center From 25167dc536f25a28a818980054f03215684794d8 Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Tue, 2 Jul 2024 18:55:46 +0200 Subject: [PATCH 56/74] Update napari/viewer.py Co-authored-by: Grzegorz Bokota --- napari/viewer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/napari/viewer.py b/napari/viewer.py index 0c7fd8307ea..14ff1153ad7 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -108,7 +108,7 @@ def export_figure( Parameters ---------- - path : Optional[str] + path : str, optional Filename for saving screenshot image. scale : float Scale factor used to increase resolution of canvas for the From 57a93dfc9eda6288f560b8a0acdeca463978161b Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Tue, 2 Jul 2024 19:28:53 +0200 Subject: [PATCH 57/74] use extent.step --- napari/_qt/_tests/test_qt_viewer.py | 4 ++++ napari/_qt/qt_main_window.py | 11 +++++------ napari/viewer.py | 27 +++++++++++++++------------ 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py index dba60f14f83..bb4d76cb216 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -288,6 +288,10 @@ def test_export_figure(make_napari_viewer, tmp_path): assert (tmp_path / 'img.png').exists() + viewer.layers.scale = [0.12, 0.12] + img = viewer.export_figure(flash=False) + assert img.shape == (250, 250, 4) + viewer.camera.center = [100, 100] camera_center = viewer.camera.center camera_zoom = viewer.camera.zoom diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 7c9738a5b34..936bfad0eb0 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1603,12 +1603,11 @@ def _screenshot( ) self._qt_viewer.viewer.reset_view() - canvas.size = ( - self._qt_viewer.viewer.layers.extent.world[1][ - -ndisplay: - ].astype(int) - + 1 - ) + extent_world = self._qt_viewer.viewer.layers.extent.world[1][ + -ndisplay: + ] + extent_step = self._qt_viewer.viewer.layers.extent.step[-ndisplay:] + canvas.size = (extent_world / extent_step).astype(int) + 1 self._qt_viewer.viewer.reset_view(margin=0) if canvas_only: diff --git a/napari/viewer.py b/napari/viewer.py index 0c7fd8307ea..b2cdeba2060 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -97,24 +97,27 @@ def export_figure( ): """Export an image of the full extent of the displayed layer data. - This function finds a tight boundary around the data, resets the view - around that boundary (and, when scale=1, such that 1 captured pixel is - equivalent to one data pixel), takes a screenshot, then restores the - previous zoom and canvas sizes. For example with one image layer with - an image of 256 x 256 pixels and layer.scale=[1,1], with scale=1 the - size of the resulting figure will be 256 x 256 pixels. If layer.scale - for both dimensions is set to 0.5, it will be 128 x 128 pixels. Currently, - only works when 2 dimensions are displayed. + This function finds a tight boundary around the data, resets the view around + that boundary, takes a screenshot for which each pixel is equal to the pixel + resolution of the data and then restores the previous zoom and canvas sizes. + The pixel resolution can be upscaled or downscaled by passing an integer value + to the scale parameter different from 1. For example with one image layer with + an image of 256 x 256 pixels and layer.scale=[15,15] (pixel resolution of for + x and y of 15), with scale=2 the size of the resulting figure will be + 512 x 512 pixels. If scale is set to 0.5, it will be 128 x 128 pixels. + If having layers with a different layer.scale, the resolution of each + screenshot pixel will be equal to the largest value of layer.scale for each + dimension across the layers. Currently, only works when 2 dimensions are + displayed. Parameters ---------- path : Optional[str] Filename for saving screenshot image. scale : float - Scale factor used to increase resolution of canvas for the - screenshot. By default, a scale of 1. A scale - of 1 corresponds to 1 data pixel per screenshot pixel if all displayed - layers have layer.scale for each displayed dimension set to 1. + Scale factor used to increase resolution per pixel for the + screenshot. By default, a scale of 1. A scale of 1 corresponds + to 1 pixel corresponding to the pixel resolution of the data. flash : bool Flag to indicate whether flash animation should be shown after the screenshot was captured. From f6ff4bf4cd3becaaf0ad95a9cc38203aee463207 Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Tue, 2 Jul 2024 19:33:39 +0200 Subject: [PATCH 58/74] remove conditional --- napari/_qt/qt_main_window.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 936bfad0eb0..b8784bb7e29 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1634,8 +1634,7 @@ 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_extent: - canvas.size = prev_size + canvas.size = prev_size camera.center = old_center camera.zoom = old_zoom else: From dde2642850188b56c3817048de21f29a3e49dce0 Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Tue, 2 Jul 2024 19:43:29 +0200 Subject: [PATCH 59/74] adjust docstring --- napari/viewer.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/napari/viewer.py b/napari/viewer.py index eb4e9f1ff89..dc0ecc9da89 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -104,11 +104,11 @@ def export_figure( to the scale parameter different from 1. For example with one image layer with an image of 256 x 256 pixels and layer.scale=[15,15] (pixel resolution of for x and y of 15), with scale=2 the size of the resulting figure will be - 512 x 512 pixels. If scale is set to 0.5, it will be 128 x 128 pixels. - If having layers with a different layer.scale, the resolution of each - screenshot pixel will be equal to the largest value of layer.scale for each - dimension across the layers. Currently, only works when 2 dimensions are - displayed. + 512 x 512 pixels (the interpolation mode of the layer will be used to upscale). + If scale is set to 0.5, it will be 128 x 128 pixels. If having layers with a + different layer.scale, the resolution of each screenshot pixel will be equal + to the largest value of layer.scale for each dimension across the layers. + Currently, only works when 2 dimensions are displayed. Parameters ---------- @@ -118,6 +118,8 @@ def export_figure( Scale factor used to increase resolution per pixel for the screenshot. By default, a scale of 1. A scale of 1 corresponds to 1 pixel corresponding to the pixel resolution of the data. + If the scale is higher than 1 then the interpolation method of + the layers will be used to upscale. flash : bool Flag to indicate whether flash animation should be shown after the screenshot was captured. @@ -151,15 +153,15 @@ def screenshot( path : str, optional Filename for saving screenshot image. size : tuple of two ints, optional - Size (resolution height x width) of the screenshot. By default, the currently displayed size. - Only used if `canvas_only` is True. + Size (resolution height x width) of the screenshot. By default, the currently + displayed size. Only used if `canvas_only` is True. scale : float, optional - Scale factor used to increase resolution of canvas for the screenshot. By default, the currently displayed resolution. - Only used if `canvas_only` is True. + Scale factor used to increase resolution of canvas for the screenshot. + By default, the currently displayed resolution.Only used if `canvas_only` is + True. canvas_only : bool - If True, screenshot shows only the image display canvas, and - if False include the napari viewer frame in the screenshot, - By default, True. + If True, screenshot shows only the image display canvas, and if False include + the napari viewer frame in the screenshot, By default, True. flash : bool Flag to indicate whether flash animation should be shown after the screenshot was captured. From 25596a8762a0be46c93fe1d6ce0962b2c35d2c47 Mon Sep 17 00:00:00 2001 From: Wouter-Michiel Vierdag Date: Tue, 2 Jul 2024 19:45:44 +0200 Subject: [PATCH 60/74] adjust docstring --- napari/viewer.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/napari/viewer.py b/napari/viewer.py index dc0ecc9da89..3faa156291c 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -92,7 +92,7 @@ def export_figure( self, path: Optional[str] = None, *, - scale: float = 1, + scale_factor: float = 1, flash: bool = True, ): """Export an image of the full extent of the displayed layer data. @@ -114,12 +114,12 @@ def export_figure( ---------- path : str, optional Filename for saving screenshot image. - scale : float - Scale factor used to increase resolution per pixel for the - screenshot. By default, a scale of 1. A scale of 1 corresponds - to 1 pixel corresponding to the pixel resolution of the data. - If the scale is higher than 1 then the interpolation method of - the layers will be used to upscale. + scale_factor : float + By default, the zoom will export approximately 1 pixel per smallest-scale + pixel on the viewer. For example, if a layer has scale 0.004nm/pixel and + another has scale 1µm/pixel, the exported figure will have 0.004nm/pixel. + Upscaling by 2 will produce a figure with 0.002nm/pixel through the + interpolation mode set on each layer. flash : bool Flag to indicate whether flash animation should be shown after the screenshot was captured. @@ -133,7 +133,7 @@ def export_figure( """ return self.window.export_figure( path=path, - scale=scale, + scale=scale_factor, flash=flash, ) From 9b1a230e26998b50f4418a03cd4de20a1f97c4a8 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Fri, 5 Jul 2024 10:20:00 +1000 Subject: [PATCH 61/74] scale -> scale_factor in gallery example Co-authored-by: Grzegorz Bokota --- examples/export_figure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/export_figure.py b/examples/export_figure.py index 8dd54a5f1c3..a573e615444 100644 --- a/examples/export_figure.py +++ b/examples/export_figure.py @@ -96,7 +96,7 @@ viewer.theme = "light" # Optionally for saving the exported figure: viewer.export_figure(path="export_figure.png") export_figure = viewer.export_figure() -scaled_export_figure = viewer.export_figure(scale=5) +scaled_export_figure = viewer.export_figure(scale_factor=5) viewer.theme = "dark" viewer.add_image(export_figure, rgb=True, name='exported_figure') From eb892644ae67c11fe036dc99f703c377ff839852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Althviz=20Mor=C3=A9?= <16781833+dalthviz@users.noreply.github.com> Date: Fri, 5 Jul 2024 18:13:27 -0700 Subject: [PATCH 62/74] Add some missing layer actions tests (split rgb, split and merge actions) (#7057) # References and relevant issues Follow up for https://github.com/napari/napari/pull/7030#discussion_r1662905437 # Description Add tests for split rgb, split and merge layer actions functions --- napari/layers/_tests/test_layer_actions.py | 43 ++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/napari/layers/_tests/test_layer_actions.py b/napari/layers/_tests/test_layer_actions.py index 4a78d1c0496..2986263366f 100644 --- a/napari/layers/_tests/test_layer_actions.py +++ b/napari/layers/_tests/test_layer_actions.py @@ -12,15 +12,58 @@ _hide_selected, _hide_unselected, _link_selected_layers, + _merge_stack, _project, _show_selected, _show_unselected, + _split_rgb, + _split_stack, _toggle_visibility, ) REG = pint.get_application_registry() +def test_split_stack(): + layer_list = LayerList() + layer_list.append(Image(np.random.rand(8, 8, 8))) + assert len(layer_list) == 1 + + layer_list.selection.active = layer_list[0] + _split_stack(layer_list) + assert len(layer_list) == 8 + + for idx in range(8): + assert layer_list[idx].data.shape == (8, 8) + + +def test_split_rgb(): + layer_list = LayerList() + layer_list.append(Image(np.random.random((8, 8, 3)))) + assert len(layer_list) == 1 + assert layer_list[0].rgb is True + + layer_list.selection.active = layer_list[0] + _split_rgb(layer_list) + assert len(layer_list) == 3 + + for idx in range(3): + assert layer_list[idx].data.shape == (8, 8) + + +def test_merge_stack(): + layer_list = LayerList() + layer_list.append(Image(np.random.rand(8, 8))) + layer_list.append(Image(np.random.rand(8, 8))) + assert len(layer_list) == 2 + + layer_list.selection.active = layer_list[0] + layer_list.selection.add(layer_list[1]) + _merge_stack(layer_list) + assert len(layer_list) == 1 + assert layer_list[0].data.shape == (2, 8, 8) + + def test_toggle_visibility(): """Test toggling visibility of a layer.""" layer_list = LayerList() From 76461453985acb96fc8aa4685c64328fe60b6900 Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Sat, 6 Jul 2024 03:25:50 +0200 Subject: [PATCH 63/74] Restore events to `QtViewer.canvas` (#7060) # References and relevant issues In #7054, I noted that in #5432 we had inadvertently removed access to mouse events from QtViewer.canvas, without a deprecation. @jni [commented](https://github.com/napari/napari/issues/7054#issuecomment-2205548968) that we could temporarily restore access with a property, followed in later versions by an alternate API and deprecation. # Description This PR adds an `events` property to QtViewer.canvas, restoring the broken behavior. It does not yet have a deprecation message because we don't have an alternate API. --- napari/_vispy/canvas.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/napari/_vispy/canvas.py b/napari/_vispy/canvas.py index 73a52685547..ab04e261ad1 100644 --- a/napari/_vispy/canvas.py +++ b/napari/_vispy/canvas.py @@ -167,6 +167,12 @@ def __init__( self.viewer.layers.events.removed.connect(self._remove_layer) self.destroyed.connect(self._disconnect_theme) + @property + def events(self): + # This is backwards compatible with the old events system + # https://github.com/napari/napari/issues/7054#issuecomment-2205548968 + return self._scene_canvas.events + @property def destroyed(self) -> pyqtBoundSignal: return self._scene_canvas._backend.destroyed From 4296ac5df4a671e5eeefe931c5fc6f3baaeacfbf Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sat, 6 Jul 2024 11:28:50 +1000 Subject: [PATCH 64/74] Use minimum step size across all dims This ensures correct sampling with anisotropic data Co-authored-by: Grzegorz Bokota --- napari/_qt/_tests/test_qt_viewer.py | 4 ++++ napari/_qt/qt_main_window.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py index bb4d76cb216..414afbf25f8 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -288,6 +288,10 @@ def test_export_figure(make_napari_viewer, tmp_path): assert (tmp_path / 'img.png').exists() + viewer.layers.scale = [0.12, 0.24] + img = viewer.export_figure(flash=False) + assert img.shape == (250, 500, 4) + viewer.layers.scale = [0.12, 0.12] img = viewer.export_figure(flash=False) assert img.shape == (250, 250, 4) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index b8784bb7e29..fa51e273e5d 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1606,7 +1606,7 @@ def _screenshot( extent_world = self._qt_viewer.viewer.layers.extent.world[1][ -ndisplay: ] - extent_step = self._qt_viewer.viewer.layers.extent.step[-ndisplay:] + extent_step = min(self._qt_viewer.viewer.layers.extent.step[-ndisplay:]) canvas.size = (extent_world / extent_step).astype(int) + 1 self._qt_viewer.viewer.reset_view(margin=0) From e8c3f6ff61e5c5e76cee7f220daf5f937d335818 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 6 Jul 2024 01:29:36 +0000 Subject: [PATCH 65/74] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- napari/_qt/_tests/test_qt_viewer.py | 2 +- napari/_qt/qt_main_window.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py index 414afbf25f8..5e7c8f895c8 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -291,7 +291,7 @@ def test_export_figure(make_napari_viewer, tmp_path): viewer.layers.scale = [0.12, 0.24] img = viewer.export_figure(flash=False) assert img.shape == (250, 500, 4) - + viewer.layers.scale = [0.12, 0.12] img = viewer.export_figure(flash=False) assert img.shape == (250, 250, 4) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index fa51e273e5d..6eb684a28c3 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1606,7 +1606,9 @@ def _screenshot( extent_world = self._qt_viewer.viewer.layers.extent.world[1][ -ndisplay: ] - extent_step = min(self._qt_viewer.viewer.layers.extent.step[-ndisplay:]) + extent_step = min( + self._qt_viewer.viewer.layers.extent.step[-ndisplay:] + ) canvas.size = (extent_world / extent_step).astype(int) + 1 self._qt_viewer.viewer.reset_view(margin=0) From f5fe4f4fa10a2ead60d68418253710fdfd76f089 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sat, 6 Jul 2024 11:31:42 +1000 Subject: [PATCH 66/74] Remove f-string from translation call --- napari/components/viewer_model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/napari/components/viewer_model.py b/napari/components/viewer_model.py index 9888c3097c5..22e77f5de3f 100644 --- a/napari/components/viewer_model.py +++ b/napari/components/viewer_model.py @@ -415,8 +415,9 @@ def reset_view(self, *, margin: float = 0.05) -> None: else: raise ValueError( trans._( - f'margin must be between 0 and 1; got {margin} instead.', + 'margin must be between 0 and 1; got {margin} instead.', deferred=True, + margin=margin, ) ) if np.max(size) == 0: From 374a6df7074176d1b5a2688360c55ebca000ed08 Mon Sep 17 00:00:00 2001 From: Draga Doncila Pop <17995243+DragaDoncila@users.noreply.github.com> Date: Sat, 6 Jul 2024 14:17:24 +0700 Subject: [PATCH 67/74] Move `IO_Utilities` and `Acquire` submenus to their own group in the `File` menu (#7075) # References and relevant issues Closes #7068 # Description Add a new group to the `File` menu for the newly contributable menus, allowing `Open Sample` to remain at the bottom of its group as it previously was. --- napari/_app_model/constants/_menus.py | 7 ++++--- napari/_qt/_qapp_model/qactions/_file.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/napari/_app_model/constants/_menus.py b/napari/_app_model/constants/_menus.py index 606760ba508..f1491ae953c 100644 --- a/napari/_app_model/constants/_menus.py +++ b/napari/_app_model/constants/_menus.py @@ -96,9 +96,10 @@ class MenuGroup: PLUGIN_SINGLE_CONTRIBUTIONS = '3_plugin_contributions' # File menubar OPEN = '1_open' - PREFERENCES = '2_preferences' - SAVE = '3_save' - CLOSE = '4_close' + UTIL = '2_util' + PREFERENCES = '3_preferences' + SAVE = '4_save' + CLOSE = '5_close' class LAYERLIST_CONTEXT: CONVERSION = '1_conversion' diff --git a/napari/_qt/_qapp_model/qactions/_file.py b/napari/_qt/_qapp_model/qactions/_file.py index b69861f4fb4..acd7562022e 100644 --- a/napari/_qt/_qapp_model/qactions/_file.py +++ b/napari/_qt/_qapp_model/qactions/_file.py @@ -55,7 +55,7 @@ SubmenuItem( submenu=MenuId.FILE_IO_UTILITIES, title=trans._('IO Utilities'), - group=MenuGroup.OPEN, + group=MenuGroup.UTIL, order=101, ), ), @@ -64,7 +64,7 @@ SubmenuItem( submenu=MenuId.FILE_ACQUIRE, title=trans._('Acquire'), - group=MenuGroup.OPEN, + group=MenuGroup.UTIL, order=101, ), ), From 46fdf4c2adb25663fbc7cd8806c287c2c9638ef9 Mon Sep 17 00:00:00 2001 From: Draga Doncila Pop <17995243+DragaDoncila@users.noreply.github.com> Date: Sat, 6 Jul 2024 19:02:45 +0700 Subject: [PATCH 68/74] Add empty menu placeholder actions using functional context keys from #6965 (#7038) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # References and relevant issues Alternative to #6950 that doesn't rely on registering/deregistering actions but instead uses the mechanism from #6965. Depends on: #6965 # Description On macOS, there is no indication by default that a menubar submenu is empty — one simply has to hover for an unnaturally long time to be sure that indeed, there is nothing in that submenu. Instead of allowing this behaviour, apps usually put an "Empty" placeholder in empty submenus that quickly indicate to the user that there is nothing in this menu. This is particularly critical for napari users since #7011, because that PR added a *lot* of menus that are empty if the user has not installed plugins. In #6950, I tried to add this issue by: - registering a do-nothing action - de-registering it and re-registering it whenever the menus changed, placing it in all menus that are empty. The issue with that approach is that de-registering and re-registering the action was itself changing the menus, which could cause infinite loops if we weren't super careful — and indeed we did, and worse, we seemed to do so stochastically. This PR instead uses just-in-time functional context evaluation to check whether a menu is empty as it's about to be shown, and show the empty key if so. https://github.com/napari/napari/assets/17995243/9b051a68-fe1d-41d9-ad39-b7fcc11d5206 ## Trying stuff out The easiest way to try this out is to just launch `napari` from main and look for an empty menu e.g. `Layers -> Data`. Then, if you want to see the placeholder disappear upon action registration, you can run the following code in the console. ```python from napari._app_model import get_app from app_model.types import Action my_action = Action( id='napari.new_action', title='On The Fly', callback=lambda: napari.utils.notifications.show_info("WOOOOOW"), menus=[{'id': 'napari/layers/data'}] ) app = get_app() deregister_action = app.register_action(my_action) # call deregister_action() to deregister the same action ``` --------- Co-authored-by: Grzegorz Bokota Co-authored-by: Juan Nunez-Iglesias --- napari/_app_model/utils.py | 95 +++++++++++++++++++++ napari/_qt/_qapp_model/qactions/__init__.py | 36 +++++++- napari/_qt/qt_main_window.py | 12 ++- 3 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 napari/_app_model/utils.py diff --git a/napari/_app_model/utils.py b/napari/_app_model/utils.py new file mode 100644 index 00000000000..8e56312ee63 --- /dev/null +++ b/napari/_app_model/utils.py @@ -0,0 +1,95 @@ +from typing import Union + +from app_model.expressions import parse_expression +from app_model.types import Action, MenuItem, SubmenuItem + +from napari._app_model import get_app +from napari._app_model.constants import MenuGroup, MenuId + +MenuOrSubmenu = Union[MenuItem, SubmenuItem] + + +def contains_dummy_action(menu_items: list[MenuOrSubmenu]) -> bool: + """True if one of the menu_items is the dummy action, otherwise False. + + Parameters + ---------- + menu_items : list[MenuOrSubmenu] + menu items belonging to a given menu + + Returns + ------- + bool + True if menu_items contains dummy item otherwise false + """ + for item in menu_items: + if hasattr(item, 'command') and 'empty_dummy' in item.command.id: + return True + return False + + +def is_empty_menu(menu_id: str) -> bool: + """Return True if the given menu_id is empty, otherwise False + + Parameters + ---------- + menu_id : str + id of the menu to check + + Returns + ------- + bool + True if the given menu_id is empty, otherwise False + """ + app = get_app() + if menu_id not in app.menus: + return True + if len(app.menus.get_menu(menu_id)) == 0: + return True + if len(app.menus.get_menu(menu_id)) == 1 and contains_dummy_action( + app.menus.get_menu(menu_id) + ): + return True + return False + + +def no_op() -> None: + """Fully qualified no-op to use for dummy actions.""" + + +def get_dummy_action(menu_id: MenuId) -> tuple[Action, str]: + """Return a dummy action to be used for the given menu. + + The part of the menu_id after the final `/` will form + a unique id_key used for the action ID and the when + expression context key. + + Parameters + ---------- + menu_id: MenuId + id of the menu to add the dummy action to + + Returns + ------- + tuple[Action, str] + dummy action and the `when` expression context key + """ + # NOTE: this assumes the final word of each contributable + # menu path is unique, otherwise, we will clash. Once we + # move to using short menu keys, the key itself will be used + # here and this will no longer be a concern. + id_key = menu_id.split('/')[-1] + action = Action( + id=f'napari.{id_key}.empty_dummy', + title='Empty', + callback=no_op, + menus=[ + { + 'id': menu_id, + 'group': MenuGroup.NAVIGATION, + 'when': parse_expression(context_key := f'{id_key}_empty'), + } + ], + enablement=False, + ) + return action, context_key diff --git a/napari/_qt/_qapp_model/qactions/__init__.py b/napari/_qt/_qapp_model/qactions/__init__.py index e7732b329c1..d7ab69a26a1 100644 --- a/napari/_qt/_qapp_model/qactions/__init__.py +++ b/napari/_qt/_qapp_model/qactions/__init__.py @@ -1,11 +1,15 @@ from __future__ import annotations -from functools import lru_cache +from functools import lru_cache, partial from itertools import chain +from typing import TYPE_CHECKING from napari._qt._qapp_model.injection._qprocessors import QPROCESSORS from napari._qt._qapp_model.injection._qproviders import QPROVIDERS +if TYPE_CHECKING: + from app_model.types import Context + # Submodules should be able to import from most modules, so to # avoid circular imports, don't import submodules at the top level here, # import them inside the init_qactions function. @@ -84,3 +88,33 @@ def init_qactions() -> None: app.menus.append_menu_items( chain(FILE_SUBMENUS, VIEW_SUBMENUS, DEBUG_SUBMENUS, LAYERS_SUBMENUS) ) + + +def add_dummy_actions(context: Context) -> None: + """Register dummy 'Empty' actions for all contributable menus. + + Each action is registered with its own `when` condition, that + ensures the action is not visible once the menu is populated. + The context key used in the `when` condition is also added to + the given `context` and assigned to a partial function that + returns True if the menu is empty, and otherwise False. + + + Parameters + ---------- + context : Context + context to store functional keys used in `when` conditions + """ + from napari._app_model import get_app + from napari._app_model.constants._menus import MenuId + from napari._app_model.utils import get_dummy_action, is_empty_menu + + app = get_app() + + actions = [] + for menu_id in MenuId.contributables(): + dummmy_action, context_key = get_dummy_action(menu_id) + if dummmy_action.id not in app.commands: + actions.append(dummmy_action) + context[context_key] = partial(is_empty_menu, menu_id) + app.register_actions(actions) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index c9ef50095a3..74473c6cb74 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -48,7 +48,7 @@ from napari._app_model.constants import MenuId from napari._app_model.context import create_context, get_context from napari._qt._qapp_model import build_qmodel_menu -from napari._qt._qapp_model.qactions import init_qactions +from napari._qt._qapp_model.qactions import add_dummy_actions, init_qactions from napari._qt._qapp_model.qactions._debug import _is_set_trace_active from napari._qt._qplugins import ( _rebuild_npe1_plugins_menu, @@ -677,6 +677,16 @@ def __init__(self, viewer: 'Viewer', *, show: bool = True) -> None: index_npe1_adapters() self._add_menus() + # TODO: the dummy actions should **not** live on the layerlist context + # as they are unrelated. However, we do not currently have a suitable + # enclosing context where we could store these keys, such that they + # **and** the layerlist context key are available when we update + # menus. We need a single context to contain all keys required for + # menu update, so we add them to the layerlist context for now. + if self._qt_viewer._layers is not None: + add_dummy_actions( + self._qt_viewer._layers.model().sourceModel()._root._ctx + ) self._update_theme() self._update_theme_font_size() get_settings().appearance.events.theme.connect(self._update_theme) From bc1d95f9bfad37c6850d0ef9e36696bcf4af0030 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sun, 7 Jul 2024 17:21:00 +1000 Subject: [PATCH 69/74] Add warning in docstring about ignored size --- 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 6eb684a28c3..1e31773ec5d 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1557,8 +1557,9 @@ def _screenshot( Flag to indicate whether flash animation should be shown after the screenshot was captured. size : tuple of two ints, optional - Size (resolution height x width) of the screenshot. By default, the currently displayed size. - Only used if `canvas_only` is True. + Size (resolution height x width) of the screenshot. By default, the + currently displayed size. Only used if `canvas_only` is True. This + argument is ignored if fit_to_data_extent is set to True. scale : float, optional Scale factor used to increase resolution of canvas for the screenshot. By default, the currently displayed resolution. From 2bd587ddd1c71529540e6e69483b9dbaad890767 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sun, 7 Jul 2024 17:21:16 +1000 Subject: [PATCH 70/74] Minor docstring clarification --- napari/_qt/qt_main_window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 1e31773ec5d..36d4b53b3d7 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1570,8 +1570,8 @@ def _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. + showing in the screenshot. If False, a screenshot of the currently + visible canvas will be generated. Returns ------- From 4d06794e0ef9c6cd75be1465e2e1c1a6d2b4b1f4 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sun, 7 Jul 2024 17:56:58 +1000 Subject: [PATCH 71/74] Fix incorrect setting of scale in test --- napari/_qt/_tests/test_qt_viewer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py index 5e7c8f895c8..e51659d67a4 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -275,7 +275,7 @@ def test_export_figure(make_napari_viewer, tmp_path): np.random.seed(0) # Add image data = np.random.randint(150, 250, size=(250, 250)) - viewer.add_image(data) + layer = viewer.add_image(data) camera_center = viewer.camera.center camera_zoom = viewer.camera.zoom @@ -288,11 +288,11 @@ def test_export_figure(make_napari_viewer, tmp_path): assert (tmp_path / 'img.png').exists() - viewer.layers.scale = [0.12, 0.24] + layer.scale = [0.12, 0.24] img = viewer.export_figure(flash=False) assert img.shape == (250, 500, 4) - viewer.layers.scale = [0.12, 0.12] + layer.scale = [0.12, 0.12] img = viewer.export_figure(flash=False) assert img.shape == (250, 250, 4) From 29f6d6650c4cfd143a196fd2e2ce0222eb3986e0 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sun, 7 Jul 2024 18:10:42 +1000 Subject: [PATCH 72/74] Use allclose to test screenshot size when rounding --- napari/_qt/_tests/test_qt_viewer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py index e51659d67a4..3f7ddd50513 100644 --- a/napari/_qt/_tests/test_qt_viewer.py +++ b/napari/_qt/_tests/test_qt_viewer.py @@ -290,7 +290,9 @@ def test_export_figure(make_napari_viewer, tmp_path): layer.scale = [0.12, 0.24] img = viewer.export_figure(flash=False) - assert img.shape == (250, 500, 4) + # allclose accounts for rounding errors when computing size in hidpi aka + # retina displays + np.testing.assert_allclose(img.shape, (250, 499, 4), atol=1) layer.scale = [0.12, 0.12] img = viewer.export_figure(flash=False) From de4bb8046c88f26ae8ca5ac7063801d968f8c6fb Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sun, 7 Jul 2024 18:11:39 +1000 Subject: [PATCH 73/74] Refactor screenshot function to clarify logic flow --- napari/_qt/qt_main_window.py | 77 +++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 36d4b53b3d7..29b4ca9534b 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1584,53 +1584,58 @@ def _screenshot( camera = self._qt_viewer.viewer.camera old_center = camera.center old_zoom = camera.zoom - if fit_to_data_extent: - if not canvas_only: - raise ValueError( - trans._( - "'fit_to_data_extent' cannot be set to True if 'canvas_only' is" - ' set to False', - deferred=True, - ) + ndisplay = self._qt_viewer.viewer.dims.ndisplay + + # Part 1: validate incompatible parameters + if not canvas_only and ( + fit_to_data_extent or size is not None or scale is not None + ): + raise ValueError( + trans._( + 'scale, size, and fit_to_data_extent can only be set for ' + 'canvas_only screenshots.', + deferred=True, ) - ndisplay = self._qt_viewer.viewer.dims.ndisplay - if ndisplay > 2: - raise NotImplementedError( - trans._( - 'fit_to_data_extent=True is not yet implemented for 3D. ' - 'Please set fit_to_data_extent to False in 3D view.', - deferred=True, - ) + ) + if fit_to_data_extent and ndisplay > 2: + raise NotImplementedError( + trans._( + 'fit_to_data_extent is not yet implemented for 3D view.', + deferred=True, + ) + ) + if size is not None and len(size) != 2: + raise ValueError( + trans._( + 'screenshot size must be 2 values, got {len_size}', + deferred=True, + len_size=len(size), ) + ) - self._qt_viewer.viewer.reset_view() + # Part 2: compute canvas size and view based on parameters + if fit_to_data_extent: extent_world = self._qt_viewer.viewer.layers.extent.world[1][ -ndisplay: ] extent_step = min( self._qt_viewer.viewer.layers.extent.step[-ndisplay:] ) - canvas.size = (extent_world / extent_step).astype(int) + 1 - self._qt_viewer.viewer.reset_view(margin=0) + size = extent_world / extent_step + 1 + if size is not None: + size = np.asarray(size) / self._qt_window.devicePixelRatio() + else: + size = np.asarray(prev_size) + if scale is not None: + # multiply canvas dimensions by the scale factor to get new size + size *= scale + # Part 3: take the screenshot if canvas_only: - if size is not None: - if len(size) != 2: - raise ValueError( - trans._( - 'screenshot size must be 2 values, got {len_size}', - len_size=len(size), - ) - ) - # Scale the requested size to account for HiDPI - size = tuple( - int(dim / self._qt_window.devicePixelRatio()) - for dim in size - ) - canvas.size = size - if scale is not None: - # multiply canvas dimensions by the scale factor to get new size - canvas.size = tuple(int(dim * scale) for dim in canvas.size) + canvas.size = tuple(size.astype(int)) + if fit_to_data_extent: + # tight view around data + self._qt_viewer.viewer.reset_view(margin=0) try: img = canvas.screenshot() if flash: From 77bf7edaac1affa1603f4edc92a3836e7296a32c Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sun, 7 Jul 2024 18:46:19 +1000 Subject: [PATCH 74/74] Docstring and typing fixes --- napari/_qt/qt_main_window.py | 11 +++------ napari/viewer.py | 45 ++++++++++++++++++++---------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py index 29b4ca9534b..bbbd4f887e3 100644 --- a/napari/_qt/qt_main_window.py +++ b/napari/_qt/qt_main_window.py @@ -1662,11 +1662,8 @@ def export_figure( This function finds a tight boundary around the data, resets the view around that boundary (and, when scale=1, such that 1 captured pixel is equivalent to one data pixel), takes a screenshot, then restores the - previous zoom and canvas sizes. For example with one image layer with - an image of 256 x 256 pixels and layer.scale=[1,1], with scale=1 the - size of the resulting figure will be 256 x 256 pixels. If layer.scale - for both dimensions is set to 0.5, it will be 128 x 128 pixels. Currently, - only works when 2 dimensions are displayed. + previous zoom and canvas sizes. Currently, only works when 2 dimensions + are displayed. Parameters ---------- @@ -1674,9 +1671,7 @@ def export_figure( Filename for saving screenshot image. scale : float Scale factor used to increase resolution of canvas for the - screenshot. By default, a scale of 1. A scale - of 1 corresponds to 1 data pixel per screenshot pixel if all displayed - layers have layer.scale for each displayed dimension set to 1. + screenshot. By default, a scale of 1. flash : bool Flag to indicate whether flash animation should be shown after the screenshot was captured. diff --git a/napari/viewer.py b/napari/viewer.py index 3faa156291c..fb92478f50a 100644 --- a/napari/viewer.py +++ b/napari/viewer.py @@ -3,6 +3,7 @@ from weakref import WeakSet import magicgui as mgui +import numpy as np from napari.components.viewer_model import ViewerModel from napari.utils import _magicgui @@ -94,36 +95,40 @@ def export_figure( *, scale_factor: float = 1, flash: bool = True, - ): + ) -> np.ndarray: """Export an image of the full extent of the displayed layer data. - This function finds a tight boundary around the data, resets the view around - that boundary, takes a screenshot for which each pixel is equal to the pixel - resolution of the data and then restores the previous zoom and canvas sizes. - The pixel resolution can be upscaled or downscaled by passing an integer value - to the scale parameter different from 1. For example with one image layer with - an image of 256 x 256 pixels and layer.scale=[15,15] (pixel resolution of for - x and y of 15), with scale=2 the size of the resulting figure will be - 512 x 512 pixels (the interpolation mode of the layer will be used to upscale). - If scale is set to 0.5, it will be 128 x 128 pixels. If having layers with a - different layer.scale, the resolution of each screenshot pixel will be equal - to the largest value of layer.scale for each dimension across the layers. - Currently, only works when 2 dimensions are displayed. + This function finds a tight boundary around the data, resets the view + around that boundary, takes a screenshot for which each pixel is equal + to the pixel resolution of the data, then restores the previous zoom + and canvas sizes. + + The pixel resolution can be upscaled or downscaled by the given + `scale_factor`. For example, an image with 800 x 600 pixels with + scale_factor 1 will be saved as 800 x 600, or 1200 x 900 with + scale_factor 1.5. + + For anisotropic images, the resolution is set by the highest-resolution + dimension. For an anisotropic 800 x 600 image with scale set to + [0.25, 0.5], the screenshot will be 800 x 1200, or 1200 x 1800 with a + scale_factor of 1.5. + + Upscaling will be done using the interpolation mode set on each layer. Parameters ---------- path : str, optional Filename for saving screenshot image. scale_factor : float - By default, the zoom will export approximately 1 pixel per smallest-scale - pixel on the viewer. For example, if a layer has scale 0.004nm/pixel and - another has scale 1µm/pixel, the exported figure will have 0.004nm/pixel. - Upscaling by 2 will produce a figure with 0.002nm/pixel through the - interpolation mode set on each layer. + By default, the zoom will export approximately 1 pixel per + smallest-scale pixel on the viewer. For example, if a layer has + scale 0.004nm/pixel and another has scale 1µm/pixel, the exported + figure will have 0.004nm/pixel. Upscaling by 2 will produce a + figure with 0.002nm/pixel through the interpolation mode set on + each layer. flash : bool Flag to indicate whether flash animation should be shown after - the screenshot was captured. - By default, True. + the screenshot was captured. By default, True. Returns -------