目录
一、基于内容的图像检索
CBIR(Content-Based Image Retrieval,基于内容的图像检索)技术用于检索在视觉上具相似性的图像。这样返回的图像可以是颜色相似、纹理相似、图像中的物体或场景相似。
矢量空间模型是一个用于表示和搜索文本文档的模型,其从文本挖掘中获取灵感,它可以应用于任何对象类型,包括图像。该模型也被称为 BOW 表示模型。通过单词计数来构建文档直方图向量 v,从而建立文档索引。通常会在单词计数时忽略掉一些常用词,这些常用词称为停用词;对于每篇文档长度需要除以直方图总和将向量归一化为单位长度;对于直方图向量中的每个元素,一般根据每个单词的重要性来赋予相应的权重。
最常用的权重是 tf-idf(词频 - 逆向文档频率 ),单词 w 在文档 d 中的词频是:
逆向文档频率为:
两者相乘可以得到矢量v中对应元素得tf-idf权重。
二、视觉单词
为了将文本挖掘技术应用到图像中,首先需要建立视觉等效单词,可以采用 SIFT 局部描述子来实现。其思想为:将描述子空间量化成一些典型实例,并将图像中的每个描述子指派到其中的某个实例中。这些典型实例可以通过分析训练图像集确定,并被视为视觉单词。所有这些视觉单词构成的集合称为视觉词汇,有时也称为视觉码本。
从一个集提取特征描述子,利用一些聚类算法可以构建出视觉单词,其中最常用得聚类方法为K-means。用视觉单词直方图来表示图像,则该模型便称为 BOW 模型。
创建词汇
其实现代码首先定义了一个vocabulary类文件:
import numpy as np
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):
nbr_images = len(featurefiles)
# 从文件中读取特征
descr = []
descr.append(sift.read_features_from_file(featurefiles[0])[1])
descriptors = descr[0] # 将所有的特征并在一起,以便后面进行 K-means 聚类
for i in np.arange(1,nbr_images):
descr.append(sift.read_features_from_file(featurefiles[i])[1])
descriptors = np.vstack((descriptors,descr[i]))
# K-means: 最后一个参数决定运行次数
self.voc,distortion = kmeans(descriptors[::subsampling,:],k,1)
self.nbr_words = self.voc.shape[0]
# 遍历所有的训练图像,并投影到词汇上
imwords = np.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 = np.log( (1.0*nbr_images) / (1.0*nbr_occurences+1) )
self.trainingdata = featurefiles
def project(self,descriptors):
# 图像单词直方图
imhist = np.zeros((self.nbr_words))
words,distance = vq(descriptors,self.voc)
for w in words:
imhist[w] += 1
return imhist
之后从图像提取出的描述子,并将每幅图像的描述子保存在一个文件里面:
import pickle
import os
from PIL import Image
from matplotlib import pyplot as plt
import numpy as np
from scipy.cluster.vq import *
import vocabulary
import sift
def get_imlist(path):
return [os.path.join(path,f) for f in os.listdir(path) if f.endswith('.jpg')]
imlist = get_imlist(r'D:\test\ukbench\use')
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])
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)
结果为:
最终可以得到保存了整个词汇对象的pkl文件以便后续使用。
三、图像索引
1.建立数据库
在索引图像前,需要建立一个数据库。因为对图像进行索引就是从这些图像中提取描述子,利用词汇将描述子转换成视觉单词,并保存视觉单词及对应图像的单词直方图。从而可以利用图像对数据库进行查询,并返回相似的图像作为搜索结果。这里使用的是SQLite作为数据库,python3已经内置了sqlite,不需要额外安装,直接导入使用即可。其使用的代码为:
import pickle
import sqlite3
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):
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()
2.添加图像
接着就是要在索引中添加图像,
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):
im = self.con.execute("select rowid from imlist where filename='%s'" % imname).fetchone()
return im != None
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]
第一个方法是获取图像文件名与 Numpy 数组,该数组包含的是在图像找到的描述子;第二个方法is_indxed() 用来检查图像是否已经被索引,第三个方法get_id() 则对一幅图像文件名给定 id 号。
由于 SQLite 的数据库在存储对象或数组时并没有一个标准类型。使用 Pickle 的 dumps() 函数创建一个字符串表示,并将其写入数据库。因此,从数据库读取数据时,需要拆封该字符串。
接着就是遍历数据库中的样本图像,并将其加入到索引中去。其实现为:
import os
import pickle
import sift
import imagesearch
def get_imlist(path):
return [os.path.join(path,f) for f in os.listdir(path) if f.endswith('.jpg')]
imlist = get_imlist(r'D:\test\ukbench\use')
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(r'D:\test\ukbench\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()
接着可以使用如下代码来验证是否保存数据库成功:
import sqlite3
con = sqlite3.connect(r'D:\test\ukbench\test.db')
print (con.execute('select count (filename) from imlist').fetchone())
print (con.execute('select * from imlist').fetchone())
运行结果为:
可以看到该数据库中一种有1000条数据,或者也可以使用数据库操作软件Navicat查看该数据库的信息:
四、在数据库中搜索图像
当建立好图像的索引后,就可以在数据库中搜索相似的图像了,这里用 BoW (Bag-of-Word,词袋模型)来表示整个图像。首先需要重新创建一个search类:
class Searcher(object):
def __init__(self,db,voc):
self.con = sqlite3.connect(db)
self.voc = voc
def __del__(self):
self.con.close()
单词索引的作用在于:可以利用单词索引获得候选集,然后只需在候选集上进行逐一比较。而不需要逐一比较整个数据库中的所有直方图,因为若图像数据库很大,那么比较直方图不可行。
1.利用索引获取候选图像
def candidates_from_word(self,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):
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 = sorted(tmp, key=lambda x: x[1], reverse=True)
# 返回排序后的列表,最匹配的排在最前面
return [w[0] for w in tmp]
上面的第一个函数会给出包含特定单词的所有图像 id 号,第二个函数从图像单词直方图的非零项创建单词 id 列表,检索每个单词获得候选集并将其合并到 candidates 列表中,然后创建一个元组列表每个元组由单词 id 和次数 count 构成,其中次数 count 是候选列表中每个单词出现的次数。
可以使用如下代码进行验证:
import os
import pickle
import sift
import imagesearch
def get_imlist(path):
return [os.path.join(path,f) for f in os.listdir(path) if f.endswith('.jpg')]
imlist = get_imlist(r'D:\test\ukbench\use')
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(r'D:\test\ukbench\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])
可以得到如下结果:
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()
# import pdb; pdb.set_trace()
return pickle.loads(s[0])
def query(self,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 = np.sqrt(sum(self.voc.idf*(h-cand_h)**2))
matchscores.append((cand_dist,imid))
matchscores.sort()
return matchscores
src = imagesearch.Searcher(r'D:\test\ukbench\test.db', voc)
print ('try a query...')
print (src.query(imlist[0])[:10])
可以得到结果为:
其中的query()方法可以获取图像的文件名,检索其单词直方图及候选图像列表。其中的结果包括候选图像与查询图像间的距离。
3.确定对比基准并绘制结果
下面的第一个函数是获得搜索的前 4 个结果,并计算分数,该分数为上评价搜索性能常采用的评价方式。分数为 4 时结果最理想;没有一个是准确的,分数为 0;仅检索到相同图像时,分数为 1;找到相同的图像并且其他三个中的两个相同时,分数为 3。
def compute_ukbench_score(src,imlist):
nbr_images = len(imlist)
pos = np.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 = np.array([ (pos[i]//4)==(i//4) for i in range(nbr_images)])*1.0
return sum(score) / (nbr_images)
def plot_results(src,res):
plt.figure()
nbr_results = len(res)
for i in range(nbr_results):
imname = src.get_filename(res[i])
plt.subplot(1,nbr_results,i+1)
plt.imshow(np.array(Image.open(imname)))
plt.axis('off')
plt.show()
imagesearch.compute_ukbench_score(src,imlist[:100])
print(imagesearch.compute_ukbench_score(src,imlist[:100]))
nbr_results = 6
res = [w[1] for w in src.query(imlist[20])[:nbr_results]]
imagesearch.plot_results(src,res)
结果为:
在该数据集上用一些查询图像进行搜索给出的一些结果。最左边的是查询图像,后面是检索到的前 5 幅图像。
五、使用几何特性对结果排序
BoW 模型的一个主要缺点是在用视觉单词表示图像时不包含图像特征的位置信息,这是为获取速度和可伸缩性而付出的代价。利用一些考虑到特征几何关系的准则重排搜索到的靠前结果,可以提高准确率。常用的方法是在查询图像与靠前图像的特征位置间拟合单应性。
下面是一个载入所有模型文件并用单应性对靠前的图像进行重排的示例:
import os
import pickle
import sift
import imagesearch
from sqlite3 import dbapi2 as sqlite
from PCV.geometry import homography
def get_imlist(path):
return [os.path.join(path,f) for f in os.listdir(path) if f.endswith('.jpg')]
# load image list and vocabulary
#载入图像列表
imlist = get_imlist(r'D:\test\ukbench\use')
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(r'D:\test\ukbench\test.db',voc)
# index of query image and number of results to return
#查询图像索引和查询返回的图像数
q_ind = 26
nbr_results = 10
# 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]) #重排后的结果
结果为:
第二张图片是没有常规查询的结果,第三张图片是重排后的结果。
六、建立演示程序及Web应用
首先需要在自己的python环境中安装CherryPy包,这里使用的是pip的清华源进行下载的。之后新建一个用于建立网页的页面,添加如下代码:
import cherrypy
import pickle
import os
import random
import imagesearch
class SearchDemo:
def __init__(self):
# 载入图像列表
self.path = r'D:/test/ukbench/use'
self.imlist = [os.path.join(self.path, f) for f in os.listdir(self.path) if f.endswith('.jpg')]
self.nbr_images = len(self.imlist)
self.ndx = list(range(self.nbr_images))
with open('vocabulary.pkl', 'rb') as f:
self.voc = pickle.load(f)
# 显示搜索返回的图像数
self.maxres = 15
# 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(r'D:/test/ukbench/test.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)
# Use the basename of the image file for the URL
html += "<a href='?query="+imname+"'>"
html += "<img src='/"+os.path.basename(imname)+"' alt='"+imname+"' width='100' height='100'/>"
html += "</a>"
else:
random.shuffle(self.ndx)
for i in self.ndx[:self.maxres]:
imname = self.imlist[i]
# Use the basename of the image file for the URL
html += "<a href='?query="+imname+"'>"
html += "<img src='/"+os.path.basename(imname)+"' alt='"+imname+"' width='100' height='100'/>"
html += "</a>"
html += self.footer
return html
index.exposed = True
# CherryPy 配置
if __name__ == '__main__':
cherrypy.config.update({
'server.socket_host': '127.0.0.1',
'server.socket_port': 8080,
'server.thread_pool': 50,
'tools.sessions.on': True,
'tools.staticdir.root': os.path.abspath(r'D:/test/ukbench/use'),
})
# 设置静态文件处理器
cherrypy.tree.mount(SearchDemo(), '/', config={
'/': {
'tools.staticdir.on': True,
'tools.staticdir.dir': ''
}
})
cherrypy.engine.start()
cherrypy.engine.block()
之后还需要配置其配置文件信息,在同级目录下新建一个service.conf文件,输入如下代码:
[global]
server.socket_host = "127.0.0.1"
server.socket_port = 8080
server.thread_pool = 50
tools.sessions.on = True
[/]
tools.staticdir.root = "D:/test/ukbench/use/"
tools.staticdir.on = True
tools.staticdir.dir = ""
之后就可以直接在网页使用127.0.0.1:8080进入打开的网页了:
点击一幅图像进行查询,会显示出搜索出来的前几幅图像,在搜索出来的图像中单击某图像可以开始新的查询。此外,页面上有一个链接,点击后可以返回原来随机选择的状态。