多媒体实验3:基于 BOF 进行相似图片搜索

前言

BOF算法是一种通过特征来实现图像检索或分类的算法。

目的/要求

使用BOF算法,在数据集上实现以图搜图:即输入数据集中某一张图,在剩下的999张图里搜索最邻近的10张图。数据集是按文件夹放好的 10 * 100 张 jpg 图片。

什么是BOF

BOF,bag of features 是由自然语言处理领域的 BOW(bag of words)引申而来的。BOW,用一个袋子装起了一个个词,顾名思义就是用(关键)词为单位去处理文章句子。引申到图像领域,就用特征(feature)代替了关键词(words),即通过处理特征来处理图像。

具体地说,BOF的思想是这样的:每个图像的特征多少不一而足,我们将这些特征进行归类,就可以把一个的图像转化“有多少个xx”类特征这样的表述,从而方便计算和表述。这个过程就像是把图像的特征扔进一个个袋子里分类,即 bag of features。

而这就有了三个子问题:1.特征哪来?2.袋子哪儿来?3.咋扔?

特征的来源很多,我们可以使 SIFT 特征,但是使用其他的特征也并非不可,例如 HOG 等。

袋子哪儿来?袋子并不是凭空出现的。而是根据数据集中的特征进行归类归出来的。这是一个把数据归类的问题,我们可以使用 KMeans 聚类算法把所有的数据归为人指定的 K 类。(K的值可能需要不断调整到最佳值)

扔进袋就是给一个查询点,将其放到最近的袋子中。我们可以使用 scipy 中 vq 来完成这一步。

这样,我们就完成了对一个数据集的初步 BOF 处理。

不过,为了更准确地进行搜索,我们还引入了 TD-IDF 对“单词”进行加权。

TI-IDF

想象一下下面两种情况:

  1. 在国际新闻中以“美国”为关键词进行搜索:搜索结果数量纷繁。
  2. 在国际新闻中以“贸易战”为关键词进行搜索:搜索的结果基本都和近年“中美贸易战”密切相关。

如果我们的目标是查询“2018年中美贸易战”有关的新闻报道,那么“贸易战”是比“美国”更重要的关键词。这是因为“美国”在几乎所有的文章中都频繁出现,而“贸易战”只在贸易战有关的新闻中频繁出现。

为此我们使用 TI-IDF进行评估。TF(term frequency)是某一个词语在一个文件中出现的频率,即词频。IDF(inverse document frequency,逆向文件频率)则衡量一个词语在所有文档中出现的频率。TF-IDF计算方法为:

  1. TF=文档中该词数量文档总词数TF=\frac{文档中该词数量}{文档总词数}
  2. IDF=lg文档总数包含该词的文档数IDF= lg\frac{文档总数}{包含该词的文档数}
  3. $TFIDF= TF\times IDF $

只有在特定文件中高,但是在整体文件中低的词语,才能取得较高的TF-IDF值。

我们使用 TF-IDF 加权以后的特征来进行搜索以提高准确率。

程序的基本结构

有关功能基均有库可调,下面简单叙述我的程序中的各个函数。

  1. get_dictionary(),读入图片,用 opencv 计算 SIFT特征。取特征的描述子使用 sklearn 中 KMeans 聚类,取聚类中心代表类。完成这一步,就相当于我们已经造好了“袋子”并找到了“词”。
  2. corel_bof():遍历前面保存的词,判断各个词最近的袋子是什么。并把结果存入一个 numpy.histogram 直方图中。这就相当于我们已经把词扔进了袋子,以后就可以用直方图代表此图片了。
  3. tf_idf():使用 sklearn 对直方图进行 TF-IDF 加权。

以上的步骤耗时较长,而且一经处理不再变动。因此可以将其导出为 JSON 或其他文件,方便后续使用。
搜索时则:

  1. search_similar():对搜索图片同样进行上述三步处理。得到结果使用 sklearn 中 cosine_similarity计算其与我们的数据集的 TI-IDF 的余弦相似度。对其排序后返回最相近结果的前 K+1 个。因为我们的图片均来自数据集,因此最像的肯定是自己。
  2. show_result:使用 pillow 显示最像的前 K 个图片。

程序中的一些小点

  1. 读入图片可以使用 glob.glob() 递归查找文件目录下的所有图片。
  2. SIFT 的结果只要其中的描述子,这是因为关键点仅包含坐标信息,描述子才真正的表示了一个特征。我们所有的操作都是对描述子进行的。
  3. cv2.imread 默认读入彩图是 BGR 格式,这会导致图片泛蓝。搜索和计算时这并不怎么影响,但是输出时就需要转为 RGB 格式或者直接用 pillow 的 Image.open()。
  4. 各个函数的格式要求不一,所以能见到反复转格式。特别是导出为 JSON 时需要转 NDArray 为 list。因为 JSON 是内置的库,但是 numpy 可不是。
  5. 试着使用了# region 来折叠代码,不过这不同于 C#,这并不是 python 本身提供的支持。我仅在 VScode 上验证可用。

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
import numpy as np
import cv2
import glob
import json
from PIL import Image
from scipy.cluster.vq import vq
from sklearn.cluster import KMeans
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.feature_extraction.text import TfidfTransformer

# debug
corel_path = './multi/corel/'
query_img_path = './multi/query.jpg'
dict_file_path = "./multi/dictionary.json"
bof_file_path = "./multi/bof.json"
tfidf_file_path = "./multi/tf-idf.json"

bag_of_features = []
features = []
tfidf_feature = []


# region preProcess
def get_dictionary(mode):
""" 根据图像生成特征字典:一共有这么多的特征。\\
包含SIFT和KMeans两步
"""
if (mode == "load"):
with open(dict_file_path, 'r') as f:
dictionary = np.array(json.load(f))
return dictionary

progress = 0
for imgpath in glob.glob(corel_path+'*/*.jpg'):
img = cv2.imread(imgpath)
# 显示进度
print("load a pic success:"+(progress+1).__str__()+"/"+"100")
progress += 1

# 获取img的所有描述子,不需要关键点
_, des = cv2.SIFT_create().detectAndCompute(img, None)

features.append(des)

# vertical stack,垂直堆叠二维数组
all_features = np.vstack(features)

# KMeans对象对 all_features 聚类
k = 64
kmeans = KMeans(n_clusters=k, max_iter=300, n_init=10)
kmeans.fit(all_features)

# 词典即聚类中心
# k*128的二维数组
dictionary = kmeans.cluster_centers_

if (mode == "save"):
with open(dict_file_path, 'w') as f:
# ndarray不能直接输出为json
json.dump(dictionary.tolist(), f)

return dictionary


def corel_bof(dictionary, mode):
""" 表示出每个图片包含特征字典中的哪些特征"""
bag_of_features = []
if (mode == 'load'):
with open(bof_file_path, 'r') as f:
bag_of_features = np.array(json.load(f))
return bag_of_features

# 遍历所有描述子
for des in features:
print("gennerated a des\n")
# 使用vq获得图片每个描述子最近的聚类中心(属于哪个聚类中心)
code, _ = vq(des, dictionary)

# 生成到直方图并归一化
# .shape[0]聚类中心的数量
hist, _ = np.histogram(code, bins=range(dictionary.shape[0]+1))
hist = hist/np.sum(hist)

# 添加到列表中1000*64维
bag_of_features.append(hist)

if (mode == "save"):
with open(bof_file_path, 'w') as f:
json.dump([arr.tolist() for arr in bag_of_features], f)
return bag_of_features


def tf_idf():
""""
计算TF-IDF\\
TF值 = 词在文档中出现的次数 / 文档总词数\\
逆向文件频率:IDF值 = log(语料库中包含某个词的文档总数 / 语料库中文档总数)\\
每个词在每篇文档中的TF-IDF值:TF值*IDF\\
在这篇“文章”中出现得多的且其他文章出现得少的“词”,才是能标明它身份的“词”\\
大部分人都有的特征不算\\
"""

# list[NDArray]->NDArray->稀疏矩阵->NDArray->list
# 通过NDArray转化为list以输出
tfidf_feature = TfidfTransformer().fit_transform(
np.array(bag_of_features)).toarray().tolist()

with open(tfidf_file_path, 'w') as f:
json.dump(tfidf_feature, f)
return tfidf_feature

# endregion


def searchSimilar(queryIMG, dictionary):

# 对单个图像的ti-idf处理
_, m_des = cv2.SIFT_create().detectAndCompute(queryIMG, None)
query_code, _ = vq(m_des, dict)
query_hist, _ = np.histogram(query_code, bins=range(dictionary.shape[0]+1))
query_hist = query_hist/np.sum(query_hist)
query_hist = np.array(query_hist).reshape(1, -1)
query_tfidf = TfidfTransformer()
query_tfidf = query_tfidf.fit_transform(query_hist).toarray()

# reshape(1, -1):转化为长度为一行的二维数组
# 返回1*1000的二维矩阵similarity
similarity = cosine_similarity(query_tfidf.reshape(1, -1), tfidf_feature)
top10_indices = np.argsort(similarity[0])[-11:][::-1]
print(top10_indices)
return top10_indices


def show_result(result_array):
imgs = []
total_width = 0
max_height = 0
for idx in result_array:
img_path = corel_path+str(idx//100)+"/"+str(idx)+".jpg"
img = Image.open(img_path)
imgs.append(img)
total_width += img.width
max_height = max(max_height, img.height)

# 创建拼接后的图像
result_image = Image.new('RGB', (total_width, max_height))

# 拼接所有图像
x_offset = 0
for image in imgs:
result_image.paste(image, (x_offset, 0))
x_offset += image.size[0]

# 显示拼接后的图像
result_image.show()


if __name__ == '__main__':

# load or save
file_op = "load"

# 字典、bof和tf-idf的获取
dict = get_dictionary(mode=file_op)
bag_of_features = corel_bof(dict, mode=file_op)
tfidf_feature = tf_idf()

# 查询
query = cv2.imread(query_img_path, cv2.IMREAD_COLOR)
result = searchSimilar(query, dict)

# 结果
show_result(result)

参考资料

  • new bing 你是我爹