前言
轮廓是构成任何一个形状的边界或外形线。前面讲了如何根据色彩及色彩的分布(直方图对比和模板匹配)来进行匹配,现在我们来看看如何利用物体的轮廓。包括以下内容:轮廓的查找、表达方式、组织方式、绘制、特性、匹配。
查找轮廓
首先我们面对的问题是如何在图像中找到轮廓,OpenCv(EmguCv)为我们做了很多工作,我们的任务只是调用现成的函数而已。 Image<TColor,TDepth>类的FindContours方法可以很方便的查找轮廓,不过在查找之前,我们需要将彩色图像转换成灰度图像,然后再将灰度图像转换成二值图像。代码如下所示:
- Image<Bgr, Byte> imageSource = new Image<Bgr, byte>(sourceImageFileName); //获取源图像
- Image<Gray, Byte> imageGray = imageSource.Convert<Gray, Byte>(); //将源图像转换成灰度图像
- int thresholdValue = tbThreshold.Value; //用于二值化的阀值
- Image<Gray, Byte> imageThreshold = imageGray.ThresholdBinary(new Gray(thresholdValue), new Gray(255d)); //对灰度图像二值化
- Contour<Point> contour=imageThreshold.FindContours();
使用上面的代码可以得到图像的默认轮廓,但是轮廓在电脑中是如何表达的呢?在OpenCv(EmguCv)中提供了两类表达轮廓的方式:顶点的序列、 Freeman链码。
1.顶点的序列
用多个顶点(或各点间的线段)来表达轮廓。假设要表达一个从(0,0)到(2,2)的矩形,
(1)如果用点来表示,那么依次 存储的可能是: (0,0),(1,0),(2,0),(2,1),(2,2),(1,2),(0,2),(0,1);
(2)如果用点间的线段来表达轮廓,那么依次存储的可能是:(0,0),(2,0),(2,2),(0,2)。
以下代码可以用来获取轮廓上的点:
- for (int i = 0; i < contour.Total; i++)
- sbContour.AppendFormat("{0},", contour);
Freeman链码需要一个起点,以及从起点出发的一 系列位移。每个位移有8个方向,从0~7分别指向从正北开始的8个方向。假设要用Freeman链码表达从(0,0)到(2,2)的矩形,可能的表示方法是:起点(0,0),方向链2,2,4,4,6,6,0,0。
EmguCv对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));
读取Freeman链码上的点
- //初始化Freeman链码读取
- [DllImport("cv200.dll")]
- public static extern void cvStartReadChainPoints(IntPtr ptrChain,IntPtr ptrReader);
- //读取Freeman链码的点
- [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)]
- //定义链码读取结构
- 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;
- }
- //将链码指针转换成结构
- MCvChain chain=(MCvChain)Marshal.PtrToStructure(ptrChain,typeof(MCvChain));
- //定义存放链码上点的列表
- List<Point> pointList = new List<Point>(chain.total);
- //链码读取结构
- MCvChainPtReader chainReader = new MCvChainPtReader();
- IntPtr ptrReader = Marshal.AllocHGlobal(sizeof(MCvSeqReader) + sizeof(byte) + sizeof(Point) + 16 * sizeof(byte));
- Marshal.StructureToPtr(chainReader, ptrReader, false);
- //开始读取链码
- cvStartReadChainPoints(ptrChain, ptrReader);
- int i = 0;
- while (ptrReader != IntPtr.Zero && i < chain.total)
- {
- //依次读取链码上的每个点
- 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函数似乎永远不会满足循环终止的条件,即ptrReader永远不会被置为null,这跟《学习 OpenCv》和参考上不一致;我们需要用chain.total来辅助终止循环,读取了所有的点之后就可以罢手了。