3、图像预处理与对齐:全局运动估计、局部运动估计与去鬼影预处理

好,咱们进入第三章。前面两章我们把HDR成像的基础和噪声模型聊透了。这一章,是真正动手干活前的关键一步——图像对齐。

你想想看,手持拍摄运动场景,三帧之间手会抖,人会在画面里走动,树叶会被风吹动。如果不做对齐就直接合成,结果就是重影、模糊、边缘发虚。我当年第一次做HDR合成,偷懒没做对齐,结果合成出来的照片像毕加索的画——嗯,抽象派。从那以后,我再也不敢跳过预处理了。

3.1 为什么需要图像对齐?

HDR合成需要多帧不同曝光的图像。理想情况下,这些图像除了亮度不同,其他应该完全一样。但现实是:

  • 相机运动:手持拍摄时的微小抖动
  • 场景运动:行人、车辆、流水、树叶
  • 视差变化:多帧之间存在透视差异

对齐的目的,就是把不同帧的像素映射到同一个坐标系下。说白了,就是让每一帧的同一个物体,出现在同一个像素位置。

核心原则:对齐精度直接影响HDR合成质量。对齐误差超过1个像素,鬼影就会肉眼可见。

3.2 全局运动估计

全局运动估计,适用于相机运动占主导的场景。比如你手持相机拍摄静态风景,画面整体平移或旋转。这时候我们用一个统一的变换模型来描述整幅图像的运动。

3.2.1 仿射变换

仿射变换是最常用的全局运动模型。它支持平移、旋转、缩放、剪切。数学上用一个2×3矩阵表示:

// 仿射变换矩阵
M = [a  b  tx]
    [c  d  ty]

// 变换公式
x' = a*x + b*y + tx
y' = c*x + d*y + ty

仿射变换有6个自由度。计算时,我们至少需要3组匹配点对。实际项目中,我会用特征点匹配(比如ORB或SIFT)找到几十组点对,然后用RANSAC剔除误匹配,最后用最小二乘法拟合出最优的M矩阵。

我的经验:仿射变换对旋转角度不大的场景效果很好。但如果画面有较大透视变化(比如你从侧面拍一个平面),仿射就不够用了。

3.2.2 透视变换

透视变换(也叫单应性变换)比仿射更强大。它用3×3矩阵表示,有8个自由度。可以处理梯形畸变、视角变化。

// 透视变换矩阵
H = [h11  h12  h13]
    [h21  h22  h23]
    [h31  h32  h33]

// 变换公式(齐次坐标)
x' = (h11*x + h12*y + h13) / (h31*x + h32*y + h33)
y' = (h21*x + h22*y + h23) / (h31*x + h32*y + h33)

透视变换至少需要4组匹配点对。我建议用8组以上,配合RANSAC,效果更稳定。

变换类型 自由度 最少匹配点数 适用场景
平移 2 1 纯平移运动
刚体(旋转+平移) 3 2 相机旋转
仿射 6 3 一般相机运动
透视 8 4 大视角变化

3.3 局部运动估计

全局运动模型搞不定局部运动。比如画面里有人在走路,你不能用一个矩阵把整个人都对齐。这时候就需要局部运动估计。

3.3.1 光流法

光流法计算每个像素的运动矢量。它假设:

  • 亮度恒定:同一个点在相邻帧之间亮度不变
  • 小运动:运动幅度不大
  • 空间一致性:相邻像素运动相似

经典的Lucas-Kanade光流法,用局部窗口内的像素求解运动矢量。OpenCV里直接用calcOpticalFlowFarneback就能算稠密光流。

// OpenCV稠密光流示例
cv::Mat flow;
cv::calcOpticalFlowFarneback(prevGray, currGray, flow,
    0.5,  // 金字塔尺度
    3,    // 金字塔层数
    15,   // 窗口大小
    3,    // 迭代次数
    5,    // 多项式邻域
    1.2,  // 标准差
    0);   // 标志位

注意:光流法计算量大。1080p图像上跑稠密光流,一帧可能要几百毫秒。我一般先降采样到640×360再算,精度损失不大,速度能快5倍。

3.3.2 块匹配

块匹配是另一种局部运动估计方法。把图像分成小块,在参考帧的搜索窗口内找最相似的块。相似度用SAD(差和)或SSD(平方差和)衡量。

// 块匹配伪代码
for each block in current frame:
    best_match = INF
    best_offset = (0, 0)
    for each offset in search_window:
        sad = computeSAD(block, reference_block)
        if sad < best_match:
            best_match = sad
            best_offset = offset
    motion_vector[block] = best_offset

块匹配比光流快,但精度稍低。我个人的习惯是:

  • 需要高精度时用光流法
  • 需要实时处理时用块匹配
  • 两者结合:先用块匹配做粗对齐,再用光流做精对齐

3.4 去鬼影预处理

对齐之后,还有个大问题——鬼影。鬼影是运动物体在不同曝光帧中位置不同导致的。即使做了对齐,运动物体本身还是对不齐的。

去鬼影预处理,就是在合成之前,先检测出运动区域,然后决定如何处理这些区域。

3.4.1 运动区域检测

常用的方法:

  1. 帧差法:两帧相减,值大于阈值的就是运动区域。简单粗暴,但对噪声敏感。
  2. 光流幅值法:计算光流,幅值大的区域就是运动区域。更准确,但计算量大。
  3. 一致性检测:用多帧之间的亮度关系判断。比如,过曝区域和正常曝光区域之间的亮度关系应该符合相机响应曲线,不符合的就是运动区域。

避坑指南:我曾经用帧差法检测运动,结果把树叶晃动也当成运动区域,导致合成结果出现大量空洞。后来改用光流幅值+形态学滤波,效果好了很多。

3.4.2 运动区域处理策略

检测到运动区域后,有三种处理方式:

策略 做法 优点 缺点
丢弃 运动区域只用参考帧 简单,无鬼影 可能丢失细节
融合 运动区域用多帧加权平均 保留细节 可能有轻微鬼影
替换 运动区域用单帧最佳曝光 无鬼影,细节好 需要选择最佳帧

我个人推荐第三种——替换策略。先找出运动区域,然后从多帧中选出曝光最合适的那一帧,把运动区域替换过去。这样既没有鬼影,又能保留细节。

3.5 本章知识体系

下面这张图,把本章的核心逻辑串起来了:

图像预处理与对齐知识体系 图像预处理与对齐知识体系 多曝光输入帧 全局运动估计 局部运动估计 仿射变换 透视变换 光流法 块匹配 对齐后的图像 去鬼影预处理 运动区域检测 运动区域处理 鬼影消除 预处理完成

3.6 实战建议

最后,给几个实战中的小建议:

  • 先全局后局部:先用仿射或透视做全局对齐,再用光流或块匹配做局部修正。这样效率高,精度也够。
  • 多尺度策略:在低分辨率上做粗对齐,再在高分辨率上做精对齐。速度能提升3-5倍。
  • 运动区域要膨胀:检测到运动区域后,用形态学膨胀操作扩大一点。否则边缘处容易残留鬼影。
  • 别忘了中值滤波:对齐后的运动矢量图,用中值滤波平滑一下,能去除很多孤立的错误匹配。

我的习惯:每次做HDR合成前,我都会先跑一遍对齐质量评估。把对齐后的帧叠在一起,用差值图看看有没有明显的未对齐区域。这一步花不了几秒钟,但能省掉后面大量调试时间。