1. 背景介绍
图像对齐技术是一种使两个或多个图像在空间上精确对齐的过程,目的是使得它们之间的共同特征或内容能够精确匹配,这一技术在计算机视觉、医学成像、遥感、视频处理等多个领域有着广泛的应用。
图像对齐技术:从特征点检测到光流 - 知乎 (zhihu.com)
其中基于全局单应性变换的方案是图像对齐领域最基本的一种方案,在平面运动以及画面特征比较丰富的场景下有着不错的对齐精度。根据全局对齐的算法实现方案可以大致分为三类:
(1) 基于特征点的对齐方法:通过特征点的检测与匹配,计算出单应性变换矩阵。这是目前比较常见的全局对齐方案,但是比较依赖于特征的质量与RANSAC算法的鲁棒度。
(2) 基于相关性的对齐方法:与传统的基于特征点的方法不同,基于相关性的对齐算法通过最大化两幅图像之间的全局相关性来估计图像间的几何变换参数。这种方法尤其适用于那些具有大量相似结构或纹理信息的图像,以及需要高精度对齐的场景。
(3) 基于深度学习的对齐方法:通过端到端的网络训练,输入两幅图像得到它们之间的单应性变换矩阵,这类方案在图像特征质量不太好的情况下也能得到不错的效果。
2. 基于特征点的方案
基于特征点检测的图像对齐方案可以分为如下几步:
(1) 特征检测:在图像中检测特征点,常用的特征检测算子有Harris、FAST等,这些算法旨在找到在图像中具有特别视觉特征的边缘、角点等的坐标位置。
(2) 特征描述:对每个检测到的特征点计算描述符,这个描述符应该能够表征该特征点的视觉特征并且对于视角变化、亮度变化等具有一定的不变性,如SIFT和SURF等算法都自带了特征描述方法。
(3) 特征匹配:将两幅图像中的特征描述符进行匹配,常见的匹配策略包括最近邻匹配、比率测试等。为了提高匹配的准确度,还需要使用RANSAC算法去除异常匹配对。
(4) 几何变换估计:基于匹配好的特征点对估计两幅图像之间的几何变换关系,通常是仿射变换或投影变换,具体取决于应用场景和所允许的变换复杂度,常用的方法包括最小二乘法、迭代最近点算法等。
(5) 图像对齐:根据上一步得到的几何变换参数,对一幅图像进行变换,使得两幅图像在相同的空间参考系下对齐。
2.1 特征点提取与描述
特征点检测可以分为位置提取和特征点描述两个步骤:常用的特征点提取算法如Harris、FAST算子可以快速提取图像中的角点,而特征点描述子则有BRIEF、FREAK、BRISK等二进制码的描述方案。
为了解决在不同条件下(如视角变化、尺度缩放、亮度改变、噪声干扰等)提取的描述子的稳定,一些检测与描述的综合性方案也被提出,如非常经典的SIFT、SURF、ORB算子等。这类算法特征的检测和描述对尺度和旋转变化鲁棒,有不少也可以满足实时应用的需求。
首先介绍两种特征点提取的方法:Harris角点和FAST特征点。
(1) Harris
Harris角点检测由Chris Harris和Mike Stephens在1988年提出,因其简单高效被广泛用于某些特定的应用场景,如立体视觉任务中在相机标定时可以用来对棋盘格进行角点检测。
Harris角点检测的核心在于计算局部区域内的图像强度变化,以此来确定一个像素是否位于角点上。比如我们用一个窗口在图像中滑动,在平坦区任意方向滑动前后灰度值变化较小,而在角点区域沿任意方向滑动前后变化很大,我们可以根据这一特点去判定角点。
当窗口 � 滑动位移量 (�,�) 时,像素值的变化量 � 可以用如下方程描述:
这里 (�,�) 是窗口 � 中的所有像素坐标集合,根据(�,�)离窗口中心点的距离可以引入一个权重 �(�,�) 提高中心点的加权比例。对 �(�+�,�+�) 进行泰勒展开:
带入变化量�中最终可以得到下式,其中 �� 和 �� 分别代表图像在水平和垂直方向上的梯度(这里为了简化权重直接设为1):
�这里我们称做Harris矩阵(也称为自动协方差矩阵),接下来一个比较容易想到的方案是带入各个方向的位移量 (�,�) 把 � 计算出来,然后设定阈值判断一个点是不是角点。
但实际上Harris算法做了进一步的观察,矩阵 �的特征值 � 和 � 反映了图像在不同方向上的变化程度:在角点处这两个特征值都应该相对较大,因为角点意味着在至少两个正交方向上都有显著变化。而在边缘处一个特征值较大而另一个较小,因为变化主要沿一个方向发生。
为了找到同时在两个方向上变化显著的点,利用 � 进一步提出了一个角点响应函数 � :
这里 ���(�) 是矩阵的行列式, �����(�) 是矩阵的迹。 � 是一个调节参数用于平衡边缘响应和角点响应,通常取值在0.04到0.06之间。
import numpy as np
from PIL import Image, ImageDraw
import matplotlib.pyplot as plt
from scipy.ndimage import convolve
def compute_gradient(image):
"""计算图像的梯度"""
sobel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])
sobel_y = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]])
Ix = convolve(image, sobel_x)
Iy = convolve(image, sobel_y)
return Ix, Iy
def compute_harris_response(image, sigma=1, k=0.04):
"""计算Harris响应"""
Ix, Iy = compute_gradient(image)
Ix2 = Ix ** 2
Iy2 = Iy ** 2
Ixy = Ix * Iy
# 高斯滤波,模拟邻域内计算
window_size = 2 * int(3 * sigma) + 1
gauss_kernel = np.fromfunction(
lambda x, y: (1 / (2 * np.pi * (sigma ** 2))) * np.exp(-(x ** 2 + y ** 2) / (2 * sigma ** 2)),
(window_size, window_size))
Ix2 = convolve(Ix2, gauss_kernel)
Iy2 = convolve(Iy2, gauss_kernel)
Ixy = convolve(Ixy, gauss_kernel)
# 计算harris矩阵M并得到响应值R
h, w = image.shape[0], image.shape[1]
M = [np.array([[Ix2[i, j], Ixy[i, j]], [Ixy[i, j], Iy2[i, j]]]) for i in range(h) for j in range(w)]
det_M, trace_M = list(map(np.linalg.det, M)), list(map(np.trace, M))
R = np.array([d - k * t ** 2 for d, t in zip(det_M, trace_M)])
R = R.reshape(h, w)
return R
def detect_harris_corners(image_path, threshold=0.05, sigma=1, k=0.06):
"""检测并显示Harris角点"""
img = np.array(Image.open(image_path).convert("L")) # 转换为灰度图像
img_rgb = np.array(Image.open(image_path))
harris_response = compute_harris_response(img, sigma, k)
corners = harris_response > threshold * harris_response.max()
img_with_corners = Image.fromarray(np.uint8(img_rgb))
draw = ImageDraw.Draw(img_with_corners)
corner_coords = np.argwhere(corners)
for coord in corner_coords:
draw.rectangle([coord[1] - 1, coord[0] - 1, coord[1] + 1, coord[0] + 1], fill=(255, 0, 0))
plt.imshow(img_with_corners)
plt.show()
image_path = 'input.png'
detect_harris_corners(image_path)
(2) FAST(features from Accelerated Segment Test)
FAST特征检测和命名一样性能很快,并且也有着不错的精度。FAST算法的基本检测方法是一个像素 � 及其周围一圈的像素(通常是16个像素,形成一个半径为3的圆环),算法检查中心像素相对于圆环上像素的亮度差异,判断是否超过了某个预先设定的阈值 � 。
当圆环上有连续的 � 个点的亮度大于 �+� 或者小于 �−� ,则该像素可以看作是角点。实现比较简单,直接调用opencv的函数:
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread("input.png")
fast = cv.FastFeatureDetector_create(threshold=30)
kp = fast.detect(img, None)
img_out = cv.drawKeypoints(img, kp, None, color=(0, 0, 255))
plt.imshow(img_out[:, :, ::-1])
plt.show()
当得到了特征点的位置坐标后,我们想把描述同一视觉特征的点匹配起来,还需要增加对各个特征点的描述才行,接下来介绍一种常用的对特征点进行描述的方法。
(1) BRIEF(Binary Robust Independent Elementary Features)
BRIEF是一种简洁而高效的特征描述子,常与FAST特征提取器一起使用,组合成一套快速的特征检测与描述方案。它的描述子是由一系列的0和1组成的二进制串,存储和匹配都非常高效。不过由于对噪声比较敏感,一般会先对图像做一个滤波。
首先,算法在特征点邻域的窗口 � 中,随机选取图像中的N个像素对。每个像素对包含两个位置用于比较它们的亮度值,例如如果第一个像素比第二个像素亮则对应位设为1,反之设为0。作者比较了一些像素对的随机化方案,发现G II中的高斯分布采样效果最佳。
对于每个特征点根据其周围的像素对亮度比较结果,生成一个固定长度的二进制字符串。这个字符串就是该特征点的BRIEF描述子,在特征匹配阶段,两个特征点的描述子通过计算汉明距离来衡量相似性。
前面介绍了一些特征提取和描述的方案,它们的缺点是无法应对在不同尺度的图像,或者图像发生了旋转的情况下进行高精度的检测和匹配。接下来介绍几种常用的特征点检测与描述的整体方案,它们提出的检测和描述方案是搭配使用的,通过了额外的处理来实现了尺度不变性和旋转不变性。
(1) SIFT(Scale-Invariant Feature Transform)
SIFT通过构建不同尺度的图像金字塔来寻找在不同尺度下的关键点,这样可以确保特征在图像缩放时仍能被正确检测。在确定特征点位置后,算法会计算关键点邻域的梯度方向直方图以此来确定特征的方向,这使得特征描述对于图像的旋转不敏感。除了尺度不变性和旋转不变性,SIFT描述子通过对关键点邻域内的像素进行对比度归一化处理,还有光照不变性等优点。
一张图像中物体的远近代表着大小和清晰度的不同,在特征提取部分SIFT构造了多尺度空间,具体来说使用多尺度高斯金字塔以及对应的高斯差分(DOG)来实现的:
其中多尺度金字塔通过不断对图像进行下采样得到不同尺寸的图像,而每个尺寸的图像又通过高斯滤波得到多个相同尺寸不同模糊程度 �� 的图像组成多尺度高斯金字塔。对于多尺度高斯金字塔的每层,计算同尺寸的相邻两层图像的残差得到DOG(difference of gaussians):
import numpy as np
import cv2
def genDOG(image, sigma = 1.6):
num_octaves = int(round(np.log(min(image.shape)) / np.log(2) - 1)) # max octaves
gaussian_pyr_full_octaves = []
# generate gaussian pyramid
image = cv2.resize(image, (0, 0), fx=2, fy=2, interpolation=cv2.INTER_LINEAR)
image = cv2.GaussianBlur(image, (0, 0), sigmaX=sigma, sigmaY=sigma)
for octave_val in range(num_octaves):
gaussian_pyr_cur_octave = []
gaussian_pyr_cur_octave.append(image) # first image in octave already has the correct blur
for blur_index in range(num_octaves + 3):
k = 2 ** (1. / (num_octaves + 3))
image = cv2.GaussianBlur(image, (0, 0), sigmaX=(k ** (blur_index - 1)) * sigma, sigmaY=(k ** (blur_index - 1)) * sigma)
gaussian_pyr_cur_octave.append(image)
cv2.imwrite("gaussian_pyr{}_{}.jpg".format(octave_val,blur_index),image)
gaussian_pyr_full_octaves.append(gaussian_pyr_cur_octave)
octave_base = gaussian_pyr_cur_octave[-3]
image = cv2.resize(octave_base, (int(octave_base.shape[1] / 2), int(octave_base.shape[0] / 2)), interpolation=cv2.INTER_NEAREST)
# generate DOG pyramid
dog_pyr_full_octaves = []
oct_val = 0
for gaussian_pyr_per_octave in gaussian_pyr_full_octaves:
dog_pyr_cur_octave = []
for idx in range(len(gaussian_pyr_per_octave[1:])):
dog_cur_image = gaussian_pyr_per_octave[idx+1] - gaussian_pyr_per_octave[idx]
dog_pyr_cur_octave.append(dog_cur_image) # ordinary subtraction will not work because the images are unsigned integers
cv2.imwrite("dog_pyr{}_{}.jpg".format(oct_val, idx), dog_cur_image)
dog_pyr_full_octaves.append(dog_pyr_cur_octave)
oct_val += 1
return gaussian_pyr_full_octaves, dog_pyr_full_octaves
接下来对于DOG的每层图像中的一个像素,都与其26个相邻像素进行对比,如果它是一个局部极值就代表该点是当前尺度(尺寸 � 和模糊程度��)下的一个潜在特征点。
def isPixelAnExtremum(first_subimage, second_subimage, third_subimage, threshold):
center_pixel_value = second_subimage[1, 1]
if abs(center_pixel_value) > threshold:
if center_pixel_value > 0:
return all(center_pixel_value >= first_subimage) and \
all(center_pixel_value >= third_subimage) and \
all(center_pixel_value >= second_subimage[0, :]) and \
all(center_pixel_value >= second_subimage[2, :]) and \
center_pixel_value >= second_subimage[1, 0] and \
center_pixel_value >= second_subimage[1, 2]
elif center_pixel_value < 0:
return all(center_pixel_value <= first_subimage) and \
all(center_pixel_value <= third_subimage) and \
all(center_pixel_value <= second_subimage[0, :]) and \
all(center_pixel_value <= second_subimage[2, :]) and \
center_pixel_value <= second_subimage[1, 0] and \
center_pixel_value <= second_subimage[1, 2]
return False
由于图像的像素属于离散信号,为了对潜在候选特征点进行精确定位,可以通过插值等方法优化特征点的位置和尺度:
同时也需要剔除低对比度和边缘响应的关键点,剔除的方法和Harris算法很像,设定阈值可以剔除一部分低响应点:
在特征描述子部分,SIFT首先基于检测到的特征点邻域内的梯度方向分布为每个特征点分配一个主方向,这使得特征描述具有旋转不变性。
此时每个特征点有如下三个信息(换算到全尺寸):坐标位置、尺度(尺寸和模糊程度)、主方向。在描述子编码的设计中,根据关键点的方向在关键点邻域内提取特定尺寸(16x16)的像素块,并计算其中像素的梯度方向和幅值。
像素块中每4x4个小窗口计算的梯度进一步被压缩在8个方向中,最后我们得到4x4x8也就是128个单元的描述子。
import cv2
import numpy as np
# 加载图像
image = cv2.imread('input.png', 0)
# 初始化SIFT检测器
if int(cv2.__version__.split('.')[0]) >= 4: # 对于OpenCV 4.x及以上版本
sift = cv2.SIFT_create()
else: # 对于OpenCV 3.x或更早版本
sift = cv2.xfeatures2d.SIFT_create()
# 检测关键点
keypoints, descriptors = sift.detectAndCompute(image, None)
# 绘制关键点
image_with_keypoints = cv2.drawKeypoints(image, keypoints, np.array([]), (0,0,255), cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
# 显示结果
cv2.imshow('SIFT Keypoints', image_with_keypoints)
cv2.waitKey(0)
cv2.destroyAllWindows()
这里使用opencv可视化了特征点的坐标,圆环对应各点的主方向(第二主方向)和不同尺度描述符的大小。
(2) SURF(Speeded up robust features)
SIFT的缺点是算法复杂度较高,计算速度较慢。SURF是对SIFT的一种加速版本,通过使用Hessian矩阵的简化计算来提高速度,同时保持较好的尺度和旋转不变性。SURF的描述子较SIFT更简洁,但仍能提供良好的匹配效果。
首先对于尺度空间的建立,与SIFT不同的是SURF通过boxfilter的kernel尺寸模拟尺度的变化,使得所有尺度的图像尺寸是一样的,而对于boxfilter采用积分图的方式也可以保证很高的效率。而对于特征点检测,SURF对图像中的每一个像素点求一个Hessian矩阵,类似Harris方法根据Hessian矩阵来判别极值点。
而对于主方向计算和特征描述部分,SURF采用了Haar小波变换方法,计算特征点一定半径范围内的小波响应:
Harr小波模板
计算出图像Haar小波变换后x和y方向的响应值,然后对响应值进行加权,主方向即灰度变化剧烈的方向,对于特征点使用夹角为60度的扇形寻找响应最大的方向即可。
在特征点主方向的邻域块中取4×4分区,统计每个小区域的Haar响应值。每个区域得到一个4维的特征向量,最终每个特征点有64维的特征向量作为SURF特征描述子。
import cv2
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
# Load the image
image1 = cv2.imread('./images/face1.jpeg')
# Convert the training image to RGB
training_image = cv2.cvtColor(image1, cv2.COLOR_BGR2RGB)
# Convert the training image to gray scale
training_gray = cv2.cvtColor(training_image, cv2.COLOR_RGB2GRAY)
# Create test image by adding Scale Invariance and Rotational Invariance
test_image = cv2.pyrDown(training_image)
test_image = cv2.pyrDown(test_image)
num_rows, num_cols = test_image.shape[:2]
rotation_matrix = cv2.getRotationMatrix2D((num_cols/2, num_rows/2), 30, 1)
test_image = cv2.warpAffine(test_image, rotation_matrix, (num_cols, num_rows))
test_gray = cv2.cvtColor(test_image, cv2.COLOR_RGB2GRAY)
(3) ORB(Oriented FAST and Rotated BRIEF)
ORB算法结合了FAST特征点检测器的快速性与BRIEF描述符的高效性,并引入了特征尺度和特征方向,使之成为一种快速且鲁棒的特征点匹配方法。
在FAST算子中给定像素p,快速将p的亮度与周围的16个像素进行比较,这些像素在p周围的一个小圆圈中。然后将圆圈中的像素分为三类(比p亮,比p暗或与p相似)。如果超过8个像素比p暗或比p亮则将其选为关键点,因此FAST找到的关键点可以提供图像中确定边缘的位置信息。
但是FAST特征没有方向分量和多尺度特征,因此ORB算法引入了多尺度图像金字塔,通过检测每个尺度中的关键点在不同的尺度上定位特征点。在定位特征点后,根据围绕该特征点的像素强度水平变化,为每个关键点分配一个方向:
而特征描述部分,测试发现原始的BRIEF算法在图片旋转45度以上后准确率就下跌到0,因此对于每个特征会根据前面计算的主方向对图片进行旋转:
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('simple.jpg', cv.IMREAD_GRAYSCALE)
# Initiate ORB detector
orb = cv.ORB_create()
# find the keypoints with ORB
kp = orb.detect(img,None)
# compute the descriptors with ORB
kp, des = orb.compute(img, kp)
# draw only keypoints location,not size and orientation
img2 = cv.drawKeypoints(img, kp, None, color=(0,255,0), flags=0)
plt.imshow(img2), plt.show()
2.2 特征点匹配
常用的特征匹配方法有暴力法(BF-Match)和快速近似最近邻搜索(FLANN)。其中暴力法对于查询图像中的每一个特征点,暴力匹配会将其描述子与数据库图像中所有特征点的描述子进行比较,计算两者之间的差异度量。而FLANN是一种用于近似最近邻搜索的算法库,它可以显著加快特征点匹配的速度,特别是在大数据集上。
选择暴力匹配还是FLANN,主要取决于应用的需求。如果精度是首要考虑因素,且数据量不大,暴力匹配可能是更好的选择。相反,如果处理大量数据且需要高效匹配,FLANN则更为合适。
2.3 特征点筛选
虽然RANSAC本身不是专门针对图像对齐算法而设计的,但它作为一种常用的模型拟合方法常用于从一组匹配中去除异常值,从而得到更准确的拟合结果,因此而在特征点筛选中较为常用。
(1)随机抽样
从所有匹配的特征点对中随机选取一个预定数量的样本集(通常是根据模型的最少需要参数决定,比如对于二维平移或旋转需要至少4对匹配点)。
(2)模型拟合
利用这些样本点计算一个假设的模型,比如一个变换矩阵,该矩阵能描述两幅图像之间的几何关系。
(3)一致性评估
将所有其他未被选作样本的特征点依据当前模型进行测试,计算它们与模型预测位置的偏差,通常使用一个阈值作为容许误差,符合模型预测且误差在阈值内的点被归为“一致集”。
(4)模型评估与更新
如果一致集中的点数超过某个阈值或比例(这表明模型较好地拟合了大部分数据),则用一致集中的点重新估计模型参数,以提高模型的准确性。否则,重复步骤1至3。
(5)迭代与选择最佳模型
上述过程会迭代进行多次(通常几百次),每次都会尝试一个新的随机样本集。所有尝试中得到的最佳模型(即一致集最大的模型)被认为是最终的模型。
(6)应用模型
利用最终确定的模型对所有特征点进行过滤,只保留那些符合模型的匹配点,从而剔除了由于光照变化、视角变化、重复纹理等因素引起的错误匹配点。
import cv2
import numpy as np
def ransac_feature_matching(img1_kp, img1_des, img2_kp, img2_des):
"""
使用RANSAC进行特征点匹配筛选
:param img1_kp: 图像1的特征点
:param img1_des: 图像1的特征描述符
:param img2_kp: 图像2的特征点
:param img2_des: 图像2的特征描述符
:return: RANSAC筛选后的匹配点对
"""
# 建立BFMatcher对象
bf = cv2.BFMatcher()
# 使用KNN检测匹配点对,k=2表示每个点找两个最接近的匹配
matches = bf.knnMatch(img1_des, img2_des, k=2)
# 应用比率测试筛选高质量的匹配
good_matches = []
for m, n in matches:
if m.distance < 0.75 * n.distance:
good_matches.append(m)
# 提取匹配点坐标
img1_points = np.float32([img1_kp[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
img2_points = np.float32([img2_kp[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)
# 应用RANSAC算法去除异常值
_, inliers = cv2.findFundamentalMat(img1_points, img2_points, method=cv2.FM_RANSAC, ransacReprojThreshold=3.0)
# 筛选出RANSAC认为的内点(即稳健匹配)
ransac_matches = [good_matches[i] for i in range(len(good_matches)) if inliers[i,0] == 1]
return ransac_matches
3. 基于相关性的方案
ECC:Parametric Image Alignment Using Enhanced Correlation Coefficient Maximization
ECC(Enhanced Correlation Coefficient)是一个基于优化思想的迭代方案,采用了最近提出的相似性度量,即增强的相关系数作为配准问题的目标函数。它对对比度和亮度的光度失真是不变的,其次虽然参数的约束方程是非线性函数,但我们将要为优化问题开发的迭代方案将是线性的。
设图像对中参考帧为�� ,待对齐帧为�� ,匹配��上坐标 � 对应的��中的坐标为�,它们之间的坐标映射关系为 � ,映射的变换矩阵为 � :
两项之差可以作为约束方程求解 � ,但为了引入光度不变性,这里加了一个亮度匹配项 � 保证对亮度不一致的图像对也能很好匹配:
用矩阵的形式描述这个约束方程:
归一化后的相似性度量约束方程可以描述如下,可以看到归一化后的表达是对亮度无关的,最小化公式 � 可以等价为最大化公式 � :
对于最大化公式 �可以使用直接搜索或基于梯度的方法来执行,这里文中使用后者用一系列的二次优化来代替原来的优化问题。每一次二次优化都依赖于前一次优化的结果,从而产生一系列参数估计,这些估计有望收敛到期望的优化向量。在每次迭代中我们不必优化目标函数,而是对该函数进行近似。
首先对��进行泰勒展开:
带入约束方程�表示如下:
接下来的最大化 � 求解 � 的推理感兴趣可以看下原论文,最终是通过步长 Δ� 迭代更新 � 直至收敛:
import cv2
import numpy as np
def align_images_ecc(img_ref, img_moving, warp_mode=cv2.MOTION_AFFINE, num_iterations=5000, termination_eps=1e-10):
"""
使用ECC方法对齐两张图像。
参数:
img_ref: 参考图像,灰度图像(numpy数组)
img_moving: 待对齐的移动图像,灰度图像(numpy数组)
warp_mode: 变换模式,如cv2.MOTION_AFFINE, cv2.MOTION_HOMOGRAPHY等
num_iterations: 最大迭代次数
termination_eps: 终止条件的阈值
返回:
img_aligned: 对齐后的图像
warp_matrix: 应用于移动图像的变换矩阵
"""
# 初始化变换矩阵
warp_matrix = np.eye(2, 3, dtype=np.float32) if warp_mode == cv2.MOTION_AFFINE else np.eye(3, 3, dtype=np.float32)
# 计算ECC并找到最佳变换
_, warp_matrix = cv2.findTransformECC(
img_ref,
img_moving,
warp_matrix,
warp_mode,
criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, num_iterations, termination_eps),
inputMask=None,
gaussFiltSize=1
)
# 应用找到的变换到移动图像
if warp_mode == cv2.MOTION_HOMOGRAPHY:
img_aligned = cv2.warpPerspective(img_moving, warp_matrix, (img_ref.shape[1], img_ref.shape[0]), flags=cv2.INTER_LINEAR + cv2.WARP_INVERSE_MAP)
else:
img_aligned = cv2.warpAffine(img_moving, warp_matrix, (img_ref.shape[1], img_ref.shape[0]), flags=cv2.INTER_LINEAR + cv2.WARP_INVERSE_MAP)
return img_aligned, warp_matrix
4. 基于深度学习的方案
传统的全局对齐算法需要通过检测特征点或相关性关系计算单应性矩阵,对齐效果十分依赖图像的内容信息的提取精度。基于深度学习的图像对齐方案通过给一个网络输入两张图像,可以直接得出homography的8个参数,对于无纹理或弱纹理图像内容具有不错的效果。
4.1 有监督学习
(1) Deep Image Homography Estimation
传统的单应性估计由特征点提取和单应性估计两个阶段组成,角点检测阶段会计算大量过完备的点集,然后使用RANSAC或平方损失函数进行鲁棒化。深度学习方法在给定一对图像的情况下简单地返回这对图像的单应性矩阵,而不需要手动设计角特征、线特征等。
这篇文章提出了一种深度卷积神经网络来估计一对图像之间的单应性,网络结构中前馈网络有10层,以两张堆叠的灰度图像作为输入,并产生一个8自由度的单应性矩阵,可用于将像素从第一张图像映射到第二张图像。
网络使用带有BN和ReLU的3x3卷积块,在架构上类似于VGG Net。网络使用大小为128x128x2的双通道灰度图像作为输入,网络使用8个卷积层,每两个卷积之后为最大池化层(2x2, stride2)。8个卷积层中每层的层数数量如下:64、64、64、64、128、128、128。卷积层之后是两个完全连接的层,第一个全连接层有1024个单元,在最后一个卷积层和第一个完全连接层之后应用概率为0.5的Dropout。
由于直接从两张图像得到一个单应性矩阵对于网络来说很难进行解释性,这里使用了一个四点的单应性参数化方法:模型最终学习到的是将一个图像的四个顶点映射到第二个图像的位移量,我们最后再手动转换成单应性矩阵。
文中提出了两种卷积神经网络结构:一种是直接估计单应性矩阵参数的回归网络,另一种是产生单应性矩阵置信度的分类网络。两个网络在最后一层之前共享相同的架构,其中第一个网络产生实值输出,第二个网络产生离散量。回归网络直接产生四个顶点8个实值数位移量,并在训练过程中使用欧几里得( �2 )损失作为最后一层。
分类网络使用量化方案,在最后一层有一个softmax,使用交叉熵损失训练。网络能够对每个角产生置信度,选择对8个输出维度中的每个维度使用21个量化bin,这就得到了一个具有168个输出神经元的最终层。
网络使用MS-COCO图像以端到端的方式进行训练,不需要单独的局部特征检测和转换估计阶段。数据生成步骤中,为了生成单个训练样例,首先在大图 � 中的 � 位置随机裁剪正方形块,这个随机裁剪块设为 �� 。然后块的四个顶点添加了 [−�,�] 范围内的值随机扰动生成单应性矩阵 ��� ,将此单应性矩阵的逆应用于大图以生成图像 �′ ,这样我们就得到了输入的两张待对齐图像和对应的四角位移量。
如果想让使该方法对运动模糊更具鲁棒性,可以将模糊应用到训练集中的图像上。如果还希望该方法对遮挡具有鲁棒性,可以在训练图像中插入随机的遮挡形状。
最终对比传统特征点提取方法和深度学习估计方法,整体看来Deep-Homo的方案更加精准:
效果展示
4.2 无监督学习
基于深度学习的单应性矩阵计算方法通常使用合成图像数据进行有监督学习,忽略了在现实世界应用中处理深度差异和移动物体的重要性。为了克服这一问题学者提出了无监督的方法,通过直接约束输入两幅图像之间的光度损失进行训练。
(1) Unsupervised Deep Homography: A Fast and Robust Homography Estimation Model
如果通过深度学习网络得到一对图像的单应性变换矩阵,实际上我们可以直接通过输入图像和矩阵判定对齐质量的好坏,而不需要额外标注的单应性变换矩阵GT标签。这篇文章提出了一个无监督网络架构,并引入了张量直接线性变换层(Tensor DLT)和空间变换层(Spatial Transformation),通过最小化像素光度损失进行无监督训练。
Tensor DLT
这里借鉴传统的DLT算法将4个顶点的偏移量转换成单应性变换矩阵 ��� :
def solve_DLT(self):
batch_size = self.params.batch_size
pts_1_tile = self.pts_1_tile
# Solve for H using DLT
pred_h4p_tile = tf.expand_dims(self.pred_h4p, [2]) # BATCH_SIZE x 8 x 1
# 4 points on the second image
pred_pts_2_tile = tf.add(pred_h4p_tile, pts_1_tile)
# Auxiliary tensors used to create Ax = b equation
M1 = tf.constant(Aux_M1, tf.float32)
M1_tensor = tf.expand_dims(M1, [0])
M1_tile = tf.tile(M1_tensor,[batch_size,1,1])
M2 = tf.constant(Aux_M2, tf.float32)
M2_tensor = tf.expand_dims(M2, [0])
M2_tile = tf.tile(M2_tensor,[batch_size,1,1])
M3 = tf.constant(Aux_M3, tf.float32)
M3_tensor = tf.expand_dims(M3, [0])
M3_tile = tf.tile(M3_tensor,[batch_size,1,1])
M4 = tf.constant(Aux_M4, tf.float32)
M4_tensor = tf.expand_dims(M4, [0])
M4_tile = tf.tile(M4_tensor,[batch_size,1,1])
M5 = tf.constant(Aux_M5, tf.float32)
M5_tensor = tf.expand_dims(M5, [0])
M5_tile = tf.tile(M5_tensor,[batch_size,1,1])
M6 = tf.constant(Aux_M6, tf.float32)
M6_tensor = tf.expand_dims(M6, [0])
M6_tile = tf.tile(M6_tensor,[batch_size,1,1])
M71 = tf.constant(Aux_M71, tf.float32)
M71_tensor = tf.expand_dims(M71, [0])
M71_tile = tf.tile(M71_tensor,[batch_size,1,1])
M72 = tf.constant(Aux_M72, tf.float32)
M72_tensor = tf.expand_dims(M72, [0])
M72_tile = tf.tile(M72_tensor,[batch_size,1,1])
M8 = tf.constant(Aux_M8, tf.float32)
M8_tensor = tf.expand_dims(M8, [0])
M8_tile = tf.tile(M8_tensor,[batch_size,1,1])
Mb = tf.constant(Aux_Mb, tf.float32)
Mb_tensor = tf.expand_dims(Mb, [0])
Mb_tile = tf.tile(Mb_tensor,[batch_size,1,1])
# Form the equations Ax = b to compute H
# Form A matrix
A1 = tf.matmul(M1_tile, pts_1_tile) # Column 1
A2 = tf.matmul(M2_tile, pts_1_tile) # Column 2
A3 = M3_tile # Column 3
A4 = tf.matmul(M4_tile, pts_1_tile) # Column 4
A5 = tf.matmul(M5_tile, pts_1_tile) # Column 5
A6 = M6_tile # Column 6
A7 = tf.matmul(M71_tile, pred_pts_2_tile) * tf.matmul(M72_tile, pts_1_tile)# Column 7
A8 = tf.matmul(M71_tile, pred_pts_2_tile) * tf.matmul(M8_tile, pts_1_tile)# Column 8
A_mat = tf.transpose(tf.stack([tf.reshape(A1,[-1,8]),tf.reshape(A2,[-1,8]),\
tf.reshape(A3,[-1,8]),tf.reshape(A4,[-1,8]),\
tf.reshape(A5,[-1,8]),tf.reshape(A6,[-1,8]),\
tf.reshape(A7,[-1,8]),tf.reshape(A8,[-1,8])],axis=1), perm=[0,2,1]) # BATCH_SIZE x 8 (A_i) x 8
print('--Shape of A_mat:', A_mat.get_shape().as_list())
# Form b matrix
b_mat = tf.matmul(Mb_tile, pred_pts_2_tile)
print('--shape of b:', b_mat.get_shape().as_list())
# Solve the Ax = b
H_8el = tf.matrix_solve(A_mat , b_mat) # BATCH_SIZE x 8.
print('--shape of H_8el', H_8el)
# Add ones to the last cols to reconstruct H for computing reprojection error
h_ones = tf.ones([batch_size, 1, 1])
H_9el = tf.concat([H_8el,h_ones],1)
H_flat = tf.reshape(H_9el, [-1,9])
self.H_mat = tf.reshape(H_flat,[-1,3,3]) # BATCH_SIZE x 3 x 3
Spatial Transformation
空间变换层将Tensor DLT得到的单应性矩阵���应用到输入的待对齐图像,得到warp后的结果:
def transform(self):
# Transform H_mat since we scale image indices in transformer
H_mat = tf.matmul(tf.matmul(self.M_tile_inv, self.H_mat), self.M_tile)
# Transform image 1 (large image) to image 2
out_size = (self.params.img_h, self.params.img_w)
warped_images, _ = transformer(self.I, H_mat, out_size)
# TODO: warp image 2 to image 1
# Extract the warped patch from warped_images by flatting the whole batch before using indices
# Note that input I is 3 channels so we reduce to gray
warped_gray_images = tf.reduce_mean(warped_images, 3)
warped_images_flat = tf.reshape(warped_gray_images, [-1])
patch_indices_flat = tf.reshape(self.patch_indices, [-1])
pixel_indices = patch_indices_flat + self.batch_indices_tensor
pred_I2_flat = tf.gather(warped_images_flat, pixel_indices)
self.pred_I2 = tf.reshape(pred_I2_flat, [self.params.batch_size, self.params.patch_size, self.params.patch_size, 1])
Loss
损失函数对经过单应性变换warp后的图像 �� 和参考图像 �� 逐像素计算强度值之差,再对所有的像素强度值之差求平均,也就是 �1 Loss:
(2) Content-Aware Unsupervised Deep Homography Estimation
这篇文章提出了一种新架构设计的无监督深度单应性估计方法,本着传统方法中RANSAC思想该网络专门学习了一个离群值mask,只选择可靠的区域进行单应性估计。
网络主要分为特征提取器 �(.) ,mask估计器 �(.) 和单应性矩阵估计器 ℎ(.):
特征提取器
与一些直接利用像素强度值作为特征基于DNN的方法不同,这里的网络自动从输入中学习深度特征,以实现鲁棒特征对齐。为此构建了一个全卷积网络(FCN),它接受大小为H × W ×1的输入,并产生大小为H × W × C的特征图。对于输入 �� 和 �� ,特征提取器共享权重并产生特征图 �� 和 �� 。
mask估计器
在非平面场景中,特别是那些包含移动物体的场景中,不存在能够将两个视图对齐的单一性矩阵。在传统算法中,RANSAC被广泛应用于寻找单应性估计的inlier,从而求解出最近似的场景对齐矩阵。按照类似的思路,本文构建了一个子网络来自动学习inlier的位置,有了mask就可以进一步对提取的特征进行加权,然后将它们送给单应性估计器,得到两个加权特征映射 �� 和 �� 。
单应性矩阵估计器
给定加权特征映射�� 和 ��,将它们连接起来构建大小为H × W × 2C的特征映射,然后将其送到单应性估计器网络中,产生4个2D偏移向量(8个值)。有了这4个偏移向量,通过求解一个线性系统就可以直接得到具有8自由度的单应矩阵 ��� 。
backbone遵循ResNet-34结构。它包含34层卷积,然后是一个全局平均池化层,无论输入特征维数如何,它都会生成固定大小(在我们的例子中为8)的特征向量。
Triplet Loss
在损失函数的设计中,根据学习到的深度特征来计算损失,而不是像之前那样直接比较图像内容。为了实现无监督训练,还为的网络制定了一种新的三重损失。
���足够精确,��′ 应该与和 ��很好地对齐,它们之间的 �1 损失很低。考虑到在真实场景中,一个单应性矩阵通常不能满足两个视图之间的变换,因此通过 �� 和 �� 对Loss进行加权。利用当前迭代次数下模型计算到的单应性矩阵,对 ��和 �� warp得到��′和里��′然后计算Loss:
直接最小化Eq(4)很容易产生不正确解,当特征提取器产生零映射即�� 和 ��等于0时,在这种情况下学习到的特征确实描述了很好对齐的事实,但它不能反映原始图像是不对齐的事实。为此涉及�� 和 ��之间的另一个损失:
除此之外在最终损失函数的设计中,交换了 �� 和 �� 的特征,还产生了另一个单应矩阵 ��� 。根据Eq(4)在warp的�� 和 ��之间添加了强制���和���为逆的约束。因此网络的优化过程写为:
无监督感知学习
如上所述,网络包含一个子网络�(.)来预测一个概率mask,显式地对特征进行加权,这样只有突出显示的特征才能被完全送到单应性估计器中,这些mask实际上充当了特征图的注意图。
其次它们也隐式地涉及到归一化损失Eq(4)中作为加权项工作,通过这样只有那些真正适合对齐的区域才会被考虑进去。对于那些包含低纹理或移动前景的区域,由于它们无法区分或误导对齐,因此在优化Triplet loss期间,它们自然会被移除。这样的内容感知完全是通过无监督学习方案实现的,没有任何GT数据作为监督。
对比传统的特征检测方案,无监督的网络结果对齐效果明显更好:
5. 文献汇总
分类 | 论文名称 | 发表时间 | 代码 |
---|---|---|---|
综述 | Image Registration Techniques A Survey | CVPR 2017 | |
综述 | Image Matching from Handcrafted to Deep Features: A Survey | IJCV 2020 | |
综述 | A Comparative Analysis of SIFT, SURF, KAZE,AKAZE, ORB, and BRISK | ICCV 2018 | |
综述 | Evaluation of Interest Point Detectors and Feature Descriptors | IJCV 2011 | |
综述 | Quantitative Comparison of Feature Matchers Implemented in OpenCV3 | CVWW 2016 | |
特征提取 | A Combined Corner and Edge Detector | AVC 1988 | https://github.com/gokhanozbulak/Harris-Detector |
特征提取 | Features from Accelerated Segment Test | ECCV 2006 | https://github.com/soh534/fast |
特征描述子 | Binary Robust Independent Elementary Features | ECCV 2010 | |
特征检测 | Object Recognition from Local Scale-Invariant Features | ICCV 1999 | https://github.com/OpenGenus/SIFT-Scale-Invariant-Feature-Transform |
特征检测 | Surf: Speeded up robust features | ECCV 2006 | https://github.com/herbertbay/SURF |
特征检测 | ORB: an efficient alternative to SIFT or SURF | ICCV 2011 | https://github.com/AhmedHisham1/ORB-feature-matching |
特征过滤 | Random Sample Consensus: A Paradigm for Model Fitting with Applications to Image Analysis and Automated Cartography | RCV 1987 | |
特征过滤 | GMS: Grid-based Motion Statistics forFast, Ultra-robust Feature Correspondence | CVPR 2017 | https://github.com/JiawangBian/GMS-Feature-Matcher |
特征匹配 | Fast Approximate Nearest Neighbor Search With The Navigating Spreading-out Graph | IJCA 2017 | https://github.com/flann-lib/flann |
相关性对齐 | An Enhanced Correlation-Based Method for Stereo Correspondence | ICCV 2005 | |
相关性对齐 | Parametric Image Alignment Using Enhanced Correlation Coefficient Maximization | TPAMI 2008 | |
深度学习对齐 | Deep Image Homography Estimation | CVPR 2016 | https://github.com/mazenmel/Deep-homography-estimation-Pytorch |
深度学习对齐 | Unsupervised Deep Homography: A Fast and Robust Homography Estimation Model | ICRA 2017 | https://github.com/tynguyen/unsupervisedDeepHomographyRAL2018 |
深度学习对齐 | Content-Aware Unsupervised Deep Homography Estimation | ICCV 2019 | https://github.com/JirongZhang/ |