Shrink a shape, grow a shape, and combine the two to remove speckles or fill holes without moving the boundary.
Overview
Operating on shape, not brightness
each filter so far has treated the image as a field of numbers. Morphological ops treat it as a set of pixels. The foreground — and ask geometric questions about that set. They're defined for binary images, which is why the visualisation above thresholds first and why the binarise control sits alongside the others.
The tool is a structuring element: a small shape, often a square or a disc, that gets placed over each pixel in turn. What happens next depends on which of two rules you apply.
Erosion and Dilation
This module needs JavaScript: the
images are computed in the page rather than downloaded.
Worth knowing
Morphology operates on shape. It is defined for binary images, which is why the threshold control comes first.
Erosion keeps a pixel only if every pixel under the structuring element is foreground. Shapes shrink and thin features vanish.
Dilation keeps a pixel if any pixel under the element is foreground. Shapes grow and small gaps close.
Open removes small bright specks; close fills small dark holes. Both leave the overall size roughly unchanged.
Erosion and Dilation
The two ops that shape a binary mask. The four things you build from them.
Erosion: every, or nothing
Erosion keeps a foreground pixel only if each pixel under the structuring element is also foreground. One background pixel anywhere under the element and the centre is turned off.
The consequences follow right. Boundaries retreat inwards by roughly the radius of the element, because a pixel near an edge always has some background under a large enough window. Small isolated specks vanish fully, since a speck smaller than the element can never have the element fit inside it. Thin links between larger blobs are cut, because a bridge one pixel wide fails the test all over along its length.
Select Erode above and raise the structuring element size. The bright speckles disappear on the first step. The shapes visibly thin. Push it far enough and the thin bright bar at the bottom of the frame is gone fully.
Dilation: any, and it counts
Dilation is erosion's mirror. A pixel turns foreground if any pixel under the structuring element is foreground.
Boundaries advance outwards. Small holes inside an object fill, because a hole smaller than the element has foreground on all sides within reach. Nearby blobs merge as their expanding boundaries meet. The dark speckles that erosion could not touch disappear here instead.
Erosion and dilation are *duals*: eroding the foreground is just dilating the background. That isn't a coincidence to be stored, it's the same rule seen from the other side.
Open and close: the useful pair
Neither op is much use alone, because both change the size of all. Composing them fixes that.
Opening is erosion followed by dilation. The erosion drops small bright specks and thins all. The dilation grows what survived back to roughly its first size. Specks that were destroyed don't come back — there's nothing left to grow. The net effect is *drop small bright things, leave all else about where it was*.
Closing is dilation followed by erosion. The dilation fills small dark holes and joins nearby pieces. The erosion shrinks the result back down. Holes that were filled stay filled. The net effect is *drop small dark things, leave all else about where it was*.
Operation
Sequence
Removes
Boundary
Erode
—
bright specks
retreats
Dilate
—
dark holes
advances
Open
erode, dilate
bright specks
roughly unchanged
Close
dilate, erode
dark holes
roughly unchanged
Set the control above to Open and compare against Erode at the same size. The speckles are gone in both. But The shapes under Open are still the size they started.
Why this always follows thresholding
A thresholded mask is almost never clean. Noise that happened to cross the threshold leaves scattered foreground pixels in the background. A highlight inside an object leaves a hole. Both are small, and both are just what open and close are for.
The standard sequence — threshold, open to despeckle, close to fill — is so common it's worth treating as one step. Only after it does counting connected components or tracing contours give sensible answers.
Choosing the element
The size is the param that matters, and there's a principle for it: the structuring element should be larger than the artefacts you want to drop and smaller than the features you want to keep. If a speck is 3 pixels across and a genuine object is 30, any of it between will do. If they're 3 and 5, no size works and morphology is the wrong tool.
The shape matters less, but not never. A square element treats main-lines differently from axes. A disc is isotropic. A flat line erodes vertical strokes while leaving flat ones alone. This Is how table rules are separated from text in doc processing.
Open and close, and what each removes
Erosion keeps a foreground pixel only where *each* pixel under the structuring element is foreground. Dilation keeps it where *any* is. Alone, each changes the size of all — erosion retreats boundaries inward and deletes specks smaller than the element, dilation advances them out and fills holes — so they are composed in pairs that despeckle without resizing:
operation
sequence
removes
net effect on size
opening
erode then dilate
small bright specks
roughly unchanged
closing
dilate then erode
small dark holes
roughly unchanged
Opening erodes first, destroying specks too small to hold the element, then dilates what survived back to about its first size. And a speck that was destroyed can't grow back, so the net effect is "drop small bright things, leave the rest where it was". Closing is the mirror: fill small holes, then shrink back, so holes stay filled. This is why the standard pipeline after thresholding is open-then-close — despeckle the background, fill the object — before counting components or tracing contours. The one param that matters is the element size, and the principle is exact: it must be larger than the artefacts to drop and smaller than the features to keep, so if a speck is 3 pixels and a real object is 30, any of it between works, and if they're 3 and 5, morphology is the wrong tool.
Where it goes wrong with Erosion and Dilation
Applying it to a greyscale image without thinking. Greyscale morphology is defined — erosion turns a local min and dilation a local max — but it means a thing different, and results that look plausible may not be what was intended.
Using an element that is too large. It drops the noise and the detail together. Thin structures are lost first and can't be got back.
Opening when you meant closing. The mnemonic is that opening drops *bright* things and closing drops *dark* ones. If your foreground is dark against a light background, they swap.
Questions people ask about Erosion and Dilation
What does erosion do? A pixel stays on only if every pixel under the structuring element is on. Foreground shrinks, and thin lines and small specks disappear.
What does dilation do? A pixel turns on if any pixel under the element is on. Foreground grows and small holes and gaps close.
What is the difference between opening and closing? Opening is erosion then dilation and removes small bright specks. Closing is dilation then erosion and fills small dark holes.
Why not just erode and dilate repeatedly? Erosion followed by dilation does not simply undo itself. Opening restores the size of surviving shapes while the removed specks stay gone.
How do I choose the structuring element? Match its shape to the shapes you want to keep or remove, and make it a little bigger than the noise. A rectangle suits boxy objects, an ellipse or cross suits rounder ones.
Do these work on greyscale images? Yes. Erosion becomes a local minimum and dilation a local maximum, which darkens or brightens regions rather than flipping pixels.
Shrinking and growing shapes with min and max
Erosion takes the min over a zone, dilation the max. Two ops. Every other morphological tool is a composition of them -- which this builds one at a time, then measures on a shape designed to have a thing for each of them to catch.
example_01.pyNumPy
import numpy as np
W, H = 26, 11
img = np.zeros((H, W), int)
img[2:8, 2:9] = 1; img[4:6, 5:9] = 0; img[4:6, 9:14] = 1
img[2:8, 14:21] = 1; img[5, 17] = 0
for r, c in ((0, 23), (9, 3), (1, 12), (6, 22)):
img[r, c] = 1
def show(a, label):
print(" %s" % label)
for row in a:
print(" " + "".join("#" if v else "." for v in row))
def erode(a, k=3):
p = k // 2
q = np.pad(a, p, mode="constant", constant_values=1)
return np.array([[q[i:i+k, j:j+k].min()
for j in range(a.shape[1])] for i in range(a.shape[0])])
def dilate(a, k=3):
p = k // 2
q = np.pad(a, p, mode="constant", constant_values=0)
return np.array([[q[i:i+k, j:j+k].max()
for j in range(a.shape[1])] for i in range(a.shape[0])])
show(img, "input: two blocks joined by a 2-pixel-tall bridge")
print(" block one has a notch, block two has a 1-pixel hole,")
print(" and there are 4 isolated specks. %d foreground pixels." % img.sum())
print()
e = erode(img)
show(e, "EROSION -- min of each 3x3 neighbourhood")
print(" %d -> %d pixels (%.0f%% removed)"
% (img.sum(), e.sum(), 100 * (1 - e.sum() / img.sum())))
print(" every speck and the bridge are gone.")
print()
d = dilate(img)
show(d, "DILATION -- max of each 3x3 neighbourhood")
print(" %d -> %d pixels (%.0f%% added)"
% (img.sum(), d.sum(), 100 * (d.sum() / img.sum() - 1)))
print(" the specks became 3x3 blobs, the hole and notch are filled.")
Output
The next block shows opening and closing -- compositions of erosion and dilation -- and measures what each one preserves and destroys.
example_02.pyNumPy
import numpy as np
W, H = 26, 11
img = np.zeros((H, W), int)
img[2:8, 2:9] = 1; img[4:6, 5:9] = 0; img[4:6, 9:14] = 1
img[2:8, 14:21] = 1; img[5, 17] = 0
for r, c in ((0,23),(9,3),(1,12),(6,22)):
img[r, c] = 1
SPECKS = [(0,23),(9,3),(1,12),(6,22)]; HOLE = (5, 17)
def show(a, label):
print(" %s" % label)
for row in a:
print(" " + "".join("#" if v else "." for v in row))
def erode(a, k=3):
p = np.pad(a, k//2, mode="constant", constant_values=1)
return np.array([[p[i:i+k, j:j+k].min()
for j in range(a.shape[1])] for i in range(a.shape[0])])
def dilate(a, k=3):
p = np.pad(a, k//2, mode="constant", constant_values=0)
return np.array([[p[i:i+k, j:j+k].max()
for j in range(a.shape[1])] for i in range(a.shape[0])])
e, d = erode(img), dilate(img)
opened, closed = dilate(erode(img)), erode(dilate(img))
show(opened, "OPENING (erode, then dilate)")
print(" %d pixels, against %d in the input" % (opened.sum(), img.sum()))
print(" specks are gone for good.")
print()
show(closed, "CLOSING (dilate, then erode)")
print(" %d pixels. hole and notch are filled and stay filled." % closed.sum())
print()
print("MEASURED on the specks and the hole:")
print("%-12s %18s %16s" % ("", "specks remaining", "hole filled?"))
for name, out in (("input", img), ("erosion", e), ("dilation", d),
("opening", opened), ("closing", closed)):
n = sum(int(out[r, c]) for r, c in SPECKS)
print("%-12s %18d %16s" % (name, n, "yes" if out[HOLE] else "no"))
print(" opening is the speck remover, closing is the hole filler.")
Output
The next block shows how the structuring element shape changes what erosion keeps, and why a horizontal element preserves the bridge that a square one destroyed.
example_03.pyNumPy
import numpy as np
W, H = 26, 11
img = np.zeros((H, W), int)
img[2:8, 2:9] = 1; img[4:6, 5:9] = 0; img[4:6, 9:14] = 1
img[2:8, 14:21] = 1; img[5, 17] = 0
for r, c in ((0, 23), (9, 3), (1, 12), (6, 22)):
img[r, c] = 1
def show(a, label):
print(" %s" % label)
for row in a:
print(" " + "".join("#" if v else "." for v in row))
print("%-10s %-44s %s" % ("operation", "what it removes", "shape size"))
for r in (("erosion", "specks, thin parts, 1 pixel of every edge", "smaller"),
("dilation", "holes, gaps, notches", "larger"),
("opening", "specks and thin protrusions", "preserved"),
("closing", "holes and thin gaps", "preserved")):
print("%-10s %-44s %s" % r)
print()
print("THE STRUCTURING ELEMENT does not have to be a square:")
print(" 3x3 square 3x3 cross 1x5 horizontal")
for r in range(3):
cross = "# # #" if r == 1 else ". # ."
horiz = "# # # # #" if r == 1 else ""
print(" %-16s%-16s%s" % ("# # #", cross, horiz))
print()
wide = np.zeros_like(img)
padded = np.pad(img, ((0, 0), (2, 2)), mode="constant", constant_values=1)
for i in range(H):
for j in range(W):
wide[i, j] = padded[i, j:j + 5].min()
show(wide, "eroded with a 1x5 horizontal element")
print(" %d pixels survive, and the bridge is among them." % wide.sum())
print(" a 1x5 horizontal window fits inside the bridge where a 3x3")
print(" square could not. the element, not the operation, is where you")
print(" encode what you are looking for.")
Output
Check yourself
0 of 3
Answer without scrolling back up.
Under erosion, when does a foreground pixel survive?
Erosion is the 'every' rule. One background pixel anywhere under the element turns the centre off, which is why boundaries retreat and thin features are cut.
Why does opening remove small specks without shrinking the larger shapes?
Dilation can only grow what is still there. Specks removed by the erosion have nothing left to grow, while larger shapes are restored to roughly their original size.
How should the structuring element be sized?
That gap is what the operation exploits. If the artefacts and the features are close in size, no element separates them and morphology is the wrong tool.
Cheat sheet
Erosion and Dilation
Apply erosion and dilation to a binary mask, then combine them into opening, closing, gradient and top-hat to clean segmentation output.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.