a \(( 2k{+}1) \times (2k{+}1)\) kernel, one output pixel per position
f input imageh kernel / filter / maskg output image
kernel = np.ones((15, 15), np.float32) / 225 # 15x15 box blur, weights sum to 1
out = cv2.filter2D(img, -1, kernel) # -1 = keep the input's dtype
Weights that sum to 1 preserve brightness. Weights that sum to 0 respond to
change, not to level — that's every edge filter.
One kernel, grayscale and colour
OpenCV applies the same kernel to each colour channel independently. Because grayscale
conversion is linear, filter then convert and convert then filter agree.
Blur: box vs Gaussian
Gaussian is the default for a reason: no hard cut-off, rotationally symmetric,
and separable — a 15×15 blur costs two 1-D passes, not 225 multiplies per pixel.
Sharpening is just adding back the detail
\( d = f - G_\sigma * f \)
\( f_{\text{sharp}} = f + \alpha\, d
= (1+\alpha)f - \alpha\,(G_\sigma * f) \)
Which is why [[0,−1,0],[−1,5,−1],[0,−1,0]] works:
with our Laplacian sign convention, it is identity − Laplacian
folded into one kernel. Weights still sum to 1.
An edge is where brightness changes fast
Edge detection = differentiation. A filter like [−1, 0, 1]
is just a finite-difference derivative.
Sobel: derivative in x, derivative in y
10−120−210−1
Sobel x
121000−1−2−1
Sobel y
Each one is a [−1, 0, 1] derivative in one direction,
combined with a [1, 2, 1] blur in the other.
Smoothing first is what stops it firing on noise.
Convolution = correlation with the kernel flipped 180°.
cv2.filter2D actually does correlation;
scipy.signal.convolve2d does convolution.
For symmetric kernels (box, Gaussian) it makes no difference — for Sobel it flips the sign.
Because convolution is associative, you can differentiate the kernel once, offline,
instead of differentiating every image at run time.
What happens at the edge of the image?
The kernel hangs off the picture — so we invent pixels.
reflect is usually the safest default: no invented dark rim (constant),
no smeared streaks (replicate), no wrap-around from the opposite side.
Does a bigger kernel do more?
Take the 3×3 Sobel and pad it with zeros. Same weights, bigger support.
Identical output, much slower — cost grows with the kernel area (k²).
Padding adds nothing; what matters is where the non-zero weights are.
Part 1 in one table
Kernel
Weights sum to
Responds to
Effect
Box / Gaussian
1
average level
smooths, removes detail
Sharpen
1
level + local contrast
exaggerates detail
Sobel / [−1,0,1]
0
change in one direction
edges
Laplacian
0
change in every direction
edges, no orientation
Notice the pattern: sum = 1 keeps brightness, sum = 0 discards it.
That sum is the filter's response at zero frequency —
\( H(0,0) = \sum_{i,j} h(i,j) \) — which is exactly
what Part 2 calls the DC term.
2
Filtering in frequency
Same operation, different alphabet.
Any signal is a sum of sine waves
\( f(t) \;=\; \sum_{k} A_k \sin\!\bigl(2\pi k t + \phi_k\bigr) \)
The Fourier transform just answers: how much of each wave, and shifted by how much?
Amount = magnitude, shift = phase.
Drag the sliders: faster wave → dots move outward,
rotate the wave → dots rotate with it. Centre of the spectrum = lowest frequency.
What does the spectrum store?
\( F(u,v) = \underbrace{|F(u,v)|}_{\text{magnitude: how much}}\;
e^{\,i\,\underbrace{\phi(u,v)}_{\text{phase: where}}} \)
In natural images, phase usually carries most recognisable structure:
magnitude says how strongly each frequency is present, while phase says where its
waves align to form edges, contours and objects.
Brighter points mean more magnitude at that frequency.
Convolution in space = multiplication in frequency
\[ f * h \;\;\xrightarrow{\;\mathcal{F}\;}\;\; F \cdot H
\qquad\text{and}\qquad f \cdot h \;\;\xrightarrow{\;\mathcal{F}\;}\;\; F * H \]
A DFT product gives circular convolution. To reproduce linear convolution, pad
sufficiently, align the kernel origin, then crop using the same boundary convention.
Beyond a size-dependent crossover, the frequency route can be cheaper.
Wide blur in space ⟷ narrow window in frequency. A big blur keeps only a small
disc of low frequencies — so "blur" and "low-pass" are literally the same statement.
Filtering by choosing a region of the spectrum
\( H_{\text{low}}(u,v) = e^{-\frac{u^2+v^2}{2r^2}}, \qquad
H_{\text{high}} = 1 - H_{\text{low}}, \qquad
g = \mathcal{F}^{-1}\{ F \cdot H \} \)
Low-pass = keep the centre → blur.
High-pass = throw the centre away → edges, no brightness.
Band-pass = keep a ring → one scale of detail.
The two halves, side by side
In space
In frequency
convolve with a kernel
multiply the spectrum by a mask
Gaussian blur
keep the low-frequency centre
sharpen / Laplacian
boost the outer frequencies
Sobel derivative
weight ∝ frequency, along one axis
larger smoothing scale (e.g. Gaussian σ)
narrower passband
dense direct: \(O(N^2k^2)\)
FFT: \(O(PQ\log(PQ))\), including padding
You never have to choose a domain to be "right" in — pick whichever makes the
question easy to answer.
3
What to carry out of this room
Four habits for Assignment 1 — and for everything after it.
Take-homes for the assignment
1 · THROWING PIXELS AWAY IS A FILTERING DECISION
A coarser grid can only carry frequencies up to its own limit. Anything above that
does not vanish — it reappears somewhere else in the spectrum.
So the question to ask before resampling is always: what is the new limit, and
what in my image lives above it?
2 · IF TWO ROUTES MUST AGREE, PROVE IT
You saw three ways to blur today. When implementations are supposed to be equivalent,
one line settles it:
np.max(np.abs(a - b)) # not "they look the same"
A tiny non-zero number is float rounding.
A large one is a bug in padding, centring, or border handling. And when there is no
exact match to check against, compare to a reference you trust and report an
error metric rather than an impression.
3 · COST IS A CLAIM YOU CAN MEASURE
For an \(N\times N\) image: \(N^2k^2\) for direct 2-D, \(N^2k\) for separable,
and \(PQ\log(PQ)\) for the FFT, where \(P,Q\) include padding.
Which wins depends on the sizes you are using.
Put all the curves on one set of
axes and find where the ranking changes. The crossover is the interesting part:
it is where a constant factor stops mattering and the exponent takes over.
4 · IF YOU'RE LOOPING OVER PIXELS, STOP
Per-pixel Python loops are slow, but worse, they usually mean the array formulation
hasn't been found yet — and that formulation is normally the clearer one.
Slicing, broadcasting, and one call to a
convolution routine replace almost every double for.
Take-homes for the rest of computer vision
WHEN AN IMAGE LOOKS WRONG, LOOK AT ITS SPECTRUM
Moiré, banding, ringing, periodic sensor noise, JPEG blocking — each has a signature in
the Fourier magnitude that is obvious there and invisible in the pixels.
A DISPLAY TRANSFORM IS NOT A DATA TRANSFORM
np.log(1 + |F|) exists so your eyes can see past the DC term.
Never feed the logged version back into the maths — a habit worth keeping for every
colourmap, normalisation and axis scale you ever choose.
THE BUGS LIVE AT THE BOUNDARY AND AT THE CENTRE
Border mode, odd vs even kernel size, and fftshift /
ifftshift account for most results that are
almost right. Check corners and check the DC term.
THE KERNELS BECAME LEARNED — THE PHYSICS DIDN'T CHANGE
A CNN learns the numbers in the little matrix, but stride and pooling are still
downsampling, so aliasing is still a real failure mode; and phase still carries the
structure, which is why it matters for generative models and image forensics.
The core fixed-kernel theory today was linear and shift-invariant, assuming
compatible boundary handling. That is what lets us describe such systems by convolution
and frequency-response multiplication. Several displayed operations — such as gradient
magnitude, clipping and resampling — are not themselves LSI.
4
Your turn
worksheet03.ipynb — exercises
Exercises
1 · SEPARABILITY
Do a 2-D Gaussian blur as two 1-D passes. Time it against the 2-D version.
Hint: cv2.getGaussianKernel already gives you the 1-D kernel.
2 · KERNEL SIZE
Blur with 5×5 vs 15×15. What changed — and what did not?
Watch σ as well as size; size alone doesn't set the amount of blur.
3 · DELETE MAGNITUDE OR PHASE
Replace one of them with random values and invert. Which destroys the image?
Predict first, then run it.
4 · GAUSSIAN HIGH-PASS
Build the mask in the frequency domain.
You already have the low-pass mask — think about 1 − mask.
Exercise 5 — design a kernel
Find the smallest kernel whose response peaks at the centre of this 3×3 blob
and is lower everywhere else.
The response counts how much of the kernel lands on the blob — full overlap (9)
only at the centre, less at every shifted position. Smaller kernels tie:
1×1 gives 1 at all nine blob pixels,
1×3 gives 3 on three of them.
Cheat sheet
# --- space -----------------------------------------------------------
k = cv2.getGaussianKernel(15, 5); k2 = np.outer(k, k) # separable Gaussian
out = cv2.filter2D(img, -1, k2, borderType=cv2.BORDER_REFLECT) # correlation!
out = signal.convolve2d(img, k2, boundary='symm', mode='same') # true convolution
# --- frequency -------------------------------------------------------
F = np.fft.fftshift(np.fft.fft2(img)) # DC term to the centre
mag, ph = np.abs(F), np.angle(F)
show = np.log(1 + np.abs(F)) # log only for DISPLAY
img2 = np.real(np.fft.ifft2(np.fft.ifftshift(F * mask))) # shift back before inverting
F = np.fft.rfft2(img); img2 = np.fft.irfft2(F, s=img.shape) # real input: half the spectrum
Two things that bite everyone: filter2D is correlation, not
convolution; and you must ifftshift before
ifft2.
Play with these afterwards
Image Kernels, explained visually — drag a kernel over a photo, live
setosa.io/ev/image-kernels/
Karl Sims — Interactive FFT tutorial — build a signal from its frequencies
karlsims.com/fft.html
An Interactive Guide to the Fourier Transform — the intuition, no integrals
betterexplained.com/articles/an-interactive-guide-to-the-fourier-transform/
HIPR2, University of Edinburgh — short reference pages per operator
homepages.inf.ed.ac.uk/rbf/HIPR2/fourier.htm
MIT Vision Book — Fourier Analysis — a rigorous computer-vision treatment
visionbook.mit.edu/image_processing_fourier.html
CNN Explainer — where this goes next: learned kernels
poloclub.github.io/cnn-explainer/
Next week these hand-designed kernels stop being hand-designed —
a CNN learns the numbers in the little matrix.
End of workshop 3
Questions?
Press S for speaker notes ·
O for the slide overview ·
F for fullscreen