4、图像配准(对齐)基础:全局平移模型、仿射变换、透视变换、光流法简介

图像配准,说白了就是让两张图“对上焦”。

咱们做多帧降噪和HDR合成时,手持拍摄的照片肯定有抖动。如果不做对齐直接叠加,画面就会糊成一片。我刚开始做这个项目时,就吃过这个亏——拍夜景时手抖了一下,结果合成出来的照片跟抽象画似的。

所以,配准是夜景系统的第一道关卡。今天咱们就把四种主流模型捋一遍。

4.1 全局平移模型(最简单,也最常用)

全局平移模型假设图像只有x和y方向的位移,没有旋转、缩放。说白了,就是整张图一起挪了个位置。

适用场景:三脚架固定拍摄、手机轻微抖动(平移占主导)

数学表达: (x', y') = (x + dx, y + dy)

怎么算这个dx和dy?我习惯用相位相关法(Phase Correlation)。原理是利用傅里叶变换的平移性质——两张图的互功率谱的逆变换会有一个峰值,峰值位置就是位移量。

import cv2
import numpy as np

def phase_correlation(img1, img2):
    # 转为灰度
    gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
    gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
    
    # 傅里叶变换
    f1 = np.fft.fft2(gray1.astype(np.float32))
    f2 = np.fft.fft2(gray2.astype(np.float32))
    
    # 互功率谱
    cross_power = (f1 * np.conj(f2)) / (np.abs(f1 * np.conj(f2)) + 1e-10)
    phase = np.fft.ifft2(cross_power)
    
    # 找峰值
    h, w = phase.shape
    phase_shift = np.fft.fftshift(phase.real)
    max_loc = np.unravel_index(np.argmax(phase_shift), phase_shift.shape)
    
    dx = max_loc[1] - w // 2
    dy = max_loc[0] - h // 2
    
    return dx, dy

我的经验:相位相关法对光照变化不敏感,但图像内容太单调(比如纯色天空)时容易误匹配。我一般会先做边缘检测再算相位,准确率能提升不少。

4.2 仿射变换(旋转+平移+缩放)

全局平移只能处理位移。但手持拍摄时,手机难免会旋转一点点。这时候就需要仿射变换了。

仿射变换有6个自由度:2个平移、1个旋转、2个缩放、1个剪切。数学上用一个2×3矩阵表示:

| a  b  tx |
| c  d  ty |

怎么算这6个参数?我常用的是最小二乘法配合RANSAC(随机抽样一致)。

避坑指南:我曾经直接用OpenCV的findHomography做仿射变换,结果发现参数算出来不对。后来才意识到——仿射变换至少需要3对匹配点,而且这3个点不能共线。否则矩阵会奇异。

def affine_align(img1, img2):
    # 提取ORB特征
    orb = cv2.ORB_create(5000)
    kp1, des1 = orb.detectAndCompute(img1, None)
    kp2, des2 = orb.detectAndCompute(img2, None)
    
    # 匹配
    bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
    matches = bf.match(des1, des2)
    matches = sorted(matches, key=lambda x: x.distance)[:100]
    
    # 提取匹配点坐标
    pts1 = np.float32([kp1[m.queryIdx].pt for m in matches]).reshape(-1, 1, 2)
    pts2 = np.float32([kp2[m.trainIdx].pt for m in matches]).reshape(-1, 1, 2)
    
    # RANSAC估计仿射矩阵
    M, mask = cv2.estimateAffine2D(pts1, pts2, method=cv2.RANSAC, ransacReprojThreshold=3.0)
    
    # 应用变换
    h, w = img1.shape[:2]
    aligned = cv2.warpAffine(img2, M, (w, h))
    
    return aligned, M

4.3 透视变换(8自由度,更灵活)

仿射变换假设物体是平面的,且相机只做刚体运动。但实际拍摄中,如果场景有深度变化,或者相机有俯仰、偏航运动,仿射变换就不够用了。

透视变换有8个自由度(3×3矩阵,最后一个元素固定为1)。它能处理梯形畸变、透视变形。

模型 自由度 能处理 不能处理
全局平移 2 x/y位移 旋转、缩放、透视
仿射变换 6 旋转、缩放、剪切 透视变形
透视变换 8 透视、梯形畸变 非刚性变形

我的建议:做多帧夜景时,如果手持拍摄且场景有近景远景,优先用透视变换。但要注意——透视变换需要至少4对匹配点,且不能有三点共线。

def perspective_align(img1, img2):
    # 特征提取与匹配(同上)
    # ...
    
    # 透视变换矩阵
    M, mask = cv2.findHomography(pts1, pts2, cv2.RANSAC, 5.0)
    
    # 应用变换
    h, w = img1.shape[:2]
    aligned = cv2.warpPerspective(img2, M, (w, h))
    
    return aligned, M

4.4 光流法(像素级配准)

前面三种都是基于特征的全局模型。但有些场景下,图像不同区域的运动不一致——比如风吹树叶、行人走动。这时候就需要光流法了。

光流法计算每个像素的运动矢量,说白了就是给每个像素算一个“速度”。

光流法的核心假设:同一个像素点在连续帧中亮度不变,且运动是平滑的。

我常用的光流算法有两种:

  • Lucas-Kanade(稀疏光流):只计算特征点的光流,速度快,适合实时系统
  • Farneback(稠密光流):计算每个像素的光流,精度高,但计算量大
def dense_optical_flow(img1, img2):
    gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
    gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
    
    # Farneback稠密光流
    flow = cv2.calcOpticalFlowFarneback(
        gray1, gray2, None,
        pyr_scale=0.5, levels=3, winsize=15,
        iterations=3, poly_n=5, poly_sigma=1.2,
        flags=0
    )
    
    return flow  # shape: (H, W, 2), 通道0是x方向, 通道1是y方向

我曾经踩过的坑:光流法对光照变化非常敏感。有一次在路灯下拍夜景,灯光闪烁导致光流算出来全是噪声。后来我加了高斯滤波预处理,效果才稳定下来。

4.5 四种模型的选择策略

嗯,这里我总结一下我的选择逻辑:

  1. 三脚架固定拍摄:全局平移就够了,相位相关法又快又准
  2. 手持轻微抖动:仿射变换,配合RANSAC剔除误匹配
  3. 手持+场景有深度:透视变换,但要注意匹配点质量
  4. 场景有运动物体:光流法,但要做好预处理和异常值剔除

你想想看,如果场景里有人在走路,你用全局模型去对齐,那走路的人肯定对不齐。这时候光流法就能派上用场了——我们可以只对齐背景,运动区域单独处理。

图像配准模型选择流程图 输入两张待配准图像 拍摄方式? 三脚架固定 全局平移模型 手持拍摄 场景深度? 场景较浅 仿射变换 透视变换 有运动物体 光流法 提示:实际项目中常组合使用——先用全局模型粗对齐,再用光流法做精细配准

4.6 实战中的配准流程

我一般按这个步骤来:

  1. 预处理:转灰度、高斯滤波、直方图均衡化
  2. 粗配准:用相位相关法或仿射变换做初步对齐
  3. 精配准:用光流法或透视变换做精细调整
  4. 质量评估:计算PSNR或SSIM,如果配准质量低于阈值,重新调整参数

核心原则:配准的精度直接影响后续降噪和HDR合成的效果。配准误差超过1个像素,合成结果就会出现重影。我一般要求配准精度控制在0.5像素以内。