1 基于内容的图像检索
在大型图像数据库上,CBIR(基于内容的图像检索)用于检索在视觉上具有相似性的图像。这样的返回的图像可以是颜色相似、纹理相似、图像中的物体或者场景相似。
矢量空间模型是一个用于表示和搜索文本文档的模型,它基本可以应用于任何对象类型,包括图像。通过单词计数来构建文档直方图向量v,从而建立起文档索引。图像内容检索与文本检索类似,使用局部特征构建视觉词袋向量来表示图像。对图像提取若干个局部特征描述子,对这些描述子进行量化。聚类后的质心即为视觉单词。得到视觉词库后,输入检索图像提取其特征,与目标库比对查找近邻向量,计算距离即可。
2 视觉单词
它的思想是将描述子空间量化成一些典型实例,并将图像中的每个描述子指派到其中的某个实例中。这些典型实例可以通过分析训练图像集确定,并被视为视觉单词。对于给定的问题、图像类型,或是在通常情况下仅需呈现视觉内容,可以创建特定的词汇。
从一个图像集提取特征描述子,利用一些聚类算法可以构建出视觉单词。采用聚类算法得到的视觉单词是聚类质心,用视觉单词直方图来表示图像,则该模型就被称为BOW模型。
为创建视觉单词词汇,首先需要提取特征描述子。在这里使用SIFT特征描述子。运行下面的代码,可以得到每幅图像提取出的描述子,并将每幅图像的描述子保存在一个文件中:
nbr_images = len(imlist)
featlist = [imlist[i][:-3] + 'sift' for i in range(nbr_images)]
for i in range(nbr_images):
sift.process_image(imlist[i], featlist[i])
创建一个词汇类,以及在训练图像数据集上训练处一个词汇的方法:
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):
nbr_images = len(featurefiles)
# read the features from file
descr = []
descr.append(sift.read_features_from_file(featurefiles[0])[1])
descriptors = descr[0] #stack all features for k-means
for i in arange(1,nbr_images):
descr.append(sift.read_features_from_file(featurefiles[i])[1])
descriptors = vstack((descriptors,descr[i]))
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
Vocabulary类包含了一个由单词聚类中心VOC与每个单词对应的逆向文档频率构成的向量,为了在某些图像集上训练词汇,train()方法获取包含有.sift后缀的描述文件列表和词汇单词数k。
下面的代码根据提取出的sift文件创建一个长为k的词汇表:
voc = vocabulary.Vocabulary('ukbenchtest')
voc.train(featlist, 1000, 10)
with open('vocabulary.pkl', 'wb') as f:
pickle.dump(voc, f)
print('vocabulary is :', voc.name, voc.nbr_words)
3 图像索引
在开始搜索之前,需要建立图像数据库和图像的视觉单词表示。
3.1 建立数据库
在索引图像之前,需要建立一个数据库。这里,对图像进行索引就是从这些图像中提取描述子,利用词汇将描述子转换成视觉单词,并保存视觉单词以及对应图像的单词直方图。从而可以利用图像对数据库进行查询,并返回相似的图像作为搜索结果。
在开始之前,需要创建表、索引和索引器Indexer类,以便将图像数据写入数据库:

class Indexer(object):
def __init__(self,db,voc):
""" Initialize with the name of the database
and a vocabulary object. """
self.con = sqlite.connect(db)
self.voc = voc
def __del__(self):
self.con.close()
def db_commit(self):
self.con.commit()
这里仅需一个包含三个表单的简单数据库模式。表单imlist包含所有索引的图像文件名,imwords包含了一个那些单词的单词索引、用到的词汇和单词出现在哪些图像中,最后imhistograms包含了全部每幅图像的单词直方图。根据矢量空间模型需要这些以便进行图像比较。下面Indexer类中的方法用于创建表单以及一些有用的索引以加快搜索速度:
def create_tables(self):
self.con.execute('create table imlist(filename)')
self.con.execute('create table imwords(imid,wordid,vocname)')
self.con.execute('create table imhistograms(imid,histogram,vocname)')
self.con.execute('create index im_idx on imlist(filename)')
self.con.execute('create index wordid_idx on imwords(wordid)')
self.con.execute('create index imid_idx on imwords(imid)')
self.con.execute('create index imidhist_idx on imhistograms(imid)')
self.db_commit()
3.2 添加图像
有了数据库表单,我们便可以在索引中添加图像。为了实现该功能,需要在Indexer中加入add_to_index()方法:
def add_to_index(self,imname,descr):
if self.is_indexed(imname): return
print ('indexing', imname)
imid = self.get_id(imname)
imwords = self.voc.project(descr)
nbr_words = imwords.shape[0]
for i in range(nbr_words):
word = imwords[i]
self.con.execute("insert into imwords(imid,wordid,vocname) values (?,?,?)", (imid,word,self.voc.name))
self.con.execute("insert into imhistograms(imid,histogram,vocname) values (?,?,?)", (imid,pickle.dumps(imwords),self.voc.name))
该方法获取图像文件名与NumPy数组,该数组包含的是在图像找到的描述子。这些描述子投影到该词汇,并插入到imwords和imhistograms表单中。接下来使用两个辅助函数:is_indexed()和get_id()检查图像是否已经被索引和对一幅图像文件名给定id号。
def get_id(self,imname):
cur = self.con.execute(
"select rowid from imlist where filename='%s'" % imname)
res=cur.fetchone()
if res==None:
cur = self.con.execute(
"insert into imlist(filename) values ('%s')" % imname)
return cur.lastrowid
else:
return res[0]
def is_indexed(self,imname):
im = self.con.execute("select rowid from imlist where filename='%s'" % imname).fetchone()
return im != None
下面的实例代码会遍历整个ukbench数据库中的样本图像,并将其加入索引。
with open('vocabulary.pkl', 'wb') as f:
pickle.dump(voc, f)
print('vocabulary is :', voc.name, voc.nbr_words)
for i in range(nbr_images):
sift.process_image(imlist[i], featlist[i])
with open('vocabulary.pkl', 'rb') as f:
voc = pickle.load(f)
indx = imagesearch.Indexer('test.db', voc)
indx.create_tables()
for i in range(nbr_images)[:1000]:
locs, descr = my_sift.read_features_from_file(featlist[i])
indx.add_to_index(imlist[i], descr)
index.db_commit()
com = sqlite.connect('test.db')
print(con.execute('select count (filename) from inlist').fetchone())
print(con.execute('select * from inlist').fetchone())
4 在数据库中搜索图像
建立好图像的索引,我们就可以在数据库中搜索相似的图像了。这里,我们用BoW(Bag-of-Word,词袋模型)来表示整个图像,不过这里介绍的过程是通用的,可以应用于寻找相似的物体、相似的脸、相似的颜色等,它完全取决于图像及所用的描述子。为实现搜索,我们在imagesearch.py中添加Searcher类:
class Searcher(object):
def __init__(self, db, voc):
""" 初始化数据库的名称. """
self.con = sqlite.connect(db)
self.voc = voc
def __del__(self):
self.con.close()
一个新的 Searcher 对象连接到数据库,一旦删除便关闭连接,这与之前的 Indexer 类中的处理过程相同。如果图像数据库很大,逐一比较整个数据库中的所有直方图往往是不可行的。我们需要找到一个大小合理的候选集(这里的“合理”是通过搜索响应时间、所需内存等确定的),单词索引的作用便在于此:我们可以利用单词索引获得候选集,然后只需在候选集上进行逐一比较。
4 在数据库中搜索图像
建立好图像的索引,我们就可以在数据库中搜索相似的图像了。这里,我们用BoW(Bag-of-Word,词袋模型)来表示整个图像,不过这里介绍的过程是通用的,可以应用于寻找相似的物体、相似的脸、相似的颜色等,它完全取决于图像及所用的描述子。为实现搜索,我们在imagesearch.py中添加Searcher类
class Searcher(object):
def __init__(self, db, voc):
""" 初始化数据库的名称. """
self.con = sqlite.connect(db)
self.voc = voc
def __del__(self):
self.con.close()
一个新的 Searcher 对象连接到数据库,一旦删除便关闭连接,这与之前的 Indexer 类中的处理过程相同。如果图像数据库很大,逐一比较整个数据库中的所有直方图往往是不可行的。我们需要找到一个大小合理的候选集(这里的“合理”是通过搜索响应时间、所需内存等确定的),单词索引的作用便在于此:我们可以利用单词索引获得候选集,然后只需在候选集上进行逐一比较
4.1 利用索引获取候选图像
我们可以利用建立起来的索引找到包含特定单词的所有图像,这不过是对数据集做一次简单的查询。在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出现的次数进行跟踪,因为这可以显示有多少单词与单词直方图中的单词匹配,该过程有下面的方法完成:
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 的列表,排在列表最前面的是最好的匹配图像。
src = 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])
运行结果:
4.2 用一幅图像进行查询
利用一幅图像进行查询时,没有必要进行完全的搜索。为了比较单词直方图,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的元组列表。
尝试对前一节的图像进行查询;
src = 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])
print('try a query...')
print(src.query(imlist[0])[:10])
5 使用几何特性对结果排序
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
# load image list and vocabulary
#载入图像列表
imlist = get_imlist('first1000/')
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)
src = imagesearch.Searcher('testImaAdd.db',voc)
# index of query image and number of results to return
#查询图像索引和查询返回的图像数
q_ind = 0
nbr_results = 20
# regular query
# 常规查询(按欧式距离对结果排序)
res_reg = [w[1] for w in src.query(imlist[q_ind])[:nbr_results]]
print 'top matches (regular):', res_reg
# load image features for query image
#载入查询图像特征
q_locs,q_descr = sift.read_features_from_file(featlist[q_ind])
fp = homography.make_homog(q_locs[:,:2].T)
# RANSAC model for homography fitting
#用单应性进行拟合建立RANSAC模型
model = homography.RansacModel()
rank = {}
# load image features for result
#载入候选图像的特征
for ndx in res_reg[1:]:
locs,descr = sift.read_features_from_file(featlist[ndx]) # because 'ndx' is a rowid of the DB that starts at 1
# get matches
matches = sift.match(q_descr,descr)
ind = matches.nonzero()[0]
ind2 = matches[ind]
tp = homography.make_homog(locs[:,:2].T)
# compute homography, count inliers. if not enough matches return empty list
try:
H,inliers = homography.H_from_ransac(fp[:,ind],tp[:,ind2],model,match_theshold=4)
except:
inliers = []
# store inlier count
rank[ndx] = len(inliers)
# sort dictionary to get the most inliers first
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]) #重排后的结果