2. 多帧降噪进阶

任务目标

本案例在案例1(多帧平均法)基础上,实现基于中值法的多帧降噪:对同一场景连续拍摄的N帧图像,在逐像素位置上取N个灰度值的中位数作为输出像素值,并与平均法结果进行差异对比。中值法对椒盐噪声、瞬态高亮噪声(如行人闪光、反光闪烁)具有更好的抑制能力,但计算量高于平均法。

环境与依赖

Python 3.8+,仅需NumPy和OpenCV(读取/保存图像用)。若无需读写文件,仅NumPy即可完成核心计算。

pip install numpy opencv-python

完整实现

import numpy as np
import cv2

def generate_noisy_frames(clean, num_frames=9, salt_prob=0.05, gauss_sigma=10):
    """生成带椒盐+高斯噪声的多帧图像序列(模拟真实拍摄噪声)"""
    frames = []
    h, w = clean.shape[:2]
    for _ in range(num_frames):
        frame = clean.copy().astype(np.float32)
        # 高斯噪声
        frame += np.random.normal(0, gauss_sigma, frame.shape)
        # 椒盐噪声
        salt_mask = np.random.random(frame.shape[:2]) < salt_prob
        pepper_mask = np.random.random(frame.shape[:2]) < salt_prob
        if len(frame.shape) == 3:
            frame[salt_mask] = 255
            frame[pepper_mask] = 0
        else:
            frame[salt_mask] = 255
            frame[pepper_mask] = 0
        frames.append(np.clip(frame, 0, 255).astype(np.uint8))
    return frames

def median_denoise(frames):
    """中值法:逐像素取多帧中位数"""
    stack = np.stack(frames, axis=0).astype(np.float32)  # (N,H,W,C)或(N,H,W)
    return np.median(stack, axis=0).astype(np.uint8)

def average_denoise(frames):
    """平均法:逐像素取多帧均值"""
    stack = np.stack(frames, axis=0).astype(np.float32)
    return np.mean(stack, axis=0).astype(np.uint8)

def psnr(img1, img2):
    """峰值信噪比(dB),越高越接近参考图"""
    mse = np.mean((img1.astype(np.float32) - img2.astype(np.float32)) ** 2)
    if mse == 0:
        return float('inf')
    return 10 * np.log10(255.0**2 / mse)

# ========== 主流程 ==========
if __name__ == "__main__":
    np.random.seed(42)  # 固定随机种子保证可复现

    # 1. 生成干净参考图(用OpenCV内置图像或自建简单图案)
    clean_img = np.zeros((128, 128, 3), dtype=np.uint8)
    clean_img[:] = (100, 150, 200)          # 均匀底色
    cv2.rectangle(clean_img, (30, 30), (90, 90), (255, 255, 255), -1)  # 白色方块
    cv2.circle(clean_img, (64, 64), 20, (0, 0, 0), 2)                  # 黑色圆环

    # 2. 生成9帧带噪图像
    noisy_frames = generate_noisy_frames(clean_img, num_frames=9)

    # 3. 两种方法降噪
    median_result = median_denoise(noisy_frames)
    average_result = average_denoise(noisy_frames)

    # 4. 定量对比(以干净图为参考)
    psnr_median = psnr(median_result, clean_img)
    psnr_average = psnr(average_result, clean_img)
    print(f"中值法 PSNR: {psnr_median:.2f} dB")
    print(f"平均法 PSNR: {psnr_average:.2f} dB")

    # 5. 保存结果供目视检查
    cv2.imwrite("median_result.png", median_result)
    cv2.imwrite("average_result.png", average_result)
    cv2.imwrite("clean_reference.png", clean_img)
    cv2.imwrite("first_noisy_frame.png", noisy_frames[0])

运行与验证

运行方式:将上述代码保存为 median_denoise.py,在命令行执行:

python median_denoise.py

验证方法:

  1. 定量判定:观察控制台输出的PSNR值。由于椒盐噪声占比5%,中值法的PSNR应显著高于平均法(通常高5~15 dB)。若随机种子固定为42,两次运行结果完全一致。
  2. 目视判定:打开生成的 median_result.png 与 average_result.png。中值法结果中不应残留黑白椒盐噪点,边缘(方块与圆环轮廓)保持清晰;平均法结果中可能仍有少量椒盐噪点残留(表现为暗/亮细点),且边缘略模糊。
  3. 差异对比:将 first_noisy_frame.png(单帧带噪图)与两张结果图并列查看,可直观看到中值法对极值噪声的彻底清除效果。

关键说明

中值法对椒盐噪声的抑制原理是:椒盐噪声将像素置为极值(0或255),而中位数对极值不敏感,因此能完全滤除孤立椒盐点。平均法会将极值拉入均值,导致噪点以“灰影”形式残留。本实现中噪声模型同时包含高斯噪声和椒盐噪声,更接近真实拍摄场景;若仅存在高斯噪声,两种方法PSNR差异会缩小,中值法略低于平均法——这正是两者适用场景的边界所在。