【C++ OpenCV】图像检索:K均值(K-Means)聚类-构建BOF特征

K均值(K-Means)聚类-构建BOF特征

在图像检索时,通常首先提取图像的局部特征,这些局部特征通常有很高的维度(例如,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={x1,x2,…,xm} 有m个无标记的样本,每个样本可以使用一个n维特征向量表示:xi=(xi1 , xi2 , … , xin),根据相似的准则,将集合D划分为k个不相交的簇 C l ∣ l = 1 , 2 , … , k Cl|l=1,2,…,k Cll=1,2,,k 每个簇可以用其聚类中心来描述 λ l = ( x   l 1   ,   x l 2   , … , x   l n   ) , l = 1 , 2 , … , k . λ^l=(x~l1~,~xl2~,…,x~ln~),l=1,2,…,k. λl=(x l1 , xl2 ,,x ln ),l=1,2,,k.

相似性度量(距离计算)
两个向量的相似性,通常可以使用距离度量,距离越大,相似性越小;距离越小,相似性越大。给定两个样本x=(x1,x2,…,xn),y=(y1,yx,…,yn),常用的距离计算有:

  • 欧氏距离(Euclidean distance),这个应该是最有名的,也是最常用的。

d i s t = ∣ ∣ x i − y i ∣ ∣ 2 ( ∑ i = 1 n ( x i − y i ) 2 ) dist=||x_i-y_i||_2\sqrt(\sum_{i=1}^{n}(x_i−y_i)^2) dist=xiyi2( i=1n(xiyi)2)
欧氏距离也是一种 l 2 l_2 l2范数。

  • 曼哈顿距离 (Manhattan distance),也被称为城市街区距离。
    d i s t = ∣ ∣ x i − y i ∣ ∣ 1 = ∑ i = 1 n ∣ ∣ x i − y i ∣ ∣ dist=||xi-yi||_1=\sum_{i=1}^n ||x_i-y_i|| dist=xiyi1=i=1nxiyi
    曼哈顿距离也是 l 1 l_1 l1范数。
  • 切比雪夫距离(Chebyshev distance)
    d i s t = m a x ( ∣ x i − y i ∣ ) dist=max(|x_i-y_i|) dist=max(xiyi)
    以上三种距离可以统称为闵可夫斯基距离( Minkowski distance)
    d i s t = ( ∑ i = 1 n ∣ x i − y i ∣ p ) 1 p dist=(\sum_{i=1}^n |x_i-y_i|^p)^\frac{1}{p} dist=(i=1nxiyip)p1
  • p=1 为曼哈顿距离
  • p=2 为欧氏距离
  • p→ ∞ ∞ 为切比雪夫距离。

当然,度量两个向量相似性的方法还有很多种,这里只列举了最常用的,在均值聚类算法中经常的使用的是欧氏距离和曼哈顿距离

k-means

聚类算法可以分为三类:

  • 原型聚类,此类算法假设聚类结构能够通过一组原型描述,这里原型指的是样本空间中具有代表性的点。
  • 密度距离,该类算法假设聚类结构能够通过样本分布的紧密程度来确定。
  • 层次聚类,在不同的层次对数据集进行划分,从而形成树形的聚结构。

k k k均值聚类是原型聚类的一种,它使用簇内的均值向量来描述每个簇,假设给定的样本集
D D D={ x 1 , x 2 , … , x m x_1,x_2,…,x_m x1,x2,,xm},得到 k k k个簇, C = C 1 , C 2 , … , C k C=C_1,C_2,…,C_k C=C1,C2,,Ck,kmeans算法的目标是使,簇内样本到簇的质心(簇内的均值向量)距离最小。
E = ∑ i = 1 k ∑ x ∈ C i ∣ ∣ x − u i ∣ ∣ 2 2 E=\sum_{i=1}^k\sum_{x ∈C_i}||x-u_i||_2^2 E=i=1kxCixui22
u i = 1 ∣ C i ∣ ∑ x ∈ C i u_i=\frac{1}{|C_i|}\sum x∈C_i ui=Ci1xCi

其中:

  • u i u_i ui是簇 C i C_i Ci的均值向量。 E E E就表示了簇内样本围绕着均值向量(簇的中心)的紧密程度, E E E越小则簇内样本相似度越高。
  • 要使得 E E E的值最小,是一个NP难题,因此均值聚类使用贪心策略,通过迭代的方法来求解最优解。

Lioyd’s Algorithm

均值聚类算法多数是基于Lioyd’s Algorithm,其流程很简单。

  • 首先,随机的确定k个初始点作为各个簇的质心。
  • 然后将数据集中每个点分配到与其最近的质心代表的簇中。
  • 然后更新各个簇的质心为该簇所有向量的均值。具体表示如下:
创建k个点作为起始质心(通常随机选择)
当任意一个点所在的簇发生变化时
    对数据集中的每个数据点
        对每个质心
            计算质心与数据点之间的距离
        将数据点分配到与其最近的簇中
    对每个簇,计算簇中所有点的均值作为新的质心

k-means算法有两个输入参数簇的个数 k k k以及初始的簇的质心

  • 簇的个数 k k k通常可以使用“肘点法”,通过最小化 E E 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,相似性度量使用 l 2 l_2 l2距离也就是欧氏距离;
  • 接着设置使用的算法为是Elkan,并且使用随机的方法确定 k k 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

基于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维的向量表示。

转载自:https://www.cnblogs.com/wangguchangqing/p/9222267.html?utm_source=debugrun&utm_medium=referral

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值