BookNotesSlidesP-sets Syllabusdraft · v0.2.49.1
◀▲⌂▶
💬Comments welcome. To leave a note, select any text and click the note / highlight button that pops up — or open the panel with the tab at the top-right (‹). Notes are visible only inside our private review group.
💡 In a hurry? Jump to this chapter’s 4 big lessons ↓

6.2 Convolution⧉

📎 Problem set

PS2 implements box/Gaussian blur, gradients, sharpening, and the bilateral filter. → Chapter 1.5, Problem sets (appendix).

A blur softens an edge by mixing neighboring values. A derivative measures their differences, and a sharpener adds some of those differences back. All three can use one table of weights applied throughout the image, a convolution kernel.

Blurring is useful before downsampling, for noise reduction, and for building image pyramids. It also models some optical degradation. A single kernel is appropriate only when the point spread is the same across the region being modeled; depth changes and camera motion often require a more general operator.

Sharpening increases local contrast. It can make surviving detail easier to see, but it is not generally the inverse of the blur that formed an image.

6.2.1 Convolution as a weighted sum of neighboring pixels⧉

A kernel assigns a weight to each displacement between an input and an output sample. Nonnegative weights summing to one form a local average. Derivative and sharpening kernels can have negative weights, so convolution is more general than averaging.

It is easiest to see in one dimension first (Figure 6.2.1), where the whole operation is a 3-tap kernel sliding along a row of pixels: each output pixel is the weighted sum of the three input pixels under the kernel, and nothing more.

fig-convolution-slide-1d
Figure 6.2.1. Convolution in 1-D: a 3-tap kernel sliding along a row of pixels. The input signal is a row of little square pixels (top), the kernel is three weights that slide over it (middle), and the output is another row (bottom). Click an output pixel to select it: its three-pixel input footprint lights up, the kernel sits over that footprint, and the weighted sum is written out. Click an input pixel in the footprint to isolate the single term it contributes; a pixel outside the footprint contributes nothing. Drag a kernel tap to change a weight, pick a preset (box, Gaussian, sharpen, derivative, shift, identity), or press "animate" to sweep the kernel across every output. The input can be one of several test signals (step, box, impulse, ramp, sine, noise), each with its own slider, and a resolution slider sets the length. This is the whole idea of convolution with the two-dimensional bookkeeping stripped away.

The two-dimensional case is the same thing with a small grid of weights instead of a row (Figure 6.2.2).

fig-convolution-slide
Figure 6.2.2. A kernel sliding over an image. At each output location the kernel (a small grid of weights, here a 3×3 box) is laid over the matching input neighborhood; each input pixel is multiplied by the weight resting on it, and the products are summed into the single output pixel beneath the kernel's center. The same weights are used at every position, and that sameness is what makes it a convolution. Click any output pixel to light up its neighborhood and see its weighted sum written out, then click an input pixel inside that footprint to isolate the single term it contributes (a pixel outside the footprint contributes nothing). Drag a kernel cell up or down, or pick a preset (box, Gaussian, sharpen, edge, shift, identity), to change the weights and watch every output pixel follow; the shift kernel makes shift-invariance concrete by translating the whole image by one pixel. Press "animate" to sweep the selection across every output. The input can be a photo or a synthetic test pattern with its own sliders, and a resolution slider sets the grid size.

Two properties of this scheme are important. First, it is linear: each output pixel is a weighted sum of input values with fixed weights, so scaling the input scales the output, and adding two images adds their outputs. Second, it is shift-invariant: the same kernel applies at every pixel, so the operation does not care where in the image you are. Formally, writing $S_d$ for a shift by $d$ pixels, a shift-invariant operation commutes with shifting, $F(S_d\,x) = S_d\,F(x)$: shift the input and the output just shifts to match. Such an operation is linear and shift-invariant (LSI). This characterization assumes an infinite grid or compatible periodic boundaries; finite-image extensions require separate attention below.

Both conditions are real constraints, and it helps to watch one break. A graduated neutral-density filter (the kind that darkens the sky at the top of the frame while leaving the foreground alone) multiplies each pixel by a factor that depends on its row, say $(1 - y/y_\max)$. That is still linear (scaling the input scales the output), but it is emphatically not shift-invariant: slide the image down and the darkening falls on different content. So it is a linear operation that is not a convolution. Convolution is the well-behaved special case where the rule is identical everywhere, and that uniformity is exactly what later lets the Fourier transform pry it open.

💡 Big lesson (L6.4) — a convolution is a shift-invariant weighted sum

A convolution uses fixed weights that depend on displacement, not on image values or absolute position. On an infinite or periodic grid it is linear and commutes with shifts. The kernel can be small or span the image. Its support determines the work of direct filtering; finite boundaries determine where the shift-invariance model applies.

A linear model lets us study which patterns a measurement preserves and which it attenuates. Physical mixing must act on linear-light values. Filtering encoded values is a different operation, even when the same arithmetic is used; Chapter 4.3, Encoding: linear, gamma or log explains the distinction.

The algorithm itself is a short loop. Zero the output, then for every output pixel, walk over the kernel, multiply each input neighbor by its weight, and accumulate:

Algorithm 6.2.1
for each output pixel (x, y):
    out[x, y] = 0
    for each kernel offset (x', y'):
        out[x, y] += in[x + x', y + y'] · g[-x', -y']

assuming the kernel's coordinates are centered on zero. The minus signs on the kernel coordinates are the flip: each input neighbor is multiplied not by the weight at its own offset but by the weight at the opposite one. That flip is exactly what separates convolution from correlation, and why it matters is the subject of the next section.

Video 6.2.1. Video: But what is a convolution?, by 3Blue1Brown (YouTube).

6.2.2 The flip: where-from vs. where-to⧉

The minus signs in the loop above, the kernel read at g[-x', -y'] rather than g[x', y'], are the flip. The textbook definition writes the same operation by subtracting the offset on the kernel:

$$ (I * g)(x) = \sum_{x'} I(x')\, g(x - x'). $$

In words: to get the output at $x$, sum over all input positions $x'$, each weighted by the kernel evaluated at $x - x'$, the kernel is flipped (and slid) before it is multiplied in. Why the flip, and does it matter?

The flip is the difference between where light goes to and where light comes from. The physical, forward model says light at scene position $x$ lands, blurred, at sensor positions $x + x'$ for the offsets in the kernel; light spreads outward. But when we compute the output at a fixed pixel $x$, we are asking the backward question: which input pixels contributed here? The answer is the ones at $x - x'$. The minus sign is nothing but the bookkeeping of inverting "where-to" into "where-from."

fig-conv-1d
Figure 6.2.3. Convolution in one dimension. The (flipped) kernel slides across the signal and each output sample is a weighted sum of nearby inputs. A box or Gaussian kernel smooths; a zero-sum (derivative) kernel responds to change rather than level; a center spike minus a blur sharpens. Switch the input or drag the kernel taps to see the output respond.
fig-convolution-flip
Figure 6.2.4. The flip: where-from vs. where-to (in 1-D). The forward model spreads light from one input location out to a neighborhood (where-to); computing one output pixel asks the reverse, which inputs landed here (where-from), which flips the kernel, $g(x - x')$ rather than $g(x + x')$. For a symmetric kernel (a box, a Gaussian) the flip is invisible; for an asymmetric one (a derivative, an off-center shift) it reverses the result, so the sign matters.
fig-convolution-probability
Figure 6.2.5. Box ⊛ box = triangle (Laplace's first convolution). Left, the flat distribution of a single fair die, a one-dimensional box. Right, the distribution of the sum of two dice, the box convolved with itself: a triangle peaking at 7, because there are more ways to make a middling total than an extreme one. The $x - x'$ flip is forced here (given a target sum, the second die must read the total minus the first), so convolution is the logic of adding independent variables, not a convention.

The same point is easy to miss with real dice. A single twelve-sided die and a pair of six-sided dice both top out at a total of twelve, yet they are nothing alike: the d12 is uniform (every total equally likely), while two d6 make a triangle, because their sum is a box convolved with a box (Figure 6.2.6).

fig-dice-12-vs-2d6
Figure 6.2.6. One d12 is not two d6. Both reach a maximum total of twelve, but the distributions differ. A single twelve-sided die is uniform: every total from 1 to 12 is equally likely. The sum of two six-sided dice is triangular, peaked at 7, which is six times as likely as 12, because that sum is a box convolved with a box. Convolving two uniforms does not give a uniform.

For a symmetric kernel, convolution and real-valued correlation agree. For an asymmetric kernel, omitting the flip changes the operation; a derivative may reverse sign and a shift may reverse direction. Correlation is useful for matching a template against an image. State which convention the code uses rather than inferring it from a function name.

Test the convention with an asymmetric kernel rather than a Gaussian. A kernel with one nonzero tap displaced from its center should shift a single bright pixel in a predictable direction. Flipping the kernel reverses that direction. With a finite-difference kernel, the sign of a rising step distinguishes the two conventions. A symmetric blur cannot expose the mistake because it is unchanged by the flip. This is why a filter test suite needs a directional example even when the application mainly uses symmetric kernels.

A convolutional neural network does not flip. A "convolution" layer computes the correlation of its input with a learned kernel, which is what almost every deep-learning framework implements under that name. Nothing is lost by it: the kernel is fitted to data, so whichever orientation the layer needs, training finds it, and a flipped kernel is just as learnable as its mirror. The name is inherited from signal processing rather than earned. The distinction comes back in two places. One is at the boundary with classical code, where a kernel taken from a paper or from a library that does flip has to be flipped to match. The other is in the backward pass: the gradient of a correlation with respect to its input is a convolution, which is where the transposed convolution used in decoders and upsampling layers comes from. Chapter 7.3, Deep learning develops the layers themselves.

fig-conv-random-sum
Figure 6.2.7. The flip, on two distributions that are not symmetric. The distribution of X + Y is the convolution of the two distributions, so P(X + Y = k) sums P(X = i)·P(Y = k − i): Y reversed and slid until its zero sits under i = k. The middle row lines the two up, with their products and one term highlighted, and the sum is the bar below. Drop the flip and the same sums give the distribution of X − Y instead, drawn as outlines over the right answer. Two fair dice hide all of this, since a symmetric distribution is its own reflection. Interactive: drag any bar to reshape either distribution, pick k, and switch the flip off.

6.2.3 Convolution as a sum of translated images⧉

Everything so far has read convolution locally: fix an output pixel and gather a weighted sum of its neighbors. The same operation has a second, global reading. Instead of building the output one pixel at a time, build it one kernel tap at a time: each tap takes the whole input image, shifts it, scales it by the tap's weight, and adds it into the running total. Read this way, the output is a weighted sum of translated copies of the input, one copy per kernel tap.

The two readings are the very same double loop, only nested in the opposite order. Both zero the output, then run over every (output pixel, kernel offset) pair and add in[x + x', y + y'] · g[-x', -y'] each time. The one change is which for each line is the outer one; the body is identical. Coloring the output-pixel loop (blue) and the kernel-offset loop (red) makes the swap plain:

Local, the output-pixel loop on the outside:

Algorithm 6.2.2
out ← 0
for each output pixel (x, y):
    for each kernel offset (x', y'):
        out[x, y] += in[x + x', y + y'] · g[-x', -y']

Global, the kernel-offset loop on the outside:

Algorithm 6.2.3
out ← 0
for each kernel offset (x', y'):
    for each output pixel (x, y):
        out[x, y] += in[x + x', y + y'] · g[-x', -y']

In the local version the inner loop assembles one output pixel from all the taps; in the global version each pass of the outer loop adds one scaled, translated copy of the whole input, so the output grows as a sum of shifted images, $\text{out} = \sum_{x', y'} g[-x', -y']\,\cdot\,\text{shift}(\text{in},(x', y'))$. Same result, accounted from opposite ends. But the global reading buys three things the local one hides.

A fixed weighted sum of warped images is still linear in image intensity when the warps and interpolation weights are fixed. Pure camera rotation produces projective warps (homographies); roll alone is an in-plane rotation. Replacing shifts with these warps models motion that a single convolution cannot capture. Translation through a scene with varying depth also requires depth and visibility.

The global form also explains Fourier analysis: a complex exponential is multiplied by a scalar under each shift, so a weighted sum of shifts multiplies it by the weighted sum of those scalars. Chapter 6.3, Fourier develops this frequency response.

Translations commute on an infinite or periodic grid, so their weighted sum commutes with translation. Cropping or re-extending a finite image between operations can break this algebra.

fig-conv-as-translated-sum
Figure 6.2.8. Convolution, read two ways. Same operation, dual viewpoints. Scatter (the global view): the output is the sum of nine copies of the whole input, each shifted by one kernel tap and scaled by that tap's weight; the nine shifted, weighted copies are laid out and added. Gather (the local view): each output pixel is the weighted sum of its own neighborhood, which a button flips to; it shows a 5×3 patch of output pixels, each as its own tight 3×3 of weighted neighbor squares, so you see the same kernel slid across many positions (the patch fades at its edges to say it keeps going). Drag a copy, or the center grid's squares, up or down to change a weight; pick a preset (box, Gaussian, sharpen, edge); or, in local mode, press "animate" to sweep the patch across the image. The point is that the output literally is a weighted sum of translated inputs, the same fact whether you read it per output pixel or per shift.
💡 Big lesson (L6.5) — convolution is a weighted sum of shifted images

Convolution can be computed per output pixel or per kernel tap. In the first order we accumulate a neighborhood; in the second we accumulate shifted copies of the image. The result is the same. Replacing shifts with known warps gives other linear operators, but they need not share convolution’s commutativity or Fourier diagonalization.

6.2.4 Properties: the impulse, normalization, symmetry, commutativity⧉

A handful of properties make convolution pleasant to work with, and the slickest way to expose them is to feed the filter the simplest possible input: one bright pixel on a black background, the impulse $\delta$ (the discrete Dirac / Kronecker delta, one at the origin, zero everywhere else).

Convolve an impulse with a kernel and you get the kernel back, stamped at the impulse's location. The flip explains why: every output pixel asks "where did my light come from?", and the only nonzero input is the single bright pixel, so each output simply picks up the kernel weight that connects it to that pixel, tracing out the kernel. This output carries a name borrowed from optics: the point spread function (PSF), or impulse response. It is literally how the system spreads a single point of light, the very blob you saw around the streetlight. So the kernel, the impulse response, and the PSF are three names for one thing, with an immediate practical corollary: to discover what a filter does, feed it an impulse and read off the kernel. This is the single best debugging input for convolution code, and we will lean on it.

fig-psf-impulse
Figure 6.2.9. Impulse in → kernel out. Convolving a single bright pixel (an impulse $\delta$, left) with a kernel reproduces the kernel itself at that location (right): the output is the point spread function / impulse response, how the system spreads one point of light. Because the impulse isolates a single input, the output traces the kernel directly, which is why an impulse is the canonical test input for any convolution.

A straight step edge gives another measurement. Under locally shift-invariant blur, differentiating its edge-spread function gives the line-spread function, the PSF integrated along the edge direction. One edge orientation gives one projection, not the full two-dimensional PSF. A natural edge also includes its original softness, texture, noise, and earlier processing; controlled slanted-edge targets are used for quantitative MTF measurement.

fig-psf-edge
Figure 6.2.10. Read the impulse response off an edge. A real photo is blurred by a known Gaussian ($\sigma = 4$); a scanline across a crisp edge (the dark bird against a smooth background) gives the edge-spread function (center, the blurred step). Differentiating it (right) recovers the line-spread function, the PSF's 1-D projection, which lands squarely on the Gaussian actually applied. An edge is the integral of an impulse, so its derivative reads off the kernel, no isolated bright pixel required.

The identity element follows at once: convolving with the impulse leaves the image untouched, $I * \delta = I$, because reading "where-from" with a kernel that points only at yourself just copies you. The remaining properties:

6.2.5 Nitty-gritty: finite images⧉

Two practical wrinkles before the kernels themselves.

First, the kernel must be centered. Our images place $(0, 0)$ at the top-left corner, but a blur kernel is naturally centered on the pixel it is computing, its weights run from $-r$ to $+r$ around the middle, where $r$ is the kernel radius, so the full size is $(2r+1) \times (2r+1)$. An off-by-one here shifts the whole image by a pixel; an impulse test catches it instantly, because the blob lands off-center.

A finite image needs an extension rule: zero, repeated edge values, reflection, or periodic wrapping. Zero padding darkens a blurred border; reflection and clamping preserve constants but make different assumptions about missing content. Reflection conventions also differ in whether the edge sample is repeated. Use one declared convention in both the model and the implementation. Allocate a separate output buffer so earlier writes do not alter later inputs.

For a finite operator $A$, its transpose must reverse both filtering and boundary handling. A symmetric kernel does not by itself make a clamped or reflected implementation self-adjoint. This distinction matters when the filter appears inside an inverse solver.

fig-conv-boundary
Figure 6.2.11. What the four boundary rules do to a photograph. The padding the kernel reads beyond the frame is drawn around the picture at its true width, R = 3σ pixels, so the band the rule affects is exactly that: zero pads with black and darkens the border, clamp repeats the edge pixel and smears it inwards, mirror reflects the picture about its own edge, and wrap reads from the opposite side, which bleeds sky into sand. The plot below follows one scanline, padding included, before and after the blur. Interactive: choose the rule and the blur σ, and click either picture to move the scanline.

6.2.6 A blur zoo⧉

With the mechanics settled, the only choice left is the kernel's shape. Two are worth knowing.

The box filter gives every sample in a $(2r+1)\times(2r+1)$ window weight $1/(2r+1)^2$. Its square footprint is not rotationally symmetric, and its frequency response has lobes and zeros. However, its nonnegative weights cannot create overshoot beyond the neighborhood’s range; a blurred step becomes a ramp, not a ringing edge. Running sums can make large box averages inexpensive.

The Gaussian is the workhorse. Its weights fall off smoothly with distance from the center, following the bell curve

$$ g(r) \propto \exp\!\left(-\frac{r^2}{2\sigma^2}\right), $$

where $r$ is the distance to the center and $\sigma$ (the standard deviation) sets the width, the blur's "radius" in spirit. Read it back: a neighbor's weight decays exponentially with the square of its distance, so nearby pixels count for a lot and far ones fade smoothly to nothing. The Gaussian is smooth, rotationally symmetric (no directional bias), and, as the next chapter shows, uniquely well-behaved in frequency, which is why it is the default blur almost everywhere.

fig-blur-zoo
Figure 6.2.12. The blur zoo: box vs. Gaussian. Top, the 1-D profiles: the box is flat then drops to zero abruptly; the Gaussian is a smooth bell that decays with distance, truncated near ±3σ. Bottom, the matching 2-D stencils (the actual weight grids). The box's hard edge causes directional blurring and faint ringing; the Gaussian's smooth falloff is isotropic and artifact-free, which is why it is the default. Both are normalized to sum to one so brightness is preserved.

Run both on an actual photograph and the difference in feel is plain: the box smears fine texture into faintly blocky, slightly directional mush, while the Gaussian dissolves it smoothly and isotropically. Use a Gaussian when the blur should look natural (Figure 6.2.13).

fig-blur-zoo-photo
Figure 6.2.13. The blur zoo on a real photo. The two everyday blurs, a box average (top) and a Gaussian (bottom), run on the same detailed photograph at increasing radius, matched per column to the same amount of smoothing ($\sigma \approx r/\sqrt{3}$, equal second moment) and computed in linear light. The box's hard square window leaves a blockier, faintly directional smear; the Gaussian's smooth falloff degrades fine-scale texture gracefully and without a preferred direction, the reason it is the default blur.

Better than reading about them, you can try the blurs directly (Figure 6.2.14): switch kernels, change the width, and watch what each does to real texture.

fig-blur-playground
Figure 6.2.14. A blur playground. Pick a photo and a blur kernel (box, disk, triangle, the "bilinear" tent, or Gaussian) and set its size; the original and the blurred result sit side by side, with the kernel drawn below. Box, triangle, and Gaussian factor into two cheap 1-D passes; the disk is a true 2-D circular average, the shape a defocused lens paints (→ Chapter 2.5, Depth of field).

An ideal Gaussian has infinite support. At a distance $3\sigma$, its weight is $e^{-9/2}\approx0.011$ of the peak; a one-dimensional cut at $\pm3\sigma$ discards about 0.27% of the mass. Choose support for the required accuracy and renormalize the sampled, truncated weights. Square truncation preserves separability; circular truncation generally does not. Sampling and truncation make rotational symmetry approximate.

To simulate physical mixing, decode to linear light, filter, then encode for display. Aesthetic smoothing can deliberately use another space, but it should be named. Linear-light processing is essential when the blur is part of a calibrated forward model. For images with transparency, filter premultiplied color and alpha consistently to avoid color bleeding from transparent pixels.

The visible encoding error is easiest to see where black meets white. Averaging their encoded values gives a middle code value; decoding that value generally yields less light than averaging the original linear intensities. A blurred highlight can therefore acquire a dark transition when filtering is done in the display encoding. The arithmetic is still linear in the stored numbers, but it no longer represents linear mixing of light.

6.2.7 Separability⧉

A $(2r+1) \times (2r+1)$ Gaussian appears to cost $(2r+1)^2$ multiplies per output pixel, quadratic in the radius, which is painful for a big blur. There is a shortcut. A 2-D filter is separable if it can be written as one 1-D filter applied along the rows followed by another along the columns, formally, the 2-D kernel is the outer product of two 1-D kernels. The Gaussian is separable, because $\exp(-(x^2 + y^2)/2\sigma^2) = \exp(-x^2/2\sigma^2) \cdot \exp(-y^2/2\sigma^2)$: a 2-D Gaussian is a horizontal 1-D Gaussian times a vertical one.

This is a genuine speedup. Instead of one $(2r+1)^2$ pass you do two $(2r+1)$ passes: blur every row with a 1-D Gaussian, then blur every column. The cost per pixel drops from $O(r^2)$ to $O(2r)$, linear in the radius. For a $\sigma = 10$ Gaussian (radius 30, a 61×61 kernel) that is the difference between ~3700 multiplies per pixel and ~120, about 30 times fewer multiplications, before accounting for memory traffic and implementation overhead. Separability is the first of several "do it in 1-D twice" tricks that recur through the book.

In code it is just two 1-D convolution passes with a scratch buffer between them:

Algorithm 6.2.4
# separable 2-D convolution: g = g_row ⊗ g_col, each 1-D and centered on zero
# pass 1: convolve every row with g_row
for each pixel (x, y):
    tmp[x, y] = 0
    for each offset x':
        tmp[x, y] += in[x + x', y] · g_row[-x']
# pass 2: convolve every column of the result with g_col
for each pixel (x, y):
    out[x, y] = 0
    for each offset y':
        out[x, y] += tmp[x, y + y'] · g_col[-y']

The box blur is separable in exactly the same way: an $n \times n$ average is an $n$-wide average along the rows followed by an $n$-wide average along the columns, so it too drops from $O(r^2)$ to $O(2r)$. Separability is a special property, though, not a universal one. A kernel factors like this only when it is rank one, its grid of weights being the outer product of a single row and a single column; a general kernel is not. The disk-shaped blur of a defocused lens is not separable, and neither is a diagonal motion streak, so those need the full 2-D sum or a different fast route (often the Fourier transform of the next chapter).

fig-separable
Figure 6.2.15. Separability: a 2-D Gaussian = 1-D ⊗ 1-D. The 2-D Gaussian kernel (center) is the outer product of a horizontal 1-D Gaussian and a vertical 1-D Gaussian, so blurring the image is equivalent to blurring the rows with the 1-D kernel and then the columns. The cost per pixel falls from $O(r^2)$ (the full square kernel) to $O(2r)$ (two 1-D passes), the practical reason large Gaussian blurs are fast.

6.2.8 Linear sharpening⧉

Blur mixes a pixel with its neighbors; sharpening does the opposite, making each pixel more different from its neighbors. Where the image is flat the neighbors agree and there is nothing to do; where a pixel stands out, at an edge or a fine texture, sharpening exaggerates the difference so the feature reads crisper.

There is an equivalent way to say it that ties straight back to blur. A blurred copy of an image is its smooth, low-detail part, so whatever the blur removed is exactly the fine detail: $\text{detail} = \text{in} - g * \text{in}$. Sharpening is amplifying that detail, putting it back with a gain $k$:

$$ \text{out} = \text{in} + k\,(\text{in} - g * \text{in}). $$

This is the unsharp mask, and because every piece is linear it collapses to a single convolution,

$$ \text{out} = \big((1 + k)\,\delta - k\,g\big) * \text{in}, $$

a kernel with a tall positive spike at the center (the identity $\delta$) minus a scaled blur around it: positive in the middle, negative in the surround. For a unit-sum blur, this kernel also sums to one and preserves a constant.

They form a small family, differing only in the blur $g$ (equivalently, the band of detail) they amplify:

Sharpening with a DoG is then just

$$ \text{out} = \big(\delta + k\,(G_{\sigma_1} - G_{\sigma_2})\big) * \text{in}. $$

Because all of these are linear convolutions they inherit this chapter's machinery, separability included (each Gaussian factors into a row pass and a column pass). Two failure modes limit linear sharpening: it amplifies noise along with detail (noise lives in the same high band), and it overshoots strong edges into bright and dark halos. Curing those needs the nonlinear, edge-aware sharpening of Chapter 8.8, Sharpening.

fig-linear-sharpen-1d
Figure 6.2.16. Linear sharpening in one and two dimensions. Laid out like the blur figure of the previous section: input on the left as a 1-D signal (top) and a 2-D image (below, a draggable scanline links them), the sharpening kernel in the middle, the sharpened output on the right. A menu picks the filter (unsharp mask, Laplacian, or difference of Gaussians) and sliders set the amount $k$ and the scale(s) ($\sigma$, plus the ratio $\sigma_2/\sigma_1$ for the DoG). As $k$ rises the center-positive, surround-negative kernel deepens and the output's edges gain overshoot; the DoG's band-pass picks which scale of detail is boosted.

6.2.9 Gradient and oriented filters⧉

Not every useful kernel blurs. The most important non-blurring kernel measures change (how fast the image brightens or darkens as you step across it) and that turns out to be the foundation of edge detection.

With pixel spacing $h$, a forward difference is $I_x[i]=(I[i+1]-I[i])/h$. The weights $[-1,+1]/h$ act as a correlation stencil at offsets $0,1$. Under this chapter’s convolution convention, the same operation has $g[-1]=1/h$ and $g[0]=-1/h$. State the offsets as well as the weights: a two-tap stencil has no central sample. A ramp increasing to the right must produce a positive derivative.

A grayscale image is a two-dimensional function (a brightness landscape with hills and valleys) so its derivative is not a single number but a gradient: a vector that, like the slope of a hill, points in the direction of steepest brightening and is perpendicular to the contours of constant brightness (Figure 6.2.17, right). Its two components are exactly the horizontal and vertical partials, $I_x$ and $I_y$, which is why we estimate each with its own finite-difference kernel.

💡 Big lesson (L6.6) — differentiation is convolution

Finite differences are convolutions with signed kernels under a declared indexing convention. In the continuous setting, $\partial_x(g*I)=(\partial_x g)*I$: convolving with a derivative-of-Gaussian differentiates a smoothed image. Gradient components are linear; gradient magnitude, thresholding, and a complete edge detector generally are not.

fig-derivative-gradient
Figure 6.2.17. From derivative to gradient. Left, in 1-D the derivative is the slope of the tangent to the curve. Right, in 2-D the gradient $\nabla f = (f_x, f_y)$ is a vector that points in the direction of steepest ascent (uphill) and is normal to the contours of constant value, the natural generalization of the slope to a brightness landscape. An image's gradient is computed component-wise: a horizontal finite difference for $I_x$, a vertical one for $I_y$.

Differencing increases the contribution of rapidly varying noise. Sobel combines a difference along one axis with smoothing along the other. The following displayed matrix is a correlation stencil, using image coordinates with $x$ rightward and $y$ downward:

$$ \text{Sobel}_x = \begin{bmatrix} -1 & 0 & +1 \\ -2 & 0 & +2 \\ -1 & 0 & +1 \end{bmatrix}, $$

Divide its response by $8h$ to estimate a derivative in intensity per unit distance on a linear ramp. For convolution, flip the displayed stencil, which negates it. Its transpose gives the vertical stencil under the same convention.

The smoothing is what Sobel buys. Scaled by 1/4, it gives a step edge the same height of response as the plain difference $[-1,\,1]$, but it averages three rows, weighted 1, 2, 1, before differencing. On independent pixel noise of standard deviation $s$, the plain difference has noise $\sqrt2\,s$ and the scaled Sobel $\sqrt{12}/4\,s\approx0.87\,s$, about 1.6 times less. The price is resolution: Sobel's response to an edge is two pixels wide (Figure 6.2.18).

fig-finite-difference-vs-sobel
Figure 6.2.18. The plain difference and Sobel on a noisy photograph, an arcade at ISO 6400. Both filters are scaled to give the same step response, so the edges have the same height in both; in the flat regions the Sobel output is visibly quieter. The noise measured in a flat patch of each derivative image is printed on its panel beside the value predicted from the filter weights.

Stacking the two partials into a vector gives the image gradient $\nabla I = (I_x, I_y)$, the brightness-landscape gradient of Figure 6.2.17 applied to a real image. Two numbers fall out of it, both central to vision:

$$ \|\nabla I\| = \sqrt{I_x^2 + I_y^2} \quad\text{(edge strength)}, \qquad \theta = \operatorname{atan2}(I_y, I_x) \quad\text{(gradient orientation)}. $$

The magnitude is large at strong intensity changes, which can be object boundaries, texture, or noise. The gradient angle points across an edge; its tangent is perpendicular, $\theta+\pi/2$ modulo $\pi$. Thresholding magnitude gives a simple detector. Canny adds smoothing, non-maximum suppression, and hysteresis.

fig-gradient-sobel
Figure 6.2.19. Gradient via Sobel. From left: the input image; the horizontal Sobel response $I_x$ (vertical edges light up); the vertical Sobel response $I_y$ (horizontal edges light up); and the gradient magnitude $\sqrt{I_x^2 + I_y^2}$, an edge-strength map that is bright on contours and dark in smooth regions. The Sobel kernel pairs a finite-difference derivative on one axis with a small smoothing on the other for noise robustness.

Blur, gradients, and sharpening all share the same comfortable assumption: one kernel, applied the same way everywhere. Before we lean on it, ask how well that assumption actually holds for the blur a real camera produces, because that is where the clean theory meets messy optics. (The linear sharpening above is only the start; its nonlinear, edge-aware forms are the Chapter 8.8, Sharpening chapter, and undoing a blur exactly, deconvolution, comes later still.)

fig-gabor-bank
Figure 6.2.20. An oriented filter at one scale and orientation, and the bank it belongs to. The kernel is a Gaussian envelope times a cosine (even: bars and lines) or a sine (odd: edges) across the chosen orientation. Either response alone depends on where the feature sits within the filter's cycle; the energy √(even² + odd²) does not, which is what makes the pair a detector of oriented structure rather than of one particular phase. Interactive: set the orientation and wavelength or click a filter in the bank, and switch between the even response, the odd response and their energy.
fig-conv-layer
Figure 6.2.21. The first layer of a convolutional network, as kernels of the kind this chapter builds by hand. Eight small RGB kernels (three edge detectors, a blur, a center-surround, two color opponents and an oriented stripe) and the feature map each produces, with the ReLU that the layer passes on. The switch between correlation and convolution is the point: a library's "convolution" layer computes the correlation, and flipping the kernels mirrors the response of every asymmetric one. The kernels here are hand-built, not taken from a trained network. Interactive: choose correlation or convolution, turn ReLU off to see the signed maps, and click a map to enlarge it.

6.2.10 Is imaging blur usually a shift-invariant convolution?⧉

This whole chapter has modeled blur as a convolution, one kernel, applied identically everywhere. How often is that actually true of real camera blur? Usually it is not, and knowing where the model breaks tells you when the clean linear-shift-invariant (LSI) tools apply and when they do not.

Camera rotation produces a sequence of homographies for a static scene and fixed intrinsics. Pan and tilt resemble translation over a narrow field, but their displacements vary across a wide image; roll traces arcs about the principal point. Camera translation introduces depth dependence and parallax. See Whyte et al. 2011/2014 and Chapter 16.4, Blind deblurring.

Defocus depends on the difference between a point’s inverse depth and the focused plane’s inverse depth, as well as the aperture and lens geometry. It is zero at the focused plane, not proportional to inverse depth alone. Visibility at foreground/background boundaries requires a fuller model than blurring each region independently.

Lens aberrations can vary with field position, wavelength, aperture, and focus. Coma and field curvature are important off-axis effects; spherical aberration is present on axis too. Longitudinal chromatic aberration shifts focus with wavelength. Lateral chromatic aberration shifts image position or scale with wavelength and can often be reduced by calibrated per-channel warps, though each broad color channel still integrates wavelength-dependent effects. See Chapter 22.1, Aberrations and optical challenges and Chapter 22.2, Aberrations correction.

Motion blur. Object motion blur follows each moving object's own trajectory (combined with the camera motion above), so different objects blur in different directions and amounts, again hard to model in 2-D at the boundary between a moving object and its background.

Diffraction is the well-behaved exception: the diffraction PSF (the Airy disk) is essentially shift-invariant across the frame, though some forms of vignetting (the aperture clipped by the lens barrel toward the corners, so not every pixel sees the whole aperture) modulate it off-axis.

Fixed software prefilters are convenient convolution models. Downsampling after a prefilter nevertheless discards information, and reconstruction filters do not generally undo that loss. A spatially varying resampling footprint is not one convolution over the original grid.

The pattern is clear: a real PSF can depend on (1) $1/\text{depth}$, (2) local motion, and (3) field (image) position, none of which a single shift-invariant kernel captures.

💡 Big lesson (L6.7) — most imaging blur is not a fully shift-invariant convolution

A convolution is an approximation when blur changes with depth, motion, field position, or wavelength. A small patch may support one kernel, but occlusion boundaries need particular care. Fixing geometry can preserve linearity in intensity even when shift-invariance is lost.

The difference between axial and lateral chromatic aberration changes the available correction. If wavelengths focus at different distances, a plane sharp in one channel may be blurred in another; aligning edges geometrically cannot restore the attenuated texture. If the principal difference is magnification, a per-channel warp can align much of the colored fringe. A broad channel still combines a range of wavelength-dependent magnifications, so even a successful alignment need not remove every within-channel smear. This is why the two effects should not be treated as one color-fringe problem.

6.2.11 Fixed and data-dependent weights⧉

A bilateral filter changes its weights with image values, so it is nonlinear. It can still be translation-equivariant on an infinite grid: translating the image translates the weights and result. Conversely, a fixed spatial mask is linear but not shift-invariant. These are separate properties, as Chapter 6.7, When linearity breaks down explains.

Chapter 6.3, Fourier analyzes fixed convolution kernels by frequency. Chapter 10.1, Bilateral filtering develops image-dependent weights for preserving boundaries while smoothing. Neither model is a universal description of neighborhood operations.


Recap: big lessons of this chapter

💡 Big lesson (L6.4) — a convolution is a shift-invariant weighted sum

A convolution uses fixed weights that depend on displacement, not on image values or absolute position. On an infinite or periodic grid it is linear and commutes with shifts. The kernel can be small or span the image. Its support determines the work of direct filtering; finite boundaries determine where the shift-invariance model applies.

💡 Big lesson (L6.5) — convolution is a weighted sum of shifted images

Convolution can be computed per output pixel or per kernel tap. In the first order we accumulate a neighborhood; in the second we accumulate shifted copies of the image. The result is the same. Replacing shifts with known warps gives other linear operators, but they need not share convolution’s commutativity or Fourier diagonalization.

💡 Big lesson (L6.6) — differentiation is convolution

Finite differences are convolutions with signed kernels under a declared indexing convention. In the continuous setting, $\partial_x(g*I)=(\partial_x g)*I$: convolving with a derivative-of-Gaussian differentiates a smoothed image. Gradient components are linear; gradient magnitude, thresholding, and a complete edge detector generally are not.

💡 Big lesson (L6.7) — most imaging blur is not a fully shift-invariant convolution

A convolution is an approximation when blur changes with depth, motion, field position, or wavelength. A small patch may support one kernel, but occlusion boundaries need particular care. Fixing geometry can preserve linearity in intensity even when shift-invariance is lost.