
Lecture 4
Computer Science Department, Colorado School of Mines
By the end of this lecture, you should be able to:
This image is too big to fit on the screen!
How would you reduce it to half its size?
Keep every other row and column, then repeat.
import matplotlib.pyplot as plt
y2 = I[::2, ::2]
y4 = y2[::2, ::2]
y8 = y4[::2, ::2]
fig, ax = plt.subplots(1, 3, figsize=(8, 4),
gridspec_kw={"width_ratios": [4, 2, 1]})
for a, y, s in zip(ax, [y2, y4, y8], [2, 4, 8]):
a.imshow(y, cmap="gray", vmin=0, vmax=255,
interpolation="nearest")
a.set_title(f"1/{s}")
a.axis("off")
plt.tight_layout()
plt.show()

Why is the 1/8 image so pixelated (and do you know what this effect is called)?
Camera / sensor hardware
CMOS sensor array
Images are a discrete, or sampled, representation of a continuous world.
A simple example: a sine wave

How would you discretize this signal?
Sample the signal at regularly spaced locations.

The sampled signal consists of discrete values.

How many samples should I take?
Can I take as many samples as I want?
The sampled signal consists of discrete values.

How many samples should I take?
Can I take as few samples as I want?
What happens when we take fewer samples?

Unsurprising effect: information is lost.
A different signal can agree with the same samples.

Unsurprising effect: information is lost.
Surprising effect: we can confuse the signal with one of lower frequency.
The same samples can also match a signal of higher frequency.

The samples alone do not uniquely identify the original signal.
We need an assumption about its highest frequency.
Aliasing: Undersampling disguises a signal as one of a lower frequency.


These patterns are also known as moiré patterns.

Striped fabric

Building façades

AC grille

The wheel can therefore appear to rotate backward—an example of temporal aliasing.
How would you deal with aliasing?
Approach 1: Use more samples to represent fine detail.

Approach 2: Smooth the image before downsampling.

Some fine detail is lost, but aliasing artifacts are reduced.
How would you smooth a signal?
Apply a Gaussian filter, then keep every other row and column. Repeat.
import cv2
import matplotlib.pyplot as plt
y2g = cv2.GaussianBlur(I, (3,3), 2)[::2, ::2]
y4g = cv2.GaussianBlur(y2g, (3,3), 2)[::2, ::2]
y8g = cv2.GaussianBlur(y4g, (3,3), 2)[::2, ::2]
fig, ax = plt.subplots(1, 3, figsize=(8, 4),
gridspec_kw={"width_ratios": [4, 2, 1]})
for a, y, s in zip(ax, [y2g, y4g, y8g], [2, 4, 8]):
a.imshow(y, cmap="gray", vmin=0, vmax=255,
interpolation="nearest")
a.set_title(f"1/{s}")
a.axis("off")
plt.tight_layout()
plt.show()
plt.close(fig)


How much smoothing is enough?
How many samples are enough?
Tip
Both depend on the Nyquist condition:
\[ f_s > 2f_{\max} \]
We’ll see what this means soon.

Image compression

Texture mapping

Image blending

Denoising

Focal stack compositing

Multiscale detection

Multiscale registration

Also: Optical flow and feature tracking estimate motion from a coarse to fine paradigm.
A Gaussian pyramid represents an image at progressively coarser spatial scales.
\[ G_0 = I, \qquad G_{i+1} = \operatorname{downsample}(h_G * G_i) \]
Construction
How many pixels does \(G_{i+1}\) have compared with \(G_i\)?
Approximately \(\frac{1}{4}\) as many pixels.
Why smooth first?
To reduce aliasing before downsampling.
Must the factor be 2?
No! Other scale factors are possible. A factor of 2 is conventional and is used by cv2.pyrDown().
cv2.pyrDown() combines Gaussian smoothing and downsampling.
import cv2
import matplotlib.pyplot as plt
G = [I]
for _ in range(3):
y = cv2.pyrDown(G[-1], borderType=cv2.BORDER_REFLECT)
G.append(y)
heights = [g.shape[0] for g in G[::-1]]
fig, ax = plt.subplots(4, 1, figsize=(10, 7.5),
gridspec_kw={"height_ratios": heights})
W = I.shape[1]
for a, i in zip(ax, range(3, -1, -1)):
h, w = G[i].shape
a.imshow(G[i], cmap="gray", vmin=0, vmax=255,
interpolation="nearest",
extent=((W-w)/2, (W+w)/2, h, 0))
a.set_xlim(0, W)
a.text(1.02, .5, rf"$G_{i}$", fontsize=28, transform=a.transAxes, va="center")
a.axis("off")
fig.subplots_adjust(left=.03, right=.87, bottom=.02, top=.98, hspace=.12)
plt.show()
What happens to the details of the image?
What is preserved at the higher levels?

How would you reconstruct the original image from an upper level image?

Gaussian smoothing suppresses detail before we even downsample.

What does the residual look like?
The residual is the original image minus the smoothed image.

Can we make a pyramid that allows exact reconstruction?
Yes! Retain the residual information along with the coarse image. This is the idea behind a Laplacian pyramid.
The original image equals an expanded coarse image plus a residual.

Do we need to store every blurred image as well as every residual?
No. We can recover each finer image from its residual and the next coarser image.
Construction
Start with \(G_0 = I\). At each level:
\[ L_i = G_i - \operatorname{expand}(G_{i+1}) \]
Repeat until the desired minimum resolution is reached.
Here, expand means upsample and filter, using cv2.pyrUp().
The sequence \(G_0, G_1, G_2, \ldots, G_n\) is the Gaussian pyramid.
The sequence \(L_0, L_1, \ldots, L_{n-1}, G_n\) is the Laplacian pyramid.
The Laplacian pyramid stores the detail needed to move back from one Gaussian level to the next finer level.
In Laplacian pyramid, expand means:
Original image: A \(2\times2\) image.
\[ \begin{bmatrix} 32 & 96 \\ 160 & 224 \end{bmatrix} \]
Insert zeros: Double width & height.
\[ \begin{bmatrix} 32 & 0 & 96 & 0 \\ 0 & 0 & 0 & 0 \\ 160 & 0 & 224 & 0 \\ 0 & 0 & 0 & 0 \end{bmatrix} \]
Interpolation filter: cv2.pyrUp().
\[ \begin{bmatrix} 80 & 96 & 120 & 128 \\ 112 & 128 & 152 & 160 \\ 160 & 176 & 200 & 208 \\ 176 & 192 & 216 & 224 \end{bmatrix} \]
Note
Filtering changes values at the original pixel positions too. The enlarged image is a smooth estimate of the finer image.
Use floating-point arrays to preserve negative residual values.
G = [I.astype("float32")]
L = []
for _ in range(3):
y = cv2.pyrDown(G[-1], borderType=cv2.BORDER_REFLECT)
u = cv2.pyrUp(y, dstsize=G[-1].shape[::-1])
L.append(G[-1] - u)
G.append(y)
L.append(G[-1]) # Keep the smallest image too
heights = [im.shape[0] for im in L[::-1]]
fig, ax = plt.subplots(4, 1, figsize=(10, 7.5),
gridspec_kw={"height_ratios": heights})
W = I.shape[1]
for a, i in zip(ax, range(3, -1, -1)):
h, w = L[i].shape
s = max(float(abs(L[i]).max()), 1)
lo, hi = (0, 255) if i == 3 else (-s, s)
a.imshow(L[i], cmap="gray", vmin=lo, vmax=hi,
interpolation="nearest",
extent=((W-w)/2, (W+w)/2, h, 0))
a.set_xlim(0, W)
label = r"$L_3=G_3$" if i == 3 else rf"$L_{i}$"
a.text(1.02, .5, label, fontsize=28,
transform=a.transAxes, va="center")
a.axis("off")
fig.subplots_adjust(left=.03, right=.76, bottom=.02,
top=.98, hspace=.12)
plt.show()
What do we need to reconstruct the original image?
1. The residuals
\[ L_0,\quad L_1,\quad L_2 \]
These retain the detail lost between consecutive Gaussian levels.
2. The smallest Gaussian image
\[ L_3 = G_3 \]
This provides the starting point for reconstruction.
The intermediate Gaussian images can also be recovered as needed.

Residual contrast is scaled for display; mid-gray means zero.
Reconstruction uses the original signed arrays.
Start with the smallest image.
At each step:
\[ G_i = \operatorname{expand}(G_{i+1}) + L_i \]
The residual restores the detail missing from the expanded image.
Keeping the residuals and smallest image allows exact reconstruction, apart from numerical roundoff.

Which pyramid takes more space to store?
Their raw storage is equal when using the same data type.
A Laplacian pyramid can require more bits per value to represent signed differences and preserve precision.
They take about 33% more than the original image:
\[ N + \frac{N}{4} + \frac{N}{16} + \cdots \approx \frac{4}{3}N \]
Gaussian pyramid

Laplacian pyramid

Raw array size and compressed file size are different.
Frequently occurring values can be encoded using fewer bits
Lossy compression

| Application | How pyramids help |
|---|---|
| Panorama stitching and blending | Combine images at multiple scales to reduce visible seams. |
| Exposure fusion | Combine well-exposed regions from photographs taken at different exposures. |
| Motion estimation and feature tracking | Estimate motion at coarse resolutions, then refine it at finer resolutions. |
| Detail and local-contrast adjustment | Strengthen or suppress image details at selected spatial scales. |

\[ \widetilde{L}_i = \left[\alpha M_i+\beta(1-M_i)\right]L_i, \qquad \alpha=1.5,\quad \beta=0.25 \]
For first-order derivative basis responses:
\[ h_\theta = \cos(\theta)\,h_x + \sin(\theta)\,h_y \]

A simplified first-order steerable pyramid: rows correspond to Gaussian pyramid levels \(G_0\), \(G_1\), and \(G_2\), while columns show orientation-selective responses synthesized from the basis responses \(h_x\) and \(h_y\).
A wavelet transform separates an image into spatial-frequency subbands:
The LL image can be decomposed again, producing a multi-scale representation.

Haar wavelet decomposition: at each level, the input is separated into a low-frequency approximation LL and three directional detail bands. The LL band becomes the input to the next level.
Computer Vision; Colorado School of Mines | Kaveh Fathian