4. 运动补偿与去鬼影

多帧降噪做到这一步,其实已经解决了一大半问题。但有个东西特别让人头疼——鬼影。你想想看,夜景拍摄时手抖一下,或者画面里有个行人走过,直接平均融合就会留下半透明的拖影,像鬼一样飘在那里。这就是我们常说的鬼影(Ghosting)。

我个人习惯把运动补偿和去鬼影放在一起讲,因为它们本质上是一件事:区分哪些像素是“可信的静态内容”,哪些是“需要特殊处理的运动区域”。说白了,就是给每一帧的每个像素打一个标签——你是好像素,还是坏像素。

4.1 运动掩膜:给像素打标签

运动掩膜(Motion Mask)是一张二值图或灰度图,标记出当前帧相对于参考帧的运动区域。白色代表运动区域,黑色代表静态区域。有了这张图,融合时就能区别对待。

核心思路:静态区域放心融合,运动区域要么用单帧数据,要么做特殊处理。

我在项目中遇到过一个问题:运动掩膜如果太敏感,会把树叶晃动也当成运动,导致降噪效果大打折扣;如果太迟钝,鬼影又去不掉。所以阈值的选择很关键。

4.2 局部运动检测:帧差法与三帧差分

运动检测的方法很多,但移动端实时处理最常用的还是帧差法。简单、快、够用。

4.2.1 帧差法(两帧差分)

原理很简单:两帧对应像素相减,差值大于阈值就认为是运动区域。

// 帧差法伪代码
for each pixel (x, y):
    diff = abs(frame_cur[x][y] - frame_ref[x][y])
    if diff > threshold:
        mask[x][y] = 255  // 运动
    else:
        mask[x][y] = 0    // 静态

但帧差法有个问题——它只能检测到运动物体的边缘,内部容易出现空洞。你想想看,一个行人走过,只有轮廓被检测出来,身体内部因为颜色均匀反而被当成静态区域,鬼影照样存在。

我的经验:帧差法适合做粗检测,后续一定要配合形态学操作(膨胀+腐蚀)把空洞填上。我曾经偷懒没做这一步,结果鬼影去掉了大半,但留下了“空心鬼影”,更难看。

4.2.2 三帧差分

三帧差分是对帧差法的改进。它用连续三帧做两次差分,然后取交集。这样做的好处是能抑制噪声,减少误检。

// 三帧差分伪代码
diff1 = abs(frame_t - frame_t-1)
diff2 = abs(frame_t+1 - frame_t)
mask = (diff1 > threshold) AND (diff2 > threshold)

为什么三帧比两帧好?因为单帧噪声是随机的,两帧差分可能把噪声误判为运动。三帧差分要求运动在连续两帧间都出现,噪声很难同时满足这个条件。

注意:三帧差分会引入一帧延迟。在实时预览场景下,这个延迟可能影响用户体验。我一般只在后处理或非实时场景用三帧差分。

4.3 多帧融合中的权重分配

有了运动掩膜,接下来就是怎么分配权重。这里我介绍几种实践中常用的策略。

4.3.1 基于运动掩膜的硬切换

最简单粗暴的方法:静态区域用多帧平均,运动区域直接用当前帧。

// 硬切换融合
for each pixel (x, y):
    if mask[x][y] == 0:  // 静态
        result[x][y] = average(all_frames[x][y])
    else:                // 运动
        result[x][y] = current_frame[x][y]

这个方法实现简单,但有个问题——运动区域和静态区域的边界会有明显的接缝。你想想看,一个行人旁边是静态背景,融合后行人区域噪点很多,背景区域很干净,边界一眼就能看出来。

4.3.2 基于运动掩膜的软切换

为了解决边界问题,我建议用灰度掩膜代替二值掩膜。运动程度用0~1之间的值表示,然后做加权融合。

// 软切换融合
weight = mask_gray[x][y] / 255.0  // 0~1
result[x][y] = weight * current_frame[x][y] 
             + (1 - weight) * average(all_frames[x][y])

这样运动区域的边缘会有一个平滑过渡,视觉上自然很多。我在项目中实测过,软切换比硬切换的PSNR能高0.5~1dB,主观效果也好得多。

4.3.3 时间域权重衰减

还有一个细节容易被忽略——时间越久的帧,可信度越低。因为手持拍摄时手抖会累积,长时间曝光时场景也可能发生变化。我习惯给每一帧加一个时间衰减系数。

帧序号 时间衰减权重 说明
第1帧(参考帧) 1.0 权重最高
第2帧 0.9 轻微衰减
第3帧 0.8 继续衰减
第4帧 0.7 衰减明显
第5帧 0.6 权重最低

这个衰减系数不是固定的,可以根据场景动态调整。比如夜景长曝光时,衰减可以更慢一些;手持拍摄抖动较大时,衰减要更快一些。

4.4 整体流程:运动补偿与去鬼影管线

下面这张图是我在实际项目中常用的运动补偿与去鬼影管线。它把前面讲的内容串起来了。

运动补偿与去鬼影管线 运动补偿与去鬼影管线 多帧输入序列 全局/局部运动对齐 运动检测(帧差/三帧差分) 参考帧提取 运动掩膜生成 权重分配与融合 去鬼影融合结果 关键:运动掩膜的质量直接决定去鬼影效果,建议配合形态学操作和软切换策略

4.5 避坑指南

做运动补偿和去鬼影,有几个坑我踩过,分享给你:

  • 阈值不要固定:不同光照条件下,帧差法的阈值应该自适应。暗光下噪声大,阈值要调高;亮光下可以调低。我一般用局部均值和标准差做自适应阈值。
  • 运动掩膜要后处理:直接用帧差法得到的掩膜很粗糙,一定要做中值滤波、膨胀、腐蚀。我曾经跳过这一步,结果鬼影没去掉,反而引入了新的伪影。
  • 注意运动物体的阴影:行人走过时,地面上的阴影也会被检测为运动区域。如果不处理,阴影区域会变成“半鬼影”。我一般用颜色信息辅助判断,阴影通常只是亮度变化,色度变化不大。
  • 权重分配不要一刀切:运动区域完全用单帧数据会引入噪声,我建议运动区域也做轻度融合,只是权重降低。比如运动区域用2~3帧融合,静态区域用全部帧融合。

我的习惯:在项目初期,我会把运动掩膜可视化出来,直接叠加在画面上看。这样能快速发现阈值设置是否合理、形态学操作是否到位。等参数调好了再关掉可视化,跑全流程。