7.14. Gaussian smoothing and edges

Two jobs dominate what neighbourhood windows get used for in classical machine vision: smoothing pixel-to-pixel variation cleanly, and finding the edges where the image changes sharply. The Gaussian filter is the standard tool for the first, the Laplacian-based detectors the standard tool for the second – and the two compose, because every edge detector works better on a lightly smoothed input.

7.14.1. The Gaussian filter

gaussian() is the centre-weighted cousin of mean(). Both compute an average over each pixel’s neighbourhood, but the Gaussian’s weights are not uniform: pixels nearer the centre of the neighbourhood count more, pixels at the edge of the neighbourhood count less, with the weights following the familiar bell curve that gives the filter its name.

The bell-shaped weighting is what makes a Gaussian filter smoother than a box average. Mean filtering can produce visible artefacts at the edges of objects – a hard cut-off in the weighting introduces small ringing patterns at sharp transitions. The Gaussian’s smoothly-falling weights avoid that ringing and produce a result that looks closer to what “blurred” should look like. The cost is more per-pixel computation than the mean filter, but not dramatically so – the per-pixel cost is still much lower than the bilateral filter.

img.gaussian(1)    # 3x3 Gaussian -- a clean light blur
img.gaussian(2)    # 5x5 Gaussian -- stronger smoothing

Gaussian smoothing is the standard first stage of almost every edge-detection pipeline. The edge detectors below all amplify high-frequency content, including the sensor noise the algorithm does not actually want to detect. Running a light Gaussian first suppresses that noise without softening real edges much, leaving the edge detector to find real edges instead of speckle.

7.14.2. Unsharp masking

A Gaussian-blurred copy of an image is the raw material the unsharp mask technique uses for classical sharpening. Setting unsharp=True on the filter switches it from “produce the blurred image” to “subtract the blurred image from the original and add the difference back to the original” – the effect is that high-frequency edges get amplified relative to smooth interiors.

img.gaussian(1, unsharp=True)

The optional mul and add parameters scale the strength of the unsharp result; the defaults (mul=1.0, add=0.0) are a moderate sharpening that does not exaggerate sensor noise.

7.14.3. The Laplacian filter

laplacian() runs a discrete approximation of the second spatial derivative of the image. The output is large where pixel values change quickly, near zero where they are constant or changing linearly. The natural reading of the result is an edge response: pixels where the image changes rapidly light up, pixels in smooth interiors stay dark.

img.laplacian(1)   # 3x3 Laplacian -- edge response

The same parameters as gaussian are available. sharpen=True produces a sharpened image (the Laplacian added back into the original rather than returned on its own). mul and add scale the response.

A practical use beyond edge detection is focus measurement. The Laplacian response averaged across a region gives a rough measure of how much high-frequency content the region carries; on a well-focused frame that average is high, on a blurry frame it drops. Comparing the Laplacian response across frames is the cheap way to ask “is the lens focused?” without needing a more expensive contrast metric.

7.14.4. The find_edges method

find_edges() runs a complete edge-detection pipeline rather than just an edge-response filter. It works on grayscale images, and the result is a binary image whose non-zero pixels mark the positions where the input has the kind of brightness change that should count as an edge.

The method takes an edge_type parameter that picks between two algorithms:

EDGE_SIMPLE runs a high-pass filter, applies a threshold, and returns the result. Fast, but the output includes every brightness change above the threshold, including noise and texture that the application probably does not care about. Reasonable for clean images and for cases where the noise is going to be cleaned up by a later morphological pass.

EDGE_CANNY runs the Canny edge detector – the classical multi-stage algorithm. It computes the brightness gradient, suppresses every non-maximum response along the gradient direction (so each edge is one pixel wide), and applies a hysteresis threshold (so an edge that is strong in one place gets traced even where it fades between). The result is a clean, thin, connected set of edge pixels of the kind every classical edge-based algorithm wants.

The threshold parameter is a two-element tuple (low, high). For EDGE_CANNY, the high value is the cutoff above which a pixel is definitely an edge, and the low value is the cutoff above which a pixel is an edge only if it is connected to a definite one. For EDGE_SIMPLE, only the high value matters; it is the single cutoff above which a pixel counts as an edge. The default of (100, 200) is a starting point worth tuning for the specific scene.

img.gaussian(1)                                   # pre-smooth
img.find_edges(image.EDGE_CANNY, threshold=(50, 100))

The Canny detector is the better choice for almost every application where edges matter. The faster EDGE_SIMPLE is worth remembering for the cases where the cost of Canny is a problem and the noise rejection of its hysteresis is not actually needed.

7.14.5. Adaptive thresholding on the Gaussian

Like the statistical filters, gaussian() accepts the threshold=True / offset=N keyword pair for adaptive thresholding. The behaviour is the same as with mean(): the Gaussian statistic at each position becomes the local cutoff, and the source pixel is compared against the statistic plus the offset to produce a binary result.

The Gaussian variant is usually the cleanest choice for adaptive thresholding when the input is reasonably noise-free. The weighted average gives a smoother cutoff than the mean filter produces, with fewer artefacts at sharp illumination transitions.