(本文所使用的 Python 库和版本号: Python 3.6, Numpy 1.14, scikit-learn 0.19, matplotlib 2.2 )
视觉词袋模型 (Bag Of Visual Words,BOVW) 来源于自然语言处理中的词袋模型(Bag Of Words, BOW), 关于词袋模型, 可以参考我的博文[火炉炼 AI] 机器学习 038-NLP 创建词袋模型 https://www.cnblogs.com/RayDean/p/9767001.html . 在 NLP 中, BOW 的核心思想是将一个文档当做一个袋子, 里面装着各种各样的单词, 根据单词出现的频次或权重来衡量某个单词的重要性. BOW 的一个重要特性是不考虑单词出现的顺序, 句子语法等因素.
视觉词袋模型 BOVW 是将 BOW 的核心思想应用于图像处理领域的一种方法, 为了表示一幅图像, 我们可以将图像看做文档, 即若干个 "视觉单词" 的集合, 和 BOW 一样, 不考虑这些视觉单词出现的顺序, 故而 BOVW 的一个缺点是忽视了像素之间的空间位置信息(当然, 针对这个缺点有很多改进版本).BOVW 的核心思想可以从下图中看出一二.
有人要问了, 提取图像的特征方法有很多, 比如 SIFT 特征提取器, Star 特征提取器等, 为什么还要使用 BOVW 模型来表征图像了? 因为 SIFT,Star 这些特征提取器得到的特征矢量是多维的, 比如 SIFT 矢量是 128 维, 而且一幅图像通常会包含成百上千个 SIFT 矢量, 在进行下游机器学习计算时, 这个计算量非常大, 效率很低, 故而通常的做法是用聚类算法对这些特征矢量进行聚类, 然后用聚类中的一个簇代表 BOVW 中的一个视觉单词, 将同一幅图像的 SIFT 矢量映射到视觉视觉单词序列, 生成视觉码本, 这样, 每一幅图像都可以用一个视觉码本矢量来描述, 在后续的计算中, 效率大大提高, 有助于大规模的图像检索.
关于 BOVW 的更详细描述, 可以参考博文: 视觉词袋模型 BOW 学习笔记及 matlab 编程实现
1. 使用 BOVW 建立图像数据集
BOVW 主要包括三个关键步骤:
1, 提取图像特征: 提取算法可以使用 SIFT,Star,HOG 等方法, 比如使用 SIFT 特征提取器, 对数据集中的每一幅图像都使用 SIFT 后, 每一个 SIFT 特征用一个 128 维描述特征向量表示, 假如有 M 幅图像, 一共提取出 N 个 SIFT 特征向量.
2, 聚类得到视觉单词: 最常用的是 K-means, 当然可以用其他聚类算法, 使用聚类对N个 SIFT 特征向量进行聚类, K-means 会将 N 个特征向量分成 K 个簇, 使得每个簇内部的特征向量都具有非常高的相似度, 而簇间的相似度较低, 聚类后会得到 K 个聚类中心(在 BOVW 中, 聚类中心被称为视觉单词). 计算每一幅图像的每一个 SIFT 特征到这 K 个视觉单词的距离, 并将其映射到距离最近的一个簇中(即该视觉单词的对应词频 + 1). 这样, 每一幅图像都变成了一个与视觉单词相对应的词频矢量.
3, 构建视觉码本: 因为每一幅图像的 SIFT 特征个数不相等, 所以需要对这些词频矢量进行归一化, 将每幅图像的 SIFT 特征个数变为频数, 这样就得到视觉码本.
整个流程可以简单地用下图描述:
下面开始准备数据集, 首先从 Caltech256 图像抽取 3 类, 每一类随机抽取 20 张图片, 组成一个小型数据集, 每一个类别放在一个文件夹中, 且文件夹的命名以数字和 "-" 开头, 数字就表示类别名称. 这个小数据集纯粹是验证算法是否能跑通. 如下为准备的数据集:
首先来看第一步的代码: 提取图像特征的代码:
- def __img_sift_features(self,image):
- '''
- 提取图片 image 中的 Star 特征的关键点, 然后用 SIFT 特征提取器进行计算,
- 得到 N 行 128 列的矩阵, 每幅图中提取的 Star 特征个数不一样, 故而 N 不一样,
- 但是经过 SIFT 计算之后, 特征的维度都变成 128 维.
- 返回该 N 行 128 列的矩阵
- '''
- keypoints=xfeatures2d.StarDetector_create().detect(image)
- gray=cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
- _,feature_vectors=xfeatures2d.SIFT_create().compute(gray,keypoints)
- return feature_vectors
- 然后将得到的所有图片的 N 行 128 列特征集合起来, 组成 M 行 128 列特征, 构建一个聚类算法, 用这个算法来映射得到含有 32 个聚类中心 (视觉单词) 的模型, 将这 128 列特征映射到 32 个视觉单词中(由于此处 Kmeans 我使用 32 个簇, 故而得到 32 个视觉单词, 越复杂的项目, 这个值要调整的越大, 从几百到几千不等.), 在统计每一个特征出现的频次, 组成一个词袋模型, 如下代码:
- def __map_feature_to_cluster(self,img_path):
- '''从单张图片中提取 Star 特征矩阵(N 行 128 列),
- 再将该特征矩阵通过 K-means 聚类算法映射到 K 个类别中, 每一行特征映射到一个簇中, 得到 N 个簇标号的向量,
- 统计每一个簇中出现的特征向量的个数, 相当于统计词袋中某个单词出现的频次.
- '''
- img_feature_vectors=self.__img_sift_features(self.__get_image(img_path)) # N 行 128 列
- cluster_labels=self.cluster_model.predict(img_feature_vectors)
- # 计算这些特征在 K 个簇中的类别, 得到 N 个数字, 每个数字是 0-31 中的某一个, 代表该 Star 特征属于哪一个簇
- # eg [30 30 30 6 30 30 23 25 23 23 30 30 16 17 31 30 30 30 4 25]
- # 统计每个簇中特征的个数
- vector_nums=np.zeros(self.clusters_num) # 32 个元素
- for num in cluster_labels:
- vector_nums[num]+=1
- # 将特征个数归一化处理: 得到百分比而非个数
- sum_=sum(vector_nums)
- return [vector_nums/sum_] if sum_>0 else [vector_nums] # 一行 32 列, 32 个元素组成的 list
- 上面仅仅是用一部分图片来得到聚类中心, 没有用全部的图像, 因为部分图像完全可以代表全部图像.
- 第三步: 获取多张图片的视觉码本, 将这些视觉码本组成一个 P 行 32 列的矩阵.
- def __calc_imgs_clusters(self,img_path_list):
- '''获取多张图片的视觉码本, 将这些视觉码本组成一个 P 行 32 列的矩阵, P 是图片张数, 32 是聚类的类别数.
- 返回该 P 行 32 列的矩阵'''
- img_paths=list(itertools.chain(*img_path_list)) # 将多层 list 展开
- code_books=[]
- [code_books.extend(self.__map_feature_to_cluster(img_path)) for img_path in img_paths]
- return code_books
- 完整的准备数据集的代码比较长, 如下:
- # 准备数据集
- import cv2,itertools,pickle,os
- from cv2 import xfeatures2d
- from glob import glob
- class DataSet:
- def __init__(self,img_folder,cluster_model_path,img_ext='jpg',max_samples=12,clusters_num=32):
- self.img_folder=img_folder
- self.cluster_model_path=cluster_model_path
- self.img_ext=img_ext
- self.max_samples=max_samples
- self.clusters_num=clusters_num
- self.img_paths=self.__get_img_paths()
- self.all_img_paths=[list(item.values())[0] for item in self.img_paths]
- self.cluster_model=self.__load_cluster_model()
- def __get_img_paths(self):
- folders=glob(self.img_folder+'/*-*') # 由于图片文件夹的名称是数字 +'-'开头, 故而可以用这个来获取
- img_paths=[]
- for folder in folders:
- class_label=folder.split('\\')[-1]
- img_paths.append({class_label:glob(folder+'/*.'+self.img_ext)})
- # 每一个元素都是一个 dict,key 为文件夹名称, value 为该文件夹下所有图片的路径组成的 list
- return img_paths
- def __get_image(self,img_path,new_size=200):
- def resize_img(image,new_size):
- '''将 image 的长或宽中的最小值调整到 new_size'''
- h,w=image.shape[:2]
- ratio=new_size/min(h,w)
- return cv2.resize(image,(int(w*ratio),int(h*ratio)))
- image=cv2.imread(img_path)
- return resize_img(image,new_size)
- def __img_sift_features(self,image):
- '''
- 提取图片 image 中的 Star 特征的关键点, 然后用 SIFT 特征提取器进行计算,
- 得到 N 行 128 列的矩阵, 每幅图中提取的 Star 特征个数不一样, 故而 N 不一样,
- 但是经过 SIFT 计算之后, 特征的维度都变成 128 维.
- 返回该 N 行 128 列的矩阵
- '''
- keypoints=xfeatures2d.StarDetector_create().detect(image)
- gray=cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
- _,feature_vectors=xfeatures2d.SIFT_create().compute(gray,keypoints)
- return feature_vectors
- def __calc_imgs_features(self,img_path_list):
- '''获取多张图片的特征矢量, 这些特征矢量是合并到一起的, 最终组成 M 行 128 列的矩阵, 返回该矩阵.
- 此处的 M 是每张图片的特征矢量个数之和, 即 N1+N2+N3....'''
- img_paths=list(itertools.chain(*img_path_list)) # 将多层 list 展开
- feature_vectors=[]
- [feature_vectors.extend(self.__img_sift_features(self.__get_image(img_path))) for img_path in img_paths]
- return feature_vectors
- def __create_save_Cluster(self):
- '''由于 folders 中含有大量图片, 故而取一小部分 (max_samples) 图片来做 K-means 聚类.
- '''
- # 获取要进行聚类的小部分图片的路径
- cluster_img_paths=[list(item.values())[0][:self.max_samples] for item in self.img_paths]
- feature_vectors=self.__calc_imgs_features(cluster_img_paths)
- cluster_model = KMeans(self.clusters_num, # 建立聚类模型
- n_init=10,
- max_iter=10, tol=1.0)
- cluster_model.fit(feature_vectors) # 对聚类模型进行训练
- # 将聚类模型保存, 以后就不需要再训练了.
- with open(self.cluster_model_path,'wb+') as file:
- pickle.dump(cluster_model,file)
- print('cluster model is saved to {}.'.format(self.cluster_model_path))
- return cluster_model
- def __map_feature_to_cluster(self,img_path):
- '''从单张图片中提取 Star 特征矩阵(N 行 128 列),
- 再将该特征矩阵通过 K-means 聚类算法映射到 K 个类别中, 每一行特征映射到一个簇中, 得到 N 个簇标号的向量,
- 统计每一个簇中出现的特征向量的个数, 相当于统计词袋中某个单词出现的频次.
- '''
- img_feature_vectors=self.__img_sift_features(self.__get_image(img_path)) # N 行 128 列
- cluster_labels=self.cluster_model.predict(img_feature_vectors)
- # 计算这些特征在 K 个簇中的类别, 得到 N 个数字, 每个数字是 0-31 中的某一个, 代表该 Star 特征属于哪一个簇
- # eg [30 30 30 6 30 30 23 25 23 23 30 30 16 17 31 30 30 30 4 25]
- # 统计每个簇中特征的个数
- vector_nums=np.zeros(self.clusters_num) # 32 个元素
- for num in cluster_labels:
- vector_nums[num]+=1
- # 将特征个数归一化处理: 得到百分比而非个数
- sum_=sum(vector_nums)
- return [vector_nums/sum_] if sum_>0 else [vector_nums] # 一行 32 列, 32 个元素组成的 list
- def __calc_imgs_clusters(self,img_path_list):
- '''获取多张图片的视觉码本, 将这些视觉码本组成一个 P 行 32 列的矩阵, P 是图片张数, 32 是聚类的类别数.
- 返回该 P 行 32 列的矩阵'''
- img_paths=list(itertools.chain(*img_path_list)) # 将多层 list 展开
- code_books=[]
- [code_books.extend(self.__map_feature_to_cluster(img_path)) for img_path in img_paths]
- return code_books
- def __load_cluster_model(self):
- '''从 cluster_model_path 中加载聚类模型, 返回该模型, 如果不存在或出错, 则调用函数准备聚类模型'''
- cluster_model=None
- if os.path.exists(self.cluster_model_path):
- try:
- with open(self.cluster_model_path, 'rb') as f:
- cluster_model = pickle.load(f)
- except:
- pass
- if cluster_model is None:
- print('No valid model found, start to prepare model...')
- cluster_model=self.__create_save_Cluster()
- return cluster_model
- def get_img_code_book(self,img_path):
- '''获取单张图片的视觉码本, 即一行 32 列的 list, 每个元素都是对应特征出现的频率'''
- return self.__map_feature_to_cluster(img_path)
- def get_imgs_code_books(self,img_path_list):
- '''获取多张图片的视觉码本, 即 P 行 32 列的 list, 每个元素都是对应特征出现的频率'''
- return self.__calc_imgs_clusters(img_path_list)
- def get_all_img_code_books(self):
- '''获取 img_folder 中所有图片的视觉码本'''
- return self.__calc_imgs_clusters(self.all_img_paths)
- def get_img_labels(self):
- '''获取 img_folder 中所有图片对应的 label, 可以从文件夹名称中获取'''
- img_paths=list(itertools.chain(*self.all_img_paths))
- return [img_path.rpartition('-')[0].rpartition('\\')[2] for img_path in img_paths]
- def prepare_dataset(self):
- '''获取 img_folder 中所有图片的视觉码本和 label, 构成数据集'''
- features=self.get_all_img_code_books()
- labels=self.get_img_labels()
- return np.c_[features,labels]
2. 使用极端随机森林建立模型
极端随机森林是随机森林算法的一个提升版本, 可以参考我以前的文章[火炉炼 AI] 机器学习 007 - 用随机森林构建共享单车需求预测模型 https://www.cnblogs.com/RayDean/p/9764648.html . 使用方法和随机森林几乎一样.
- # 极端随机森林分类器
- from sklearn.ensemble import ExtraTreesClassifier
- class CLF_Model:
- def __init__(self,n_estimators=100,max_depth=16):
- self.model=ExtraTreesClassifier(n_estimators=n_estimators,
- max_depth=max_depth, random_state=12)
- def fit(self,train_X,train_y):
- self.model.fit(train_X,train_y)
- def predict(self,newSample_X):
- return self.model.predict(newSample_X)
其实, 这个分类器很简单, 没必要写成类的形式.
对该分类器进行训练:
- dataset_df=pd.read_csv('./prepared_set.txt',index_col=[0])
- dataset_X,dataset_y=dataset_df.iloc[:,:-1].values,dataset_df.iloc[:,-1].values
- model=CLF_Model()
- model.fit(dataset_X,dataset_y)
3. 使用训练后模型预测新样本
如下, 我随机测试三张图片, 均得到了比较好的结果.
- # 用训练好的 model 预测新图片, 看看它属于哪一类
- new_img1='E:\PyProjects\DataSet\FireAI/test0.jpg'
- img_code_book=dataset.get_img_code_book(new_img1)
- predicted=model.predict(img_code_book)
- print(predicted)
- new_img2='E:\PyProjects\DataSet\FireAI/test1.jpg'
- img_code_book=dataset.get_img_code_book(new_img2)
- predicted=model.predict(img_code_book)
- print(predicted)
- new_img3='E:\PyProjects\DataSet\FireAI/test2.jpg'
- img_code_book=dataset.get_img_code_book(new_img3)
- predicted=model.predict(img_code_book)
- print(predicted)
------------------------------------- 输 --------- 出 --------------------------------
[0] [1] [2]
-------------------------------------------- 完 -------------------------------------
######################## 小 ********** 结 ###############################
1, 这个项目的难点在于视觉词袋模型的理解和数据集准备, 所以我将其写成了类的形式, 这个类具有一定的通用性, 可以用于其他项目数据集的制备.
2, 从这个项目可以看出视觉词袋模型相对于原始的 Star 特征的优势: 如果使用原来的 Star 特征, 一张图片会得到 N 行 128 列的特征数, 而使用了 BOVW 模型, 我们将 N 行 128 列的特征数据映射到 1 行 32 列的空间中, 所以极大的降低了特征数, 使得模型简化, 训练和预测效率提高.
3, 一旦准备好了数据集, 就可以用各种常规的机器学习分类器进行分类, 也可以用各种方法评估该分类器的优劣, 比如性能报告, 准确率, 召回率等, 由于这部分我在前面的文章中已经讲过多次, 故而此处省略.
来源: https://juejin.im/post/5bcf2094e51d457a765bd05a