In this module, we start with a pure Python segmentation function and
progressively integrate it with napari using magicgui widgets and
customizable sliders. By the end, you will have an interactive thresholding
widget inside napari that updates in real time.
1. A pure Python threshold function¶
We begin with a pure Python function that takes a numpy array, applies a Gaussian blur and a threshold, and returns a binary mask.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15from skimage import data, filters import napari import numpy as np def threshold( data: np.ndarray, sigma: float = 0.5, threshold: float = 0.3, ) -> np.ndarray: """Apply a gaussian filter and threshold to image data.""" norm = (data - np.min(data)) / np.max(data) blur = filters.gaussian(norm, sigma=sigma) blobs = blur >= threshold return blobs
Let’s test it outside napari first.
image = data.cells3d()[30, 1] # 2d slice
blobs = threshold(image, sigma=1, threshold=0.5)
print(f"Input shape: {image.shape}, Output shape: {blobs.shape}")
print(f"Number of foreground pixels: {blobs.sum()}")Input shape: (256, 256), Output shape: (256, 256)
Number of foreground pixels: 298
We can now run it on an image loaded into napari.
viewer = napari.Viewer()
image = data.cells3d()[30, 1] # 2d
image_layer = viewer.add_image(image)
blobs = threshold(image_layer.data, sigma=1, threshold=0.5)
blobs_layer = viewer.add_image(blobs)This works, but every time we want to try different parameters we have to
re-run the function. Wouldn’t it be nice to tweak sigma and threshold
interactively inside napari?
2. napari + magicgui — a widget from a function¶
napari has built-in support for magicgui, a library that automatically generates GUIs from Python function type annotations.
Let’s rewrite our function so it takes a napari Image layer instead of a raw
array, and returns LayerDataTuple — a format napari understands for creating
new layers.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21from skimage import data, filters import napari import numpy as np def threshold( layer: napari.layers.Image, sigma: float = 0.5, threshold: float = 0.3, ) -> list[napari.types.LayerDataTuple]: """Apply a gaussian filter and threshold to a napari Image.""" if not layer: return norm = (layer.data - np.min(layer.data)) / np.max(layer.data) blur = filters.gaussian(norm, sigma=sigma) blobs = blur >= threshold return [ (blur, {'name': 'blur'}, 'image'), (blobs, {'name': 'blobs'}, 'image'), ]
Notice the changes:
The first parameter is now
layer: napari.layers.Image— magicgui will read the type annotation and create a dropdown to select an Image layer.The return type
list[napari.types.LayerDataTuple]tells magicgui to create new layers from the returned data.We had to add a
if not layerguard, to prevent errors when no Image layer is present in the viewer.
Now let’s launch napari and add this function as a dock widget.
viewer = napari.Viewer()
image = data.cells3d()[30, 1] # 2d
image_layer = viewer.add_image(image)
viewer.window.add_function_widget(threshold)You should see a widget panel with dropdowns for layer, and spin boxes
for sigma and threshold. Click Run to apply the function. Try different
values!
This is already more interactive than the pure function, but the controls are still
a bit clunky, and we have to press Run every time we change the parameters...
3. Annotated sliders with auto_call¶
Let’s replace the spin boxes with sliders, and autorun the function every time the
parameters are changed. We use typing.Annotated to attach widget metadata to each
parameter, and use the magic_kwargs={'auto_call': True} argument when adding the
function widget to napari.
Now move the sliders and watch the magic happen!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31from skimage import data, filters import napari import numpy as np from typing import Annotated def threshold( layer: napari.layers.Image, sigma: Annotated[float, {'widget_type': 'FloatSlider', 'min': 0, 'max': 2, 'step': 0.1}] = 0.5, threshold: Annotated[float, {'widget_type': 'FloatSlider', 'min': 0, 'max': 1, 'step': 0.05}] = 0.3, ) -> list[napari.types.LayerDataTuple]: """Apply a gaussian filter and threshold to a napari Image. When added to napari as a function widget, expose parameters as sliders. """ if not layer: return norm = (layer.data - np.min(layer.data)) / np.max(layer.data) blur = filters.gaussian(norm, sigma=sigma) blobs = blur >= threshold return [ (blur, {'name': 'blur'}, 'image'), (blobs, {'name': 'blobs'}, 'image'), ] viewer = napari.Viewer() image = data.cells3d()[30, 1] # 2d image_layer = viewer.add_image(image) viewer.window.add_function_widget(threshold, magic_kwargs={'auto_call': True})
Recap¶
1. Pure Python function on numpy arrays — works but requires manual re-runs.
2. Magicgui widget from a function with napari type annotations — interactive GUI inside napari, but still needs a “Run” click.
3. Annotated type hints for sliders +
auto_call— fully interactive, real-time updates.
Next up: we’ll turn this thresholding into a full segmentation pipeline with label cleaning, region properties, and watershed refinement.