Before this unit: 2D convolution

What Is an Edge? Step, Ramp and Roof, and the Two Derivatives That Find Them

An edge is a peak in the first derivative and a sign change in the second. Worked by hand on nine pixels: the peak lands on column 4 and the second difference is exactly zero there. Add noise of at most 0.03 and the zero-crossing moves by 0.05 px, which is small; the real cost of the second derivative is the sign changes noise makes on its own, with no size attached to threshold, which is why Canny is built on the first derivative.

Luis Condados · · Updated October 3, 2026 · 9 min read Beginner
One row of pixels across the frame, with its first and second derivatives underneath. The edge is a peak in the middle curve and a sign change in the bottom one.
One row of pixels across the frame, with its first and second derivatives underneath. The edge is a peak in the middle curve and a sign change in the bottom one.

Wherever one thing ends and another begins in a photograph, the brightness jumps. Read a single row of pixels and that jump is visible in the numbers themselves. Below, a yellow row sweeps down a frame of a match, and underneath is the brightness along it.

A frame of a pickleball court with a yellow horizontal line moving down it; below, a graph of brightness along that line, flat over the blue court with sharp spikes where the line crosses the white paint
Over the blue court the brightness is flat. Each white line it crosses is a narrow spike (a rise and a fall a few pixels apart) and a player’s white shirt against the court is a wider block. The lesson measures on a temple photograph; this is one frame of a match. Frames from “2026.07.25 WD Open … (Gold Medal match)” by pickleball4you, CC BY 3.0.

You have met the one-row view of an edge before:

  • Barcode scanners read one line of light and dark and turn the places where it switches into the bar widths.
  • Caliper tools in machine vision measure a part by looking along one line for the two places where brightness jumps.
  • Contrast autofocus in many cameras moves the lens until edges like these are as steep as they get.

An edge is a place where brightness changes fast, and “fast” has a derivative in it. On nine pixels worked by hand, the first derivative peaks at column 4 with a value of 0.30, and the second derivative crosses zero at the same pixel. Disturbing the row by no more than 0.03 moves the crossing by a twentieth of a pixel; what hurts the second derivative is the extra sign changes noise creates, which carry no size a threshold could reject.

Where we are

Lesson 1 of Edge Detection, and the criterion behind every step of its pipeline: before the image is smoothed, measured, thinned and linked, we need a definition of an edge a computer can evaluate. The only tool it needs is the sliding window from 2D convolution: every derivative below is a small kernel applied at each pixel.

Take the unit’s photograph and the silhouette of a column against the black backdrop. You can point at where the column ends; a program cannot. What it can do is read one row of pixels across that boundary and do arithmetic on the numbers.

An edge is a change, and a derivative measures change

Walk across that boundary and the intensities go from dark to bright. The rate of that change is the first derivative, and on a discrete row of pixels the cheapest honest estimate is the central difference: look one step either way and halve.

I′[x]  =  I[x+1]−I[x−1]2I'[x] \;=\; \frac{I[x+1] - I[x-1]}{2}

Do it twice and you have the second derivative, which measures how fast the rate itself is changing:

I′′[x]  =  I′[x+1]−I′[x−1]2I''[x] \;=\; \frac{I'[x+1] - I'[x-1]}{2}

The edge shows up in both, differently. Where the intensity climbs fastest, the first derivative reaches a peak. At that same place the climb stops accelerating and starts decelerating, so the second derivative passes through zero. One number to maximise, one to find a sign change in.

Not every edge is a cliff

A column against the backdrop has a sharp boundary. The border of a shadow on the base does not, and a thin highlight along a column’s edge is a third thing again. Gonzalez and Woods name the three profiles worth knowing [1]: a step, a ramp, and a roof.

stepramproofII′I″
A step is an idealisation no lens produces. A ramp is what a real boundary looks like once optics and sampling have blurred it, and its first derivative is a plateau rather than a spike, which is why real edge maps come out several pixels thick. A roof has no dark-to-bright transition at all: it is a thin bright line, so its first derivative crosses zero at the ridge and its second derivative has two sign changes, one on each flank.

The roof is the case that breaks a naive reading. If you look for a peak in the first derivative you will find two, one on each side of the ridge, and miss the ridge itself sitting between them.

Now you try

Pick a model, then widen it. Watch the first derivative of the ramp flatten into a plateau as the edge gets wider, and watch the roof produce two zero-crossings where the step produces one.

Then turn up the noise with smoothing still at zero, and read the crossing counter.

How close is this to OpenCV?

The worked example fits in four lines of NumPy, and there are two library functions that compute the same central difference: cv2.Sobel with ksize=1, which is the kernel [−1,0,1][-1, 0, 1] with no smoothing, and np.gradient. Run all three on the nine pixels.

The worked example in NumPy
import numpy as np
import cv2

row = np.array([0.20, 0.20, 0.20, 0.20, 0.50,
                0.80, 0.80, 0.80, 0.80])

def central(a):
    return (a[2:] - a[:-2]) / 2

d1 = central(row)  # columns 1..7
d2 = central(d1)   # columns 2..6
print("first: ", np.round(d1, 3))
print("second:", np.round(d2, 3))
print("peak at column", 1 + d1.argmax())
Output
first:  [0.   0.   0.15 0.3  0.15 0.   0.  ]
second: [ 0.075  0.15   0.    -0.15  -0.075]
peak at column 4
Against cv2.Sobel and np.gradient
noisy = np.array([0.22, 0.18, 0.21, 0.19, 0.50,
                  0.79, 0.82, 0.78, 0.81])

for name, r in [("clean", row), ("noisy", noisy)]:
    # a one-row image; [-1 0 1] halved
    img = r[None, :].astype(np.float32)
    cv = cv2.Sobel(img, cv2.CV_32F, 1, 0,
                   ksize=1)[0] / 2
    print(name)
    print("  ours ", np.round(central(r), 3))
    print("  cv2  ", np.round(cv, 3))
    print("  numpy", np.round(np.gradient(r), 3))
Output
clean
  ours  [0.   0.   0.15 0.3  0.15 0.   0.  ]
  cv2   [0.   0.   0.   0.15 0.3  0.15 0.   0.   0.  ]
  numpy [0.   0.   0.   0.15 0.3  0.15 0.   0.   0.  ]
noisy
  ours  [-0.005  0.005  0.145  0.3    0.16  -0.005 -0.005]
  cv2   [ 0.    -0.005  0.005  0.145  0.3    0.16  -0.005 -0.005  0.   ]
  numpy [-0.04  -0.005  0.005  0.145  0.3    0.16  -0.005 -0.005  0.03 ]

On the interior columns all three agree to the last digit, on both rows. They disagree at the two ends, and each disagreement is a choice about pixels that do not exist. Ours has no answer there, which is why it is two columns shorter. cv2.Sobel mirrors the row across its end, so column 0 of the noisy row reports 0.000. np.gradient switches to a one-sided difference and reports −0.04. Neither is a measurement of the scene; the same thing happens in two dimensions in lesson 2.

In the wild

The profiles above are drawn. Here is what an edge in a photograph measures.

Taking 400 rows at random from a temple cloister, finding the steepest transition in each, and measuring its equivalent width: the total rise divided by the steepest slope across it. For a blurred step that comes out at about 2.5σ, and unlike counting pixels between two thresholds it cannot be capped by the window it was measured in:

edges measured400
median width2.30 px
10th–90th percentile2.11 – 2.80 px
wider than 2 px98.8%
implied Gaussian σ0.92 px
A magnified crop of one edge in a stone cloister, beside the intensity profile of the row crossing it, which rises over several pixels rather than jumping
One edge, magnified, and the row that crosses it. The rise takes a couple of pixels, so it is a ramp. Source: “The Cloister Mandapam, in One point perspective” by Sindugab (Wikimedia Commons), CC0.

What it means: almost no edge in this photograph is a step. The median transition takes 2.3 pixels and 98.8% take more than two, which is the ramp model from earlier in this lesson, and it is not a defect. Lenses have a point-spread function, sensors integrate over a photosite’s area, demosaicing interpolates and the anti-alias filter blurs on purpose. Every one of those widens a boundary before you see it. A detector built for perfect steps has no work to do in the real world, which is why the operators in the rest of this unit all begin by choosing a scale.

Where this breaks

Take the same nine pixels and disturb each by no more than 0.03, less than one part in thirty and invisible to the eye:

column012345678
II0.220.180.210.190.500.790.820.780.81
I′I'-0.0050.0050.1450.3000.160-0.005-0.005
I′′I''0.0750.14750.0075-0.1525-0.0825

The first derivative’s peak is still 0.300 at column 4. The second derivative was 0.0000 at column 4 and is now 0.0075, so the sign change no longer happens at a pixel. It happens between columns 4 and 5, and interpolating in a straight line between the two values places it at

x=4+0.00750.0075−(−0.1525)=4+0.00750.16=4.05x = 4 + \frac{0.0075}{0.0075 - (-0.1525)} = 4 + \frac{0.0075}{0.16} = 4.05

What it means: noise you cannot see moved the zero-crossing by 0.05 pixels. That is a small error, and on its own it would be no reason to prefer one derivative over the other.

The real problem shows up on a longer row. On nine pixels the edge dominates the arithmetic; on a real row of several hundred, noise in the flat regions produces sign changes of its own, and a profile containing one edge reports dozens of crossings. The lab above shows it: set noise to 0.05 with smoothing at zero and read the counter. Every one of those crossings looks the same as the real one, because a sign change carries no size. A peak in the first derivative does carry one, and a threshold on it can throw the small ones away.

Two consequences run through the rest of the unit. Even in the clean case, the ramp’s plateau means a real gradient map is thick, so something has to decide which pixel of the ridge is the edge, which is the job of Canny’s non-maximum suppression, lesson 3. And the second derivative needs its noise handled some other way, which is why Canny is built on the first [2] and why the zero-crossing method has smoothing built into it rather than bolted on [3]; lesson 4 measures what that smoothing costs.

Exercises

  1. Measure the edge’s width. The “In the wild” table uses the equivalent width: the total rise divided by the steepest slope. Compute it for the nine clean pixels and store it in width.
Your turn
width = 0.0  # total rise / steepest slope
print(width)
  1. Predict, then run. Spread the same rise over four pixels instead of two: 0.20 0.20 0.20 0.35 0.50 0.65 0.80 0.80 0.80. Before running, write down the first derivative’s peak value, how many columns reach it, and the width.
  2. Find the roof’s ridge. Put a single bright pixel in a dark row, 0.2 0.2 0.2 0.2 0.8 0.2 0.2 0.2 0.2, and look for the ridge with the first derivative’s peak. Where does the peak land, and what does the first derivative say at column 4 itself?
What you should see

These come from the “What is an edge?” cells of the unit notebook, edge_detection.ipynb, and are the same on OpenCV 4.11 (this page) and 5.0.

  1. width = 0.6 / 0.3 = 2.0 pixels: the two-pixel transition the row was built with.
  2. A peak of 0.15, held over three columns (3, 4 and 5), and a width of 4.0. Half the slope, twice the width, and a plateau where there was a spike: the ramp in the figure above.
  3. Two peaks, +0.3 at column 3 and −0.3 at column 5, and 0 at column 4, where the ridge is. A peak search finds the flanks and misses the line, which is why a roof is detected by a zero-crossing of the first derivative.

Check yourself

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

  1. On the nine clean pixels the first derivative peaks at column 4 with 0.30. What does the second derivative do at that column?

  2. On a long noisy row, why is a zero-crossing of the second derivative harder to clean up than a peak of the first?

  3. A thin bright line in a dark row is a roof. Where does a peak search on the first derivative find it?

  4. In the cloister photograph the median edge is 2.30 px wide and 98.8% of edges are wider than 2 px. What does that say?

Next

We have a definition and no way to apply it to an image. A row of pixels is not a photograph, and the operator has to work in two directions at once. Lesson 2 builds it, and answers why three operators that compute the same derivative behave differently on a noisy photo.

References

[1] Gonzalez, R. C., & Woods, R. E. (2018). Digital Image Processing (4th ed.), section 10.2 “Point, Line, and Edge Detection”, Edge Models, p. 772. Pearson.

[2] 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

[3] 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