From e480ddbddfd5e9b4b6f612534e9c6dbd5ed6fc8d Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Thu, 3 Dec 2020 22:57:55 +0100 Subject: [PATCH 1/7] add thumbnail_size property --- dash_slicer/slicer.py | 49 ++++++++++++++++++++----------------------- tests/test_slicer.py | 4 ++++ 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/dash_slicer/slicer.py b/dash_slicer/slicer.py index 8c418ec..f6fce84 100644 --- a/dash_slicer/slicer.py +++ b/dash_slicer/slicer.py @@ -30,6 +30,8 @@ class VolumeSlicer: scene_id (str): the scene that this slicer is part of. Slicers that have the same scene-id show each-other's positions with line indicators. By default this is derived from ``id(volume)``. + thumbnail_size (int): linear size of low-resolution data to be uploaded + to the client. This is a placeholder object, not a Dash component. The components that make up the slicer can be accessed as attributes. These must all @@ -73,6 +75,7 @@ def __init__( axis=0, reverse_y=True, scene_id=None, + thumbnail_size=32, ): if not isinstance(app, Dash): @@ -97,6 +100,11 @@ def __init__( self._other_axii = [0, 1, 2] self._other_axii.pop(self._axis) + # Check and store thumbnail size + if not (isinstance(thumbnail_size, int)): + raise ValueError("thumbnail_size must be an integer.") + self._thumbnail_size = thumbnail_size + # Check and store scene id, and generate if scene_id is None: n = len(_assigned_scene_ids) @@ -137,6 +145,11 @@ def axis(self): """The axis at which the slicer is slicing.""" return self._axis + @property + def thumbnail_size(self): + """Linear size of low-resolution data.""" + return self._thumbnail_size + @property def nslices(self): """The number of slices for this slicer.""" @@ -260,7 +273,9 @@ def _create_dash_components(self): info = self._slice_info # Prep low-res slices - thumbnail_size = get_thumbnail_size(info["size"][:2], (32, 32)) + thumbnail_size = get_thumbnail_size( + info["size"][:2], (self._thumbnail_size, self._thumbnail_size) + ) thumbnails = [ img_array_to_uri(self._slice(i), thumbnail_size) for i in range(info["size"][2]) @@ -275,10 +290,7 @@ def _create_dash_components(self): dragmode="pan", # good default mode ) fig.update_xaxes( - showgrid=False, - showticklabels=False, - zeroline=False, - constrain="range", + showgrid=False, showticklabels=False, zeroline=False, constrain="range", ) fig.update_yaxes( showgrid=False, @@ -291,9 +303,7 @@ def _create_dash_components(self): # Create the graph (graph is a Dash component wrapping a Plotly figure) self._graph = Graph( - id=self._subid("graph"), - figure=fig, - config={"scrollZoom": True}, + id=self._subid("graph"), figure=fig, config={"scrollZoom": True}, ) initial_index = info["size"][2] // 2 @@ -365,8 +375,7 @@ def _create_server_callbacks(self): app = self._app @app.callback( - Output(self._server_data.id, "data"), - [Input(self._index.id, "data")], + Output(self._server_data.id, "data"), [Input(self._index.id, "data")], ) def upload_requested_slice(slice_index): slice = img_array_to_uri(self._slice(slice_index)) @@ -432,11 +441,7 @@ def _create_client_callbacks(self): Output(self._slider.id, "value"), [ Input( - { - "scene": self._scene_id, - "context": ALL, - "name": "setpos", - }, + {"scene": self._scene_id, "context": ALL, "name": "setpos",}, "data", ) ], @@ -491,10 +496,7 @@ def _create_client_callbacks(self): """.replace( "{{ID}}", self._context_id ), - [ - Output(self._index.id, "data"), - Output(self._timer.id, "disabled"), - ], + [Output(self._index.id, "data"), Output(self._timer.id, "disabled"),], [Input(self._slider.id, "value"), Input(self._timer.id, "n_intervals")], [State(self._timer.id, "interval")], ) @@ -620,10 +622,7 @@ def _create_client_callbacks(self): ) for axis in self._other_axii ], - [ - State(self._info.id, "data"), - State(self._indicator_traces.id, "data"), - ], + [State(self._info.id, "data"), State(self._indicator_traces.id, "data"),], ) # ---------------------------------------------------------------------- @@ -650,7 +649,5 @@ def _create_client_callbacks(self): Input(self._img_traces.id, "data"), Input(self._indicator_traces.id, "data"), ], - [ - State(self.graph.id, "figure"), - ], + [State(self.graph.id, "figure"),], ) diff --git a/tests/test_slicer.py b/tests/test_slicer.py index ee4fe55..28f5a25 100644 --- a/tests/test_slicer.py +++ b/tests/test_slicer.py @@ -21,6 +21,10 @@ def test_slicer_init(): with raises(ValueError): VolumeSlicer(app, vol, axis=4) + # Need a valide thumbnail_size + with raises(ValueError): + VolumeSlicer(app, vol, thumbnail_size=20.2) + # This works s = VolumeSlicer(app, vol) From 4b7d83353b2e2766e3d007f77f56ab289bb29284 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Fri, 4 Dec 2020 14:33:52 +0100 Subject: [PATCH 2/7] most recent black --- dash_slicer/slicer.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/dash_slicer/slicer.py b/dash_slicer/slicer.py index f6fce84..c06f4e2 100644 --- a/dash_slicer/slicer.py +++ b/dash_slicer/slicer.py @@ -290,7 +290,10 @@ def _create_dash_components(self): dragmode="pan", # good default mode ) fig.update_xaxes( - showgrid=False, showticklabels=False, zeroline=False, constrain="range", + showgrid=False, + showticklabels=False, + zeroline=False, + constrain="range", ) fig.update_yaxes( showgrid=False, @@ -303,7 +306,9 @@ def _create_dash_components(self): # Create the graph (graph is a Dash component wrapping a Plotly figure) self._graph = Graph( - id=self._subid("graph"), figure=fig, config={"scrollZoom": True}, + id=self._subid("graph"), + figure=fig, + config={"scrollZoom": True}, ) initial_index = info["size"][2] // 2 @@ -375,7 +380,8 @@ def _create_server_callbacks(self): app = self._app @app.callback( - Output(self._server_data.id, "data"), [Input(self._index.id, "data")], + Output(self._server_data.id, "data"), + [Input(self._index.id, "data")], ) def upload_requested_slice(slice_index): slice = img_array_to_uri(self._slice(slice_index)) @@ -441,7 +447,11 @@ def _create_client_callbacks(self): Output(self._slider.id, "value"), [ Input( - {"scene": self._scene_id, "context": ALL, "name": "setpos",}, + { + "scene": self._scene_id, + "context": ALL, + "name": "setpos", + }, "data", ) ], @@ -496,7 +506,10 @@ def _create_client_callbacks(self): """.replace( "{{ID}}", self._context_id ), - [Output(self._index.id, "data"), Output(self._timer.id, "disabled"),], + [ + Output(self._index.id, "data"), + Output(self._timer.id, "disabled"), + ], [Input(self._slider.id, "value"), Input(self._timer.id, "n_intervals")], [State(self._timer.id, "interval")], ) @@ -622,7 +635,10 @@ def _create_client_callbacks(self): ) for axis in self._other_axii ], - [State(self._info.id, "data"), State(self._indicator_traces.id, "data"),], + [ + State(self._info.id, "data"), + State(self._indicator_traces.id, "data"), + ], ) # ---------------------------------------------------------------------- @@ -649,5 +665,7 @@ def _create_client_callbacks(self): Input(self._img_traces.id, "data"), Input(self._indicator_traces.id, "data"), ], - [State(self.graph.id, "figure"),], + [ + State(self.graph.id, "figure"), + ], ) From 363712986be758e26b7c2955dfc43f329a47089b Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Fri, 4 Dec 2020 16:04:35 +0100 Subject: [PATCH 3/7] add to use full res client-side --- dash_slicer/slicer.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/dash_slicer/slicer.py b/dash_slicer/slicer.py index c06f4e2..bd1f90d 100644 --- a/dash_slicer/slicer.py +++ b/dash_slicer/slicer.py @@ -30,8 +30,9 @@ class VolumeSlicer: scene_id (str): the scene that this slicer is part of. Slicers that have the same scene-id show each-other's positions with line indicators. By default this is derived from ``id(volume)``. - thumbnail_size (int): linear size of low-resolution data to be uploaded - to the client. + thumbnail_size (int or None): linear size of low-resolution data to be + uploaded to the client. If ``None``, the full-resolution data are + uploaded client-side. This is a placeholder object, not a Dash component. The components that make up the slicer can be accessed as attributes. These must all @@ -101,8 +102,8 @@ def __init__( self._other_axii.pop(self._axis) # Check and store thumbnail size - if not (isinstance(thumbnail_size, int)): - raise ValueError("thumbnail_size must be an integer.") + if thumbnail_size is not None and not (isinstance(thumbnail_size, int)): + raise ValueError("thumbnail_size must be an integer, or None.") self._thumbnail_size = thumbnail_size # Check and store scene id, and generate @@ -128,7 +129,8 @@ def __init__( # Build the slicer self._create_dash_components() - self._create_server_callbacks() + if thumbnail_size is not None: + self._create_server_callbacks() self._create_client_callbacks() # Note(AK): we could make some stores public, but let's do this only when actual use-cases arise? @@ -273,14 +275,18 @@ def _create_dash_components(self): info = self._slice_info # Prep low-res slices - thumbnail_size = get_thumbnail_size( - info["size"][:2], (self._thumbnail_size, self._thumbnail_size) - ) + if self._thumbnail_size is None: + thumbnail_size = None + info["lowres_size"] = info["size"] + else: + thumbnail_size = get_thumbnail_size( + info["size"][:2], (self._thumbnail_size, self._thumbnail_size) + ) + info["lowres_size"] = thumbnail_size thumbnails = [ img_array_to_uri(self._slice(i), thumbnail_size) for i in range(info["size"][2]) ] - info["lowres_size"] = thumbnail_size # Create the figure object - can be accessed by user via slicer.graph.figure self._fig = fig = Figure(data=[]) @@ -339,7 +345,9 @@ def _create_dash_components(self): self._overlay_data = Store(id=self._subid("overlay"), data=[]) # Slice data provided by the server - self._server_data = Store(id=self._subid("server-data"), data="") + self._server_data = Store( + id=self._subid("server-data"), data={"index": -1, "slice": None} + ) # Store image traces for the slicer. self._img_traces = Store(id=self._subid("img-traces"), data=[]) From 266913742341dcb56ffc6d553002bce475b8ba56 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Sun, 6 Dec 2020 23:11:27 +0100 Subject: [PATCH 4/7] address review comments --- dash_slicer/slicer.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/dash_slicer/slicer.py b/dash_slicer/slicer.py index bd1f90d..6a2f8eb 100644 --- a/dash_slicer/slicer.py +++ b/dash_slicer/slicer.py @@ -30,9 +30,10 @@ class VolumeSlicer: scene_id (str): the scene that this slicer is part of. Slicers that have the same scene-id show each-other's positions with line indicators. By default this is derived from ``id(volume)``. - thumbnail_size (int or None): linear size of low-resolution data to be - uploaded to the client. If ``None``, the full-resolution data are - uploaded client-side. + thumbnail (int or bool): linear size of low-resolution data to be + uploaded to the client. If ``False``, the full-resolution data are + uploaded client-side. If ``True`` (default), a default value of 32 is + used. This is a placeholder object, not a Dash component. The components that make up the slicer can be accessed as attributes. These must all @@ -76,7 +77,7 @@ def __init__( axis=0, reverse_y=True, scene_id=None, - thumbnail_size=32, + thumbnail=True, ): if not isinstance(app, Dash): @@ -101,10 +102,15 @@ def __init__( self._other_axii = [0, 1, 2] self._other_axii.pop(self._axis) - # Check and store thumbnail size - if thumbnail_size is not None and not (isinstance(thumbnail_size, int)): - raise ValueError("thumbnail_size must be an integer, or None.") - self._thumbnail_size = thumbnail_size + # Check and store thumbnail + if not (isinstance(thumbnail, (int, bool))): + raise ValueError("thumbnail must be a boolean or an integer.") + # No thumbnail if thumbnail size is larger than image size + if isinstance(thumbnail, int) and thumbnail > np.max(volume.shape): + thumbnail = False + if thumbnail is True: + thumbnail = 32 # default size + self._thumbnail = thumbnail # Check and store scene id, and generate if scene_id is None: @@ -129,7 +135,7 @@ def __init__( # Build the slicer self._create_dash_components() - if thumbnail_size is not None: + if thumbnail: self._create_server_callbacks() self._create_client_callbacks() @@ -147,11 +153,6 @@ def axis(self): """The axis at which the slicer is slicing.""" return self._axis - @property - def thumbnail_size(self): - """Linear size of low-resolution data.""" - return self._thumbnail_size - @property def nslices(self): """The number of slices for this slicer.""" @@ -275,12 +276,12 @@ def _create_dash_components(self): info = self._slice_info # Prep low-res slices - if self._thumbnail_size is None: + if self._thumbnail is False: thumbnail_size = None info["lowres_size"] = info["size"] else: thumbnail_size = get_thumbnail_size( - info["size"][:2], (self._thumbnail_size, self._thumbnail_size) + info["size"][:2], (self._thumbnail, self._thumbnail) ) info["lowres_size"] = thumbnail_size thumbnails = [ From 54e86633fb8d3fba9e753b5ba091f217decb31bb Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Sun, 6 Dec 2020 23:20:14 +0100 Subject: [PATCH 5/7] fixed tests --- tests/test_slicer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_slicer.py b/tests/test_slicer.py index 28f5a25..d6eb271 100644 --- a/tests/test_slicer.py +++ b/tests/test_slicer.py @@ -21,9 +21,9 @@ def test_slicer_init(): with raises(ValueError): VolumeSlicer(app, vol, axis=4) - # Need a valide thumbnail_size + # Need a valide thumbnail with raises(ValueError): - VolumeSlicer(app, vol, thumbnail_size=20.2) + VolumeSlicer(app, vol, thumbnail=20.2) # This works s = VolumeSlicer(app, vol) From f62aa09e25a94cedf61eb72492cc0b737efef248 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Mon, 7 Dec 2020 13:53:35 +0100 Subject: [PATCH 6/7] added test --- tests/test_slicer.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_slicer.py b/tests/test_slicer.py index d6eb271..84146d1 100644 --- a/tests/test_slicer.py +++ b/tests/test_slicer.py @@ -35,6 +35,20 @@ def test_slicer_init(): assert all(isinstance(store, (dcc.Store, dcc.Interval)) for store in s.stores) +def test_slicer_thumbnail(): + app = dash.Dash() + vol = np.random.uniform(0, 255, (100, 100, 100)).astype(np.uint8) + + s = VolumeSlicer(app, vol) + # Test for name pattern of server-side callback when thumbnails are used + assert any(["server-data.data" in key for key in app.callback_map]) + + app = dash.Dash() + s = VolumeSlicer(app, vol, thumbnail=False) + # No server-side callbacks when no thumbnails are used + assert not any(["server-data.data" in key for key in app.callback_map]) + + def test_scene_id_and_context_id(): app = dash.Dash() From 82f297040e8e6e1f6e794151220667f2cb1971d8 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Mon, 7 Dec 2020 13:57:30 +0100 Subject: [PATCH 7/7] try to make black happy --- tests/test_slicer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_slicer.py b/tests/test_slicer.py index 84146d1..089a7d2 100644 --- a/tests/test_slicer.py +++ b/tests/test_slicer.py @@ -39,12 +39,12 @@ def test_slicer_thumbnail(): app = dash.Dash() vol = np.random.uniform(0, 255, (100, 100, 100)).astype(np.uint8) - s = VolumeSlicer(app, vol) + _ = VolumeSlicer(app, vol) # Test for name pattern of server-side callback when thumbnails are used assert any(["server-data.data" in key for key in app.callback_map]) app = dash.Dash() - s = VolumeSlicer(app, vol, thumbnail=False) + _ = VolumeSlicer(app, vol, thumbnail=False) # No server-side callbacks when no thumbnails are used assert not any(["server-data.data" in key for key in app.callback_map])