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.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) |
| 抗离群值 | 弱 | 强 |
| 边缘保留 | 一般 | 较好 |
| 适用场景 | 静止场景、高斯噪声 | 有运动物体、椒盐噪声 |
4.3 自适应加权平均(Adaptive Weighted Average)
均值和中值都是「一刀切」的策略。但实际场景中,不同帧的同一像素,可信度是不一样的。比如运动区域的像素变化大,静止区域的像素稳定。自适应加权平均就是给每个像素分配一个权重,权重越大,贡献越多。
权重的计算方式有很多种,我常用的是基于帧差法的方案:
- 计算当前帧与参考帧(通常是第一帧或均值帧)的差
- 差值越小,权重越大(用高斯函数映射)
- 归一化权重,使所有帧的权重之和为 1
- 加权求和得到输出
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)
4.4 三种算法的对比与选择
为了让你更直观地理解它们的差异,我画了一张流程图:
实际项目中,我很少只用一种算法。比如在手机夜景模式里,通常是先做运动检测,静止区域用均值,运动区域用中值或自适应加权。这种混合策略效果最好。
均值滤波:约 15ms
中值滤波:约 120ms
自适应加权平均:约 80ms
(测试环境:Intel i7-10750H, Python 3.8, OpenCV 4.5)
嗯,到这里三种经典算法就讲完了。均值简单高效,中值稳健抗噪,自适应加权灵活可控。你可以在自己的数据集上跑一跑,看看哪种更适合你的场景。代码我已经放在上面了,直接复制就能用。