Note
Go to the end to download the full example code. or to run this example in your browser via Binder
Flood Fill#
Flood fill is an algorithm to identify and/or change adjacent values in an image based on their similarity to an initial seed point [1]. The conceptual analogy is the ‘paint bucket’ tool in many graphic editors.
Basic example#
First, a basic example where we will change a checkerboard square from white to mid-gray.
import numpy as np
import matplotlib.pyplot as plt
from skimage import data, filters, color, morphology
from skimage.segmentation import flood, flood_fill
checkers = data.checkerboard()
# Fill a square near the middle with value 127, starting at index (76, 76)
filled_checkers = flood_fill(checkers, (76, 76), 127)
fig, ax = plt.subplots(ncols=2, figsize=(10, 5))
ax[0].imshow(checkers, cmap=plt.cm.gray)
ax[0].set_title('Original')
ax[1].imshow(filled_checkers, cmap=plt.cm.gray)
ax[1].plot(76, 76, 'wo') # seed point
ax[1].set_title('After flood fill')
plt.show()
Advanced example#
Because standard flood filling requires the neighbors to be strictly equal, its use is limited on real-world images with color gradients and noise. The tolerance keyword argument widens the permitted range about the initial value, allowing use on real-world images.
Here we will experiment a bit on the cameraman. First, turning his coat from dark to light.
cameraman = data.camera()
# Change the cameraman's coat from dark to light (255). The seed point is
# chosen as (155, 150)
light_coat = flood_fill(cameraman, (155, 150), 255, tolerance=10)
fig, ax = plt.subplots(ncols=2, figsize=(10, 5))
ax[0].imshow(cameraman, cmap=plt.cm.gray)
ax[0].set_title('Original')
ax[0].axis('off')
ax[1].imshow(light_coat, cmap=plt.cm.gray)
ax[1].plot(150, 155, 'ro') # seed point
ax[1].set_title('After flood fill')
ax[1].axis('off')
plt.show()
The cameraman’s coat is in varying shades of gray. Only the parts of the coat matching the shade near the seed value is changed.
Experimentation with tolerance#
To get a better intuitive understanding of how the tolerance parameter works, here is a set of images progressively increasing the parameter with seed point in the upper left corner.
output = []
for i in range(8):
tol = 5 + 20 * i
output.append(flood_fill(cameraman, (0, 0), 255, tolerance=tol))
# Initialize plot and place original image
fig, ax = plt.subplots(nrows=3, ncols=3, figsize=(12, 12))
ax[0, 0].imshow(cameraman, cmap=plt.cm.gray)
ax[0, 0].set_title('Original')
ax[0, 0].axis('off')
# Plot all eight different tolerances for comparison.
for i in range(8):
m, n = np.unravel_index(i + 1, (3, 3))
ax[m, n].imshow(output[i], cmap=plt.cm.gray)
ax[m, n].set_title(f'Tolerance {5 + 20 * i}')
ax[m, n].axis('off')
ax[m, n].plot(0, 0, 'bo') # seed point
fig.tight_layout()
plt.show()
Flood as mask#
A sister function, flood, is available which returns a mask identifying the flood rather than modifying the image itself. This is useful for segmentation purposes and more advanced analysis pipelines.
Here we segment the nose of a cat. However, multi-channel images are not supported by flood[_fill]. Instead we Sobel filter the red channel to enhance edges, then flood the nose with a tolerance.
cat = data.chelsea()
cat_sobel = filters.sobel(cat[..., 0])
cat_nose = flood(cat_sobel, (240, 265), tolerance=0.03)
fig, ax = plt.subplots(nrows=3, figsize=(10, 20))
ax[0].imshow(cat)
ax[0].set_title('Original')
ax[0].axis('off')
ax[1].imshow(cat_sobel)
ax[1].set_title('Sobel filtered')
ax[1].axis('off')
ax[2].imshow(cat)
ax[2].imshow(cat_nose, cmap=plt.cm.gray, alpha=0.3)
ax[2].plot(265, 240, 'wo') # seed point
ax[2].set_title('Nose segmented with `flood`')
ax[2].axis('off')
fig.tight_layout()
plt.show()
Flood-fill in HSV space and mask post-processing#
Since flood fill operates on single-channel images, we transform here the image to the HSV (Hue Saturation Value) space in order to flood pixels of similar hue.
In this example we also show that it is possible to post-process the binary
mask returned by skimage.segmentation.flood()
thanks to the functions
of skimage.morphology
.
img = data.astronaut()
img_hsv = color.rgb2hsv(img)
img_hsv_copy = np.copy(img_hsv)
# flood function returns a mask of flooded pixels
mask = flood(img_hsv[..., 0], (313, 160), tolerance=0.016)
# Set pixels of mask to new value for hue channel
img_hsv[mask, 0] = 0.5
# Post-processing in order to improve the result
# Remove white pixels from flag, using saturation channel
mask_postprocessed = np.logical_and(mask, img_hsv_copy[..., 1] > 0.4)
# Remove thin structures with binary opening
mask_postprocessed = morphology.binary_opening(mask_postprocessed, np.ones((3, 3)))
# Fill small holes with binary closing
mask_postprocessed = morphology.binary_closing(mask_postprocessed, morphology.disk(20))
img_hsv_copy[mask_postprocessed, 0] = 0.5
fig, ax = plt.subplots(1, 2, figsize=(8, 4))
ax[0].imshow(color.hsv2rgb(img_hsv))
ax[0].axis('off')
ax[0].set_title('After flood fill')
ax[1].imshow(color.hsv2rgb(img_hsv_copy))
ax[1].axis('off')
ax[1].set_title('After flood fill and post-processing')
fig.tight_layout()
plt.show()
Total running time of the script: (0 minutes 9.593 seconds)