# Mimicking ImageJ’s watershed algorithm#

In ImageJ there is an algorithm called “Watershed” which allows splitting segmented dense objects within a binary image. This notebook demonstrates how to achieve a similar operation in Python.

The “Watershed” in ImageJ was applied to a binary image using this macro:

open("../BioImageAnalysisNotebooks/data/blobs_otsu.tif");
run("Watershed");

from skimage.io import imread, imshow
import matplotlib.pyplot as plt
import napari_segment_blobs_and_things_with_membranes as nsbatwm
import numpy as np

from scipy import ndimage as ndi
from skimage.feature import peak_local_max
from skimage.filters import gaussian, sobel
from skimage.measure import label
from skimage.segmentation import watershed
from skimage.morphology import binary_opening


Starting point for the demonstration is a binary image.

binary_image = imread("../../data/blobs_otsu.tif")
imshow(binary_image)

<matplotlib.image.AxesImage at 0x1f8e41b2700>


After applying the macro shown above, the result image in ImageJ looksl ike this:

binary_watershed_imagej = imread("../../data/blobs_otsu_watershed.tif")
imshow(binary_watershed_imagej)

<matplotlib.image.AxesImage at 0x1f8e4209100>


The Napari plugin napari-segment-blobs-and-things-with-membranes offers a function for mimicking the functionality from ImageJ.

binary_watershed_nsbatwm = nsbatwm.split_touching_objects(binary_image)
imshow(binary_watershed_nsbatwm)

<matplotlib.image.AxesImage at 0x1f8e42a63d0>


## Comparing results#

When comparing results, it is obvious that the results are not 100% identical.

fig, axs = plt.subplots(1, 2, figsize=(10,10))

axs[0].imshow(binary_watershed_imagej)
axs[0].set_title("ImageJ")
axs[1].imshow(binary_watershed_nsbatwm)
axs[1].set_title("nsbatwm")

Text(0.5, 1.0, 'nsbatwm')


## Fine-tuning results#

Modifying the result is possible by tuning the sigma parameter.

fig, axs = plt.subplots(1, 4, figsize=(10,10))

for i, sigma in enumerate(np.arange(2, 6, 1)):
result = nsbatwm.split_touching_objects(binary_image, sigma=sigma)
axs[i].imshow(result)
axs[i].set_title("sigma="+str(sigma))


## How does it work?#

Under the hood, ImageJ’s watershed algorithm uses a distance image and spot-detection. The following code attempts to replicate the result.

Again, we start from the binary image.

imshow(binary_image)

<matplotlib.image.AxesImage at 0x1f8e55b87f0>


The first step is to produce a distance image.

distance = ndi.distance_transform_edt(binary_image)
imshow(distance)

C:\Users\haase\mambaforge\envs\bio39\lib\site-packages\skimage\io\_plugins\matplotlib_plugin.py:150: UserWarning: Float image out of standard range; displaying image with stretched contrast.
lo, hi, cmap = _get_display_range(image)

<matplotlib.image.AxesImage at 0x1f8e55f17c0>


To avoid very small split objects, we blur the distance image using the sigma parameter.

sigma = 3.5

blurred_distance = gaussian(distance, sigma=sigma)
imshow(blurred_distance)

<matplotlib.image.AxesImage at 0x1f8e56ec5e0>


Within this blurred image, we search for local maxima and receive them as list of coordinates.

fp = np.ones((3,) * binary_image.ndim)
coords = peak_local_max(blurred_distance, footprint=fp, labels=binary_image)

# show the first 5 only
coords[:5]

array([[  8, 254],
[ 97,   1],
[ 10, 108],
[230, 180],
[182, 179]], dtype=int64)


We next write these maxima into a new image and label them.

mask = np.zeros(distance.shape, dtype=bool)
imshow(markers, cmap='jet')

C:\Users\haase\mambaforge\envs\bio39\lib\site-packages\skimage\io\_plugins\matplotlib_plugin.py:150: UserWarning: Low image data range; displaying image with stretched contrast.
lo, hi, cmap = _get_display_range(image)

<matplotlib.image.AxesImage at 0x1f8e59c1c40>


Next, we apply scikit-image’s Watershed algorithm (example). It takes a distance image and a label image as input. Optional input is the binary_image to limit spreading the labels to far.

labels = watershed(-blurred_distance, markers, mask=binary_image)
imshow(labels, cmap='jet')

<matplotlib.image.AxesImage at 0x1f8e5a97f40>


To create a binary image again as ImageJ does, we now identify the edges between the labels.

# identify label-cutting edges
edges_labels = sobel(labels)
edges_binary = sobel(binary_image)

edges = np.logical_xor(edges_labels != 0, edges_binary != 0)
imshow(edges)

<matplotlib.image.AxesImage at 0x1f8e5aaea00>


Next we subtract those edges from the original binary_image.

almost = np.logical_not(edges) * binary_image
imshow(almost)

<matplotlib.image.AxesImage at 0x1f8e55c8610>


As this result is not perfect yet, we apply a binary opening.

result = binary_opening(almost)
imshow(result)

<matplotlib.image.AxesImage at 0x1f8e3b81e50>