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")

运行与验证

  1. 将上述代码保存为 motion_denoise.py,直接运行 python motion_denoise.py。
  2. 程序构造3帧含噪运动序列(方块右移),执行运动补偿多帧降噪。
  3. 观察输出PSNR值:由于运动补偿正确对齐了方块位置,降噪后PSNR应显著高于不做补偿直接平均的结果(可自行对比)。

通过条件:程序无报错运行结束,输出PSNR大于30 dB(含噪单帧PSNR约25 dB,补偿平均后应明显提升)。若将运动估计改为全零矢量(即不做补偿),PSNR会因重影而下降,以此验证运动补偿的有效性。

常见问题

  • 块效应:块大小16在运动边界处可能出现块状伪影,可减小块大小或后续增加重叠块/加权窗优化。
  • 大运动失效:搜索范围±8像素仅适用于小位移,运动剧烈时需增大搜索范围或采用多尺度策略。
  • 边界处理:搜索越界时跳过该候选矢量,补偿后边界像素保持参考帧原值,不影响整体效果。