Computer vision

Noise & Blurring

Where the grain in an image comes from, and four filters that remove it without losing too much.

What is image noise?

Noise is random variation in pixel intensity: an unwanted signal on top of the real one.

Sources

  • Sensor imperfections
  • Low-light conditions
  • Electrical interference
  • Transmission errors
  • Compression artifacts

Impact

It degrades image quality and affects processing algorithms: edge detectors, thresholds and feature detectors all respond to noise as if it were detail.

Types of noise

Gaussian

  • Random values from a normal distribution
  • The most common type
  • Additive: noisy = clean + noise
  • Looks like a grainy texture

Salt-and-pepper

  • Random black and white pixels
  • Also called impulse noise
  • Pixels are either 0 or 255
  • Looks like random dots

Poisson

  • Varies with signal intensity
  • Common in low-light imaging
  • Variance proportional to intensity

Noise, live

clean
noisy

One row of pixels (dashed line): clean vs noisy

Adding Gaussian noise


        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]
      

Adding salt-and-pepper noise


        # 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
      

Gaussian blur for noise reduction

Why Gaussian blur?

  • Effective for Gaussian noise
  • Weighted averaging: near pixels count more, so features survive better than with a box blur
  • Smooth, natural-looking results

Trade-off

It reduces noise but also blurs edges: the filter cannot tell an edge from noise.

Gaussian blur parameters


        # 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 filter for salt-and-pepper

median → … mean → …

Median blur in OpenCV


        # 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
      

Bilateral filter (edge-preserving)

Smooths noise while keeping edges sharp.

  • Two weights: spatial and intensity
  • Close pixels with similar intensity → high weight
  • Far pixels or different intensity → low weight
  • Best for: natural images, portrait smoothing
patch across an edge
spatial
×
intensity
=
bilateral

Bright = high weight. The weights stop at the edge, so the two sides are never averaged together.

Bilateral filter in OpenCV


        # 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
      

Non-local means denoising

NLM compares patches, not just neighbourhoods.

  • Concept: similar patches anywhere in the image are averaged.
  • Advantage: better noise reduction, preserves texture.
  • Disadvantage: computationally expensive.

← 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.

NLM denoising in OpenCV


        # 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
      

Denoising, live

Comparing denoising methods

Gaussian blur

  • Fast, simple
  • Blurs edges
  • Good for light Gaussian noise

Median filter

  • Excellent for salt-and-pepper
  • Preserves edges
  • Slower than Gaussian

Bilateral filter

  • Edge-preserving
  • Good for natural images
  • Moderate speed

NLM denoising

  • Best quality
  • Preserves texture
  • Slowest

Exercises

Answer first, then press → to check.

Exercises 1 · concepts

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.

Exercises 2 · concepts

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.

Exercises 3 · coding


        # 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:
      

Exercises 4 · coding


        # 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:
      

Exercises 5 · coding


        # 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
      

Exercises 6 · mixed


        # 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:
      

Exercises 7 · mixed


        # 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
      

Exercises 8 · advanced


        # 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