๐Ÿงฉ Image Segmentation: Dividing the World into Pixels of Interest#

Image segmentation is all about labeling pixels to distinguish objects of interest ๐Ÿ•ต๏ธโ€โ™‚๏ธ from their background. Whether itโ€™s coins on a table ๐Ÿ’ฐ or planets in the night sky ๐ŸŒŒ, segmentation helps us break an image into meaningful parts. Letโ€™s take a journey through some powerful segmentation techniques with a quirky engineering twist!

๐Ÿช™ Coin Segmentation Challenge: When Backgrounds Play Tricks#

Weโ€™ll start by segmenting coins ๐Ÿช™ from a dark background using the skimage.data.coins image.

Hereโ€™s the initial image:

import skimage as ski
import matplotlib.pyplot as plt

coins = ski.data.coins()

plt.imshow(coins, cmap="gray")
<matplotlib.image.AxesImage at 0x7f4edf11a550>
../../_images/5cdcc5f0c46548279ca464a6646e985d2eb42b747b2c434a2db74ba76d1e795c.png

At first glance, this seems easyโ€”just pick out the bright pixels for coins, right? Not so fast! The background shares some similar gray levels with the coins. A simple threshold wonโ€™t cut it! Letโ€™s see why:

from skimage.exposure import histogram

hist, hist_centers = ski.exposure.histogram(coins)
plt.figure(figsize=(10, 5))
plt.plot(hist_centers, hist, lw=2)
plt.title("Histogram of Coin Image")
plt.xlabel("Pixel Intensity")
plt.ylabel("Frequency")
plt.show()
../../_images/7b41899dec5b3478a74b66a03871b35d8ab0ac71cd002ad59e8d493e0b5428b9.png
fig, axes = plt.subplots(1, 2, figsize=(12, 6))

axes[0].imshow(coins > 100, cmap="gray")
axes[0].set_title("Threshold > 100")

axes[1].imshow(coins > 150, cmap="gray")
axes[1].set_title("Threshold > 150")

for ax in axes:
    ax.axis("off")

plt.tight_layout()
plt.show()
../../_images/4bb36738ba99f46dae83a5ac44eeded5ea31528abbb42cf89045ff4d5b8169e8.png

๐Ÿ“ˆ Why Simple Thresholding Fails#

Thresholding assumes clear boundaries between objects (coins) and the background. But due to uneven lighting, it either misses parts of the coins or merges the background into them. Not great for an engineer designing a coin-sorting machine! โš™๏ธ๐Ÿ’ต

๐Ÿ—ก๏ธ Edge-Based Segmentation: Sharpening the Boundaries#

Letโ€™s try detecting edges around the coins using the Canny edge detector. This method finds sharp intensity changes (edges) in the image:

edges = ski.feature.canny(coins / 255.0)

plt.imshow(edges, cmap="gray")
<matplotlib.image.AxesImage at 0x7f4e860fdc10>
../../_images/28427fc9b1e8025b307cfbcbf5b0f2700d115115d221f7937bc7253de177a697.png

Tip

Engineers often use edge detection to analyze cracks in airplane wings โœˆ๏ธ or leaks in oil pipelines ๐Ÿ›ข๏ธ.

Once edges are detected, we fill the coin interiors using binary_fill_holes:

import scipy as sp

fill_coins = sp.ndimage.binary_fill_holes(edges)

plt.imshow(fill_coins, cmap="gray")
<matplotlib.image.AxesImage at 0x7f4e86169e50>
../../_images/ef139a82ac39215ba5211f1e849e2441ac01d4111ee13c1484abc5c602acf3fa.png

๐ŸŽ‰ Result: Most coins are segmented, but one stubborn coin didnโ€™t make the cut. Why? The contour wasnโ€™t fully closed. This method is sensitive to gaps in edges. ๐Ÿ˜•

๐Ÿงผ Cleaning Up Small Artifacts#

Background noise can be removed using ndi.label, which identifies connected components and filters out small ones:

import numpy as np

label_objects, nb_labels = sp.ndimage.label(fill_coins)
sizes = np.bincount(label_objects.ravel())
mask_sizes = sizes > 20
mask_sizes[0] = 0
coins_cleaned = mask_sizes[label_objects]

plt.imshow(coins_cleaned, cmap="gray")
<matplotlib.image.AxesImage at 0x7f4e85f6d7d0>
../../_images/8ae35584d461f03f047208c89f3d0e715a60893ab5b730d8b11e22108659c45a.png

๐Ÿ’ฆ Region-Based Segmentation: Letโ€™s Flood the Image!#

For a more robust approach, we turn to watershed segmentation, inspired by hydrology ๐ŸŒŠ. Imagine โ€œfloodingโ€ the image with water from predefined markers (sources). Watershed lines separate regions, creating well-defined boundaries.

Step 1: Define Markers ๐Ÿ#

Markers are pixels we can confidently label as part of the object (coins) or background. Here, we use extreme intensity values:

markers = np.zeros_like(coins)
markers[coins < 30] = 1  # Background
markers[coins > 150] = 2  # Coins

plt.imshow(markers, cmap="gray")
<matplotlib.image.AxesImage at 0x7f4e85f91e50>
../../_images/f8ebc3c7d44619ccdd9c120a9e392aa0b1443d9498039cbc0e3d1e31ef8dcdbe.png

Step 2: Elevation Map ๐ŸŒ„#

We create an elevation map where pixel gradients act as barriers. Higher gradients separate objects from the background:

elevation_map = ski.filters.sobel(coins)


plt.imshow(elevation_map, cmap="viridis")
plt.title("Elevation Map")
plt.colorbar()
plt.show()
../../_images/a73bdbaaf30aee901f29aa14640525973d4516766c823ba1fe907cfe3b8b2d55.png
import plotly.graph_objects as go

fig = go.Figure(data=[go.Surface(z=elevation_map)])
fig.update_layout(
    title="3D Elevation Map",
    autosize=False,
    width=800,
    height=800,
    margin=dict(l=65, r=50, b=65, t=90),
)
fig.show()
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[9], line 1
----> 1 import plotly.graph_objects as go
      3 fig = go.Figure(data=[go.Surface(z=elevation_map)])
      4 fig.update_layout(
      5     title="3D Elevation Map",
      6     autosize=False,
   (...)
      9     margin=dict(l=65, r=50, b=65, t=90),
     10 )

ModuleNotFoundError: No module named 'plotly'

Step 3: Apply Watershed Segmentation#

Flood the elevation map starting from the markers:

segmentation = ski.segmentation.watershed(elevation_map, markers)

plt.imshow(segmentation, cmap="tab20b")
<matplotlib.image.AxesImage at 0x7f0a6efebec0>
../../_images/4619a3ea5e2de087c9f16e8fdf9dae81db82bc9afdc36e14f0bd6aaae703675d.png

Now we fill any small holes using binary_fill_holes:

segmentation = sp.ndimage.binary_fill_holes(segmentation - 1)

plt.imshow(segmentation, cmap="tab20b")
<matplotlib.image.AxesImage at 0x7f0a6f48d7c0>
../../_images/7a9c0286977d50cf16b323c48c41620c16993648ba00d2b9bb852b0b534b8dc9.png

Finally, we label each segmented coin:

from color_map import discrete_cmap

colormap = discrete_cmap(30, "cubehelix")

labeled_coins, _ = sp.ndimage.label(segmentation)

# need to make the baground -10 to be visible in the colormap
labeled_coins[labeled_coins == 0] = -10

plt.imshow(labeled_coins, cmap=colormap)
<matplotlib.image.AxesImage at 0x7f0a6fcd46e0>
../../_images/0a6a3a1f762f64e95ae0dc228cf1591074f80d26e7b6be51e8b49764b8516e43.png

๐ŸŽ‰ Result: All coins are perfectly segmented! Even with challenging lighting conditions, the watershed transform triumphs. ๐Ÿ†

๐Ÿง  Where Does This Work in Real Life?#

  • Manufacturing: Detecting defects in coins ๐Ÿ’ฐ, circuit boards ๐Ÿ’ป, or mechanical parts โš™๏ธ.

  • Medicine: Separating tumors ๐Ÿฉบ from healthy tissue in medical scans.

  • Astronomy: Identifying planets ๐ŸŒ or stars โœจ in telescope images.

With these techniques, you can tackle segmentation challenges with confidence. Whether youโ€™re designing a robotic coin sorter ๐Ÿค–๐Ÿ’ฐ or analyzing biological samples ๐Ÿงฌ, scikit-image has your back! You will have the opportunity to work more with scikit-image in your lab.