Introduction to Dash Slicer

The dash_slicer library provides an easy way to visualize 3D image data
by slicing along one dimension. Multiple views on the same data can be linked,
to help with navigation. There is also support for mask overlays.

Install Dash Slicer with:

pip install -U dash-slicer

The source is on GitHub at plotly/dash-slicer.

Guide

A first example

Let’s get started with a simple example.
(The examples on this page are built with dash-slicer version 0.3.0)

import dash
import dash_html_components as html
import imageio
from dash_slicer import VolumeSlicer


app = dash.Dash(__name__, update_title=None)

vol = imageio.volread("imageio:stent.npz")
slicer = VolumeSlicer(app, vol)
slicer.graph.config["scrollZoom"] = False

app.layout = html.Div([slicer.graph, slicer.slider, *slicer.stores])


if __name__ == "__main__":
    app.run_server(debug=True, dev_tools_props_check=False)

In the code above, we first load a 3D numpy array (a volumetric image). Then
we instantiate a VolumeSlicer object with that data. This object is not
a Dash component, but has attributes that are. We place its graph and
slider in the layout, as well as a handful of Store components that the
slicer needs to function.

The graph’s scrollZoom config option is turned off here, because
it would make the plot grab scroll events as you scroll down this page.

If the server is run in debug mode, consider setting dev_tools_props_check
to False, because it has a big impact on the performance.

Multiple slicers

In the next example, we create multiple slicers, one for each dimension,
and put them in a layout, just like we did in the previous example.

import dash
import dash_html_components as html
import imageio
from dash_slicer import VolumeSlicer


app = dash.Dash(__name__, update_title=None)

vol = imageio.volread("imageio:stent.npz")
slicer0 = VolumeSlicer(app, vol, axis=0)
slicer1 = VolumeSlicer(app, vol, axis=1)
slicer2 = VolumeSlicer(app, vol, axis=2)

slicer0.graph.config["scrollZoom"] = False
slicer1.graph.config["scrollZoom"] = False
slicer2.graph.config["scrollZoom"] = False

app.layout = html.Div(
    style={
        "display": "grid",
        "gridTemplateColumns": "33% 33% 33%",
    },
    children=[
        html.Div([slicer0.graph, html.Br(), slicer0.slider, *slicer0.stores]),
        html.Div([slicer1.graph, html.Br(), slicer1.slider, *slicer1.stores]),
        html.Div([slicer2.graph, html.Br(), slicer2.slider, *slicer2.stores]),
    ],
)


if __name__ == "__main__":
    app.run_server(debug=True, dev_tools_props_check=False)



You can see how each view contains indicator lines, which show the
position of the other slicers. Each slicer has a certain color
(which is auto-selected, but can also be provided), which is shown
in the corners around the image. The same color is used to draw the
slicer’s indicator lines in the other views.

Slicers indicate each-other’s position if they are linked based on their
scene_id. This is a property that can be provided when you
instantiate a VolumeSlicer. By default, the scene_id is derived
from the volume. That’s why the linking in this example works: each
slicer is given the same numpy array object. By explicitly setting
the scene_id, multiple views on different data (e.g. MRI and CT) can
be linked as well.

In addition to using the sliders, you can click in one of the
views to make the other views go to the clicked location. Try it!
Thanks to this navigation mode, you can optionally hide sliders in
the layout when two or more views are present (see the VolumeSlicer.slider
property ).

Anisotropic data

In the next example, we make the data non-isotropic. This means
that the distance between voxels is not equal for all dimensions.
The voxel-spacing can be provided via the spacing argument.
Similarly, an origin can also be provided. You can zoom into the
view on the right to see that the voxels are elongated.

import dash
import dash_html_components as html
import imageio
from dash_slicer import VolumeSlicer


app = dash.Dash(__name__, update_title=None)

vol = imageio.volread("imageio:stent.npz")
vol = vol[::3,:,:]
spacing = 3, 1, 1

slicer0 = VolumeSlicer(app, vol, spacing=spacing, axis=0)
slicer1 = VolumeSlicer(app, vol, spacing=spacing, axis=1)

slicer0.graph.config["scrollZoom"] = False
slicer1.graph.config["scrollZoom"] = False

app.layout = html.Div(
    style={
        "display": "grid",
        "gridTemplateColumns": "33% 33%",
    },
    children=[
        html.Div([slicer0.graph, html.Br(), slicer0.slider, *slicer0.stores]),
        html.Div([slicer1.graph, html.Br(), slicer1.slider, *slicer1.stores]),
    ],
)


if __name__ == "__main__":
    app.run_server(debug=True, dev_tools_props_check=False)


Reacting to the slicer state

This example illustrates how your application can react to the slicer’s
position and view by using the state store as an input. Note that the
id of this store is a dict, which makes it possible to write a
pattern matching Input
to collect the states of all slicers with a certain scene_id.
See the reference docs for details.

import json
import dash
import dash_html_components as html
from dash.dependencies import Input, Output
import imageio
from dash_slicer import VolumeSlicer


app = dash.Dash(__name__, update_title=None)

vol = imageio.volread("imageio:stent.npz")
slicer = VolumeSlicer(app, vol)
slicer.graph.config["scrollZoom"] = False

app.layout = html.Div([slicer.graph, slicer.slider, html.Div(id='info'), *slicer.stores])

@app.callback(
    Output("info", "children"),
    [Input(slicer.state.id, "data")]
)
def update_info(state):
    return json.dumps(state, indent=4)


if __name__ == "__main__":
    app.run_server(debug=True, dev_tools_props_check=False)

More examples

More examples are available at the
dash-slicer repository.

Reference

The VolumeSlicer class

class VolumeSlicer(app, volume, *, spacing=None, origin=None, axis=0, reverse_y=True, clim=None, scene_id=None, color=None, thumbnail=True)

A slicer object to show 3D image data in Dash. Upon
instantiation one can provide the following parameters:

  • app (dash.Dash): the Dash application instance.
  • volume (ndarray): the 3D numpy array to slice through. The dimensions
    are assumed to be in zyx order. If this is not the case, you can
    use np.swapaxes to make it so.
  • spacing (tuple of float): the distance between voxels for each
    dimension (zyx). The spacing and origin are applied to make the slice
    drawn in “scene space” rather than “voxel space”.
  • origin (tuple of float): the offset for each dimension (zyx).
  • axis (int): the dimension to slice in. Default 0.
  • reverse_y (bool): whether to reverse the y-axis, so that the origin of
    the slice is in the top-left, rather than bottom-left. Default True.
    Note: setting this to False affects performance, see #12. This has been
    fixed, but the fix has not yet been released with Dash.
  • clim (tuple of float): the (initial) contrast limits. Default the min
    and max of the volume.
  • 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).
  • color (str): the color for this slicer. By default the color
    is a shade of blue, orange, or green, depending on the axis. Set
    to empty string to prevent drawing indicators for this slicer.
  • thumbnail (int or bool): the preferred 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.

Note that this is not a Dash Component. The components that make
up the slicer (and which must be present in the layout) are:
slicer.graph, slicer.slider, and slicer.stores.

method VolumeSlicer.create_overlay_data(mask, color=None)

Given a 3D mask array, create an object that can be used as
output for slicer.overlay_data. Set mask to None to clear the mask.
The color can be a hex color or an rgb/rgba tuple. Alternatively,
color can be a list of such colors, defining a colormap.

property VolumeSlicer.axis (int): The axis to slice.

property VolumeSlicer.clim: A dcc.Store to be used as Output, representing the contrast
limits as a 2-element tuple. This value should probably not be
changed too often (e.g. on slider drag) because the thumbnail
data is recreated on each change.

property VolumeSlicer.extra_traces: A dcc.Store to be used as an Output to define additional
traces to be shown in this slicer. The data must be a list of
dictionaries, with each dict representing a raw trace object.

property VolumeSlicer.graph: The dcc.Graph for this slicer. Use graph.figure to access the
Plotly Figure object.

property VolumeSlicer.nslices (int): The number of slices for this slicer.

property VolumeSlicer.overlay_data: A dcc.Store to be used an Output for the overlay data. The
form of this data is considered an implementation detail; users
are expected to use create_overlay_data to create it.

property VolumeSlicer.scene_id (str): The id of the “virtual scene” for this slicer. Slicers that have
the same scene_id show each-other’s positions.

property VolumeSlicer.slider: The dcc.Slider to change the index for this slicer. If you
don’t want to use the slider, wrap it in a div with style
display: none.

property VolumeSlicer.state: A dcc.Store representing the current state of the slicer (present
in slicer.stores). This store is intended for use as State or Input.
Its data is a dict with the fields:

  • “index”: the integer slice index.
  • “index_changed”: a bool indicating whether the index changed since last time.
  • “xrange”: the view range (2 floats) in the x-dimension (2D).
  • “yrange”: the view range (2 floats) in the y-dimension (2D).
  • “zpos”: the float position aling the axis, in scene coordinates.
  • “axis”: the axis (int) for this slicer.
  • “color”: the color (str) for this slicer.

The id of the store is a dictionary so it can be used in a
pattern matching Input. Its field are: context, scene, name.
Where scene is the scene_id and name is “state”.

property VolumeSlicer.stores: A list of dcc.Store objects that the slicer needs to work.
These must be added to the app layout. Note that public stores
like state and extra_traces are also present in this list.

Reacting to slicer state

It is possible to get notified of updates to slicer position and
view ranges. To get this for all slicers with a specific scene_id, create
a pattern matching input
like this:

Input({"scene": scene_id, "context": ALL, "name": "state"})

See the state property for details.

Setting slicer positions

To programatically set the position of the slicer, create a dcc.Store with
a dictionary-id that has the following fields:

  • ‘context’: a unique name for this store.
  • ‘scene’: the scene_id of the slicer objects to set the position for.
  • ‘name’: ‘setpos’

The value in the store must be an 3-element tuple (x, y, z) in scene coordinates.
To apply the position for one dimension only, use e.g (None, None, x).

Performance tips

There tends to be a lot of interaction in an application that contains
slicer objects. To realize a smooth user experience, performance matters.
Here are some tips to help with that:

  • Most importantly, when running the server in debug mode, consider setting
    dev_tools_props_check=False.
  • Also consider creating the Dash application with update_title=None.
  • Setting reverse_y to False negatively affects performance. This will be
    fixed in a future version of Plotly/Dash.
  • For a smooth experience, avoid triggering unnecessary figure updates.
  • When adding a callback that uses the slicer position, use the (rate limited)
    state store rather than the slider value.