Lecture 07 — Edges/Contours & Local Features

Lecture 7

Dr. Muhammad Kazim

Department of Intelligent Systems, University of Lahore

Today’s Agenda

Part 1 — Edges & Contours

  • The Canny detector, in depth
  • Hough transform — lines & circles
  • Contour tracing & polygon approximation
  • Region descriptors: moments, Hu invariants, shape matching

Part 2 — Local Features

  • Harris & Shi–Tomasi corners
  • Scale space & Difference-of-Gaussians blobs
  • FAST
  • Repeatability & invariance

Lab 7 builds a feature-matching + shape-detection application from everything here.

From Gradients to Edges

Sobel/Laplacian (Lecture 4) give a gradient magnitude image — every pixel near an edge lights up, producing thick, noisy edge bands. An edge detector must turn that into a clean, one-pixel-wide, thresholded edge map. Canny (1986) is still the reference algorithm because it explicitly optimises three criteria:

  1. Good detection — low probability of missing real edges or flagging fake ones.
  2. Good localisation — detected edges should be close to the true edge position.
  3. Single response — one detection per real edge, not a cluster.

Canny, Step by Step (1–2)

1. Noise suppression: convolve with a Gaussian \(G_\sigma\) first — Canny is a gradient method, and gradients amplify noise.

2. Gradient computation: Sobel (or similar) gives \(g_x, g_y\), from which:

\[ M(x,y) = \sqrt{g_x^2 + g_y^2}, \qquad \theta(x,y) = \operatorname{atan2}(g_y, g_x) \]

The direction \(\theta\) is quantised to one of 4 discrete directions (0°, 45°, 90°, 135°) for the next step.

Canny, Step 3: Non-Maximum Suppression

At every pixel, compare the gradient magnitude \(M\) against its two neighbours along the gradient direction \(\theta\) (i.e. perpendicular to the edge). Keep the pixel only if it is a local maximum in that direction; otherwise suppress it to 0.

  • This is what turns a thick gradient ridge into a thin, 1-pixel edge.
  • Without NMS, every edge detector produces blurry multi-pixel bands.

Canny, Steps 4–5: Hysteresis Thresholding

A single threshold is fragile: too low → noise passes as edges; too high → weak-but-real edges break into gaps. Canny uses two thresholds \(T_{low} < T_{high}\):

  • Pixels \(> T_{high}\): strong edges, kept immediately.
  • Pixels between \(T_{low}\) and \(T_{high}\): weak edges, kept only if connected (8-connectivity) to a strong edge.
  • Pixels \(< T_{low}\): discarded.
edges = cv2.Canny(gray, threshold1=50, threshold2=150, apertureSize=3, L2gradient=True)

Practitioner rule of thumb: start with \(T_{high}:T_{low} \approx 2\)–\(3 : 1\), and always tune on your own footage with a trackbar (Lab 6’s UI pattern) — there is no universal setting.

The Hough Transform — Voting in Parameter Space

Given a noisy, broken edge map, how do we find the lines or circles hiding in it? The Hough transform reframes shape detection as voting: every edge pixel votes for every shape it could be part of, and peaks in the vote accumulator are the detected shapes.

Key idea: a line through image point \((x,y)\) in the usual \(y=mx+c\) form has an unbounded slope. Instead use the normal form:

\[ \rho = x\cos\theta + y\sin\theta \]

Every edge point \((x,y)\) traces a sinusoid in \((\rho,\theta)\) space; points that are collinear in the image produce sinusoids that all cross at one \((\rho,\theta)\) — that intersection is a peak in the accumulator, i.e. a detected line.

Hough Lines in Practice

lines = cv2.HoughLinesP(edges, rho=1, theta=np.pi/180,
                         threshold=80, minLineLength=40, maxLineGap=10)
for x1, y1, x2, y2 in lines[:, 0]:
    cv2.line(out, (x1, y1), (x2, y2), (0, 255, 0), 2)
  • Standard Hough (HoughLines) returns infinite lines \((\rho,\theta)\); Probabilistic Hough (HoughLinesP) returns finite segments and is faster — the practical default.
  • threshold = minimum votes to accept a line; minLineLength/maxLineGap merge nearby broken segments.

Hough Circles

A circle needs 3 parameters — centre \((a,b)\) and radius \(r\): \((x-a)^2+(y-b)^2=r^2\). A brute-force 3D accumulator is expensive, so OpenCV’s HOUGH_GRADIENT uses the edge gradient direction to vote only along the likely radius direction from each edge pixel, then searches candidate radii — much faster.

circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, dp=1.5, minDist=30,
                            param1=100, param2=40, minRadius=5, maxRadius=60)

Common use: coin/pupil/bolt-hole counting on a fixed camera rig where circles are the dominant shape.

Contour Tracing

A contour is an ordered list of boundary points around a connected region in a binary mask — the natural output of thresholding + morphology (Lecture 6).

contours, hierarchy = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
  • Retrieval mode (RETR_EXTERNAL, RETR_LIST, RETR_TREE, …) controls whether holes/nested contours are returned and how the parent–child hierarchy is built.
  • Approximation method: CHAIN_APPROX_NONE stores every boundary pixel; CHAIN_APPROX_SIMPLE compresses straight segments to their endpoints — far fewer points, same shape.
  • Once you have a contour: cv2.contourArea, cv2.arcLength, cv2.boundingRect, cv2.minAreaRect give geometric measurements directly (Lab 5, Lab 9).

Polygon Approximation

The Ramer–Douglas–Peucker algorithm simplifies a contour to a polygon with fewer vertices while staying within a tolerance \(\varepsilon\) of the original curve:

  1. Start with the line joining the first and last contour points.
  2. Find the point on the contour farthest from that line; if its distance \(> \varepsilon\), keep it as a new vertex and recurse on the two sub-segments.
  3. Stop when every remaining point is within \(\varepsilon\).
eps = 0.02 * cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, eps, True)
n_sides = len(approx)      # 3=triangle, 4=quad, ... , circle-ish otherwise

This is the classical way to tell “is this a rectangle, a triangle, or a blob?” from a contour — and the geometric core of the Lab 8 document-scanner homography (find the 4-corner quad, then warp).

Region Descriptors: Moments & Hu Invariants

Image moments summarise a shape’s mass distribution:

\[ m_{pq} = \sum_{x,y} x^p y^q\, I(x,y) \]

  • \(m_{00}\) = area; centroid \((\bar x,\bar y) = (m_{10}/m_{00},\, m_{01}/m_{00})\).
  • Central moments \(\mu_{pq}\) (computed about the centroid) are translation-invariant.
  • Normalised central moments \(\eta_{pq}\) add scale invariance.

Hu moment invariants (7 values, Hu 1962) are non-linear combinations of \(\eta_{pq}\) up to 3rd order that are invariant to translation, scale, and rotation — a compact shape fingerprint.

M = cv2.moments(cnt)
hu = cv2.HuMoments(M).flatten()
score = cv2.matchShapes(cnt1, cnt2, cv2.CONTOURS_MATCH_I1, 0)   # smaller = more similar

From Edges to Corners

Edges localise well in one direction but not along themselves. For matching two images of the same scene, we need points that are localised in both directions — corners and blobs.

What Makes a Good Local Feature?

  • Repeatability — the same physical point should be detected again under a different viewpoint, scale, or lighting.
  • Distinctiveness — its neighbourhood should look different from most other points, so it can be matched uniquely.
  • Locality — robust to occlusion and clutter, since only a small region is used.
  • Invariance — ideally unaffected by rotation, scale, illumination, and (to a degree) viewpoint change.
  • Efficiency — on an embedded target, detection + description must run in milliseconds, not seconds.

Harris Corner Detector

A corner is a point where the image intensity changes sharply in two directions (an edge changes in only one). Harris (1988) formalises this with the structure tensor over a window \(w\):

\[ M = \sum_{w} \begin{bmatrix} I_x^2 & I_x I_y \\ I_x I_y & I_y^2 \end{bmatrix}, \qquad R = \det(M) - k\,(\operatorname{trace}(M))^2 \]

  • Eigenvalues \(\lambda_1,\lambda_2\) of \(M\) summarise local structure: both small → flat region; one large → edge; both large → corner.
  • \(R \gg 0\) → corner; \(R \ll 0\) → edge; \(|R|\) small → flat. \(k \approx 0.04\)–0.06 empirically.
harris = cv2.cornerHarris(gray_f32, blockSize=2, ksize=3, k=0.04)
corners = harris > 0.01 * harris.max()

Shi–Tomasi (“Good Features to Track”)

Shi & Tomasi (1994) showed that a more direct and more stable corner score is simply the smaller eigenvalue of the same structure tensor:

\[ R_{ST} = \min(\lambda_1, \lambda_2) \]

  • A point is a good corner if even its weakest direction of intensity change is strong — more principled than Harris’s heuristic \(k\) term.
  • This is the criterion behind OpenCV’s goodFeaturesToTrack, the standard seed-point detector for KLT optical-flow tracking (Lecture 11).
pts = cv2.goodFeaturesToTrack(gray, maxCorners=200, qualityLevel=0.01,
                               minDistance=8, blockSize=7)

Scale Space

Harris/Shi–Tomasi detect corners at a single, fixed scale (the window size). A real corner can appear at many scales (a building corner vs. a whole building), and the “right” scale depends on the object — unknown in advance.

Scale space repeatedly Gaussian-blurs the image with increasing \(\sigma\), building a stack \(L(x,y,\sigma) = G_\sigma * I(x,y)\). Features are then detected as extrema across both space and scale, giving each keypoint a characteristic scale — this is what makes a detector scale-invariant.

Difference-of-Gaussians (DoG) Blob Detection

The Laplacian-of-Gaussian (LoG) is the ideal blob detector (responds maximally when the blob size matches \(\sigma\)), but is expensive. The Difference of Gaussians is a cheap, close approximation:

\[ D(x,y,\sigma) = L(x,y,k\sigma) - L(x,y,\sigma) \;\approx\; (k-1)\,\sigma^2 \nabla^2 G * I \]

  • Build a pyramid of DoG images across octaves/scales; a keypoint is a pixel that is a local extremum among its 8 spatial neighbours and its 9+9 neighbours in the scale levels above/below (26 comparisons total).
  • This is exactly the detection stage of SIFT (Lecture 8) — the descriptor stage comes next lecture.

FAST (Features from Accelerated Segment Test)

Harris/DoG are accurate but too slow for real-time embedded use. FAST (Rosten & Drummond, 2006) trades some accuracy for a huge speed-up:

  1. Consider a Bresenham circle of 16 pixels around a candidate \(p\).
  2. \(p\) is a corner if a contiguous arc of \(N\) (typically 12) pixels on the circle are all brighter than \(I(p)+t\), or all darker than \(I(p)-t\).
  3. A fast reject test checks only pixels 1, 5, 9, 13 first — most non-corners are rejected in 4 comparisons.
  4. (Original FAST) a decision-tree classifier, trained with ID3, further accelerates the test.
fast = cv2.FastFeatureDetector_create(threshold=20, nonmaxSuppression=True)
kps = fast.detect(gray, None)

FAST alone has no scale or orientation information — that is exactly the gap ORB fills by pairing FAST detection with an orientation estimate and the BRIEF descriptor (next lecture).

Repeatability & Invariance — Comparing Detectors

DetectorRotation-invariantScale-invariantSpeedTypical use
HarrisYesNoMediumCalibration targets, planar scenes
Shi–TomasiYesNoMediumKLT tracking seed points
DoG (SIFT detector)YesYesSlowWide-baseline matching, panoramas
FASTNoNoVery fastReal-time / embedded (paired with a descriptor)

Repeatability is normally reported as the fraction of keypoints detected in image A that reappear (within a small pixel tolerance, after applying the known transform) in image B — the standard benchmark protocol is Mikolajczyk & Schmid (2005).

Practitioner Notes

  • For an embedded target (Pi 5, no GPU): FAST + a binary descriptor (ORB) is typically 10–50× faster than DoG + SIFT with only a modest drop in match quality — the default choice for Lab 7 and beyond.
  • Canny/Hough parameters are scene- and lighting-dependent — always expose them as trackbars during development, then bake in fixed values for deployment.
  • approxPolyDP’s \(\varepsilon\) should scale with contour perimeter (e.g. 0.02 * arcLength), not be a fixed pixel count, or it will misbehave across resolutions.
  • Hu invariants are elegant but comparatively weak descriptors by modern standards — use them for coarse shape sorting, not fine-grained recognition.

This Week’s Lab

Lab 7 — Features & Matching.

  • Detect corners/keypoints with Harris, Shi–Tomasi, and FAST; compare counts and locations.
  • Extract ORB and SIFT descriptors and match keypoints between two images.
  • Detect Hough lines and circles on a structured scene (e.g. a door frame, a set of coins).
  • Deliverable: a feature-matching + shape-detection application.

Key Takeaways

  • Canny = smooth → gradient → non-max suppression → hysteresis threshold; every step exists to fix a specific failure mode of naive gradient thresholding.
  • The Hough transform turns shape-fitting into accumulator voting — robust to gaps and noise in the edge map.
  • Contours + moments/Hu invariants give you measurable, comparable shape descriptors straight from a binary mask.
  • Corners (Harris/Shi–Tomasi/FAST) and blobs (DoG) are the two families of local-feature detectors; they trade off speed, scale-invariance, and rotation-invariance differently.
  • Detection is only half the story — next lecture adds descriptors so these keypoints can be matched across images.

Further Reading

  • Szeliski, Computer Vision: Algorithms and Applications 2e — §7.1–7.3 (feature detection & description).
  • Gonzalez & Woods, Digital Image Processing 4e — §10.2–10.3 (edges, Hough, boundary description).
  • Harris & Stephens (1988), “A Combined Corner and Edge Detector.” Rosten & Drummond (2006), “Machine Learning for High-Speed Corner Detection” (FAST).