在圖像檢索時,通常首先提取圖像的局部特征,這些局部特征通常有很高的維度(例如,sift是128維),有很多的冗余信息,直接利用局部特征進行檢索,效率和准確度上都不是很好。這就需要重新對提取到的局部特征進行編碼,以便於匹配檢索。
常用的局部特征編碼方法有三種:
- BoF
- VLAD
- FV
本文主要介紹基於k-means聚類算法的BoF的實現。
- BoF的原理
- k均值聚類概述
- 使用OpenCV實現的BoF
BoF
該方法源自於文本處理的詞袋模型。Bag-of-words model (BoW model) 最早出現在NLP和IR領域. 該模型忽略掉文本的語法和語序, 用一組無序的單詞(words)來表達一段文字或一個文檔. 近年來, BoW模型被廣泛應用於計算機視覺中. 與應用於文本的BoW類比, 圖像的特征(feature)被當作單詞(Word).
例如下面的句子:
John likes to watch movies. Mary likes too. John also likes to watch football games.
就可以構建一個詞典
{"John": 1, "likes": 2, "to": 3, "watch": 4, "movies": 5, "also": 6, "football": 7, "games": 8, "Mary": 9, "too": 10}
該字典中包含10個單詞, 每個單詞有唯一索引, 注意它們的順序和出現在句子中的順序沒有關聯. 根據這個字典, 我們能將上述兩句話重新表達為下述兩個向量:
[1, 2, 1, 1, 1, 0, 0, 0, 1, 1] [1, 1, 1, 1, 0, 1, 1, 1, 0, 0]
這兩個向量共包含10個元素, 其中第i個元素表示字典中第i個單詞在句子中出現的次數。
統計詞頻的時候有兩種方法:
- 詞集模型(set of words model) 將每個詞出現與否作為特征,忽略詞出現的次數,這種模型得到的向量只有0和1兩個值;
- 詞袋模型(bag of words model)要統計詞出現的次數。
特征詞袋(BoF,Bag Of Feature)借鑒文本處理的詞袋(BoW,Bag Of Bag)算法,將圖像表示成視覺關鍵詞的統計直方圖。就像上面對文本的處理一樣,提取文本中出現單詞組成詞匯表,這里關鍵是得到圖像庫的“詞匯表”。為了得到圖像庫的“詞匯表",通常對提取到的圖像特征進行聚類,得到一定個數的簇。這些聚類得到的簇,就是圖像的”詞匯“,可以稱為視覺詞(Visual Word)。聚類形成的簇,可以使用聚類中心來描述,所以,視覺詞指的是圖像的局部區域特征(如紋理,特征點)經過聚類形成的聚類中心。
有了視覺詞的集合后,就可以將一幅圖像表示為\(K\)維的向量(\(K\)為聚類中心的個數,也就是視覺詞的個數),向量的每個分量表示某個視覺詞在圖像中出現的次數。
構建圖像的BoF的步驟如下:以SIFT特征為例
- SIFT特征提取 提取訓練集中所有圖像的SIFT特征,設有\(M\)幅圖像,共得到\(N\)個SIFT特征。
- 構建視覺詞匯表 對提取到的\(N\)個SIFT特征進行聚類,得到\(K\)個聚類中心,組成圖像的視覺詞匯表。
- 圖像的視覺詞向量表示,統計每幅圖像中視覺詞匯的出現的次數,得到圖像的特征向量。在檢索時,該特征向量就代表該幅圖像。統計時,計算圖像中提取到的SIFT特征點到各個視覺詞(聚類中心)的距離,將其歸類到聚類最近的視覺詞中。
所以聚類在構建BoF是很重要的一步,接下來簡單的介紹下聚類的基本知識以及最常用的聚類算法k-means算法。
聚類概述
聚類(Clustering)是一種無監督學習算法,其目的是將數據集中的樣本划分為若干個不相交的子集,每個子集稱為一個簇(Cluster)。聚類的時候並不關心某一類是什么,只根據數據的相似性,將數據划分到不同的組中。每個組內的成員具有相似的性質。
聚類算法說白了就是給你一大堆點的坐標(維度可以是很高),然后通過一個向量的相似性准則(通常是距離,比如歐拉距離),然后把相近的點放在一個集合里面,歸為一類。
更正式的說,假設有樣本集 \(D = \{x_1,x_2,\dots,x_m\}\)有\(m\)個無標記的樣本,每個樣本可以使用一個\(n\)維特征向量表示:\(x_i = (x_{i1};x_{i2};\dots;x_{in})\),根據相似的准則,將集合\(D\)划分為\(k\)個不相交的簇\(\{C_l|l = 1,2,\dots,k\}\)。每個簇可以用其聚類中心來描述\(\lambda_l = (x_{l1},x_{l2},\dots,x_{ln}),l = 1,2,\dots,k\).
相似性度量(距離計算)
兩個向量的相似性,通常可以使用距離度量,距離越大,相似性越小;距離越小,相似性越大。給定兩個樣本\(x = (x_1,x_2,\dots,x_n), y = (y_1,y_x,\dots,y_n)\),常用的距離計算有:
- 歐氏距離 Euclidean distance,這個應該是最有名的了。\(dist = \|x_i-y_i\|_2 \sqrt{\sum_{i=1}^n(x_i - y_i)^2}\),歐氏距離也是一種\(l_2\)范數。
- 曼哈頓距離 Manhattan distance,也被稱為城市街區距離。\(dist = \|x_i-y_i\|_1= \sum_{i=1}^n\|x_i -y_i|\),曼哈頓距離也是\(l_1\)范數。
- 切比雪夫距離 Chebyshev distance,\(dist = max(|x_i - y_i|)\)
以上三種距離可以統稱為閔可夫斯基距離 Minkowski distance,\(dist = (\sum_{i=1}^n|x_i-y_i|^p)^{\frac{1}{p}}\)
- \(p=1\)為曼哈頓距離
- \(p=2\)為歐氏距離
- \(p\to\infty\)為切比雪夫距離。
當然,度量兩個向量相似性的方法還有很多種,這里只列舉了最常用的,在均值聚類算法中經常的使用的是歐氏距離和曼哈頓距離。
k-means
聚類算法可以分為三類:
- 原型聚類,此類算法假設聚類結構能夠通過一組原型描述,這里原型指的是樣本空間中具有代表性的點。
- 密度距離,該類算法假設聚類結構能夠通過樣本分布的緊密程度來確定。
- 層次聚類,在不同的層次對數據集進行划分,從而形成樹形的聚結構。
\(k\)均值聚類是原型聚類的一種,它使用簇內的均值向量來描述每個簇,假設給定的樣本集\(D = \{x_1,x_2,\dots,x_m\}\),得到\(k\)個簇,\(C = {C_1,C_2,\dots,C_k}\),\(k\)means算法的目標是使,簇內樣本到簇的質心(簇內的均值向量)距離最小
\(u_i\)是簇\(C_i\)的均值向量。\(E\)就表示了簇內樣本圍繞着均值向量(簇的中心)的緊密程度,\(E\)越小則簇內樣本相似度越高。
要使得\(E\)的值最小,是一個NP難題,因此均值聚類使用貪心策略,通過迭代的方法來求解最優解。
Lioyd's Algorithm
均值聚類算法多數是基於Lioyd's Algorithm,其流程很簡單。首先,隨機的確定\(k\)個初始點作為各個簇的質心。然后將數據集中每個點分配到與其最近的質心代表的簇中。然后更新各個簇的質心為該簇所有向量的均值。具體表示如下:
創建k個點作為起始質心(通常隨機選擇)
當任意一個點所在的簇發生變化時
對數據集中的每個數據點
對每個質心
計算質心與數據點之間的距離
將數據點分配到與其最近的簇中
對每個簇,計算簇中所有點的均值作為新的質心
k-means算法有兩個輸入參數簇的個數\(k\)以及初始的簇的質心
- 簇的個數\(k\)通常可以使用“肘點法”,通過最小化\(E\)來確定
- 對於初始的質心的選擇,可以隨機確定或者使用
k-means++來確定
vlfeat以及OpenCV實現
vlfeat
vlfeat實現了三種的k-means算法:
- Lioyd's Algorithm
- Elkan's Algorithm 使用三角形不等式對Lioyd算法的一種優化,提高了其計算的速度,本質上兩者是一樣的。
- ANN Algorithm 適用於大規模的數據集(百萬級)簇的個數成百上千
可以使用如下代碼來初始化k-means算法:
VlKMeans * fkmeans = vl_kmeans_new(VL_TYPE_FLOAT, VlDistanceL2);
vl_kmeans_set_algorithm(fkmeans, VlKMeansElkan);
vl_kmeans_init_centers_with_rand_data(fkmeans,data,data_dim,data_num,k);
首先設置聚類時的數據類型為float,相似性度量使用l2距離也就是歐氏距離;接着設置使用的算法為是Elkan,並且使用隨機的方法確定k個簇的中心。
初始化完成后,使用如下代碼進行聚類
vl_kmeans_cluster(fkmeans, data, data_dim, data_num, k);
需要指定數據,數據的維度,數據的個數以及簇的中心,這里需要注意的是數據的維度。聚類數據的維度指的是,一個數據有幾個分量組成。例如,
- 一幅灰度圖像,其聚類的對象是像素的像素值。灰度圖,一個像素只有一個分量,則灰度圖聚類數據的維度就是1維。
- RGB圖像,一個像素有RGB三個分量組成,則其聚類數據的維度就是3維。
- sift描述子,一個sift描述子是128維的向量,則其聚類數據的維度就是128維。
OpenCV
相較於vlfeat,OpenCV中的kmeans則更易於調用。
double cv::kmeans(InputArray data,
int K,
InputOutputArray bestLabels,
TermCriteria criteria,
int attempts,
int flags,
OutputArray centers = noArray()
)
- data 數據集,每一行代表數據集中的一個樣本
- k 聚類形成簇的個數
- bestLabels 數據集中每個樣本在簇的index
- criteria 迭代終止的條件。
- attempts 算法執行的次數
- flags 初始質心的指定方法,
KMEANS_RANDOM_CENTERS隨機指定;KMEANS_PP_CENTERSk-means++;KMEANS_USE_INITIAL_LABELS算法第一次執行時,使用用戶提供的初始質心;第二次及以后的執行使用隨機或者半隨機的方式初始化質心
在OpenCV中TermCriteria表示迭代算法結束的兩種條件:
- 達到了迭代的次數
- 迭代產生的結果達到了指定的精度
該類的初始化需要三個參數
- type 有三種選擇
COUNT, EPS or COUNT + EPS - maxCount 最大的迭代次數
- epsilon 精度
構建BoF
在上一篇文章圖像檢索(1): 再論SIFT-基於vlfeat實現中實現了SIFT特征點的提取,這里再對提取到的特征點進行聚類,構建圖像集的視覺詞匯表。
基於SIFT特征構建BoF的步驟:
- 提取sift特征點
- 聚類生成視覺詞匯表 Visual Vocabulary
- 統計視覺詞在每張圖像中出現的頻率,形成BoF
基於OpenCV的實現如下:
void bof_encode(const string &image_folder,int k,vector<Mat> &bof) {
vector<string> image_file_list;
get_file_name_list(image_folder,image_file_list);
// 提取圖像的sift
vector<Mat> descriptor_list;
Ptr<xfeatures2d::SIFT> sift = xfeatures2d::SIFT::create();
for(const string & file: image_file_list){
cout << "Extracte sift feature #" << file << endl;
vector<KeyPoint> kpts;
Mat des;
Mat img = imread(file);
CV_Assert(!img.empty());
sift->detectAndCompute(img,noArray(),kpts,des);
descriptor_list.push_back(des);
}
// 將各個圖像的sift特征組合到一起
Mat descriptor_stack;
vconcat(descriptor_list,descriptor_stack);
// 聚類
Mat cluster_centers;
vector<int> labels;
kmeans(descriptor_stack,k,labels,TermCriteria(TermCriteria::EPS + TermCriteria::COUNT,
10, 1.0),3, KMEANS_RANDOM_CENTERS,cluster_centers);
// labels已經得到了每個樣本(特征點)所屬的簇,需要進行統計得到每一張圖像的BoF
int index = 0;
for(Mat img : descriptor_list){
// For all keypoints of each image
auto cluster = new int[k];
for(int i = 0; i < img.rows; i ++){
cluster[labels[index]] ++;
index ++;
}
Mat mat(1,k,CV_32S);
auto ptr = mat.ptr<int>(0);
mempcpy(ptr,cluster,sizeof(int) * k);
bof.push_back(mat);
delete cluster;
}
}
提取特征點后,需要將得到的sift的特征描述子組合到一起,進行聚類,需要用到函數vconcat,該函數在y方向上將Mat組合在一起,需要各個Mat的列是一樣,組合得到的Mat仍然有相同的列;同樣的函數hconcat在水平方向上組合Mat,組合得到的Mat的行保持不變。
在聚類后可以得到所有圖像的各個sift特征所屬的簇,上述代碼的:
// labels已經得到了每個樣本(特征點)所屬的簇,需要進行統計得到每一張圖像的BoF
int index = 0;
for(Mat img : descriptor_list){
// For all keypoints of each image
auto cluster = new int[k];
for(int i = 0; i < img.rows; i ++){
cluster[labels[index]] ++;
index ++;
}
Mat mat(1,k,CV_32S);
auto ptr = mat.ptr<int>(0);
mempcpy(ptr,cluster,sizeof(int) * k);
bof.push_back(mat);
delete cluster;
}
就是統計每張圖像中,各個Visual Word的個數。這樣一幅圖像就可以使用一個K維的向量表示。
