文章目录
第 7 章 图像搜索
引言
本章将展示如何利用文本挖掘技术对基于图像视觉内容进行图像搜索。阐明了提出利用视觉单词的基本思想。
7.1基于内容的图像检索
在大型图像数据库上, CBIR( Content-Based Image Retrieval,基于内容的图像检索)技术用于检索在视觉上具相似性的图像。这样返回的图像可以是颜色相似、纹理相似、图像中的物体或场景相似;总之,基本上可以是这些图像自身共有的任何信息。
对于高层查询,比如寻找相似的物体,将查询图像与数据库中所有的图像进行完全比较(比如用特征匹配)往往是不可行的。在数据库很大的情况下,这样的查询方式会耗费过多时间。在过去的几年里,研究者成功地引入文本挖掘技术到 CBIR 中处理问题,使在数百万图像中搜索具有相似内容的图像成为可能。
从文本挖掘中获取灵感——矢量空间模型
矢量空间模型是一个用于表示和搜索文本文档的模型。我们将看到,它基本上可以应用于任何对象类型,包括图像。该名字来源于用矢量来表示文本文档,这些矢量是由文本词频直方图构成的。换句话说,矢量包含了每个单词出现的次数,而且在其他别的地方包含很多 0 元素。由于其忽略了单词出现的顺序及位置,该模型也被称为 BOW 表示模型。
通过单词计数来构建文档直方图向量 v,从而建立文档索引。通常,在单词计数时会忽略掉一些常用词,如“这”“和”“是”等,这些常用词称为停用词。由于每篇文档长度不同,故除以直方图总和将向量归一化成单位长度。对于直方图向量中的每个元素,一般根据每个单词的重要性来赋予相应的权重。通常,数据集(或语料库)中一个单词的重要性与它在文档中出现的次数成正比,而与它在语料库中出现的次数成反比。
最常用的权重是 tf-idf( term frequency-inverse document frequency,词频 - 逆向文档频率 ),单词 w 在文档 d 中的词频是:
t f w , d = n w ∑ j n j \mathrm{tf}_{w, d}=\frac{n_{w}}{\sum_{j} n_{j}} tfw,d=∑jnjnw
n
w
n_w
nw 是单词 w 在文档 d 中出现的次数。为了归一化,将
n
w
n_w
nw除以整个文档中单词的总数。逆向文档频率为:
idf
w
,
d
=
log
∣
(
D
)
∣
∣
{
d
:
w
∈
d
}
∣
\operatorname{idf}_{w, d}=\log \frac{|(D)|}{|\{d: w \in d\}|}
idfw,d=log∣{d:w∈d}∣∣(D)∣
|D| 是在语料库 D 中文档的数目,分母是语料库中包含单词 w 的文档数 d。将两者相乘可以得到矢量 v 中对应元素的 tf-idf 权重。
上面就是我们目前需要掌握的知识。接下来让我们看看怎样将该模型应用到基于视觉内容的图像索引及搜索中
7.2 视觉单词
为了将文本挖掘技术应用到图像中,我们首先需要建立视觉等效单词;这通常可以采用 2.2 节中介绍的 SIFT 局部描述子做到。它的思想是**将描述子空间量化成一些典型实例,并将图像中的每个描述子指派到其中的某个实例中。**这些典型实例可以通过分析训练图像集确定,并被视为视觉单词。所有这些视觉单词构成的集合称为视觉词汇,有时也称为视觉码本。对于给定的问题、图像类型,或在通常情况下仅需呈现视觉内容,可以创建特定的词汇。
从一个(很大的训练图像)集提取特征描述子,利用一些聚类算法可以构建出视觉单词。聚类算法中最常用的是 K-means,这里也将采用 K-means。视觉单词并不高端,只是在给定特征描述子空间中的一组向量集,在采用 K-means 进行聚类时得到的视觉单词是聚类质心。用视觉单词直方图来表示图像,则该模型便称为 BOW 模型。
我们首先介绍一个示例数据集,并利用它来说明 BOW 概念。该ukbench 数据集有很多子集,每个子集包含四幅图像,这四幅图像具有相同的场景或物体,而且存储的文件名是连续的,即 0 . . . 3 属于同一图像子集, 4 . . . 7 属于另外同一图像子集,以此类推。下图展示了数据集中的一些图像:
创建词汇
为创建视觉单词词汇,首先需要提取特征描述子。这里,我们使用 SIFT 特征描述子。如前面一样, imlist 包含的是图像的文件名。运行下面的代码,可以得到每幅图像提取出的描述子,并将每幅图像的描述子保存在一个文件中:
在这里插入代码片
上图为根据图片提取的对应特征。
Vocabulary 类包含了一个由单词聚类中心 VOC 与每个单词对应的逆向文档频率构成的向量,为了在某些图像集上训练词汇, train() 方法获取包含有 .sift 描后缀的述子文件列表和词汇单词数 k。在 K-means 聚类阶段可以对训练数据下采样,因为如果使用过多特征,会耗费很长时间。
代码最后部分用 pickle 模块保存了整个词汇对象以便后面使用。
7.3 图像索引
在开始搜索之前,我们需要建立图像数据库和图像的视觉单词表示。
建立数据库
在索引图像前,我们需要建立一个数据库。这里,对图像进行索引就是从这些图像中提取描述子,利用词汇将描述子转换成视觉单词,并保存视觉单词及对应图像的单词直方图。从而可以利用图像对数据库进行查询,并返回相似的图像作为搜索结果。
这里,我们使用 SQLite 作为数据库。
在开始之前,我们需要创建表、索引和索引器 Indexer 类,以便将图像数据写入数据库。
我这里直接调用了,所以就不展示书中的代码了。
from PCV.imagesearch import imagesearch
下面的示例代码会遍历整个ukbench数据库中的样本图像,并将其加入我们的索引:
def traversePicture(imagepath):
""" 遍历整个文件数据库中的样本图像,并为其添加索引"""
imlist = get_imlist(imagepath[0])
nbr_images = len(imlist)
featlist = [imlist[i][:-3] + 'sift' for i in range(nbr_images)]
# 载入词汇
with open('first1000/vocabulary.pkl', 'rb') as f:
voc = pickle.load(f)
# 创建索引器
indx = imagesearch.Indexer('test.db',voc)
indx.create_tables()
# 遍历这个图像库,将特征投影到词汇上并添加到索引中
for i in range(nbr_images)[:109]:
locs,descr = sift.read_features_from_file(featlist[i])
indx.add_to_index(imlist[i],descr)
# 提交到数据库
indx.db_commit()
return
下图为找到的索引编码
7.4 在数据库中搜索图像
建立好图像的索引,我们就可以在数据库中搜索相似的图像了。这里,我们用 BoW( Bag-of-Word,词袋模型)来表示整个图像,不过这里介绍的过程是通用的,可以应用于寻找相似的物体、相似的脸、相似的颜色等,它完全取决于图像及所用的描述子。
如果图像数据库很大,逐一比较整个数据库中的所有直方图往往是不可行的。我们需要找到一个大小合理的候选集(这里的“合理”是通过搜索响应时间、所需内存等确定的),单词索引的作用便在于此:我们可以利用单词索引获得候选集,然后只需在候选集上进行逐一比较。
利用索引获取候选图像
我们可以利用建立起来的索引找到包含特定单词的所有图像,这不过是对数据集做一次简单的查询。在Searcher类中加入下面方法:
def candidates_from_word(self, imword):
""" 获取包含 imword 的图像列. """
im_ids = self.con.execute(
"select distinct imid from imwords where wordid=%d" % imword).fetchall()
return [i[0] for i in im_ids]
上面会给出包含特定单词的所有图像 id 号。为了获得包含多个单词的候选图像,例如一个单词直方图中的全部非零元素,我们在每个单词上进行遍历,得到包含该单词的图像,并合并这些列表 。这里,我们仍然需要在合并了的列表中对每一个图像id 出现的次数进行跟踪,因为这可以显示有多少单词与单词直方图中的单词匹配。该过程可以通过下面的 candidates_from_histogram 方法完成:
def candidates_from_histogram(self, imwords):
""" 获取具有相似单词的图像列表 """
# 获取单词 id
words = imwords.nonzero()[0]
# 寻找候选图像
candidates = []
for word in words:
c = self.candidates_from_word(word)
candidates += c
# 获取所有唯一的单词,并按出现次数反向排序
tmp = [(w, candidates.count(w)) for w in set(candidates)]
tmp.sort(key=cmp_to_key(lambda x, y: operator.gt(x[1], y[1])))
tmp.reverse()
# 返回排序后的列表,最匹配的排在最前面
return [w[0] for w in tmp]
该方法从图像单词直方图的非零项创建单词 id 列表,检索每个单词获得候选集并将其合并到candidates 列表中,然后创建一个元组列表每个元组由单词 id 和次数 count构成,其中次数 count 是候选列表中每个单词出现的次数。同时,我们还以元组中的第二个元素为准,用 sort() 方法和一个自定义的比较函数对列表进行排序(考虑到后面的效率)。该自定义比较函数进行用 lambda 函数内联声明,对于单行函数声明,使用 lambda 函数非常方便。最后结果返回一个包含图像 id 的列表,排在列表最前面的是最好的匹配图像。
def findTenIndex(imagepath,voc):
imlist = get_imlist(imagepath[0])
nbr_images = len(imlist)
featlist = [imlist[i][:-3] + 'sift' for i in range(nbr_images)]
src = imagesearch.Searcher('test1.db', voc)
locs, descr = sift.read_features_from_file(featlist[0])
iw = voc.project(descr)
print('ask using a histogram...')
print(src.candidates_from_histogram(iw)[:10])
return
代码问题结果没有出来,后面再调代码。
用一幅图像进行查询
利用一幅图像进行查询时,没有必要进行完全的搜索。为了比较单词直方图, Searcher
类需要从数据库读入图像的单词直方图。将下面的方法添加到 Searcher 类中:
def get_imhistogram(self, imname):
""" 返回一幅图像的单词直方图 . """
im_id = self.con.execute(
"select rowid from imlist where filename='%s'" % imname).fetchone()
s = self.con.execute(
"select histogram from imhistograms where rowid='%d'" % im_id).fetchone()
# 用 pickle 模块从字符串解码 Numpy 数组
return pickle.loads(str(s[0]))
这里,为了在字符串和 NumPy 数组间进行转换,我们再次用到了 pickle 模块,这次使用的是 loads()。
现在,我们可以全部合并到查询方法中:
def query(self, imname):
""" 查找所有与 imname 匹配的图像列表 . """
h = self.get_imhistogram(imname)
candidates = self.candidates_from_histogram(h)
matchscores = []
for imid in candidates:
# 获取名字
cand_name = self.con.execute(
"select filename from imlist where rowid=%d" % imid).fetchone()
cand_h = self.get_imhistogram(cand_name)
cand_dist = sqrt(sum(self.voc.idf * (h - cand_h) ** 2))
matchscores.append((cand_dist, imid))
# 返回排序后的距离及对应数据库 ids 列表
matchscores.sort()
return matchscores
该 query() 方法获取图像的文件名,检索其单词直方图及候选图像列表(如果你的数据集很大,候选集的大小应该限制在某个最大值)。对于每个候选图像,我们用标准的欧式距离比较它和查询图像间的直方图,并返回一个经排序的包含距离及图像id 的元组列表。
我们尝试对前一节的图像进行查询:
imagepath = [ r'C:/hqq/document/python/computervision/ch07/first1000',
]
voc = getDescriptor(imagepath)
# voc,featlist = traversePicture(imagepath)
# findTenIndex(imagepath,voc)
imlist = get_imlist(imagepath[0])
src = imagesearch.Searcher('test1.db', voc)
print('try a query...')
print(src.query(imlist[0])[:10])
return
这会再次打印前 10 个结果,包括候选图像与查询图像间的距离:
程序还是有问题,问题主要是两个,一个是书中的版本太久,另外一个就是他的代码是很乱的,所以我再用的时后自己添加了几个函数,想让代码好看点,但是由于我对代码的理解不够,导致后面再划分函数内容时,全局变量会被变为函数内部的局部变量,所以在另外的函数中调用时,就会出错。单数我又不想一大堆函数放在那里,所以代码问题后面再解决。
7.5使用几何特性对结果排序
让我们简要地看一种用 BoW 模型改进检索结果的常用方法。 BoW 模型的一个主要缺点是在用视觉单词表示图像时不包含图像特征的位置信息,这是为获取速度和可伸缩性而付出的代价。
利用一些考虑到特征几何关系的准则重排搜索到的靠前结果,可以提高准确率。最常用的方法是在查询图像与靠前图像的特征位置间拟合单应性。为了提高效率,可以将特征位置存储在数据库中,并由特征的单词 id 决定它们之间的关联(要注意的是,只有在词汇足够大,使单词 id 包含很多准确匹配时,它才起作用)。然而,这需要大幅重写我们上面的数据库和代码,并复杂化表示形式。为了进行说明,我们仅重载靠前图像的特征,并对它们进行匹配。
# -*- coding: utf-8 -*-
import pickle
from PCV.localdescriptors import sift
from PCV.imagesearch import imagesearch
from PCV.geometry import homography
from PCV.tools.imtools import get_imlist
# 要记得将PCV放置在对应的路径下
class Vocabulary(object):
def __init__(self,name):
self.name = name
self.voc = []
self.idf = []
self.trainingdata = []
self.nbr_words = 0
def train(self, featurefiles, k=100, subsampling=10):
"""用含有k个单词的K-means列出在featurefiles中的特征文件训练出一个词汇。
对训练数据下采样可以加快训练速度"""
nbr_images = len(featurefiles)
# 从文件中读取特征
descr = []
descr.append(sift.read_features_from_file(featurefiles[0])[1])
descriptors = descr[0] # 所有的特征并在一起,以便后面进行K-means聚类
for i in range(1, nbr_images):
descr.append(sift.read_features_from_file(featurefiles[i])[1])
descriptors = vstack((descriptors,descr[i]))
# K-means:最后一个参数决定运行次数
self.voc,distortion = kmeans(descriptors[::subsampling,:],k,1)
self.nbr_words = self.voc.shape[0]
# 遍历所有的训练图像,并投影到词汇上
imwords = zeros((nbr_images, self.nbr_words))
for i in range(nbr_images):
imwords[i] = self.project(descr[i])
nbr_occurences = sum((imwords > 0) * 1, axis=0)
self.idf = log((1.0 * nbr_images) / (1.0 * nbr_occurences + 1))
self.trainingdata = featurefiles
def project(self, descriptors):
""" 将描述子投影到词汇上,以创建单词直方图 """
# 图像单词直方图
imhist = zeros((self.nbr_words))
words, distance = vq(descriptors, self.voc)
for w in words:
imhist[w] += 1
return imhist
# 载入图像列表
imlist = get_imlist('C:/hqq/document/python/computervision/ch07/first1000') # 存放数据集的路径
nbr_images = len(imlist)
# nbr_images = 300
# 载入特征列表
featlist = [imlist[i][:-3]+'sift' for i in range(nbr_images)]
# 载入词汇
with open('C:/hqq/document/python/computervision/ch07/first1000/vocabulary.pkl', 'rb+') as f: # 存放模型的路径
voc = pickle.load(f)
src = imagesearch.Searcher('testImaAdd.db', voc)
# 查询图像索引和查询返回的图像数
q_ind =46 # 查询图片的索引
nbr_results = 20
# 常规查询(按欧式距离对结果排序)
res_reg = [w[1] for w in src.query(imlist[q_ind])[:nbr_results]]
print ('top matches (regular):', res_reg)
# 载入查询图像特征
q_locs, q_descr = sift.read_features_from_file(featlist[q_ind])
fp = homography.make_homog(q_locs[:, :2].T)
# 用单应性进行拟合建立RANSAC模型
model = homography.RansacModel()
rank = {}
# 载入候选图像的特征
for ndx in res_reg[1:]:
locs, descr = sift.read_features_from_file(featlist[ndx])
# 获取匹配数
# get matches执行完后会出现两张图片
matches = sift.match(q_descr, descr)
ind = matches.nonzero()[0]
ind2 = matches[ind]
tp = homography.make_homog(locs[:, :2].T)
# 计算单应性,对内点技术。如果没有足够的匹配书则返回空列表
try:
H, inliers = homography.H_from_ransac(fp[:, ind], tp[:, ind2], model, match_theshold=4)
except:
inliers = []
# 存储内点数
rank[ndx] = len(inliers)
# 将字典排序,以首先获取最内层的内点数
sorted_rank = sorted(rank.items(), key=lambda t: t[1], reverse=True)
res_geom = [res_reg[0]]+[s[0] for s in sorted_rank]
print ('top matches (homography):', res_geom)
# 显示靠前的搜索结果
imagesearch.plot_results(src, res_reg[:8]) # 常规查询
imagesearch.plot_results(src, res_geom[:8]) # 重排后的结果
首先,载入图像列表、特征列表(分别包含图像文件名和 SIFT 特征文件)及词汇。然后,创建一个 Searcher 对象,执行定期查询,并将结果保存在 res_reg 列表中。然后载入 res_reg 列表中每一幅图像的特征,并和查询图像进行匹配。单应性通过计算匹配数和计数内点数得到。最终,我们可以通过减少内点的数目对包含图像索引和内点数的字典进行排序。打印搜索结果列表到控制台,并可视化检索靠前的图像。
代码有问题,后面再重头改一下。
7.6 小结
本章介绍图像搜索的方法。通过类比单词计数来构建文档直方图向量 v,从而建立文档索引。到图片中使用SIFT 局部描述子,将描述子空间量化成一些典型实例,并将图像中的每个描述子指派到其中的某个实例中。**这些典型实例可以通过分析训练图像集确定,并被视为视觉单词。所有这些视觉单词构成的集合称为视觉词汇,有时也称为视觉码本。对于给定的问题、图像类型,或在通常情况下仅需呈现视觉内容,可以创建特定的词汇。这样就可以使用文档索引的方式将图片索引建立起来。之后再利用数据库,对图像进行搜索查找。