Skillversitas

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 typeRight filterWhy
General sensor noise (Gaussian-ish)GaussianBlurmatched to the noise statistics, smooth result
Salt-and-pepper (isolated bright/dark specks)medianBlura single outlier cannot be the median of a neighborhood
Noise with important edgesbilateralFiltercolor-weighted: edges survive, flats smooth
Speed-critical, quality-tolerantblura 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.