在本文中,我们将使用(可以说)最基本的图像描述符之一(颜色直方图)来量化和描述我们的图片 。
我们的数据集中共有25个不同的图像,每个类别有5个。
我们要做的第一件事是索引数据集中的25个图像。索引(index)是通过使用图像描述符从每个图像中提取特征来量化我们的数据集的过程,同时将我们得到的特征存储以供以后使用(例如执行搜索)
图像描述符定义了我们如何量化图像,因此从图像中提取特征被称为描述图像(describing an image)。图像描述符的输出是特征向量,是图像本身的抽象。简而言之,它是用于表示图像的数字列表。
可以使用距离度量来比较两个特征向量。距离度量用于通过检查两个特征向量之间的距离来确定两个图像的“相似”程度。在图像搜索引擎的情况下,我们给脚本提供一个查询图像,并要求它根据图像与查询的相关性对索引中的图像进行排序。
这样想吧。当您访问Google并在搜索框中输入“指环王”时,您希望Google向您返回与Tolkien的图书和电影特许经营相关的网页。类似地,如果我们给图像搜索引擎提供我们的查询图像,我们希望它返回与图像内容相关的图像——因此,我们有时将图像搜索引擎称为学术界中更常见的基于内容的图像检索(CBIR)系统。
我们的图像搜索引擎的目的:给定来自五个不同类别之一的查询图像,在前10个结果中返回类别的相应图像。
接下来让我们建立我们的Image Search Engine
The 4 Steps to Building an Image Search Engine
- 定义描述符:你要使用什么类型的描述符?
- 索引我们的数据集:将描述符应用于数据集中的每个图像,提取一系列特征。
- 定义我们的相似度量:你将如何定义两个图像的“相似”程度? 您可能会使用某种距离指标。常见的选择包括Euclidean,Cityblock(Manhattan),余弦和卡方等等。
- 搜索:要执行搜索,先将描述符应用于我们要查询图像,然后询问距离指标,以便对索引中的图像与查询图像的相似程度进行排名。通过相似性对结果进行排序,然后检查它们。
Step1:The Descriptor——A 3D RGB Color Histogram
我们的图像描述符是RGB颜色空间中的3D颜色直方图,每个红色,绿色和蓝色通道有8个区间(bins)。
解释3D直方图的最佳方法是使用连接AND(conjunctive AND)。该图像描述符将询问给定图像有多少红色像素具有落入bin#1和多少绿色像素落入bin#1的以及有多少蓝色像素落入bin#1。对于每个bins的组合将重复该过程; 但是,它将以计算有效的方式完成。
当计算具有8个bins的3D直方图时,OpenCV将特征向量存储为(8,8,8)数组。我们简单地将它展平并重塑为(512,)。一旦它被展平(flattened),我们就可以轻松地将特征向量进行比较以获得相似性。
rgbHistogram.py
import imutils import cv2 class RGBHistogram: def __init__(self,bins): # store the number of bins the histogram will use self.bins = bins def describe(self,image): # compute a 3D histogram in the RGB colorspace, # then normalize the histogram so that images # with the same content, but either scaled larger # or smaller will have (roughly) the same histogram hist = cv2.calcHist([image],[0,1,2],None,self.bins,[0,256,0,256,0,256]) # normalize with openCV 2.4 if imutils.is_cv2(): hist = cv2.normalize(hist) # otherwise normalize with OpenCV 3+ else: hist = cv2.normalize(hist,hist) # return out 3D histogram as a flattened array return hist.flatten()
如您所见,我已经定义了RGBHistogram类。我倾向于将我的图像描述符定义为类而不是函数。这是因为您很少单独从单个图像中提取特征。您改为从整个图像数据集中提取特征。此外,您希望从所有图像中提取的特征使用相同的参数——在这种情况下,是直方图的bins数目。如果您打算比较它们的相似性,从一个图像中使用32个bins来提取特征,而另外一个图像使用128个bins来提取特征,最后来比较它们的相似性是没有意义的。
这里我定义了RGBHistogram的构造函数。我们需要的唯一参数是直方图中每个通道的bins数。同样,这就是为什么我更喜欢使用类而不是图像描述符的函数——通过在构造函数中放置相关参数,可以确保为每个图像使用相同的参数。
接下来是describe方法,它用于“描述”图像并返回特征向量
使用cv2.calcHist函数,我们提取实际的3D RGB直方图(或实际上是BGR,因为OpenCV将图像存储为NumPy数组,但通道的顺序相反)。我们假设self.bins是三个整数的列表,指定每个通道的bin数。
重要的是我们根据像素数量对直方图进行标准化。如果我们使用图像的原始(整数)像素计数,然后将其缩小50%并再次描述它,我们将为相同的图像提供两个不同的特征向量。在大多数情况下,您希望避免这种情况。我们通过将原始整数像素计数转换为实值百分比来获得尺度不变性。例如,我们不说bin#1中有120个像素,而是说bin#1中有20%的像素。同样,通过使用像素计数的百分比而不是原始的整数像素计数,我们可以确保两个相同的图像(仅在大小上不同)将具有(大致)相同的特征向量。
当计算3D直方图时,直方图将表示为具有(N,N,N)个bins的NumPy数组。为了更容易地计算直方图之间的距离,我们简单地将该直方图展平为具有(N ** 3,)的形状。例如:当我们实例化RGBHistogram时,每个通道将使用8个bin。没有展平(flatten)我们的直方图,形状将是(8,8,8)。但是通过展平它,形状变为(512,)。
现在我们已经定义了图像描述符,接下来可以进行数据集的索引处理。
Step2:Indexing our Dataset
前面我们已经确定我们的图像描述符是3D RGB直方图。下一步是将我们的图像描述符应用于数据集中的每个图像。
这仅仅意味着我们将遍历我们的25个图像数据集,从每个图像中提取3D RGB直方图,将特征存储在字典中,并将字典写入文件。
index.py
from rgbHistogram import RGBHistogram from imutils.paths import list_images import os.path import argparse import pickle import cv2 # construct the argument parser and parse the arguments ap = argparse.ArgumentParser() ap.add_argument('-d',"--dataset",required=True, help="Path to the directory that contains the images to be indexed") ap.add_argument("-i", "--index", required=True, help="Path to where the computed index will be stored") args = vars(ap.parse_args()) # initialize the index dictionary to store our our quantifed # images, with the 'key' of the dictionary being the image # filename and the 'value' our computed features index = {}
我们将使用cPickle将索引转储到磁盘。我们将使用list_images来获取我们要索引的图像的路径。
--dataset参数是我们的图像存储在磁盘上的路径,而--index选项是我们在计算索引后存储索引的路径。
最后,我们将初始化索引为字典类型。字典的key是图像文件名。我们假设所有文件名都是唯一的,事实上,对于这个数据集,它们是唯一的。字典的value将是图像的计算直方图。
# initialize our image descriptor -- a 3D RGB histogram with # 8 bins per channel desc = RGBHistogram([8, 8, 8])
这里我们实例化我们的RGBHistogram。同样,我们将分别为红色,绿色和蓝色通道使用8个bins。
# use list_images to grab the image paths and loop over them for imagePath in list_images(args["dataset"]): # extract our unique image ID(here is our image filename) j, k = os.path.split(imagePath) # load the image, describe it using our RGB histogram # descriptor, and update the index image = cv2.imread(imagePath) features = desc.describe(image) index[k] = features
我们使用list_images来抓取图像路径并开始遍历我们的数据集。我们提取图像的名字作为我们字典的key。因为数据集中的所有文件名都是唯一的,因此文件名本身就足以作为key。然后将图像从磁盘加载,然后我们使用RGBHistogram从图像中提取直方图。然后将直方图存储在index中。
# we are now done indexing our image -- now we can write our # index to disk f = open(args["index"],'wb') f.write(pickle.dumps(index)) f.close() # show how many images we indexed print("[INFO] done... indexed {} images".format(len(index)))
现在已经计算了我们的索引,我们将它写入磁盘,以便我们以后可以使用它进行搜索。
要为图像搜索引擎编制索引,只需在终端中输入以下内容即可
python index.py -d images --index index.cpickle
Step3:The Search
我们的磁盘上有我们的index,接下来准备进行搜索。
我们如何比较两个特征向量以及我们如何确定它们的相似程度?先看代码的实现。
searcher.py
import numpy as np class Searcher: def __init__(self, index): # store our index of images self.index = index def search(self, queryFeatures): # initialize our dictionary of results results = {} # loop over the index for (k,features) in self.index.items(): # compute the chi-squared distance between the features # in our index and our query features -- using the # chi-squared distance which is normally used in the # computer vision field to compare histograms d = self.chi2_distance(features,queryFeatures) # now that we have the distance between the two feature # vectors, we can udpate the results dictionary -- the # key is the current image ID in the index and the # value is the distance we just computed, representing # how 'similar' the image in the index is to our query results[k] = d # sort our results, so that the smaller distances (i.e. the # more relevant images are at the front of the list) results = sorted(([v,k] for (k,v) in results.items())) # return our results return results def chi2_distance(self,histA,histB,eps=1e-10): # conpute the chi-squared distance d = 0.5 * np.sum([((a - b) ** 2) / (a + b + eps) for (a,b) in zip(histA,histB)]) # return the chi-squared distance return d
我们首先定义Searcher类和一个带有单个参数的构造函数——index。假定该index是我们在index步骤中写入文件的index字典。
我们定义一个字典来存储我们的结果。字典的key是图像文件名,value是给定图像与查询图像的相似程度。
这是我们实际执行searching的部分。我们遍历索引中的图像文件名和相应特征。然后我们使用卡方距离(Chi-square distance)来比较我们的颜色直方图。然后将计算的距离存储在结果字典中,指示两个图像彼此有多相似。最后结果按照相关性(卡方距离越小,越相关)进行排序并返回。
最后,我们定义用于比较两个直方图的卡方距离函数。一般来说,大垃圾箱与小垃圾箱之间的差异不那么重要,应该按此加权。这正是卡方距离的作用。我们提供epsilon以避免那些讨厌的“除以零”错误。如果图像的特征向量的卡方距离为零,则认为图像是相同的。距离越大,它们就越不相似。
Step4:Performing a Search
接下来我们从磁盘加载图像并执行搜索:
search.py
from searcher import Searcher import numpy as np import argparse import os import pickle import cv2 # construct the argument parser and parse the arguments ap = argparse.ArgumentParser() ap.add_argument("-d", "--dataset", required = True, help = "Path to the directory that contains the images we just indexed") ap.add_argument("-i", "--index", required = True, help = "Path to where we stored our index") args = vars(ap.parse_args()) # load the index and initialize our searcher index = pickle.loads(open(args["index"],'rb').read()) searcher = Searcher(index)
我们使用cPickle从磁盘加载index并初始化我们的Searcher。
# loop over images in the index -- we will use each one as # a query image for (query,queryFeatures) in index.items(): # perform the search using the current query results = searcher.search(queryFeatures) # load the query image and display it path = os.path.join(args["dataset"],query) queryImage = cv2.imread(path) cv2.imshow("Query",queryImage) print("query: {}".format(query)) # initialize the two montages to display our results -- # we have a total of 25 images in the index, but let's only # display the top 10 results; 5 images per montage, with # images that are 400x166 pixels montageA = np.zeros((166 * 5,400,3),dtype="uint8") montageB = np.zeros((166 * 5, 400, 3), dtype="uint8") # loop over the top ten results for j in range(0,10): # grab the result (we are using row-major order) and # load the result image (score,imageName) = results[j] path = os.path.join(args["dataset"],imageName) result = cv2.imread(path) print("\t{} . {} : {:.3f}".format(j+1,imageName,score)) # check to see if the first montage should be used if j < 5: montageA[j * 166:(j + 1) * 166,:] = result # otherwise , the second montage should be used else: montageB[(j - 5) * 166:((j - 5) + 1) * 166,:] = result # show the results cv2.imshow("Results 1-5",montageA) cv2.imshow("Result 6-10",montageB) cv2.waitKey(0)
我们将把index中的每个图像视为一个query,看看我们得到的结果。通常,查询是外部的而不是数据集的一部分,但在我们开始之前,让我们只执行一些示例搜索。
我们将当前图像视作为query并执行searh方法。
然后加载并显示我们的query图像。
为了显示前10个结果,我决定使用两个montage图像。第一个montage显示结果1-5,第二个montage显示结果6-10。最后显示我们search的results给用户。
执行脚本
python search.py --dataset images --index index.cpickle
最终结果:
query: Mordor-002.png 1. Mordor-002.png : 0.000 2. Mordor-004.png : 0.296 3. Mordor-001.png : 0.532 4. Mordor-003.png : 0.564 5. Mordor-005.png : 0.711 6. Goblin-002.png : 0.825 7. Rivendell-002.png : 0.838 8. Rivendell-004.png : 0.980 9. Goblin-001.png : 0.994 10. Rivendell-005.png : 0.996
Bonus: External Queries
截至目前,我只向您展示了如何使用索引中已有的图像执行搜索。 但显然,这不是所有图像搜索引擎的工作方式。 Google允许您上传自己的图片。我们为什么不能? 让我们看看我们如何使用尚未编入索引的图像执行搜索:
search_external.py
from rgbhistogram import RGBHistogram from searcher import Searcher import numpy as np import argparse import os import pickle import cv2 # construct the argument parser and parse the arguments ap = argparse.ArgumentParser() ap.add_argument("-d", "--dataset", required = True, help = "Path to the directory that contains the images we just indexed") ap.add_argument("-i", "--index", required = True, help = "Path to where we stored our index") ap.add_argument("-q", "--query", required = True, help = "Path to query image") args = vars(ap.parse_args()) # load the query image and show it queryImage = cv2.imread(args["query"]) cv2.imshow("Query",queryImage) print("query: {}".format(args["query"])) # describe the query in the same way that we did in # index.py -- a 3D RGB histogram with 8 bins per channel desc = RGBHistogram([8,8,8]) queryFeatures = desc.describe(queryImage) # load the index perform the search index = pickle.loads(open(args["index"],"rb").read()) searcher = Searcher(index) results = searcher.search(queryFeatures) # initialize the two montages to display our results -- # we have a total of 25 images in the index, but let's only # display the top 10 results; 5 images per montage, with # images that are 400x166 pixels montageA = np.zeros((166 * 5, 400, 3), dtype = "uint8") montageB = np.zeros((166 * 5, 400, 3), dtype = "uint8") # loop over the top ten results for j in range(0,10): # grab the result (we are using row-major order) and # load the result image (score,imageName) = results[j] path = os.path.join(args["dataset"],imageName) result = cv2.imread(path) print("\t{} . {} : {:.3f}".format(j+1,imageName,score)) # check to see if the first montage should be used if j < 5: montageA[j * 166:(j + 1) * 166, :] = result # otherwise, the second montage should be used else: montageB[(j - 5) * 166:((j - 5) + 1) * 166, :] = result # show the results cv2.imshow("Results 1-5", montageA) cv2.imshow("Results 6-10", montageB) cv2.waitKey(0)
--query是我们将要query的图像的路径。然后载入我们的query image并显示出来。
使用与索引步骤中完全相同的bin数来实例化我们的RGBHistogram。然后,我们从查询图像中提取特征。
使用cPickle将我们的索引加载到磁盘上并执行搜索。最后显示我们的结果。
在query文件夹中有我们索引中没有的两张图片。这两个图像将是我们的查询。
执行我们的脚本:
python search_external.py --dataset images --index index.cpickle --query queries\rivendell-query.png
结果:
query: queries\rivendell-query.png 1 . Rivendell-002.png : 0.195 2 . Rivendell-004.png : 0.449 3 . Rivendell-001.png : 0.643 4 . Rivendell-005.png : 0.757 5 . Rivendell-003.png : 0.769 6 . Mordor-001.png : 0.809 7 . Mordor-003.png : 0.858 8 . Goblin-002.png : 0.875 9 . Mordor-005.png : 0.894 10 . Mordor-004.png : 0.909
觉得有用可以关注加收藏转发,完整代码和图集请私信我。