文章目录
7.1基于内容的图像检索
在大型图像数据库上,CBIR(Content-Based Image Retrieval,基于内容的图像检索)技术用于检索在视觉上具相似性的图像。这样返回的图像可以是颜色相似、纹理相似、图像中的物体或场景相似;总之,基本上可以是这些图像自身共有的任何信息。
任给定一个检索图像示例P,计算其特征向量F=(F1,F2,F3…),Fi为图像的第i种特征。
根据F检索图像特征索引库,得到与F距离最小的特征向量F′ ,则F′ 所对应的图像P′ 即为与P最相似的检索结果。
基础知识:矢量空间模型
矢量空间模型是一个用于表示和搜索文本文档的模型。它基本上可以应用于任何对象类型,包括图像。该名字来源于用矢量来表示文本文档,这些矢量是由文本词频直方图构成的。矢量包含了每个单词出现的次数,而且在其他别的地方包含很多 0 元素。由于其忽略了单词出现的顺序及位置,该模型也被称为 BOW 表示模型。
最常用的权重是 tf-idf(term frequency-inverse document frequency,词频 - 逆向文档频率 ),单词 w 在文档 d 中的词频是:
nw 是单词 w 在文档 d 中出现的次数。为了归一化,将 nw 除以整个文档中单词的总数。
逆向文档频率为:
|D| 是在语料库 D 中文档的数目,分母是语料库中包含单词 w 的文档数 d。将两者相乘可以得到矢量 v 中对应元素的 tf-idf 权重。
# -*-coding:utf-8 -*-
# 在词袋模型中,文档的特征就是其包含的word,corpus的每一个元素对应一篇文档
texts = [['human', 'interface', 'computer'],
['survey', 'user', 'computer', 'system', 'response', 'time'],
['eps', 'use', 'interface', 'system'],
['system', 'human', 'system', 'eps'],
['user', 'response', 'time'],
['trees'],
['graph', 'trees'],
['graph', 'minors', 'trees'],
['graph', 'minors', 'survey']]
# 训练语料的预处理,将原始文本特征表达转换成词袋模型对应的系数向量
from gensim import corpora
# from gensim.models.word2vec import Word2Vec
dictionary = corpora.Dictionary(texts) # texts就是若干个被拆成单词集合的文档的集合,而dictionary就是把所有单词取一个set(),并对set中每个单词分配一个Id号的map
print(dictionary)
# 是把文档 doc变成一个稀疏向量,[(0, 1), (1, 1)],表明id为0,1的词汇出现了1次,至于其他词汇,没有出现,在这里可以看出set()中computer的id是0,human的id是1...
corpus = [dictionary.doc2bow(text) for text in texts]
print(corpus) # 输出为[(0, 1), (1, 1), (2, 1)],就表示id为0,1,2,即单词computer,human,interface,在第一个维度中都出现了一次
# tf-idf的计算
from gensim import models
tfidf = models.TfidfModel(corpus)
print(tfidf)
doc_bow = [(0, 1), (1, 1)]
print(tfidf[doc_bow])
print(tfidf.idfs)
TF-IDF实际上是:TF * IDF。主要思想是:如果某个词或短语在一篇文章中出现的频率高(即TF高),并且在其他文章中很少出现(即IDF高),则认为此词或者短语具有很好的类别区分能力,适合用来分类。
7.2 视觉单词
视觉单词的思想是将描述子空间量化成一些典型实例,并将图像中的每个描述子指派到其中的某个实例中。这些典型实例可以通过分析训练图像集确定,并被视为视觉单词。所有这些视觉单词构成的集合称为视觉词汇,有时也称为视觉码本。对于给定的问题、图像类型,或在通常情况下仅需呈现视觉内容,可以创建特定的词汇。
从一个(很大的训练图像)集提取特征描述子,利用一些聚类算法可以构建出视觉单词。聚类算法中最常用的是 K-means1,这里也将采用 K-means。视觉单词并不高端,只是在给定特征描述子空间中的一组向量集,在采用 K-means 进行聚类时得到的视觉单词是聚类质心。用视觉单词直方图来表示图像,则该模型便称为 BOW 模型。
创建词汇
为创建视觉单词词汇,首先需要提取特征描述子。这里,我们使用 SIFT 特征描述
子。imlist 包含的是图像的文件名。运行下面的代码,可以得到每幅图像提取出的描述子,并将每幅图像的描述子保存在一个文件中:
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 *
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
# -*- coding: utf-8 -*-
import pickle
from PCV.imagesearch import vocabulary
from PCV.tools.imtools import get_imlist
from PCV.localdescriptors import sift
#获取图像列表
imlist = get_imlist('C:\\Users\\xqm\\PycharmProjects\\char77\\')
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, 1000, 10)
#保存词汇
with open('C:\\Users\\xqm\\PycharmProjects\\char77\\vocabulary.pkl', 'wb') as f:
pickle.dump(voc, f)
print ('vocabulary is:', voc.name, voc.nbr_words)
控制台结果
Vocabulary是调用PCV下 imagesearch 中的文件 vocabulary.py ,该文件代码的作用是创建一个词汇类,以及在训练图像数据集上训练出一个词汇。其中包含了一个有单词聚类中心与每个单词对应的逆向文档频率构成的向量。该词汇类中还包含了train()函数。train()方法获取包含有.sift描后缀的述子文件列表和词汇单词数k(聚类中心数)。最后用pickle模块保存了整个词汇对象以便后面使用,pickle.dump(voc, f)是将词汇写入到.pkl文件中。
7.3图像索引
7.3.1 建立数据库
在开始搜索之前,我们需要建立图像数据库和图像的视觉单词表示。这里,对图像进行索引就是从这些图像中提取描述子,利用词汇将描述子转换成视觉单词,并保存视觉单词及对应图像的单词直方图。从而可以利用图像对数据库进行查询,并返回相似的图像作为搜索结果。
在开始之前,我们需要创建表、索引和索引器 Indexer 类,以便将图像数据写入数
据库。首先,创建一个名为 imagesearch.py 的文件,将下面的代码添加进去:
import pickle
from pysqlite2 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 可以从 pysqlite2 模块中导入。Indexer 类连接数据库,并且一旦创建(调用 init() 方法)后就可以保存词汇对象。del() 方法可以确保关闭数据库连接,db_commit() 可以将更改写入数据库文件。
我们仅需一个包含三个表单的简单数据库模式。
表单 imlist 包含所有要索引的图像文件名;imwords 包含了一个那些单词的单词索引、用到了哪个词汇、以及单词出现在哪些图像中;最后,imhistograms 包含了全部每幅图像的单词直方图。根据矢量空间模型,我们需要这些以便进行图像比较。
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_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]
代码示例,遍历整个数据库的样本图像,并将其加入我们的索引。
# -*- coding: utf-8 -*-
import pickle
from PCV.imagesearch import vocabulary
from PCV.tools.imtools import get_imlist
from PCV.localdescriptors import sift
from PCV.imagesearch import imagesearch
#获取图像列表
imlist = get_imlist('C:\\Users\\xqm\\PycharmProjects\\char77\\')
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, 1000, 10)
#保存词汇
with open('C:\\Users\\xqm\\PycharmProjects\\char77\\vocabulary.pkl', 'wb') as f:
pickle.dump(voc, f)
print ('vocabulary is:', voc.name, voc.nbr_words)
# 载入词汇
with open('C:\\Users\\xqm\\PycharmProjects\\char77\\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 = 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').fetchall()
控制台输出结果:
7.4 在数据库中搜索图像
如果图像数据库很大,逐一比较整个数据库中的所有直方图往往是不可行的。我们
需要找到一个大小合理的候选集(这里的“合理”是通过搜索响应时间、所需内存
等确定的),单词索引的作用便在于此:我们可以利用单词索引获得候选集,然后只
需在候选集上进行逐一比较。
7.4.1 利用索引获取候选图像
可以利用建立起来的索引找到包含特定单词的所有图像,这不过是对数据库做
一次简单的查询。
在 Searcher 类中加入 candidates_from_word() 方法会给出包含特定单词的所有图像 id 号。为了获得包含多个单词的候选图像,例如一个单词直方图中的全部非零元素,在每个单词上进行遍历,得到包含该单词的图像,并合并这些列表 。这里,我们仍然需要在合并了的列表中对每一个图像id 出现的次数进行跟踪,因为这可以显示有多少单词与单词直方图中的单词匹配。该过程可以通过 candidates_from_histogram 方法完成
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
# 获取所有唯一的单词,并按出现次数反向排序
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]
代码示例,在索引中查找前五个图像的ID:
# -*- coding: utf-8 -*-
import pickle
from PCV.imagesearch import vocabulary
from PCV.tools.imtools import get_imlist
from PCV.localdescriptors import sift
from PCV.imagesearch import imagesearch
imlist = get_imlist('C:\\Users\\xqm\\PycharmProjects\\char77\\')
nbr_images = len(imlist)
#获取特征列表
featlist = [imlist[i][:-3]+'sift' for i in range(nbr_images)]
with open('C:\\Users\\xqm\\PycharmProjects\\char77\\vocabulary.pkl', 'rb') as f:
voc = pickle.load(f)
src = imagesearch.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)[:5]
控制台输出结果
7.4.2 用一幅图像进行查询
利用一幅图像进行查询时,没有必要进行完全的搜索。为了比较单词直方图,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]))
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()
return matchscores
该 query() 方法获取图像的文件名,检索其单词直方图及候选图像列表(如果你的
数据集很大,候选集的大小应该限制在某个最大值)。对于每个候选图像,我们用标
准的欧式距离比较它和查询图像间的直方图,并返回一个经排序的包含距离及图像
id 的元组列表。
再一次对之前的图像进行查询
# -*- coding: utf-8 -*-
import pickle
from PCV.imagesearch import vocabulary
from PCV.tools.imtools import get_imlist
from PCV.localdescriptors import sift
from PCV.imagesearch import imagesearch
imlist = get_imlist('C:\\Users\\xqm\\PycharmProjects\\char77\\')
nbr_images = len(imlist)
#获取特征列表
featlist = [imlist[i][:-3]+'sift' for i in range(nbr_images)]
with open('C:\\Users\\xqm\\PycharmProjects\\char77\\vocabulary.pkl', 'rb') as f:
voc = pickle.load(f)
src = imagesearch.Searcher('test.db', voc)
locs,descr = sift.read_features_from_file(featlist[0])
iw = voc.project(descr)
print 'try a query...'
print src.query(imlist[0])[:5]
这次控制台输出的结果,包含候选图像与查询图像间的距离。
与之前控制台输出的结果相比,此次显然更好。距离为0的图像对应查询图像本身;相似图像距离较近。
7.4.3 确定对比基准并绘制结果
为了评价搜索结果的好坏,我们可以计算前 4 个位置中搜索到相似图像数。将计算分数的函数添加到 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。
7.5 建立演示程序及web应用
7.5.1 用CherryPy创建web应用
为了建立这些演示程序,采用 CherryPy 包,参见 http://www.cherrypy.org。CherryPy 是一个纯 Python 轻量级 Web 服务器,使用面向对象模型。
7.5.2 图像搜索演示程序
# -*- coding: utf-8 -*-
import cherrypy
import pickle
import urllib
import os
from numpy import *
#from PCV.tools.imtools import get_imlist
from PCV.imagesearch import imagesearch
import random
"""
This is the image search demo in Section 7.6.
"""
class SearchDemo:
def __init__(self):
# 载入图像列表
self.path = 'first1000/'
#self.path = 'D:/python_web/isoutu/first500/'
self.imlist = [os.path.join(self.path,f) for f in os.listdir(self.path) if f.endswith('.jpg')]
#self.imlist = get_imlist('./first500/')
#self.imlist = get_imlist('E:/python/isoutu/first500/')
self.nbr_images = len(self.imlist)
print (self.imlist)
print (self.nbr_images)
self.ndx = list(range(self.nbr_images))
print (self.ndx)
# 载入词汇
# f = open('first1000/vocabulary.pkl', 'rb')
with open('first1000/vocabulary.pkl','rb') as f:
self.voc = pickle.load(f)
#f.close()
# 显示搜索返回的图像数
self.maxres = 10
# header and footer html
self.header = """
<!doctype html>
<head>
<title>Image search</title>
</head>
<body>
"""
self.footer = """
</body>
</html>
"""
def index(self, query=None):
self.src = imagesearch.Searcher('testImaAdd.db', self.voc)
html = self.header
html += """
<br />
Click an image to search. <a href='?query='> Random selection </a> of images.
<br /><br />
"""
if query:
# query the database and get top images
#查询数据库,并获取前面的图像
res = self.src.query(query)[:self.maxres]
for dist, ndx in res:
imname = self.src.get_filename(ndx)
html += "<a href='?query="+imname+"'>"
html += "<img src='"+imname+"' alt='"+imname+"' width='100' height='100'/>"
print (imname+"################")
html += "</a>"
# show random selection if no query
# 如果没有查询图像则随机显示一些图像
else:
random.shuffle(self.ndx)
for i in self.ndx[:self.maxres]:
imname = self.imlist[i]
html += "<a href='?query="+imname+"'>"
html += "<img src='"+imname+"' alt='"+imname+"' width='100' height='100'/>"
print (imname+"################")
html += "</a>"
html += self.footer
return html
index.exposed = True
cherrypy.quickstart(SearchDemo(), '/', config=os.path.join(os.path.dirname(__file__), 'service.conf'))
该类包含一个初始化 int() 方法和一个“索引”页面 index() 方法。这两个方法可以自动地映射至 URL,并且方法中的参数可以直接传递到 URL 中。index 方法里有一个查询参数,在本例中,该参数是查询图像,用来对其他图像排序。如果该参数是空的, 就会随机显示一些图像。
index.exposed = True
上面一行使索引 URL 可以被访问,上面 searchsemo.py 中紧接着该行的最后一行通过读取 service.conf 配置文件开启 CherryPy Web服务器。
打开浏览器,在地址栏输入http://127.0.0.1:8080/,你可以看到随机挑选出来的图像的初始页面: