7.1 基于内容的图像检索
在大型图像数据库上,CBIR(Content-Based Image Retrieval,基于内容的图像检索)技术用于检索在视觉上具相似性的图像。这样返回的图像可以是颜色相似、纹理相似、图像中的物体或场景相似;
对于高层查询,比如寻找相似的物体,将查询图像与数据库中所有的图像进行完全比较(比如用特征匹配)往往是不可行的。在数据库很大的情况下,这样的查询方式会耗费过多时间。研究者成功地引入文本挖掘技术到CBIR 中处理问题,使在数百万图像中搜索具有相似内容的图像成为可能。
从文本挖掘中获取灵感——矢量空间模型
矢量空间模型是一个用于表示和搜索文本文档的模型。它基本上可以应用于任何对象类型,包括图像。矢量空间模型的名字来源于用矢量表示文本文档,这些矢量是由文本词频直方图构成,即矢量包含了每个单词出现的次数,且在其他别的地方包含了很多0元素。由于其忽略了单词出现的顺序及位置故也称为BOW表示模型。
通过单词计数来构建文档直方图向量v,从而建立文档索引。由于每篇文档的长度不同,因此除以直方图总和将向量归一化成单位长度。对于直方图向量中的每个元素,一般根据每个单词的重要性来赋予相应的权重。
最常用的权重是tf-idf(term frequency-inverse document frequency,词频- 逆向文档频率),单词w在文档d中的词频是:
\(\mathrm{tf}_{w,d}=\frac{n_w}{\sum_jn_j}\)
\(n_{w}\)是单词w在文档d中出现的次数。为了归一化,将\(n_{w}\)除以整个文档中单词的总数。逆向文档频率就为:
\(\mathrm{idf}_{w,d}=\log\frac{|(D)|}{|\{d:w\in d\}|}\)
\(|D|\)是在语料库D中文档的数目,分母是语料库中包含单词w的文档数d。将两者相乘就可以得到矢量v中对应元素的tf-idf权重。
7.2 视觉单词
可以利用SIFT局部描述子建立视觉等效单词,从而将文本挖掘技术应用到图像中。它的思想是将描述子空间量化成一些典型实例,并将图像中的每个描述子指派到其中的某个实例中,这些典型实例可以通过分析训练图像集确定,并被视为视觉单词。视觉单词组成的集合称为视觉词汇,亦或者视觉码本。
从一个很大的训练图像集提取特征描述子,利用一些聚类算法就可以构建出视觉单词,聚类算法中最常用的是K-means,这时得到的视觉单词是聚类质心。这里使用的数据集是书中给出的肯塔基大学物体识别数据集。可以在上一章给出的链接里下载得到。
创建词汇
为创建视觉单词词汇,首先需要提取特征描述子,这里使用的是SIFT特征描述子。
from PCV.tools import imtools
import sift
imlist=imtools.get_imlist('first1000')
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])
创建名为vocabulary.py 的文件,将下面代码添加进去。该代码创建一个词汇类,以及在训练图像数据集上训练出一个词汇的方法:
from numpy import *
from scipy.cluster.vq import *
import sift
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 arange(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
这个类中包含了一个由单词聚类中心VOC与每个单词对应的逆向文档频率构成的向量,为了在某些图像集上训练词汇,train()方法获取包含由.sift描后缀的述子文件列表和词汇单词数k。我们可以在K-means聚类阶段对训练数据下采样,避免消耗过多时间。
现在在你计算机的某个文件夹中,保存了图像及提取出来的sift 特征文件,下面的代码会创建一个长为k ≈ 1000 的词汇表。这里,再次假设imlist 是一个包含了图像文件名的列表:
import pickle
from PCV.imagesearch import vocabulary
from PCV.tools import imtools
imlist=imtools.get_imlist('first1000')
nbr_images = len(imlist)
featlist = [ imlist[i][:-3]+'sift' for i in range(nbr_images) ]
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)
7.3 图像索引
在开始搜索之前,我们需要建立图像数据库和图像的视觉单词表示。
7.3.1 建立数据库
在索引前,需要先建立一个数据库,这里对图像进行索引就是从这些图像中提取描述子,利用词汇将描述子转换成视觉单词,并保存视觉单词及对应图像的单词直方图。从而利用图像对数据库进行查询,并返回相似的图像作为结果。
使用SQLite作为数据库,SQLite将所有信息都保存到一个文件。在开始之前,我们需要创建表、索引和索引器Indexer 类,以便将图像数据写入数据库。首先,创建一个名为imagesearch.py 的文件,将下面的代码添加进去:
import pickle
from sqlite3 import dbapi2 as sqlite
class Indexer(object):
def __init__(self,db,voc):
""" 初始化数据库的名称及词汇对象 """
self.con = sqlite.connect(db)
self.voc = voc
def __del__(self):
self.con.close()
def db_commit(self):
self.con.commit()
使用pickle模块将数组编码成字符串以及将字符串进行解码,SQLite可以从sqlite3模块中导入,上述方法存在indexer类中,创建之后就可以保存词汇对象。__del__()方法确保关闭数据库连接,db_commit()可以将更改写入数据库文件。
下面为存储图像及视觉单词的简单数据库模式:
lmlist包含所有要索引的图像文件名,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()
7.3.2 添加图像
有了数据库表单,我们便可以在索引中添加图像。为了实现该功能,我们需要在Indexer 类中添加add_to_index() 方法。将下面的方法添加到imagesearch.py 中:
def add_to_index(self,imname,descr):
""" 获取一幅带有特征描述子 的图像,投影到词汇上并添加到数据库 """
if self.is_indexed(imname): return
print ('indexing', imname)
# 获得图像id
imid = self.get_id(imname)
# 获取单词
imwords = self.voc.project(descr)
nbr_words = imwords.shape[0]
# 将每个单词与图像链接起来
for i in range(nbr_words):
word = imwords[i]
# wordid就是单词本身的数字
self.con.execute("insert into imwords(imid,wordid,vocname) values (?,?,?)", (imid,word,self.voc.name))
# 存储图像的单词直方图
# 用pickle模块将NumPy数组编码成字符串
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号。
将下面的代码添加进imagesearch.py:
def get_id(self,imname):
""" 获取图像id,不存在就进行添加 """
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):
""" 当图像名字被索引返回True """
im = self.con.execute("select rowid from imlist where filename='%s'" % imname).fetchone()
return im != None
由于SQLite 的数据库在存储对象或数组时并没有一个标准类型。所以,我们用Pickle 的dumps() 函数创建一个字符串表示,并将其写入数据库。
下面的示例代码会遍历整个ukbench 数据库中的样本图像,并将其加入我们的索引。这里,假设列表imlist 和featlist 分别包含之前图像文件名及图像描述子,vocabulary.pkl 包含已经训练好的词汇:
import pickle
import sift
import imagesearch
from PCV.tools import imtools
imlist = imtools.get_imlist('first1000')
nbr_images = len(imlist)
featlist = [imlist[i][:-3] + 'sift' for i in range(nbr_images)]
# 载入词汇
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)[:100]:
locs,descr = sift.read_features_from_file(featlist[i])
indx.add_to_index(imlist[i],descr)
# 提交到数据库
indx.db_commit()
检查数据库中的内容:
from sqlite3 import dbapi2 as sqlite
con = sqlite.connect('test.db')
print (con.execute('select count (filename) from imlist').fetchone())
print (con.execute('select * from imlist').fetchone())
7.4 在数据库中搜索图像
建立好图像的索引,就可以在数据库中搜索相似的图像了。这里,使用BoW(词袋模型)来表示整个图像,这是通用的,可以应用于寻找相似的物体、相似的脸、相似的颜色等,它完全取决于图像及所用的描述子。
为了实现搜索,在Imagesearch.py中添加Searcher类。
class Searcher(object):
def __init__(self,db,voc):
""" 初始化数据库的名称 """
self.con = sqlite3.connect(db)
self.voc = voc
def __del__(self):
self.con.close()
如果图像数据库很大,逐一比较整个数据库中的所有直方图往往是不可行的。这就需要找到一个大小合理的候选集(这里的“合理”是通过搜索响应时间、所需内存等确定的),单词索引的作用便在于此:可以利用单词索引获得候选集,然后只需在候选集上进行逐一比较。
7.4.1 利用索引获取候选图像
利用建立起来的索引找到包含特定单词的所有图像,这不过是对数据库做一次简单的查询。在Searcher 类中加入candidates_from_word() 方法:会给出包含特定单词的所有图像id 号。
def candidates_from_word(self,imword):
""" G 获取包含imword 的图像列表"""
im_ids = self.con.execute(
"select distinct imid from imwords where wordid=%d" % imword).fetchall()
return [i[0] for i in im_ids]
candidates_from_histogram() 方法从图像单词直方图的非零项创建单词id列表,检索每个单词获得候选集并将其合并到candidates列表中,然后创建一个元组列表每个元组由单词id和次数count构成,其中次数count是候选列表中每个单词出现的次数。同时,还以元组中的第二个元素为准,用sort()方法和一个自定义的比较函数对列表进行排序。该自定义比较函数进行用lambda函数内联声明,对于单行函数声明,使用lambda函数非常方便。最后结果返回一个包含图像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(cmp=lambda x,y:cmp(x[1],y[1]))
tmp.reverse()
# 返回排序后的列表,最匹配的排在最前面
return [w[0] for w in tmp]
7.4.2 用一幅图像进行查询
get_imhistogram() 用于返回一幅图像的单词直方图。
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]))
query() 方法用于获取图像的文件名,检索其单词直方图及候选图像列表。对于每个候选图像,用标准的欧氏距离比较它和查询图像间的直方图,并返回一个经排序的包含距离及图像id的元组列表。
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 ) ) # 用L2 距离度量相似性
matchscores.append( (cand_dist,imid) )
# 返回排序后的距离及对应数据库ids 列表
matchscores.sort()
对前一节的图像进行查询:
import pickle
from PCV.tools import imtools
import imagesearch
from PCV.localdescriptors import sift
imlist = imtools.get_imlist('first1000\\')
nbr_images = len(imlist)
featlist = [imlist[i][:-3] + 'sift' for i in range(nbr_images)]
# 载入词汇
with open('ch07\\7.3\\vocabulary.pkl', 'rb') as f:
voc = pickle.load(f)
src = imagesearch.Searcher('test.db', voc)
print('try a query....')
print(src.query(imlist[0])[:10])
7.4.3 确定对比基准并绘制结果
为评价搜索结果的好坏,可以计算前4个位置中搜索到相似图像数。计算分数的函数为:
def compute_ukbench_score(src,imlist):
""" 对查询返回的前4个结果计算平均相似图像数,并返回结果 """
nbr_images = len(imlist)
pos = zeros((nbr_images,4))
# 获取每幅查询图像的前4个结果
for i in range(nbr_images):
pos[i] = [w[1]-1 for w in src.query(imlist[i])[:4]]
# 计算分数,并返回平均分数
score = array([ (pos[i]//4)==(i//4) for i in range(nbr_images)])*1.0
return sum(score) / (nbr_images)
获取搜素的前4个结果,将query()返回的索引减去1,因为数据库索引是从1开始的,而图像列表的索引是从0开始的。利用每4幅图像为一组时相似图像文件名是连续的这一事实,用整数相除计算得到最终的分数。分数为4时结果最为理想;没有一个是准确的时,分数为0;仅检索到相同图像时,分数为1,当找到相同的图像并且其他三个中的两个相同时,分数为3。
用于显示实际搜索结果的函数:
def plot_results(src,res):
""" 显示在列表res中的图像 """
figure()
nbr_results = len(res)
for i in range(nbr_results):
imname = src.get_filename(res[i])
subplot(1,nbr_results,i+1)
imshow(array(Image.open(imname)))
axis('off')
show()
import pickle
from PCV.tools import imtools
from PCV.imagesearch import imagesearch
from PCV.localdescriptors import sift
imlist = imtools.get_imlist('first1000\\')
nbr_images = len(imlist)
featlist = [imlist[i][:-3] + 'sift' for i in range(nbr_images)]
# 载入词汇
with open('ch07\\7.3\\vocabulary.pkl', 'rb') as f:
voc = pickle.load(f)
src = imagesearch.Searcher('test.db', voc)
nbr_results = 6
res = [w[1] for w in src.query(imlist[0])[:nbr_results]]
imagesearch.plot_results(src,res)
7.5 使用几何特性对结果排序
BoW模型的一个主要缺点是在用视觉单词表示图像时不包含图像特征的位置信息,是为获取速度和可伸缩性而付出的代价。
一个载入所有模型文件并用单应性对靠前的图像进行重排的完整例子:
import pickle
from PCV.imagesearch import imagesearch
from PCV.localdescriptors import sift
from PCV.geometry import homography
from PCV.tools import imtools
imlist = imtools.get_imlist('first1000\\')
nbr_images = len(imlist)
featlist = [imlist[i][:-3] + 'sift' for i in range(nbr_images)]
# 载入词汇
with open('vocabulary.pkl', 'rb') as f:
voc = pickle.load(f)
src = imagesearch.Searcher('test.db',voc)
# 查询图像的索引号和返回的搜结果数目
q_ind = 50
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])
# 获取匹配数
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]) # 重排后的结果