@@ -111,6 +111,8 @@ class VolumeSlicer:
111
111
the slice is in the top-left, rather than bottom-left. Default True.
112
112
Note: setting this to False affects performance, see #12. This has been
113
113
fixed, but the fix has not yet been released with Dash.
114
+ * `clim` (tuple of `float`): the (initial) contrast limits. Default the min
115
+ and max of the volume.
114
116
* `scene_id` (`str`): the scene that this slicer is part of. Slicers
115
117
that have the same scene-id show each-other's positions with
116
118
line indicators. By default this is derived from `id(volume)`.
@@ -137,6 +139,7 @@ def __init__(
137
139
origin = None ,
138
140
axis = 0 ,
139
141
reverse_y = True ,
142
+ clim = None ,
140
143
scene_id = None ,
141
144
color = None ,
142
145
thumbnail = True ,
@@ -161,21 +164,29 @@ def __init__(
161
164
self ._axis = int (axis )
162
165
self ._reverse_y = bool (reverse_y )
163
166
167
+ # Check and store contrast limits
168
+ if clim is None :
169
+ self ._initial_clim = self ._volume .min (), self ._volume .max ()
170
+ elif isinstance (clim , (tuple , list )) and len (clim ) == 2 :
171
+ self ._initial_clim = float (clim [0 ]), float (clim [1 ])
172
+ else :
173
+ raise ValueError ("The clim must be None or a 2-tuple of floats." )
174
+
164
175
# Check and store thumbnail
165
176
if not (isinstance (thumbnail , (int , bool ))):
166
177
raise ValueError ("thumbnail must be a boolean or an integer." )
167
178
if thumbnail is False :
168
- self ._thumbnail = False
179
+ self ._thumbnail_param = None
169
180
elif thumbnail is None or thumbnail is True :
170
- self ._thumbnail = 32 # default size
181
+ self ._thumbnail_param = 32 # default size
171
182
else :
172
183
thumbnail = int (thumbnail )
173
184
if thumbnail >= np .max (volume .shape [:3 ]):
174
- self ._thumbnail = False # dont go larger than image size
185
+ self ._thumbnail_param = None # dont go larger than image size
175
186
elif thumbnail <= 0 :
176
- self ._thumbnail = False # consider 0 and -1 the same as False
187
+ self ._thumbnail_param = None # consider 0 and -1 the same as False
177
188
else :
178
- self ._thumbnail = thumbnail
189
+ self ._thumbnail_param = thumbnail
179
190
180
191
# Check and store scene id, and generate
181
192
if scene_id is None :
@@ -207,8 +218,7 @@ def __init__(
207
218
208
219
# Build the slicer
209
220
self ._create_dash_components ()
210
- if thumbnail :
211
- self ._create_server_callbacks ()
221
+ self ._create_server_callbacks ()
212
222
self ._create_client_callbacks ()
213
223
214
224
# Note(AK): we could make some stores public, but let's do this only when actual use-cases arise?
@@ -271,6 +281,14 @@ def state(self):
271
281
"""
272
282
return self ._state
273
283
284
+ @property
285
+ def clim (self ):
286
+ """A `dcc.Store` representing the contrast limits as a 2-element tuple.
287
+ This value should probably not be changed too often (e.g. on slider drag)
288
+ because the thumbnail data is recreated on each change.
289
+ """
290
+ return self ._clim
291
+
274
292
@property
275
293
def extra_traces (self ):
276
294
"""A `dcc.Store` that can be used as an output to define
@@ -377,31 +395,31 @@ def _subid(self, name, use_dict=False, **kwargs):
377
395
assert not kwargs
378
396
return self ._context_id + "-" + name
379
397
380
- def _slice (self , index ):
398
+ def _slice (self , index , clim ):
381
399
"""Sample a slice from the volume."""
400
+ # Sample from the volume
382
401
indices = [slice (None ), slice (None ), slice (None )]
383
402
indices [self ._axis ] = index
384
- im = self ._volume [tuple (indices )]
385
- return (im .astype (np .float32 ) * (255 / im .max ())).astype (np .uint8 )
403
+ im = self ._volume [tuple (indices )].astype (np .float32 )
404
+ # Apply contrast limits
405
+ clim = min (clim ), max (clim )
406
+ im = (im - clim [0 ]) * (255 / (clim [1 ] - clim [0 ]))
407
+ im [im < 0 ] = 0
408
+ im [im > 255 ] = 255
409
+ return im .astype (np .uint8 )
386
410
387
411
def _create_dash_components (self ):
388
412
"""Create the graph, slider, figure, etc."""
389
413
info = self ._slice_info
390
414
391
415
# Prep low-res slices. The get_thumbnail_size() is a bit like
392
416
# a simulation to get the low-res size.
393
- if not self ._thumbnail :
394
- thumbnail_size = None
417
+ if self ._thumbnail_param is None :
395
418
info ["thumbnail_size" ] = info ["size" ]
396
419
else :
397
- thumbnail_size = self ._thumbnail
398
420
info ["thumbnail_size" ] = get_thumbnail_size (
399
- info ["size" ][:2 ], thumbnail_size
421
+ info ["size" ][:2 ], self . _thumbnail_param
400
422
)
401
- thumbnails = [
402
- img_array_to_uri (self ._slice (i ), thumbnail_size )
403
- for i in range (info ["size" ][2 ])
404
- ]
405
423
406
424
# Create the figure object - can be accessed by user via slicer.graph.figure
407
425
self ._fig = fig = plotly .graph_objects .Figure (data = [])
@@ -451,8 +469,11 @@ def _create_dash_components(self):
451
469
# A dict of static info for this slicer
452
470
self ._info = Store (id = self ._subid ("info" ), data = info )
453
471
472
+ # A list of contrast limits
473
+ self ._clim = Store (id = self ._subid ("clim" ), data = self ._initial_clim )
474
+
454
475
# A list of low-res slices, or the full-res data (encoded as base64-png)
455
- self ._thumbs_data = Store (id = self ._subid ("thumbs" ), data = thumbnails )
476
+ self ._thumbs_data = Store (id = self ._subid ("thumbs" ), data = [] )
456
477
457
478
# A list of mask slices (encoded as base64-png or null)
458
479
self ._overlay_data = Store (id = self ._subid ("overlay" ), data = [])
@@ -482,6 +503,7 @@ def _create_dash_components(self):
482
503
483
504
self ._stores = [
484
505
self ._info ,
506
+ self ._clim ,
485
507
self ._thumbs_data ,
486
508
self ._overlay_data ,
487
509
self ._server_data ,
@@ -498,15 +520,29 @@ def _create_server_callbacks(self):
498
520
app = self ._app
499
521
500
522
@app .callback (
501
- Output (self ._server_data .id , "data" ),
502
- [Input (self ._state .id , "data" )],
523
+ Output (self ._thumbs_data .id , "data" ),
524
+ [Input (self ._clim .id , "data" )],
503
525
)
504
- def upload_requested_slice (state ):
505
- if state is None or not state ["index_changed" ]:
506
- return dash .no_update
507
- index = state ["index" ]
508
- slice = img_array_to_uri (self ._slice (index ))
509
- return {"index" : index , "slice" : slice }
526
+ def upload_thumbnails (clim ):
527
+ return [
528
+ img_array_to_uri (self ._slice (i , clim ), self ._thumbnail_param )
529
+ for i in range (self .nslices )
530
+ ]
531
+
532
+ if self ._thumbnail_param is not None :
533
+ # The callback to push full-res slices to the client is only needed
534
+ # if the thumbnails are not already full-res.
535
+
536
+ @app .callback (
537
+ Output (self ._server_data .id , "data" ),
538
+ [Input (self ._state .id , "data" ), Input (self ._clim .id , "data" )],
539
+ )
540
+ def upload_requested_slice (state , clim ):
541
+ if state is None or not state ["index_changed" ]:
542
+ return dash .no_update
543
+ index = state ["index" ]
544
+ slice = img_array_to_uri (self ._slice (index , clim ))
545
+ return {"index" : index , "slice" : slice }
510
546
511
547
def _create_client_callbacks (self ):
512
548
"""Create the callbacks that run client-side."""
@@ -714,7 +750,7 @@ def _create_client_callbacks(self):
714
750
State (self ._info .id , "data" ),
715
751
State (self ._graph .id , "figure" ),
716
752
],
717
- # prevent_initial_call=True,
753
+ prevent_initial_call = True ,
718
754
)
719
755
720
756
# ----------------------------------------------------------------------
@@ -770,9 +806,9 @@ def _create_client_callbacks(self):
770
806
Input (self ._slider .id , "value" ),
771
807
Input (self ._server_data .id , "data" ),
772
808
Input (self ._overlay_data .id , "data" ),
809
+ Input (self ._thumbs_data .id , "data" ),
773
810
],
774
811
[
775
- State (self ._thumbs_data .id , "data" ),
776
812
State (self ._info .id , "data" ),
777
813
State (self ._img_traces .id , "data" ),
778
814
],
0 commit comments