圖像檢索(5):基於OpenCV實現小型的圖像數據庫檢索

本文對前面的幾篇文章進行個總結,實現一個小型的圖像檢索應用。html

一個小型的圖像檢索應用能夠分爲兩部分:數據庫

  • train,構建圖像集的特徵數據庫。
  • retrieval,檢索,給定圖像,從圖像庫中返回最相似的圖像

構建圖像數據庫的過程以下:多線程

  • 生成圖像集的視覺詞彙表(Vocabulary)
    • 提取圖像集全部圖像的sift特徵
    • 對獲得的sifte特徵集合進行聚類,聚類中心就是Vocabulary
  • 對圖像集中的圖像從新編碼表示,可以使用BoW或者VLAD,這裏選擇VLAD.
  • 將圖像集中全部圖像的VLAD表示組合到一塊兒獲得一個VLAD表,這就是查詢圖像的數據庫。

獲得圖像集的查詢數據後,對任一圖像查找其在數據庫中的最類似圖像的流程以下:ide

  • 提取圖像的sift特徵
  • 加載Vocabulary,使用VLAD表示圖像
  • 在圖像數據庫中查找與該VLAD最類似的向量

構建圖像集的特徵數據庫的流程一般是offline的,查詢的過程則須要是實時的,基本流程參見下圖:
測試

由兩部分構成:offline的訓練過程以及online的檢索查找ui

各個功能模塊的實現

下面就使用VLAD表示圖像,實現一個小型的圖像數據庫的檢索程序。下面實現須要的功能模塊編碼

  • 特徵點提取
  • 構建Vocabulary
  • 構建數據庫

第一步,特徵點的提取

無論是BoW仍是VLAD,都是基於圖像的局部特徵的,本文選擇的局部特徵是SIFT,使用其擴展RootSift。提取到穩定的特徵點尤其的重要,本文使用OpenCV體哦那個的SiftDetecotr,實例化以下:線程

auto fdetector = xfeatures2d::SIFT::create(0,3,0.2,10);

create的聲明以下:rest

static Ptr<SIFT> cv::xfeatures2d::SIFT::create     (     int      nfeatures = 0,
        int      nOctaveLayers = 3,
        double      contrastThreshold = 0.04,
        double      edgeThreshold = 10,
        double      sigma = 1.6 
    )
  • nfeatures 設置提取到的特徵點的個數,每一個sift的特徵點都根據其對比度(local contrast)計算出來一個分數。設置了該值後,會根據分數排序,只保留前nfeatures個返回
  • nOctaveLayers 每一個octave中的層數,該值能夠根據圖像的分辨率大小計算出來。D.Lowe論文中該值爲3
  • contrastThreshold 過濾掉低對比度的不穩定特徵點,該值越大,提取到的特徵點越少
  • edgeThreshold 過濾邊緣處的特徵點,該值越大,提取到的特徵點就越多
  • sigma 高斯濾波器的參數,該濾波器應用於第0個Octave

我的的一些看法。
設置參數時,主要是設置contrastThresholdedgeThresholdcontrastThreshold是過濾掉平滑區域的一些不穩定的特徵點,edgeThreshold是過慮相似邊緣的不穩定關鍵點。設置參數時,應儘可能保證提取的特徵點個數適中,不易過多,也不要過少。另外,contrastThresholdedgeThreshold的平衡,應根據要提取的目標是比較平滑的區域仍是紋理較多的區域,來平衡這兩個參數的設置。code

對於有些圖像,可能設置的提取特徵點的參數叫嚴格,提取特徵點的個數過少,這時候可改變寬鬆一些的參數。

auto fdetector = xfeatures2d::SIFT::create(0,3,0.2,10);
fdetector->detectAndCompute(img,noArray(),kpts,feature);

if(kpts.size() < 10){
    fdetector = xfeatures2d::SIFT::create();
    fdetector->detectAndCompute(img,noArray(),kpts,feature);
}

閾值10,可根據具體的狀況進行調節。

更多關於sift的內容能夠參看文章:

關於RootSift和VLAD能夠參考前面的文章圖像檢索(4):IF-IDF,RootSift,VLAD

第二步,構建Vocabulary

Vocabulary的構建過程,實際就是對提取到的圖像特徵點的聚類。首先提取圖像庫圖像sift特徵,並將其擴展爲RootSift,而後對提取到的RootSift進行聚類獲得Vocabulary。
這裏建立class Vocabulary,主要如下方法:

  • create 從提取到的特徵點構建聚類獲得視覺詞彙表Vocabulary
void Vocabulary::create(const std::vector<cv::Mat> &features,int k)
{
    Mat f;
    vconcat(features,f);
    vector<int> labels;
    kmeans(f,k,labels,TermCriteria(TermCriteria::COUNT + TermCriteria::EPS,100,0.01),3,cv::KMEANS_PP_CENTERS,m_voc);
    m_k = k;
}
  • loadsave,爲了使用方便,須要可以將生成的視覺詞彙表Vocabulary保存問文件(.yml)
  • tranform_vlad,將輸入的圖像進行轉換爲vlad表示
void Vocabulary::transform_vlad(const cv::Mat &f,cv::Mat &vlad)
{
    // Find the nearest center
    Ptr<FlannBasedMatcher> matcher = FlannBasedMatcher::create();
    vector<DMatch> matches;
    matcher->match(f,m_voc,matches);
    // Compute vlad
    Mat responseHist(m_voc.rows,f.cols,CV_32FC1,Scalar::all(0));
    for( size_t i = 0; i < matches.size(); i++ ){
        auto queryIdx = matches[i].queryIdx;
        int trainIdx = matches[i].trainIdx; // cluster index
        Mat residual;
        subtract(f.row(queryIdx),m_voc.row(trainIdx),residual,noArray());
        add(responseHist.row(trainIdx),residual,responseHist.row(trainIdx),noArray(),responseHist.type());
    }

    // l2-norm
    auto l2 = norm(responseHist,NORM_L2);
    responseHist /= l2;
    //normalize(responseHist,responseHist,1,0,NORM_L2);

    //Mat vec(1,m_voc.rows * f.cols,CV_32FC1,Scalar::all(0));
    vlad = responseHist.reshape(0,1); // Reshape the matrix to 1 x (k*d) vector
}

class Vocabulary有如下方法:

  • 從圖像列表中構建視覺詞彙表Vocabulary
  • 將生成的Vocabulary保存到本地,並提供了load方法
  • 將圖像表示爲VLAD

第三步,建立圖像數據庫

圖像數據庫也就是將圖像VLAD表示的集合,在該數據庫檢索時,返回與query圖像類似的VLAD所對應的圖像。
本文使用OpenCV提供的Mat構建一個簡單的數據庫,Mat保存全部圖像的vlad向量組成的矩陣,在檢索時,實際就是對該Mat的檢索。
聲明類class Database,其具備如下功能:

  • add 添加圖像到數據庫
  • saveload 將數據庫保存爲文件(.yml)
  • retrieval 檢索,對保存的vald向量的Mat建立索引,返回最類似的結果。

第四步,Trainer

在上面實現了特徵點的提取,構建視覺詞彙表,構建圖像表示爲VLAD的數據庫,這裏將其組合到一塊兒,建立Trainer類,方便訓練使用。

class Trainer{

public:

    Trainer();
    ~Trainer();

    Trainer(int k,int pcaDim,const std::string &imageFolder,
        const std::string &path,const std::string &identifiery,std::shared_ptr<RootSiftDetector> detector);
    
    void createVocabulary();
    void createDb();

    void save();

private:

    int m_k; // The size of vocabulary
    int m_pcaDimension; // The retrain dimensions after pca

    Vocabulary* m_voc;
    Database* m_db;

private:

    /*
        Image folder
    */
    std::string m_imageFolder;

    /*
        training result identifier,the name suffix of vocabulary and database
        voc-identifier.yml,db-identifier.yml
    */
    std::string m_identifier;

    /*
        The location of training result
    */
    std::string m_resultPath;
};

使用Trainer 須要配置

  • 圖像集所在的目錄
  • 視覺詞彙表的大小(聚類中心的個數)
  • PCA後VLAD保留的維度,可先無論設置爲0,不進行PCA
  • 訓練後數據的保存路徑。 訓練後的數據保存爲yml形式,命名規則是voc-m_identifier.ymldb-m_identifier.yml。 爲了方便測試不一樣參數的數據,這裏設置一個後綴參數m_identifier,來區分不一樣的參數的訓練數據。

其使用代碼以下:

int main(int argc, char *argv[])
{
    const string image_200 = "/home/test/images-1";
    const string image_6k = "/home/test/images/sync_down_1";
    
    auto detector = make_shared<RootSiftDetector>(5,5,10);
    Trainer trainer(64,0,image_200,"/home/test/projects/imageRetrievalService/build","test-200-vl-64",detector);

    trainer.createVocabulary();
    trainer.createDb();
    
    trainer.save();

    return 0;
}

偷懶,沒有配置爲參數,使用時須要設置好圖像的路徑,以及訓練後數據的保存數據。

第五步,Searcher

Database中,已經實現了retrieval的方法。 這裏之因此再封裝一層,是爲了更好的契合業務上的一些需求。好比,圖像的一些預處理,分塊,多線程處理,查詢結果的過濾等等。關於Searcher和具體的應用耦合比較深,這裏只是簡單的實現了個retrieval方法和查詢參數的配置。

class Searcher{

public:
    Searcher();
    ~Searcher();

    void init(int keyPointThreshold);
    void setDatabase(std::shared_ptr<Database> db);

    void retrieval(cv::Mat &query,const std::string &group,std::string &md5,double &score);

    void retrieval(std::vector<char> bins,const std::string &group,std::string &md5,double &score);

private:
    int m_keyPointThreshold;

    std::shared_ptr<Database> m_db;
};

使用也很簡單了,從文件中加載VaocabularyDatabase,設置Searcher的參數。

Vocabulary voc;

    stringstream ss;
    ss << path << "/voc-" << identifier << ".yml";

    cout << "Load vocabulary from " << ss.str() << endl;
    voc.load(ss.str());

    cout << "Load vocabulary successful." << endl;

    auto detector = make_shared<RootSiftDetector>(5,0.2,10);

    auto db = make_shared<Database>(detector);

    cout << "Load database from " << path << "/db-" << identifier << ".yml" << endl;
    db->load1(path,identifier);
    db->setVocabulary(voc);
    cout << "Load database successful." << endl;

     Searcher s;
    s.init(10);
    s.setDatabase(db);

Summary

上圖來總結下整個流程

  • 建立Vocabulary
  • 建立Database
  • Search Similary list
相關文章
相關標籤/搜索