4. 多帧对齐技术:让每一帧都找到自己的位置

多帧夜景处理,说白了就是让几帧画面“叠”在一起。但手持拍摄时,手会抖,画面会飘。如果直接叠加,边缘全是重影,夜景就成了“夜影”。

我刚开始做多帧对齐时,踩过一个坑:以为简单的平移就能搞定。结果一测试,画面边缘的鬼影惨不忍睹。后来才明白——真正的对齐,得从像素级别去算。

4.1 基于光流的特征点匹配

光流法,是当前多帧对齐的核心手段。它的思路很简单:找到前一帧和后一帧之间,每个像素“流”到了哪里。

具体怎么做?我习惯分三步走:

  1. 提取特征点——用 FAST、Harris 或 Shi-Tomasi 角点检测,找到图像中纹理丰富的区域。比如建筑物的边缘、树叶的轮廓。
  2. 计算光流向量——用 Lucas-Kanade 金字塔光流法,在相邻帧之间追踪这些特征点的位移。
  3. 筛选可靠点——不是所有光流都可信。我会用 RANSAC 剔除那些明显偏离主运动的野点。

关键指标:光流计算的精度直接影响对齐效果。我一般要求特征点数量在 200~500 个之间,太少容易欠拟合,太多计算量扛不住。

举个例子。假设我们有两帧图像,第一帧是参考帧,第二帧是待对齐帧。光流法会输出一个向量场,告诉每个像素在第二帧中移动了多少。这个向量场,就是后续对齐的基础。

// 伪代码:Lucas-Kanade 光流计算
cv::Mat prevFrame, currFrame;
std::vector<cv::Point2f> prevPts, currPts;

// 1. 提取特征点
cv::goodFeaturesToTrack(prevFrame, prevPts, 300, 0.01, 10);

// 2. 计算光流
std::vector<uchar> status;
std::vector<float> err;
cv::calcOpticalFlowPyrLK(prevFrame, currFrame, prevPts, currPts, status, err);

// 3. 筛选有效点
std::vector<cv::Point2f> goodPrev, goodCurr;
for (size_t i = 0; i < status.size(); i++) {
    if (status[i]) {
        goodPrev.push_back(prevPts[i]);
        goodCurr.push_back(currPts[i]);
    }
}

我的经验:光流金字塔的层数很关键。层数太少,大位移追不上;层数太多,小细节容易丢失。我一般设 3~4 层,窗口大小 21x21,效果比较稳。

4.2 网格对齐与仿射变换

光流算完了,接下来怎么用?直接对整个图像做全局仿射变换?不行。手持拍摄时,画面不同区域的运动往往不一致——近处的物体位移大,远处的位移小。全局变换会出问题。

所以,我改用网格对齐。

思路是这样的:把图像分成 MxN 个网格,每个网格独立计算一个局部变换矩阵。这样,每个区域都能找到自己的“最佳对齐方式”。

对齐方式 适用场景 优点 缺点
全局仿射 纯旋转/缩放 计算快,稳定 无法处理局部运动
网格仿射 手持夜景 局部精度高 计算量大,需平滑
网格透视 大视角变化 自由度更高 容易过拟合

网格对齐的流程,我总结为四步:

  1. 划分网格——通常 8x8 或 16x16,网格太密容易过拟合,太疏又不够精细。
  2. 计算局部变换——每个网格内,用该区域的特征点拟合一个仿射矩阵(6 个自由度)。
  3. 平滑过渡——相邻网格的变换矩阵不能突变,否则画面会出现“撕裂感”。我会用双线性插值做平滑。
  4. 重采样——根据变换矩阵,把待对齐帧的像素映射到参考帧坐标系。

注意:网格对齐后,一定要做边缘检查。有些网格可能因为特征点太少,算出的变换矩阵是病态的。我遇到过这种情况,结果画面直接扭曲成麻花。解决方案是:对特征点不足的网格,直接继承相邻网格的变换。

4.3 对齐失败的处理

对齐不是万能的。总有一些场景,光流算不准,网格对不上。比如:

  • 大面积的纯色区域——比如夜空、白墙,没有纹理,光流根本算不出来。
  • 快速移动的物体——比如行人、汽车,运动速度太快,帧间位移过大。
  • 极端低光——信噪比太低,特征点全是噪声。

遇到这些情况怎么办?硬对齐只会让画面更糟。我一般用三种策略:

  1. 置信度加权——每个特征点都有一个置信度分数。对齐时,低置信度的点权重降低,甚至直接忽略。
  2. 局部跳过——如果某个网格的对齐误差超过阈值,就放弃该网格的对齐,直接用参考帧的对应区域。
  3. 回退到全局——如果整帧的对齐质量都差(比如特征点数量不足 50 个),就回退到全局仿射变换,至少保证整体不扭曲。

我曾经遇到过一个极端案例:用户在完全黑暗的环境下拍夜景,画面几乎全黑。光流算出来的向量全是随机噪声。最后我加了一个前置判断——如果图像平均亮度低于某个阈值,直接跳过对齐,用简单平移代替。效果反而更好。

4.4 多帧对齐的整体流程

把上面这些串起来,就是一套完整的对齐流水线。我画了一张图,方便你理解:

多帧对齐流水线 多帧输入 特征点提取 (FAST/Shi-Tomasi) L-K 金字塔光流计算 RANSAC 筛选 + 置信度评估 特征点充足? 是 网格对齐 + 仿射变换 否 回退到全局仿射 对齐后多帧输出

你看,整个流程的核心就是:先算光流,再判断质量,最后决定用哪种对齐方式。这个判断逻辑,我建议你写成一个可配置的阈值参数,方便在不同场景下调优。

总结一下:多帧对齐不是一锤子买卖。光流、网格、回退策略,三者缺一不可。我见过很多团队只做全局仿射,结果夜景模式一塌糊涂。记住——对齐的精度,决定了夜景的清晰度。