AI Hints
Home›Computer Vision Tutorial›Getting a Good Image
Computer Vision · Lesson 48 of 111

Removing Noise

Which filter keeps the edges?.

⏱ 2 min read▶ Video at 1:17:10🎓 Free
Watch this lesson in the Computer Vision courseWatch on YouTube ↗Full Computer Vision course

In low light, a camera turns up its gain, and speckle appears all over the photo. Filtering removes it. But filtering also softens edges, and edges are exactly what you need. So which filter should you pick? Instead of guessing, let's measure.

Removing Noise
From the video: Removing Noise
  • Low light means speckle
  • Filtering softens edges
  • Let's measure

Comparing filters

We add strong noise to a clean image. Then we try three filters, and score each one with PSNR, which measures how close the result is to the clean original, in decibels. Higher is better. Gaussian averages neighbouring pixels. Median takes the middle value. And bilateral averages only neighbours of similar colour, to protect edges. Run it. All three are a big improvement on the noisy twenty decibels. Median narrowly wins here, at almost thirty two. Bilateral scores lowest here, because this noise is so strong that it can't tell noise from real edges. There's no universal best filter. So measure on your own images.

denoise.pypython
import cv2
import numpy as np
from belt import belt

clean, _, _ = belt()
rng = np.random.default_rng(0)
noisy = np.clip(clean + rng.normal(0, 25, clean.shape), 0, 255).astype(np.uint8)

def psnr(a, b):
    return 10 * np.log10(255 ** 2 / ((a.astype(float) - b) ** 2).mean())

print(f"Noisy:     {psnr(clean, noisy):.1f} dB")
print(f"Gaussian:  {psnr(clean, cv2.GaussianBlur(noisy, (5, 5), 0)):.1f} dB")
print(f"Median:    {psnr(clean, cv2.medianBlur(noisy, 5)):.1f} dB")
print(f"Bilateral: {psnr(clean, cv2.bilateralFilter(noisy, 9, 60, 7)):.1f} dB")
Output
Noisy:     20.2 dB
Gaussian:  31.3 dB
Median:    31.9 dB
Bilateral: 29.1 dB
Also uses belt.py
import numpy as np
import cv2

def belt(n=7, seed=1, size=(480, 640), bruises=2, light=1.0):
    """Draw a simulated conveyor belt of apples. Returns the image and the true boxes."""
    rng = np.random.default_rng(seed)
    h, w = size
    img = np.full((h, w, 3), (70, 75, 80), np.uint8)
    img = cv2.add(img, rng.normal(0, 6, img.shape).clip(0, 30).astype(np.uint8))
    boxes, spots, taken = [], [], []
    while len(boxes) < n:
        r = int(rng.integers(32, 48))
        x, y = int(rng.integers(r + 5, w - r - 5)), int(rng.integers(r + 5, h - r - 5))
        if any((x - a) ** 2 + (y - b) ** 2 < (r + c + 8) ** 2 for a, b, c in taken):
            continue
        taken.append((x, y, r))
        red = int(rng.integers(150, 225))
        cv2.circle(img, (x, y), r, (35, 40, red), -1, cv2.LINE_AA)
        cv2.circle(img, (x - r // 3, y - r // 3), r // 4, (90, 90, min(255, red + 30)), -1, cv2.LINE_AA)
        boxes.append((x - r, y - r, x + r, y + r))
    for x, y, r in taken[:bruises]:
        cv2.ellipse(img, (x + r // 3, y + r // 4), (r // 3, r // 4), 20, 0, 360, (25, 45, 70), -1, cv2.LINE_AA)
        spots.append((x, y, r))
    img = cv2.GaussianBlur(img, (3, 3), 0)
    return np.clip(img * light, 0, 255).astype(np.uint8), boxes, spots

Which filter?

Here's a quick guide. Gaussian suits gentle, even noise, and it's very fast. Median is superb on salt and pepper noise, which means random white and black dots. Bilateral protects edges when noise is moderate. Non local means gives the best classic quality, but it's slow. And trained denoising networks do better still, if you've got the compute.

FilterGood for
GaussianGentle noise, speed
MedianSalt and pepper noise
BilateralKeeping edges
Non-local meansBest classic quality
Neural denoiserTop quality, more compute

Summary

Measure filters on your own images. Next, a completely different way of seeing an image, as waves.

Key takeaways
  • PSNR scores denoising
  • Median beat the rest here
  • There's no universal best
Next up: Images as Waves — the frequency domain