文章目录
引言
本章将展示如何利用文本挖掘技术对基于图像视觉内容进行图像搜索,本章提出视觉单词的基本思想,并解释了完整的安装细节,在一个示例数据集上进行了测试。
7.1 基于内容的图像搜索
大型图像数据库上,CBIR(Content-Based Image Retrieval)基于内容的图像检索技术用于检索在视觉上具相似性的图像,返回的图像可以是颜色相似、纹理相似、物体或场景相似等等。
但对于高层查询,将查询图像与数据库中所有的图像进行完全比较(比如用于特征匹配)往往是不可行的。研究者成功地引入文本挖掘技术到CBIR中处理问题,使在数百万图像中搜索具有相似内容地图像成为可能。
研究者从文本挖掘中获取灵感,提出了矢量空间模型
矢量空间模型用于表示和搜索文本文档的模型,基本可以应用于任何对象类型,包括图像。矢量由文本词频直方图构成,换句话说矢量包含每个单词出现的次数,由于忽略了单词出现的顺序及位置,该模型也别称为BOW表示模型。
文档索引的建立是通过单词计数构建文档直方图v,通常,单词计数时会忽略掉一些常用词,如“这”,“和”,“是”等。由于文档长度不同,故除以直方图总和将向量归一化成单位长度。且对于向量中的每个元素,一般根据每个单词的重要性来赋予相应的权重,通常,单词的重要性与它在文档中出现的次数成正比,而在数据集中出现的次数成反比。
最常见的权重是tf-idf(词频-逆向文档频率),单词w在文档d的词频是(将其归一化):
t
f
w
,
d
=
n
w
∑
j
n
j
tf_{w,d} = \frac{n_w}{\sum_{j} n_j}
tfw,d=∑jnjnw
逆向文档频率为:
i
d
f
w
,
d
=
l
o
g
∣
(
D
)
∣
∣
d
:
w
∈
d
∣
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 视觉单词
如何将文本挖掘技术应用到图像中呢?首先需要建立视觉等效单词,SIFT局部描述子可以做到,其思想是将描述子空间量化成典型实例,并将每个描述子指派到其中的某个实例中。典型实例可以通过分析训练图像集确定,并称为视觉单词,所有视觉单词构成的集合称为视觉词汇,或视觉码本。
从训练图像集提取特征描述子,利用聚类算法可以构建视觉单词,通常是K-means,在采用K-means进行聚类时得到的视觉单词是聚类质心。用视觉单词直方图来表示图像,称为BOW模型。
数据集中有很多子集,每个子集中包含四幅图像,具有相同的场景或物体,且存储的文件名是连续的。
7.3.1 创建词汇
提取SIFT特征描述子创建单词视觉词汇,imlist 包含的是图像的文件名。运行下面的代码,可以得到每幅图像提取出的描述子,并将每幅图像的描述子保存在一个文件中:
import os
from PIL import Image
from pylab import *
from numpy import *
def get_imlist(path):
""" Returns a list of filenames for
all jpg images in a directory. """
return [os.path.join(path,f) for f in os.listdir(path) if f.endswith('.jpg')]
imlist = get_imlist('first1000')
import 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])
下面的代码创建词汇类,及在训练数据集上训练出词汇的方法:
#K-means
from scipy.cluster.vq import *
import sift
from numpy import *
class Vocabulary(object):
def __init__(self,name):
self.name = name
self.voc = []
self.idf = []
self.trainingdata = []
self.nbr_words = 0
def project(self,descriptors):
# 图像单词直方图
imhist = zeros((self.nbr_words))
words,distance = vq(descriptors,self.voc)
for w in words:
imhist[w] += 1
return imhist
def train(self,featurefiles,k=100,subsampling=10):
nbr_images = len(featurefiles)
# 从文件中读取特征
descr = []
# 返回描述子f[:,4:]
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: 最后一个参数决定运行次数,返回k个对象
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
类包含了单词聚类中心VOC和每个单词对应的idf构成的向量,train()方法获取.sift的描述子文件列表和词汇单词数k,在聚类阶段对训练数据下采样。
使用pickle板块保存词汇对象:
import pickle
import vocabulary
voc = vocabulary.Vocabulary('ukbenchtest')
#1000个样本
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作为数据库,创建表、索引、索引器Indexser,以便将图像数据写入:
import pickle
import sqlite3 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()
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()
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))
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]
获取图像文件名和数组,包含描述子,投影到词汇上,插入到imwords和imhistograms表单中,is_indexed()用来检查是否被索引,get_id()则给定id号。
遍历数据库中的样本图像,并加入索引,图像文件名、描述子、pickle板块均已得到:
import pickle
import sift
import imagesearch
nbr_images = len(imlist)
# 载入词汇
with open('vocabulary.pkl', 'rb') as f:
voc = pickle.load(f)
# 创建索引器,test.db是数据库名称,voc是词汇对象
indx = Indexer('test.db',voc)
# 遍历整个图像库,将特征投影到词汇上并添加到索引中
for i in range(nbr_images)[:1000]:
locs,descr = sift.read_features_from_file(featlist[i])
indx.add_to_index(imlist[i],descr)
# 提交到数据库
indx.db_commit()
索引至下标999。
7.4 在数据库中搜索图像
我们用BoW(Bag of Word,词袋模型)来表示整个图像,取决于图像及所用的描述子,应用于寻找相似的物体、颜色灯等。
添加Search类,与Indexer类似:
class Searcher(object):
def __init__(self,db,voc):
""" 初始化数据库的名称 """
self.con = sqlite.connect(db)
self.voc = voc
def __del__(self):
self.con.close()
7.4.1 利用索引获取候选图像
利用建立起来的索引找到特定单词的图像,加Searcher类中加入函数
获取包含 imword 的所有图像列表:
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]
上面的函数将给出包含特定单词的图像id号,我们将在单词集里进行遍历,得到图像,合并列表,我们需要在合并列表中对每个图像id次数进行追踪,可以显示多少单词与单词直方图中的单词匹配,由以下函数完成:
from functools import cmp_to_key
import operator
class Searcher(object):
def __init__(self,db,voc):
""" 初始化数据库的名称 """
self.con = sqlite.connect(db)
self.voc = voc
def __del__(self):
self.con.close()
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]
def candidates_from_histogram(self,imwords):
""" 获取具有相似单词的图像列表 """
# 获取单词 id
words = imwords.nonzero()[0]
# 寻找候选图像
candidates = []
for word in words:
#获取图像列表
c = self.candidates_from_word(word)
candidates+=c
# 获取所有唯一的单词,并按出现次数反向排序,注意set表示唯一的单词
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列表,合并到候选列表中,创建列表放单词id和次数count,对列表进行排序,排在最前面的是最好的匹配图像。
一个例子:
from functools import cmp_to_key
src = Searcher('test.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])
最匹配的排前面,然而输出是倒叙输出,并不准确,我们可以取出该列表中任意数量的元素并匹配直方图,将极大提高检索效率。
7.4.2 用一幅图像进行查询
利用图像进行查询时,没有必要进行完全的搜索,为了比较单词直方图,需要从数据库读入图像的单词直方图:
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(s[0])
query() 方法获取图像的文件名,检索其单词直方图及候选图像列表。对于每个候选图像,我们用标准的欧式距离比较它和查询图像间的直方图,并返回一个经排序的包含距离及图像id 的元组列表:
import math
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 = math.sqrt( sum(self.voc.idf* (h-cand_h)**2 ) ) # 用 L2 距离度量相似性
matchscores.append( (cand_dist,imid) )
# 返回排序后的距离及对应数据库 ids 列表
matchscores.sort()
return matchscores
显示的是再次查询的结果,参数是cand_dist和imid,结果是图像集中与第一幅图像最接近的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)
因query索引是从1开始,所以减去1,利用每4幅图像相似文件名连续,整数相除得到最终的分数。
实际显示结果函数:
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]