Pixels2GenAI
Path i Foundations
M 03 · 3.4.2 · hands-on

3.4.2 Sobel Edge Detection

Specialise convolution into a gradient operator. Two 3×3 kernels — one measuring horizontal change, one measuring vertical — combine via Euclidean magnitude into an orientation-independent edge map.

Duration18–22 min
Levelintermediate
Load3 core concepts
Prereqs3.4.1 (convolution)

Overview

The previous lesson’s “edge detection” kernel was a single 3×3 grid that fires wherever the centre pixel differs from its neighbours. The Sobel operator, developed by Irwin Sobel at Stanford in 1968 [1], does something more refined: it splits the question into horizontal and vertical gradient measurements with two separate kernels, then recombines them via the gradient magnitude √(Gₓ² + Gᵧ²). The result is an orientation-independent edge map that handles diagonal boundaries as cleanly as axis-aligned ones [2]. The same Sobel kernels live inside every modern edge detector, from the Canny algorithm to the convolutional stages of deep object-detection networks.

Learning objectives

  1. Read and apply the Sobel Gₓ and Gᵧ kernels — one measures rate of change along x, the other along y.
  2. Combine the two gradient images into a magnitude map with √(Gₓ² + Gᵧ²).
  3. Threshold the magnitude to isolate strong edges from background noise.
  4. Compare Sobel to Prewitt (equal-weight neighbours) and Roberts (2×2 cross) operators.

Quick start — Sobel on geometric shapes

python · quick_start.py
import numpy as np
from PIL import Image

H, W = 256, 256
image = np.zeros((H, W), dtype=np.float64)
image[80:180, 60:120] = 255                          # rectangle

y, x = np.ogrid[:H, :W]
image[(x - 180) ** 2 + (y - 128) ** 2 <= 40 ** 2] = 255   # circle

# The two Sobel kernels
Gx = np.array([[-1, 0, 1],
               [-2, 0, 2],
               [-1, 0, 1]], dtype=np.float64)
Gy = np.array([[-1, -2, -1],
               [ 0,  0,  0],
               [ 1,  2,  1]], dtype=np.float64)

mag = np.zeros((H, W), dtype=np.float64)
for y_i in range(1, H - 1):
    for x_i in range(1, W - 1):
        n = image[y_i - 1:y_i + 2, x_i - 1:x_i + 2]
        gx = np.sum(Gx * n)
        gy = np.sum(Gy * n)
        mag[y_i, x_i] = np.hypot(gx, gy)             # √(gx² + gy²)

mag = (255 * mag / mag.max()).astype(np.uint8)
Image.fromarray(mag, 'L').save('edge_detection_output.png')
A black background showing the white outlines of a rectangle and a circle, both produced by Sobel edge detection. The edges are 1-pixel wide and clearly delineate the original shapes.
Fig. 1 Sobel applied to a black canvas with a white rectangle and circle. The interiors and background go to zero; only the boundaries — where intensity changes — survive.

Core concepts

Concept 1 — The two Sobel kernels

The horizontal Sobel kernel Gₓ measures the rate of change along x — i.e. how much pixel value changes as you step rightward across the kernel:

text
        | -1   0   1 |
Gₓ  =   | -2   0   2 |
        | -1   0   1 |

Positive on the right, negative on the left, zero in the middle. In a uniform region the positives and negatives cancel. At a vertical edge — a sharp change from dark to bright as you move right — they reinforce. The “1-2-1” weighting along the centre row is the Sobel signature: it weighs the immediate row above and below more than the far neighbours, which doubles as a small Gaussian-like smoothing along the perpendicular axis [1].

The vertical Sobel kernel Gᵧ is the same kernel rotated 90°:

text
        | -1  -2  -1 |
Gᵧ  =   |  0   0   0 |
        |  1   2   1 |

Same logic, perpendicular direction.

A diagram showing the two Sobel kernels side by side. The Gx kernel has negative values on the left (coloured blue), zero in the middle, and positive values on the right (coloured red), with an arrow pointing right. The Gy kernel has negative on top, zero middle, positive bottom, with an arrow pointing down.
Fig. 2 The two Sobel kernels. Blue = negative, red = positive. The arrows show the direction of measurement; the kernels respond strongest to edges perpendicular to their arrow.

Concept 2 — Gradient magnitude

Convolving the image with each kernel gives two gradient images, Gₓ_image and Gᵧ_image. These are signed — a value is positive when intensity rises across the kernel, negative when it falls. To collapse direction into a single “edge strength” per pixel, take the Euclidean gradient magnitude |∇I| = sqrt(Gₓ² + Gᵧ²).

NumPy’s np.hypot(gx, gy) is the numerically-stable form of np.sqrt(gx*gx + gy*gy) — it avoids intermediate overflow when both gradients are large [3]. The result is non-negative and orientation-independent: a vertical, horizontal, or 45° edge all produce the same magnitude for the same intensity step.

A four-panel comparison. Top-left: the original image with horizontal and vertical stripes. Top-right: Gx output showing only the vertical stripe edges. Bottom-left: Gy output showing only the horizontal stripe edges. Bottom-right: the magnitude combination showing all edges.
Fig. 3 `Gₓ` alone misses horizontal edges; `Gᵧ` alone misses vertical edges. The magnitude `√(Gₓ² + Gᵧ²)` is the orientation-independent combiner.

Concept 3 — Sobel vs Prewitt vs Roberts

Three classical operators dominated edge detection in the 1960s-70s before Canny arrived. They differ only in the kernel weights [4, 5, 6]:

OperatorYearKernel sizeGₓ weights
Roberts cross19652×2[[1, 0], [0, -1]] (diagonal pair only)
Prewitt19703×3[[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]] (equal weights)
Sobel19683×3[[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]] (centre row doubled)

Roberts is fastest but noisy because it only samples two pixels. Prewitt and Sobel have similar quality; Sobel’s centre-weighted rows give it slightly smoother edges and a tiny Gaussian-like noise filter built in. The Canny detector (1986) layered Gaussian smoothing, Sobel-or-Prewitt gradient, non-maximum suppression, and hysteresis thresholding on top — it remains the gold standard [7].

Exercises

Three exercises in Execute → Modify → Create order: run the geometric shapes, restrict to one direction or threshold, then implement Sobel on a custom pattern.

EXECUTE I.

Run the rectangle-and-circle test

Run simple_edge_detection.py from the downloads. The script generates the rectangle + circle test image, applies the Sobel pipeline, and saves the result.

Reflection questions

  • Why do the insides of the rectangle and circle appear black?
  • Are all four sides of the rectangle equally bright? Why or why not?
  • Why does the circle, whose boundary is curved (no axis-aligned edges), still show up cleanly?
MODIFY II.

Three Sobel variations

Edit simple_edge_detection.py for each goal.

Goals

  1. Vertical edges only — use |Gₓ| as the output, ignore Gᵧ.
  2. Horizontal edges only — use |Gᵧ|, ignore Gₓ.
  3. Threshold — keep only edge pixels brighter than 30% of the maximum; set the rest to zero.
CREATE III.

Sobel on a custom pattern

Build a procedural test image of your choice (a “T” shape, concentric rings, diagonal stripes — anything) and run the full Sobel pipeline on it. The output should clearly outline the pattern.

python · exercise3_starter.py
import numpy as np
from PIL import Image

H, W = 256, 256
image = np.zeros((H, W), dtype=np.float64)

# TODO 1: draw your pattern. Example: a "T" shape using rectangles.

# TODO 2: define Gx and Gy (you know what these are now).
# Gx = ...
# Gy = ...

# TODO 3: convolve and compute magnitude in nested loops.

# TODO 4: normalise to [0, 255] and save.

Image.fromarray(...).save('my_edges.png')

Make it your own

  • Swap Gx, Gy for the Prewitt kernels ([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]]). Compare the outputs — they should be very similar.
  • Apply Sobel to each colour channel of an RGB image separately and combine the three magnitudes with np.linalg.norm. The result is a colour-aware edge detector.
  • Pre-blur with the Gaussian kernel from 3.4.1 before running Sobel. This is the first stage of the Canny pipeline and produces much cleaner edges on noisy photos.

Downloads

simple_edge_detection.py — quick-start Sobel gradient_comparison.py — Gx/Gy comparison edge_detection_starter.py — Exercise 3 starter edge_detection_solution.py — Exercise 3 reference

Summary

Common pitfalls to avoid

  • Calling Gₓ the “horizontal-edge kernel” — it is the horizontal-gradient kernel, which detects vertical edges.
  • Using np.sqrt(gx*gx + gy*gy) instead of np.hypot — both work for moderate inputs, but hypot is numerically stable for very large or very small magnitudes.
  • Forgetting to normalise — the raw magnitude often peaks above 1000, and .astype(np.uint8) wraps modulo 256 instead of clipping.
  • Skipping the border — the 3×3 kernel cannot be centred on the outermost pixels. Either pad first (3.4.1) or loop from (1, 1) to (H-1, W-1) and accept a 1-pixel ring of black.
  • Working in uint8 — the multiplication overflows immediately. Cast to float64 before convolving.

References

  1. [1] Sobel, I., & Feldman, G. (1968). A 3×3 isotropic gradient operator for image processing. Talk at Stanford Artificial Intelligence Project (SAIL).
  2. [2] Gonzalez, R. C., & Woods, R. E. (2018). Digital Image Processing (4th ed.). Pearson.
  3. [3] NumPy Community. (2024). numpy.hypot. NumPy Documentation. numpy.org/hypot
  4. [4] Roberts, L. G. (1965). Machine perception of three-dimensional solids. In Optical and Electro-Optical Information Processing (pp. 159–197). MIT Press.
  5. [5] Prewitt, J. M. S. (1970). Object enhancement and extraction. In B. Lipkin & A. Rosenfeld (Eds.), Picture Processing and Psychopictorics (pp. 75–149). Academic Press.
  6. [6] Marr, D., & Hildreth, E. (1980). Theory of edge detection. Proceedings of the Royal Society of London. Series B, 207(1167), 187–217.
  7. [7] Canny, J. (1986). A computational approach to edge detection. IEEE Transactions on Pattern Analysis and Machine Intelligence, PAMI-8(6), 679–698. doi:10.1109/TPAMI.1986.4767851