3. 多帧合成原理:多帧降噪的核心思想、帧间配准、时域滤波基础
多帧降噪,说白了就是「用数量换质量」。一张照片噪点多,那我拍三张、五张、甚至十几张,把它们合成一张干净的。这个思路在夜景模式里是核心中的核心。
我刚开始做这个方向时,总觉得单帧算法调调参数也能用。后来在暗光场景下测试,发现单帧再怎么调,暗部细节都是糊的。从那以后,我才真正重视起多帧合成这条路。
3.1 多帧降噪的核心思想
多帧降噪的底层逻辑其实很简单:信号是相关的,噪声是随机的。同一个场景拍多张,每张里的真实场景内容基本不变,但噪声每张都不一样。把多张图叠加平均,噪声就会相互抵消,信号则会保留下来。
举个例子:你拍一张夜景,某个像素的真实亮度是100,噪声是±10。单张可能看到90或110。但如果你拍4张,平均一下,噪声会降到±5左右。拍9张,降到±3.3。拍16张,降到±2.5。这就是多帧降噪的数学基础——信噪比提升与帧数的平方根成正比。
核心公式(直观理解版)
SNR多帧 ≈ SNR单帧 + 10 × log10(N) (dB)
其中 N 是帧数。4帧提升约6dB,9帧提升约9.5dB。
但这里有个坑:帧数不是越多越好。帧数多了,手持拍摄时的抖动累积、场景中的运动物体、甚至光线变化都会引入新的问题。我个人习惯是夜景模式用3-6帧,极端暗光场景最多用到9帧。再往上,收益递减,副作用递增。
| 帧数 | 理论信噪比提升 | 实际效果 | 典型场景 |
|---|---|---|---|
| 2帧 | +3 dB | 改善有限,暗部仍有明显噪点 | 快速抓拍 |
| 3-4帧 | +4.8 ~ +6 dB | 明显降噪,细节保留较好 | 手持夜景 |
| 6-9帧 | +7.8 ~ +9.5 dB | 大幅降噪,但需注意运动伪影 | 三脚架夜景 |
| 12帧以上 | +10.8 dB+ | 收益递减,对齐难度剧增 | 专业长曝光 |
3.2 帧间配准
多帧降噪的前提是:所有帧必须对齐。你想想看,如果每张图里同一个物体位置不一样,直接平均就会产生重影。这就是帧间配准要解决的问题。
帧间配准的核心是估计帧与帧之间的运动矢量。我把它分为两个层次:
- 全局运动估计:处理手持抖动、平移、旋转。通常用仿射变换或透视变换模型。
- 局部运动估计:处理场景中物体的独立运动,比如行人、车辆、树叶晃动。这个更难,需要光流或块匹配。
实际项目中,我一般先用全局配准把整体对齐,再用局部配准处理运动区域。这样效率高,效果也稳。
我的经验
全局配准用金字塔光流法,从低分辨率到高分辨率逐层细化。这样既能处理大位移,又能保证精度。我曾经试过直接在高分辨率做块匹配,结果慢得没法用。
帧间配准的常见方法对比:
| 方法 | 精度 | 速度 | 适用场景 |
|---|---|---|---|
| 相位相关 | 中等 | 快 | 纯平移运动 |
| 特征点匹配(SIFT/ORB) | 高 | 中等 | 旋转+缩放场景 |
| 金字塔光流 | 高 | 中等 | 手持抖动+局部运动 |
| 块匹配(BM3D风格) | 很高 | 慢 | 静态场景高精度需求 |
避坑指南
我曾经在配准时忽略了「运动物体」的处理。结果夜景模式拍街道,行人被平均成了半透明的鬼影。后来我加了运动掩膜,对运动区域做特殊处理——要么不参与平均,要么用更保守的融合权重。
3.3 时域滤波基础
配准做完,接下来就是时域滤波。时域滤波的核心是:沿着时间轴(帧序列)对每个像素做滤波。
最简单的时域滤波就是平均滤波:
// 伪代码:多帧平均
for each pixel (x, y):
sum = 0
for each frame i:
sum += frame_i[x][y]
result[x][y] = sum / N
但实际项目中,我很少用纯平均。原因有两个:
- 配准不可能完美,总有残差。纯平均会把残差也平均进去,导致边缘模糊。
- 运动区域需要保护,不能简单平均。
所以实际用的是加权时域滤波:
// 伪代码:加权时域滤波
for each pixel (x, y):
weight_sum = 0
result = 0
for each frame i:
// 根据配准置信度、运动检测结果计算权重
w = compute_weight(frame_i[x][y], reference_frame[x][y])
result += w * frame_i[x][y]
weight_sum += w
result[x][y] /= weight_sum
权重怎么算?我常用的策略:
- 基于像素差异:当前帧像素与参考帧像素差异越大,权重越低。这能抑制配准残差和运动区域。
- 基于梯度:边缘区域的权重降低,保护细节。
- 基于帧序号:离参考帧越远的帧,权重越低。这能处理缓慢的光线变化。
还有一种更高级的时域滤波——递归时域滤波。它不需要缓存所有帧,而是逐帧更新:
// 伪代码:递归时域滤波
result_0 = frame_0
for each frame i (i > 0):
alpha = compute_alpha(frame_i, result_{i-1})
result_i = alpha * frame_i + (1 - alpha) * result_{i-1}
alpha 是融合系数,范围0~1。alpha 越大,当前帧贡献越大,响应快但降噪弱;alpha 越小,历史帧贡献大,降噪强但容易产生拖影。我一般把 alpha 设在0.3~0.7之间,根据运动检测结果动态调整。
时域滤波的关键权衡
降噪强度 vs 运动响应速度。降噪越强,运动拖影越明显。这个矛盾没法完全消除,只能通过运动检测和自适应权重来平衡。
3.4 多帧降噪全流程框架
把上面三个环节串起来,就是多帧降噪的完整流程。我画了一张图,方便你理解整体结构。
这个流程里,每一步都有很多细节可以深挖。但核心逻辑就是:先对齐,再融合,最后精修。我在实际项目中,花在配准上的时间往往比滤波还多。因为配准做不好,后面再怎么调滤波参数都是白搭。
我的建议
刚开始做多帧降噪时,别急着上复杂算法。先用简单的平均滤波跑通流程,感受一下「配准误差」对结果的影响。等你亲眼看到重影和模糊,再去研究光流和自适应权重,理解会深刻得多。