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.

Preview: Exploring spatial filtering, low pass

Joshua Stough DIP 3.4 and 3.5 on spatial filtering and low-pass. See correlate

Here we’re going to look at a couple of spatial filters that reduce high frequency information. In other words, they blur the image.

  • First is a simple averaging, as we’ve done before.

  • Second is a Gaussian, which gives more weight to pixels closer to the center and less farther away. The Gaussian has better properties with respect to frequency information. We’ve seen Gaussian histograms, but you can read more about them in DIP 2.6.

((512, 512, 4), dtype('float64'), np.float64(0.0), np.float64(1.0))
<Figure size 900x300 with 2 Axes>
((512, 512, 3), dtype('float64'), np.float64(0.0), np.float64(0.9882352948188782))

 

Low-pass filtering, or blurring, involves averaging neighborhoods of pixels,

thereby reducing the difference between neighboring pixels. We can do this in a number of ways. We’ve already seen local, equal weight averaging. We’re going to use a relatively large kernel size (filter size) to see the differences between the filters.

Note also, we’ll apply the filter to each channel separately. Zoom in on the figure to see the local differences.

Correlation, kernels, and normalization

correlate slides the kernel (or filter, or mask) hh over the image ff. At each pixel it computes a weighted sum of the neighborhood under the kernel, with the kernel values as the weights. For a kernel of size (2a+1)×(2b+1)(2a+1) \times (2b+1) centered on the pixel:

g(r,c)=∑s=−aa∑t=−bbh(s,t) f(r+s,c+t)g(r, c) = \sum_{s=-a}^{a} \sum_{t=-b}^{b} h(s, t)\, f(r+s, c+t)

Our mean_h is 11×1111 \times 11 with every entry 1121\frac{1}{121}, so gg is just the average of the 11×1111 \times 11 neighborhood. The weights sum to 1, which is what keeps the overall brightness unchanged: a region of constant intensity stays exactly the same. A kernel whose weights summed to 2 would also double the brightness.

A few practical details:

  • Convolution is the same operation with the kernel flipped (f(r−s,c−t)f(r-s, c-t) instead of f(r+s,c+t)f(r+s, c+t)). It’s the version with the nice mathematical properties, as we’ll see with the Fourier transform. For symmetric kernels like both of ours, correlation and convolution give identical results.

  • Borders: near the edge of the image, part of the kernel hangs off the image. scipy.ndimage.correlate handles this by default by reflecting the image at its border (mode='reflect'). Other choices are padding with zeros (mode='constant', which darkens the borders of a blurred image) or repeating the edge pixel (mode='nearest').

  • Cost: an 11×1111 \times 11 kernel means 121 multiply-adds per pixel per channel. Large blurs get expensive, which is one motivation for separable filters (below) and for filtering in the frequency domain.

<Figure size 800x300 with 2 Axes>

 

We can also try a weighted average that more highly weights

the pixels in the center of the neighborhood. A Gaussian is a good choice here.

The 2D Gaussian kernel is built as the outer product of a 1D Gaussian with itself: h(s,t)=g(s) g(t)h(s,t) = g(s)\,g(t). A kernel like this is called separable. Filtering with it is equivalent to filtering every row with the 1D gg and then every column with gg again, which costs 11+11=2211 + 11 = 22 operations per pixel instead of 121. (The box filter is separable too.) We divide by the sum so that, as with the average, the weights add to 1.

<Figure size 640x480 with 1 Axes>
<Figure size 800x300 with 2 Axes>
<Figure size 800x300 with 3 Axes>

Beyond the amount of blur, there’s a difference in its character, though on a natural photo like this one it’s subtle. The box filter weights every pixel in its square equally and then cuts off abruptly. A Gaussian’s weights fall off smoothly and evenly in every direction, which is much closer to how an out-of-focus lens blurs. In frequency terms, the Gaussian is a much cleaner low-pass filter. It attenuates fine detail smoothly, while the box filter lets some fine, regular patterns leak back through, and on striped or checkered textures that can produce visible artifacts. We’ll make that precise in the Fourier chapter.

 

Let’s consider a midline through the image, and see what the

two filters did to the intensity values.

<Figure size 800x400 with 2 Axes>

Along a single row, both filters follow the broad shape of the intensity profile while shaving off the narrow spikes (fur texture, the sharp edges of the eye). This is “low-pass” in action: slow variation passes through, rapid variation is attenuated. The box average flattens the spikes more, consistent with its wider effective width.

 

Interactive Visualization

We’re going to interactively visualize the Gaussian blur as you change the sigma/width of the gaussian.

See jupyter-matplotlib

<Figure size 640x480 with 0 Axes>
<Figure size 1000x300 with 3 Axes>

As you increase σ\sigma, the kernel spreads out and the blur grows. But notice that make_gaus always builds a 25×2525 \times 25 kernel. A Gaussian has most of its weight within about 3σ3\sigma of the center, so a kernel needs to be about 6σ+16\sigma + 1 wide to hold it. Past roughly σ=4\sigma = 4, our kernel cuts off the Gaussian’s tails. By σ=20\sigma = 20 the kernel is nearly flat across its 25 pixels, so we’re really applying something close to a 25×2525 \times 25 box filter, and the image stops getting blurrier. Watch the kernel display in the middle panel as you slide.