Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

From pure Python to interactive napari widget

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.

01_pure_python.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from 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)
Loading...

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.

02_magicgui.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from 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:

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!

Loading...

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!

03_sliders.py

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
31
from 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})
Loading...

Recap

Next up: we’ll turn this thresholding into a full segmentation pipeline with label cleaning, region properties, and watershed refinement.