The [seeded] watershed algorithm#

The watershed algorithm is traditionally used for cell segmentation in microscopy images when a membrane marker was used. If image quality and resolution is high, the algorithm works great. In cases where high image quality cannot be achieved, deep-learning approaches are outperforming the watershed algorithm.

We will use the scriptable napari plugin napari-segment-blobs-and-things-with-membranes. Under the hood, this plugin uses functions from scikit-image. We also use napari-simpleitk-image-processing which is based on the Insight Toolkit, maybe the oldest open-source image processing library on earth.

import napari_segment_blobs_and_things_with_membranes as nsbatwm
import napari_simpleitk_image_processing as nsitk
from skimage.io import imread, imshow
from skimage.data import cells3d
import stackview

As example we will use a single 2d slice from the cells3d dataset from scikit-image. The image has two channels: A nuclei-channel and a channel showing cell membranes.

image_stack = cells3d()

nuclei_image = image_stack[30, 1]
imshow(nuclei_image)
<matplotlib.image.AxesImage at 0x16f1e19a0a0>
../_images/13462473674ba33c5488ec094b16feac7198aed0fbc68fbeefea10dfd45dc900.png
membrane_image = image_stack[30, 0]
imshow(membrane_image, vmax=10000)
<matplotlib.image.AxesImage at 0x16f1f1c37f0>
../_images/60e16ddf626b969545240aef868b6fe8d5c2c7a0ba6ab2f573f0662bf42ef061.png

Before we can apply the seeded watershed algorithm, we need seeds. The nuclei channel allows us to make seeds, e.g. using the Voronoi-Otsu-Labeling algorithm.

nuclei_labels = nsbatwm.voronoi_otsu_labeling(nuclei_image, spot_sigma=10, outline_sigma=1)

nuclei_labels
nsbatwm made image
shape(256, 256)
dtypeint32
size256.0 kB
min0
max25

We can then use these seeds to flood the membrane-image using the watershed algorithm.

cell_labels = nsbatwm.seeded_watershed(membrane_image, nuclei_labels)

cell_labels
nsbatwm made image
shape(256, 256)
dtypeint32
size256.0 kB
min1
max25

In case the segmentation would not look perfect, it is common to apply pre-processing filters to the membrane image. This allows to make outlines smoother for example:

blurred_image = nsbatwm.gaussian_blur(membrane_image, sigma=4)

blurred_image
nsbatwm made image
shape(256, 256)
dtypefloat64
size512.0 kB
min0.011357974378858864
max0.18709104423856565
cell_labels2 = nsbatwm.seeded_watershed(blurred_image, nuclei_labels)

cell_labels2
nsbatwm made image
shape(256, 256)
dtypeint32
size256.0 kB
min1
max25

Splitting touching objects#

There are other use-cases for the watershed algorithm. In this section we will split objects in binary images that have a roundish shape and touch each other.

Starting point for this is a binary image, e.g. made using thresholding.

blobs = imread('../03b_image_processing/data/blobs.tif')

stackview.insight(blobs)
shape(254, 256)
dtypeuint8
size63.5 kB
min8
max248
binary = nsbatwm.threshold_otsu(blobs).astype(bool)

binary
<__array_function__ internals>:180: RuntimeWarning: Converting input from bool to <class 'numpy.uint8'> for compatibility.
nsbatwm made image
shape(254, 256)
dtypebool
size63.5 kB
minFalse
maxTrue

We can then split the touching object by only taking the binary image into account. The underlying algorithm aims to produce similar results to ImageJ’s binary watershed algorithm and the implementation here also works in 3D.

split_objects = nsbatwm.split_touching_objects(binary)
split_objects
<__array_function__ internals>:180: RuntimeWarning: Converting input from bool to <class 'numpy.uint8'> for compatibility.
nsbatwm made image
shape(254, 256)
dtypebool
size63.5 kB
minFalse
maxTrue

It is also possible to retrieve a label image as result. Note that in this case, the black line/gap between objects will not be present.

touching_labels = nsitk.touching_objects_labeling(binary)
touching_labels
n-sitk made image
shape(254, 256)
dtypeuint32
size254.0 kB
min0
max68