Introduction

Recently, while studying computer vision, I encountered various image denoising techniques. In this series, I will mainly introduce traditional image denoising methods. Before diving into the denoising techniques, let’s first introduce some common noise types and evaluation metrics.

Introduction to Common Noises and Example Code

Before introducing the common noises, let’s start with our original image, which is also a classic image in the field of computer vision:
lena.jpg

Gaussian Noise

Gaussian noise is simply random noise that follows a Gaussian distribution, whose probability density function is given by
$$ P(x) = \frac{1}{\sqrt{2\pi\sigma^2}} e^{-\frac{(x - \mu)^2}{2\sigma^2}} $$
where $\mu$ determines the mean of the noise and $\sigma^2$ determines the noise intensity. Note that Gaussian noise is generally assumed to be independently and identically distributed.

1
2
3
4
5
6
7
8
def add_gaussian_noise(image, mean=0, std=25):
row, col, ch = image.shape
gauss = np.random.normal(mean, std, (row, col, ch))
gauss = gauss.reshape(row, col, ch)
noisy_image = image + gauss
noisy_image = np.clip(noisy_image, 0, 255) # Ensure pixel values are in the range 0-255
noisy_image = noisy_image.astype(np.uint8)
return noisy_image

Example:
lena_gaussian.jpg

Salt and Pepper Noise

Salt and pepper noise refers to noise where some pixels are randomly set to the maximum value (salt) or the minimum value (pepper), often used to simulate noise due to transmission errors or sensor dirt. Salt and pepper noise does not have an explicit probability density function; instead, the probability $p$ is used to control the occurrence of noisy pixels.

1
2
3
4
5
6
7
8
9
10
def add_salt_pepper_noise(image, prob=0.05):
noisy_image = np.copy(image)
h, w, c = noisy_image.shape
pepper = np.random.rand(h, w) < prob
noisy_image[pepper] = 0
salt = np.random.rand(h, w) < prob
noisy_image[salt] = 255
noisy_image = np.clip(noisy_image, 0, 255) # 确保像素值在 0-255 范围内
noisy_image = noisy_image.astype(np.uint8)
return noisy_image

Example:
lena_gaussian.jpg

Poisson Noise

Poisson noise, also known as “shot noise” or “granular noise”, is generated by the random arrival of photons in a photodetector, and its characteristics are related to the signal intensity. Poisson noise is signal-dependent and is commonly observed in low-light imaging. Its distribution follows the Poisson distribution: $P(k) = \frac{\lambda^{k} e^{-\lambda}}{k!}$
where $\lambda$ is the original pixel value and $k$ is the noisy pixel value.

1
2
3
4
5
6
7
def add_poisson_noise(image):
vals = len(np.unique(image))
vals = 2 ** np.ceil(np.log2(vals))
noisy_image = np.random.poisson(image * vals) / float(vals)
noisy_image = np.clip(noisy_image, 0, 255)
noisy_image = noisy_image.astype(np.uint8)
return noisy_image

Example:
lena_gaussian.jpg

Multiplicative Noise

Multiplicative noise is noise that is proportional to the signal intensity. A common model is:
$$ g(x, y) = f(x, y) · \eta(x,y) \qquad \eta \sim U[a,b]$$
Of course, $\eta$ can also follow a Gaussian distribution with mean 0, which is the typical speckle noise.

1
2
3
4
5
6
7
8
def add_multiplicative_noise(image, mean=0, std=0.1):
row, col, ch = image.shape
gauss = np.random.normal(mean, std, (row, col, ch))
gauss = gauss.reshape(row, col, ch)
noisy_image = image * gauss
noisy_image = np.clip(noisy_image, 0, 255)
noisy_image = noisy_image.astype(np.uint8)
return noisy_image

Example:
lena_gaussian.jpg

Uniform Noise

Uniform noise is an additive noise that follows a uniform distribution:
$$ g(x, y) = f(x, y) + \eta(x,y) \qquad \eta \sim U[a,b]$$

1
2
3
4
5
6
7
def add_uniform_noise(image, low=-50, high=50):
row, col, ch = image.shape
uniform_noise = np.random.uniform(low, high, (row, col, ch))
noisy_image = image + uniform_noise
noisy_image = np.clip(noisy_image, 0, 255)
noisy_image = noisy_image.astype(np.uint8)
return noisy_image

Example:
lena_gaussian.jpg

周期噪声

Periodic noise is caused by electronic interference and manifests as a regular sinusoidal noise pattern:
$$ n(x, y) = A · \sin(2\pi (ux + vy) + \phi) $$
where $A$ is the amplitude, $u$ and $v$ are the frequencies, and $\phi$ is the phase.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def add_periodic_noise(image, frequency=50, amplitude=50):
row, col, ch = image.shape
x = np.arange(0, col)
y = np.arange(0, row)
X, Y = np.meshgrid(x, y)
# 生成单通道噪声
noise = amplitude * np.sin(2 * np.pi * frequency * X / col)
# 将噪声扩展到三通道
noise = np.stack([noise, noise, noise], axis=-1)
# 添加噪声到图像
noisy_image = image + noise
noisy_image = np.clip(noisy_image, 0, 255) # 确保像素值在 0-255 范围内
noisy_image = noisy_image.astype(np.uint8)
return noisy_image

Example:
lena_gaussian.jpg

Mixed Noise

Mixed noise is simply a combination of several common noise types.

1
2
3
4
def add_mixed_noise(image):
noisy_image = add_gaussian_noise(image, std=20)
noisy_image = add_salt_pepper_noise(noisy_image)
return noisy_image

Example:
lena_gaussian.jpg

Metrics

PSNR (Peak Signal-to-Noise Ratio)

  • Formula
    $$ \text{PSNR} = 20 \log_{10}\left(\frac{255}{\sqrt{\text{MSE}}}\right) $$
  • Meaning:Quantifies pixel-level differences; a higher value (typically >30dB) indicates better denoising performance.
  • Characteristics:Simple calculation, but not sensitive to human visual perception.

SSIM(结构相似性指数)

  • Formula
    The product of three components: luminance ($\mu$), contrast ($\sigma$), and structure ($\sigma_{xy}$):
    $$
    \text{SSIM} = \frac{(2\mu_x\mu_y + C_1)(2\sigma_{xy} + C_2)}{(\mu_x^2 + \mu_y^2 + C_1)(\sigma_x^2 + \sigma_y^2 + C_2)}
    $$

    • $\mu_x, \mu_y$: Mean of the images (luminance)
    • $\sigma_x, \sigma_y$: Standard deviation (contrast)
    • $\sigma_{xy}$: Covariance (structural similarity)
    • $C_1=(k_1L)^2, C_2=(k_2L)^2$: Constants (with $L$ as the pixel range, typically $k_1=0.01, k_2=0.03$)
  • Meaning:Ranges from [0,1]; closer to 1 indicates better structural preservation.

  • Characteristics:Provides a comprehensive evaluation of texture, edge, and contrast preservation.

MSE(Mean Squared Error)

  • Formula
    $$ \text{MSE} = \frac{1}{N}\sum (x_i - y_i)^2 $$
  • Meaning:Directly reflects the average pixel difference; lower values are better.
  • Characteristics:Ignores spatial structure and is sensitive to outliers.

EPI(Edge Preservation Index)

  • Formula
    $$ \text{EPI} = \frac{\sum|\nabla\text{denoised} - \nabla\text{original}|}{\sum|\nabla\text{original}|} $$
  • Meaning:Evaluates the ability to preserve edges; lower values (ideally 0) indicate less detail loss.
  • Characteristics:Specifically used for detecting edge blurring, but sensitive to the type of noise.

LPIPS(Learned Perceptual Image Patch Similarity)

  • Principle:Based on the feature distance of a pre-trained VGG network.
    $$ \text{LPIPS} = |\phi(x) - \phi(y)|_2 $$
  • Meaning:Lower values indicate closer visual similarity, reflecting high-level semantic similarity.
  • Characteristics:Computationally complex, requires GPU, and depends on deep learning models.

指标对比

Metric Type Representative Metric Core Meaning Computational Complexity
Pixel Difference PSNR/MSE Global pixel error Low
Structural Similarity SSIM Texture/edge preservation Medium
Edge Preservation EPI Degree of detail loss Medium
Perceptual Quality LPIPS Visual similarity (human perception) High
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import cv2
import numpy as np
import lpips
import torch
from skimage.metrics import structural_similarity as ssim

def PSNR(original, compressed):
mse = np.mean((original - compressed) ** 2)
if mse == 0:
return 100
max_pixel = 255.0
psnr = 20 * np.log10(max_pixel / np.sqrt(mse))
return psnr

def SSIM(original, denoised):
# 使用 `channel_axis` 参数替代已弃用的 `multichannel`
return ssim(original, denoised, channel_axis=-1)

def MSE(original, compressed):
mse = np.mean((original - compressed) ** 2)
return mse

def EPI(original, denoised):
# 将图像转换为灰度图
gray_original = cv2.cvtColor(original, cv2.COLOR_BGR2GRAY)
gray_denoised = cv2.cvtColor(denoised, cv2.COLOR_BGR2GRAY)

# 计算 Sobel 梯度
sobel_original = cv2.Sobel(gray_original, cv2.CV_64F, 1, 1, ksize=3)
sobel_denoised = cv2.Sobel(gray_denoised, cv2.CV_64F, 1, 1, ksize=3)

# 计算 EPI
return np.sum(np.abs(sobel_denoised - sobel_original)) / np.sum(np.abs(sobel_original))

# 初始化 LPIPS 模型
loss_fn = lpips.LPIPS(net='vgg')

def LPIPS(original, denoised):
# 将图像转换为 PyTorch 张量并归一化
original_torch = torch.from_numpy(original).permute(2, 0, 1).unsqueeze(0).float() / 255.0
denoised_torch = torch.from_numpy(denoised).permute(2, 0, 1).unsqueeze(0).float() / 255.0

# 计算 LPIPS
return loss_fn(original_torch, denoised_torch).item()

def evaluate(original, compressed):
psnr = PSNR(original, compressed)
ssim_val = SSIM(original, compressed) # 避免与函数名冲突
mse = MSE(original, compressed)
epi = EPI(original, compressed)
lpips_val = LPIPS(original, compressed) # 避免与函数名冲突

# 打印结果
print(f"PSNR: {psnr}, SSIM: {ssim_val}, MSE: {mse}, EPI: {epi}, LPIPS: {lpips_val}")
return psnr, ssim_val, mse, epi, lpips_val