要实现两张图片的简单拼接,必须计算homography矩阵(单应性矩阵),矩阵的计算至少需要在图中找到四个相似的点,用这个矩阵把那张图片变换后放到另一张图片相应的位置 ( 就是相当于把两张图片中定好的四个相似的点給重合在一起)。如此,就可以实现简单的全景拼接。当然,因为拼合之后图片会重叠在一起,所以需要重新计算图片重叠部分的像素值,否则结果会很难看。所以总结起来其实就两个步骤:
当然,我们可以手动的寻找相似的点,但是这样比较麻烦。因为相似点越多或者相似点对应的位置越准确,所得的结果就越好,但是人的肉眼所找的位置总是有误差的,而且找出很多的点也不是一件容易的事。所以就有聪明的人设计了自动寻找相似点的算法,这里我们就用了 SIFT 算法,而 OpenCV 也给我们提供 SIFT 算法的接口,所以我们就不需要自己费力去实现了。如下是两张测试图片的原图和找出相似点后的图片。
sift = cv.xfeatures2d_SIFT().create()
# find the keypoints and descriptors with SIFT
# 计算出图像的关键点和sift特征向量,img1gray表示输入的原始图
# 像,可以使三通道或单通道图像
#第二个参数keypoints:特征点向量,向量内每一个元素是一个KeyPoint对象,包含了特征点的各种属性信息
#第三个参数outImage:特征点绘制的画布图像,可以是原图像
#第四个参数color:绘制的特征点的颜色信息,默认绘制的是随机彩色
#第五个参数flags:特征点的绘制模式,其实就是设置特征点的那些信息需要绘制,那些不需要绘制,有以下几种模式可选
kp1, des1 = sift.detectAndCompute(img1gray, None)
kp2, des2 = sift.detectAndCompute(img2gray, None)
原因:sift已经被申请专利了,所以,在opencv3.4.3.16 版本后,这个功能就不能用了。
解决方法是把版本退回到3.4.3以前。
pip uninstall opencv-python
pip install opencv-python==3.4.2.16
pip install opencv-contrib-python==3.4.2.16
其中红色的点是 SIFT 算法找出的相似点,而绿色的线表示的是在所有找出的相似的点中所筛选出的可信度更高的相似的点。因为算法找出的相似点并不一定是百分百正确的。然后就可以根据这些筛选出的相似点计算变换矩阵,当然 OpenCV 也提供了相应的接口方便我们的计算,而具体的代码实现也可以在 OpenCV 的 Python tutorial 中找到 [1]。
计算出变换矩阵后,接下来就是第二步,用计算出的变换矩阵对其中一张图做变换,然后把变换的图片与另一张图片重叠在一起,并重新计算重叠区域新的像素值。对于计算重叠区域的像素值,其实可以有多种方法去实现一个好的融合效果,这里就用最简单粗暴的但效果也不错的方式。直白来说就是实现一个图像的线性渐变,对于重叠的区域,靠近左边的部分,让左边图像内容显示的多一些,靠近右边的部分,让右边图像的内容显示的多一些。用公式表示就是,假设 alpha 表示像素点横坐标到左右重叠区域边界横坐标的距离,新的像素值就为 newpixel = 左图像素值 × (1 - alpha) + 右图像素值 × alpha 。这样就可以实现一个简单的融合效果,如果想实现更复杂或更好的效果,可以去搜索和尝试一下 multi-band 融合,这里就不过多赘述了。最后附上实现的结果和代码,可供参考
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
import gdal
if __name__ == '__main__':
top, bottom, left, right = 100, 100, 0, 500
img1 = cv.imread('C:/Users/Administrator/Desktop/Gis/image22.jpg')
img2 = cv.imread('C:/Users/Administrator/Desktop/Gis/image24.jpg')
# cv.copyMakeBorder功能扩充src的边缘,将图像变大,然后以各种外插方式自动填充图像边界,
# 这个函数实际上调用了函数cv::borderInterpolate,这个函数最重要的功能就是为了处理边界,
# 比如均值滤波或者中值滤波中,使用copyMakeBorder将原图稍微放大,然后我们就可以处理边界的情况了
# top, bottom, left, right分别表示在原图四周扩充边缘的大小
# borderType:扩充边缘的类型,就是外插的类型,OpenCV中给出以下几种方式
# *BORDER_REPLICATE(复制法,也就是复制最边缘像素。) aaaaaa|abcdefgh|hhhhhhh
# *BORDER_REFLECT (轴对称法,也就是以边界为轴,对称。)fedcba|abcdefgh|hgfedcb
# *BORDER_REFLECT_101(轴对称法,也就是以最边缘像素为轴,对称。)hgfedcb|abcdefgh|gfedcba
# *BORDER_WRAP cdefgh|abcdefgh|abcdefg
# *BORDER_CONSTANT(常量法) iiiiii|abcdefgh|iiiiiii with some specified 'i'
srcImg = cv.copyMakeBorder(img1, top, bottom, left, right, cv.BORDER_CONSTANT, value=(0, 0, 0))
testImg = cv.copyMakeBorder(img2, top, bottom, left, right, cv.BORDER_CONSTANT, value=(0, 0, 0))
# BGR转换到灰度空间(opencv默认的彩色图像的颜色空间是BGR)
img1gray = cv.cvtColor(srcImg, cv.COLOR_BGR2GRAY)
img2gray = cv.cvtColor(testImg, cv.COLOR_BGR2GRAY)
# 得到特征提取器的一个实例
sift = cv.xfeatures2d_SIFT().create()
# find the keypoints and descriptors with SIFT
# 计算出图像的关键点和sift特征向量,img1gray表示输入的原始图
# 像,可以使三通道或单通道图像
kp1, des1 = sift.detectAndCompute(img1gray, None)
kp2, des2 = sift.detectAndCompute(img2gray, None)
# FLANN parameters随机kd树,平行搜索。默认trees=4
FLANN_INDEX_KDTREE = 1
# 创建字典
index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
print(index_params)
# 指定递归遍历的次数checks
search_params = dict(checks=50)
print(search_params)
# 最近邻近似匹配,所以当我们需要找到一个相对好的匹配但是不需要最佳匹配的时候往往使用FlannBasedMatcher。
flann = cv.FlannBasedMatcher(index_params, search_params)
# des1:图片,des2:搜索的图片,matches:匹配的结果,K:阈值,越高精度越高,匹配的数量越少。
# 该函数,一组返回的俩个DMatch类型DMatch。那么这个这个DMatch数据结构究竟是什么呢?
# 它包含三个非常重要的数据分别是queryIdx,trainIdx,distance。
# 先说一下这三个分别是什么在演示其用途:
# queryIdx:测试图像的特征点描述符的下标(第几个特征点描述符),同时也是描述符对应特征点的下标。
# trainIdx:样本图像的特征点描述符下标,同时也是描述符对应特征点的下标。
# distance:代表这怡翠匹配的特征点描述符的欧式距离,数值越小也就说明俩个特征点越相近。
# K近邻匹配,在匹配的时候选择K个和特征点最相似的点,如果这K个点之间的区别足够大,
# 则选择最相似的那个点作为匹配点,通常选择K = 2,也就是最近邻匹配。
# 对每个匹配返回两个最近邻的匹配,如果第一匹配和第二匹配距离比率足够大(向量距离足够远),
# 则认为这是一个正确的匹配,比率的阈值通常在2左右。
matches = flann.knnMatch(des1, des2, k=2)
for i, matche in enumerate(matches):
print(matche)
# Need to draw only good matches, so create a mask
matchesMask = [[0, 0] for i in range(len(matches))]
good = []
pts1 = []
pts2 = []
# ratio test as per Lowe's paper
for i, (m, n) in enumerate(matches):
if m.distance < 0.7*n.distance:
good.append(m)
pts2.append(kp2[m.trainIdx].pt)
pts1.append(kp1[m.queryIdx].pt)
matchesMask[i] = [1, 0]
draw_params = dict(matchColor=(0, 255, 0),
singlePointColor=(255, 0, 0),
matchesMask=matchesMask,
flags=0)
img3 = cv.drawMatchesKnn(img1gray, kp1, img2gray, kp2, matches, None, **draw_params)
plt.imshow(img3, ), plt.show()
rows, cols = srcImg.shape[:2]
MIN_MATCH_COUNT = 10
if len(good) > MIN_MATCH_COUNT:
src_pts = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2)
dst_pts = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2)
M, mask = cv.findHomography(src_pts, dst_pts, cv.RANSAC, 5.0)
warpImg = cv.warpPerspective(testImg, np.array(M), (testImg.shape[1], testImg.shape[0]), flags=cv.WARP_INVERSE_MAP)
for col in range(0, cols):
if srcImg[:, col].any() and warpImg[:, col].any():
left = col
break
for col in range(cols-1, 0, -1):
if srcImg[:, col].any() and warpImg[:, col].any():
right = col
break
res = np.zeros([rows, cols, 3], np.uint8)
for row in range(0, rows):
for col in range(0, cols):
if not srcImg[row, col].any():
res[row, col] = warpImg[row, col]
elif not warpImg[row, col].any():
res[row, col] = srcImg[row, col]
else:
srcImgLen = float(abs(col - left))
testImgLen = float(abs(col - right))
alpha = srcImgLen / (srcImgLen + testImgLen)
res[row, col] = np.clip(srcImg[row, col] * (1-alpha) + warpImg[row, col] * alpha, 0, 255)
# opencv is bgr, matplotlib is rgb
res = cv.cvtColor(res, cv.COLOR_BGR2RGB)
# show the result
plt.figure()
plt.imshow(res)
plt.show()
else:
print("Not enough matches are found - {}/{}".format(len(good), MIN_MATCH_COUNT))
matchesMask = None