Lecture 09 — Segmentation I & II

Lecture 9

Dr. Muhammad Kazim

Department of Intelligent Systems, University of Lahore

Today’s Roadmap

  • L17 — Segmentation I: region growing & split/merge; k-means & mean-shift in colour space; SLIC superpixels; IoU/Dice evaluation.
  • L18 — Segmentation II: watershed; graph cuts & GrabCut; active contours (snakes); grouping cues; the bridge from hand-designed masks to learned segmentation.

Reading: Szeliski 2e §7.5. Lab 9 this week; the project proposal is due.

What Is Segmentation?

Segmentation partitions an image into regions that are homogeneous with respect to some property (colour, texture, motion) and, ideally, correspond to meaningful scene structures.

  • Classification — one label for the whole image.
  • Detection — a box per instance.
  • Segmentation — a label per pixel: which region/object does this pixel belong to?

Two flavours you will meet later with deep nets: semantic segmentation (label per class, instances merged) and instance segmentation (label per object instance). Today is entirely classical, pixel- and region-based.

Region Growing

  1. Pick one or more seed pixels.
  2. Add a neighbouring pixel to the region if it satisfies a homogeneity test (e.g. \(|I(p) - \mu_{\text{region}}| < T\)).
  3. Update the region statistic (mean) and repeat until no pixel qualifies.

Pros: simple, connects naturally, good with a good seed. Cons: seed-dependent, sensitive to noise, slow on large images (implement with a queue/stack, not recursion).

Split & Merge (Quadtree)

  1. Split: if a region is not homogeneous, divide it into four quadrants; recurse.
  2. Merge: after splitting, merge adjacent regions whose union is still homogeneous.

Homogeneity test: e.g. \(\max(I) - \min(I) < T\) or variance < threshold. Produces a quadtree; naturally multi-resolution and less seed-dependent than region growing, but blocky boundaries (mitigated by the merge step).

K-Means in Colour Space

Treat every pixel as a point \(x_i \in \mathbb{R}^3\) (e.g. Lab or HSV) and cluster into \(K\) groups minimising:

\[ J = \sum_{k=1}^{K} \sum_{x_i \in C_k} \lVert x_i - \mu_k \rVert^2 \]

  1. Initialise \(K\) centroids (k-means++ for stability).
  2. Assign each pixel to its nearest centroid.
  3. Recompute centroids as the mean of assigned pixels.
  4. Repeat 2–3 until assignments stop changing.

Practitioner note: cluster in Lab, not RGB — perceptual distance behaves much better; add \((x, y)\) to the feature vector for spatial coherence.

Code — K-Means Colour Segmentation

import cv2, numpy as np

img = cv2.imread("scene.jpg")
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
Z = lab.reshape((-1, 3)).astype(np.float32)

K = 4
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 20, 1.0)
_, labels, centers = cv2.kmeans(Z, K, None, criteria, 5, cv2.KMEANS_PP_CENTERS)

centers = centers.astype(np.uint8)
seg = centers[labels.flatten()].reshape(img.shape)
seg_bgr = cv2.cvtColor(seg, cv2.COLOR_LAB2BGR)
cv2.imwrite("kmeans_seg.jpg", seg_bgr)

Mean-Shift Segmentation

Mean-shift treats pixels (colour + optionally position) as samples from an unknown density and climbs the density gradient to a mode using a kernel window:

\[ m(x) = \frac{\sum_i K(x_i - x)\, x_i}{\sum_i K(x_i - x)} - x \]

  1. For each point, iteratively shift the window centre by \(m(x)\) until convergence (a mode).
  2. Points converging to the same mode form one segment.

Unlike k-means, you don’t choose \(K\) — the number of modes falls out of the bandwidth \(h\). OpenCV: cv2.pyrMeanShiftFiltering(img, sp, sr) (spatial and colour radius).

SLIC Superpixels

SLIC (Simple Linear Iterative Clustering) is a local k-means in a 5-D space \([L, a, b, x, y]\), restricted to a search window around each cluster — this makes it near-linear time.

  1. Seed \(K\) cluster centres on a regular grid, step \(S = \sqrt{N/K}\).
  2. Move seeds to the lowest-gradient position in a 3×3 neighbourhood (avoid edges/noise).
  3. Assign each pixel to the nearest centre within \(2S\) using a weighted colour+space distance.
  4. Update centres to the mean of their pixels; repeat until convergence; enforce connectivity as a post-pass.

Superpixels are a pre-processing step, not a final segmentation — they reduce an image with millions of pixels to a few hundred/thousand perceptually-meaningful atoms for the next stage (graph cuts, classification, etc.).

Code — SLIC Superpixels

import cv2

img = cv2.imread("scene.jpg")
slic = cv2.ximgproc.createSuperpixelSLIC(img, algorithm=cv2.ximgproc.SLICO,
                                          region_size=30)
slic.iterate(10)
mask = slic.getLabelContourMask()
overlay = img.copy()
overlay[mask == 255] = (0, 0, 255)
print("Superpixels:", slic.getNumberOfSuperpixels())
cv2.imwrite("slic_overlay.jpg", overlay)

Requires opencv-contrib-python (the ximgproc module).

Evaluating Segmentation: IoU & Dice

Given predicted mask \(P\) and ground-truth mask \(G\):

\[ \text{IoU} = \frac{|P \cap G|}{|P \cup G|}, \qquad \text{Dice} = \frac{2|P \cap G|}{|P| + |G|} \]

  • IoU is stricter than Dice for small overlaps; both range \([0, 1]\), 1 = perfect.
  • \(\text{Dice} = \dfrac{2\,\text{IoU}}{1 + \text{IoU}}\) — they are monotonically related, but not the same number.
  • Report per-class IoU and mean IoU (mIoU) for multi-class masks — a single global pixel-accuracy number hides small-object failures.

Watershed Transform

Treat the (gradient) image as a topographic surface: bright ridges are gradient maxima (object boundaries), dark basins are homogeneous regions. “Flooding” from markers upward fills basins until they meet at ridge lines — those lines are the segmentation boundary.

  1. Compute the gradient magnitude image.
  2. Mark known foreground (e.g. via distance-transform local maxima) and known background; leave the rest unknown (label 0).
  3. Run cv2.watershed(img, markers) — boundaries are written back as \(-1\) in markers.

Failure mode: over-segmentation from noisy gradients — always smooth first and seed with good markers (distance transform + connected components is the standard recipe for touching objects, e.g. coins or cells).

Graph Cuts

Model the image as a graph: one node per pixel plus two terminals source (foreground) and sink (background). Minimise an energy:

\[ E(L) = \sum_p D_p(L_p) + \lambda \sum_{(p,q) \in \mathcal{N}} V_{p,q}(L_p, L_q) \]

  • \(D_p\) — data term: cost of labelling pixel \(p\) fg/bg (from colour likelihood).
  • \(V_{p,q}\) — smoothness term: penalises label changes between similar neighbours (edge-aware).
  • The global optimum for this binary energy is found in polynomial time by min-cut / max-flow on the graph.

GrabCut

GrabCut wraps graph cuts with iterative colour-model refinement, seeded from just a bounding box:

  1. User (or an upstream detector) provides a rectangle roughly bounding the foreground.
  2. Fit Gaussian Mixture Models (GMMs) to the fg/bg colour distributions inside/outside the box.
  3. Run graph-cut min-cut using the GMMs as the data term to get a fg/bg labelling.
  4. Re-fit the GMMs using the new labelling; repeat 2–4 until convergence.
import cv2, numpy as np

img = cv2.imread("object.jpg")
mask = np.zeros(img.shape[:2], np.uint8)
bgd, fgd = np.zeros((1, 65), np.float64), np.zeros((1, 65), np.float64)
rect = (50, 50, img.shape[1] - 100, img.shape[0] - 100)

cv2.grabCut(img, mask, rect, bgd, fgd, 5, cv2.GC_INIT_WITH_RECT)
final = np.where((mask == 2) | (mask == 0), 0, 1).astype("uint8")
out = img * final[:, :, None]

Active Contours (Snakes)

A snake is a parametric curve \(v(s) = (x(s), y(s))\) that deforms to minimise an energy balancing smoothness and image attraction:

\[ E_{\text{snake}} = \int_0^1 \underbrace{\alpha |v'(s)|^2 + \beta |v''(s)|^2}_{\text{internal (smoothness)}} + \underbrace{E_{\text{ext}}(v(s))}_{\text{image energy, e.g. } -|\nabla I|^2} \, ds \]

  • Initialise a rough contour near the boundary; it “snaps” to nearby strong edges.
  • Sensitive to initialisation and to \((\alpha, \beta)\); struggles with concave boundaries (addressed by gradient-vector-flow variants).
  • OpenCV: cv2.snake is legacy/removed in places — skimage.segmentation.active_contour is the practical implementation today.

Grouping Cues (Gestalt Principles)

Every classical method above implicitly encodes one or more Gestalt grouping cues that humans use too:

  • Proximity — nearby elements are grouped (spatial term in k-means/SLIC).
  • Similarity — similar colour/texture elements are grouped (colour term).
  • Continuity — smooth contours are preferred (snake internal energy, graph-cut smoothness term).
  • Closure — the visual system completes partially-occluded boundaries.
  • Common fate — elements moving together are grouped (motion segmentation, later in optical flow).

From Hand-Designed Masks to Learned Segmentation

Every method today hand-designs the similarity or energy function. The deep-learning alternative learns a pixel-to-label mapping end-to-end from data:

  • FCN (Fully Convolutional Network) — replace classifier heads with 1×1 convolutions; upsample to full resolution.
  • U-Net — a symmetric encoder–decoder with skip connections that fuse coarse semantics with fine spatial detail. You will build this from scratch in Lab 13.
  • Mask R-CNN — detection + per-instance mask head (instance segmentation).

Classical segmentation doesn’t disappear: GrabCut/SLIC are still used for dataset annotation tools, and IoU/Dice remain the evaluation metrics for the learned models.

Choosing a Method

MethodNeeds a seed?SpeedBest for
Region growingYes (seed pixel)FastHomogeneous blobs, known start point
K-meansNo (choose K)FastColour-distinct regions, quick prototyping
Mean-shiftNo (choose bandwidth)SlowUnknown region count, smooth colour fields
SLICNoVery fastPre-processing / over-segmentation for later stages
WatershedYes (markers)FastTouching/overlapping convex objects
GrabCutYes (box)ModerateInteractive foreground extraction

Common Pitfalls / Practitioner Notes

  • RGB clustering looks fine on screen but clusters badly — always segment in Lab or HSV.
  • Watershed over-segments without good markers; smooth + distance-transform first.
  • GrabCut fails when the box excludes part of the object — give it a generous rectangle.
  • IoU is harsh on small objects: a 1-pixel boundary shift on a 10×10 mask moves IoU a lot more than on a 500×500 mask.
  • Classical methods have no notion of “class” — they group by appearance, not semantics. That gap is exactly what CNN segmentation closes.

This Week’s Lab

Lab 9 — Build a Real-World CV System.

  • Plan the project and design its architecture.
  • Measure the size of an object in 2-D (using calibration — preview of next week).
  • Build a defect-recognition system.
  • Integrate a database (SQLite/MySQL) to log results.

Deliverable: an end-to-end measuring / defect system logging to a database.

Also due this week: the project proposal for your capstone.

Key Takeaways

  • Segmentation assigns a label to every pixel; classical methods encode a hand-designed similarity or energy function.
  • K-means/mean-shift/SLIC group by colour (+ position); watershed/graph-cuts/GrabCut/snakes use boundaries and energy minimisation.
  • IoU and Dice are the standard overlap metrics — they will follow you into every learned-segmentation lab from here on.
  • Classical segmentation is the conceptual and practical bridge to U-Net (Lab 13) and Mask R-CNN.

Further Reading

  • Szeliski, Computer Vision: Algorithms and Applications, 2e, §7.5 (Segmentation).
  • Gonzalez & Woods, Digital Image Processing, 4e, Ch.10 (region-based segmentation, watershed).
  • Achanta et al., “SLIC Superpixels Compared to State-of-the-Art Superpixel Methods”, TPAMI 2012.
  • Rother, Kolmogorov & Blake, “GrabCut”, SIGGRAPH 2004.