From 26508fe0b65234bbc4483f009f50b49a750dd38e Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 30 Oct 2020 12:10:11 +0100 Subject: [PATCH 1/4] wip low-res data --- dash_3d_viewer/slicer.py | 22 +++++++++++++++++----- dash_3d_viewer/utils.py | 14 ++++++++++---- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/dash_3d_viewer/slicer.py b/dash_3d_viewer/slicer.py index d367ed4..d8e4050 100644 --- a/dash_3d_viewer/slicer.py +++ b/dash_3d_viewer/slicer.py @@ -34,6 +34,12 @@ def __init__(self, app, volume, axis=0, id=None): slice_size = list(reversed(arr_shape)) self._max_index = self._volume.shape[self._axis] - 1 + # Prep low-res slices + thumbnails = [ + img_array_to_uri(self._slice(i), (32, 32)) + for i in range(self._max_index + 1) + ] + # Create the figure object fig = Figure() fig.update_layout( @@ -88,6 +94,7 @@ def __init__(self, app, volume, axis=0, id=None): Store(id=self._subid("slice-index"), data=volume.shape[self._axis] // 2), Store(id=self._subid("_requested-slice-index"), data=0), Store(id=self._subid("_slice-data"), data=""), + Store(id=self._subid("_slice-data-lowres"), data=thumbnails), ] self._create_server_callbacks(app) @@ -101,7 +108,8 @@ def _slice(self, index): """Sample a slice from the volume.""" indices = [slice(None), slice(None), slice(None)] indices[self._axis] = index - return self._volume[tuple(indices)] + im = self._volume[tuple(indices)] + return (im.astype(np.float32) * (255 / im.max())).astype(np.uint8) def _create_server_callbacks(self, app): """Create the callbacks that run server-side.""" @@ -112,7 +120,6 @@ def _create_server_callbacks(self, app): ) def upload_requested_slice(slice_index): slice = self._slice(slice_index) - slice = (slice.astype(np.float32) * (255 / slice.max())).astype(np.uint8) return [slice_index, img_array_to_uri(slice)] def _create_client_callbacks(self, app): @@ -158,7 +165,7 @@ def _create_client_callbacks(self, app): app.clientside_callback( """ - function handle_incoming_slice(index, index_and_data, ori_figure) { + function handle_incoming_slice(index, index_and_data, ori_figure, lowres) { let new_index = index_and_data[0]; let new_data = index_and_data[1]; // Store data in cache @@ -167,9 +174,11 @@ def _create_client_callbacks(self, app): slice_cache[new_index] = new_data; // Get the data we need *now* let data = slice_cache[index]; + //slice_cache[new_index] = undefined; // todo: disabled cache for now! // Maybe we do not need an update if (!data) { - return window.dash_clientside.no_update; + // return window.dash_clientside.no_update; + data = lowres[index]; } if (data == ori_figure.layout.images[0].source) { return window.dash_clientside.no_update; @@ -188,5 +197,8 @@ def _create_client_callbacks(self, app): Input(self._subid("slice-index"), "data"), Input(self._subid("_slice-data"), "data"), ], - [State(self._subid("graph"), "figure")], + [ + State(self._subid("graph"), "figure"), + State(self._subid("_slice-data-lowres"), "data"), + ], ) diff --git a/dash_3d_viewer/utils.py b/dash_3d_viewer/utils.py index 61846e1..68ab52c 100644 --- a/dash_3d_viewer/utils.py +++ b/dash_3d_viewer/utils.py @@ -1,19 +1,25 @@ +import io import random +import base64 import PIL.Image import skimage -from plotly.utils import ImageUriValidator def gen_random_id(n=6): return "".join(random.choice("abcdefghijklmnopqrtsuvwxyz") for i in range(n)) -def img_array_to_uri(img_array): +def img_array_to_uri(img_array, new_size=None): img_array = skimage.util.img_as_ubyte(img_array) # todo: leverage this Plotly util once it becomes part of the public API (also drops the Pillow dependency) # from plotly.express._imshow import _array_to_b64str # return _array_to_b64str(img_array) img_pil = PIL.Image.fromarray(img_array) - uri = ImageUriValidator.pil_image_to_uri(img_pil) - return uri + if new_size: + img_pil.thumbnail(new_size) + # The below was taken from plotly.utils.ImageUriValidator.pil_image_to_uri() + f = io.BytesIO() + img_pil.save(f, format="PNG") + base64_str = base64.b64encode(f.getvalue()).decode() + return "data:image/png;base64," + base64_str From 4f5e653dc26d6210249c1ff4551631a1632a5a81 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Mon, 2 Nov 2020 16:54:31 +0100 Subject: [PATCH 2/4] use Image trace instead of layout images. --- dash_3d_viewer/slicer.py | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/dash_3d_viewer/slicer.py b/dash_3d_viewer/slicer.py index d8e4050..c7f1922 100644 --- a/dash_3d_viewer/slicer.py +++ b/dash_3d_viewer/slicer.py @@ -1,5 +1,5 @@ import numpy as np -from plotly.graph_objects import Figure +from plotly.graph_objects import Figure, Image from dash import Dash from dash.dependencies import Input, Output, State from dash_core_components import Graph, Slider, Store @@ -7,6 +7,9 @@ from .utils import gen_random_id, img_array_to_uri +empty_img_str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNgAAAAAgABSK+kcQAAAABJRU5ErkJggg==" + + class DashVolumeSlicer: """A slicer to show 3D image data in Dash.""" @@ -40,39 +43,28 @@ def __init__(self, app, volume, axis=0, id=None): for i in range(self._max_index + 1) ] + # Create a placeholder trace + # todo: can add "%{z[0]}", but that would be the scaled value ... + trace = Image(source=empty_img_str, hovertemplate="(%{x}, %{y})") # Create the figure object - fig = Figure() + fig = Figure(data=[trace]) fig.update_layout( template=None, margin=dict(l=0, r=0, b=0, t=0, pad=4), ) fig.update_xaxes( showgrid=False, - range=(0, slice_size[0]), + # range=(0, slice_size[0]), showticklabels=False, zeroline=False, ) fig.update_yaxes( showgrid=False, scaleanchor="x", - range=(slice_size[1], 0), # todo: allow flipping x or y + # range=(slice_size[1], 0), # todo: allow flipping x or y showticklabels=False, zeroline=False, ) - # Add an empty layout image that we can populate from JS. - fig.add_layout_image( - dict( - source="", - xref="x", - yref="y", - x=0, - y=0, - sizex=slice_size[0], - sizey=slice_size[1], - sizing="contain", - layer="below", - ) - ) # Wrap the figure in a graph # todo: or should the user provide this? self.graph = Graph( @@ -180,13 +172,15 @@ def _create_client_callbacks(self, app): // return window.dash_clientside.no_update; data = lowres[index]; } - if (data == ori_figure.layout.images[0].source) { + //if (data == ori_figure.layout.images[0].source) { + if (data == ori_figure.data[0].source) { return window.dash_clientside.no_update; } // Otherwise, perform update console.log("updating figure"); let figure = {...ori_figure}; - figure.layout.images[0].source = data; + //figure.layout.images[0].source = data; + figure.data[0].source = data; return figure; } """.replace( From 0b27f23d3fbfde2f5a462a5b09b2564b6c2266d2 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Tue, 3 Nov 2020 10:19:25 +0100 Subject: [PATCH 3/4] tweaks --- dash_3d_viewer/slicer.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/dash_3d_viewer/slicer.py b/dash_3d_viewer/slicer.py index c7f1922..64d251a 100644 --- a/dash_3d_viewer/slicer.py +++ b/dash_3d_viewer/slicer.py @@ -7,9 +7,6 @@ from .utils import gen_random_id, img_array_to_uri -empty_img_str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNgAAAAAgABSK+kcQAAAABJRU5ErkJggg==" - - class DashVolumeSlicer: """A slicer to show 3D image data in Dash.""" @@ -32,9 +29,9 @@ def __init__(self, app, volume, axis=0, id=None): self._id = id # Get the slice size (width, height), and max index - arr_shape = list(volume.shape) - arr_shape.pop(self._axis) - slice_size = list(reversed(arr_shape)) + # arr_shape = list(volume.shape) + # arr_shape.pop(self._axis) + # slice_size = list(reversed(arr_shape)) self._max_index = self._volume.shape[self._axis] - 1 # Prep low-res slices @@ -45,7 +42,7 @@ def __init__(self, app, volume, axis=0, id=None): # Create a placeholder trace # todo: can add "%{z[0]}", but that would be the scaled value ... - trace = Image(source=empty_img_str, hovertemplate="(%{x}, %{y})") + trace = Image(source="", hovertemplate="(%{x}, %{y})") # Create the figure object fig = Figure(data=[trace]) fig.update_layout( @@ -53,15 +50,15 @@ def __init__(self, app, volume, axis=0, id=None): margin=dict(l=0, r=0, b=0, t=0, pad=4), ) fig.update_xaxes( - showgrid=False, # range=(0, slice_size[0]), + showgrid=False, showticklabels=False, zeroline=False, ) fig.update_yaxes( + # range=(slice_size[1], 0), # todo: allow flipping x or y showgrid=False, scaleanchor="x", - # range=(slice_size[1], 0), # todo: allow flipping x or y showticklabels=False, zeroline=False, ) From 31eaf6410e5a2dc0f331a4b4951a561299210c0d Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Tue, 3 Nov 2020 10:35:14 +0100 Subject: [PATCH 4/4] cleanup --- dash_3d_viewer/slicer.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/dash_3d_viewer/slicer.py b/dash_3d_viewer/slicer.py index 64d251a..1938e02 100644 --- a/dash_3d_viewer/slicer.py +++ b/dash_3d_viewer/slicer.py @@ -166,17 +166,14 @@ def _create_client_callbacks(self, app): //slice_cache[new_index] = undefined; // todo: disabled cache for now! // Maybe we do not need an update if (!data) { - // return window.dash_clientside.no_update; data = lowres[index]; } - //if (data == ori_figure.layout.images[0].source) { if (data == ori_figure.data[0].source) { return window.dash_clientside.no_update; } // Otherwise, perform update console.log("updating figure"); let figure = {...ori_figure}; - //figure.layout.images[0].source = data; figure.data[0].source = data; return figure; }