摘要:
使用基于python的opencv中的sift算法检测图像中的特征点。通过knn匹配,每个关键点两个match,即最近邻与次近邻。 采用SIFT作者提出的比较最近邻距离与次近邻距离的SIFT匹配方式来筛选出最近邻远优于次近邻的匹配作为good matches。最后,根据投影映射关系,使用计算出来的单应性矩阵H进行透视变换,再进行拼接。
准备:
首先,准备好几个库:
1 2 3 4 | import cv2 import matplotlib.pyplot as plt import numpy as np %matplotlib inline |
注:使用的是opencv中的SIFT算法,由于涉及到专利,有些opencv版本没有这个算法。需将opencv调制3.4.2或之前的版本。(听说最近这个专利到期了,但不清楚这个算法是否已经入库。)
1 2 | print(cv2.__version__) 3.4.2 |
不急,我们再看一下图片:
1 2 3 | # 使用cv2读取 img_left = cv2.imread('1.png', 1) img_right = cv2.imread('2.png', 1) |
查看图片
1 2 3 4 5 | plt.subplot(121) plt.imshow(cv2.cvtColor(img_left, cv2.COLOR_BGR2RGB)) plt.subplot(122) plt.imshow(cv2.cvtColor(img_right, cv2.COLOR_BGR2RGB)) plt.show() |
注:cv2.cvtColor(imgA, cv2.COLOR_BGR2RGB)的作用是对cv2读取的图片进行通道变换,cv2读取的通道顺序是BGR,而matplotlib.pyplot对应的是RGB。
1 2 3 4 5 6 | # 下面是没有进行通道变换的显示结果 plt.subplot(121) plt.imshow(img_left) plt.subplot(122) plt.imshow(img_right) plt.show() |
第一步、检测关键点
1 2 3 4 5 6 7 8 9 | def detect(image): # 转化为灰度图 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 创建SIFT生成器 # descriptor是一个对象,这里使用的是SIFT算法 descriptor = cv2.xfeatures2d.SIFT_create() # 检测特征点及其描述子(128维向量) kps, features = descriptor.detectAndCompute(image, None) return (kps,features) |
看下效果:
1 2 3 4 5 6 7 8 9 10 | # 可以看下特征点(若果发现几次运行特征点数目不同,要重新读取图片) def show_points(image): gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) descriptor = cv2.xfeatures2d.SIFT_create() kps, features = descriptor.detectAndCompute(image, None) print(f"特征点数:{len(kps)}") img_left_points = cv2.drawKeypoints(img_left, kps, img_left) plt.figure(figsize=(9,9)) plt.imshow(img_left_points) show_points(img_left) |
特征点数:1793
第二步、匹配所有特征点
使用knnMatch最近邻匹配:取一幅图像中的一个sift关键点,并找出另一幅图像中最近的前两个关键点。在这两个关键点中,如果最近的距离除以次近的距离得到的ratio少于某个阀值T,则接受这对匹配点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | def match_keypoints(kps_left,kps_right,features_left,features_right,ratio,threshold): """ kpsA,kpsB,featureA,featureB: 两张图的特征点坐标及特征向量 threshold: 阀值 """ # 建立暴力匹配器 matcher = cv2.DescriptorMatcher_create("BruteForce") # 使用knn检测,匹配left,right图的特征点 raw_matches = matcher.knnMatch(features_left, features_right, 2) print(len(raw_matches)) matches = [] # 存坐标,为了后面 good = [] # 存对象,为了后面的演示 # 筛选匹配点 for m in raw_matches: # 筛选条件 # print(m[0].distance,m[1].distance) if len(m) == 2 and m[0].distance < m[1].distance * ratio: good.append([m[0]]) matches.append((m[0].queryIdx, m[0].trainIdx)) """ queryIdx:测试图像的特征点描述符的下标==>img_keft trainIdx:样本图像的特征点描述符下标==>img_right distance:代表这怡翠匹配的特征点描述符的欧式距离,数值越小也就说明俩个特征点越相近。 """ # 特征点对数大于4就够用来构建变换矩阵了 kps_left = np.float32([kp.pt for kp in kps_left]) kps_right = np.float32([kp.pt for kp in kps_right]) print(len(matches)) if len(matches) > 4: # 获取匹配点坐标 pts_left = np.float32([kps_left[i] for (i,_) in matches]) pts_right = np.float32([kps_right[i] for (_,i) in matches]) # 计算变换矩阵(采用ransac算法从pts中选择一部分点) H,status = cv2.findHomography(pts_right, pts_left, cv2.RANSAC, threshold) return (matches, H, good) return None |
这里我们可以看一下匹配的点,图中只显示了一部分:
1 2 3 4 5 6 7 8 | img_left = cv2.imread('1.png', 1) img_right = cv2.imread('2.png', 1) kps_left, features_left = detect(img_left) kps_right, features_right = detect(img_right) matches, H, good = match_keypoints(kps_left,kps_right,features_left,features_right,0.5,0.99) img = cv2.drawMatchesKnn(img_left,kps_left,img_right,kps_right,good[:30],None,flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS) plt.figure(figsize=(20,20)) plt.imshow(img) |
1 2 3 4 | print(H) # 单应性矩阵 [[ 3.66077113e-01 -2.46664063e-02 4.23006873e+02] [-2.46890577e-01 8.17757134e-01 6.32946430e+01] [-8.24224575e-04 -7.01709596e-05 1.00000000e+00]] |
第三步、变换,拼接
获取左边图像到右边图像的投影映射关系,透视变换将左图象放在相应的位置,将图像拷贝到特定位置完成拼接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | def drawMatches(img_left, img_right, kps_left, kps_right, matches, H): # 获取图片宽度和高度 h_left, w_left = img_left.shape[:2] h_right, w_right = img_right.shape[:2] """对imgB进行透视变换 由于透视变换会改变图片场景的大小,导致部分图片内容看不到 所以对图片进行扩展:高度取最高的,宽度为两者相加""" image = np.zeros((max(h_left, h_right), w_left+w_right, 3), dtype='uint8') # 初始化 image[0:h_left, 0:w_left] = img_right """利用以获得的单应性矩阵进行变透视换""" image = cv2.warpPerspective(image, H, (image.shape[1], image.shape[0]))#(w,h """将透视变换后的图片与另一张图片进行拼接""" image[0:h_left, 0:w_left] = img_left return image |
老样子,我们还是先看下效果先:
1 2 3 4 | vis = drawMatches(img_left, img_right, kps_left, kps_right, matches, H) plt.xticks([]), plt.yticks([]) plt.imshow(cv2.cvtColor(vis, cv2.COLOR_BGR2RGB)) plt.show() |
结:
除了明暗有点不太协调,其他方便都挺好的。可以写个函数方便调用了。
1 2 3 4 5 6 7 8 9 10 11 12 13 | # 现在该定义一个主函数了 def main(img_left,img_right, size=(20,20)): # 模块一:提取特征 kps_left, features_left = detect(img_left) kps_right, features_right = detect(img_right) # 模块二:特征匹配 matches, H, good = match_keypoints(kps_left,kps_right,features_left,features_right,0.5,0.99) # 模块三:透视变换-拼接 vis = drawMatches(img_left, img_right, kps_left, kps_right, matches, H) # show plt.figure(figsize= size) plt.imshow(cv2.cvtColor(vis, cv2.COLOR_BGR2RGB)) plt.show() |
测试一下:
1 2 3 4 | # 读取图片 img_left = cv2.imread('1.png', 1) img_right = cv2.imread('2.png', 1) main(img_left,img_right) |
1793
141
换个图片瞧瞧:
1 2 3 4 5 6 7 8 9 10 | # 读取图片 img_left = cv2.imread('left.jpg', 1) img_right = cv2.imread('right.jpg', 1) # 查看原图 plt.subplot(121) plt.imshow(cv2.cvtColor(img_left, cv2.COLOR_BGR2RGB)) plt.subplot(122) plt.imshow(cv2.cvtColor(img_right, cv2.COLOR_BGR2RGB)) plt.show() main(img_left,img_right,(22,22)) |
426
118
(还可以用gamma变换调节一下明暗,不过就到这里吧)