Where the grain in an image comes from, and four filters that remove it without losing too much.
Noise is random variation in pixel intensity: an unwanted signal on top of the real one.
It degrades image quality and affects processing algorithms: edge detectors, thresholds and feature detectors all respond to noise as if it were detail.
noisy = clean + noiseOne row of pixels (dashed line): clean vs noisy
import cv2
import numpy as np
# generate gaussian noise
mean = 0
sigma = 25
gaussian_noise = np.random.normal(mean, sigma, img.shape)
# ans: mean=0, sigma=25 controls noise strength
# add noise to image
noisy = img + gaussian_noise # ans:
noisy = np.clip(noisy, 0, 255).astype(np.uint8) # ans:
# ans: clip to the valid range [0, 255]
# add salt-and-pepper noise
def add_salt_pepper(img, prob=0.01):
noisy = img.copy()
# salt (white pixels)
num_salt = int(prob * img.size * 0.5)
coords = [np.random.randint(0, i, num_salt) for i in img.shape[:2]]
noisy[coords[0], coords[1]] = 255
# pepper (black pixels)
num_pepper = int(prob * img.size * 0.5)
coords = [np.random.randint(0, i, num_pepper) for i in img.shape[:2]]
noisy[coords[0], coords[1]] = 0
return noisy
# ans: prob controls the fraction of noisy pixels
It reduces noise but also blurs edges: the filter cannot tell an edge from noise.
# gaussian blur for noise reduction
denoised = cv2.GaussianBlur(noisy, (5, 5), 0) # ans:
# ans: 5x5 kernel, sigma computed from the size
# larger kernel for more smoothing
denoised = cv2.GaussianBlur(noisy, (9, 9), 0)
# ans: 9x9 kernel removes more noise, more blur
# specific sigma value
denoised = cv2.GaussianBlur(noisy, (0, 0), sigmaX=2.0)
# ans: kernel size computed from sigma=2.0
# different sigma for x and y
denoised = cv2.GaussianBlur(noisy, (0, 0), sigmaX=2.0, sigmaY=1.0)
# ans: anisotropic blur (different in x and y)
# median blur (best for salt-and-pepper)
denoised = cv2.medianBlur(noisy, 5) # ans:
# ans: 5x5 kernel, takes the median of 25 pixels
# larger kernel for heavy noise
denoised = cv2.medianBlur(noisy, 9)
# ans: 9x9 kernel for strong salt-and-pepper noise
# why does median work for salt-and-pepper?
# ans: the median ignores extreme values (0 or 255)
# ans: and keeps original pixel values better
Smooths noise while keeping edges sharp.
Bright = high weight. The weights stop at the edge, so the two sides are never averaged together.
# bilateral filter (edge-preserving denoising)
denoised = cv2.bilateralFilter(noisy, d=9, sigmaColor=75, sigmaSpace=75) # ans:
# d: diameter of pixel neighborhood
# ans: larger d = considers more neighbours, slower
# sigmaColor: intensity difference threshold
# ans: larger value = more colours averaged (more blur)
# sigmaSpace: spatial distance threshold
# ans: larger value = farther pixels considered
# when to use bilateral?
# ans: to preserve edges while removing noise
# ans: better than Gaussian for natural images
NLM compares patches, not just neighbourhoods.
← Click a pixel. Cyan: its 7×7 patch. Yellow brightness: how similar each patch in the 21×21 search window is, which is its weight.
# non-local means denoising (grayscale)
gray_noisy = cv2.cvtColor(noisy, cv2.COLOR_BGR2GRAY)
denoised = cv2.fastNlMeansDenoising(gray_noisy, None, h=10, # ans:
templateWindowSize=7, searchWindowSize=21) # ans:
# for color images
denoised = cv2.fastNlMeansDenoisingColored(noisy, None, h=10, hColor=10, # ans:
templateWindowSize=7, searchWindowSize=21) # ans:
# h: filter strength
# ans: higher h = more denoising, more blur
# templateWindowSize: patch size
# ans: usually 7x7
# searchWindowSize: search area
# ans: usually 21x21
Answer first, then press → to check.
What causes image noise?
Sensor issues, low light, interference, compression.
The most common noise type?
Gaussian noise.
Which noise looks like random dots?
Salt-and-pepper (impulse) noise.
The best filter for salt-and-pepper?
The median filter.
What does bilateral preserve?
Edges, while smoothing noise.
The trade-off of denoising?
It removes noise but also blurs details and edges.
Why does the median ignore salt-and-pepper?
The median is not affected by outliers (extreme values).
What is NLM?
Non-local means: it averages similar patches.
The fastest denoising method?
Gaussian blur.
What is sigmaColor in bilateral?
The intensity difference threshold for averaging.
# add gaussian noise (sigma=20)
mean = 0
sigma = 20
noise = np.random.normal(mean, sigma, img.shape)
noisy = np.clip(img + noise, 0, 255).astype(np.uint8) # ans:
# denoise with gaussian blur
denoised = cv2.GaussianBlur(noisy, (5, 5), 0) # ans:
# denoise with median filter
denoised = cv2.medianBlur(noisy, 5) # ans:
# bilateral denoising
denoised = cv2.bilateralFilter(noisy, 9, 75, 75) # ans:
# NLM denoising on grayscale
denoised = cv2.fastNlMeansDenoising(gray_noisy, None, h=10, # ans:
templateWindowSize=7, searchWindowSize=21) # ans:
# NLM denoising on color image
denoised = cv2.fastNlMeansDenoisingColored(noisy, None, h=10, hColor=10, # ans:
templateWindowSize=7, searchWindowSize=21) # ans:
# strong gaussian blur (11x11)
denoised = cv2.GaussianBlur(noisy, (11, 11), 0) # ans:
# median with large kernel
denoised = cv2.medianBlur(noisy, 11) # ans:
# bilateral with strong smoothing
denoised = cv2.bilateralFilter(noisy, 15, 150, 150) # ans:
# compare sizes before and after
print(noisy.shape, denoised.shape)
# ans: same shape, only pixel values change
# apply gaussian blur twice
blur1 = cv2.GaussianBlur(noisy, (5, 5), 0)
blur2 = cv2.GaussianBlur(blur1, (5, 5), 0) # ans:
# ans: double blur = stronger smoothing
# denoise each channel separately
b, g, r = cv2.split(noisy)
b_denoised = cv2.GaussianBlur(b, (5, 5), 0) # ans:
g_denoised = cv2.GaussianBlur(g, (5, 5), 0) # ans:
r_denoised = cv2.GaussianBlur(r, (5, 5), 0) # ans:
denoised = cv2.merge([b_denoised, g_denoised, r_denoised]) # ans:
# NLM with higher h value
denoised = cv2.fastNlMeansDenoising(gray_noisy, None, h=20, # ans:
templateWindowSize=7, searchWindowSize=21) # ans:
# ans: h=20 removes more noise, more blur
# bilateral with different sigmas
denoised = cv2.bilateralFilter(noisy, 9, 50, 100)
# ans: sigmaColor=50 (less colour smoothing)
# ans: sigmaSpace=100 (larger spatial range)
# combine median and gaussian
median = cv2.medianBlur(noisy, 5)
final = cv2.GaussianBlur(median, (3, 3), 0) # ans:
# ans: median removes salt-and-pepper, gaussian smooths
# create noisy image with both types
gaussian_noise = np.random.normal(0, 15, img.shape)
noisy = np.clip(img + gaussian_noise, 0, 255).astype(np.uint8)
# add salt-pepper on top
prob = 0.02
num_salt = int(prob * noisy.size * 0.5)
coords = [np.random.randint(0, i, num_salt) for i in noisy.shape[:2]]
noisy[coords[0], coords[1]] = 255
num_pepper = int(prob * noisy.size * 0.5)
coords = [np.random.randint(0, i, num_pepper) for i in noisy.shape[:2]]
noisy[coords[0], coords[1]] = 0
# ans: combined gaussian and salt-and-pepper noise
# denoise strategy
median = cv2.medianBlur(noisy, 5)
denoised = cv2.bilateralFilter(median, 9, 75, 75) # ans:
# ans: median first removes salt-and-pepper, then bilateral smooths