Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

stough 202-

Preview: Using Fourier Transform for Blurring, Sharpening

DIP 4.7 shows similar.

We saw in fft_intro that we can denoise an image in the Fourier space, also called the frequency domain. We also saw that we can consider the magnitude and phase components separately given the Fourier Transform of an image. Here we’re going to play around further in the Fourier domain and show how we can blur and sharpen an image through clever manipulation in the frequency domain.

Imports

We’ll use the Fast Fourier Functions again.

Blur, Edginess, Sharpen

In this worksheet we’ll take an image and manipulate it in the frequency space.

  • Use fftn to convert to the Fourier domain

  • Scale and manipulate the spectrum coefficients in the Fourier domain.

  • Use ifftn to convert back to the image/spatial domain.

Of note in this exercise is that visualization can be a bear. We’ll use fftshift to visualize the Fourier domain in a way that shows the low-frequency components in the center of the image and higher frequencies out towards the extremes.

((1536, 2048, 3), dtype('float64'), np.float64(0.0), np.float64(1.0))
<Figure size 400x300 with 1 Axes>
((1536, 2048, 3), dtype('complex128'), np.complex128(-231815.0537404591-282716.775763718j), np.complex128(4825197.364705882-0j))

One subtlety: fftn transforms along every axis, so on our 1536×2048×31536 \times 2048 \times 3 color image it also takes a (tiny, 3-point) Fourier transform across the color channels. Everything we do below scales the spectrum the same way for all three of those color “frequencies”, so the result is exactly the same as filtering each channel separately with a 2D FFT. If you want to be explicit, fft2(I, axes=(0,1)) transforms only the two spatial axes.

Visualizing in the Fourier domain

A bit difficult artifact about fft is that the output places the lower-frequency coefficients at the corners of the transform array. Normally however, we wish to visualize the spectrum such that lower frequencies are in the center of the figure. We’ll use fftshift to do this.

<Figure size 400x300 with 1 Axes>

Using a Gaussian for smooth scaling

In fft_intro we simply zero’d out all the coefficients outside of some square in the fourier space in order to eliminate high-frequency content. However this can lead to weird artifacts called ringing (the Gibbs phenomenon): ripples alongside strong edges, caused by the abrupt cutoff. The proper way to manipulate the fourier domain is through smoother scaling, like the Gaussian. Below we’ll define a function that generates a 2D Gaussian of the same shape as an input image. We did something like this using scipy’s norm and np.outer in color_quantization. Here, we give ourselves a little more flexibility by directly evaluating the Gaussian exponential at all the pixel coordinates we want.

N(μ,σ)(x)=e−(x−μ)2/2σ2\mathcal{N}(\mu, \sigma)(x) = e^{-(x-\mu)^2/2\sigma^2}
<Figure size 400x300 with 1 Axes>

Blur: Use the Gaussian as a scalar multiple in the frequency domain

Great reference for matplotlib surface visualizations. Here I just want to visualize what the scalar we’ll use looks like over the image, as a surface in 3D.

<Figure size 400x400 with 2 Axes>

The Actual scaling in frequency space

We want to multiply the Fourier coefficients of the image by this Gaussian image, thereby scaling the coefficients. We just have to remember that I_fft needs to be fftshift’d to make the pixels line up correctly. We only do the shifting for visualization until now. Note that the variable temp comes from several cells ago, when we first visualized I_fft.

<Figure size 800x300 with 2 Axes>
<Figure size 400x300 with 1 Axes>

Now, we’ll invert the fourier transform to go back to image space.

((1536, 2048, 3), dtype('complex128'), np.complex128(0.07292675544127314-2.752391556703007e-18j), np.complex128(0.9999999999765902+1.777827989816071e-18j))
<Figure size 800x300 with 2 Axes>

The blur is gentle: the fine texture of the trees and village is softened, but the overall scene is intact. The center of the spectrum, the low frequencies, is multiplied by nearly 1 and passes through untouched, while frequencies farther out are increasingly suppressed.

 

The Convolution Theorem

Here’s the deep connection between this notebook and spatial filtering. The convolution theorem says that convolving an image with a kernel in the spatial domain is exactly the same as multiplying their Fourier transforms:

F{f∗h}=F{f}⋅F{h}\mathcal{F}\{f * h\} = \mathcal{F}\{f\} \cdot \mathcal{F}\{h\}

So multiplying the spectrum by our Gaussian was a convolution with some spatial kernel: the one whose Fourier transform is that Gaussian. And the Fourier transform of a Gaussian is another Gaussian, with its width inverted. A Gaussian of standard deviation σf\sigma_f in frequency corresponds to a spatial Gaussian of standard deviation

σs=N2πσf\sigma_s = \frac{N}{2\pi\sigma_f}

along an axis of NN pixels. A narrow window in frequency (keep only the very lowest frequencies) means a wide blur in space, and vice versa. Our image isn’t square, so the same σf=100\sigma_f = 100 gives a slightly different σs\sigma_s along each axis. Let’s check by blurring the image directly with scipy.ndimage.gaussian_filter. One detail: the FFT treats the image as if it repeats periodically, so to match exactly we tell gaussian_filter to wrap around at the borders.

Equivalent spatial sigmas: 2.44 rows, 3.26 columns
Largest difference from the frequency-domain blur: 2.9446101112995393e-10

Identical, up to floating-point round-off. Two routes to the same blur: a weighted average of a small neighborhood in space, or a multiplication in frequency. Which is faster depends on the kernel size. A small kernel is cheap to apply directly, but the cost of direct convolution grows with the kernel’s area, while the FFT route costs about the same no matter how wide the blur.

This also explains the earlier comparison between box and Gaussian blurs. The Fourier transform of a box is a sinc\text{sinc} function, which ripples, with lobes that go negative and back again at higher and higher frequencies. So a box blur doesn’t steadily suppress high frequencies: it lets some of them back through, flipped in sign. The Gaussian’s transform just falls off smoothly.

Edge Finder: Use the complement of the Gaussian as the scalar

Thereby keeping only the higher frequency content.

<Figure size 400x400 with 2 Axes>
<Figure size 400x300 with 1 Axes>
((1536, 2048, 3), dtype('float64'), np.float64(-0.5787901883732011), np.float64(0.6158012435677704))
((1536, 2048, 3), dtype('float64'), np.float64(0.0), np.float64(1.0))
<Figure size 800x300 with 2 Axes>

Using 1−Gaussian1 - \text{Gaussian} keeps the high frequencies and suppresses the low ones, a high-pass filter. The result is dark wherever the image is smooth, including the sky, the haze, and the broad slopes, and bright only at fine detail and edges: the rooftops, the tree line, the ridge. It’s the frequency-domain cousin of the Laplacian. Notice that the low-pass and high-pass scalings add up to exactly 1, so I_blurred + I_edges gives back the original image: we’ve split the image into its smooth part and its detail.

 

🔨 Your Turn: Sharpen in the Frequency Domain

We’ve blurred the image by scaling its spectrum with a Gaussian, and isolated its detail by scaling with one minus a Gaussian. How might we potentially sharpen the image?

Task: Sharpen I by scaling its Fourier transform, and show the original and sharpened images side by side. Plot your scaling factor as a surface, the way we did for the Gaussian, and explain its shape.

Guidance:

  • Recall unsharp masking in the spatial domain: the original image plus some multiple of its detail. You already have both of those here.

  • The Fourier transform is linear: the transform of a sum of images is the sum of their transforms, and the transform of kk times an image is kk times its transform. So you can do all of your work as one multiplication in the frequency domain. What single scaling factor would do it?

  • As above, the factor is built centered, so fftshift the transform before multiplying and ifftshift it after. Take .real of the inverse transform, and np.clip to [0,1][0,1] for display.

  • Try a few amounts of sharpening and a few Gaussian widths. Zoom in on the village and the trees to compare.