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, high pass

Joshua Stough DIP 3.6 on high-pass. See correlate

Here we’re going to look at the high-pass spatial filter, which derives from the local second derivative. See these cool visualizations of the Gaussian and its derivatives, and read here.

((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))

 

High-pass filtering, involves computing a pixel’s difference from its neighborhood

array([[-1., -1., -1.], [-1., 8., -1.], [-1., -1., -1.]])

Where does this kernel come from? In 1D, the second derivative of a sampled signal is approximated by the difference of differences:

f′′(x)≈[f(x+1)−f(x)]−[f(x)−f(x−1)]=f(x+1)−2f(x)+f(x−1)f''(x) \approx [f(x+1) - f(x)] - [f(x) - f(x-1)] = f(x+1) - 2f(x) + f(x-1)

which is correlation with [1,−2,1][1, -2, 1]. Adding the second differences in xx and yy gives the 4-neighbor Laplacian kernel with -4 at the center and 1 at the four direct neighbors, and including the diagonals gives the 8-neighbor version. Our kernel is the negative of that 8-neighbor Laplacian (8 at the center, -1 around it); the sign convention is chosen so that we can simply add it to sharpen.

Two properties to notice:

  • The weights sum to zero, so in any flat region the response is exactly 0. Unlike the blurring kernels, this filter throws away the average brightness and keeps only the differences. It’s a high-pass filter.

  • Since 8=9−18 = 9 - 1, this kernel is exactly 9×9 \times (the pixel minus the average of its 3×33 \times 3 neighborhood). So it measures how much each pixel stands out from its surroundings.

Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers). Got range [-1.5254902429878712..4.552941180765629].
<Figure size 800x300 with 2 Axes>

The filtered image is mostly black: about two-thirds of its values are within ±0.05\pm 0.05 of zero, the flat regions. What’s left are outlines: the cat’s fur, whiskers and eyes, and the stripes of the pillow. (I_laplace actually ranges from about -1.5 to 4.3; imshow clips the display to [0,1][0,1], so we’re only seeing its positive part here.)

 

Some fraction of the LaPlace can simply be added to the original image

to enhance edges.

<Figure size 1000x300 with 4 Axes>

Adding the Laplacian back to the image, I+αLI + \alpha L, boosts each pixel by how much it differs from its neighbors: pixels brighter than their surroundings get brighter, darker ones get darker. That makes edges crisper. With larger α\alpha the image starts to look harsh and noisy, since every bit of fine texture and noise is amplified along with the edges.

If this sounds familiar, it should. Since L=9(I−Iˉ)L = 9(I - \bar{I}) where Iˉ\bar{I} is the 3×33 \times 3 average, adding αL\alpha L is exactly the unsharp masking we did earlier with a 3×33 \times 3 blur and k=9αk = 9\alpha. Sharpening by “adding the Laplacian” and sharpening by “subtracting a blur” are the same idea.

 

Interactive Visualization

See jupyter-matplotlib

<Figure size 640x480 with 0 Axes>
<Figure size 800x400 with 2 Axes>

 

🧠 Further Efforts

So far, we’ve used a simple 3×33 \times 3 Laplacian kernel. These problems explore the alternatives, and the scale at which we look for edges.

  1. Four neighbors or eight? Repeat the sharpening with the 4-neighbor kernel described above (4 at the center, -1 at the four direct neighbors, 0 at the corners). How does the result differ from the 8-neighbor version? Look in particular at diagonal edges and at the overall strength of the effect.
    Guidance: Use the same addLap approach and the same fractions, and compare with vis_pair so you can zoom into the same region of both.

  2. The Laplacian at different scales. The Laplacian can be computed at different scales using Gaussian derivatives, the Laplacian of Gaussian, which lets us enhance detail at a coarser or finer level:

    from scipy.ndimage import gaussian_laplace
    I_log = np.stack([gaussian_laplace(I[...,i], sigma=2) for i in range(3)], axis=-1)

    Add different amounts of this to the original image, as we did before, at sigma of 1, 2 and 4. How do the edges detected at sigma=1 differ from those at sigma=2 or sigma=4? What happens to fine details versus coarse structures as sigma grows?
    Guidance: Watch the sign: gaussian_laplace computes the Laplacian itself, while our laplace_h is its negative (chosen so that we could add it). Also check the range of I_log with arr_info at each sigma: the response gets weaker as sigma grows, so a fair comparison needs a different fraction at each scale (multiplying by σ2\sigma^2 is the standard fix). See filters_explore3D for what these kernels look like.

  3. Two sliders. Write a version of addLap that takes both a sigma and a fraction, and build an interactive figure with a slider for each, to explore the scale and amount of sharpening together.
    Guidance: Follow the slider pattern above, with one update function observed by both sliders. Recomputing gaussian_laplace on every slider move is slow, so precompute the response for a handful of sigma values and let that slider choose among them.