4、经典多帧降噪算法:均值滤波、中值滤波、自适应加权平均的Python实现

多帧降噪,说白了就是利用多张同一场景的照片,把噪声给「平均」掉。你想想看,每张照片里的噪声都是随机的,但真实场景的信号是固定的。那我们多拍几张,把像素值加起来再取个平均,噪声不就互相抵消了吗?

这个思路听起来简单,但落地的时候坑不少。我最早做安防摄像头项目时,就吃过这个亏——直接对运动物体做均值滤波,结果画面糊成一团。嗯,今天我们就从最基础的三种算法讲起,把原理和代码都掰开揉碎了说清楚。

核心思想: 多帧降噪的本质是利用帧间冗余信息,在时域上对像素进行统计滤波。帧数越多,噪声抑制越强,但对场景静止的要求也越高。

4.1 均值滤波(Mean Filter)

均值滤波是最直观的方法。假设我们有 N 帧图像,每个像素位置 (x, y) 在 N 帧中的值分别为 I₁(x,y), I₂(x,y), ..., Iₙ(x,y),那么输出就是它们的算术平均:

I_out(x,y) = (1/N) * Σ I_k(x,y)  (k=1 to N)

我在项目中遇到过一个问题:直接用 OpenCV 的 cv2.addWeighted 逐帧累加,结果发现随着帧数增加,图像越来越暗。后来才意识到是数据类型溢出——uint8 累加超过 255 就截断了。正确的做法是先转成 float32 再累加。

下面是一个完整的 Python 实现:

import cv2
import numpy as np

def mean_multi_frame_denoise(frames):
    """
    多帧均值降噪
    :param frames: list of numpy arrays (H, W, C), uint8
    :return: denoised image, uint8
    """
    # 转成 float32 防止溢出
    frames_float = [frame.astype(np.float32) for frame in frames]
    
    # 累加所有帧
    accum = np.zeros_like(frames_float[0])
    for frame in frames_float:
        accum += frame
    
    # 取平均
    result = accum / len(frames_float)
    
    # 转回 uint8
    return np.clip(result, 0, 255).astype(np.uint8)
我的经验: 帧数建议控制在 4-16 帧之间。少于 4 帧降噪效果不明显,多于 16 帧收益递减,而且对运动更敏感。我一般取 8 帧,效果和效率比较平衡。

4.2 中值滤波(Median Filter)

均值滤波有个致命弱点——对离群值(比如椒盐噪声、运动前景)非常敏感。一个像素如果有一帧是运动物体,均值就会被拉偏。中值滤波就是来解决这个问题的。

中值滤波的做法是:对每个像素位置,收集 N 帧的值,排序后取中间那个。这样即使有几帧是异常值,只要不超过一半,就不会影响结果。

def median_multi_frame_denoise(frames):
    """
    多帧中值降噪
    :param frames: list of numpy arrays (H, W, C), uint8
    :return: denoised image, uint8
    """
    # 堆叠成 (N, H, W, C) 的张量
    stack = np.stack(frames, axis=0)
    
    # 沿第0维取中值
    result = np.median(stack, axis=0)
    
    return np.clip(result, 0, 255).astype(np.uint8)

我曾经在夜间监控项目里用过这个算法。路灯下的车牌区域,均值滤波会把车灯的高光扩散到周围,中值滤波就能很好地保留边缘。但要注意——中值滤波的计算量比均值大得多,排序操作在帧数多的时候很慢。

对比项 均值滤波 中值滤波
计算复杂度 O(N) O(N log N)
抗离群值 弱 强
边缘保留 一般 较好
适用场景 静止场景、高斯噪声 有运动物体、椒盐噪声
避坑指南: 我曾经在 32 帧的中值滤波上踩过坑——内存直接爆了。因为 np.median 会一次性加载所有帧到内存。建议帧数超过 16 时,用分块处理或者改用快速选择算法。

4.3 自适应加权平均(Adaptive Weighted Average)

均值和中值都是「一刀切」的策略。但实际场景中,不同帧的同一像素,可信度是不一样的。比如运动区域的像素变化大,静止区域的像素稳定。自适应加权平均就是给每个像素分配一个权重,权重越大,贡献越多。

权重的计算方式有很多种,我常用的是基于帧差法的方案:

  1. 计算当前帧与参考帧(通常是第一帧或均值帧)的差
  2. 差值越小,权重越大(用高斯函数映射)
  3. 归一化权重,使所有帧的权重之和为 1
  4. 加权求和得到输出
def adaptive_weighted_denoise(frames, sigma=20.0):
    """
    自适应加权平均降噪
    :param frames: list of numpy arrays (H, W, C), uint8
    :param sigma: 高斯函数的sigma参数,控制权重衰减速度
    :return: denoised image, uint8
    """
    frames_float = [frame.astype(np.float32) for frame in frames]
    n_frames = len(frames_float)
    
    # 用均值帧作为参考帧
    ref_frame = np.mean(frames_float, axis=0)
    
    # 计算每帧与参考帧的差异
    weights = []
    for frame in frames_float:
        diff = np.abs(frame - ref_frame)
        # 对每个像素计算权重:exp(-diff^2 / (2*sigma^2))
        weight = np.exp(-diff**2 / (2 * sigma**2))
        weights.append(weight)
    
    # 归一化权重
    weight_sum = np.sum(weights, axis=0)
    # 防止除零
    weight_sum = np.maximum(weight_sum, 1e-6)
    
    # 加权求和
    result = np.zeros_like(frames_float[0])
    for i, frame in enumerate(frames_float):
        result += frame * (weights[i] / weight_sum)
    
    return np.clip(result, 0, 255).astype(np.uint8)
参数调优: sigma 值控制权重的敏感度。sigma 越小,只有差异很小的像素才会被赋予高权重,适合运动剧烈的场景;sigma 越大,权重分布越均匀,接近均值滤波。我一般从 15 开始调,根据噪声水平上下浮动。

4.4 三种算法的对比与选择

为了让你更直观地理解它们的差异,我画了一张流程图:

多帧降噪算法选择流程 输入 N 帧图像 场景是否静止? 均值滤波 速度快,适合高斯噪声 运动区域占比? 中值滤波 抗离群值,保留边缘 自适应加权平均 灵活,但计算量大 选择建议 • 静止场景 → 均值 • 少量运动 → 中值 • 复杂运动 → 自适应

实际项目中,我很少只用一种算法。比如在手机夜景模式里,通常是先做运动检测,静止区域用均值,运动区域用中值或自适应加权。这种混合策略效果最好。

性能对比(8帧 1080p 图像):
均值滤波:约 15ms
中值滤波:约 120ms
自适应加权平均:约 80ms
(测试环境:Intel i7-10750H, Python 3.8, OpenCV 4.5)

嗯,到这里三种经典算法就讲完了。均值简单高效,中值稳健抗噪,自适应加权灵活可控。你可以在自己的数据集上跑一跑,看看哪种更适合你的场景。代码我已经放在上面了,直接复制就能用。