4. 运动补偿与对齐
多帧降噪和HDR合成,核心前提是什么?
是帧与帧之间能对上。
你想想看,手持拍摄时手会抖,场景里可能有行人走过,树叶被风吹动。如果直接把这几帧叠在一起,画面就糊了。所以,运动补偿与对齐,就是解决「怎么把多帧图像在像素级对齐」的问题。
我个人习惯把这个问题拆成三层来处理:全局运动估计、局部运动补偿、去鬼影。这三层缺一不可。
4.1 全局运动估计:仿射变换与透视变换
全局运动,说白了就是相机本身的运动。平移、旋转、缩放,甚至轻微的视角变化。
我最早做这个的时候,以为用个简单的平移模型就够了。后来发现,手机拍摄时经常有轻微的旋转和缩放,平移模型根本扛不住。所以,工业界最常用的两个模型是:仿射变换和透视变换。
| 模型 | 自由度 | 适用场景 | 矩阵形式 |
|---|---|---|---|
| 仿射变换 | 6参数 | 平移 + 旋转 + 缩放 + 剪切 | 2×3矩阵 |
| 透视变换 | 8参数 | 仿射 + 透视畸变(视角变化) | 3×3矩阵 |
怎么估计这些参数?最经典的方法是特征点匹配 + RANSAC。
- 提取特征点:比如ORB、SIFT、AKAZE。我个人偏爱AKAZE,速度快,而且对光照变化鲁棒。
- 特征匹配:用暴力匹配或FLANN,找到两帧之间的对应点对。
- RANSAC筛选:剔除误匹配,用内点拟合变换矩阵。
嗯,这里要注意。RANSAC的阈值设置很关键。设得太松,会把错误匹配放进来;设得太紧,内点太少,矩阵算不准。我一般设2~3个像素,具体看图像分辨率。
// 伪代码:仿射变换估计
vector<Point2f> src_pts, dst_pts;
// 假设已经通过特征匹配得到了对应点对
Mat affine_matrix = estimateAffinePartial2D(src_pts, dst_pts);
// 或者用findHomography做透视变换
Mat homography_matrix = findHomography(src_pts, dst_pts, RANSAC, 3.0);
4.2 局部运动补偿:网格化与光流
全局运动搞定了,但问题还没完。
场景里总有独立运动的物体——行人、汽车、飘动的旗帜。这些局部运动,全局变换是管不了的。怎么办?
我的做法是:网格化局部补偿。
把图像分成M×N的网格,每个网格独立计算运动矢量。这样,每个局部区域都能得到自己的偏移量。
- 网格大小:我一般用32×32或64×64。网格太小,容易受噪声干扰;网格太大,又无法捕捉精细运动。
- 运动估计方法:可以用光流(如Farneback光流),也可以用块匹配(Block Matching)。
- 插值:每个像素的运动矢量,由周围网格的运动矢量双线性插值得到。
我曾经在一个项目中,直接用稠密光流做逐像素补偿。效果是好,但计算量太大,手机端根本跑不动。后来改成网格化,速度提升了5倍,效果损失很小。
// 伪代码:网格化运动补偿
int grid_size = 32;
for (int gy = 0; gy < height/grid_size; gy++) {
for (int gx = 0; gx < width/grid_size; gx++) {
// 计算当前网格的运动矢量
Point2f motion = calcBlockMotion(ref_img, cur_img, gx*grid_size, gy*grid_size);
grid_motion[gy][gx] = motion;
}
}
// 对每个像素,插值得到运动矢量并补偿
4.3 去鬼影检测与抑制
运动补偿做完了,是不是就能直接融合了?
不行。还有一个大坑——鬼影。
什么是鬼影?比如你拍HDR,三帧曝光不同。第一帧有个人站着,第二帧他走了。如果你直接融合,这个人就会变成一个半透明的「鬼影」残留在画面里。
去鬼影的核心,就是检测出哪些像素是「异常」的,然后抑制它们。
4.3.1 鬼影检测方法
我常用的方法有两种:
- 基于亮度差异的检测:把当前帧与参考帧做差,差异超过阈值的区域标记为鬼影区域。
- 基于运动一致性的检测:如果某个像素的运动矢量与周围像素不一致,说明它属于独立运动物体,可能是鬼影。
在实际项目中,我更喜欢把两者结合起来。先用亮度差异做粗筛,再用运动一致性做精筛。
// 伪代码:鬼影检测
Mat diff = abs(ref_frame - cur_frame);
Mat ghost_mask = diff > threshold; // 粗筛
// 运动一致性检查
for each pixel (x, y) {
if (ghost_mask.at<uchar>(y, x)) {
Point2f motion = getMotion(x, y);
Point2f avg_motion = getAverageMotion(x, y, 5); // 5x5邻域
if (norm(motion - avg_motion) > motion_threshold) {
ghost_mask.at<uchar>(y, x) = 255; // 确认是鬼影
}
}
}
4.3.2 鬼影抑制策略
检测到鬼影后,怎么处理?
- 直接丢弃:鬼影区域的像素不参与融合。简单粗暴,但可能导致空洞。
- 权重降低:给鬼影区域的像素一个很低的权重,让它们对最终结果影响最小。
- 替换为参考帧:用参考帧的像素直接替换鬼影区域的像素。这是最常用的方法。
我个人习惯用第二种——权重降低。因为直接丢弃或替换,可能会在边缘产生不自然的过渡。降低权重,配合后续的融合算法,效果更自然。
4.4 整体流程与SVG框架图
好了,把上面三个步骤串起来,就是完整的运动补偿与对齐流程。
你看这个流程,每一步都依赖上一步的结果。全局运动没对齐好,局部补偿就会出错;局部补偿有残留,去鬼影的压力就大。所以,每一步都要做到位。