1. 全景图像拼接
1.1 基本介绍
全景图像拼接简单的说就是将多张存在重叠部分图片拼成一幅无缝的全景图或高分辨率图像。也就是将两幅存在一定重合部分的图像,通过特征匹配算法(如SIFT)进行特征点匹配从而得到对应点,将两张图像得到的对应点重合并保留两张图片的未重合部分,便可以得到两张图像拼接的结果,以此类推可以得到多张图像拼接而成的全景图。
1.2 图像拼接整体流程
- 根据给定图像/集,实现特征匹配
- 通过匹配特征计算图像之间的变换结构
- 利用图像变换结构,实现图像映射
- 针对叠加后的图像,采用APAP之类的算法,对齐特征点
- 通过图割方法,自动选取拼接缝
- 根据multi-band blending策略实现融合
1.3 基本原理
1.3.1 特征匹配算法
在之前的博文SIFT算法-图像特征匹配、地理标记图像匹配中已经对SIFT算法已及Harris角点检测有较详细的介绍,这里不再赘述。
1.3.1 RANSAC算法
RANSAC算法即RANdom SAmple Consensus,随机抽样一致算法,采用迭代的方式从一组包含离群的被观测数据中估算出数学模型的参数。
RANSAC算法假设数据中包含正确数据和异常数据(或称为噪声)。正确数据记为内点(inliers),异常数据记为外点(outliers)。同时RANSAC也假设,给定一组正确的数据,存在可以计算出符合这些数据的模型参数的方法。
在利用SIFT算法进行特征点匹配时,我们会发现常常存在特征点匹配错误的现象,这些匹配错误的点将会对图像拼接的效果产生很大的影响,所以我们需要利用一定的方法剔除匹配错误的特征点,我们常会用到RANSAC算法来筛选SIFT匹配的特征点以减少误差,这个算法现在在图像配准以及拼接上得到了广泛的应用。
1.3.1.1 RANSAC算法基本思想
- 随机选择两个点
- 根据随机选取的两个点构造方程y=ax+b
- 将所有的数据点套到这个模型中计算误差。
- 给定阈值,计算inliers数量
- 不断重复上述过程,直到达到一定迭代次数后,选择inliers数量最多的直线方程,作为问题的解。
1.3.1.2 RANSAC 求解单应矩阵
· RANSAC loop:
- 随机选择四对匹配特征
- 根据DLT计算单应矩阵 H H H(唯一解)
- 对所有匹配点,计算映射误差ε = ∥ p i p_i pi ′ , H H H p i p_i pi∥
- 根据误差阈值,确定inliers(例如3-5像素)
- 针对最大inliers集合,重新计算单应矩阵 H H H
1.3.2 APAP算法
在图像拼接融合的过程中,受客观因素的影响,拼接融合后的图像可能会存在上图中的“鬼影现象”以及图像间过度不连续等问题。2013年,Julio Zaragoza等人发表了一种新的图像配准算法APAP(As-Projective-As-Possible Image Stitching with Moving DLT)解决鬼影现象可以采用APAP算法。
1.3.2.1算法流程:
1.提取两张图片的SIFT特征点
2.对两张图片的特征点进行匹配
3.匹配后,使用RANSAC算法进行特征点对的筛选,排除错误点。筛选后的特征点基本能够一一对应。
4.使用DLT算法,将剩下的特征点对进行透视变换矩阵的估计。
5.因为得到的透视变换矩阵是基于全局特征点对进行的,即一个刚性的单应性矩阵完成配准。为提高配准的精度,APAP将图像切割成无数多个小方块,对每个小方块进行单应性矩阵变换。
APAP虽然能够较好地完成配准,但非常依赖于特征点对。若图像高频信息较少,特征点对过少,配准将完全失效,并且对大尺度的图像进行配准,其效果也不是很好,一切都决定于特征点对的数量。
1.3.3 寻找最佳拼接缝(Seam Finding)
若在两张图像的重叠区域选择一条最佳拼接缝,拼接缝左边的图像为第一张图像的内容,拼接缝右边的图像为第二张图像的内容,并且拼接缝上两幅图像的差异应该尽可能小,这样不仅可以有效的解决鬼影问题且拼接的效果会更好。
1.3.3.1 使用最大流最小割算法寻找拼接缝
如何找到最佳的拼接缝,使两张图片完美的重合在一起便是我们要解决的问题。最大流最小割作为解决图论问题的经典算法,可以应用于寻找最佳拼接缝。
伪代码如下:
1.3.4 根据multi-band bleing策略实现图像融合
图像拼接完成后会发现在拼接的交界处有明显的衔接痕迹,可能由于图像光线不同的缘故使两张图像的交界处过度的不好,所以我们需要特定的处理解决这种不自然。multi-band bleing策略采用Laplacian(拉普拉斯)金字塔,通过对相邻两层的高斯金字塔进行差分,将原图分解成不同尺度的子图,对每一个之图进行加权平均,得到每一层的融合结果,最后进行金字塔的反向重建,得到最终融合效果过程,融合之后可以得到较好的拼接效果。
2. 具体实现
2.1 实验代码
# -*- coding=utf-8 -*-
# name: nan chen
# date: 2021/4/17 10:53
# ch3_panorama_test.py
from pylab import *
from numpy import *
from PIL import Image
# If you have PCV installed, these imports should work
from PCV.geometry import homography, warp
from PCV.localdescriptors import sift
"""
This is the panorama example from section 3.3.
"""
# set paths to data folder
featname = [r'D:\project3image\lib00' + str(i + 1) + '.sift' for i in range(5)]
imname = [r'D:\project3image\lib00' + str(i + 1) + '.jpg' for i in range(5)]
# extract features and match
l = {}
d = {}
for i in range(5):
sift.process_image(imname[i], featname[i])
l[i], d[i] = sift.read_features_from_file(featname[i])
matches = {}
for i in range(4):
matches[i] = sift.match(d[i + 1], d[i])
# visualize the matches (Figure 3-11 in the book)
for i in range(4):
im1 = array(Image.open(imname[i]))
im2 = array(Image.open(imname[i + 1]))
figure()
sift.plot_matches(im2, im1, l[i + 1], l[i], matches[i], show_below=True)
# function to convert the matches to hom. points
def convert_points(j):
ndx = matches[j].nonzero()[0]
fp = homography.make_homog(l[j + 1][ndx, :2].T)
ndx2 = [int(matches[j][i]) for i in ndx]
tp = homography.make_homog(l[j][ndx2, :2].T)
# switch x and y - TODO this should move elsewhere
fp = vstack([fp[1], fp[0], fp[2]])
tp = vstack([tp[1], tp[0], tp[2]])
return fp, tp
# estimate the homographies
model = homography.RansacModel()
fp, tp = convert_points(1)
H_12 = homography.H_from_ransac(fp, tp, model)[0] # im 1 to 2
fp, tp = convert_points(0)
H_01 = homography.H_from_ransac(fp, tp, model)[0] # im 0 to 1
tp, fp = convert_points(2) # NB: reverse order
H_32 = homography.H_from_ransac(fp, tp, model)[0] # im 3 to 2
tp, fp = convert_points(3) # NB: reverse order
H_43 = homography.H_from_ransac(fp, tp, model)[0] # im 4 to 3
# warp the images
delta = 2000 # for padding and translation
im1 = array(Image.open(imname[1]), "uint8")
im2 = array(Image.open(imname[2]), "uint8")
im_12 = warp.panorama(H_12, im1, im2, delta, delta)
im1 = array(Image.open(imname[0]), "f")
im_02 = warp.panorama(dot(H_12, H_01), im1, im_12, delta, delta)
im1 = array(Image.open(imname[3]), "f")
im_32 = warp.panorama(H_32, im1, im_02, delta, delta)
im1 = array(Image.open(imname[4]), "f")
im_42 = warp.panorama(dot(H_32, H_43), im1, im_32, delta, 2 * delta)
figure()
imshow(array(im_42, "uint8"))
axis('off')
savefig("example5.png", dpi=300)
show()
2.2 实验结果及分析
2.2.1 单平面(景深落差小)
- 5张图像拼接
数据集:
实验结果:
结果分析:
- 第一组照片为嘉庚图书馆内部宣传栏的拼接,从整体来看效果还可以,宣传栏的边框几乎保持水平状态,放大之后可以看到中间有一条明显的拼接缝,并且有些许的错位,没有发生"鬼影现象",总的来说有不错的效果。
- 第二组位于中山纪念馆前拍摄的图像,图像的中间及左边效果都蛮不错,但是右边的楼可以明显的看出倾斜,该算法是以中山纪念馆的图像为中心,应该是在拼接过程中最右的图像进行了仿射变换,导致了楼的扭曲。
2.2.2 多平面(景深落差大)
- 5张图像拼接
数据集:
实验结果:
结果分析:
- 可以看出上面的上面三组照片的拼接大致看效果是还可以的,但是光影效果的变换导致天空颜色的不同让三组照片都可以明显的看出拼接的痕迹。
- 第一组湖边拍摄的照片,放大来看,建筑的纹理有些许变形,且位于图像右边的建筑有些许的倾斜现象并且湖边的原本平滑的长石椅可以看出有些许拼接错位,和上述嘉庚图书馆前的地面相似,拼接图象时一些平行线的对齐是图像拼接过程中值得注意的一个问题,可以看出在景深落差大且图像较为复杂的图像拼接中该算法效果没有那么好。
- 第二组照片则可以很明显的看出近处的两个柱子,有一个柱子有明显的变形,数据集中视角的变化导致柱子的宽度也有明显的差别且柱子的每一处特征点都较为相似,这可能导致叠加后的图像在对齐特征点时发生偏差,可以看出单应性矩阵变换在多平面景深变化大的场景下进行拼接会发生一定的扭曲。
- 第三组为五张拍摄于集美大学嘉庚图书馆外部的图像拼接后的效果,由于光影的影响可以明显的看到拼接的痕迹,可以明显的看到原本平行的地板拼接错位。嘉庚图书馆楼的拼接效果还蛮不错,但是近处地面的效果就不是很好。拍摄这组照片的时候我是固定嘉庚图书馆为中心点,然后分别向左右偏转一定的角度进行拍摄,角度的偏转可能导致图像在特征点匹配的时候对远处的楼有更好的匹配效果,而近处的地面不能很好的匹配,导致地板的错位。
3. 实验小结
- 对于景深落差小的平面,该算法可以得到不错的效果,有些许的错位现象,且对于平行线的拼接存在明显的错位。
- 对于景深变化较大的多平面场景,该算法能在部分区域较好的完成图像的拼接,但是对于其余的部分区域,可能有较为明显的扭曲现象。在选取中间的图像为中心点进行拼接时,位于两侧的建筑物可能发生倾斜现象。
- 在使用上面的代码进行图像拼接时,一定要保证图像的从右往左的顺序,若有一张图像的顺序出现错误,则图像的拼接就会发生很大的错乱QAQ。
- 在图像拼接的过程中,若直接使用原图,则需要很长很长的时间(也可能是机器太垃圾了),为了节省一定的时间,最好还是将图像压缩后再进行拼接。
- 代码中的delta参数为拍摄图像时你相对平移的距离的变量,当拍摄的图像为近景时候,这个参数尽量该小,远景相反。dpi是图像精细度的变量,可以通过修改来改变图像的分辨率,数值越大表示图像越精细,即分辨率越高。
3. 遇到的问题
1.出现 ValueError
ValueError: all the input arrays must have same number of dimensions
这是由于图像的大小不同产生的错误,把所有要拼接的图像大小调为相同后,错误解决。
2.拼接效果糟糕
一开始进行实验的时候几组图像得到的结果都十分的糟糕,原本以为是拍摄的图片不行,后来才发现图像的顺序是要从右往左,而我一直是从左往右的顺序,导致拼接效果糟糕。