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 四种模型的选择策略
嗯,这里我总结一下我的选择逻辑:
- 三脚架固定拍摄:全局平移就够了,相位相关法又快又准
- 手持轻微抖动:仿射变换,配合RANSAC剔除误匹配
- 手持+场景有深度:透视变换,但要注意匹配点质量
- 场景有运动物体:光流法,但要做好预处理和异常值剔除
你想想看,如果场景里有人在走路,你用全局模型去对齐,那走路的人肯定对不齐。这时候光流法就能派上用场了——我们可以只对齐背景,运动区域单独处理。
4.6 实战中的配准流程
我一般按这个步骤来:
- 预处理:转灰度、高斯滤波、直方图均衡化
- 粗配准:用相位相关法或仿射变换做初步对齐
- 精配准:用光流法或透视变换做精细调整
- 质量评估:计算PSNR或SSIM,如果配准质量低于阈值,重新调整参数
核心原则:配准的精度直接影响后续降噪和HDR合成的效果。配准误差超过1个像素,合成结果就会出现重影。我一般要求配准精度控制在0.5像素以内。