OpenCV影象的輪廓的匹配
一個跟輪廓相關的最常用到的功能是匹配兩個輪廓.如果有兩個輪廓,如何比較它們;或者如何比較一個輪廓和另一個抽象模板.
矩
比較兩個輪廓最簡潔的方式是比較他們的輪廓矩.這裡先簡短介紹一個矩的含義.簡單的說,矩是通過對輪廓上所有點進行積分運算(或者認為是求和運算)而得到的一個粗略特徵.通常,我們如下定義一個輪廓的(p,q)矩:
在公式中p對應x緯度上的矩,q對應y維度上的矩,q對應y維度上的矩,階數表示對應的部分的指數.該計算是對輪廓邊界上所有畫素(數目為n)進行求和.如果p和q全為0,那麼m00實際上對輪廓邊界上點的數目.
下面的函式用於計算這些輪廓矩
void cvContoursMoments(CvSeq* contour,CvMoments* moments)
第一個引數是我們要處理的輪廓,第二個引數是指向一個結構,該結構用於儲存生成的結果.CvMonments結構定義如下
/* Spatial and central moments */ typedef struct CvMoments { double m00, m10, m01, m20, m11, m02, m30, m21, m12, m03; /* spatial moments */ double mu20, mu11, mu02, mu30, mu21, mu12, mu03; /* central moments */ double inv_sqrt_m00; /* m00 != 0 ? 1/sqrt(m00) : 0 */ } CvMoments;
在cvContourMoments()函式中,只用到m00,m01,...,m03幾個引數;以mu開頭的引數在其他函式中使用.在使用CvMoment結構的時候,我們可以使用以下的函式來方便地一個特定的矩:
CVAPI(double) cvGetSpatialMoment( CvMoments* moments, int x_order, int y_order );
呼叫cvContoursMonments()函式會計算所有3階的矩(m21和m12會被計算,但是m22不會被計算).再論矩
剛剛描述的矩計算給出了一些輪廓的簡單屬性,可以用來比較兩個輪廓.但是在很多實際使用中,剛才的計算方法得到的矩並不是做比較時的最好的引數.具體說來,經常會用到歸一化的矩(因此,不同大小但是形狀相同的物體會有相同的值).同樣,剛才的小節中的簡單的矩依賴於所選座標系,這意味這物體旋轉後就無法正確匹配.
OpenCV提供了計算Hu不變矩[Hu62]以及其他歸一化矩的函式.CvMoments結構可以用cvmoments或者cvContourMoments計算.並且,cvContourMoments現在只是cvMoments的一個別名.
一個有用的小技巧是用cvDrawContour()描繪一幅輪廓的影象後,呼叫一個矩的函式處理該影象.使用無論輪廓填充與否,你都能用同一個函式處理.
以下是4個相關函式的定義:
/* Calculates all spatial and central moments up to the 3rd order */ CVAPI(void) cvMoments( const CvArr* arr, CvMoments* moments, int binary CV_DEFAULT(0));
CVAPI(double) cvGetCentralMoment( CvMoments* moments, int x_order, int y_order );
CVAPI(double) cvGetNormalizedCentralMoment(CvMoments* moments,int x_order, int y_order);
/* Calculates 7 Hu's invariants from precalculated spatial and central moments */ CVAPI(void) cvGetHuMoments( CvMoments* moments, CvHuMoments* hu_moments );
第一個函式除了使用的是影象(而不是輪廓)作為引數,其他方面和cvContoursMoments()函式相同,另外還增加了一個引數.增加的引數isBinary如果為CV_TRUE,cvMoments將把影象當作二值影象處理,所有的非0畫素都當作1.當函式被呼叫的時候,所有的矩被計算(包含中心矩,請看下一段).除了x和y的值被歸一化到以0為均值,中心距本質上跟剛才描述的矩一樣.歸一化矩和中心矩也基本相同,除了每個矩都要除以m00的某個冪:
最後來介紹Hu矩,Hu矩是歸一化中心距的線性組合.之所以這樣做是為了能夠獲取代表影象某個特性的矩函式,這些矩函式對於某些變化如縮放,旋轉和映象對映(除了h1)具有不變性.Hu矩是從中心矩中計算得到,其計算公式如下所示:
參考圖8-9和表8-1,我們可以直觀地看到每個影象對應的7個Hu矩.通過觀察可以發現,當階數變高時,Hu矩一般會變小.對於這一點不必感到奇怪,因為根據定義,高階Hu矩由多個歸一化矩的高階冪計算得到,而歸一化矩都是小於1的,所以指數越大,計算所得的值越小.
需要特別注意的是"I",它對於180度旋轉和鏡面反射都是對稱的,它的h3到h7矩都是0;而"O"具有同樣的對稱特性,所有的Hu矩都是非0的.使用Hu矩進行匹配
/* Compares two contours by matching their moments */ CVAPI(double) cvMatchShapes( const void* object1, const void* object2, int method, double parameter CV_DEFAULT(0));
很自然,使用Hu矩我們想要比較兩個物體並且判明他們是否相似.當然,可能有很多"相似"的定義.為了使比較過程變得簡單,OpenCV的函式cvMatShapes()允許我們簡單地提供兩個物體,然後計算他們的矩並根據我們提供的標準進行比較.這些物體可以是灰度圖影象或者輪廓.如果你提供了影象,cvMatchShape()會在對比的程序之間為你計算矩.cvMatchShapes()使用的方法是表8-2中列出的三種中的一種.
關於對比度量標準(metric)是如何被計算的,表8-2中的三個常量每個都用了不同的方法.這個度量標準最終決定了cvMatchShapes()的返回值.最後一個引數變數現在不能用,因此我們可以把它設成預設值0.
等級匹配
我們經常想要匹配兩個輪廓,然後用一個相似度量來計算輪廓所有匹配部分.使用概況引數的方法(比如矩)是相當快的,但是他們能夠表達的資訊卻不是很多.
為了找到一個更精確的相似度量度,首先考慮一下輪廓樹的結構應該會有幫助.請注意,此外的輪廓樹是用來表述一個特定形狀(不是多個特定形狀)內各部分的等級關係.
類似於cvFindContours()著怎樣的函式放回多個輪廓,輪廓樹(contout tree)並不會把這些等級關係搞混,事實上,他正是對於某個特定輪廓形狀的登記描述.
理解了輪廓樹的建立會比較容易理解輪廓樹.從一個輪廓建立一個輪廓樹是從底端(葉節點)到頂端(根節點)的.首相搜尋三角形突出或凹陷的形狀的周邊(輪廓上的每一個點都不是完全和它的相鄰點共線的).每個這樣的三角形被一條線段代替,這條線段通過連線非相鄰點的兩點得到;因此實際上三角形或者被削平(例如,圖8-10的三角形D)或者被填滿(三角形C).每個這樣的替代把輪廓的頂點減少1,並且給輪廓建立一個新節點.如果這樣一個三角形的兩側有原始的邊,那麼它就是得到的輪廓樹的葉子;如果一側是已存在三角形,那麼他就是那個三角形的父節點.這個過程的迭代最終把物體的外形剪成一個四邊形,這個四邊形也被剖開;得到的兩個三角形是根節點的兩個子節點.
結果的二分樹(圖8-11)最終將原始輪廓的形狀資訊編碼.每個節點被它對應的三角形資訊(比如三角形的大小,它的生成是被切出來還是被填進去的,這樣的資訊)所註釋
這些樹一旦被建立,就可以很有效的對比兩個輪廓.這個過程開始定義兩個樹節點的對應關係,然後比較對應節點的特性.對吼的結果就是兩個樹的相似度.
事實上,我們基本不需要理解這個過程.OpenCV提供了一個函式從普通的CvContour物件自動生成輪廓樹並轉換返回;還提供一個函式用來對比兩個樹.不幸的是,建立的輪廓樹並不太魯棒(例如,輪廓上很小的改變可能會徹底改變結果的樹).同事,最初的三角形(樹的根節點)是隨意選取的.因此,為了得到較好的描述實現使用函式cvApproxPoly()之後將輪廓排列(運用迴圈移動)成最初的三角形不怎麼受到旋轉影響的狀態.
CvContourTree* cvCreateContourTree(const CvSeq* contour,CvMemStorage* storage,double threshold);
CvSeq * cvContourFromContourTree(const CvContourTree* tree,CvMemStorage* storage, CvTermCriteria criteris);
double cvMatchContourTrees(const CvContourTree* tree1,const CvContourTree* tree2,int method,double threshold);
這個程式碼提到了CvTremCriteria(),該函式細節將在第9章給出.現在可以用下面的預設值使用cvTermCriteria()簡單建立一個結構體.
CvTermCriteria termcrit = cvTermCriteria(CV_TERMCRIT_ITER | CV_TeRMCRT_EPS,5,1);
輪廓的凸包和凸缺陷
另一個理解物體形狀或輪廓的有用的方法是計算一個物體的凸包(convex hull)然後計算其凸缺陷(convexity defects)[Homma85].很多複雜物體的特效能很好的被這種缺陷表現出來.
圖8-12用人手舉例說明了凸缺陷這一概念.手周圍深色的線描畫出了凸包,A到H被標出的區域是凸包的各個"缺陷".正如所看到的,這些凸度缺陷提供了手以及手狀態的特徵表現的方法.
enum { CV_CLOCKWISE =1, CV_COUNTER_CLOCKWISE =2 };
/* Calculates exact convex hull of 2d point set */ CVAPI(CvSeq*) cvConvexHull2( const CvArr* input, void* hull_storage CV_DEFAULT(NULL), int orientation CV_DEFAULT(CV_CLOCKWISE), int return_points CV_DEFAULT(0));
/* Checks whether the contour is convex or not (returns 1 if convex, 0 if not) */ CVAPI(int) cvCheckContourConvexity( const CvArr* contour );
/* Finds convexity defects for the contour */ CVAPI(CvSeq*) cvConvexityDefects( const CvArr* contour, const CvArr* convexhull, CvMemStorage* storage CV_DEFAULT(NULL));
OpenCV有三個關於凸包和凸缺陷的重要函式.第一個函式簡單計算已知輪廓的凸包,第二個函式用來檢查一個已知輪廓是否是凸的.第三個函式在已知輪廓是凸包的情況下計算凸缺陷.
函式cvConvexHull2()的第一個引數是點的陣列,這個陣列是一個n行2列的矩陣(n×2),或者是一個輪廓.如果是點矩陣,點應該是32位整型(CV_32SC1)或者是浮點型(CV_32F1).下一個引數是指向記憶體儲存的一個指標,為結果分配記憶體空間.下一引數是CV_CLOCkWISE或者是CV_COUNTERCLOCkWISE中的一個.這引數決定了程式返回點的排列方向.最後一個引數returnPoints,可以是0或1.如果設定為1,點會被儲存在返回陣列中.如果設定為0,只有索引被儲存在返回陣列中.索引是傳遞給cvConvexHull2()的原始陣列索引.
讀著可能要問:"如果引數hull_storage是記憶體儲存,為什麼它的型別是void* 而不是CvMemSotrage* ?",這是因為很多時候作為凸包放回的點的形式,陣列可能比序列更加有用.可慮到這一點,引數hull_storage的另一個可能性是傳遞一個指向矩陣的指標CvMat*. 這種情況下,矩陣應該是一維的且和輸入點的個數相同.當cvConvexHull2()被呼叫的時候,它會修改矩陣頭來指明當前的列數.
有時候,已知一個輪廓但並不知道它是否是凸的.這種情況下,我們可以呼叫函式cvCheckContourConvexity().這個測試簡單快速,但是如果傳遞的輪廓自身有交叉的時候不會得到正確的結果.
第三個函式cvConvexityDefects(),計算凸缺陷返回一個缺陷的序列.為了完成這個任務,cvConvexityDefects()要求輸入輪廓,凸包和記憶體空間,從這個記憶體空間來獲得存放結果序列的記憶體.前兩個引數是CvArr*,和傳遞給cvConvexHull2()的引數input的形式相同.
typedef struct CvConvexityDefect { CvPoint* start; /* point of the contour where the defect begins */ CvPoint* end; /* point of the contour where the defect ends */ CvPoint* depth_point; /* the farthest from the convex hull point within the defect */ float depth; /* distance between the farthest point and the convex hull */ } CvConvexityDefect;
函式cvConvexityDefects()返回一個CvConvexityDefect結構體的序列,其中包括一些簡單的引數用來描述凸缺陷.start和end是凸包上的缺陷的起始點和終止點.depth_point是缺陷中的距離凸包的邊(跟該缺陷有關的凸包便)最遠的點.最後一個引數depth是最遠點和包的邊(edge)的距離.成對幾何直方圖
Freeman鏈碼編碼是對一個多邊形的的序列如何"移動"的描述,每個這樣的移動有固定長度和特定的方向.但是,我們並沒有更多說明為什麼需要用到這種描述.
Freeman鏈碼編碼的用處很多,但最常見的一種值得深入瞭解一下,因為它支援了成對幾何直方圖(PGH)的基本思想.
PGH實際上是鏈碼編碼直方圖(CCH)的一個擴充套件或延伸.CCH是一種直方圖,用來統計一個輪廓的Freeman鏈碼編碼每一種走法的數字.這種直方圖有一些良好的性質.最顯著的是,將物體旋轉45度,那麼新的直方圖是老直方圖的迴圈平移(圖8-13).這就提供了一個不被此類旋轉影響的形狀識別方法.
PGH的構成如下圖所示(圖8-14).多邊形的每一個邊被選擇成為"基準邊".之後考慮其他的邊相對於這些基礎邊的關係,並且計算三個值:dmin,dmax和θ.dmin是兩條邊的最小距離,dmax是最大距離,θ是兩邊的夾角.PGH是一個二維直方圖,其兩個維度分別是角度和距離.對於每一對邊,有兩個bin,一個bin為(dmin,θ),另一個bin為(dmax,θ).對於這樣的每一組邊,這兩個bin都被增長,中間值d(dmin和dmax之間的值)同樣也被增長.
PGh的使用和FCC相似.一個重要不同是,PGH的描述能力更強,因此在嘗試解決複雜問題的時候很有用,比如說大量形狀需要被辨識,並且/或者有很多背景噪聲的時候.用來計算PGh的函式是
void cvCalcPGH(const CvSeq* contour,CvHistogram* hist)
在這裡輪廓可以包含整數值的點的座標;當然直方圖必須是二維的.