4
4
from dash .dependencies import Input , Output , State , ALL
5
5
from dash_core_components import Graph , Slider , Store
6
6
7
- from .utils import img_array_to_uri , get_thumbnail_size_from_shape
7
+ from .utils import img_array_to_uri , get_thumbnail_size_from_shape , shape3d_to_size2d
8
8
9
9
10
10
class DashVolumeSlicer :
@@ -13,6 +13,11 @@ class DashVolumeSlicer:
13
13
Parameters:
14
14
app (dash.Dash): the Dash application instance.
15
15
volume (ndarray): the 3D numpy array to slice through.
16
+ The dimensions are assumed to be in zyx order.
17
+ spacing (tuple of floats): The voxel size for each dimension (zyx).
18
+ The spacing and origin are applied to make the slice drawn in
19
+ "scene space" rather than "voxel space".
20
+ origin (tuple of floats): The offset for each dimension (zyx).
16
21
axis (int): the dimension to slice in. Default 0.
17
22
scene_id (str): the scene that this slicer is part of. Slicers
18
23
that have the same scene-id show each-other's positions with
@@ -38,14 +43,21 @@ class DashVolumeSlicer:
38
43
39
44
_global_slicer_counter = 0
40
45
41
- def __init__ (self , app , volume , axis = 0 , scene_id = None ):
46
+ def __init__ (
47
+ self , app , volume , * , spacing = None , origin = None , axis = 0 , scene_id = None
48
+ ):
49
+ # todo: also implement xyz dim order?
42
50
if not isinstance (app , Dash ):
43
51
raise TypeError ("Expect first arg to be a Dash app." )
44
52
self ._app = app
45
53
# Check and store volume
46
54
if not (isinstance (volume , np .ndarray ) and volume .ndim == 3 ):
47
55
raise TypeError ("Expected volume to be a 3D numpy array" )
48
56
self ._volume = volume
57
+ spacing = (1 , 1 , 1 ) if spacing is None else spacing
58
+ spacing = float (spacing [0 ]), float (spacing [1 ]), float (spacing [2 ])
59
+ origin = (0 , 0 , 0 ) if origin is None else origin
60
+ origin = float (origin [0 ]), float (origin [1 ]), float (origin [2 ])
49
61
# Check and store axis
50
62
if not (isinstance (axis , int ) and 0 <= axis <= 2 ):
51
63
raise ValueError ("The given axis must be 0, 1, or 2." )
@@ -60,20 +72,26 @@ def __init__(self, app, volume, axis=0, scene_id=None):
60
72
DashVolumeSlicer ._global_slicer_counter += 1
61
73
self .context_id = "slicer_" + str (DashVolumeSlicer ._global_slicer_counter )
62
74
63
- # Get the slice size (width, height), and max index
64
- arr_shape = list (volume .shape )
65
- arr_shape .pop (self ._axis )
66
- self ._slice_size = tuple (reversed (arr_shape ))
67
- self ._max_index = self ._volume .shape [self ._axis ] - 1
75
+ # Prepare slice info
76
+ info = {
77
+ "shape" : tuple (volume .shape ),
78
+ "axis" : self ._axis ,
79
+ "size" : shape3d_to_size2d (volume .shape , axis ),
80
+ "origin" : shape3d_to_size2d (origin , axis ),
81
+ "spacing" : shape3d_to_size2d (spacing , axis ),
82
+ }
68
83
69
84
# Prep low-res slices
70
- thumbnail_size = get_thumbnail_size_from_shape (arr_shape , 32 )
85
+ thumbnail_size = get_thumbnail_size_from_shape (
86
+ (info ["size" ][1 ], info ["size" ][0 ]), 32
87
+ )
71
88
thumbnails = [
72
89
img_array_to_uri (self ._slice (i ), thumbnail_size )
73
- for i in range (self . _max_index + 1 )
90
+ for i in range (info [ "size" ][ 2 ] )
74
91
]
92
+ info ["lowres_size" ] = thumbnail_size
75
93
76
- # Create a placeholder trace
94
+ # Create traces
77
95
# todo: can add "%{z[0]}", but that would be the scaled value ...
78
96
image_trace = Image (
79
97
source = "" , dx = 1 , dy = 1 , hovertemplate = "(%{x}, %{y})<extra></extra>"
@@ -106,22 +124,20 @@ def __init__(self, app, volume, axis=0, scene_id=None):
106
124
config = {"scrollZoom" : True },
107
125
)
108
126
# Create a slider object that the user can put in the layout (or not)
109
- # todo: use tooltip to show current value?
110
127
self .slider = Slider (
111
128
id = self ._subid ("slider" ),
112
129
min = 0 ,
113
- max = self . _max_index ,
130
+ max = info [ "size" ][ 2 ] - 1 ,
114
131
step = 1 ,
115
- value = self . _max_index // 2 ,
132
+ value = info [ "size" ][ 2 ] // 2 ,
116
133
tooltip = {"always_visible" : False , "placement" : "left" },
117
134
updatemode = "drag" ,
118
135
)
119
136
# Create the stores that we need (these must be present in the layout)
120
137
self .stores = [
121
- Store (
122
- id = self ._subid ("_slice-size" ), data = self ._slice_size + thumbnail_size
123
- ),
138
+ Store (id = self ._subid ("info" ), data = info ),
124
139
Store (id = self ._subid ("index" ), data = volume .shape [self ._axis ] // 2 ),
140
+ Store (id = self ._subid ("position" ), data = 0 ),
125
141
Store (id = self ._subid ("_requested-slice-index" ), data = 0 ),
126
142
Store (id = self ._subid ("_slice-data" ), data = "" ),
127
143
Store (id = self ._subid ("_slice-data-lowres" ), data = thumbnails ),
@@ -175,6 +191,17 @@ def _create_client_callbacks(self):
175
191
[Input (self ._subid ("slider" ), "value" )],
176
192
)
177
193
194
+ app .clientside_callback (
195
+ """
196
+ function update_position(index, info) {
197
+ return info.origin[2] + index * info.spacing[2];
198
+ }
199
+ """ ,
200
+ Output (self ._subid ("position" ), "data" ),
201
+ [Input (self ._subid ("index" ), "data" )],
202
+ [State (self ._subid ("info" ), "data" )],
203
+ )
204
+
178
205
app .clientside_callback (
179
206
"""
180
207
function handle_slice_index(index) {
@@ -205,7 +232,7 @@ def _create_client_callbacks(self):
205
232
206
233
app .clientside_callback (
207
234
"""
208
- function handle_incoming_slice(index, index_and_data, indicators, ori_figure, lowres, slice_size ) {
235
+ function handle_incoming_slice(index, index_and_data, indicators, ori_figure, lowres, info ) {
209
236
let new_index = index_and_data[0];
210
237
let new_data = index_and_data[1];
211
238
// Store data in cache
@@ -214,18 +241,18 @@ def _create_client_callbacks(self):
214
241
slice_cache[new_index] = new_data;
215
242
// Get the data we need *now*
216
243
let data = slice_cache[index];
217
- let x0 = 0, y0 = 0, dx = 1, dy = 1;
244
+ let x0 = info.origin[0], y0 = info.origin[1];
245
+ let dx = info.spacing[0], dy = info.spacing[1];
218
246
//slice_cache[new_index] = undefined; // todo: disabled cache for now!
219
247
// Maybe we do not need an update
220
248
if (!data) {
221
249
data = lowres[index];
222
250
// Scale the image to take the exact same space as the full-res
223
251
// version. It's not correct, but it looks better ...
224
- // slice_size = full_w, full_h, low_w, low_h
225
- dx = slice_size[0] / slice_size[2];
226
- dy = slice_size[1] / slice_size[3];
227
- x0 = 0.5 * dx - 0.5;
228
- y0 = 0.5 * dy - 0.5;
252
+ dx *= info.size[0] / info.lowres_size[0];
253
+ dy *= info.size[1] / info.lowres_size[1];
254
+ x0 += 0.5 * dx - 0.5 * info.spacing[0];
255
+ y0 += 0.5 * dy - 0.5 * info.spacing[1];
229
256
}
230
257
if (data == ori_figure.data[0].source && indicators.version == ori_figure.data[1].version) {
231
258
return window.dash_clientside.no_update;
@@ -253,7 +280,7 @@ def _create_client_callbacks(self):
253
280
[
254
281
State (self ._subid ("graph" ), "figure" ),
255
282
State (self ._subid ("_slice-data-lowres" ), "data" ),
256
- State (self ._subid ("_slice-size " ), "data" ),
283
+ State (self ._subid ("info " ), "data" ),
257
284
],
258
285
)
259
286
@@ -266,18 +293,22 @@ def _create_client_callbacks(self):
266
293
# * match any of the selected axii
267
294
app .clientside_callback (
268
295
"""
269
- function handle_indicator(indices1, indices2, slice_size, current) {
270
- let w = slice_size[0], h = slice_size[1];
271
- let dx = w / 20, dy = h / 20;
296
+ function handle_indicator(positions1, positions2, info, current) {
297
+ let x0 = info.origin[0], y0 = info.origin[1];
298
+ let x1 = x0 + info.size[0] * info.spacing[0], y1 = y0 + info.size[1] * info.spacing[1];
299
+ x0 = x0 - info.spacing[0], y0 = y0 - info.spacing[1];
300
+ let d = ((x1 - x0) + (y1 - y0)) * 0.5 * 0.05;
272
301
let version = (current.version || 0) + 1;
273
302
let x = [], y = [];
274
- for (let index of indices1) {
275
- x.push(...[-dx, -1, null, w, w + dx, null]);
276
- y.push(...[index, index, index, index, index, index]);
303
+ for (let pos of positions1) {
304
+ // x relative to our slice, y in scene-coords
305
+ x.push(...[x0 - d, x0, null, x1, x1 + d, null]);
306
+ y.push(...[pos, pos, pos, pos, pos, pos]);
277
307
}
278
- for (let index of indices2) {
279
- x.push(...[index, index, index, index, index, index]);
280
- y.push(...[-dy, -1, null, h, h + dy, null]);
308
+ for (let pos of positions2) {
309
+ // x in scene-coords, y relative to our slice
310
+ x.push(...[pos, pos, pos, pos, pos, pos]);
311
+ y.push(...[y0 - d, y0, null, y1, y1 + d, null]);
281
312
}
282
313
return {
283
314
type: 'scatter',
@@ -296,15 +327,15 @@ def _create_client_callbacks(self):
296
327
{
297
328
"scene" : self .scene_id ,
298
329
"context" : ALL ,
299
- "name" : "index " ,
330
+ "name" : "position " ,
300
331
"axis" : axis ,
301
332
},
302
333
"data" ,
303
334
)
304
335
for axis in axii
305
336
],
306
337
[
307
- State (self ._subid ("_slice-size " ), "data" ),
338
+ State (self ._subid ("info " ), "data" ),
308
339
State (self ._subid ("_indicators" ), "data" ),
309
340
],
310
341
)
0 commit comments