Interactive image stack viewing in jupyter notebooks based on ipycanvas and ipywidgets.
You can use stackview
from within jupyter notebooks as shown below.
Starting point is a 3D image dataset provided as numpy array.
import stackview
stackview.__version__
'0.7.1'
import numpy as np
from skimage.io import imread
from skimage.filters import gaussian
image = imread('https://github.com/haesleinhuepf/stackview/blob/main/docs/data/Haase_MRT_tfl3d1.tif?raw=true', plugin='tifffile')
You can then view it slice-by-slice:
image[:3].shape
(3, 160, 160)
stackview.slice(image[:3], continuous_update=True)
VBox(children=(HBox(children=(VBox(children=(ImageWidget(height=160, width=160),)),)), IntSlider(value=1, desc…
You can also get a static view using a maximum-intensity projection with additional information.
stackview.insight(image)
|
|
To draw label images, use annotate
.
import numpy as np
labels = np.zeros(image.shape).astype(np.uint32)
stackview.annotate(image, labels)
VBox(children=(HBox(children=(HBox(children=(VBox(children=(ImageWidget(height=160, width=160),)),)), VBox(chi…
To read the intensity of pixels where the mouse is moving, use the picker.
stackview.picker(image, continuous_update=True)
VBox(children=(HBox(children=(VBox(children=(ImageWidget(height=160, width=160),)),)), IntSlider(value=60, des…
Orthogonal views are also available:
stackview.orthogonal(image, continuous_update=True)
HBox(children=(VBox(children=(HBox(children=(VBox(children=(ImageWidget(height=160, width=160),)),)), IntSlide…
For visualization of an original image in combination with a processed version, a curtain view may be helpful:
modified_image = image.max() - image
stackview.curtain(image, modified_image, continuous_update=True)
VBox(children=(HBox(children=(VBox(children=(ImageWidget(height=160, width=160),)),)), IntSlider(value=60, des…
One can also use the curtain to visualize semantic segmentation results as label images.
labels = (image > 5000)*1 + (image > 15000)*1 + (image > 30000)*1
stackview.curtain(image, labels, continuous_update=True)
VBox(children=(HBox(children=(VBox(children=(ImageWidget(height=160, width=160),)),)), IntSlider(value=60, des…
The curtain can also be partially transparent using the alpha value.
stackview.curtain(image, labels, continuous_update=True, alpha=0.3)
VBox(children=(HBox(children=(VBox(children=(ImageWidget(height=160, width=160),)),)), IntSlider(value=60, des…
The curtain also works with 2D data
slice_image = imread('https://github.com/haesleinhuepf/stackview/blob/main/docs/data/blobs.tif?raw=true', plugin='tifffile')
from skimage.filters import threshold_otsu
binary = (slice_image > threshold_otsu(slice_image))
binary.shape, slice_image.shape
((254, 256), (254, 256))
stackview.curtain(slice_image, binary, continuous_update=True)
VBox(children=(HBox(children=(VBox(children=(ImageWidget(height=254, width=256),)),)), IntSlider(value=127, de…
Also label images are supported. Images are shown as labels in case their pixel type is (unsigned) integer 32-bit or 32-bit.
from skimage.measure import label
labels = label(binary)
stackview.curtain(slice_image, labels, continuous_update=True)
VBox(children=(HBox(children=(VBox(children=(ImageWidget(height=254, width=256),)),)), IntSlider(value=127, de…
A side-by-side view of two stacks is also available. It might be useful for colocalization visualization or showing subsequent time points of a timelapse.
image_stack = imread('https://github.com/haesleinhuepf/stackview/blob/main/docs/data/CalibZAPWfixed_000154_max.tif?raw=true', plugin='tifffile').swapaxes(1,2)
stackview.side_by_side(image_stack[1:], image_stack[:-1], continuous_update=True, display_width=300)
VBox(children=(HBox(children=(HBox(children=(VBox(children=(ImageWidget(height=235, width=389),)),)), HBox(chi…
labels_stack = np.asarray([label(image > 100) for image in image_stack])
stackview.side_by_side(image_stack, labels_stack, continuous_update=True, display_width=300)
VBox(children=(HBox(children=(HBox(children=(VBox(children=(ImageWidget(height=389, width=235),)),)), HBox(chi…
If there are many images you would like to inspect, you can pass them as dictionary or list to switch()
.
stackview.switch({
"image":slice_image,
"binary":binary,
"labels":labels
})
VBox(children=(HBox(children=(VBox(children=(ImageWidget(height=254, width=256),)),)), HBox(children=(Button(d…
stackview.switch([image_stack[1:], image_stack[:-1]])
VBox(children=(HBox(children=(VBox(children=(ImageWidget(height=389, width=235),)),)), HBox(children=(Button(d…
stackview.switch([
slice_image,
binary,
labels
])
VBox(children=(HBox(children=(VBox(children=(ImageWidget(height=254, width=256),)),)), HBox(children=(Button(d…
hela_cells = imread("data/hela-cells.tif")
hela_cells.shape
(512, 672, 3)
stackview.switch(
{"lysosomes": hela_cells[:,:,0],
"mitochondria":hela_cells[:,:,1],
"nuclei": hela_cells[:,:,2]
},
colormap=["pure_magenta", "pure_green", "pure_blue"],
toggleable=True
)
VBox(children=(HBox(children=(VBox(children=(ImageWidget(height=512, width=672),)),)), HBox(children=(ToggleBu…
The crop
widget allows to crop the image in Z,Y and X.
crop_widget = stackview.crop(image_stack)
crop_widget
_Cropper(children=(IntRangeSlider(value=(0, 100), description='Z'), IntRangeSlider(value=(0, 389), description…
You can retrieve the range the user entered like this:
crop_widget.range
(slice(0, 100, 1), slice(0, 389, 1), slice(0, 235, 1))
You can also get the cropped image directly.
crop_widget.crop()
|
|
You can also use interact
to explore parameters of some supported functions that process images, e.g. from scikit-image:
from skimage.filters.rank import maximum
stackview.interact(maximum, slice_image)
VBox(children=(interactive(children=(IntSlider(value=0, description='footprint'), Output()), _dom_classes=('wi…
from skimage.filters import gaussian
stackview.interact(gaussian, slice_image)
VBox(children=(interactive(children=(FloatSlider(value=1.0, description='sigma', max=25.0, step=1.0), Output()…
This might be interesting for custom functions executing image processing workflows.
from skimage.filters import gaussian, threshold_otsu, sobel
def my_custom_code(image, sigma:float = 1, show_labels: bool = True):
sigma = abs(sigma)
blurred_image = gaussian(image, sigma=sigma)
binary_image = blurred_image > threshold_otsu(blurred_image)
edge_image = sobel(binary_image)
if show_labels:
return label(binary_image)
else:
return edge_image * 255 + image
stackview.interact(my_custom_code, slice_image, continuous_update=True)
VBox(children=(interactive(children=(FloatSlider(value=1.0, description='sigma', max=25.0, step=1.0), Checkbox…
If you want to configure the range of a slider explicitly, you need to hand over the ipywidgets slider as default value:
from skimage.filters import gaussian
from ipywidgets import FloatSlider
stackview.interact(gaussian, slice_image, sigma=FloatSlider(min=0, max=100, value=15), continuous_update=True)
VBox(children=(interactive(children=(FloatSlider(value=15.0, description='sigma'), Output()), _dom_classes=('w…
The stackview.assist()
function can guide you through all imported (and supported) image processing functions.
stackview.assist(context=globals(), continuous_update=True)
VBox(children=(Text(value='', description='Search', placeholder='Type here to search'), Dropdown(description='…
import numpy as np
silly_image = np.zeros((100, 100))
silly_image[:,50:] = 1
silly_image[50:] = silly_image[50:] + 2
stackview.picker(silly_image.astype(np.uint32))
VBox(children=(HBox(children=(VBox(children=(ImageWidget(height=100, width=100),)),)), IntSlider(value=50, des…
silly_image = np.zeros((3, 3))
silly_image[:,1:] = 1
silly_image[1:] = silly_image[1:] + 2
stackview.picker(silly_image.astype(np.uint32), zoom_factor=20)
VBox(children=(HBox(children=(VBox(children=(ImageWidget(height=60, width=60),)),)), IntSlider(value=1, descri…