Image Smoothing and Filters
Smoothing filters replace each pixel with a weighted combination of its neighborhood, and the weighting is the whole story: a box blur averages, a Gaussian blur weights by distance, a median blur takes the middle value, and a bilateral blur weights by both distance and color similarity. Each one denoises in a different way and damages detail in a different way, so the choice is a deliberate trade - and the kernel size plus sigma parameters control how aggressive the trade is.
The Blur Family
import cv2
img = cv2.imread("noisy.jpg")
# box blur: plain average over the kernel - fast, flattens edges
box = cv2.blur(img, (5, 5))
# gaussian: distance-weighted - the default general-purpose blur
gauss = cv2.GaussianBlur(img, (5, 5), 0) # sigma 0 = computed from size
# median: the neighborhood's middle value - the salt-and-pepper killer
med = cv2.medianBlur(img, 5)
# bilateral: smooths similar pixels, preserves strong edges
bil = cv2.bilateralFilter(img, 9, 75, 75) # d, sigmaColor, sigmaSpace
cv2.imwrite("box.jpg", box)
cv2.imwrite("gauss.jpg", gauss)
cv2.imwrite("median.jpg", med)
cv2.imwrite("bilateral.jpg", bil)
The kernel size is always odd (3, 5, 7) and odd-sized because it has a center. For GaussianBlur, passing sigmaX=0 lets OpenCV derive the sigma from the kernel size - the normal case. The four filters, in plain terms: box is the cheapest average, Gaussian is the standard smooth, median is the impulse-noise specialist, and bilateral is the edge-aware one that smooths flat regions while leaving boundaries sharp - the one used for "smoothish the photo without blurring the eyes."
Choosing the Right Blur
| Noise type | Right filter | Why |
|---|---|---|
| General sensor noise (Gaussian-ish) | GaussianBlur | matched to the noise statistics, smooth result |
| Salt-and-pepper (isolated bright/dark specks) | medianBlur | a single outlier cannot be the median of a neighborhood |
| Noise with important edges | bilateralFilter | color-weighted: edges survive, flats smooth |
| Speed-critical, quality-tolerant | blur | a simple average, the fastest of the four |
The median filter is the one to reach for on impulse noise - a Gaussian blur smears a single hot pixel across its neighbors, while the median simply discards it. The bilateral filter is the premium option: its sigmaColor controls how much color difference is allowed before a neighbor stops contributing (low = preserves more edges), and sigmaSpace the spatial reach. Both sigmas are the real knobs, not the kernel size.
Smoothing as a Preprocessing Step
import cv2
img = cv2.imread("photo.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# smoothing BEFORE edge detection: Canny on the raw image is spotty,
# on a blurred image the edges are continuous
raw_edges = cv2.Canny(gray, 100, 200)
smooth_edges = cv2.Canny(cv2.GaussianBlur(gray, (5, 5), 0), 100, 200)
# the denoise-specialist for higher-fidelity smoothing
nlf = cv2.fastNlMeansDenoising(gray, None, 10, 7, 21)
cv2.imwrite("edges_raw.jpg", raw_edges)
cv2.imwrite("edges_smooth.jpg", smooth_edges)
cv2.imwrite("denoised.jpg", nlf)
Blur-then-edge is the standard pipeline because Canny's gradient computation is noise-sensitive: a single speck creates a spurious edge, and a light Gaussian before the Canny merges the noise into a clean contour. fastNlMeansDenoising is the non-local-means denoiser - slower than a Gaussian but far better at preserving texture while removing noise, the right tool when the image itself is the product (a document scan, a medical image) rather than a preprocessing step for another algorithm.
What the Kernel Does
Every linear filter is a convolution: slide the kernel over the image, multiply elementwise, sum. The box kernel is all-equal weights; the Gaussian kernel is a 2-D bell curve, normalized to sum to 1. You can supply your own kernel with filter2D for anything the presets do not cover:
import cv2
import numpy as np
img = cv2.imread("photo.jpg")
# a sharpening kernel - the center boosted, neighbors subtracted
sharp = np.array([[0, -1, 0],
[-1, 5, -1],
[0, -1, 0]], dtype="float32")
sharpened = cv2.filter2D(img, -1, sharp)
cv2.imwrite("sharpened.jpg", sharpened)
The sharpening kernel above is the inverse idea of a blur: it adds back the difference between the center and its neighborhood, which accentuates edges. Designing kernels is where "filtering" becomes "image processing" - the edge detectors in the next chapter are, at their core, specific kernels (Sobel is a directional difference kernel) applied with an interpretation on top.
Summary
Pick the blur by the noise: GaussianBlur for general sensor noise (the default), medianBlur for salt-and-pepper (an outlier cannot be a median), bilateralFilter when edges must survive (tune sigmaColor/sigmaSpace), and blur when speed is the priority. Use smoothing as a preprocessing step - blur before Canny to get continuous edges - and reach for fastNlMeansDenoising when the image itself is the deliverable. Understand that every linear filter is a convolution, and the filter2D custom-kernel form (like the sharpening kernel) is how the presets are built.