3. 多帧降噪
任务目标
在手持或车载等运动场景中,直接对多帧图像做时域平均会产生重影。本案例实现基于块匹配的运动估计与运动补偿,将参考帧对齐到当前帧后再进行多帧加权平均,从而在抑制噪声的同时保留运动区域的清晰度。
核心原理
对当前帧 Ft 的每个块,在相邻帧 Ft-1、Ft+1 的搜索窗口内寻找最佳匹配块(SAD最小),得到运动矢量;然后按矢量将相邻帧像素搬移到当前帧坐标,得到补偿帧;最后对当前帧与补偿帧做加权平均输出。本实现采用灰度图、固定块大小16×16、搜索范围±8像素,以最小可复现方式演示完整流程。
环境依赖
仅需 Python 3.8+ 与 NumPy,无需 OpenCV 等第三方视觉库。若需读取真实图像,可自行用 PIL 或 imageio 加载后转为灰度 ndarray,本案例核心代码直接以 ndarray 为输入。
关键完整代码
import numpy as np
def motion_estimate(ref, cur, block_size=16, search_range=8):
"""对当前帧cur的每个块,在参考帧ref中搜索最佳匹配块,返回运动矢量场(dy, dx)"""
h, w = cur.shape
mv_y = np.zeros((h // block_size, w // block_size), dtype=np.int8)
mv_x = np.zeros_like(mv_y)
for by in range(0, h - block_size + 1, block_size):
for bx in range(0, w - block_size + 1, block_size):
cur_block = cur[by:by+block_size, bx:bx+block_size]
best_sad = float('inf')
best_dy, best_dx = 0, 0
for dy in range(-search_range, search_range + 1):
for dx in range(-search_range, search_range + 1):
ry = by + dy
rx = bx + dx
if ry < 0 or rx < 0 or ry + block_size > h or rx + block_size > w:
continue
ref_block = ref[ry:ry+block_size, rx:rx+block_size]
sad = np.sum(np.abs(cur_block.astype(np.int16) - ref_block.astype(np.int16)))
if sad < best_sad:
best_sad = sad
best_dy, best_dx = dy, dx
mv_y[by // block_size, bx // block_size] = best_dy
mv_x[by // block_size, bx // block_size] = best_dx
return mv_y, mv_x
def motion_compensate(ref, mv_y, mv_x, block_size=16):
"""按运动矢量场将参考帧ref搬移到当前帧坐标,输出补偿帧"""
h, w = ref.shape
comp = np.zeros_like(ref)
for by in range(0, h - block_size + 1, block_size):
for bx in range(0, w - block_size + 1, block_size):
my = by // block_size
mx = bx // block_size
sy = by + mv_y[my, mx]
sx = bx + mv_x[my, mx]
comp[by:by+block_size, bx:bx+block_size] = ref[sy:sy+block_size, sx:sx+block_size]
return comp
def multi_frame_denoise(frames, block_size=16, search_range=8):
"""输入帧序列(至少3帧),以中间帧为当前帧,前后帧做运动补偿后加权平均"""
assert len(frames) >= 3, "至少需要3帧"
mid_idx = len(frames) // 2
cur = frames[mid_idx].astype(np.float32)
acc = cur.copy()
weight = 1.0
for i, f in enumerate(frames):
if i == mid_idx:
continue
mv_y, mv_x = motion_estimate(f, frames[mid_idx], block_size, search_range)
comp = motion_compensate(f, mv_y, mv_x, block_size).astype(np.float32)
acc += comp
weight += 1.0
return (acc / weight).astype(np.uint8)
# 最小验证入口
if __name__ == "__main__":
np.random.seed(0)
h, w = 64, 64
# 构造静态背景 + 一个向右移动的方块,模拟运动场景
base = np.zeros((h, w), dtype=np.uint8)
base[20:40, 20:40] = 200
frames = []
for shift in [-2, 0, 2]: # 方块位置随时间变化
frame = base.copy()
if shift > 0:
frame[20:40, 20+shift:40+shift] = 200
frame[20:40, 20:40] = 0
elif shift < 0:
frame[20:40, 20+shift:40+shift] = 200
frame[20:40, 20:40] = 0
# 添加高斯噪声
noisy = frame + np.random.normal(0, 15, (h, w)).astype(np.int16)
frames.append(np.clip(noisy, 0, 255).astype(np.uint8))
denoised = multi_frame_denoise(frames)
# 计算降噪后与无噪声中间帧的PSNR
clean_mid = base.copy() # shift=0时方块位置即原始位置
mse = np.mean((denoised.astype(np.float32) - clean_mid.astype(np.float32)) ** 2)
psnr = 10 * np.log10(255**2 / mse)
print(f"降噪后PSNR: {psnr:.2f} dB")
运行与验证
- 将上述代码保存为
motion_denoise.py,直接运行python motion_denoise.py。 - 程序构造3帧含噪运动序列(方块右移),执行运动补偿多帧降噪。
- 观察输出PSNR值:由于运动补偿正确对齐了方块位置,降噪后PSNR应显著高于不做补偿直接平均的结果(可自行对比)。
通过条件:程序无报错运行结束,输出PSNR大于30 dB(含噪单帧PSNR约25 dB,补偿平均后应明显提升)。若将运动估计改为全零矢量(即不做补偿),PSNR会因重影而下降,以此验证运动补偿的有效性。
常见问题
- 块效应:块大小16在运动边界处可能出现块状伪影,可减小块大小或后续增加重叠块/加权窗优化。
- 大运动失效:搜索范围±8像素仅适用于小位移,运动剧烈时需增大搜索范围或采用多尺度策略。
- 边界处理:搜索越界时跳过该候选矢量,补偿后边界像素保持参考帧原值,不影响整体效果。