前言
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
想象一下下面两种情况:
在国际新闻中以“美国”为关键词进行搜索:搜索结果数量纷繁。
在国际新闻中以“贸易战”为关键词进行搜索:搜索的结果基本都和近年“中美贸易战”密切相关。
如果我们的目标是查询“2018年中美贸易战”有关的新闻报道,那么“贸易战”是比“美国”更重要的关键词。这是因为“美国”在几乎所有的文章中都频繁出现,而“贸易战”只在贸易战有关的新闻中频繁出现。
为此我们使用 TI-IDF进行评估。TF(term frequency)是某一个词语在一个文件中出现的频率,即词频。IDF(inverse document frequency,逆向文件频率)则衡量一个词语在所有文档中出现的频率。TF-IDF计算方法为:
T F = 文档中该词数量 文档总词数 TF=\frac{文档中该词数量}{文档总词数} T F = 文 档 总 词 数 文 档 中 该 词 数 量
I D F = l g 文档总数 包含该词的文档数 IDF= lg\frac{文档总数}{包含该词的文档数} I D F = l g 包 含 该 词 的 文 档 数 文 档 总 数
$TFIDF= TF\times IDF $
只有在特定文件中高,但是在整体文件中低的词语,才能取得较高的TF-IDF值。
我们使用 TF-IDF 加权以后的特征来进行搜索以提高准确率。
程序的基本结构
有关功能基均有库可调,下面简单叙述我的程序中的各个函数。
get_dictionary(),读入图片,用 opencv 计算 SIFT特征。取特征的描述子使用 sklearn 中 KMeans 聚类,取聚类中心代表类。完成这一步,就相当于我们已经造好了“袋子”并找到了“词”。
corel_bof():遍历前面保存的词,判断各个词最近的袋子是什么。并把结果存入一个 numpy.histogram 直方图中。这就相当于我们已经把词扔进了袋子,以后就可以用直方图代表此图片了。
tf_idf():使用 sklearn 对直方图进行 TF-IDF 加权。
以上的步骤耗时较长,而且一经处理不再变动。因此可以将其导出为 JSON 或其他文件,方便后续使用。
搜索时则:
search_similar():对搜索图片同样进行上述三步处理。得到结果使用 sklearn 中 cosine_similarity计算其与我们的数据集的 TI-IDF 的余弦相似度。对其排序后返回最相近结果的前 K+1 个。因为我们的图片均来自数据集,因此最像的肯定是自己。
show_result:使用 pillow 显示最像的前 K 个图片。
程序中的一些小点
读入图片可以使用 glob.glob() 递归查找文件目录下的所有图片。
SIFT 的结果只要其中的描述子,这是因为关键点仅包含坐标信息,描述子才真正的表示了一个特征。我们所有的操作都是对描述子进行的。
cv2.imread 默认读入彩图是 BGR 格式,这会导致图片泛蓝。搜索和计算时这并不怎么影响,但是输出时就需要转为 RGB 格式或者直接用 pillow 的 Image.open()。
各个函数的格式要求不一,所以能见到反复转格式。特别是导出为 JSON 时需要转 NDArray 为 list。因为 JSON 是内置的库,但是 numpy 可不是。
试着使用了# 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 npimport cv2import globimport jsonfrom PIL import Imagefrom scipy.cluster.vq import vqfrom sklearn.cluster import KMeansfrom sklearn.metrics.pairwise import cosine_similarityfrom sklearn.feature_extraction.text import TfidfTransformercorel_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 = [] 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 _, des = cv2.SIFT_create().detectAndCompute(img, None ) features.append(des) all_features = np.vstack(features) k = 64 kmeans = KMeans(n_clusters=k, max_iter=300 , n_init=10 ) kmeans.fit(all_features) dictionary = kmeans.cluster_centers_ if (mode == "save" ): with open (dict_file_path, 'w' ) as f: 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" ) code, _ = vq(des, dictionary) hist, _ = np.histogram(code, bins=range (dictionary.shape[0 ]+1 )) hist = hist/np.sum (hist) 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\\ 在这篇“文章”中出现得多的且其他文章出现得少的“词”,才是能标明它身份的“词”\\ 大部分人都有的特征不算\\ """ 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 def searchSimilar (queryIMG, dictionary ): _, 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() 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__' : file_op = "load" 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)
参考资料