Chapter 9 · Morphological Image Processing

Image ProcessingIntermediate31 minOct 4, 2026

Needs: Chapter 8 · Image Compression and Watermarking

What you’ll learn

  • How to treat a binary image as a set of pixel coordinates, and how a small structuring element probes it
  • The two primitive operations, erosion and dilation, and how they combine into opening and closing
  • How the hit-or-miss transform finds exact pixel patterns such as corners and isolated points
  • Classic algorithms built from those primitives: boundaries, hole filling, connected components, convex hulls, thinning, skeletons and pruning
  • Morphological reconstruction, which removes objects without distorting the ones that survive
  • Grayscale morphology: gradients, top-hat correction of uneven lighting, and size distributions (granulometry)

The big picture

Most of the filters you met in earlier chapters were linear: each output pixel is a weighted sum of input pixels. Morphology is different. It asks yes-or-no questions about shape: “Does this little shape fit here?” “Does it touch anything here?” The answers are built only from minimum, maximum, intersection and union. No multiplication is involved.

Why it matters: morphology is the standard toolbox for cleaning binary masks (from thresholding in Chapter 10, or from a neural network), for measuring objects (counting, sizing, finding their skeletons), and for correcting uneven backgrounds. Its theory, mathematical morphology, grew out of work by Georges Matheron and Jean Serra on random sets and on the geometry of porous materials [2], [3]. Haralick, Sternberg and Zhuang’s tutorial brought it to the wider image-analysis community [4], and Soille’s book remains the standard practical reference [5]. This chapter follows the topic order of Gonzalez and Woods [1] and uses scikit-image [12] and SciPy for the code.

Preliminaries

In plain words: a binary image is a list of “on” pixel positions, and a structuring element is a tiny stencil with a marked centre that we slide over that list.

Images as sets

Let a binary image have foreground pixels (value 1) and background pixels (value 0). We describe the foreground by the set

A={(x,y)∈Z2∣I(x,y)=1},A = \{ (x, y) \in \mathbb{Z}^2 \mid I(x, y) = 1 \},

where II is the image and (x,y)(x, y) are integer pixel coordinates. The usual set operations now have image meanings: the complement AcA^c is the background, the union A∪CA \cup C is a pixel-wise OR, the intersection A∩CA \cap C is a pixel-wise AND, and the difference A−C=A∩CcA - C = A \cap C^c removes from AA everything in CC.

Structuring elements

A structuring element (SE) BB is a second, small set of offsets with a designated origin, usually its centre. Think of it as the probe. Common choices are a 3×33 \times 3 square, a cross (4-neighbourhood), a disk, or a line segment. The SE’s shape decides which features the operation responds to: a disk is direction-neutral, a horizontal line responds only to horizontal structure. scikit-image calls an SE a footprint. In practice, an SE is stored as a small Boolean array whose True entries are its members; elements shown as “don’t care” in diagrams are simply not in the set.

Reflection and translation

Two operations on sets appear in every definition below. The reflection of BB is

B^={w∣w=−b, b∈B},\hat{B} = \{ w \mid w = -b,\ b \in B \},

that is, every offset bb is replaced by −b-b: the SE is rotated 180° about its origin. The translation of BB by a vector z=(z1,z2)z = (z_1, z_2) is

(B)z={c∣c=b+z, b∈B},(B)_z = \{ c \mid c = b + z,\ b \in B \},

which places the SE’s origin at pixel zz. For symmetric SEs such as disks and squares, B^=B\hat{B} = B, so the reflection is invisible; for asymmetric ones it matters.

import numpy as np
from skimage import morphology

B = morphology.disk(2)            # 5x5 disk; origin at the centre (2, 2)
B_hat = B[::-1, ::-1]             # reflection: flip both axes
L = np.array([[1, 1, 0],
              [0, 1, 0],
              [0, 0, 0]], bool)   # an asymmetric element
print(L[::-1, ::-1].astype(int))  # its reflection points the other way

Erosion and dilation

In plain words: erosion keeps a pixel only if the whole stencil fits inside the shape there; dilation turns a pixel on if the stencil touches the shape at all.

Erosion

The erosion of AA by BB is

A⊖B={z∣(B)z⊆A},A \ominus B = \{ z \mid (B)_z \subseteq A \},

the set of all positions zz at which the translated SE lies entirely inside the foreground. Equivalently, A⊖B={z∣(B)z∩Ac=∅}A \ominus B = \{ z \mid (B)_z \cap A^c = \emptyset \}: the SE must not overlap any background pixel. Erosion shrinks objects by roughly the SE’s radius, deletes any part thinner than the SE, and separates objects joined by thin bridges. If BB contains its origin, erosion is anti-extensive: A⊖B⊆AA \ominus B \subseteq A.

Dilation

The dilation of AA by BB is

A⊕B={z∣(B^)z∩A≠∅},A \oplus B = \{ z \mid (\hat{B})_z \cap A \neq \emptyset \},

the set of positions at which the reflected SE overlaps at least one foreground pixel. An equivalent form is the union of copies of BB placed at every foreground pixel: A⊕B=⋃a∈A(B)aA \oplus B = \bigcup_{a \in A} (B)_a. Dilation grows objects, bridges gaps narrower than the SE, and fills small holes. It is extensive (A⊆A⊕BA \subseteq A \oplus B) when BB contains its origin. Unlike erosion, dilation is commutative: A⊕B=B⊕AA \oplus B = B \oplus A.

The reflection in the dilation formula is what makes dilation a true Minkowski sum and keeps it consistent with convolution’s flip. For symmetric SEs it changes nothing.

Duality

Erosion and dilation are duals with respect to complement and reflection:

(A⊖B)c=Ac⊕B^,(A⊕B)c=Ac⊖B^.(A \ominus B)^c = A^c \oplus \hat{B}, \qquad (A \oplus B)^c = A^c \ominus \hat{B}.

Eroding the foreground is the same as dilating the background (with the reflected SE), then flipping the result. The proof is one line: z∉A⊖Bz \notin A \ominus B exactly when (B)z(B)_z meets AcA^c, which is the definition of z∈Ac⊕B^z \in A^c \oplus \hat{B} (because B^^=B\hat{\hat{B}} = B). In code, duality holds exactly only if you treat the image border consistently: pixels outside the image must count as foreground for the erosion when they count as background for the dilation of the complement.

import numpy as np
from scipy import ndimage as ndi
from skimage import data, morphology

A = ~data.horse()                 # True = horse pixels
B = morphology.disk(3)

eroded  = ndi.binary_erosion(A, structure=B)
dilated = ndi.binary_dilation(A, structure=B)
print(A.sum(), eroded.sum(), dilated.sum())     # erosion shrinks, dilation grows

# Duality check. border_value=1 treats outside pixels as foreground for the
# erosion, which matches background-outside for the dilation of ~A.
lhs = ~ndi.binary_erosion(A, structure=B, border_value=1)
rhs = ndi.binary_dilation(~A, structure=B[::-1, ::-1])
print("duality holds:", np.array_equal(lhs, rhs))   # True

Opening and closing

In plain words: opening is “erode, then dilate back”: it removes specks and thin parts but leaves big shapes nearly unchanged. Closing is “dilate, then erode back”: it fills small holes and narrow cracks.

Definitions

The opening of AA by BB is erosion followed by dilation with the same SE,

A∘B=(A⊖B)⊕B,A \circ B = (A \ominus B) \oplus B,

and the closing is dilation followed by erosion,

A∙B=(A⊕B)⊖B.A \bullet B = (A \oplus B) \ominus B.

Erosion alone throws away detail but also shrinks everything; the second step restores the size of whatever survived. The pair is dual, just like erosion and dilation: (A∙B)c=Ac∘B^(A \bullet B)^c = A^c \circ \hat{B} and (A∘B)c=Ac∙B^(A \circ B)^c = A^c \bullet \hat{B}. Closing the foreground is opening the background.

Geometric interpretation: the rolling ball

Opening has a direct picture. It equals the union of all translates of BB that fit inside AA:

A∘B=⋃{(B)z∣(B)z⊆A}.A \circ B = \bigcup \{ (B)_z \mid (B)_z \subseteq A \}.

Roll a ball (the SE) around inside the foreground, staying fully inside. Every pixel the ball can reach is kept; corners sharper than the ball, thin necks and specks smaller than the ball are lost. Closing is the same picture applied from the outside: roll the ball around in the background; every background pixel the ball cannot reach (narrow gaps, small holes, deep thin bays) becomes foreground.

Properties

Opening and closing are both morphological filters in the technical sense: they are increasing and idempotent [5].

  • Opening is anti-extensive: A∘B⊆AA \circ B \subseteq A. It only removes pixels.
  • Closing is extensive: A⊆A∙BA \subseteq A \bullet B. It only adds pixels.
  • Increasing: if C⊆DC \subseteq D then C∘B⊆D∘BC \circ B \subseteq D \circ B, and the same for closing. Bigger input never gives smaller output.
  • Idempotent: (A∘B)∘B=A∘B(A \circ B) \circ B = A \circ B and (A∙B)∙B=A∙B(A \bullet B) \bullet B = A \bullet B. Applying the same opening twice changes nothing, because whatever survived the first pass already consists of places where the ball fits.

Idempotence is the property that separates opening and closing from erosion and dilation: repeated erosions keep shrinking, while repeated openings stop. Figure 9.1 shows all four operations on a noisy silhouette. The opening removes the white specks in the background; the closing removes the black specks inside the horse; opening followed by closing removes both, a simple and very common morphological noise filter.

Six panels: a noisy horse silhouette, its erosion, dilation, opening, closing, and opening followed by closing, all with a disk of radius 2.
Figure 9.1 — Binary erosion, dilation, opening and closing of a horse silhouette with 4% of pixels flipped, using a disk of radius 2. Opening then closing removes both kinds of speck while keeping the outline.
import numpy as np
from skimage import data, morphology

A = ~data.horse()
B = morphology.disk(4)
op = morphology.binary_opening(A, B)
cl = morphology.binary_closing(A, B)
print("opening is anti-extensive:", not (op & ~A).any())
print("closing is extensive:     ", not (A & ~cl).any())
print("opening is idempotent:    ", np.array_equal(morphology.binary_opening(op, B), op))

The hit-or-miss transform

In plain words: the hit-or-miss transform (HMT) is a template matcher for binary patterns. You say which pixels must be foreground and which must be background, and it lights up exactly where both conditions hold.

Use two disjoint SEs: B1B_1, the pixels that must lie in the foreground, and B2B_2, the pixels that must lie in the background. The HMT of AA is

A⊛B1,2={z∣(B1)z⊆A and (B2)z⊆Ac}=(A⊖B1)∩(Ac⊖B2).A \circledast B_{1,2} = \{ z \mid (B_1)_z \subseteq A \ \text{and}\ (B_2)_z \subseteq A^c \} = (A \ominus B_1) \cap (A^c \ominus B_2).

Here B1,2B_{1,2} denotes the pair (B1,B2)(B_1, B_2), A⊖B1A \ominus B_1 finds positions where the foreground pattern fits, and Ac⊖B2A^c \ominus B_2 finds positions where the background pattern fits. Pixels in neither B1B_1 nor B2B_2 are don’t-care positions. It is common to draw the pair as one small grid with 1 (must be foreground), 0 (must be background) and blank (don’t care).

Some useful templates:

  • Isolated point: B1B_1 is the centre pixel; B2B_2 is its 8 neighbours.
  • Upper-left corner: B1B_1 is the centre plus its right and lower neighbours; B2B_2 is the upper and left neighbours.
  • End point of a one-pixel line: the centre and exactly one neighbour are foreground; the rest are background. You need one template per direction (8 rotations).

Figure 9.2 applies the first two templates. Note that the staircase outline of a digital disk contains several genuine upper-left corners at pixel level: the HMT is literal, not geometric.

Three panels: a binary scene with rectangles, an L shape, a disk and two single pixels; detected upper-left corners circled in red; detected isolated pixels circled in orange.
Figure 9.2 — The hit-or-miss transform as an exact pattern detector. Middle: upper-left corners (including small steps on the digital disk). Right: isolated foreground pixels.
import numpy as np
from scipy import ndimage as ndi

A = np.zeros((7, 9), bool)
A[1:5, 1:6] = True                # a rectangle
A[5, 7] = True                    # an isolated pixel
hit  = np.array([[0, 0, 0], [0, 1, 1], [0, 1, 0]], bool)   # must be foreground
miss = np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]], bool)   # must be background
corners = ndi.binary_hit_or_miss(A, structure1=hit, structure2=miss)
print(np.argwhere(corners))       # [[1 1]]: the upper-left corner only

The HMT is the building block of thinning, thickening and the convex hull algorithm below.

Some basic morphological algorithms

In plain words: with erosion, dilation and the HMT you can build many practical tools. Most of them are short loops that repeat one operation until nothing changes.

Boundary extraction

The inner boundary of a set is what erosion removes:

β(A)=A−(A⊖B),\beta(A) = A - (A \ominus B),

where BB is a 3×33 \times 3 square (giving an 8-connected boundary) or a cross (giving a thicker, 4-connected one). Using a larger BB gives a thicker boundary. The first panel of Figure 9.3 shows β(A)\beta(A) for the horse.

Hole filling

A hole is a background region completely surrounded by foreground. Given one seed pixel inside a hole, grow the seed by repeated dilation but clip it to the background at every step:

Xk=(Xk−1⊕B)∩Ac,k=1,2,3,…X_k = (X_{k-1} \oplus B) \cap A^c, \qquad k = 1, 2, 3, \ldots

Here X0X_0 contains only the seed, BB is the 4-connected cross, and the intersection with AcA^c stops the growth at the hole’s wall. Stop when Xk=Xk−1X_k = X_{k-1}; then Xk∪AX_k \cup A is the filled shape. This “dilate, then clip” pattern is called conditional dilation, and it reappears below as geodesic dilation. Using the cross (not the square) as BB matters: with an 8-connected foreground wall, the 4-connected background fill cannot leak diagonally through the wall.

import numpy as np
from scipy import ndimage as ndi

def fill_hole(A, seed):
    """Grow X from a seed inside one hole: X_k = dilate(X_{k-1}) AND not A."""
    B = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]], bool)   # 4-connected cross
    X = np.zeros_like(A); X[seed] = True
    while True:
        X_next = ndi.binary_dilation(X, structure=B) & ~A
        if np.array_equal(X_next, X):
            return A | X
        X = X_next

yy, xx = np.mgrid[-12:13, -12:13]
ring = (np.hypot(yy, xx) <= 10) & (np.hypot(yy, xx) > 6)   # boolean ring
print(ring.sum(), fill_hole(ring, (12, 12)).sum())        # 204 -> 317

Extraction of connected components

Swap the roles: to extract the connected component of AA that contains a seed pixel, grow the seed by dilation but clip it to the foreground:

Xk=(Xk−1⊕B)∩A.X_k = (X_{k-1} \oplus B) \cap A.

With BB a 3×33 \times 3 square the result is the seed’s 8-connected component; with a cross it is the 4-connected component. Repeating this for every unlabelled seed labels all components. Real libraries use faster one- or two-pass labelling algorithms, but the result is the same: scipy.ndimage.label(A, structure=np.ones((3, 3))) returns a label image and the number of components.

Convex hull

A set is convex if the straight segment between any two of its points stays inside it. The convex hull C(A)C(A) is the smallest convex set containing AA. A purely morphological approximation uses four HMT templates BiB^i, i=1,…,4i = 1, \ldots, 4, each one detecting background pixels that have foreground on one side (left, top, right, bottom). Starting from X0i=AX_0^i = A, iterate

Xki=(Xk−1i⊛Bi)∪Xk−1iX_k^i = (X_{k-1}^i \circledast B^i) \cup X_{k-1}^i

until convergence, giving DiD^i, and take C(A)=⋃i=14DiC(A) = \bigcup_{i=1}^4 D^i. Each template keeps adding pixels in one direction until no concavity facing that direction remains. The result is the convex hull with respect to those four directions; it can grow beyond the true Euclidean hull, so implementations often limit growth to the bounding box. In practice skimage.morphology.convex_hull_image computes the polygonal hull of the foreground pixel coordinates and rasterises it, which is faster and closer to the Euclidean hull.

Thinning

Thinning removes boundary pixels selectively, without breaking the shape apart, until only a one-pixel-wide line remains. One thinning step with template BB is

A⊗B=A−(A⊛B),A \otimes B = A - (A \circledast B),

which deletes exactly the pixels matched by the HMT. A full thinning cycles through a sequence of templates {B1,B2,…,Bn}\{B^1, B^2, \ldots, B^n\}, usually 8 rotations of an “edge pixel” pattern,

A⊗{B}=((⋯((A⊗B1)⊗B2)⋯ )⊗Bn),A \otimes \{B\} = (( \cdots ((A \otimes B^1) \otimes B^2) \cdots ) \otimes B^n),

and repeats the whole cycle until no pixel changes. The templates are designed so that deleting a matched pixel never disconnects the shape and never removes the endpoint of a line. Zhang and Suen’s parallel thinning algorithm is a widely used, efficient version of this idea [6]; skimage.morphology.skeletonize uses it by default for 2-D images, and skimage.morphology.thin implements a related parallel thinning.

Thickening

Thickening is the dual of thinning: it adds background pixels matched by a template,

A⊙B=A∪(A⊛B).A \odot B = A \cup (A \circledast B).

Rather than designing separate thickening templates, the usual practical route is to thin the background and complement the result. Carried to convergence, thinning the background leaves a thin network of background lines running midway between objects, so thickening is a classic way to grow objects until they meet without ever merging.

Skeletons

The skeleton S(A)S(A) is a thin set of lines running down the middle of a shape. One formal definition uses maximal disks: a point is on the skeleton if it is the centre of a disk that fits inside AA and is not contained in any other fitting disk. Such disks touch the boundary in at least two places. The skeleton has a purely morphological formula:

S(A)=⋃k=0KSk(A),Sk(A)=(A⊖kB)−((A⊖kB)∘B),S(A) = \bigcup_{k=0}^{K} S_k(A), \qquad S_k(A) = (A \ominus kB) - \big( (A \ominus kB) \circ B \big),

where A⊖kBA \ominus kB means kk successive erosions of AA by BB and KK is the last kk before the erosion becomes empty. Each Sk(A)S_k(A) holds the points that are “ridge” points at erosion depth kk: they are present after kk erosions but would not survive an opening at that depth. The decomposition is invertible:

A=⋃k=0K(Sk(A)⊕kB),A = \bigcup_{k=0}^{K} \big( S_k(A) \oplus kB \big),

so storing each SkS_k with its index kk is a lossless shape code. The catch is that this skeleton is not guaranteed to be connected. Thinning gives connected, one-pixel-wide skeletons, and the medial axis (computed from a distance transform) attaches the radius of the maximal disk to each skeleton point, as in the right panel of Figure 9.3.

Four panels: the boundary of the horse, its convex hull in light blue around the dark horse, the thinning skeleton in red over the horse, and the medial axis coloured by distance to the boundary.
Figure 9.3 — Algorithms built from erosion and the HMT: inner boundary, convex hull, thinning skeleton, and medial axis whose colour encodes the local half-width.

Pruning

Skeletons are sensitive to boundary noise: a single bump on the outline sprouts a short spur (parasitic branch), visible at the horse’s tail in Figure 9.3. Pruning removes spurs shorter than some length LL:

  1. Thin the skeleton by removing end points (pixels with exactly one 8-neighbour) LL times. Every spur of length at most LL disappears, but every genuine branch also loses LL pixels at its tip.
  2. Find the end points of the shortened skeleton.
  3. Grow those end points back by LL conditional dilations, constrained to the original skeleton, to restore the main branches to full length.
  4. Take the union of the shortened skeleton and the regrown tips.
import numpy as np
from scipy import ndimage as ndi
from skimage import data, morphology

def endpoints(S):
    n = ndi.convolve(S.astype(int), np.ones((3, 3), int), mode="constant") - S
    return S & (n == 1)            # foreground pixels with exactly one neighbour

def prune_tips(S, L):
    """Step 1 of pruning: strip L layers of end points."""
    S = S.copy()
    for _ in range(L):
        S &= ~endpoints(S)
    return S

skel = morphology.skeletonize(~data.horse())
print(skel.sum(), prune_tips(skel, 10).sum())

Morphological reconstruction

In plain words: reconstruction lets you say “keep every object that contains this seed” and get the object back exactly, not a rounded copy of it. It uses two images: a marker, which says where to start, and a mask, which says how far you may grow.

Geodesic dilation and erosion

Let FF be the marker and GG the mask, with F⊆GF \subseteq G. The geodesic dilation of size 1 of FF with respect to GG is

DG(1)(F)=(F⊕B)∩G,D_G^{(1)}(F) = (F \oplus B) \cap G,

where BB is a small SE (usually the 3×33 \times 3 square). The marker grows by one step but may not leave the mask. Size nn means nn repetitions: DG(n)(F)=DG(1)(DG(n−1)(F))D_G^{(n)}(F) = D_G^{(1)}\big(D_G^{(n-1)}(F)\big). The word geodesic refers to distances measured along paths that stay inside GG, like walking inside a building rather than through its walls.

The dual geodesic erosion of size 1 is

EG(1)(F)=(F⊖B)∪G,E_G^{(1)}(F) = (F \ominus B) \cup G,

with F⊇GF \supseteq G: the marker shrinks, but never below the mask.

Reconstruction by dilation and by erosion

Iterate geodesic dilation until nothing changes. The result is the reconstruction by dilation of mask GG from marker FF:

RGD(F)=DG(k)(F)with k such that DG(k)(F)=DG(k+1)(F).R_G^D(F) = D_G^{(k)}(F) \quad \text{with } k \text{ such that } D_G^{(k)}(F) = D_G^{(k+1)}(F).

In binary images, RGD(F)R_G^D(F) is exactly the union of the connected components of GG that contain at least one marker pixel. Likewise, the reconstruction by erosion RGE(F)R_G^E(F) iterates geodesic erosion to stability. Vincent’s paper gives the standard efficient algorithms (queue-based, visiting each pixel a bounded number of times rather than iterating full-image passes) and many applications [7]; skimage.morphology.reconstruction follows that approach.

Opening by reconstruction

An ordinary opening removes small objects but also rounds the corners and erases the thin parts of the large ones. Opening by reconstruction avoids that:

OR(n)(F)=RFD(F⊖nB),O_R^{(n)}(F) = R_F^D\big(F \ominus nB\big),

where FF is the input image, F⊖nBF \ominus nB is nn erosions by BB (the marker), and the input itself is the mask. Erosion decides which objects survive (those that contain at least one placement of the SE); reconstruction restores each survivor to its original shape. Compare the “ordinary opening” and “opening by reconstruction” panels of Figure 9.4: the thin arm on the square and the sharp corners return intact. Closing by reconstruction is the dual, built from dilation and reconstruction by erosion.

Automatic hole filling

Reconstruction also fills all holes at once without seeds. Holes are background regions that cannot be reached from the image frame. So build a marker that is the complement of the image on the frame and zero elsewhere,

F(x,y)={1−I(x,y)if (x,y) is on the image border,0otherwise,F(x, y) = \begin{cases} 1 - I(x, y) & \text{if } (x, y) \text{ is on the image border,} \\ 0 & \text{otherwise,} \end{cases}

reconstruct the background from it, and complement:

H=[RIcD(F)]c.H = \big[ R_{I^c}^D(F) \big]^c .

RIcD(F)R_{I^c}^D(F) is the background reachable from the border; everything else, HH, is the foreground plus its holes.

Border clearing

Objects that touch the image border are often incomplete and should be excluded from measurements. Using the image itself on the border as the marker,

F(x,y)={I(x,y)if (x,y) is on the border,0otherwise,X=I−RID(F),F(x, y) = \begin{cases} I(x, y) & \text{if } (x, y) \text{ is on the border,} \\ 0 & \text{otherwise,} \end{cases} \qquad X = I - R_I^D(F),

the reconstruction recovers every object that touches the border, and subtracting it leaves only interior objects.

Six panels on a synthetic binary scene: input; eroded marker; ordinary opening that rounds corners and removes the thin arm; opening by reconstruction that restores large shapes exactly; holes filled in red; border-touching objects removed.
Figure 9.4 — Reconstruction. The eroded marker (disk of radius 6) picks the large objects; reconstruction restores them exactly, including the thin arm. Bottom: automatic hole filling and border clearing.
import numpy as np
from skimage import morphology, segmentation

def open_by_reconstruction(A, footprint):
    marker = morphology.binary_erosion(A, footprint)
    return morphology.reconstruction(marker.astype(np.uint8), A.astype(np.uint8),
                                     method="dilation").astype(bool)

def fill_holes_auto(A):
    """Background reachable from the image frame is background; the rest is hole."""
    Ac = ~A
    marker = np.zeros_like(Ac)
    marker[0, :], marker[-1, :] = Ac[0, :], Ac[-1, :]
    marker[:, 0], marker[:, -1] = Ac[:, 0], Ac[:, -1]
    reached = morphology.reconstruction(marker.astype(np.uint8), Ac.astype(np.uint8),
                                        method="dilation").astype(bool)
    return ~reached

# Library shortcuts: scipy.ndimage.binary_fill_holes(A), segmentation.clear_border(A)

Summary of binary operations

The table collects the binary operations of this chapter. Throughout, AA is the input set, BB the structuring element, FF a marker, GG a mask, and BB is assumed to contain its origin.

OperationFormulaWhat it does
Translation(B)z={b+z}(B)_z = \{ b + z \}Moves the SE’s origin to zz
ReflectionB^={−b}\hat{B} = \{ -b \}Rotates the SE by 180°
ComplementAcA^cSwaps foreground and background
ErosionA⊖BA \ominus BKeeps positions where BB fits; shrinks, removes thin parts
DilationA⊕BA \oplus BKeeps positions where B^\hat{B} touches AA; grows, bridges gaps
Opening(A⊖B)⊕B(A \ominus B) \oplus BRemoves specks, thin necks, sharp corners; idempotent
Closing(A⊕B)⊖B(A \oplus B) \ominus BFills small holes and narrow gaps; idempotent
Hit-or-miss(A⊖B1)∩(Ac⊖B2)(A \ominus B_1) \cap (A^c \ominus B_2)Finds an exact foreground/background pattern
BoundaryA−(A⊖B)A - (A \ominus B)One-pixel inner outline
Hole fillingXk=(Xk−1⊕B)∩AcX_k = (X_{k-1} \oplus B) \cap A^cFills the hole that contains the seed
Connected componentXk=(Xk−1⊕B)∩AX_k = (X_{k-1} \oplus B) \cap AExtracts the component that contains the seed
Convex hullunion of iterated HMTsFills concavities
ThinningA−(A⊛B)A - (A \circledast B), over a template sequencePeels pixels to a 1-pixel skeleton, keeps connectivity
ThickeningA∪(A⊛B)A \cup (A \circledast B)Dual of thinning
Skeleton⋃k(A⊖kB)−(A⊖kB)∘B\bigcup_k (A \ominus kB) - (A \ominus kB) \circ BMedial lines; invertible with the index kk
Pruningend-point removal, then conditional regrowthRemoves short spurs
Geodesic dilation(F⊕B)∩G(F \oplus B) \cap GOne step of growth inside a mask
Geodesic erosion(F⊖B)∪G(F \ominus B) \cup GOne step of shrinking above a mask
Reconstruction by dilationDG(k)(F)D_G^{(k)}(F) at stabilityComponents of GG hit by FF
Opening by reconstructionRFD(F⊖nB)R_F^D(F \ominus nB)Removes small objects, keeps others exact
Hole filling (auto)[RIcD(F)]c[R_{I^c}^D(F)]^cFills all holes, no seeds
Border clearingI−RID(F)I - R_I^D(F)Removes border-touching objects

Grayscale morphology

In plain words: for gray images, “fits inside” becomes “local minimum” and “touches” becomes “local maximum”. Picture the image as a landscape where brightness is height. Erosion lowers each pixel to the lowest ground under the stencil; dilation raises it to the highest.

Grayscale erosion and dilation

With a flat SE bb (a set of offsets, all of height zero), grayscale erosion and dilation of an image ff are

[f⊖b](x,y)=min⁡(s,t)∈bf(x+s, y+t),[f⊕b](x,y)=max⁡(s,t)∈bf(x−s, y−t),[f \ominus b](x, y) = \min_{(s, t) \in b} f(x + s,\ y + t), \qquad [f \oplus b](x, y) = \max_{(s, t) \in b} f(x - s,\ y - t),

where (s,t)(s, t) runs over the SE’s offsets. The minus sign in dilation is the reflection again. On a binary image these reduce exactly to the set definitions. Erosion darkens the image, shrinks bright features and widens dark ones; dilation does the opposite.

A nonflat SE bNb_N also has heights bN(s,t)b_N(s, t):

[f⊖bN](x,y)=min⁡(s,t)∈bN{f(x+s, y+t)−bN(s,t)},[f⊕bN](x,y)=max⁡(s,t)∈bN{f(x−s, y−t)+bN(s,t)}.[f \ominus b_N](x, y) = \min_{(s, t) \in b_N} \big\{ f(x + s,\ y + t) - b_N(s, t) \big\}, \qquad [f \oplus b_N](x, y) = \max_{(s, t) \in b_N} \big\{ f(x - s,\ y - t) + b_N(s, t) \big\}.

Nonflat SEs are rarely used in practice: the result depends on the image’s intensity scale, and erosion can go below the image’s minimum. Duality carries over with the gray complement fc=−ff^c = -f (or L−1−fL - 1 - f for an LL-level image): (f⊖b)c=fc⊕b^(f \ominus b)^c = f^c \oplus \hat{b}.

Opening and closing

The definitions are unchanged: f∘b=(f⊖b)⊕bf \circ b = (f \ominus b) \oplus b and f∙b=(f⊕b)⊖bf \bullet b = (f \oplus b) \ominus b. The geometric picture is a ball pushed up from beneath the intensity surface: the opening is the highest surface the ball can reach while staying below ff. Bright peaks narrower than the ball are cut down; everything wider is left alone. Closing pushes the ball down from above: dark valleys narrower than the ball are filled. The properties (anti-extensive/extensive, increasing, idempotent) carry over with ≤\le in place of ⊆\subseteq.

Morphological smoothing

Because opening suppresses small bright details and closing suppresses small dark details, the sequence opening followed by closing with the same SE removes both kinds of small structure, while keeping sharp edges of larger regions. This is the grayscale version of the noise filter in Figure 9.1. A related scheme, alternating sequential filtering, applies opening–closing pairs with SEs of increasing size, which avoids the artefacts of using one large SE at once.

Morphological gradient

Dilation minus erosion gives a measure of local contrast:

g=(f⊕b)−(f⊖b).g = (f \oplus b) - (f \ominus b).

Inside uniform regions the max and min are nearly equal, so g≈0g \approx 0; across an edge they differ by the edge height. Unlike a Sobel derivative, gg is non-negative and direction-independent (for a symmetric bb). Using only one side gives the internal gradient f−(f⊖b)f - (f \ominus b) or the external gradient (f⊕b)−f(f \oplus b) - f, which place the edge response just inside or just outside the brighter region.

Six panels of the cameraman image: input, grayscale erosion, dilation, opening-then-closing smoothing, morphological gradient, and internal gradient.
Figure 9.5 — Grayscale morphology with a flat disk of radius 3: erosion (local min), dilation (local max), smoothing by opening then closing, the morphological gradient, and the internal gradient.
from skimage import data, morphology, util

f = util.img_as_float(data.camera())
b = morphology.disk(3)                       # flat structuring element
ero  = morphology.erosion(f, b)              # local minimum
dil  = morphology.dilation(f, b)             # local maximum
grad = dil - ero                             # morphological gradient (>= 0)
smooth = morphology.closing(morphology.opening(f, b), b)

Top-hat and bottom-hat transforms

Opening with an SE larger than the bright objects removes those objects but keeps the slowly varying background. Subtracting the opening from the image therefore leaves only the objects. This is the top-hat (or white top-hat) transform:

That(f)=f−(f∘b).T_{\text{hat}}(f) = f - (f \circ b).

Its dual, the bottom-hat (or black top-hat) transform, extracts dark objects on a lighter background:

Bhat(f)=(f∙b)−f.B_{\text{hat}}(f) = (f \bullet b) - f.

Both are non-negative. The main use is correcting uneven illumination before thresholding. In Figure 9.6, a synthetic image of grains lit from one side defeats a global Otsu threshold (Chapter 10): the bright side of the background is brighter than the grains on the dark side. After a top-hat with a disk of radius 8 (larger than any grain), the background is flat and a single threshold separates the grains almost perfectly. The SE size is the only parameter: it must be larger than the objects and smaller than the scale over which the background changes.

Six panels: grains on a background that brightens to the right; the opening, which estimates the background; the top-hat result with flat background; Otsu thresholding of the input (fails on the right side); Otsu thresholding of the top-hat (clean); and a row profile of input, opening and top-hat.
Figure 9.6 — Top-hat correction of uneven illumination. The opening with a disk larger than the grains is a background estimate; subtracting it flattens the background. IoU is measured against the known grain mask.

Drag the slider to compare the input with its top-hat:

Grains under uneven illumination, before and after the white top-hat transform
InputTop-hat (disk r=8)
from skimage import data, filters, morphology, util

coins = util.img_as_float(data.coins())
th = morphology.white_tophat(coins, morphology.disk(30))   # bright objects
bh = morphology.black_tophat(coins, morphology.disk(30))   # dark objects
mask = th > filters.threshold_otsu(th)

Granulometry

Granulometry measures the size distribution of particles in an image without segmenting them. Open the image with SEs of increasing size rr; each opening removes the bright particles smaller than brb_r. Record the total intensity that remains,

V(r)=∑x,y[f∘br](x,y),r=0,1,2,…,V(r) = \sum_{x, y} [f \circ b_r](x, y), \qquad r = 0, 1, 2, \ldots,

where brb_r is a disk of radius rr and b0b_0 is a single pixel (so V(0)V(0) is the sum of ff). V(r)V(r) never increases, because larger disks fit in fewer places. Its negative discrete derivative,

PS(r)=V(r−1)−V(r),PS(r) = V(r - 1) - V(r),

is a size histogram called the pattern spectrum [8]: a large value at rr means a lot of image “mass” lives in features of radius about rr. Figure 9.7 shows a scene with two particle sizes and the two corresponding peaks. The idea of a family of openings indexed by size, with the “sieve” interpretation, goes back to Matheron [2]; Maragos developed the pattern spectrum as a multiscale shape descriptor [8].

Left: dark image with small and large bright discs. Middle: normalized sum of opened image versus radius, dropping in two steps. Right: bar chart with peaks at radius 5 and radius 12.
Figure 9.7 — Granulometry of a scene with discs of radius 5 and 12. The granulometric curve drops twice, and the pattern spectrum peaks at the two radii.
import numpy as np
from skimage import morphology

def granulometry(f, radii):
    total = f.sum()
    vol = np.array([morphology.opening(f, morphology.disk(r)).sum() / total
                    for r in radii])
    spectrum = -np.diff(vol)                 # loss from radius r-1 to r
    return vol, spectrum

Textural segmentation

Morphology can also separate regions that differ in the size of their texture elements, for example a field of small blobs next to a field of large blobs. The recipe uses the same size logic as granulometry:

  1. Close the image with an SE larger than the gaps between the small blobs (for bright blobs on a dark background, closing fills those dark gaps). The small-blob region turns into a nearly uniform bright patch, while the large blobs, whose gaps are wider than the SE, stay separate.
  2. Open the result with an SE larger than the large blobs but smaller than the merged patch. This erases the isolated large blobs and leaves the merged small-blob patch.
  3. Take the morphological gradient (or a threshold) of the result. The boundary between the two textures appears as a single strong edge.

The exact SE sizes depend on the blob and gap sizes in your data, which you can measure with a granulometry first.

Grayscale reconstruction

All reconstruction definitions carry over, with ∩\cap replaced by the point-wise minimum ∧\wedge and ∪\cup by the maximum ∨\vee. Geodesic dilation of a marker image ff under a mask image gg (with f≤gf \le g) is

Dg(1)(f)=(f⊕b)∧g,D_g^{(1)}(f) = (f \oplus b) \wedge g,

and reconstruction by dilation iterates this to stability. Grayscale reconstruction powers several useful filters [7]:

  • Opening by reconstruction, RfD(f⊖nb)R_f^D(f \ominus nb): removes bright details smaller than the SE but restores the exact shape of everything else. A top-hat by reconstruction, f−RfD(f⊖nb)f - R_f^D(f \ominus nb), is cleaner than the ordinary top-hat because the background estimate does not have rounded “shoulders”.
  • Regional maxima and the h-dome transform: reconstruct ff from the marker f−hf - h. The reconstruction flattens every peak of height at most hh; subtracting it from ff keeps only the “domes” (peaks), each at most hh tall. Thresholding the domes detects bright blobs regardless of the local background level.
from skimage import data, morphology, util
import numpy as np

f = util.img_as_float(data.camera())
h = 0.1
background = morphology.reconstruction(np.clip(f - h, 0, None), f, method="dilation")
domes = f - background                     # bright peaks, each at most h tall

Modern view

Morphology is old, but it is far from finished. Five lines of work are worth knowing.

Foundations and tutorials. Matheron’s work on random sets [2] and Serra’s Image Analysis and Mathematical Morphology [3] set up the algebra: erosion and dilation as Minkowski operations, openings as “sieves”, and the lattice-theoretic view that later extended everything from sets to gray-level functions. Haralick, Sternberg and Zhuang’s 1987 tutorial [4] is still one of the clearest introductions to binary and grayscale morphology for engineers; it defines the operators, proves their basic properties, and connects them to practical algorithms. Soille’s Morphological Image Analysis [5] is the practitioner’s reference: it covers geodesic transforms, reconstruction, watersheds, granulometries, and efficient implementations, and it is the place to look when a library’s documentation is too terse.

Reconstruction and efficient algorithms. Vincent’s 1993 paper [7] made reconstruction practical. Its main takeaway is that the naive “iterate until stable” loop can be replaced by queue-based algorithms whose cost grows roughly linearly with the image size, which turned reconstruction into an everyday tool for hole filling, regional extrema, and h-domes.

Connected operators and component trees. A filter is connected if it can only remove or merge flat zones (connected regions of constant value) and never creates new contours. Openings by reconstruction are the simplest example. Salembier and Wilkinson’s review [10] explains the family: area openings, attribute filters, levelings, and tree-based filtering. Breen and Jones introduced attribute openings and thinnings [9], which keep or remove a connected component based on an attribute (area, elongation, moment of inertia) rather than whether an SE fits. Efficient implementations represent an image as a max-tree or min-tree (a tree of the connected components of all threshold sets) and filter by pruning the tree. Carlinet and Géraud compare many max-tree construction algorithms in a single framework and give a decision tree for choosing one [11]. scikit-image exposes these as area_opening, area_closing, diameter_opening and max_tree.

Size distributions. Maragos’s pattern spectrum [8] formalised granulometry as a multiscale shape descriptor; it is still used for texture and particle-size analysis, and attribute-based granulometries [9] extend it to shape attributes other than size.

Morphology meets deep learning. Deep learning has changed morphology in two directions.

Morphology inside networks. Max pooling is already a flat grayscale dilation followed by subsampling, so the connection is natural. Several groups have built layers whose structuring elements are learned by gradient descent. Mondal et al. study networks built from learnable dilation and erosion neurons and show that two blocks of dilations and erosions, each followed by a linear combination, can approximate any continuous function [13]. Nogueira et al. propose DeepMorphNet, which replaces convolutions with learnable morphological operations and compares the features it learns with standard convolutional networks [14]. A practical difficulty is that min and max have sparse gradients. Kirszenberg et al. [16] and Hermary et al. [15] build smooth morphological layers from differentiable approximations of max and min (generalised means and soft-max functions whose sharpness is set by a learnable parameter), improving on earlier p-convolution layers, and study when such layers actually recover erosions and dilations. These remain research tools: morphological layers are not a standard component of mainstream vision backbones.

Morphology around networks. In segmentation, morphology is used constantly as post-processing of predicted masks: opening to remove false-positive specks, closing or hole filling to repair fragmented objects, remove_small_objects, border clearing, and skeletons for measuring tubular structures. It also appears inside loss functions. clDice [17] computes a differentiable “soft skeleton” by iterating min- and max-pooling (soft erosion and dilation), and uses overlap between the predicted and true skeletons as a loss that encourages topologically correct segmentation of vessels, roads and neurons.

What has not changed: for cleaning, measuring and correcting binary or grayscale images, the classical operators of this chapter are still the fastest, most predictable tools, and they come with guarantees (idempotence, connectivity, exact shape preservation) that learned filters do not offer.

Key takeaways

  • Morphology works with shape, not frequency. It uses only min, max, union and intersection, controlled by a structuring element whose size and shape encode the question you ask.
  • Erosion keeps positions where the SE fits; dilation keeps positions where the reflected SE touches. They are duals: eroding the foreground equals dilating the background.
  • Opening (erode then dilate) removes small bright parts; closing (dilate then erode) fills small dark gaps. Both are idempotent filters, and opening followed by closing is a robust noise cleaner.
  • The hit-or-miss transform matches an exact foreground/background pattern and underlies thinning, thickening, and convex hulls.
  • Reconstruction (marker plus mask, grow until stable) keeps whole objects exactly: opening by reconstruction, automatic hole filling and border clearing all follow from it.
  • In grayscale, erosion and dilation are local min and max. The gradient, top-hat and bottom-hat give edges, background correction, and small-object extraction; granulometry gives size distributions without segmentation.
  • Today morphology lives on as post-processing for neural-network masks, as differentiable building blocks in losses such as clDice, and as a research topic in learnable morphological layers.

Exercises

  1. A binary image contains a rectangle 40 pixels wide and 10 pixels tall. Describe its erosion and its opening by (a) a 5×55 \times 5 square, (b) a horizontal line 15 pixels long, (c) a vertical line 15 pixels long. Which SE removes the rectangle entirely, and why?
Hint

Erosion keeps the positions of the SE’s origin where the whole SE fits. The 5×55 \times 5 square fits at a 36×636 \times 6 block of positions, and the opening restores the full rectangle, because the rectangle is a union of fitting squares. The horizontal line also fits, so its opening is again the full rectangle. The vertical line is taller than the rectangle and never fits: both its erosion and its opening are empty.

  1. Show that dilation is commutative and associative for sets: A⊕B=B⊕AA \oplus B = B \oplus A and (A⊕B)⊕C=A⊕(B⊕C)(A \oplus B) \oplus C = A \oplus (B \oplus C). Use the second identity to explain why dilating twice with a 3×33 \times 3 square equals dilating once with a 5×55 \times 5 square. Is the same true for erosion?
Hint

Write dilation as the Minkowski sum {a+b∣a∈A,b∈B}\{ a + b \mid a \in A, b \in B \}; both identities follow from the commutativity and associativity of vector addition. A 3×33 \times 3 square plus itself is a 5×55 \times 5 square. For erosion, use (A⊖B)⊖C=A⊖(B⊕C)(A \ominus B) \ominus C = A \ominus (B \oplus C), which follows from duality.

  1. Design the hit-or-miss template pair (B1,B2)(B_1, B_2) that detects the end points of a one-pixel-wide 8-connected line pointing to the right (the line arrives from the left). How many rotated templates do you need to catch all end points? Test your answer with scipy.ndimage.binary_hit_or_miss on skimage.morphology.skeletonize(~skimage.data.horse()).
Hint

Centre in B1B_1 plus the left neighbour in B1B_1; the other seven neighbours in B2B_2. A line can also arrive diagonally, so you need 8 templates (4 axis directions and 4 diagonals). Compare your total count with the count from a neighbour-counting function like endpoints() in the pruning snippet.

  1. Your segmentation network outputs masks of cells. Many masks have small holes, a few single-pixel false positives, and some cells are cut by the image border. Write a five-line post-processing pipeline in scikit-image and justify the order of the steps.
Hint

One reasonable order: remove_small_objects (or a small opening) first, so that specks do not get filled or merged; then binary_fill_holes; then a small closing if contours are ragged; then clear_border; finally label to count. Discuss what would go wrong if you closed before removing specks (nearby specks can merge into a “cell”).

  1. For a grayscale image ff and a flat SE bb, prove that the top-hat T(f)=f−(f∘b)T(f) = f - (f \circ b) is non-negative and that it is idempotent: T(T(f))=T(f)T(T(f)) = T(f).
Hint

Non-negativity follows from f∘b≤ff \circ b \le f. For idempotence, show that T(f)∘b=0T(f) \circ b = 0. Take any translate WW of bb and let yy be the point of WW where ff is smallest. Since the opening is at least min⁡Wf\min_W f everywhere on WW and at most ff, it equals f(y)f(y) at yy, so T(f)(y)=0T(f)(y) = 0. Every translate of bb therefore contains a zero of T(f)T(f), the erosion of T(f)T(f) by bb is zero, and so is its opening.

References

  1. R. C. Gonzalez and R. E. Woods, Digital Image Processing, 4th ed., Pearson, 2018, Ch. 9. publisher page
  2. G. Matheron, Random Sets and Integral Geometry, Wiley, 1975. library record
  3. J. Serra, Image Analysis and Mathematical Morphology, Academic Press, 1982. library record
  4. R. M. Haralick, S. R. Sternberg, and X. Zhuang, “Image Analysis Using Mathematical Morphology,” IEEE Trans. Pattern Analysis and Machine Intelligence, vol. PAMI-9, pp. 532–550, 1987. doi
  5. P. Soille, Morphological Image Analysis, Springer, 2004. doi
  6. T. Y. Zhang and C. Y. Suen, “A Fast Parallel Algorithm for Thinning Digital Patterns,” Communications of the ACM, vol. 27, pp. 236–239, 1984. doi
  7. L. Vincent, “Morphological Grayscale Reconstruction in Image Analysis: Applications and Efficient Algorithms,” IEEE Trans. Image Processing, vol. 2, pp. 176–201, 1993. doi
  8. P. Maragos, “Pattern Spectrum and Multiscale Shape Representation,” IEEE Trans. Pattern Analysis and Machine Intelligence, vol. 11, no. 7, pp. 701–716, 1989. doi
  9. E. J. Breen and R. Jones, “Attribute Openings, Thinnings, and Granulometries,” Computer Vision and Image Understanding, vol. 64, no. 3, pp. 377–389, 1996. doi
  10. P. Salembier and M. H. F. Wilkinson, “Connected Operators: A Review of Region-Based Morphological Image Processing Techniques,” IEEE Signal Processing Magazine, vol. 26, no. 6, pp. 136–157, 2009. doi
  11. E. Carlinet and T. Géraud, “A Comparative Review of Component Tree Computation Algorithms,” IEEE Trans. Image Processing, vol. 23, pp. 3885–3895, 2014. doi
  12. S. van der Walt et al., “scikit-image: Image Processing in Python,” PeerJ, 2014. arXiv
  13. R. Mondal, S. Santra, S. S. Mukherjee, and B. Chanda, “Morphological Network: How Far Can We Go with Morphological Neurons?,” BMVC, 2022. arXiv
  14. K. Nogueira, J. Chanussot, M. Dalla Mura, and J. A. dos Santos, “An Introduction to Deep Morphological Networks,” arXiv:1906.01751, 2019. arXiv
  15. R. Hermary, G. Tochon, É. Puybareau, A. Kirszenberg, and J. Angulo, “Learning Grayscale Mathematical Morphology with Smooth Morphological Layers,” Journal of Mathematical Imaging and Vision, vol. 64, pp. 736–753, 2022. doi
  16. A. Kirszenberg, G. Tochon, É. Puybareau, and J. Angulo, “Going Beyond p-convolutions to Learn Grayscale Morphological Operators,” arXiv:2102.10038, 2021. arXiv
  17. S. Shit et al., “clDice — A Novel Topology-Preserving Loss Function for Tubular Structure Segmentation,” CVPR, 2021. arXiv