SIFT算法的應用
-目標識別之用Bag-of-words模型表示一幅圖像
作者:Savitch
出處:http://blog.csdn.net/assiduousknight/article/details/16901427
-
引言
本blog之前已經寫了四篇關於SIFT的文章,請參考九、圖像特徵提取與匹配之SIFT算法,九(續)、sift算法的編譯與實現,九(再續)、教你一步一步用c語言實現sift算法、上,及九(再續)、教你一步一步用c語言實現sift算法、下。
上述這4篇文章對SIFT算法的原理和C語言實現都做了詳細介紹,用SIFT做圖像匹配效果不錯。現在考慮更爲高層的應用,將SIFT算法應用於目標識別:發現圖像中包含的物體類別,這是計算機視覺領域最基本也是最重要的任務之一。
且原經典算法研究系列可能將改名爲算法珠璣--經典算法的通俗演義。改名考慮到三點:1、不求面面俱到所有算法,所以掏煉,謂之“珠璣”;2、突出本博客內算法內容的特色-通俗易懂、簡明直白,謂之“通俗”;3、側重經典算法的研究與實現,以及實際應用,謂之“演義”。
OK,閒話少說,上一篇我們介紹了六(續)、從KMP算法一步一步談到BM算法。下面我們來介紹有關SIFT算法的目標識別的應用--Bag-of-words模型。
-
Bag-of-words模型簡介
Bag-of-words模型是信息檢索領域常用的文檔表示方法。在信息檢索中,BOW模型假定對於一個文檔,忽略它的單詞順序和語法、句法等要素,將其僅僅看作是若干個詞彙的集合,文檔中每個單詞的出現都是獨立的,不依賴於其它單詞是否出現。也就是說,文檔中任意一個位置出現的任何單詞,都不受該文檔語意影響而獨立選擇的。例如有如下兩個文檔:
1:Bob likes to play basketball, Jim likes too.
2:Bob also likes to play football games.
基於這兩個文本文檔,構造一個詞典:
Dictionary = {1:”Bob”, 2. “like”, 3. “to”, 4. “play”, 5. “basketball”, 6. “also”, 7. “football”, 8. “games”, 9. “Jim”, 10. “too”}。
這個詞典一共包含10個不同的單詞,利用詞典的索引號,上面兩個文檔每一個都可以用一個10維向量表示(用整數數字0~n(n爲正整數)表示某個單詞在文檔中出現的次數):
1:[1, 2, 1, 1, 1, 0, 0, 0, 1, 1]
2:[1, 1, 1, 1 ,0, 1, 1, 1, 0, 0]
向量中每個元素表示詞典中相關元素在文檔中出現的次數(下文中,將用單詞的直方圖表示)。不過,在構造文檔向量的過程中可以看到,我們並沒有表達單詞在原來句子中出現的次序(這是本Bag-of-words模型的缺點之一,不過瑕不掩瑜甚至在此處無關緊要)。
-
Bag-of-words模型的應用
Bag-of-words模型的適用場合
現在想象在一個巨大的文檔集合D,裏面一共有M個文檔,而文檔裏面的所有單詞提取出來後,一起構成一個包含N個單詞的詞典,利用Bag-of-words模型,每個文檔都可以被表示成爲一個N維向量,計算機非常擅長於處理數值向量。這樣,就可以利用計算機來完成海量文檔的分類過程。
考慮將Bag-of-words模型應用於圖像表示。爲了表示一幅圖像,我們可以將圖像看作文檔,即若干個“視覺詞彙”的集合,同樣的,視覺詞彙相互之間沒有順序。
圖1 將Bag-of-words模型應用於圖像表示
由於圖像中的詞彙不像文本文檔中的那樣是現成的,我們需要首先從圖像中提取出相互獨立的視覺詞彙,這通常需要經過三個步驟:(1)特徵檢測,(2)特徵表示,(3)單詞本的生成,請看下圖2:
圖2 從圖像中提取出相互獨立的視覺詞彙
通過觀察會發現,同一類目標的不同實例之間雖然存在差異,但我們仍然可以找到它們之間的一些共同的地方,比如說人臉,雖然說不同人的臉差別比較大,但眼睛,嘴,鼻子等一些比較細小的部位,卻觀察不到太大差別,我們可以把這些不同實例之間共同的部位提取出來,作爲識別這一類目標的視覺詞彙。
而SIFT算法是提取圖像中局部不變特徵的應用最廣泛的算法,因此我們可以用SIFT算法從圖像中提取不變特徵點,作爲視覺詞彙,並構造單詞表,用單詞表中的單詞表示一幅圖像。
Bag-of-words模型應用三步
接下來,我們通過上述圖像展示如何通過Bag-of-words模型,將圖像表示成數值向量。現在有三個目標類,分別是人臉、自行車和吉他。
Bag-of-words模型的第一步是利用SIFT算法,從每類圖像中提取視覺詞彙,將所有的視覺詞彙集合在一起,如下圖3所示:
圖3 從每類圖像中提取視覺詞彙
第二步是利用K-Means算法構造單詞表。K-Means算法是一種基於樣本間相似性度量的間接聚類方法,此算法以K爲參數,把N個對象分爲K個簇,以使簇內具有較高的相似度,而簇間相似度較低。SIFT提取的視覺詞彙向量之間根據距離的遠近,可以利用K-Means算法將詞義相近的詞彙合併,作爲單詞表中的基礎詞彙,假定我們將K設爲4,那麼單詞表的構造過程如下圖4所示:
圖4 利用K-Means算法構造單詞表
第三步是利用單詞表的中詞彙表示圖像。利用SIFT算法,可以從每幅圖像中提取很多個特徵點,這些特徵點都可以用單詞表中的單詞近似代替,通過統計單詞表中每個單詞在圖像中出現的次數,可以將圖像表示成爲一個K=4維數值向量。請看下圖5:
圖5 每幅圖像的直方圖表示
上圖5中,我們從人臉、自行車和吉他三個目標類圖像中提取出的不同視覺詞彙,而構造的詞彙表中,會把詞義相近的視覺詞彙合併爲同一類,經過合併,詞彙表中只包含了四個視覺單詞,分別按索引值標記爲1,2,3,4。通過觀察可以看到,它們分別屬於自行車、人臉、吉他、人臉類。統計這些詞彙在不同目標類中出現的次數可以得到每幅圖像的直方圖表示(我們假定存在誤差,實際情況亦不外如此):
人臉: [3,30,3,20]
自行車:[20,3,3,2]
吉他: [8,12,32,7]
其實這個過程非常簡單,就是針對人臉、自行車和吉他這三個文檔,抽取出相似的部分(或者詞義相近的視覺詞彙合併爲同一類),構造一個詞典,詞典中包含4個視覺單詞,即Dictionary = {1:”自行車”, 2. “人臉”, 3. “吉他”, 4. “人臉類”},最終人臉、自行車和吉他這三個文檔皆可以用一個4維向量表示,最後根據三個文檔相應部分出現的次數畫成了上面對應的直方圖。
需要說明的是,以上過程只是針對三個目標類非常簡單的一個示例,實際應用中,爲了達到較好的效果,單詞表中的詞彙數量K往往非常龐大,並且目標類數目越多,對應的K值也越大,一般情況下,K的取值在幾百到上千,在這裏取K=4僅僅是爲了方便說明。
下面,我們再來總結一下如何利用Bag-of-words模型將一幅圖像表示成爲數值向量:
-
第一步:利用SIFT算法從不同類別的圖像中提取視覺詞彙向量,這些向量代表的是圖像中局部不變的特徵點;
-
第二步:將所有特徵點向量集合到一塊,利用K-Means算法合併詞義相近的視覺詞彙,構造一個包含K個詞彙的單詞表;
-
第三步:統計單詞表中每個單詞在圖像中出現的次數,從而將圖像表示成爲一個K維數值向量。
下面我們按照以上步驟,用C++一步步實現上述過程。
-
C++逐步實現:Bag-of-words模型表示一幅圖像
在具體編碼之前,我們需要事先搭配開發環境。
一. 搭建開發環境
使用的開發平臺是windows xp sp3 + vs2010(windows xp sp3 + vc6.0的情況,請參考此文:九(續)、sift算法的編譯與實現)
1. 從Rob Hess的個人主頁:http://blogs.oregonstate.edu/hess/code/sift/,下載最新版本的sift開源庫源代碼sift-latest_win.zip;
2. 由於sift-latest_win.zip 要求的opencv版本是2.0以上,也下載最新版本 OpenCV-2.2.0-win32-vs2010.exe,運行安裝程序將opencv安裝在本地某路徑下。例如,我安裝在D盤根目錄下。
3. 運行vs2010,建立一個空的控制檯應用程序,取名bow。
4. 配置opencv環境。在vs2010下選擇project菜單下的bow property子菜單,調出bow property pages對話框,需要配置的地方有三處:在vc++ Directory選項裏需要配置Include Directories和Library Directories,在Linker選項卡的Input選項裏需要配置Additional Dependencies。
至此,開發環境全部搭建並配置完畢。
二.創建c++類CSIFTDiscriptor
爲了方便使用,我們將SIFT庫用C++類CSIFTDiscriptor封裝,該類可以計算並獲取指定圖像的特徵點向量集合。類的聲名在SIFTDiscriptor.h文件中,內容如下:
- #ifndef _SIFT_DISCRIPTOR_H_
- #define _SIFT_DISCRIPTOR_H_
- #include <string>
- #include <highgui.h>
- #include <cv.h>
- extern "C"
- {
- #include "../sift/sift.h"
- #include "../sift/imgfeatures.h"
- #include "../sift/utils.h"
- };
- class CSIFTDiscriptor
- {
- public:
- int GetInterestPointNumber()
- {
- return m_nInterestPointNumber;
- }
- struct feature *GetFeatureArray()
- {
- return m_pFeatureArray;
- }
- public :
- void SetImgName(const std::string &strImgName)
- {
- m_strInputImgName = strImgName;
- }
- int CalculateSIFT();
- public:
- CSIFTDiscriptor(const std::string &strImgName);
- CSIFTDiscriptor()
- {
- m_nInterestPointNumber = 0;
- m_pFeatureArray = NULL;
- }
- ~CSIFTDiscriptor();
- private:
- std::string m_strInputImgName;
- int m_nInterestPointNumber;
- feature *m_pFeatureArray;
- };
- #endif
成員函數實現在SIFTDiscriptor.cpp文件中,其中,CalculateSIFT函數完成特徵點的提取和計算,其主要內部流程如下:
1) 調用OpenCV函數cvLoadImage加載輸入圖像;
2) 爲了統一輸入圖像的尺寸,CalculateSIFT函數的第二步是調整輸入圖像的尺寸,這通過調用cvResize函數實現;
3) 如果輸入圖像是彩色圖像,我們需要首先將其轉化成灰度圖,這通過調用cvCvtColor函數實現;
4) 調用SIFT庫函數sift_feature獲取輸入圖像的特徵點向量集合和特徵點個數。
- #include "SIFTDiscriptor.h"
- int CSIFTDiscriptor::CalculateSIFT()
- {
- IplImage *pInputImg = cvLoadImage(m_strInputImgName.c_str());
- if (!pInputImg)
- {
- return -1;
- }
- int nImgWidth = 320; //訓練用標準圖像大小
- double dbScaleFactor = pInputImg->width / 300.0; //縮放因子
- IplImage *pTmpImg = cvCreateImage(cvSize(pInputImg->width / dbScaleFactor, pInputImg->height / dbScaleFactor),
- pInputImg->depth, pInputImg->nChannels);
- cvResize(pInputImg, pTmpImg); //縮放
- cvReleaseImage(&pInputImg);
- if (pTmpImg->nChannels != 1) //非灰度圖
- {
- IplImage *pGrayImg = cvCreateImage(cvSize(pTmpImg->width, pTmpImg->height),
- pTmpImg->depth, 1);
- cvCvtColor(pTmpImg, pGrayImg, CV_RGB2GRAY);
- m_nInterestPointNumber = sift_features(pGrayImg, &m_pFeatureArray);
- cvReleaseImage(&pGrayImg);
- }
- else
- {
- m_nInterestPointNumber = sift_features(pTmpImg, &m_pFeatureArray);
- }
- cvReleaseImage(&pTmpImg);
- return m_nInterestPointNumber;
- }
- CSIFTDiscriptor::CSIFTDiscriptor(const std::string &strImgName)
- {
- m_strInputImgName = strImgName;
- m_nInterestPointNumber = 0;
- m_pFeatureArray = NULL;
- CalculateSIFT();
- }
- CSIFTDiscriptor::~CSIFTDiscriptor()
- {
- if (m_pFeatureArray)
- {
- free(m_pFeatureArray);
- }
- }
三.創建c++類CImgSet,管理實驗圖像集合
Bag-of-words模型需要從多個目標類圖像中提取視覺詞彙,不同目標類的圖像存儲在不同子文件夾中,爲了方便操作,我們設計了一個專門的類CImgSet用來管理圖像集合,聲明在文件ImgSet.h中:
- #ifndef _IMG_SET_H_
- #define _IMG_SET_H_
- #include <vector>
- #include <string>
- #pragma comment(lib, "shlwapi.lib")
- class CImgSet
- {
- public:
- CImgSet (const std::string &strImgDirName) : m_strImgDirName(strImgDirName+"//"), m_nImgNumber(0){}
- int GetTotalImageNumber()
- {
- return m_nImgNumber;
- }
- std::string GetImgName(int nIndex)
- {
- return m_szImgs.at(nIndex);
- }
- int LoadImgsFromDir()
- {
- return LoadImgsFromDir("");
- }
- private:
- int LoadImgsFromDir(const std::string &strDirName);
- private:
- typedef std::vector <std::string> IMG_SET;
- IMG_SET m_szImgs;
- int m_nImgNumber;
- const std::string m_strImgDirName;
- };
- #endif
- //成員函數實現在文件ImgSet.cpp中:
- #include "ImgSet.h"
- #include <windows.h>
- #include <Shlwapi.h>
- /**
- strSubDirName:子文件夾名
- */
- int CImgSet::LoadImgsFromDir(const std::string &strSubDirName)
- {
- WIN32_FIND_DATAA stFD = {0};
- std::string strDirName;
- if ("" == strSubDirName)
- {
- strDirName = m_strImgDirName;
- }
- else
- {
- strDirName = strSubDirName;
- }
- std::string strFindName = strDirName + "//*";
- HANDLE hFile = FindFirstFileA(strFindName.c_str(), &stFD);
- BOOL bExist = FindNextFileA(hFile, &stFD);
- for (;bExist;)
- {
- std::string strTmpName = strDirName + stFD.cFileName;
- if (strDirName + "." == strTmpName || strDirName + ".." == strTmpName)
- {
- bExist = FindNextFileA(hFile, &stFD);
- continue;
- }
- if (PathIsDirectoryA(strTmpName.c_str()))
- {
- strTmpName += "//";
- LoadImgsFromDir(strTmpName);
- bExist = FindNextFileA(hFile, &stFD);
- continue;
- }
- std::string strSubImg = strDirName + stFD.cFileName;
- m_szImgs.push_back(strSubImg);
- bExist = FindNextFileA(hFile, &stFD);
- }
- m_nImgNumber = m_szImgs.size();
- return m_nImgNumber;
- }
LoadImgsFromDir遞歸地從圖像文件夾中獲取所有實驗用圖像名,包括子文件夾。該函數內部通過循環調用windows API函數FindFirstFile和FindNextFile來找到文件夾中所有圖像的名稱。
四.創建CHistogram,生成圖像的直方圖表示
- //ImgHistogram.h
- #ifndef _IMG_HISTOGRAM_H_
- #define _IMG_HISTOGRAM_H_
- #include <string>
- #include "SIFTDiscriptor.h"
- #include "ImgSet.h"
- const int cnClusterNumber = 1500;
- const int ciMax_D = FEATURE_MAX_D;
- class CHistogram
- {
- public:
- void SetTrainingImgSetName(const std::string strTrainingImgSet)
- {
- m_strTrainingImgSetName = strTrainingImgSet;
- }
- int FormHistogram();
- CvMat CalculateImgHistogram(const string strImgName, int pszImgHistogram[]);
- CvMat *GetObservedData();
- CvMat *GetCodebook()
- {
- return m_pCodebook;
- }
- void SetCodebook(CvMat *pCodebook)
- {
- m_pCodebook = pCodebook;
- m_bSet = true;
- }
- public:
- CHistogram():m_pszHistogram(0), m_nImgNumber(0), m_pObservedData(0), m_pCodebook(0), m_bSet(false){}
- ~CHistogram()
- {
- if (m_pszHistogram)
- {
- delete m_pszHistogram;
- m_pszHistogram = 0;
- }
- if (m_pObservedData)
- {
- cvReleaseMat(&m_pObservedData);
- m_pObservedData = 0;
- }
- if (m_pCodebook && !m_bSet)
- {
- cvReleaseMat(&m_pCodebook);
- m_pCodebook = 0;
- }
- }
- private :
- bool m_bSet;
- CvMat *m_pCodebook;
- CvMat *m_pObservedData;
- std::string m_strTrainingImgSetName;
- int (*m_pszHistogram)[cnClusterNumber];
- int m_nImgNumber;
- };
- #endif
- #include "ImgHistogram.h"
- int CHistogram::FormHistogram()
- {
- int nRet = 0;
- CImgSet iImgSet(m_strTrainingImgSetName);
- nRet = iImgSet.LoadImgsFromDir();
- const int cnTrainingImgNumber = iImgSet.GetTotalImageNumber();
- m_nImgNumber = cnTrainingImgNumber;
- CSIFTDiscriptor *pDiscriptor = new CSIFTDiscriptor[cnTrainingImgNumber];
- int nIPNumber(0) ;
- for (int i = 0; i < cnTrainingImgNumber; ++i) //計算每一幅訓練圖像的SIFT描述符
- {
- const string strImgName = iImgSet.GetImgName(i);
- pDiscriptor[i].SetImgName(strImgName);
- pDiscriptor[i].CalculateSIFT();
- nIPNumber += pDiscriptor[i].GetInterestPointNumber();
- }
- double (*pszDiscriptor)[FEATURE_MAX_D] = new double[nIPNumber][FEATURE_MAX_D]; //存儲所有描述符的數組。每一行代表一個IP的描述符
- ZeroMemory(pszDiscriptor, sizeof(int) * nIPNumber * FEATURE_MAX_D);
- int nIndex = 0;
- for (int i = 0; i < cnTrainingImgNumber; ++i) //遍歷所有圖像
- {
- struct feature *pFeatureArray = pDiscriptor[i].GetFeatureArray();
- int nFeatureNumber = pDiscriptor[i].GetInterestPointNumber();
- for (int j = 0; j < nFeatureNumber; ++j) //遍歷一幅圖像中所有的IP(Interesting Point興趣點
- {
- for (int k = 0; k < FEATURE_MAX_D; k++)//初始化一個IP描述符
- {
- pszDiscriptor[nIndex][k] = pFeatureArray[j].descr[k];
- }
- ++nIndex;
- }
- }
- CvMat *pszLabels = cvCreateMat(nIPNumber, 1, CV_32SC1);
- //對所有IP的描述符,執行KMeans算法,找到cnClusterNumber個聚類中心,存儲在pszClusterCenters中
- if (!m_pCodebook) //構造碼元表
- {
- CvMat szSamples,
- *pszClusterCenters = cvCreateMat(cnClusterNumber, FEATURE_MAX_D, CV_32FC1);
- cvInitMatHeader(&szSamples, nIPNumber, FEATURE_MAX_D, CV_32FC1, pszDiscriptor);
- cvKMeans2(&szSamples, cnClusterNumber, pszLabels,
- cvTermCriteria( CV_TERMCRIT_EPS+CV_TERMCRIT_ITER, 10, 1.0 ),
- 1, (CvRNG *)0, 0, pszClusterCenters); //
- m_pCodebook = pszClusterCenters;
- }
- m_pszHistogram = new int[cnTrainingImgNumber][cnClusterNumber]; //存儲每幅圖像的直方圖表示,每一行對應一幅圖像
- ZeroMemory(m_pszHistogram, sizeof(int) * cnTrainingImgNumber * cnClusterNumber);
- //計算每幅圖像的直方圖
- nIndex = 0;
- for (int i = 0; i < cnTrainingImgNumber; ++i)
- {
- struct feature *pFeatureArray = pDiscriptor[i].GetFeatureArray();
- int nFeatureNumber = pDiscriptor[i].GetInterestPointNumber();
- // int nIndex = 0;
- for (int j = 0; j < nFeatureNumber; ++j)
- {
- // CvMat szFeature;
- // cvInitMatHeader(&szFeature, 1, FEATURE_MAX_D, CV_32FC1, pszDiscriptor[nIndex++]);
- // double dbMinimum = 1.79769e308;
- // int nCodebookIndex = 0;
- // for (int k = 0; k < m_pCodebook->rows; ++k)//找到距離最小的碼元,用最小碼元代替原//來的詞彙
- // {
- // CvMat szCode = cvMat(1, m_pCodebook->cols, m_pCodebook->type);
- // cvGetRow(m_pCodebook, &szCode, k);
- // double dbDistance = cvNorm(&szFeature, &szCode, CV_L2);
- // if (dbDistance < dbMinimum)
- // {
- // dbMinimum = dbDistance;
- // nCodebookIndex = k;
- // }
- // }
- int nCodebookIndex = pszLabels->data.i[nIndex++]; //找到第i幅圖像中第j個IP在Codebook中的索引值nCodebookIndex
- ++m_pszHistogram[i][nCodebookIndex]; //0<nCodebookIndex<cnClusterNumber;
- }
- }
- //資源清理,函數返回
- // delete []m_pszHistogram;
- // m_pszHistogram = 0;
- cvReleaseMat(&pszLabels);
- // cvReleaseMat(&pszClusterCenters);
- delete []pszDiscriptor;
- delete []pDiscriptor;
- return nRet;
- }
- //double descr_dist_sq( struct feature* f1, struct feature* f2 );
- CvMat CHistogram::CalculateImgHistogram(const string strImgName, int pszImgHistogram[])
- {
- if ("" == strImgName || !m_pCodebook || !pszImgHistogram)
- {
- return CvMat();
- }
- CSIFTDiscriptor iImgDisp;
- iImgDisp.SetImgName(strImgName);
- iImgDisp.CalculateSIFT();
- struct feature *pImgFeature = iImgDisp.GetFeatureArray();
- int cnIPNumber = iImgDisp.GetInterestPointNumber();
- // int *pszImgHistogram = new int[cnClusterNumber];
- // ZeroMemory(pszImgHistogram, sizeof(int)*cnClusterNumber);
- for (int i = 0; i < cnIPNumber; ++i)
- {
- double *pszDistance = new double[cnClusterNumber];
- CvMat iIP = cvMat(FEATURE_MAX_D, 1, CV_32FC1, pImgFeature[i].descr);
- for (int j = 0; j < cnClusterNumber; ++j)
- {
- CvMat iCode = cvMat(1, FEATURE_MAX_D, CV_32FC1);
- cvGetRow(m_pCodebook, &iCode, j);
- CvMat *pTmpMat = cvCreateMat(FEATURE_MAX_D, 1, CV_32FC1);
- cvTranspose(&iCode, pTmpMat);
- double dbDistance = cvNorm(&iIP, pTmpMat); //計算第i個IP與第j個code之間的距離
- pszDistance[j] = dbDistance;
- cvReleaseMat(&pTmpMat);
- }
- double dbMinDistance = pszDistance[0];
- int nCodebookIndex = 0; //第i個IP在codebook中距離最小的code的索引值
- for (int j = 1; j < cnClusterNumber; ++j)
- {
- if (dbMinDistance > pszDistance[j])
- {
- dbMinDistance = pszDistance[j];
- nCodebookIndex = j;
- }
- }
- ++pszImgHistogram[nCodebookIndex];
- delete []pszDistance;
- }
- CvMat iImgHistogram = cvMat(cnClusterNumber, 1, CV_32SC1, pszImgHistogram);
- return iImgHistogram;
- }
- CvMat *CHistogram::GetObservedData()
- {
- CvMat iHistogram;
- cvInitMatHeader(&iHistogram, m_nImgNumber, cnClusterNumber, CV_32SC1, m_pszHistogram);
- CvMat *m_pObservedData = cvCreateMat(iHistogram.cols, iHistogram.rows, CV_32SC1);
- cvTranspose(&iHistogram, m_pObservedData);
- return m_pObservedData;
- }