新建一个coverdescriptor.py
import numpy as np import cv2 class CoverDescriptor: def __init__(self,useSIFT = False): self.useSIFT = useSIFT
我们首先定义了我们的CoverDescriptor类,该类封装了在图像中查找关键点的方法,然后使用局部不变描述符描述每个关键点周围的区域。
定义init构造函数,需要一个可选参数:useSIFT,一个布尔值,指示是否应使用SIFT关键点检测器和描述符。
keypoints,features,and opencv 3
现在,在我们深入之前,让我们简要讨论一下OpenCV库组织的一个重要变化。
在v3.0版本中,OpenCV已将SIFT,SURF,FREAK以及其他关键点检测器和本地不变描述符实现移至可选的opencv_contrib包中。 这一举措是为了巩固(1)算法的实验性实现,以及(2)OpenCV将“非自由”(即专利)算法(包括许多流行的关键点检测器和局部不变描述符)称为100%可选模块, OpenCV不需要安装和运行。简而言之,如果您曾经使用过OpenCV 2.4.X中的cv2.FeatureDetectorcreate或cv2.DescriptorExtractor创建函数,它们就不再是OpenCV的一部分。 您仍然可以访问免费的非专利方法,例如ORB和BRISK,但如果您需要SIFT和SURF,则必须在编译和安装时明确启用它们。有关OpenCV的这一更改及其对关键点检测器,本地不变描述符以及哪些Python版本可以访问哪些功能的影响的更多信息,请参考:https://www.pyimagesearch.com/2015/07/16/where-did-sift-and-surf-go-in-opencv-3/
由于OpenCV不再随自动启用SIFT模块一起提供,我们现在为名为useSIFT的init方法提供一个布尔值,默认值为False,表示只有在程序员明确要求的情况下才能使用SIFT。
接下来让我们继续:
def describe(self,image): # initialize the BRISK detector and feature extractor(the # standard openCV 3 install includes BRISK by default) descriptor = cv2.BRISK_create() # check if SIFT should be utilized to detect and extract # features (this this will cause an error if you are using # OpenCV 3.0+ and do not have the `opencv_contrib` module # installed and use the `xfeatures2d` package) if self.useSIFT: descriptor = cv2.xfeatures2d.SIFT_create() # detect keypoints in the image, describing the region # surrounding each keypoint, then convert the keypoints # to a NumPy array (kps,descs) = descriptor.detectAndCompute(image,None) kps = np.float32([kp.pt for kp in kps]) # return a tuple of keypoints and descriptor return (kps,descs)
为了从图像中提取关键点和描述符,我们定义了describe方法,该方法接收单个参数 ——要从中提取关键点和描述符的图像。
接着,我们使用BRISK初始化我们的描述符方法。如果我们设置useSIFT=True,那么我们就使用SIFT重新初始化我们的描述符了。
现在我们已经初始化了我们的描述符,接着我们调用detectAndCompute方法。正如这个名字所暗示的,这个方法既检测关键点(即图像的“interesting”区域),然后描述和量化每个关键点周围的区域。因此,关键点检测是“检测”阶段,而区域的实际描述是“计算”阶段。
关键点列表包含多个由OpenCV定义的keyPoint对象。这些对象包含关键点的位置(x,y),关键点的大小和旋转角度以及其他属性等信息。
对于我们现在这个应用程序,我们只需要包含在pt属性中的关键点的(x,y)坐标。
我们获取关键点的(x,y)坐标,丢弃其他属性,并将这些点存储为NumPy数组。
最后,将关键点和相应描述符以元组形式返回给调用函数。
目前,我们可以从书籍的封面中提取关键点和描述符。但是如何比较它们呢?
让我们新建一个covermatcher.py文件
import numpy as np import cv2 class CoverMatcher: def __init__(self,descriptor,coverPaths,ratio=0.7, minMatches=40,useHamming=True): # store the descriptor, book cover paths, ratio and minimum # number of matches for the homography calculation, then # initialize the distance metric to be used when computing # the distance between features self.descriptor = descriptor self.coverPaths = coverPaths self.ratio = ratio self.minMatches = minMatches self.distanceMethod = "BruteForce" # if the Hamming distance should be used, then update the # distance method if useHamming: self.distanceMethod += "-Hamming"
我们首先定义了CoverMatcher类以及构造函数。构造函数接收两个必须参数和三个可选参数。两个必须参数是我们的描述符,假设它是上面定义的CoverDescriptor的实例,以及封面图片路径存储的路径。
三个可选参数解析如下:
- ratio:Lowe建议的最近邻距离的比率,以减少需要计算单应性的关键点的数量
- minMatches:要计算单应性所需的最小匹配数。
- useHamming:一个布尔值,指示是否应使用汉明或欧几里德距离来比较特征向量。
前两个参数,ratio和minMatches,我们将在match函数中详细讨论。第三个参数useHamming我们将现在进行探讨。
值得注意的是,SIFT和SURF产生real-valued特征向量,而ORB,BRISK以及AKAZE则产生binary特征向量。在比较real-valued描述符的时候,比如,SIFT或者SURF,我们希望使用欧氏距离(Euclidean distance)。然而,如果我们使用BRISK特征(产生binary feature),我们应该使用Hamming距离。你所选择的特征向量描述符(SIFT vs BISK)将会影响你的distance method。由于我们默认使用BRISK features,因此我们将使用Hamming method。
接下来,我们定义search方法,看看关键点和描述符将如何匹配:
def search(self,queryKps,queryDescs): # initialize the dictionary of results results = {} # loop over the book cover images for coverPath in self.coverPaths: # load the query image,convert it to grayscale,and # extract keypoints and descriptors cover = cv2.imread(coverPath) gray = cv2.cvtColor(cover,cv2.COLOR_BGR2GRAY) (kps,descs) = self.descriptor.describe(gray) # determine the number of matched , inlier keypoints, # then update the results score = self.match(queryKps,queryDescs,kps,descs) results[coverPath] = score # if matches were found,sort them if len(results) > 0: results = sorted([(v,k) for (v,k) in results.items() if v > 0],reverse = True) # return the results return results
我们首先定义了我们的search方法,该方法需要两个参数——从查询图像(query image)中提取的关键点和描述符集。此方法的目标是从查询图像中获取关键点和描述符,然后与关键点数据库进行匹配 数据库中具有最佳“匹配”的条目将被选为书籍封面的标识。
为了存储我们的匹配准确度结果,我们定义了一个results字典。字典的key是覆盖唯一的书籍封面文件名,balue将是关键点(keypoints)的匹配百分比。
然后,我们开始循环遍历列表封面路径。书籍封面从磁盘加载,接着被转换为灰度,然后使用CoverDescriptor从中提取关键点和描述符。
然后使用match方法(下面定义)确定匹配关键点的数量,并更新results字典
接着,我们做一个快速检查,以确保至少存在一些结果。然后结果按降序排序,书籍封面和更多关键点匹配位于列表顶部。
然后,排序的结果将返回给调用者。
接下来让我们定义match方法:
def match(self,kpsA,featuresA,kpsB,featuresB): # compute the raw matches and initialize the list of actual # matches matcher = cv2.DescriptorMatcher_create(self.distanceMethod) rawMatches = matcher.knnMatch(featuresB,featuresA,2) matches = [] # loop over the raw matches for m in rawMatches: # ensure the distance is within a certain ratio of each # other if len(m) == 2 and m[0].distance < m[1].distance * self.ratio: matches.append((m[0].trainIdx,m[1].queryIdx)) # check to see if there are enough matches to process if len(matches) > self.minMatches: # construct the two sets of points ptsA = np.float32([kpsA[i] for (i,_) in matches]) ptsB = np.float32([kpsB[j] for (_,j) in matches]) # conpute the homography between the two sets of points # and compute the ratio of matched points (_,status) = cv2.findHomography(ptsA,ptsB,cv2.RANSAC,4.0) # return the ratio of the number of matched keypoints # to the total number of keypoints return float(status.sum()) / status.size # no matches were found return -1.0
我们定义了我们的match方法。这个方法有四个参数,详述如下:
- kpsA:与要匹配的第一个图像关联的关键点列表。
- featuresA:与要匹配的第一图像相关联的特征向量的列表
- kpsB:与要匹配的第二个图像关联的关键点列表
- featuresB:与要匹配的第二图像相关联的特征向量的列表。
然后我们使用cv2.DescriptorMatcher_create这个函数定义了我们的匹配器(matcher)。这个值将是BruteForce或BruteForce-Hamming,表明我们将使用Euclidean或Hamming距离将特征A中的每个描述符与特征B中的每个描述符进行比较。将具有最小距离的特征向量作为“匹配”。
我们使用matcher的knnMatch方法进行我们的匹配。函数的“kNN”部分代表“k-最近邻”,其中“最近邻居”由特征向量之间的最小欧几里德距离定义。具有最小欧几里德距离的两个特征向量被认为是“邻居(Neighbors)”。特征A和特征B都被传递给`knnMatch1函数,第三个参数为2,表示我们想要为每个特征向量找到两个最近邻。
knnMatch方法的输出结果赋值给rawMatches变量。但是着并不是实际的mapped keypoints。我们还要采取一些步骤。
首先初始化实际匹配列表。接着,我们循环我们的rawMatches。
然后确保下面两个情况成立(make a check to ensure two cases hold)。第一个是首先确保确实有two matches。第二个是应用David Lowe ratio进行测试,确保the first match的距离小于the second match 乘以 ratio的距离。
假设比率测试成立,则使用第一个关键点的索引和第二个关键点的索引的元组更新匹配列表(matches list)。
然后我们做了第二个重要的check。我们确保匹配数量至少是最小匹配数(minimum matches)。如果没有足够的匹配,则不值得计算单应性(homography),因为两个图像(可能)不会包含相同的书籍封面。
同样,假设上面测试成立,我们接着定义了两个列表ptsA和ptsB,以存储每组匹配关键点的(x,y)坐标。
最后,我们可以计算单应性,这是两个关键点平面(具有相同的投影中心)之间的映射。
实际上,该算法将采用wine吧的匹配和确定哪些关键点确实是“匹配”以及哪些是误报。
为实现这一目标,我们使用cv2.findHomography函数和RANSAC算法,它代表随机样本共识
RANSAC从我们的match列表中随机抽样。然后,RANSAC尝试将这些样本匹配在一起并验证关键点是否为内点(inliers)的假设.RANSAC继续这样做,直到足够大的匹配集被认为是内点。接着,RANSAC采用一系列内部函数并寻找更多匹配。
重要的是RANSAC算法是迭代的。它继续这个过程,直到达到停止标准。
RANSAC算法由cv2.findHomography函数实现,该函数接收四个参数。前面两个是ptsA和ptsB(潜在匹配的(x,y)坐标)。
第三个参数是单应性方法。我们传递cv2.RANSAC表示我们想使用RANSAC算法。当然,我们也可以使用cv2.LMEDS方法,这是Least-Median robust方法。
最后一个参数是RANSAC重新投影阈值,它允许关键点之间存在一些“摆动空间”。假设ptsA和ptsB的(x,y)坐标是以像素为单位测量的,我们传递的值为4.0表示 任何一对关键点被视为内部的容差将被容忍4.0像素的误差。
在cv2.RANSAC和cv2.LMEDS之间进行选择通常取决于问题的范围。虽然cv2.LM-EDS方法的好处是不必明确定义重新投影阈值,但缺点是它通常只能在当至少50%的关键点是内点时起作用。
cv2.findHomograpy函数返回一个包含两个值的元组。第一个是转换矩阵,我们忽略了。
我们对第二个返回值,the status,更感兴趣,状态(the status)变量是布尔值列表,如果匹配的是ptsA和ptsB中的相应关键点,则值为1,如果不匹配,则值为0。
我们计算内部数量与潜在匹配总数的比率,并将其返回给调用者。高分表示两个图像之间更好的“匹配”。
最后,如果最小匹配数测试失败,则返回值-1.0,表示无法计算内部数。
新建一个search.py文件
from __future__ import print_function from preprocess.coverdescriptor import CoverDescriptor from preprocess.covermatcher import CoverMatcher import argparse import glob import csv import cv2 # construct the argument parse and parse the arguments ap = argparse.ArgumentParser() ap.add_argument("-d", "--db", required = True, help = "path to the book database") ap.add_argument("-c", "--covers", required = True, help = "path to the directory that contains our book covers") ap.add_argument("-q", "--query", required = True, help = "path to the query book cover") ap.add_argument("-s", "--sift", type = int, default = 0, help = "whether or not SIFT should be used") args = vars(ap.parse_args()) # initialize the database dictionary of covers db = {} # loop over the database for l in csv.reader(open(args["db"]): # update the database using image ID as the key db[l[0]] = l[1:]
首先,我们导入我们将使用的包。CoverDescriptor将从图像中提取关键点和局部不变描述符,而CoverMatcher将确定两本书籍封面的“匹配程度”。
argparse包将用于解析命令行参数,glob用于获取书籍封面图像的路径,csv用于解析书籍的.csv数据库,cv2用于OpenCV绑定。
从左到右的属性是书籍封面的唯一文件名,书籍的作者和书的标题。
接着我们解析命令行参数。--db指向书籍数据库CSV文件的位置,而--covers是包含书籍封面图像的目录的路径。--query开关 是我们查询图像的路径。最后,可选的--sift 开关 用于指示是否应该使用SIFT方法而不是BRISK算法(默认情况下将使用BRISK)。这里的目标是获取查询图像并在数据库中找到具有最佳匹配的书籍封面。
然后我们构建图书信息数据库。首先,定义db字典。然后,打开书籍数据库CSV文件并循环每一行。数据库字典使用书籍的唯一文件名作为关键字以及书籍和作者的标题作为值进行更新(,得到的结果,比如:'cover001.png': ['Michael Crichton', 'Next'])。
# initialize the default parameters using BRISK is being used useSIFT = args["sift"] > 0 useHamming = args["sift"] == 0 ratio = 0.7 minMatches = 40 # if SIFT is to be used, then update the parameters if useSIFT: minMatches = 50
在上面,我们做的第一件事是确定是否应该使用SIFT算法代替(默认)BRISK算法。如果--sift命令行参数的值>0,我们将使用SIFT;否则,我们将使用默认的BRISK。
现在已经确定了BRISK和SIFT之间的选择,我们还可以确定是否应该使用汉明距离(Hamming distance)。如果我们使用SIFT算法,那么我们将提取实值特征向量——因此 应使用欧几里德距离。但是,如果我们使用BRISK算法,那么我们将计算二进制特征向量,而应该使用汉明距离。
然后初始化Lowe's ratio test的默认值和最小化匹配数。
在我们使用SIFT算法的情况下,我们将添加额外约束,我们应找到更多匹配,以确保更准确的书籍封面识别(设置minMatches=50)。
# initialize the cover descriptor and cover matcher cd = CoverDescriptor(useSIFT = useSIFT) cv = CoverMatcher(cd,glob.glob(args["covers"] + "\\*.png"), ratio=ratio,minMatches=minMatches,useHamming = useHamming) # load the query image, convert it to grayscale,and extract # keypoints and descriptors queryImage = cv2.imread(args["query"]) gray = cv2.cvtColor(queryImage,cv2.COLOR_BGR2GRAY) (queryKps,queryDescs) = cd.describe(gray) # try to match the book cover to a know database of images results = cv.search(queryKps,queryDescs) # show the query cover cv2.imshow("Query", queryImage)
在这里,我们首先实例化我们的CoverDescriptor,然后实例化我们的CoverMatcher,将我们的CoverDescriptor和书籍封面路径列表作为参数传递。
然后加载查询图像并转换为灰度。
接下来,我们从查询图像中提取我们的关键点和局部不变描述符。
为了执行实际匹配,调用CoverMatcher类的搜索方法,其中我们提供查询关键点和查询描述符。返回排序的结果列表,最佳书籍封面匹配位于列表顶部。
最后,我们向用户显示查询图像。
# check to see if no results were found if len(results) == 0: print("I could not find a match for that cover!") cv2.waitKey(0) # otherwise , matches were found else: # loop over the results for (i,(score,coverPath)) in enumerate(results): # grab the book information (author,title) = db[coverPath[coverPath.rfind("\\") + 1:]] print("{} . {:.2f}% : {} -- {}".format(i+1,score * 100, author,title)) # load the result image and show it result = cv2.imread(coverPath) cv2.imshow("Result",result) cv2.waitKey(0)
首先,我们检查以确保找到至少一本书籍封面匹配。如果找不到匹配项,打印出信息让用户知道。
如果找到匹配,则我们开始循环results。
提取书籍的唯一文件名,并从书籍数据库中抓取作者和书名,并显示给用户。
最后,实际的书籍封面本身从磁盘上加载并显示给用户。
执行我们的脚本文件:
python search.py --db books.csv --covers covers --query queries\query01.png
执行结果:
1 . 98.72% : Preston and Child -- Dance of Death
可以看到:
封面成功匹配,超过97%的关键点也匹配。
可以选择收藏,以后说不定会用到哦!!
博客地址(有完整代码):https://0leo0.github.io/2018/case_study_07.html
关注不迷路哦!!