作者:王先榮
前言
輪廓是構(gòu)成任何一個(gè)形狀的邊界或外形線。前面講了如何根據(jù)色彩及色彩的分布(直方圖對(duì)比和模板匹配)來(lái)進(jìn)行匹配,現(xiàn)在我們來(lái)看看如何利用物體的輪廓。包括以下內(nèi)容:輪廓的查找、表達(dá)方式、組織方式、繪制、特性、匹配。
查找輪廓
首先我們面對(duì)的問(wèn)題是如何在圖像中找到輪廓,OpenCv(EmguCv)為我們做了很多工作,我們的任務(wù)只是調(diào)用現(xiàn)成的函數(shù)而已。Image<TColor,TDepth>類的FindContours方法可以很方便的查找輪廓,不過(guò)在查找之前,我們需要將彩色圖像轉(zhuǎn)換成灰度圖像,然后再將灰度圖像轉(zhuǎn)換成二值圖像。代碼如下所示:
 查找輪廓
Image<Bgr, Byte> imageSource = new Image<Bgr, byte>(sourceImageFileName); //獲取源圖像 Image<Gray, Byte> imageGray = imageSource.Convert<Gray, Byte>(); //將源圖像轉(zhuǎn)換成灰度圖像 int thresholdValue = tbThreshold.Value; //用于二值化的閥值 Image<Gray, Byte> imageThreshold = imageGray.ThresholdBinary(new Gray(thresholdValue), new Gray(255d)); //對(duì)灰度圖像二值化 Contour<Point> contour=imageThreshold.FindContours();
輪廓的表達(dá)方式
使用上面的代碼可以得到圖像的默認(rèn)輪廓,但是輪廓在電腦中是如何表達(dá)的呢?在OpenCv(EmguCv)中提供了兩類表達(dá)輪廓的方式:頂點(diǎn)的序列、Freeman鏈碼。
1.頂點(diǎn)的序列
用多個(gè)頂點(diǎn)(或各點(diǎn)間的線段)來(lái)表達(dá)輪廓。假設(shè)要表達(dá)一個(gè)從(0,0)到(2,2)的矩形, (1)如果用點(diǎn)來(lái)表示,那么依次存儲(chǔ)的可能是:(0,0),(1,0),(2,0),(2,1),(2,2),(1,2),(0,2),(0,1); (2)如果用點(diǎn)間的線段來(lái)表達(dá)輪廓,那么依次存儲(chǔ)的可能是:(0,0),(2,0),(2,2),(0,2)。 以下代碼可以用來(lái)獲取輪廓上的點(diǎn):
for (int i = 0; i < contour.Total; i++) sbContour.AppendFormat("{0},", contour[i]);
2.Freeman鏈碼
Freeman鏈碼需要一個(gè)起點(diǎn),以及從起點(diǎn)出發(fā)的一系列位移。每個(gè)位移有8個(gè)方向,從0~7分別指向從正北開(kāi)始的8個(gè)方向。假設(shè)要用Freeman鏈碼表達(dá)從(0,0)到(2,2)的矩形,可能的表示方法是:起點(diǎn)(0,0),方向鏈2,2,4,4,6,6,0,0。
EmguCv對(duì)Freeman鏈碼的支持很少,我們需要做一系列的工作才能在.net中使用Freeman鏈碼: (1)獲取Freeman鏈碼
 查找用Freeman鏈碼表示的輪廓
//查找用Freeman鏈碼表示的輪廓 Image<Gray,Byte> imageTemp=imageThreshold.Copy(); IntPtr storage = CvInvoke.cvCreateMemStorage(0); IntPtr ptrFirstChain = IntPtr.Zero; int total = CvInvoke.cvFindContours(imageTemp.Ptr, storage, ref ptrFirstChain, sizeof(MCvChain), mode, CHAIN_APPROX_METHOD.CV_CHAIN_CODE, new Point(0, 0));
(2)遍歷Freeman鏈碼上的點(diǎn)
 讀取Freeman鏈碼上的點(diǎn)
//初始化Freeman鏈碼讀取 [DllImport("cv200.dll")] public static extern void cvStartReadChainPoints(IntPtr ptrChain,IntPtr ptrReader); //讀取Freeman鏈碼的點(diǎn) [DllImport("cv200.dll")] public static extern Point cvReadChainPoint(IntPtr ptrReader); [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Ansi)] //定義鏈碼讀取結(jié)構(gòu) public struct MCvChainPtReader { //seqReader public MCvSeqReader seqReader; /// char public byte code; /// POINT->tagPOINT public Point pt; /// char[16] [System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.ByValTStr, SizeConst = 16)] public string deltas; }
//將鏈碼指針轉(zhuǎn)換成結(jié)構(gòu) MCvChain chain=(MCvChain)Marshal.PtrToStructure(ptrChain,typeof(MCvChain)); //定義存放鏈碼上點(diǎn)的列表 List<Point> pointList = new List<Point>(chain.total); //鏈碼讀取結(jié)構(gòu) MCvChainPtReader chainReader = new MCvChainPtReader(); IntPtr ptrReader = Marshal.AllocHGlobal(sizeof(MCvSeqReader) + sizeof(byte) + sizeof(Point) + 16 * sizeof(byte)); Marshal.StructureToPtr(chainReader, ptrReader, false); //開(kāi)始讀取鏈碼 cvStartReadChainPoints(ptrChain, ptrReader); int i = 0; while (ptrReader != IntPtr.Zero && i < chain.total) { //依次讀取鏈碼上的每個(gè)點(diǎn) Point p = cvReadChainPoint(ptrReader); if (ptrReader == IntPtr.Zero) break; else { pointList.Add(p); sbChain.AppendFormat("{0},", p); i++; } } imageResult.DrawPolyline(pointList.ToArray(), true, new Bgr(lblExternalColor.BackColor), 2);
需要注意的是:cvReadChainPoint函數(shù)似乎永遠(yuǎn)不會(huì)滿足循環(huán)終止的條件,即ptrReader永遠(yuǎn)不會(huì)被置為null,這跟《學(xué)習(xí)OpenCv》和參考上不一致;我們需要用chain.total來(lái)輔助終止循環(huán),讀取了所有的點(diǎn)之后就可以罷手了。
輪廓之間的組織方式
在查找到輪廓之后,不同輪廓是怎么組織的呢?根據(jù)不同的選擇,它們可能是:(1)列表;(2)雙層結(jié)構(gòu);(3)樹(shù)型結(jié)構(gòu)。
從縱向上來(lái)看,列表只有一層,雙層結(jié)構(gòu)有一或者兩層,樹(shù)型結(jié)構(gòu)可能有一層或者多層。 如果要遍歷所有的輪廓,可以使用遞歸的方式,代碼如下:
 遍歷輪廓
//遍歷輪廓,并生成遍歷結(jié)果 private void TravelContour(Contour<Point> contour,ref int total,ref StringBuilder sbContour) { if (contour != null) { sbContour.Append("------------------------\r\n"); sbContour.AppendFormat("輪廓{0},右節(jié)點(diǎn):{1},下級(jí)節(jié)點(diǎn):{2},外接矩形:({3})\r\n", total, contour.HNext != null, contour.VNext != null, contour.BoundingRectangle); sbContour.AppendFormat("包含{0}個(gè)點(diǎn)(面積:{1},周長(zhǎng):{2}):\r\n", contour.Total, contour.Area, contour.Perimeter); for (int i = 0; i < contour.Total; i++) sbContour.AppendFormat("{0},", contour[i]); sbContour.Append("\r\n"); total++; if (contour.HNext != null) TravelContour(contour.HNext, ref total, ref sbContour); if (contour.VNext != null) TravelContour(contour.VNext, ref total, ref sbContour); } }
輪廓的繪制
輪廓的繪制比較簡(jiǎn)單,用上面提到的方法取得輪廓的所有點(diǎn),然后把這些點(diǎn)連接成一個(gè)多邊形即可。
當(dāng)然,對(duì)于用頂點(diǎn)序列表示的輪廓,用Image<TColor,TDepth>.Draw方法或者cvDrawContours函數(shù)可以很方便的繪制出輪廓。我發(fā)現(xiàn),如果將參數(shù)max_level設(shè)置成2,可以繪制出所有的輪廓。
繪制輪廓的代碼如下:
 繪制輪廓
Image<Bgr, Byte> imageResult = imageThreshold.Convert<Bgr, Byte>(); //結(jié)果圖像 int maxLevel = 0; //繪制的輪廓深度 int.TryParse(txtMaxLevel.Text, out maxLevel); imageResult.Draw(contour, new Bgr(lblExternalColor.BackColor), new Bgr(lblHoleColor.BackColor), maxLevel, 2);
 輪廓的特性
輪廓的特性有很多,下面一一介紹。
1.輪廓的多邊形逼近 輪廓的多邊形逼近指的是:使用多邊形來(lái)近似表示一個(gè)輪廓。
多邊形逼近的目的是為了減少輪廓的頂點(diǎn)數(shù)目。 多邊形逼近的結(jié)果依然是一個(gè)輪廓,只是這個(gè)輪廓相對(duì)要粗曠一些。
可以使用Contour<Point>.ApproxPoly方法或者cvApproxyPoly函數(shù)來(lái)對(duì)輪廓進(jìn)行多邊形逼近,示例代碼如下:
contour = firstContour.ApproxPoly(double.Parse(txtApproxParameter.Text), 2, new MemStorage());
2.輪廓的關(guān)鍵點(diǎn) 輪廓的關(guān)鍵點(diǎn)是:輪廓上包含曲線信息比較多的點(diǎn)。關(guān)鍵點(diǎn)是輪廓頂點(diǎn)的子集。
可以使用cvFindDominantPoints函數(shù)來(lái)獲取輪廓上的關(guān)鍵點(diǎn),該函數(shù)返回的結(jié)果一個(gè)包含 關(guān)鍵點(diǎn)在輪廓頂點(diǎn)中索引
的序列。再次強(qiáng)調(diào):是索引,不是具體的點(diǎn)。如果要得到關(guān)鍵點(diǎn)的具體坐標(biāo),可以用索引到輪廓上去找。 以下代碼演示了如何獲取輪廓上的關(guān)鍵點(diǎn):
 輪廓的關(guān)鍵點(diǎn)
//得到關(guān)鍵點(diǎn)信息 private void GetDominantPointsInfo(Contour<Point> contour, ref StringBuilder sbContour, ref Image<Bgr, Byte> imageResult, double parameter1, double parameter2, double parameter3, double parameter4, Bgr dominantPointColor) { if (contour.Total > 2) { MemStorage storage = new MemStorage(); try { IntPtr ptrSeq = cvFindDominantPoints(contour.Ptr, storage.Ptr, (int)CV_DOMINANT.CV_DOMINANT_IPAN, parameter1, parameter2, parameter3, parameter4); Seq<int> seq = new Seq<int>(ptrSeq, storage); sbContour.AppendFormat("{0}個(gè)關(guān)鍵點(diǎn):\r\n", seq.Total); for (int i = 0; i < seq.Total; i++) { int idx = seq[i]; //關(guān)鍵點(diǎn)序列中存儲(chǔ)的數(shù)據(jù) 是 關(guān)鍵點(diǎn)在輪廓中所處位置的索引 Point p = contour[idx]; //得到關(guān)鍵點(diǎn)的坐標(biāo) sbContour.AppendFormat("{0}({1},{2}),", idx, p.X, p.Y); imageResult.Draw(new CircleF(new PointF(p.X, p.Y), 3), dominantPointColor, -1); } sbContour.Append("\r\n"); } catch (CvException ex) { sbContour.AppendFormat("在獲取關(guān)鍵點(diǎn)時(shí)發(fā)生異常,錯(cuò)誤描述:{0},錯(cuò)誤源:{1},錯(cuò)誤堆棧:{2}\r\n錯(cuò)誤文件:{3},函數(shù)名:{4},行:{5},錯(cuò)誤內(nèi)部描述:{6}\r\n", ex.Message, ex.Source, ex.StackTrace, ex.FileName, ex.FunctionName, ex.Line, ex.ErrorStr); } catch (Exception e) { sbContour.AppendFormat("在獲取關(guān)鍵點(diǎn)時(shí)發(fā)生異常,錯(cuò)誤描述:{0},錯(cuò)誤源:{1},錯(cuò)誤堆棧:{2}\r\n", e.Message, e.Source, e.StackTrace); } finally { storage.Dispose(); } } }
3.輪廓的周長(zhǎng)和面積
輪廓的周長(zhǎng)可以用Contour<Point>.Perimeter屬性或者cvArcLength函數(shù)來(lái)獲取。
輪廓的面積可以用Contour<Point>.Area屬性或者cvContourArea函數(shù)來(lái)獲取。
4.輪廓的邊界框 有三種常見(jiàn)的邊界框:矩形、圓形、橢圓。
(1)矩形:在圖像處理系統(tǒng)中提供了一種叫Rectangle的矩形,不過(guò)它只能表達(dá)邊垂直或水平的特例;OpenCv中還有一種叫Box的矩形,它跟數(shù)學(xué)上的矩形一致,只要4個(gè)角是直角即可。
如果要獲取輪廓的Rectangle,可以使用Contour<Point>.BoundingRectangle屬性或者cvBoundingRect函數(shù)。
如果要獲取輪廓的Box,可以使用Contour<Point>.GetMinAreaRect方法或者cvMinAreaRect2函數(shù)。
(2)圓形 如果要獲取輪廓的圓形邊界框,可以使用cvMinEnclosingCircle函數(shù)。 (3)橢圓
如果要獲取輪廓的橢圓邊界框,可以使用cvFitEllipse2函數(shù)。 下列代碼演示了如何獲取輪廓的各種邊界框:
 輪廓的邊界框
//得到邊界框信息 private void GetEdgeInfo(Contour<Point> contour, string edge, ref StringBuilder sbContour, ref Image<Bgr, Byte> imageResult, Bgr edgeColor) { if (edge == "Rect") //矩形 imageResult.Draw(contour.BoundingRectangle, edgeColor, 2); else if (edge == "MinAreaRect") { //最小矩形 MCvBox2D box = CvInvoke.cvMinAreaRect2(contour.Ptr, IntPtr.Zero); PointF[] points = box.GetVertices(); Point[] ps = new Point[points.Length]; for (int i = 0; i < points.Length; i++) ps[i] = new Point((int)points[i].X, (int)points[i].Y); imageResult.DrawPolyline(ps, true, edgeColor, 2); } else if (edge == "Circle") { //圓形 PointF center; float radius; CvInvoke.cvMinEnclosingCircle(contour.Ptr, out center, out radius); imageResult.Draw(new CircleF(center, radius), edgeColor, 2); } else { //橢圓 if (contour.Total >= 6) { MCvBox2D box = CvInvoke.cvFitEllipse2(contour.Ptr); imageResult.Draw(new Ellipse(box), edgeColor, 2); } else sbContour.Append("輪廓點(diǎn)數(shù)小于6,不能創(chuàng)建外圍橢圓。\r\n"); } }
5.輪廓的矩
我們可以使用Contour<Point>.GetMoments方法或者cvMoments函數(shù)方便的得到輪廓的矩集,然后再相應(yīng)的方法或函數(shù)獲取各種矩。
特定的矩:MCvMoments.GetSpatialMoment方法、cvGetSpatialMoment函數(shù)
中心矩:MCvMoments.GetCentralMoment方法、cvGetCentralMoment函數(shù)
歸一化中心矩:MCvMoments.GetNormalizedCentralMoment方法、cvGetNormalizedCentralMoment函數(shù)
Hu矩:MCvMoments.GetHuMoment方法、McvHuMoments.hu1~hu7字段、cvGetHuMoments函數(shù)
以下代碼演示了如何獲取輪廓的矩:
 輪廓的矩
//得到各種矩的信息 private void GetMomentsInfo(Contour<Point> contour, ref StringBuilder sbContour) { //矩 MCvMoments moments = contour.GetMoments(); //遍歷各種情況下的矩、中心矩及歸一化矩,必須滿足條件:xOrder>=0; yOrder>=0; xOrder+yOrder<=3; for (int xOrder = 0; xOrder <= 3; xOrder++) { for (int yOrder = 0; yOrder <= 3; yOrder++) { if (xOrder + yOrder <= 3) { double spatialMoment = moments.GetSpatialMoment(xOrder, yOrder); double centralMoment = moments.GetCentralMoment(xOrder, yOrder); double normalizedCentralMoment = moments.GetNormalizedCentralMoment(xOrder, yOrder); sbContour.AppendFormat("矩(xOrder:{0},yOrder:{1}),矩:{2:F09},中心矩:{3:F09},歸一化矩:{4:F09}\r\n", xOrder, yOrder, spatialMoment, centralMoment, normalizedCentralMoment); } } } //Hu矩 MCvHuMoments huMonents = moments.GetHuMoment(); sbContour.AppendFormat("Hu矩 h1:{0:F09},h2:{1:F09},h3:{2:F09},h4:{3:F09},h5:{4:F09},h6:{5:F09},h7:{6:F09}\r\n", huMonents.hu1, huMonents.hu2, huMonents.hu3, huMonents.hu4, huMonents.hu5, huMonents.hu6, huMonents.hu7); }
6.輪廓的輪廓樹(shù)
輪廓樹(shù)用來(lái)描述某個(gè)特定輪廓的內(nèi)部特征。注意:輪廓樹(shù)跟輪廓是一一對(duì)應(yīng)的關(guān)系;輪廓樹(shù)不用于描述多個(gè)輪廓之間的層次關(guān)系。
可以用函數(shù)cvCreateContourTree來(lái)構(gòu)造輪廓樹(shù)。
IntPtr ptrTree1 = CvInvoke.cvCreateContourTree(contour1.Ptr, new MemStorage().Ptr, thresholdOfCreate);
7.輪廓的凸包和凸缺陷
輪廓的凸包和凸缺陷用于描述物體的外形。凸包和凸缺陷很容易獲得,不過(guò)我目前不知道它們到底怎么使用。
如果要判斷輪廓是否是凸的,可以用Contour<Point>.Convex屬性和cvCheckContourConvexity函數(shù)。
如果要獲取輪廓的凸包,可以用Contour<Point>.GetConvexHull方法或者cvConvexHull2函數(shù),返回的是包含頂點(diǎn)的序列。
如果要獲取輪廓的凸缺陷,可以用Contour<Point>.GetConvexityDefacts方法或者cvConvexityDefects函數(shù)。
注意:EmguCv將缺陷的單詞拼寫(xiě)錯(cuò)了,defect才是缺陷。 以下代碼演示了如何獲取輪廓的凸包及凸缺陷:
 輪廓的凸包和凸缺陷
//得到凸包及缺陷信息 private void GetConvexInfo(Contour<Point> contour,ref StringBuilder sbContour,ref Image<Bgr,Byte> imageResult) { if (!contour.Convex) //判斷輪廓是否為凸 { //凸包 Seq<Point> convexHull = contour.GetConvexHull(ORIENTATION.CV_CLOCKWISE); //缺陷 Seq<MCvConvexityDefect> defects = contour.GetConvexityDefacts(new MemStorage(), ORIENTATION.CV_CLOCKWISE); //顯示信息 sbContour.AppendFormat("輪廓的凸包有{0}個(gè)點(diǎn),依次為:", convexHull.Total); Point[] points = new Point[convexHull.Total]; for (int i = 0; i < convexHull.Total; i++) { Point p = convexHull[i]; points[i] = p; sbContour.AppendFormat("{0},", p); } sbContour.Append("\r\n"); imageResult.DrawPolyline(points, true, new Bgr(lblConvexColor.BackColor), 2); MCvConvexityDefect defect; sbContour.AppendFormat("輪廓有{0}個(gè)缺陷,依次為:\r\n", defects.Total); for (int i = 0; i < defects.Total; i++) { defect = defects[i]; sbContour.AppendFormat("缺陷:{0},起點(diǎn):{1},終點(diǎn):{2},最深的點(diǎn):{3},深度:{4}\r\n", i, defect.StartPoint, defect.EndPoint, defect.DepthPoint, defect.Depth); } } else sbContour.Append("輪廓是凸的,凸包和輪廓一樣。\r\n"); }
8.輪廓的成對(duì)幾何直方圖 成對(duì)幾何直方圖的資料比較少,我是這么理解的。
(1)輪廓保存的是一系列的頂點(diǎn),輪廓是由一系列線段組成的多邊形。對(duì)于看起來(lái)光滑的輪廓(例如圓),只是線段條數(shù)比較多,線段長(zhǎng)度比較短而已。實(shí)際上,電腦中顯示的任何曲線都由線段組成。
(2)每?jī)蓷l線段之間都有一定的關(guān)系,包括它們(或者它們的延長(zhǎng)線)之間的夾角,兩條線段的夾角范圍是:(0,180)。
(3)每?jī)蓷l線段上的點(diǎn)之間還有距離關(guān)系,包括最短(?。┚嚯x、最遠(yuǎn)(大)距離,以及平均距離。最大距離我用了一個(gè)偷懶的計(jì)算方法,我把輪廓外界矩形的對(duì)角線長(zhǎng)度看作了最大距離。
(4)成對(duì)幾何直方圖所用的統(tǒng)計(jì)數(shù)據(jù)包括了夾角和距離。 可以用函數(shù)cvCalcPGH來(lái)計(jì)算輪廓的成對(duì)幾何直方圖,示例代碼如下:
 輪廓的成對(duì)幾何直方圖
//生成成對(duì)幾何直方圖 Rectangle rect1 = contour1.BoundingRectangle; float maxDist1 = (float)Math.Sqrt(rect1.Width * rect1.Width + rect1.Height * rect1.Height); //輪廓的最大距離:這里使用輪廓矩形邊界框的對(duì)角線長(zhǎng)度 int[] bins1 = new int[] { 60, 20 }; RangeF[] ranges1 = new RangeF[] { new RangeF(0f, 180f), new RangeF(0f, maxDist1) }; //直方圖第0維為角度,范圍在(0,180),第2維為輪廓兩條邊緣線段的距離 DenseHistogram hist1 = new DenseHistogram(bins1, ranges1); CvInvoke.cvCalcPGH(contour1.Ptr, hist1.Ptr);

輪廓的匹配
如果要比較兩個(gè)物體,可供選擇的特征很多。如果要判斷某個(gè)人的性別,可以根據(jù)他(她)頭發(fā)的長(zhǎng)短來(lái)判斷,這很直觀,在長(zhǎng)發(fā)男稀有的年代準(zhǔn)確率也很高。也可以根據(jù)這個(gè)人尿尿的射程來(lái)判斷,如果射程大于0.50米,則是男性。總之,方法很多,不一而足。
我們?cè)谏衔闹械玫搅溯喞倪@么多特征,它們也可以用于進(jìn)行匹配。典型的輪廓匹配方法有:Hu矩匹配、輪廓樹(shù)匹配、成對(duì)幾何直方圖匹配。 1.Hu矩匹配
輪廓的Hu矩對(duì)包括縮放、旋轉(zhuǎn)和鏡像映射在內(nèi)的變化具有不變性。Contour<Point>.MatchShapes方法和cvMatchShapes函數(shù)可以很方便的實(shí)現(xiàn)對(duì)2個(gè)輪廓間的匹配。 2.輪廓樹(shù)匹配
用樹(shù)的形式比較兩個(gè)輪廓。cvMatchContourTrees函數(shù)實(shí)現(xiàn)了輪廓樹(shù)的對(duì)比。 3.成對(duì)幾何直方圖匹配
在得到輪廓的成對(duì)幾何直方圖之后,可以使用直方圖對(duì)比的方法來(lái)進(jìn)行匹配。如果您和我一樣忘記了直方圖的對(duì)比方式,可以看看我寫(xiě)的另一篇文章《顏色直方圖的計(jì)算、顯示、處理、對(duì)比及反向投影(How
to Use Histogram? Calculate, Show, Process, Compare and BackProject)》。
各種輪廓匹配的示例代碼如下:
 輪廓的匹配
 //開(kāi)始匹配 private void btnStartMatch_Click(object sender, EventArgs e) { //準(zhǔn)備輪廓(這里只比較最外圍的輪廓) Image<Bgr, Byte> image1 = new Image<Bgr, byte>((Bitmap)pbImage1.Image); Image<Bgr, Byte> image2 = new Image<Bgr, byte>((Bitmap)pbImage2.Image); Image<Gray, Byte> imageGray1 = image1.Convert<Gray, Byte>(); Image<Gray, Byte> imageGray2 = image2.Convert<Gray, Byte>(); Image<Gray, Byte> imageThreshold1 = imageGray1.ThresholdBinaryInv(new Gray(128d), new Gray(255d)); Image<Gray, Byte> imageThreshold2 = imageGray2.ThresholdBinaryInv(new Gray(128d), new Gray(255d)); Contour<Point> contour1 = imageThreshold1.FindContours(CHAIN_APPROX_METHOD.CV_CHAIN_APPROX_SIMPLE, RETR_TYPE.CV_RETR_EXTERNAL); Contour<Point> contour2 = imageThreshold2.FindContours(CHAIN_APPROX_METHOD.CV_CHAIN_APPROX_SIMPLE, RETR_TYPE.CV_RETR_EXTERNAL); /*if (contour1.Perimeter / 50 > 2 && contour2.Perimeter / 50 > 2) { contour1 = contour1.ApproxPoly(contour1.Perimeter / 50, 2, new MemStorage()); //對(duì)輪廓進(jìn)行多邊形逼近(參數(shù)設(shè)為輪廓周長(zhǎng)的1/50) contour2 = contour2.ApproxPoly(contour2.Perimeter / 50, 2, new MemStorage()); }*/ //進(jìn)行匹配 string result = ""; if (rbHuMoments.Checked) result = MatchShapes(contour1, contour2); //Hu矩匹配 else if (rbContourTree.Checked) result = MatchContourTrees(contour1, contour2); //輪廓樹(shù)匹配 else if (rbPGH.Checked) result = MatchPghHist(contour1, contour2); //成對(duì)幾何直方圖匹配 txtResult.Text += result; }
//Hu矩匹配 private string MatchShapes(Contour<Point> contour1, Contour<Point> contour2) { //匹配方法 CONTOURS_MATCH_TYPE matchType = rbHuI1.Checked ? CONTOURS_MATCH_TYPE.CV_CONTOUR_MATCH_I1 : (rbHuI2.Checked ? CONTOURS_MATCH_TYPE.CV_CONTOURS_MATCH_I2 : CONTOURS_MATCH_TYPE.CV_CONTOURS_MATCH_I3); Stopwatch sw = new Stopwatch(); sw.Start(); //匹配 double matchValue = contour1.MatchShapes(contour2, matchType); sw.Stop(); double time = sw.Elapsed.TotalMilliseconds; return string.Format("Hu矩匹配({0:G}),結(jié)果:{1:F05},用時(shí):{2:F05}毫秒\r\n", matchType, matchValue, time); }
//輪廓樹(shù)匹配 private string MatchContourTrees(Contour<Point> contour1, Contour<Point> contour2) { //生成輪廓樹(shù) double thresholdOfCreate = double.Parse(txtThresholdOfCreateContourTrees.Text); //生成輪廓樹(shù)的閥值 IntPtr ptrTree1 = CvInvoke.cvCreateContourTree(contour1.Ptr, new MemStorage().Ptr, thresholdOfCreate); IntPtr ptrTree2 = CvInvoke.cvCreateContourTree(contour2.Ptr, new MemStorage().Ptr, thresholdOfCreate); //匹配 double thresholdOfMatch = double.Parse(txtThresholdOfMatchContourTrees.Text); //比較輪廓樹(shù)的閥值 Stopwatch sw = new Stopwatch(); sw.Start(); double matchValue = CvInvoke.cvMatchContourTrees(ptrTree1, ptrTree2, MATCH_CONTOUR_TREE_METHOD.CONTOUR_TREES_MATCH_I1, thresholdOfMatch); sw.Stop(); double time = sw.Elapsed.TotalMilliseconds; return string.Format("輪廓樹(shù)匹配(生成輪廓樹(shù)的閥值:{0},比較輪廓樹(shù)的閥值:{1}),結(jié)果:{2:F05},用時(shí):{3:F05}毫秒\r\n", thresholdOfCreate, thresholdOfMatch, matchValue, time); }
//成對(duì)幾何直方圖匹配 private string MatchPghHist(Contour<Point> contour1, Contour<Point> contour2) { //生成成對(duì)幾何直方圖 Rectangle rect1 = contour1.BoundingRectangle; float maxDist1 = (float)Math.Sqrt(rect1.Width * rect1.Width + rect1.Height * rect1.Height); //輪廓的最大距離:這里使用輪廓矩形邊界框的對(duì)角線長(zhǎng)度 int[] bins1 = new int[] { 60, 20 }; RangeF[] ranges1 = new RangeF[] { new RangeF(0f, 180f), new RangeF(0f, maxDist1) }; //直方圖第0維為角度,范圍在(0,180),第2維為輪廓兩條邊緣線段的距離 DenseHistogram hist1 = new DenseHistogram(bins1, ranges1); CvInvoke.cvCalcPGH(contour1.Ptr, hist1.Ptr); Rectangle rect2 = contour2.BoundingRectangle; float maxDist2 = (float)Math.Sqrt(rect2.Width * rect2.Width + rect2.Height * rect2.Height); int[] bins2 = new int[] { 60, 20 }; RangeF[] ranges2 = new RangeF[] { new RangeF(0f, 180f), new RangeF(0f, maxDist2) }; DenseHistogram hist2 = new DenseHistogram(bins2, ranges2); CvInvoke.cvCalcPGH(contour2.Ptr, hist2.Ptr); //匹配 Stopwatch sw = new Stopwatch(); sw.Start(); double compareResult; HISTOGRAM_COMP_METHOD compareMethod = rbHistCorrel.Checked ? HISTOGRAM_COMP_METHOD.CV_COMP_CORREL : (rbHistChisqr.Checked ? HISTOGRAM_COMP_METHOD.CV_COMP_CHISQR : (rbHistIntersect.Checked ? HISTOGRAM_COMP_METHOD.CV_COMP_INTERSECT : HISTOGRAM_COMP_METHOD.CV_COMP_BHATTACHARYYA)); if (rbHistEmd.Checked) { //EMD //將直方圖轉(zhuǎn)換成矩陣 Matrix<Single> matrix1 = FormProcessHist.ConvertDenseHistogramToMatrix(hist1); Matrix<Single> matrix2 = FormProcessHist.ConvertDenseHistogramToMatrix(hist2); compareResult = CvInvoke.cvCalcEMD2(matrix1.Ptr, matrix2.Ptr, DIST_TYPE.CV_DIST_L2, null, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); matrix1.Dispose(); matrix2.Dispose(); } else { //直方圖對(duì)比方式 hist1.Normalize(1d); hist2.Normalize(1d); compareResult = CvInvoke.cvCompareHist(hist1.Ptr, hist2.Ptr, compareMethod); } sw.Stop(); double time = sw.Elapsed.TotalMilliseconds; return string.Format("成對(duì)幾何直方圖匹配(匹配方式:{0}),結(jié)果:{1:F05},用時(shí):{2:F05}毫秒\r\n", rbHistEmd.Checked ? "EMD" : compareMethod.ToString("G"), compareResult, time); }


通過(guò)以上代碼,可以計(jì)算出兩個(gè)輪廓對(duì)比的值,但是這些值具體代表什么意義呢?實(shí)際上,我目前還不清楚,需要進(jìn)行大量的試驗(yàn)才行。
感謝您耐心看完本文,希望對(duì)您有所幫助。
|