图像拼接(Image Stitching)是一种利用实景图像组成全景空间的技术,它将多幅图像拼接成一幅大尺度图像或360度全景图,接可以看做是场景重建的一种特殊情况,其中图像仅通过平面单应性进行关联。图像拼接在运动检测和跟踪,增强现实,分辨率增强,视频压缩和图像稳定等机器视觉领域有很大的应用。
图像拼接的输出是两个输入图像的并集。通常用到四个步骤:
特征提取(Feature Extraction):检测输入图像中的特征点。
图像配准(Image Registration):建立了图像之间的几何对应关系,使它们可以在一个共同的参照系中进行变换、比较和分析。
图像变形(Warping):图像变形是指将其中一幅图像的图像重投影,并将图像放置在更大的画布上。
图像融合(Blending):图像融合是通过改变边界附近的图像灰度级,去除这些缝隙,创建混合图像,从而在图像之间实现平滑过渡。混合模式(Blend modes)用于将两层融合到一起。
1、用SIFT提取图像中的特征点,并对每个关键点周围的区域计算特征向量。可以使用比SIFT快的SURF方法,但是我的opencv版本为最新版,不知道是专利的原因还是什么原因用SURF = cv2.xfeatures2D.SURF_create ()实例化的时候会报错,网上说可以退opencv版本,但是我这里没有尝试,就用了sift = cv2.SIFT_create()。
2、在分别提取好了两张图片的关键点和特征向量以后,可以利用它们进行两张图片的匹配。在拼接图片中,可以使用Knn进行匹配,但是使用FLANN快速匹配库更快,图片拼接,需要用到FLANN的单应性匹配。
3、单应性匹配完之后可以获得透视变换H矩阵,用这个的逆矩阵来对第二幅图片进行透视变换,将其转到和第一张图一样的视角,为下一步拼接做准备。
4、透视变化完后就可以直接拼接图片了,将图片通过numpy直接加到透视变化完成的图像的左边,覆盖掉重合的部分,得到拼接图片,但是这样拼接得图片中间会有一条很明显的缝隙,可以通过加权平均法,界线的两侧各取一定的比例来融合缝隙,速度快,但不自然。或者羽化法,或者拉普拉斯金字塔融合,效果最好。在这里用的是加权平均法,可以把第一张图叠在左边,但是对第一张图和它的重叠区做一些加权处理,重叠部分,离左边图近的,左边图的权重就高一些,离右边近的,右边旋转图的权重就高一些,然后两者相加,使得过渡是平滑地,这样看上去效果好一些,速度就比较慢。
#导入库
import cv2
import numpy as np
import sys
from PIL import Image
#图像显示函数
def show(name,img):
cv2.imshow(name, img)
cv2.waitKey(0)
cv2.destroyAllWindows()
#读取输入图片
ima = cv2.imread("you.jpg")
imb = cv2.imread("zuo.jpg")
A = ima.copy()
B = imb.copy()
imageA = cv2.resize(A,(0,0),fx=0.2,fy=0.2)
imageB = cv2.resize(B,(0,0),fx=0.2,fy=0.2)
#检测A、B图片的SIFT关键特征点,并计算特征描述子
def detectAndDescribe(image):
# 建立SIFT生成器
sift = cv2.SIFT_create()
# 检测SIFT特征点,并计算描述子
(kps, features) = sift.detectAndCompute(image, None)
# 将结果转换成NumPy数组
kps = np.float32([kp.pt for kp in kps])
# 返回特征点集,及对应的描述特征
return (kps, features)
#检测A、B图片的SIFT关键特征点,并计算特征描述子
kpsA, featuresA = detectAndDescribe(imageA)
kpsB, featuresB = detectAndDescribe(imageB)
# 建立暴力匹配器
bf = cv2.BFMatcher()
# 使用KNN检测来自A、B图的SIFT特征匹配对,K=2
matches = bf.knnMatch(featuresA, featuresB, 2)
good = []
for m in matches:
# 当最近距离跟次近距离的比值小于ratio值时,保留此匹配对
if len(m) == 2 and m[0].distance < m[1].distance * 0.75:
# 存储两个点在featuresA, featuresB中的索引值
good.append((m[0].trainIdx, m[0].queryIdx))
# 当筛选后的匹配对大于4时,计算视角变换矩阵
if len(good) > 4:
# 获取匹配对的点坐标
ptsA = np.float32([kpsA[i] for (_, i) in good])
ptsB = np.float32([kpsB[i] for (i, _) in good])
# 计算视角变换矩阵
H, status = cv2.findHomography(ptsA, ptsB, cv2.RANSAC,4.0)
# 匹配两张图片的所有特征点,返回匹配结果
M = (matches, H, status)
# 如果返回结果为空,没有匹配成功的特征点,退出程序
if M is None:
print("无匹配结果")
sys.exit()
# 否则,提取匹配结果
# H是3x3视角变换矩阵
(matches, H, status) = M
# 将图片A进行视角变换,result是变换后图片
result = cv2.warpPerspective(imageA, H, (imageA.shape[1] + imageB.shape[1], imageA.shape[0]))
# 将图片B传入result图片最左端
result[0:imageB.shape[0], 0:imageB.shape[1]] = imageB
show('res',result)
print(result.shape)
效果:
可以发现直接拼接虽然可以拼接但是在拼接的地方会有一条很明显的缝隙,不过直接拼接的速度比较快只用了2点多秒。
我们通常使用alpha因子,通常称为alpha通道,它在中心像素处的值为1,在与边界像素线性递减后变为0。当输出拼接图像中至少有两幅重叠图像时,我们将使用如下的alpha值来计算其中一个像素处的颜色:假设两个图像,在输出图像中重叠;每个像素点在图像,其中(R,G,B)是像素的颜色值,我们将在缝合后的输出图像中计算(x, y)的像素值:
代码如下:
import cv2
import numpy as np
from matplotlib import pyplot as plt
import time
def show(name,img):
cv2.imshow(name, img)
cv2.waitKey(0)
cv2.destroyAllWindows()
MIN = 10
FLANN_INDEX_KDTREE = 0
starttime = time.time()
img1 = cv2.imread('zuo.jpg') #query
img2 = cv2.imread('you.jpg') #train
imageA = cv2.resize(img1,(0,0),fx=0.2,fy=0.2)
imageB = cv2.resize(img2,(0,0),fx=0.2,fy=0.2)
surf=cv2.xfeatures2d.SIFT_create()#可以改为SIFT
#sift = cv2.SIFT_create()
kp1,descrip1 = sift.detectAndCompute(imageA,None)
kp2,descrip2 = sift.detectAndCompute(imageB,None)
#创建字典
indexParams = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)
searchParams = dict(checks=50)
flann=cv2.FlannBasedMatcher(indexParams,searchParams)
match=flann.knnMatch(descrip1,descrip2,k=2)
good=[]
#过滤特征点
for i,(m,n) in enumerate(match):
if(m.distance<0.75*n.distance):
good.append(m)
# 当筛选后的匹配对大于10时,计算视角变换矩阵
if len(good) > MIN:
src_pts = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1,1,2)
ano_pts = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1,1,2)
M,mask = cv2.findHomography(src_pts,ano_pts,cv2.RANSAC,5.0)
warpImg = cv2.warpPerspective(imageB, np.linalg.inv(M), (imageA.shape[1]+imageB.shape[1], imageB.shape[0]))
direct=warpImg.copy()
direct[0:imageA.shape[0], 0:imageB.shape[1]] =imageA
simple=time.time()
show('res',warpImg)
rows,cols=imageA.shape[:2]
print(rows)
print(cols)
for col in range(0,cols):
# 开始重叠的最左端
if imageA[:, col].any() and warpImg[:, col].any():
left = col
print(left)
break
for col in range(cols-1, 0, -1):
#重叠的最右一列
if imageA[:, col].any() and warpImg[:, col].any():
right = col
print(right)
break
#加权处理
res = np.zeros([rows, cols, 3], np.uint8)
for row in range(0, rows):
for col in range(0, cols):
if not imageA[row, col].any(): # 如果没有原图,用旋转的填充
res[row, col] = warpImg[row, col]
elif not warpImg[row, col].any():
res[row, col] = imageA[row, col]
else:
srcImgLen = float(abs(col - left))
testImgLen = float(abs(col - right))
alpha = srcImgLen / (srcImgLen + testImgLen)
res[row, col] = np.clip(imageA[row, col] * (1 - alpha) + warpImg[row, col] * alpha, 0, 255)
warpImg[0:imageA.shape[0], 0:imageA.shape[1]]=res
show('res',warpImg)
final=time.time()
print(final-starttime)
效果:
可以发现经过加权处理融合后的图片要比直接拼接效果要好,但是时间用了差不多16秒,而且还是有一条黑缝,目前还没有找到解决的办法,有好方法的友友们可以在评论区留意哟。
除了加权处理的方法外,还可以尝试用羽化和拉普拉斯金字塔等方法来实现图像拼接,这里给出实现的原理,方便以后尝试。
羽化(原文连接):
加载原始图像并找到轮廓。
模糊原始图像并将其保存在不同的变量中。
创建一个空的蒙版并在其上绘制检测到的轮廓。
使用 np.where() 方法从要模糊值的蒙版(轮廓)中选择像素,然后替换它。
拉普拉斯金字塔(原文连接);