(本文所使用的Python庫和版本號: Python 3.6, Numpy 1.14, scikit-learn 0.19, matplotlib 2.2 )html
視覺詞袋模型(Bag Of Visual Words,BOVW)來源於天然語言處理中的詞袋模型(Bag Of Words, BOW),關於詞袋模型,能夠參考個人博文【火爐煉AI】機器學習038-NLP建立詞袋模型.在NLP中,BOW的核心思想是將一個文檔當作一個袋子,裏面裝着各類各樣的單詞,根據單詞出現的頻次或權重來衡量某個單詞的重要性。BOW的一個重要特性是不考慮單詞出現的順序,句子語法等因素。git
視覺詞袋模型BOVW是將BOW的核心思想應用於圖像處理領域的一種方法,爲了表示一幅圖像,咱們能夠將圖像看作文檔,即若干個「視覺單詞」的集合,和BOW同樣,不考慮這些視覺單詞出現的順序,故而BOVW的一個缺點是忽視了像素之間的空間位置信息(固然,針對這個缺點有不少改進版本)。BOVW的核心思想能夠從下圖中看出一二。github
有人要問了,提取圖像的特徵方法有不少,好比SIFT特徵提取器,Star特徵提取器等,爲何還要使用BOVW模型來表徵圖像了?由於SIFT,Star這些特徵提取器獲得的特徵矢量是多維的,好比SIFT矢量是128維,並且一幅圖像一般會包含成百上千個SIFT矢量,在進行下游機器學習計算時,這個計算量很是大,效率很低,故而一般的作法是用聚類算法對這些特徵矢量進行聚類,而後用聚類中的一個簇表明BOVW中的一個視覺單詞,將同一幅圖像的SIFT矢量映射到視覺視覺單詞序列,生成視覺碼本,這樣,每一幅圖像均可以用一個視覺碼本矢量來描述,在後續的計算中,效率大大提升,有助於大規模的圖像檢索。算法
關於BOVW的更詳細描述,能夠參考博文:視覺詞袋模型BOW學習筆記及matlab編程實現編程
BOVW主要包括三個關鍵步驟:app
1,提取圖像特徵:提取算法可使用SIFT,Star,HOG等方法,好比使用SIFT特徵提取器,對數據集中的每一幅圖像都使用SIFT後,每個SIFT特徵用一個128維描述特徵向量表示,假若有M幅圖像,一共提取出N個SIFT特徵向量。dom
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]
複製代碼
極端隨機森林是隨機森林算法的一個提高版本,能夠參考我之前的文章【火爐煉AI】機器學習007-用隨機森林構建共享單車需求預測模型.使用方法和隨機森林幾乎同樣。
# 極端隨機森林分類器
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)
複製代碼
以下,我隨機測試三張圖片,均獲得了比較好的結果。
# 用訓練好的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,一旦準備好了數據集,就能夠用各類常規的機器學習分類器進行分類,也能夠用各類方法評估該分類器的優劣,好比性能報告,準確率,召回率等,因爲這部分我在前面的文章中已經講過屢次,故而此處省略。
#################################################################
注:本部分代碼已經所有上傳到(個人github)上,歡迎下載。
參考資料:
1, Python機器學習經典實例,Prateek Joshi著,陶俊傑,陳小莉譯