Before this unit: 2D convolution

Image Gradients: Central Differences, Prewitt and Sobel

Three operators compute the same derivative and behave differently on a noisy photo. Worked by hand on a 3x3 patch: Sobel gives Gx = Gy = 1.80, a magnitude of 2.55 and an orientation of 45 degrees, while Prewitt gives 1.20 and 1.70 on identical pixels. Measured across a noise sweep, Prewitt beats Sobel at every level, and the reason is that its normalised weights have a smaller sum of squares.

Luis Condados · · Updated October 3, 2026 · 10 min read Intermediate
The same pixels through a central difference, Prewitt and Sobel, each keeping its strongest 5%. The operators disagree about where the information is.
The same pixels through a central difference, Prewitt and Sobel, each keeping its strongest 5%. The operators disagree about where the information is.

The gradient says, at every pixel, how quickly brightness changes and in which direction. Below, each pixel of a match is coloured by that direction and made brighter the stronger the change is.

A pickleball rally rendered as glowing coloured outlines on black: the two sidelines in two different colours, the baseline in a third, the players outlined in shifting colours
The colour wheel in the corner is the key. The two sidelines run different ways, so they take different colours, and each keeps its colour along its whole length; a player’s outline cycles through the wheel as it curves. The lesson measures on a temple photograph; this is the same operator on video. Frames from “2026.07.25 WD Open … (Gold Medal match)” by pickleball4you, CC BY 3.0.

The direction as well as the size of the change is used in:

  • Emboss and relief filters, which are a derivative taken in one chosen direction.
  • Focus peaking on mirrorless cameras, which paints the pixels with the strongest gradient so you can see what is sharp.
  • Pedestrian detectors built on HOG, which describe a region by a histogram of its gradient directions.
  • SIFT, whose descriptor is also built from gradient directions (unit 3.3).

A photograph has edges running every way, so the change has to be measured in two directions at once and combined into a strength and a direction. On one 3x3 patch Sobel returns a magnitude of 2.55 at 45 degrees. Across a noise sweep Prewitt recovers 0.7968 of its clean edge set at sigma = 20 against Sobel’s 0.7814, and the reason is one line of arithmetic.

Where we are

Lesson 1 defined an edge as a peak in the first derivative, on one row of pixels. A photograph is not one row. A column’s sides run vertically, the base runs horizontally, and the top of the temple is a staircase of both. A single row of differences finds the vertical edges and misses the horizontal ones entirely. This is lesson 2 of the Edge Detection unit, the step of its pipeline that measures the change at every pixel, and every operator in it is a 3x3 kernel slid over the image in the sense of 2D convolution.

Two directions, then combine

Measure the derivative twice, once horizontally and once vertically, and treat the pair as a vector. Its length says how strong the change is and its direction says which way the change points:

∣∇I∣=Gx2+Gy2θ=arctan⁡ ⁣(GyGx)|\nabla I| = \sqrt{G_x^2 + G_y^2} \qquad \theta = \arctan\!\left(\frac{G_y}{G_x}\right)

The gradient points across the edge, uphill toward the bright side, so the edge itself runs perpendicular to θ\theta. That direction is what lesson 3 needs in order to thin a ridge, and it is carried forward again by every feature detector built on gradients [4], so orientation is not decoration here.

Measuring GxG_x with the central difference from lesson 1 works, and it uses three pixels. The operators in practice use nine. The extra six are there to average out the rows either side of the one being differenced, and how they are averaged is the entire difference between the named operators [1]. The best known of them was never published as a paper: Sobel and Feldman presented it as a talk in 1968 and it reached print five years later, in Duda and Hart’s textbook [3].

Sₓ-101-202-101=121×-101average down the columnsdifference across the rowSᴪ-1-2-1000121=-101×121difference down the columnsaverage across the row
Each Sobel kernel is an outer product: a smoothing vector in one direction times a difference vector in the other. That is the separability from the 2D convolution post, and it is why a 3x3 gradient costs six multiplies per pixel rather than nine. Prewitt is the same picture with the smoothing vector changed from [1 2 1] to [1 1 1].

Now you try

First the mechanics: slide the kernel one window at a time and watch the multiply-accumulate that produced the numbers above.

Input
Kernel 3×3
Output

Then the same kernel on a real image. Switch between sobelx and sobely and notice that each is blind to edges running along its own direction, which is why you need both and then the magnitude.

Kernel
Original
Filtered

How close is this to OpenCV?

Writing Sobel yourself takes two lines of NumPy, and comparing it with cv2.Sobel is the quickest way to know you have the mechanism right. The numbers elsewhere in this lesson come from the temple photograph, run in the unit’s notebook. These cells use a shape whose answer is known before you run anything: a white disc on black, where the gradient on the rim has to point into the disc.

Sobel in NumPy
import numpy as np
import cv2

# a white disc on black
img = np.zeros((160, 160), np.float32)
cv2.circle(img, (80, 80), 50, 1, -1)

def sobel_x(a):
    # difference along each row
    d = a[:, 2:] - a[:, :-2]
    # [1 2 1] down the columns
    return d[:-2] + 2*d[1:-1] + d[2:]

def sobel_y(a):
    # the same kernel, turned
    return sobel_x(a.T).T

gx, gy = sobel_x(img), sobel_y(img)
mag = np.hypot(gx, gy)
show(mag / mag.max(), "our gradient")
print("ours:", gx.shape)
Output
ours: (158, 158)
our gradient
our gradient
Against cv2.Sobel
ref_x = cv2.Sobel(img, cv2.CV_32F, 1, 0, ksize=3)
ref_y = cv2.Sobel(img, cv2.CV_32F, 0, 1, ksize=3)
print("OpenCV:", ref_x.shape)

# compare inside the 1-pixel border
ix = ref_x[1:-1, 1:-1]
iy = ref_y[1:-1, 1:-1]
print("max diff Gx:", np.abs(gx - ix).max())
print("max diff Gy:", np.abs(gy - iy).max())

rim = {"right": (80, 130),
       "left": (80, 30),
       "top": (30, 80),
       "bottom": (130, 80)}
for name, (r, c) in rim.items():
    x, y = ref_x[r, c], ref_y[r, c]
    a = np.degrees(np.arctan2(y, x))
    # ours is one pixel smaller per side
    mine = gx[r - 1, c - 1]
    print(f"{name:6} Gx={x:+.0f} "
          f"Gy={y:+.0f} angle={a:+.0f} "
          f"ours={mine:+.0f}")
Output
OpenCV: (160, 160)
max diff Gx: 0.0
max diff Gy: 0.0
right  Gx=-4 Gy=+0 angle=+180 ours=-4
left   Gx=+4 Gy=+0 angle=+0 ours=+4
top    Gx=+0 Gy=+4 angle=+90 ours=+0
bottom Gx=+0 Gy=-4 angle=-90 ours=+0

Inside the image the two agree to the last bit. The shapes do not: ours is 158x158 because a 3x3 window centred on the outermost ring hangs off the image, and OpenCV returns 160x160 because it fills the missing pixels by reflecting the image across its border. On the right-hand rim OpenCV reports Gx=−4G_x = -4 and an angle of 180°: the gradient points left, uphill into the bright disc, and the 4 is the sum of the [1 2 1] weights times a step of 1. That unnormalised scale is the same one that made Sobel’s magnitude 1.5x Prewitt’s on the patch above.

Which averaging wins, measured

The three operators differ only in the smoothing vector: none, [1 1 1], [1 2 1]. Running all three on the canonical frame with noise added, at a matched budget of the strongest 5% of pixels, and asking how much of the clean-image edge set survives:

operatorsigma = 2sigma = 5sigma = 10sigma = 20
central difference0.96620.91580.82650.6262
Prewitt0.98140.95090.89950.7968
Sobel0.98020.94720.89480.7814

Averaging is worth 17 points at sigma = 20, which is the expected result. The unexpected one is that Prewitt leads Sobel at every level. The gap is small against the run-to-run spread at low noise (1.5 times the larger standard deviation of the two at sigma = 2: a gap of 0.0012 against standard deviations of 0.0008 and 0.0004) and grows to 3.4 times at sigma = 20 (0.0154 against 0.0033 and 0.0045). The standard deviations are under noise_robustness in output/edge_numbers.json. No single level settles it on its own; what carries the claim is that the ordering is the same at all four.

The mechanism is arithmetic. Noise passed through a weighted average has its variance scaled by the sum of the squared weights. Normalise each smoothing vector so it preserves brightness and compare:

Prewitt: (13)2 ⁣×3=13=0.3333Sobel: (14)2 ⁣+(12)2 ⁣+(14)2=38=0.375\text{Prewitt: } \left(\tfrac{1}{3}\right)^2 \! \times 3 = \tfrac{1}{3} = 0.3333 \qquad \text{Sobel: } \left(\tfrac{1}{4}\right)^2 \!+ \left(\tfrac{1}{2}\right)^2 \!+ \left(\tfrac{1}{4}\right)^2 = \tfrac{3}{8} = 0.375

A box average spreads its weight evenly, and for a fixed support that is the arrangement with the smallest sum of squares. Prewitt therefore passes 0.3333/0.375=0.8890.3333 / 0.375 = 0.889, about 11% less noise power, than Sobel. What Sobel’s [1 2 1] buys instead is a response that varies less as an edge rotates, which matters when you need the orientation and not only the magnitude.

Scope: one image, one edge budget, synthetic Gaussian noise, five runs per level. The ordering was consistent at every level tested, which makes variance an unlikely explanation and does not make it a law.

In the wild

Gradients have a direction as well as a size, and on a real subject that direction is not uniformly distributed. Buildings are the clearest case: they are built plumb and level, so their edges should pile up at two orientations ninety degrees apart.

Taking the strongest 5% of gradients in the cloister and histogramming their orientation over 0–180°:

share of strong gradients
within ±10° of the peak (0°)53.3%
within ±10° of the perpendicular (90°)12.2%
those two bands together65.5%
what a uniform distribution would give23.3%
The cloister photograph beside a histogram of gradient orientation, with a tall spike at zero degrees and a smaller one at ninety
Where the edges point. Two narrow bands, 42 of the 180 possible degrees between them, hold two thirds of the strong gradients. Source: “The Cloister Mandapam, in One point perspective” by Sindugab (Wikimedia Commons), CC0.

What it means: two bands covering 23% of the possible orientations hold 65.5% of the strong-gradient pixels, nearly three times what chance would give. That is the building, recovered from nothing but first derivatives. It is also the practical reason the orientation half of a gradient is worth keeping: a magnitude tells you that something changed, and it is the angle that says the change belongs to a column rather than to a shadow. Every method later in the track that bins orientations, from Hough to SIFT’s descriptor, is living on this histogram.

The asymmetry between the two peaks (53% against 12%) is not the operator preferring one axis: Sobel’s GxG_x and GyG_y are the same kernel rotated. I did not measure what causes it. Two explanations fit and I cannot separate them from this artifact: the frame may hold more vertical structure than horizontal, and in a one-point perspective shot the horizontal lines that run down the corridor converge toward the vanishing point, so they leave the 90° band and spread across other orientations.

Where this breaks

We have a magnitude per pixel. Turning that into edges means picking a threshold, and no value of it works. Sweeping the threshold on the canonical frame:

thresholdmagnitude pxthinned pxridge thicknesscoverage
1076,26614,9465.10 px1.000
2060,20212,6514.76 px0.846
4040,3789,5964.21 px0.642
8020,3885,7293.56 px0.383
1606,6232,2952.89 px0.154

Ridge thickness is the magnitude pixels divided by the thinned pixels at the same threshold, so it is the average width of an edge you get back. Coverage is how much of the lowest threshold’s edge structure survives.

At threshold 10 an edge comes back 5.10 pixels wide, which is the ramp’s plateau from lesson 1 showing up in two dimensions. Raise the threshold to 160 and the ridge narrows to 2.89 pixels, but you are down to 15.4% of the edge structure. Thin and complete are not available from the same number.

One correction to the usual telling, since the sweep contradicts it: raising the threshold does not fragment the edges. The fragment count falls from 2,269 to 213 across this sweep, because weak structure disappears outright rather than breaking apart. The cost of a high threshold is coverage, not fragmentation.

Two separate problems, then. Raising the threshold does narrow the ridge, but only by discarding coverage, and it never gets close to one pixel: at 160 the ridge is still 2.89 pixels wide with 85% of the structure gone. Thinness cannot be bought with the threshold, so it needs a mechanism of its own, and coverage needs another. Lesson 3 supplies both, which is why Canny is two ideas.

The border is a smaller version of the same lesson: a number OpenCV returns is not always a measurement. On a plain left-to-right ramp, whose true gradient is the same everywhere, cv2.Sobel returns 8 in the interior and 0 on the first and last columns. Reflecting a ramp across its edge makes a peak that is not in the scene, so the outermost pixels of any gradient image report the border rule and not the picture. Crop them, or pick the border mode on purpose.

Exercises

  1. Match OpenCV everywhere, on a ramp. Make gx_full, the same size as the image, so that it equals cv2.Sobel on every pixel including the border. np.pad adds the missing ring; one of its modes copies OpenCV’s default border.
Your turn
# brightness rises 1 per column
ramp = np.tile(np.arange(64.0), (64, 1))
ramp = ramp.astype(np.float32)

# 62x62 so far: pad ramp first
gx_full = sobel_x(ramp)
print(gx_full.shape)
  1. Predict, then run. Change the 2 in sobel_x to a 1, which makes it Prewitt. Before running both cells again, write down what ours on the right-hand rim becomes, and what happens to the largest difference.
  2. Test a claim this lesson makes. “Which averaging wins” says Sobel’s [1 2 1] buys a response that varies less as an edge rotates. Draw a straight edge at 0°, 15°, 30° and 45° (a ramp along a rotated axis works), normalise both kernels to sum to 1, and compare the largest magnitude each operator returns at each angle.
What you should see

These numbers come from section 2c of the unit notebook, edge_detection.ipynb, and are the same on OpenCV 4.11 (this page) and 5.0.

  1. Only mode="reflect" passes. NumPy’s "reflect" mirrors without repeating the edge pixel, which is OpenCV’s BORDER_REFLECT_101; "edge" and "symmetric" are both off by 4 on the border columns.
  2. ours=-3 while OpenCV still says −4-4, and the largest difference becomes 1.0 in both GxG_x and GyG_y (sobel_y reuses sobel_x): the weights now sum to 3.
  3. On a ramp two pixels wide, the peak falls from 1 at 0° to 0.854 at 45° for Sobel and to 0.805 for Prewitt, a drop of 14.6% against 19.5%. The claim holds here, on a drawn edge. The noise result runs the other way: on Gaussian noise with a standard deviation of 0.1, Sobel’s GxG_x has a standard deviation of 0.0866 and Prewitt’s 0.0816, a ratio of 0.94, which is 0.889\sqrt{0.889} from the arithmetic above.

Check yourself

Four questions on this lesson. Pick an answer to see why it is right or wrong.

  1. On the same 3x3 patch, Sobel gives a gradient magnitude of 2.55 and Prewitt gives 1.70. What does that tell you?

  2. A white disc sits on a black background. At a pixel on the right-hand rim of the disc, which way does the gradient point?

  3. Why does Prewitt let less noise through than Sobel?

  4. You raise the threshold on the gradient magnitude from 10 to 160. What happens to the edges?

Next

The gradient is measured; the ridge it gives is five pixels wide and no threshold makes it one. Lesson 3, Canny, adds the two steps that fix it: thin the ridge to its peak, then keep weak pixels only where they connect to strong ones [5]. The other way to measure the change, the second derivative Marr and Hildreth built on [2], comes after it in lesson 4.

References

[1] Gonzalez, R. C., & Woods, R. E. (2018). Digital Image Processing (4th ed.), section 10.2 “Point, Line, and Edge Detection”, Gradient Operators, p. 779. Pearson. Covers the Prewitt and Sobel masks and the magnitude/orientation definitions used here.

[2] Marr, D., & Hildreth, E. (1980). Theory of edge detection. Proceedings of the Royal Society of London B, 207(1167), 187-217. doi:10.1098/rspb.1980.0020

[3] Sobel, I., & Feldman, G. (1968). A 3x3 Isotropic Gradient Operator for Image Processing. Presented at the Stanford Artificial Intelligence Laboratory; never formally published. It first reached print in Duda, R. O., & Hart, P. E. (1973). Pattern Classification and Scene Analysis, pp. 271-272. Wiley. Sobel later published his own account as An Isotropic 3x3 Image Gradient Operator (2014). Citations of a 1968 Duda and Hart book are a conflation of the talk’s year with the book’s; the book is 1973.

[4] Szeliski, R. (2022). Computer Vision: Algorithms and Applications (2nd ed.), ch. 7 “Feature Detection and Matching”. Springer. Free PDF

[5] Canny, J. F. (1986). A Computational Approach to Edge Detection. IEEE Transactions on Pattern Analysis and Machine Intelligence, 8(6), 679–698. doi:10.1109/TPAMI.1986.4767851