Computer vision

Edge Detection

Find where intensity changes sharply: the outlines of everything in the picture.

What are edges?

Edge: a significant local change in pixel intensity, the boundary between regions.

Step edge

A sharp intensity change.

Ramp edge

A gradual intensity change.

Roof edge

A peak or a valley.

Why detect edges? Object boundaries · feature extraction · image segmentation · shape analysis

Gradient-based edge detection

Image gradient: the rate of change of intensity.

  • \(G_x\): horizontal gradient, \(\partial I / \partial x\)
  • \(G_y\): vertical gradient, \(\partial I / \partial y\)
  • Magnitude: \(G = \sqrt{G_x^2 + G_y^2}\)
  • Direction: \(\theta = \arctan(G_y / G_x)\)

Large gradient = edge!

Gx Gy G, θ

Sobel edge detection

Sobel kernels approximate the gradient with smoothing.

Sobel X
Sobel Y

Gradient + smoothing

The 1-2-1 weights average across the edge direction.

3×3 kernels

Small and fast.

Noise-resistant

Good noise resistance thanks to the smoothing.

Sobel in OpenCV


        import cv2
        import numpy as np
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        # compute sobel x (vertical edges)
        sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
        # ans: dx=1, dy=0 means the x-derivative (vertical edges)
        # compute sobel y (horizontal edges)
        sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
        # ans: dx=0, dy=1 means the y-derivative (horizontal edges)
        # combine both gradients
        magnitude = np.sqrt(sobelx**2 + sobely**2)  # ans:
        # convert to uint8 for display
        magnitude = np.uint8(np.clip(magnitude, 0, 255))  # ans:
      

Sobel parameters


        # cv2.Sobel(src, ddepth, dx, dy, ksize)
        sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=5)
        # ddepth: output image depth
        # ans: CV_64F, so negative values aren't clipped
        # dx, dy: derivative order
        # ans: (1, 0) = x-direction, (0, 1) = y-direction
        # ksize: kernel size (1, 3, 5, 7)
        # ans: larger kernel = more smoothing, less noise
      

Gradients of a real photo

gray

Scharr edge detection

Scharr: a more accurate gradient than Sobel for small 3×3 kernels.

Scharr X
Scharr Y

When to use: when you need more accuracy from a 3×3 kernel.

Scharr in OpenCV


        # scharr gradient (more accurate than sobel)
        scharrx = cv2.Scharr(gray, cv2.CV_64F, 1, 0)
        # ans: x-direction gradient
        scharry = cv2.Scharr(gray, cv2.CV_64F, 0, 1)
        # ans: y-direction gradient
        # only works with a 3x3 kernel
        # ans: more accurate than a 3x3 Sobel
        # combine gradients
        magnitude = np.sqrt(scharrx**2 + scharry**2)
        # ans: the same as combining Sobel
      

Laplacian edge detection

\(\nabla^2 I = \dfrac{\partial^2 I}{\partial x^2} + \dfrac{\partial^2 I}{\partial y^2}\)

A second-derivative operator. Where the first derivative peaks, the second crosses zero.

Property: very sensitive to noise (press Add noise), so smooth first!

Laplacian in OpenCV


        # laplacian (2nd derivative)
        laplacian = cv2.Laplacian(gray, cv2.CV_64F, ksize=3)
        # ans: detects edges in all directions
        # why the second derivative?
        # ans: a zero-crossing marks the edge location
        # the laplacian is very noise-sensitive
        blurred = cv2.GaussianBlur(gray, (3, 3), 0)
        laplacian = cv2.Laplacian(blurred, cv2.CV_64F)
        # ans: blur first to reduce noise
      

Canny edge detection

input (grayscale)

Canny is a multi-stage algorithm, and the classic best edge detector.

  1. Gaussian smoothing (noise reduction)
  2. Gradient calculation (Sobel)
  3. Non-maximum suppression (thin edges)
  4. Double thresholding (strong / weak edges)
  5. Edge tracking by hysteresis

Output: a binary edge map (0 or 255).

Canny parameters


        # canny edge detection
        edges = cv2.Canny(gray, threshold1=50, threshold2=150)
        # ans: threshold1 = low, threshold2 = high
        # threshold1 (low threshold):
        # ans: weak edges below this are discarded
        # threshold2 (high threshold):
        # ans: strong edges above this are kept
        # ratio recommendation:
        # ans: threshold2 = 2 × threshold1 or 3 × threshold1
        # edges between the thresholds:
        # ans: kept if connected to a strong edge (hysteresis)
      

Canny in OpenCV


        # basic canny
        edges = cv2.Canny(gray, 50, 150)
        # ans: detects edges, outputs a binary image
        # canny with an aperture size
        edges = cv2.Canny(gray, 50, 150, apertureSize=3)
        # ans: apertureSize for the Sobel operator (3, 5, 7)
        # canny with the L2 gradient
        edges = cv2.Canny(gray, 50, 150, L2gradient=True)
        # ans: L2gradient=True uses √(Gx²+Gy²): more accurate
        # ans: L2gradient=False uses |Gx|+|Gy|: faster
      

Choosing thresholds

  • Low threshold too low → noise edges
  • High threshold too high → missed edges

Strategy:

  • Try a ratio of 2:1 or 3:1 (e.g. 50, 150)
  • Adjust based on the results
  • High-contrast images → higher thresholds; low-contrast → lower

Exercises

Answer first, then press → to check.

Exercises 1 · concepts

What defines an edge?

A significant change in pixel intensity.

What is the gradient magnitude?

√(Gx² + Gy²): the strength of the intensity change.

Which edges does Sobel X detect?

Vertical edges (left-right transitions).

Why use CV_64F for Sobel?

It keeps negative values instead of clipping them.

What does Canny output?

A binary edge map (0 or 255).

Exercises 2 · concepts

What is the difference between Sobel and Scharr?

Scharr is more accurate for 3×3 kernels.

Why is the Laplacian noise-sensitive?

A second derivative amplifies noise.

What is non-maximum suppression?

It thins edges to single-pixel width.

What is hysteresis in Canny?

It keeps weak edges that connect to strong edges.

What Canny threshold ratio is recommended?

high = 2 × low or 3 × low.

Exercises 3 · coding


        # convert to grayscale
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)  # ans:
        # compute sobel x
        sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)  # ans:
        # compute sobel y
        sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)  # ans:
        # combine sobel x and y
        magnitude = np.sqrt(sobelx**2 + sobely**2)  # ans:
        magnitude = np.uint8(np.clip(magnitude, 0, 255))  # ans:
      

Exercises 4 · coding


        # apply canny with thresholds 100, 200
        edges = cv2.Canny(gray, 100, 200)  # ans:
        # apply the laplacian
        laplacian = cv2.Laplacian(gray, cv2.CV_64F, ksize=3)  # ans:
        # apply scharr x
        scharrx = cv2.Scharr(gray, cv2.CV_64F, 1, 0)  # ans:
        # apply scharr y
        scharry = cv2.Scharr(gray, cv2.CV_64F, 0, 1)  # ans:
      

Exercises 5 · coding


        # canny with different thresholds
        edges1 = cv2.Canny(gray, 30, 90)
        edges2 = cv2.Canny(gray, 100, 300)
        # ans: edges1 detects more (lower thresholds)
        # ans: edges2 detects fewer (higher thresholds)
        # blur before canny
        blurred = cv2.GaussianBlur(gray, (5, 5), 0)  # ans:
        edges = cv2.Canny(blurred, 50, 150)  # ans:
        # ans: reduces noise in the edge map
      

Exercises 6 · mixed


        # sobel with a larger kernel
        sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=7)
        # ans: more smoothing, less sensitive to noise
        # absolute value of sobel
        abs_sobelx = np.absolute(sobelx)  # ans:
        # ans: turns negative gradients positive
        # canny with the L2 gradient
        edges = cv2.Canny(gray, 50, 150, L2gradient=True)
        # ans: a more accurate gradient calculation
      

Exercises 7 · mixed


        # gradient direction from sobel
        sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0)
        sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1)
        direction = np.arctan2(sobely, sobelx)  # ans:
        # ans: direction in radians
        # combine laplacian and gaussian
        blurred = cv2.GaussianBlur(gray, (5, 5), 0)  # ans:
        laplacian = cv2.Laplacian(blurred, cv2.CV_64F)  # ans:
        # what is LoG?
        # ans: Laplacian of Gaussian (blur + laplacian)
        # ans: it reduces noise sensitivity
      

Exercises 8 · advanced


        # automatic canny thresholds (median-based)
        median = np.median(gray)
        lower = int(max(0, 0.7 * median))  # ans:
        upper = int(min(255, 1.3 * median))  # ans:
        edges = cv2.Canny(gray, lower, upper)  # ans:
        # combine canny with morphology
        edges = cv2.Canny(gray, 50, 150)
        kernel = np.ones((3, 3), dtype=np.uint8)
        edges = cv2.dilate(edges, kernel, iterations=1)  # ans:
        # ans: thickens edges for better visibility