大型图像数据库上,CBIR(Content-Based Image Retrieval,基于内容的图像检索)技术用于检索在视觉上具相似性的图像。这样返回的图像可以是颜色相似、纹理相似、图像中的物体或场景相似;总之,基本上可以是这些图像自身共有的任何信息。
对于高层查询,比如寻找相似的物体,将查询图像与数据库中所有的图像进行完全比较(比如用特征匹配)往往是不可行的。在数据库很大的情况下,这样的查询方式会消耗过多时间。在过去的几年里,研究者成功地引入文本挖掘技术到CBIR中处理问题,使在数百万图像中搜索具有相似内容的图像成为可能。
矢量空间模型是用于表示和搜索文本文档的模型。我们将看到,它基本上可以应用于任何对象类型,包括图像。该名字来源于用矢量来表示文本文档,这些矢量是有文本词频直方图构成的。换句话说,矢量包含了每个单词出现的次数,而且在其他别的地方包含很多0元素。由于其忽略了单词出现的顺序及位置,该模型也被称为BOW表示模型。
通过单词计算来构建文档直方图向量v,从而建立文档索引。通常,在单词计数时会忽略掉一些常用词,如“这”“和”“是”等,这些常用词称为停用词。由于每篇文档长度不同,故除以直方图总和将向量归一化成单位长度。对于直方图向量中的每个元素,一般根据每个单词的重要性来赋予相应的权重。通常,数据集(或语料库)中一个单词的重要性来赋予相应的权重。通常,数据集(或语料库)中一个单词的重要性与它在文档中出现的次数成正比,而与它在语料库中出现的次数为反比。
最常用的权重是 tf-idf(term frequency-inverse document frequency,词频 - 逆向文
档频率 ),单词 w 在文档 d 中的词频是:
nw是单词w在文档d中出现的次数。为了归一化,将nw除以整个文档中单词的总数。逆向文档频率为:
D|是语料库D中文档的数目,分母是语料库中包含单词w的文档数d。二者相乘可以得到矢量v(直方图向量)对应元素tf-idf权重。
为了将文本发掘技术应用到图像中,我们首先需要建立视觉等效单词;这通常可以采用2.2节中介绍的SIFT局部描述子做到。它的思想是将描述子空间量化成一些典型实例,并将图像中的每个描述子指派到其中的某个实例中。这些典型实例可以通过分析训练图像集确定,并被视为视觉单词。所有这些视觉单词构成的集合称为视觉词汇,有时也称为视觉码本。对于给定的问题、图像类型,或在通常情况下仅需呈现视觉内容,可以创建特定的词汇。
从一个(很大的训练图像)集提取特征描述子,利用一些聚类算法可以构建出视觉单词。聚类算法中最常用的是K-means,这里也将采用K-means。视觉单词并不高端。只是在给定特征描述子空间中的一组向量集,在采用K-means进行聚类时得到的视觉单词是聚类质心。用视觉单词直方图来表示图像,则该模型便称为BOW模型。
创建名为 vocabulary.py 的文件,将下面代码添加进去。该代码创建一个词汇类,以及在训练图像数据集上训练出一个词汇的方法:
from numpy import *
from scipy.cluster.vq import *
from PCV.localdescriptors 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])
# 将所有的特征并在一起,以便后面进行 K-means 聚类
descriptors = descr[0]
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
Vocabulary 类包含了一个由单词聚类中心 VOC 与每个单词对应的逆向文档频率构成的向量,为了在某些图像集上训练词汇,train() 方法获取包含有 .sift 描后缀的述子文件列表和词汇单词数k。在 K-means 聚类阶段可以对训练数据下采样,因为如果使用过多特征,会耗费很长时间。
现在在你计算机的某个文件夹中,保存了图像及提取出来的 sift 特征文件,下面的代码会创建一个长为 k ≈ 1000 的词汇表。这里,再次假设 imlist 是一个包含了图像文件名的列表:
import pickle
from PCV.imagesearch import vocabulary
from PCV.tools.imtools import get_imlist
from PCV.localdescriptors import sift
imlist = get_imlist('path3/')
nbr_images = len(imlist)
# 获取特征列表
featlist = [imlist[i][:-3] + 'sift' for i in range(nbr_images)]
#提取文件夹下图像的sift特征
for i in range(nbr_images):
sift.process_image(imlist[i], featlist[i])
#生成词汇
voc = vocabulary.Vocabulary('ukbenchtest')
# 15个样本 运行10次
voc.train(featlist, 15, 10)
# 保存词汇
with open('path3/vocabulary.pkl', 'wb') as f:
pickle.dump(voc, f)
print("vocabulary is:", voc.name, voc.nbr_words)
代码最后部分用 pickle 模块保存了整个词汇对象以便后面使用。
在开始搜索之前,我们需要建立图像数据库和图像的视觉单词表示。
在索引图像前,我们需要建立一个数据库。这里,对图像进行索引就是从这些图像中提取描述子,利用词汇将描述子转换成视觉单词,并保存视觉单词及对应图像的单词直方图。从而可以利用图像对数据库进行查询,并返回相似的图像作为搜索结果。
使用SQLite 作为数据库。SQLite 将所有信息都保存到一个文件,是一个易于安装和使用的数据库。不涉及数据库和服务器的配置,很容易上手。SQLite 对应的Python 版本是pysqlite,可以从 http://code.google.com/p/pysqlite/ 获取。如果你的Python版本较高(本人为3.7),则pysqlite已经被包括在标准库内。
首先,创建一个名为 imagesearch.py 的文件,将下面的代码添加进去:
from numpy import *
import pickle
import sqlite3
from functools import cmp_to_key
import operator
class Indexer(object):
def __init__(self,db,voc):
""" 初始化数据库的名称及词汇对象 """
self.con = sqlite3.connect(db)
self.voc = voc
def __del__(self):
self.con.close()
def db_commit(self):
self.con.commit()
def create_tables(self):
""" Create the database tables. """
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()
用pickle 模块将这些数组编码成字符串以及将字符串进行解码; SQLite 可以从 pysqlite2 模块中导入。 Indexer 类连接数据 库,并且一旦创建(调用 init() 方法)后就可以保存词汇对象。del() 方法 可以确保关闭数据库连接,db_commit() 可以将更改写入数据库文件。
表单 imlist 包含所有要索引的图像文件名;imwords 包含了一个那些单词的单词索引、用到了哪个词汇、以及单词出现在哪些图像中。
有了数据库表单,我们便可以在索引中添加图像。为了实现该功能,我们需要在
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_indxed() 用来检查图像是否已经被索引,get_id() 则对一幅图像文件名给定 id 号。将下面的代码添加进 imagesearch.py:
def is_indexed(self,imname):
""" 如果图像名字(imname)被索引到,就返回 True"""
im = self.con.execute("select rowid from imlist where filename='%s'" % imname).fetchone()
return im != None
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]
下面使用代码遍历样本,加入索引
import pickle
import sqlite3
import imagesearch
from PCV.imagesearch import vocabulary
from PCV.tools.imtools import get_imlist
from PCV.localdescriptors import sift
# 获取图像列表
imlist = get_imlist('path3/')
nbr_images = len(imlist)
# 获取特征列表
featlist = [imlist[i][:-3] + 'sift' for i in range(nbr_images)]
# 提取文件夹下图像的sift特征
for i in range(nbr_images):
sift.process_image(imlist[i], featlist[i])
# 生成词汇
voc = vocabulary.Vocabulary('ukbenchtest')
voc.train(featlist, 15, 10)
# 保存词汇
with open('path3/vocabulary.pkl', 'wb') as f:
pickle.dump(voc, f)
print('vocabulary is:', voc.name, voc.nbr_words)
with open('path3/vocabulary.pkl', 'rb') as f:
voc = pickle.load(f)
# 创建索引器
indx = imagesearch.Indexer('path3/test.db',voc)
indx.create_tables()
# 遍历所有的图像,并将它们的特征投影到词汇上
for i in range(nbr_images)[:15]:
locs, descr = sift.read_features_from_file(featlist[i])
indx.add_to_index(imlist[i], descr)
# 提交到数据库
indx.db_commit()
con = sqlite3.connect('path3/test.db') # 连接到数据库
print(con.execute('select count (filename) from imlist').fetchone()) # 数据库操作
print(con.execute('select * from imlist').fetchone())
最后一行用 fetchall() 来代替 fetchone(),会得到一个包含所有文件名的长列表:
建立好图像的索引,我们就可以在数据库中搜索相似的图像了。这里,我们用 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 类中加入 candidates_from_word() 方法:
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]
利用一幅图像进行查询时,没有必要进行完全的搜索。为了比较单词直方图,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 的元组列表。
为了评价搜索结果的好坏,我们可以计算前 4 个位置中搜索到相似图像数。这是在
ukbench 图像集上评价搜索性能常采用的评价方式。这里给出了计算分数的函数,
将它添加到 imagesearch.py 中,你就可以开始优化查询了:
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。
下面是显示实际搜索结果的函数,添加到 imagesearch.py 中:
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()
def get_filename(self,imid):
""" 返回图像 id 对应的文件名 """
s = self.con.execute("select filename from imlist where rowid='%d'" % imid).fetchone()
return s[0]
BoW 模型的一个主要缺点是在用视觉单词表示图像时不包含图像特征的位置信息,这是为获取速度和可伸缩性而付出的代价。
利用一些考虑到特征几何关系的准则重排搜索到的靠前结果,可以提高准确率。最常用的方法是在查询图像与靠前图像的特征位置间拟合单应性。
为了提高效率,可以将特征位置存储在数据库中,并由特征的单词 id 决定它们之间的关联(要注意的是,只有在词汇足够大,使单词 id 包含很多准确匹配时,它才起作用)。然而,这需要大幅重写我们上面的数据库和代码,并复杂化表示形式。为了进行说明,我们仅重载靠前图像的特征,并对它们进行匹配。
# -*- coding: utf-8 -*-
import pickle
from PCV.localdescriptors import sift
import imagesearch
from PCV.geometry import homography
from PCV.tools.imtools import get_imlist
# load image list and vocabulary
#载入图像列表
imlist = get_imlist('path3/')
nbr_images = len(imlist)
#载入特征列表
featlist = [imlist[i][:-3]+'sift' for i in range(nbr_images)]
#载入词汇
with open('path3/vocabulary.pkl', 'rb') as f:
voc = pickle.load(f)
src = imagesearch.Searcher('path3/test.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]) #重排后的结果
上面实验例子运行之后又报错提示,经过查阅以为是Python版本不同所以导致没有运行出来,更改之后也是没有运行出来,所以此算法暂且还没有解决。