Binary mask refinement#

A typical post-processing step after thresholding is refining binary masks. This step can be crucial to smooth outlines around segmented objects, remove single pixels which were segmented as positive and for filling black holes in white regions.

See also

We start with the segmented embryos_grey.tif example image. This image is a single-channel crop of an image known from the ImageJ example images.

from skimage.io import imread
from skimage import filters
import matplotlib.pyplot as plt
from skimage.morphology import disk, binary_erosion, binary_dilation, binary_opening, binary_closing
import numpy as np
from scipy.ndimage import binary_fill_holes
import pyclesperanto_prototype as cle
# load image
image = imread("../../data/embryos_grey.tif")

# binarize the image
threshold = filters.threshold_otsu(image)
binary_image = image <= threshold

# Show original image and binary image side-by-side
fig, axs = plt.subplots(1, 2, figsize=(15,15))
cle.imshow(image, plot=axs[0])
axs[0].set_title('Original')

cle.imshow(binary_image, plot=axs[1])
axs[1].set_title('Binary')
Text(0.5, 1.0, 'Binary')
../_images/b7f7385e423d17562dbbd808346a31bcf4a5fbb1e7439b960820879909bace2d.png

Binary erosion#

Binary erosion turns white pixels black which have a black neighboring pixel. The neighborhood is defined by a structuring element. Thus, coastlines of the islands are eroded.

eroded1 = binary_erosion(binary_image, disk(1))
eroded4 = binary_erosion(binary_image, disk(4))

fig, axs = plt.subplots(1, 3, figsize=(15,15))
cle.imshow(binary_image, plot=axs[0])
axs[0].set_title('Binary image')

cle.imshow(eroded1, plot=axs[1])
axs[1].set_title('Eroded r=1')

cle.imshow(eroded4, plot=axs[2])
axs[2].set_title('Eroded r=4')
Text(0.5, 1.0, 'Eroded r=4')
../_images/0313d2863927d61301b0607926104d18b29d5d8975b1dfa3016369d77250356d.png

Binary dilation#

Analogously, dilation turns black pixels white which have a white neighbor.

dilated1 = binary_dilation(binary_image, disk(1))
dilated4 = binary_dilation(binary_image, disk(4))

fig, axs = plt.subplots(1, 3, figsize=(15,15))
cle.imshow(binary_image, plot=axs[0])
axs[0].set_title('Binary image')

cle.imshow(dilated1, plot=axs[1])
axs[1].set_title('Dilated r=1')

cle.imshow(dilated4, plot=axs[2])
axs[2].set_title('Dilated r=4')
Text(0.5, 1.0, 'Dilated r=4')
../_images/06ecc2172d9c2cd59f8f4b6f8c171cfe0b34fa398fe93be8efa8427ab4e35523.png

Fill holes#

Another important operation is fill holes which is part of the scipy package.

filled = binary_fill_holes(binary_image)

fig, axs = plt.subplots(1, 2, figsize=(15,15))
cle.imshow(binary_image, plot=axs[0])
axs[0].set_title('Binary image')

cle.imshow(filled, plot=axs[1])
axs[1].set_title('Holes filled')
Text(0.5, 1.0, 'Holes filled')
../_images/ee9dd6d23c7ecab60907582d6b95b4834238dd7d5783b2eab69e8c4e199e4ee4.png

Binary closing and opening#

By combining operations such as erosion and dilation subsequently, one can close and open binary images.

opened = binary_opening(binary_image, disk(4))
closed = binary_closing(binary_image, disk(4))

fig, axs = plt.subplots(1, 3, figsize=(15,15))
cle.imshow(binary_image, plot=axs[0])
axs[0].set_title('Binary image')

cle.imshow(opened, plot=axs[1])
axs[1].set_title('Opened')

cle.imshow(closed, plot=axs[2])
axs[2].set_title('Closed')
Text(0.5, 1.0, 'Closed')
../_images/a10bdde8d7ca4fbda898db9052f737037ee5b9dcc968e8cbe1a1dcc6b88c1375.png

In some libraries, such as clesperanto there are no dedicated functions for binary opening and closing. However, one can use morphological opening and closing as these operations are mathematically both suitable for processsing binary images. Applying binary opening and closing to intensity images is not recemmended.

opened2 = cle.opening_sphere(binary_image, radius_x=4, radius_y=4)
closed2 = cle.closing_sphere(binary_image, radius_x=4, radius_y=4)

fig, axs = plt.subplots(1, 3, figsize=(15,15))
cle.imshow(binary_image, plot=axs[0])
axs[0].set_title('Binary image')

cle.imshow(opened2, plot=axs[1])
axs[1].set_title('Opened')

cle.imshow(closed2, plot=axs[2])
axs[2].set_title('Closed')
Text(0.5, 1.0, 'Closed')
../_images/a10bdde8d7ca4fbda898db9052f737037ee5b9dcc968e8cbe1a1dcc6b88c1375.png

Comparing binary images#

For better visualization of differenced between binary images, we would like to subtract one of the two binary images from the other. If we compute the absolute of this image, we should an image, where all pixels are have value 1 where the two binary images have different values. Unfortunately, we cannot subtract binary images with values True and False using the - operator. We first should turn the True/False binary images into numeric images. This is possible by multiplying the images with 1:

absolute_difference = np.abs(opened * 1 - binary_image * 1)

cle.imshow(absolute_difference)
../_images/ef6d52ee7f9b83e7996c0c7b2d973efb6f2a0d35daa2b26fb077e9fcd4aedd9c.png

The same result can also be achieved using pyclesperanto’s absolute_difference function:

absolute_difference2 = cle.absolute_difference(opened, binary_image)

cle.imshow(absolute_difference2)
../_images/ef6d52ee7f9b83e7996c0c7b2d973efb6f2a0d35daa2b26fb077e9fcd4aedd9c.png

Exercise#

In the following code example, embryos_grey.jpg is processed using Gaussian filtering and Otsu-thresholding. Process the same image only using Otsu-thresholding and binary post-processing operations. Can you achieve the same binary image?

from skimage.io import imread, imshow
image = imread("../../data/embryos_grey.tif")

from skimage import filters

# noise removal
blurred = filters.gaussian(image, sigma=4)

# thresholding
threshold = filters.threshold_otsu(blurred)
binary_image = blurred <= threshold

# result visualization
cle.imshow(binary_image * 1)
../_images/50cf9f8c5db0b96dbeef0f32c1b79a83b8554b848d5620bfb888091b164621cb.png