How can a computer reduce a thick binary shape to a one-pixel-wide skeleton without accidentally breaking the shape apart? Zhang–Suen thinning answers by deleting boundary pixels only when a local neighbourhood test says the deletion should preserve the essential connectivity of the foreground. The algorithm repeats two carefully different deletion passes until no more pixels can be removed.
This article teaches Zhang–Suen thinning as a Learning Hall progression from eight-neighbour pixel geometry to professional image-processing validation. It complements broader image-segmentation and computer-vision material by owning a narrower job: understand how iterative local deletion rules can produce a topology-preserving skeleton from a binary 2D foreground.
Quick Read
- Zhang–Suen thinning operates on a binary 2D image.
- For each foreground pixel, it inspects the eight surrounding neighbours in a fixed circular order.
- B(P1) counts foreground neighbours; a deletable pixel requires 2 ≤ B(P1) ≤ 6.
- A(P1) counts 0→1 transitions around the circular neighbourhood; a deletable pixel requires A(P1) = 1.
- The algorithm uses two subiterations with different directional product tests.
- Pixels selected during one subiteration must be marked first and deleted together afterward. Immediate in-scan deletion changes the algorithm.
- The passes repeat until neither subiteration removes anything.
- Professional use requires careful preprocessing, border handling, topology checks, performance engineering and downstream validation.
1. Beginner Level: What Is Thinning?
Imagine a thick handwritten stroke drawn as a band of black pixels. For some tasks we do not need the full thickness; we need the stroke’s structural centreline. A thinning algorithm repeatedly removes suitable boundary pixels until the foreground becomes a thin representation of the same basic connected shape.
This is different from simply shrinking every object. An ordinary erosion can shorten branches, remove endpoints and eventually erase the object. Zhang–Suen thinning is designed to be selective: it tries to remove redundant boundary pixels while preserving connectivity and endpoints under its digital-topology rules.
2. Name the 8-Neighbourhood Before Doing Anything Else
Let the candidate foreground pixel be P1. Number its eight neighbours clockwise:
P9 P2 P3
P8 P1 P4
P7 P6 P5
So P2 is north, P3 north-east, P4 east, P5 south-east, P6 south, P7 south-west, P8 west and P9 north-west. This numbering is not cosmetic. The transition-count rule depends on traversing the neighbours in exactly the circular sequence P2, P3, …, P9, then back to P2.
A large fraction of implementation mistakes begin here: a rotated diagram is harmless if the rules are rotated consistently, but mixing one convention’s diagram with another convention’s formulas is not.
3. B(P1): How Many Foreground Neighbours?
Define:
B(P1) = P2 + P3 + P4 + P5 + P6 + P7 + P8 + P9
For binary pixels, B(P1) is simply the number of foreground neighbours. Zhang–Suen requires:
2 ≤ B(P1) ≤ 6
The lower bound helps protect isolated endpoints and very thin structures. The upper bound avoids deleting pixels whose neighbourhood is almost completely filled, where the local geometry has a different topological role.
4. A(P1): Count Connectivity Transitions, Not Just Neighbours
Neighbour count alone cannot tell whether deleting P1 would split one connected foreground component into two. Zhang–Suen therefore also counts the number of 0→1 transitions while walking around the neighbourhood circle:
A(P1) = number of 0→1 transitions in
P2, P3, P4, P5, P6, P7, P8, P9, P2
The deletion rule requires A(P1) = 1. Intuitively, the surrounding foreground neighbours should form one connected arc around P1, rather than several separated arcs. If there are two distinct 0→1 transitions, deleting the centre pixel may sever connectivity between regions that meet through it.
5. Why the Circular Wraparound Matters
The final pair P9→P2 is part of the neighbourhood circle. If code counts transitions only from P2→P3 through P8→P9 and forgets P9→P2, some configurations receive the wrong A(P1) value.
This is a good example of a small implementation detail carrying mathematical meaning. The neighbourhood is not an eight-element line. It is a cycle.
6. Subiteration 1: Remove One Directional Class of Boundary Pixels
During the first subiteration, a foreground pixel P1 is marked for deletion only when all four conditions hold:
2 ≤ B(P1) ≤ 6
A(P1) = 1
P2 * P4 * P6 = 0
P4 * P6 * P8 = 0
The product test equals zero when at least one pixel in the specified triple is background. These directional conditions prevent the first subiteration from removing certain structural configurations too aggressively.
7. Subiteration 2: Change the Directional Constraint
After all pixels marked in subiteration 1 are deleted simultaneously, scan again using the same B and A requirements but different product tests:
2 ≤ B(P1) ≤ 6
A(P1) = 1
P2 * P4 * P8 = 0
P2 * P6 * P8 = 0
The two-pass asymmetry is deliberate. Alternating the directional restrictions lets the shape thin from different sides without allowing a single scan direction to dominate the result.
8. The Critical Rule: Mark First, Delete Together
Suppose pixel X is examined early in a scan and qualifies for deletion. If the program immediately sets X to background, then a neighbouring pixel Y examined a moment later sees a different neighbourhood from the one defined for that subiteration. The result becomes scan-order dependent.
The correct teaching pattern is:
- read all candidate decisions from the current subiteration image;
- store the coordinates of pixels to remove;
- finish the scan;
- delete all marked pixels simultaneously;
- then begin the next subiteration.
This is the algorithm’s synchronous update semantics. Changing it is not merely an optimisation; it changes the computation.
9. Pseudocode
ZHANG_SUEN(image):
repeat:
changed = false
marks = []
for each foreground pixel P1 not on the outer border:
read P2..P9
if 2 ≤ B(P1) ≤ 6
and A(P1) = 1
and P2*P4*P6 = 0
and P4*P6*P8 = 0:
marks.append(P1)
delete all pixels in marks simultaneously
if marks not empty: changed = true
marks = []
for each foreground pixel P1 not on the outer border:
read P2..P9
if 2 ≤ B(P1) ≤ 6
and A(P1) = 1
and P2*P4*P8 = 0
and P2*P6*P8 = 0:
marks.append(P1)
delete all pixels in marks simultaneously
if marks not empty: changed = true
until changed = false
return image
10. Transparent Python Teaching Implementation
import numpy as np
def neighbours(img, r, c):
# P2, P3, ..., P9 clockwise
return [
img[r - 1, c],
img[r - 1, c + 1],
img[r, c + 1],
img[r + 1, c + 1],
img[r + 1, c],
img[r + 1, c - 1],
img[r, c - 1],
img[r - 1, c - 1],
]
def transition_count(p):
return sum(p[i] == 0 and p[(i + 1) % 8] == 1 for i in range(8))
def zhang_suen(binary):
img = (binary != 0).astype(np.uint8).copy()
rows, cols = img.shape
while True:
changed = False
for step in (0, 1):
to_delete = []
for r in range(1, rows - 1):
for c in range(1, cols - 1):
if img[r, c] == 0:
continue
p = neighbours(img, r, c)
b = sum(p)
a = transition_count(p)
if not (2 <= b <= 6 and a == 1):
continue
p2, p3, p4, p5, p6, p7, p8, p9 = p
if step == 0:
directional_ok = (
p2 * p4 * p6 == 0 and
p4 * p6 * p8 == 0
)
else:
directional_ok = (
p2 * p4 * p8 == 0 and
p2 * p6 * p8 == 0
)
if directional_ok:
to_delete.append((r, c))
if to_delete:
changed = True
for r, c in to_delete:
img[r, c] = 0
if not changed:
return img
This version deliberately scans the whole interior each time so every rule remains visible. Production implementations can avoid reconsidering large unchanged regions, use vectorised neighbourhood operations or use highly optimised library code.
11. A Hand Trace Worth Doing Before Coding
Draw a 5×5 filled square surrounded by background. Pick one boundary pixel. Write out P2 through P9, calculate B, then circle every 0→1 transition to calculate A. Test the two directional conditions. Do this for a corner-adjacent pixel, a side pixel and an interior pixel.
Then predict all pixels that will be marked in subiteration 1 without erasing any of them yet. Only after the entire pass should the class delete the marked set. This physical separation between “decide” and “commit” makes the synchronous nature of the algorithm much easier to understand.
12. Convergence and Complexity
Every successful deletion changes a foreground pixel to background, and deleted pixels are never restored. Since a finite binary image contains only finitely many foreground pixels, the algorithm eventually reaches a fixed point where neither subiteration can delete anything.
For an H×W image, a straightforward implementation that scans the full image in each of I outer iterations performs O(IHW) neighbourhood checks. I depends on the geometry and thickness of the shapes. Memory can remain O(HW) for the image plus a deletion mask or coordinate list.
Faster implementations often track pixels near recent changes instead of rescanning stable interiors. But any optimisation must preserve the same effective subiteration semantics and neighbourhood conditions.
13. OpenCV and scikit-image Practice
OpenCV’s current ximgproc.thinning interface includes a THINNING_ZHANGSUEN mode for thinning binary blobs. scikit-image’s current skeletonize documentation also exposes Zhang’s method for 2D skeletonization and distinguishes it from Lee’s method, which supports 2D or 3D processing.
# scikit-image
from skimage.morphology import skeletonize
skeleton = skeletonize(binary_image, method="zhang")
# OpenCV (requires the ximgproc module in opencv-contrib)
import cv2
thin = cv2.ximgproc.thinning(
binary_uint8,
thinningType=cv2.ximgproc.THINNING_ZHANGSUEN,
)
Check each library’s expected foreground values and data type. A mathematically correct algorithm can appear broken when the input convention is wrong—for example, booleans versus 0/255 bytes or foreground/background polarity reversed.
14. Skeletonization Is Not Automatically a Medial Axis
Several image operations produce something that visually resembles a centreline, but they optimise different objects. Iterative thinning uses deletion rules. A medial-axis transform is tied to distance-to-boundary structure and maximal inscribed disks. Morphological skeletons have yet another construction.
Do not choose Zhang–Suen simply because the output “looks thin.” Choose it because its topology-preserving thinning behaviour matches the downstream representation you need.
15. Preprocessing Can Dominate the Result
Zhang–Suen assumes the binary mask already represents the object of interest. Noise, holes, touching objects, jagged segmentation boundaries and small isolated components all become part of the skeletonization problem if they are left in the mask.
- Thresholding errors can create false branches.
- Small foreground speckles can become tiny skeleton components.
- Small holes can create loops or branch structures.
- Aliased boundaries can produce spurs.
- Touching objects may become one connected skeleton even if the real objects should be separate.
Professional evaluation therefore begins upstream: define what the binary foreground means, how it was segmented and what topological properties are expected to survive.
16. Failure Modes Strong Learners Should Test
- Immediate deletion: mutating the image during a scan makes results depend on scan order.
- Wrong neighbour order: A(P1) and the directional products no longer describe the intended geometry.
- Forgotten wraparound: omitting P9→P2 corrupts the transition count.
- Border access: indexing outside the image or silently using wrapped array indices changes neighbourhoods.
- Non-binary input: multiplying arbitrary grayscale values does not implement the binary rule.
- Foreground polarity reversal: thinning the background produces nonsense that can still look structured.
- 3D misuse: the classic Zhang–Suen neighbourhood and rules are 2D.
- Spur overinterpretation: small segmentation irregularities can survive as branches.
- Connectivity-only validation: preserving connectivity does not guarantee that every geometric measurement derived from the skeleton is accurate.
17. Professional-Level Validation
A useful test suite should include straight bars of different widths, corners, T-junctions, X-junctions, loops, diagonal strokes, touching components, one-pixel lines, endpoints, isolated pixels and noisy masks. For each case, compare properties before and after thinning:
- number of connected foreground components;
- presence of expected holes or loops;
- endpoint count;
- branch-point count;
- path length or graph structure;
- distance between the skeleton and expected centreline where a geometric ground truth exists.
If the skeleton feeds handwriting recognition, fingerprint analysis, road extraction, vessel analysis or graph construction, validate the downstream task as well. A skeleton can pass visual inspection and still change the feature that the next algorithm actually consumes.
18. Learning Progression: Beginner to Professional
- Beginner: label P1–P9 and manually calculate B and A on small neighbourhood cards.
- Intermediate: perform one full two-subiteration thinning cycle on graph paper using a separate “mark” colour before deleting.
- Advanced: implement the algorithm, create adversarial shapes and compare the result with scikit-image or OpenCV.
- Professional: optimise the candidate frontier, build topology-aware regression tests, measure downstream task error and document the binary-mask assumptions.
For teaching, use a Predict–Run–Investigate–Modify sequence: predict which pixels qualify, run one subiteration, investigate unexpected survivors or deletions, then modify the shape and predict again. Programming-education research on PRIMM, code tracing and subgoal-labelled worked examples supports this shift from reading a finished algorithm toward explaining and modifying its recurring decision structure.
19. Practice Problems
- For eight different neighbourhood patterns, calculate B(P1) and A(P1) by hand.
- Construct a neighbourhood with B=4 but A=2. Explain why neighbour count alone is insufficient.
- Modify the teaching implementation to use a boolean deletion mask instead of a Python list.
- Deliberately delete pixels immediately during scanning. Find a shape whose result changes with scan order.
- Compare Zhang–Suen and Lee skeletonization on the same set of 2D shapes.
- Add salt-and-pepper noise before thresholding and measure the change in branch-point count.
- Track only neighbours of recently deleted pixels as future candidates and verify output equality against the full-scan version.
20. Sources and Further Reading
- T. Y. Zhang and C. Y. Suen (1984), “A Fast Parallel Algorithm for Thinning Digital Patterns”.
- OpenCV ximgproc thinning documentation.
- scikit-image morphology and skeletonize documentation.
- PRIMM: structured programming pedagogy.
- Subgoal-labelled worked examples in introductory programming.
- Programming-trace research and novice code-writing skill.
Final idea: Zhang–Suen thinning is a compact lesson in local rules with global consequences. Every decision uses only a 3×3 neighbourhood, yet the algorithm’s success depends on preserving a global structural property—connectivity—across many synchronized deletion rounds. That tension between local computation and global invariant is one of the most transferable ideas in algorithm design.
