Gradients on a grid
For a 2D image I(x, y), the gradient ∇I = (∂I/∂x, ∂I/∂y) points in the direction of steepest brightness increase. On a discrete grid we approximate partial derivatives with finite differences—exactly what convolution kernels like Sobel implement. The gradient magnitude |∇I| ≈ √(Gx² + Gy²) highlights edges regardless of orientation; the angle atan2(Gy, Gx) describes edge direction.
Gx, Gy
Horizontal and vertical derivative images. Large values where intensity jumps across columns or rows.
Magnitude
Combines both directions—useful for visualization and non-max suppression in Canny.
Pre-blur
Derivatives amplify noise. A little Gaussian blur before Sobel/Canny stabilizes results.
Sobel and Scharr
cv2.Sobel computes separable 3×3 (or larger, odd) derivative filters. Use CV_64F (float) to avoid clipping negative slopes, then rescale for display. Scharr is a 3×3 variant with slightly better rotation invariance for equal cost.
import cv2
import numpy as np
gray = cv2.imread("photo.jpg", cv2.IMREAD_GRAYSCALE)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
gx = cv2.Sobel(blur, cv2.CV_64F, 1, 0, ksize=3)
gy = cv2.Sobel(blur, cv2.CV_64F, 0, 1, ksize=3)
mag = np.sqrt(gx * gx + gy * gy)
mag_u8 = np.uint8(255 * mag / (mag.max() + 1e-6))
gx_s = cv2.Scharr(blur, cv2.CV_64F, 1, 0)
gy_s = cv2.Scharr(blur, cv2.CV_64F, 0, 1)
Convert to uint8 only for saving or imshow; keep floats for further math.
Gradient angle (optional)
angle = np.arctan2(gy, gx) # radians, in [-pi, pi]
Laplacian
The Laplacian is the second derivative (sum of ∂²/∂x² and ∂²/∂y²). It is sensitive to noise but highlights fine detail and zero-crossings near edges. Often used in sharpening (unsharp mask) and as a building block in blob detection.
import cv2
gray = cv2.imread("photo.jpg", cv2.IMREAD_GRAYSCALE)
lap = cv2.Laplacian(gray, cv2.CV_64F, ksize=3)
lap_vis = cv2.convertScaleAbs(lap)
convertScaleAbs maps floats to 8-bit for display after Laplacian/Sobel.
Canny edge detector
Canny is a multi-stage algorithm: Gaussian smoothing, gradient magnitude and angle, non-maximum suppression (thin edges along the gradient normal), and hysteresis thresholding with a low and high threshold. Pixels above threshold2 are strong edges; weak edges are kept only if connected to strong ones—this reduces broken fragments.
import cv2
gray = cv2.imread("photo.jpg", cv2.IMREAD_GRAYSCALE)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
# Classic ratio: t_high : t_low often ~ 2:1 or 3:1; tune per image scale
t1, t2 = 50, 150
edges = cv2.Canny(blur, t1, t2, apertureSize=3, L2gradient=False)
More Canny examples
# Finer edges (noisier): lower thresholds
fine = cv2.Canny(blur, 30, 90)
# Fewer, stronger edges: raise thresholds
coarse = cv2.Canny(blur, 100, 200)
# L2 gradient uses sqrt(Gx^2+Gy^2) internally for magnitude
edges_l2 = cv2.Canny(blur, 50, 150, apertureSize=3, L2gradient=True)
# Larger Sobel aperture inside Canny (5 or 7): slightly smoother gradients
edges_ap5 = cv2.Canny(blur, 50, 150, apertureSize=5)
Practical pipeline
For document or industrial images, thresholds are easier to set if intensities are normalized. One pattern: blur → optional CLAHE (see histogram chapter) → Canny. For color images, either convert to grayscale or run Canny per channel and combine with bitwise OR.
import cv2
import numpy as np
bgr = cv2.imread("scene.jpg")
gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (5, 5), 0)
med = np.median(gray)
t1 = int(max(0, 0.66 * med))
t2 = int(min(255, 1.33 * med))
auto_edges = cv2.Canny(gray, t1, t2)
Median-based heuristic for thresholds—starting point only; validate on your data.
Takeaways
- Use float Sobel/Scharr, then visualize with
convertScaleAbsor manual scaling. - Canny needs two thresholds and benefits from light pre-blur.
- Tune thresholds per resolution and lighting; consider auto heuristics then refine.