Find where intensity changes sharply: the outlines of everything in the picture.
Edge: a significant local change in pixel intensity, the boundary between regions.
A sharp intensity change.
A gradual intensity change.
A peak or a valley.
Why detect edges? Object boundaries · feature extraction · image segmentation · shape analysis
Image gradient: the rate of change of intensity.
Large gradient = edge!
Sobel kernels approximate the gradient with smoothing.
The 1-2-1 weights average across the edge direction.
Small and fast.
Good noise resistance thanks to the smoothing.
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:
# 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
Scharr: a more accurate gradient than Sobel for small 3×3 kernels.
When to use: when you need more accuracy from a 3×3 kernel.
# 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
\(\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 (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 is a multi-stage algorithm, and the classic best edge detector.
Output: a binary edge map (0 or 255).
# 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)
# 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
Strategy:
Answer first, then press → to check.
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).
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.
# 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:
# 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:
# 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
# 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
# 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
# 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