Project 2: Fun with Filters and Frequencies

Ray Sun · CS 180 · Fall 2026

Part 1 explores blur, derivatives, and edges. Part 2 uses spatial frequencies to sharpen photographs, create hybrid images, and blend at multiple scales.

1.1 · Convolution from scratch

Both implementations flip the kernel, zero-pad the image, and return the same input dimensions. Four loops visit each pixel and filter weight; two loops let NumPy compute the inner weighted sum.

Four loops

def convolve_four_loops(image, kernel):
    """Zero-filled 'same' convolution, with one loop per array dimension."""
    image, padded, flipped = prepare(image, kernel)
    height, width = image.shape
    kh, kw = flipped.shape
    output = np.zeros_like(image)
    for y in range(height):
        for x in range(width):
            for ky in range(kh):
                for kx in range(kw):
                    output[y, x] += padded[y + ky, x + kx] * flipped[ky, kx]
    return output

Two loops

def convolve_two_loops(image, kernel):
    """Same operation, with the kernel's multiply-and-sum handled by NumPy."""
    image, padded, flipped = prepare(image, kernel)
    height, width = image.shape
    kh, kw = flipped.shape
    output = np.zeros_like(image)
    for y in range(height):
        for x in range(width):
            patch = padded[y:y + kh, x:x + kw]
            output[y, x] = np.sum(patch * flipped)
    return output

Shared padding

Float64 preserves negative derivatives. Zero padding treats the exterior as black, darkening the blurred border. Even-sized filters get one extra zero on the top/left to match SciPy’s same convention.

def prepare(image, kernel):
    image = np.asarray(image, dtype=np.float64)
    kernel = np.asarray(kernel, dtype=np.float64)
    if image.ndim != 2 or kernel.ndim != 2 or not image.size or not kernel.size:
        raise ValueError("image and kernel must be nonempty 2D arrays")
    kh, kw = kernel.shape
    # Even kernels need the extra zero on the top/left to match SciPy's 'same'.
    padded = np.pad(image, ((kh // 2, (kh - 1) // 2),
                            (kw // 2, (kw - 1) // 2)), mode="constant")
    return image, padded, kernel[::-1, ::-1]

Averaging and differentiating a portrait

The grayscale portrait is 248 × 256. The box filter averages 81 neighbors; Dx = [1, −1] and Dy = Dxᵀ measure horizontal and vertical changes. Gray is zero in both derivative displays.

box = np.ones((9, 9)) / 81
DX = np.array([[1.0, -1.0]]); DY = DX.T
outputs = [convolve_two_loops(portrait, k) for k in (box, DX, DY)]
Grayscale portrait
Grayscale portraitResized to 248 × 256 for the loop experiments.
9 × 9 box blur
9 × 9 box blurAll 81 weights are 1/81. Fine details soften; zero padding darkens the border.
Portrait · x derivative
Portrait · x derivativeIntensity changes across columns; vertical contours stand out.
Portrait · y derivative
Portrait · y derivativeIntensity changes across rows; horizontal contours stand out.

Comparison with SciPy

All methods agree within 2.22e-15. SciPy is fastest. NumPy speeds up the box filter, but per-pixel overhead makes two loops less useful for tiny derivative filters.

Median time (ms), 3 runs. Same image, filters, and zero padding; file I/O excluded.
FilterFour loopsTwo loopsSciPy
9 × 9 box1284.1199.885.45
D_x37.4390.220.50
D_y40.4893.150.65

1.2 · Finite difference operator

Convolving with Dx = [1, −1] measures I[y, x] − I[y, x−1]; Dy is its transpose. A large response means intensity changes quickly in that direction. Combining the two derivatives gives a direction-independent edge strength.

magnitude = √(gx² + gy²)   ·   edge = magnitude > threshold

gx = convolve2d(image, DX, mode="same", boundary="fill", fillvalue=0)
gy = convolve2d(image, DY, mode="same", boundary="fill", fillvalue=0)
magnitude = np.hypot(gx, gy)
edges = magnitude > 0.12
Cameraman · x derivative
Cameraman · x derivative
Cameraman · y derivative
Cameraman · y derivative
Gradient magnitude
Gradient magnitudesqrt(gx² + gy²), combining both directions.
Selected edges · threshold 0.12
Selected edges · threshold 0.12White means the gradient magnitude exceeds the threshold.

The x/y displays share a symmetric ±0.969 range. All cameraman magnitude displays on this page share a range from 0 to 1.109, the raw magnitude maximum. Thresholds operate on the original float values, before display scaling.

Choosing the threshold

Threshold 0.08
Threshold 0.08
Threshold 0.12
Threshold 0.12Selected
Threshold 0.18
Threshold 0.18

At 0.08, more weak contours survive, but so does dense ground texture. At 0.18, much of that texture disappears along with weaker parts of the subject and background. A threshold of 0.12 retains the main silhouette, camera, and tripod while removing some weaker responses. It still leaves visible ground texture; thresholding alone cannot distinguish texture from object boundaries.

Zero padding also makes a bright image boundary look like an edge against a black exterior. Those border responses remain visible here so the padding convention is explicit.

1.3 · Derivative of Gaussian

A Gaussian blur suppresses small intensity fluctuations before differentiation. The Gaussian uses a 9 × 9 support and σ = 1.5 pixels. OpenCV constructs a normalized 1D Gaussian; its outer product forms the 2D filter.

column = cv2.getGaussianKernel(9, 1.5)
gaussian = column @ column.T
dog_x = convolve2d(gaussian, DX, mode="full")
dog_y = convolve2d(gaussian, DY, mode="full")
Original cameraman
Original cameraman512 × 512; intensities scaled to [0, 1].
Gaussian blur
Gaussian blurSmall texture variations are averaged before differentiation.
Finite-difference magnitude
Finite-difference magnitudeFine grass and background texture produce many responses.
Smoothed magnitude
Smoothed magnitudeSmoothing lowers and spreads the response around edges.

After smoothing, the main outline is more continuous and the fine ground texture is reduced. The magnitude is lower and the transitions are wider. Because smoothing reduces edge strength, the raw threshold of 0.12 would discard useful contours; the smoothed result uses 0.04.

Threshold 0.02
Threshold 0.02
Threshold 0.04
Threshold 0.04Selected
Threshold 0.06
Threshold 0.06

At 0.02, more weak background and ground structure remains. At 0.06, fewer distracting responses survive, but weaker contours break apart. The selected 0.04 keeps the subject and camera structure while reducing the fine texture seen with finite differences alone. The lost small details are the tradeoff for smoother edges.

The filters themselves

Gaussian G · 9 × 9
Gaussian G · 9 × 9σ = 1.5 pixels; weights sum to 1.
G * Dx · 9 × 10
G * Dx · 9 × 10Horizontal difference of the Gaussian.
G * Dy · 10 × 9
G * Dy · 10 × 9Vertical difference of the Gaussian.

The Gaussian is shown from zero (black) to its maximum (white). Both DoG displays use the same symmetric range, ±0.02768, with zero in middle gray. Their positive and negative lobes measure change after local averaging. Full convolution preserves the complete filter support, including the extra column or row introduced by the two-tap derivative.

Two convolutions or one?

(I * G) * Dx = I * (G * Dx)

Associativity lets the derivative be applied to the Gaussian first. Once the DoG kernels are prepared, each derivative needs just one convolution with the image. The comparison below uses the same input, kernels, zero extension, final crop, and threshold.

Blur, then differentiate
Blur, then differentiateTwo convolutions per direction, retaining the intermediate halo.
Differentiate the Gaussian, then filter
Differentiate the Gaussian, then filterOne image convolution per direction.
Two-step edges · threshold 0.04
Two-step edges · threshold 0.04
DoG edges · threshold 0.04
DoG edges · threshold 0.04

Agreement

Maximum magnitude difference: 8.26e-16. The binary maps differ at 0 pixels.

Boundary handling

Both routes keep the full convolution and crop only once at the end. Cropping between filters would change the padding and introduce border differences.

2.1 · Image sharpening

A Gaussian blur keeps low frequencies. Subtracting it leaves fine detail; adding α times that detail makes edges stronger.

sharp = I + α(I − G * I)

Combining the steps gives one unsharp mask filter, with δ equal to an impulse:

kernel = (1 + α)δ − αG

Taj Mahal

Original
Original
Blurred · σ = 1.5
Blurred · σ = 1.5
High frequencies · gray = zero
High frequencies · gray = zero
Sharpened · α = 0.5
Sharpened · α = 0.5
Sharpened · α = 1
Sharpened · α = 1
Sharpened · α = 2
Sharpened · α = 2

Telegraph Avenue

Original
Original
Blurred · σ = 1.5
Blurred · σ = 1.5
High frequencies · gray = zero
High frequencies · gray = zero
Sharpened · α = 0.5
Sharpened · α = 0.5
Sharpened · α = 1
Sharpened · α = 1
Sharpened · α = 2
Sharpened · α = 2

α = 0.5 is subtle; α = 1 strengthens stonework and bricks; α = 2 adds visible halos and emphasizes noise. The single-filter and blur/subtract/add implementations agree within 1.33e-15.

Blur, then sharpen

The street photo is blurred at σ = 2, then sharpened with σ = 2 and α = 1.5. Edges regain contrast, but fine detail stays soft. MSE falls from 0.00919 to 0.00719; sharpening does not fully undo the blur.

Sharp reference
Sharp reference
Blurred
Blurred
Resharpened
Resharpened
Reference crop
Reference crop
Blurred crop
Blurred crop
Resharpened crop
Resharpened crop

2.2 · Hybrid images

Combine the low frequencies of one image with the high frequencies of another. Fine detail dominates nearby; the broad shapes emerge farther away.

Kendrick + Drake

Drake supplies the low frequencies and Kendrick the high frequencies. Kendrick's fine features stand out nearby; Drake's broad face appears in the small view. Different facial proportions leave some overlapping mouth and cheek contours.

Kendrick nearby / Drake farther away · near
Kendrick nearby / Drake farther away · near
Kendrick nearby / Drake farther away · far
Kendrick nearby / Drake farther away · farSame result reduced to 40 × 40.

Sources and alignment

Drake · original
Drake · originalZach Gold
Kendrick · original
Kendrick · originalKendrick portrait
Drake · aligned
Drake · aligned
Kendrick · aligned
Kendrick · aligned

Rotation, uniform scale, and translation align the eyes at (120, 150) and (280, 150) on a 400 × 400 grayscale canvas. Both aligned images stay within their source photographs.

Filtering

low = blur(drake, 14)
high = kendrick - blur(kendrick, 4)
hybrid = low + 1.5 * high
Low-pass Drake · σ = 14
Low-pass Drake · σ = 14
High-pass Kendrick · σ = 4
High-pass Kendrick · σ = 4Before gain; gray is zero, range ±0.577.

Choosing the frequency split

Gain stays at 1.5. The 6/2 split leaves more Drake detail nearby. The selected 14/4 split makes Kendrick clearer, at the cost of more ghosting.

σ = 6 / 2
σ = 6 / 2
σ = 10 / 3
σ = 10 / 3
σ = 14 / 4 · selected
σ = 14 / 4 · selected

Reversing the same pair

Swap the same two photographs: Kendrick supplies low frequencies and Drake high frequencies. Alignment, σ = 14/4, and gain = 1.5 stay fixed.

Drake nearby / Kendrick farther away · near
Drake nearby / Kendrick farther away · near
Drake nearby / Kendrick farther away · far
Drake nearby / Kendrick farther away · farSame result reduced to 40 × 40.

Fourier analysis

Log-magnitude spectra share the same scale. Low-pass energy concentrates near the center; high-pass filtering suppresses that center. The hybrid combines both frequency ranges.

Drake input
Drake input
Kendrick input
Kendrick input
Low-pass Drake
Low-pass Drake
High-pass × 1.5
High-pass × 1.5
Hybrid
Hybrid

Computed with log1p(abs(fftshift(fft2(image)))) on unclipped arrays.

The other pairs also use grayscale and eye alignment on a 400 × 400 canvas. Nutmeg needs reflected border pixels (3.9% of the canvas). Small views are the same results reduced to 40 pixels.

Derek + Nutmeg

σ low/high = 7/4; gain = 1. Nutmeg's fur and eyes dominate nearby; Derek's face becomes clearer at a distance. The ears still leave some ghosting.

Derek
Derek
Nutmeg
Nutmeg
Hybrid · near
Hybrid · near
Hybrid · far
Hybrid · far

Outdoor smile + indoor portrait

σ low/high = 6/3; gain = 1. Glasses and the neutral expression dominate nearby; the outdoor smile appears farther away. Different head poses leave some overlapping contours.

Outdoor portrait
Outdoor portrait
Indoor portrait
Indoor portrait
Hybrid · near
Hybrid · near
Hybrid · far
Hybrid · far

2.3 · Gaussian and Laplacian stacks

Gaussian stack

Blur the original at σ = 2, 4, 8, 16, and 32. G0 is the input. Every level keeps the same resolution, with no downsampling or built-in pyramid functions.

Laplacian stack

Lk = Gk − Gk+1. The last level keeps G5. Adding all six levels reconstructs the original, including its broad color and brightness.

Gaussian levels

Apple

G0 · σ = 0
G0 · σ = 0
G1 · σ = 2
G1 · σ = 2
G2 · σ = 4
G2 · σ = 4
G3 · σ = 8
G3 · σ = 8
G4 · σ = 16
G4 · σ = 16
G5 · σ = 32
G5 · σ = 32

Orange

G0 · σ = 0
G0 · σ = 0
G1 · σ = 2
G1 · σ = 2
G2 · σ = 4
G2 · σ = 4
G3 · σ = 8
G3 · σ = 8
G4 · σ = 16
G4 · σ = 16
G5 · σ = 32
G5 · σ = 32

Laplacian levels

Gray is zero. Each band has its own display scale, shared by apple and orange; the residual uses ordinary RGB values.

Apple

L0
L0
L1
L1
L2
L2
L3
L3
L4
L4
Residual · G5
Residual · G5

Orange

L0
L0
L1
L1
L2
L2
L3
L3
L4
L4
Residual · G5
Residual · G5

Figure 3.42 (a)–(l)

Columns: masked apple, masked orange, and their sum. Rows: L0, L2, L4, then reconstruction using all six levels.

Each frequency row shares one signed scale. The bottom contributions are clipped previews; the final blend adds the raw contributions before clipping.

(a) First contribution · L0
(a) First contribution · L0
(b) Second contribution · L0
(b) Second contribution · L0
(c) Combined · L0
(c) Combined · L0
(d) First contribution · L2
(d) First contribution · L2
(e) Second contribution · L2
(e) Second contribution · L2
(f) Combined · L2
(f) Combined · L2
(g) First contribution · L4
(g) First contribution · L4
(h) Second contribution · L4
(h) Second contribution · L4
(i) Combined · L4
(i) Combined · L4
(j) First reconstructed
(j) First reconstructed
(k) Second reconstructed
(k) Second reconstructed
(l) Final blend
(l) Final blend

Basketball + soccer across scales

Fine bands retain the seams and surface texture; coarse levels keep the round shape and broad shading. Selected levels of the final blend:

G0 · σ = 0
G0 · σ = 0
G2 · σ = 4
G2 · σ = 4
G5 · σ = 32
G5 · σ = 32
L0
L0
L2
L2
Residual · G5
Residual · G5

Golden Gate across scales

Fine bands isolate cables and foliage; coarse levels retain towers, hills, and the transition to color. Selected levels of the final blend:

G0 · σ = 0
G0 · σ = 0
G2 · σ = 4
G2 · σ = 4
G5 · σ = 32
G5 · σ = 32
L0
L0
L2
L2
Residual · G5
Residual · G5

2.4 · Multiresolution blending

Build Laplacian stacks for the images and a Gaussian stack for the mask. Blend each band with its matching mask, then add the bands. Fine detail crosses a narrow seam while broad color transitions more gradually.

result = Σ [MkLAk + (1 − Mk)LBk]

Apple + orange

A vertical mask selects apple on the left and orange on the right. Multiresolution blending softens the color seam while keeping the texture.

First image
First image
Second image
Second image
Mask · white selects first
Mask · white selects first
Direct cut
Direct cut
Blurred mask · σ = 32
Blurred mask · σ = 32
Multiresolution blend
Multiresolution blend

Mask across scales

σ = 0
σ = 0
σ = 2
σ = 2
σ = 4
σ = 4
σ = 8
σ = 8
σ = 16
σ = 16
σ = 32
σ = 32

Basketball + soccer

Both balls are centered and resized to a 256-pixel diameter on a 320 × 320 white canvas. A simple vertical mask joins the basketball’s left half to the soccer ball’s right half. The silhouette stays continuous while orange fades into white.

First image
First imagePhotography by User: MrX · photo · CC BY-SA 3.0 · adapted
Second image
Second imageCAPS LOCK; crop by Kmccoy; transparent background by Kmf164 · photo · CC BY-SA 3.0 · adapted
Mask · white selects first
Mask · white selects first
Direct cut
Direct cut
Blurred mask · σ = 32
Blurred mask · σ = 32
Multiresolution blend
Multiresolution blendBlend: CC BY-SA 3.0

Golden Gate · construction to 2013

The 1930s photograph and the 2013 view are aligned at two tower tops. A curved, irregular mask joins construction on the left to the finished bridge on the right. The viewpoints are close, but foreground and cable differences remain.

First image
First imageUS National Park Service · photo · Public domain (US federal government) · adapted
Second image
Second imageLeandro Neumann Ciuffo · photo · CC BY 2.0 · adapted
Mask · white selects first
Mask · white selects first
Direct cut
Direct cut
Blurred mask · σ = 32
Blurred mask · σ = 32
Multiresolution blend
Multiresolution blend

Golden Gate · masked frequency bands

Construction-era contribution, 2013 contribution, and their sum at L0, L2, and L4. The last row reconstructs all six levels, including the residual. Display conventions match Figure 3.42 above.

(a) First contribution · L0
(a) First contribution · L0
(b) Second contribution · L0
(b) Second contribution · L0
(c) Combined · L0
(c) Combined · L0
(d) First contribution · L2
(d) First contribution · L2
(e) Second contribution · L2
(e) Second contribution · L2
(f) Combined · L2
(f) Combined · L2
(g) First contribution · L4
(g) First contribution · L4
(h) Second contribution · L4
(h) Second contribution · L4
(i) Combined · L4
(i) Combined · L4
(j) First reconstructed
(j) First reconstructed
(k) Second reconstructed
(k) Second reconstructed
(l) Final blend
(l) Final blend

Lesson I learned

The biggest lesson was that different spatial frequencies need different treatment. Sharpening strengthens existing detail, hybrid images change which features stand out at a distance, and multiresolution blending softens color transitions while preserving texture. These operations can change how an image looks, but they cannot recover missing detail or fix mismatched geometry.