本文转自
http://www.cnblogs.com/xrwang/archive/2010/03/03/ImageFeatureDetection.html
前言
轮廓是构成任何一个形状的边界或外形线。前面讲了如何根据色彩及色彩的分布(直方图对比和模板匹配)来进行匹配,现在我们来看看如何利用物体的轮廓。包括以下内容:轮廓的查找、表达方式、组织方式、绘制、特性、匹配。
查找轮廓
首先我们面对的问题是如何在图像中找到轮廓,OpenCv(EmguCv)为我们做了很多工作,我们的任务只是调用现成的函数而已。Image<TColor,TDepth>类的FindContours方法可以很方便的查找轮廓,不过在查找之前,我们需要将彩色图像转换成灰度图像,然后再将灰度图像转换成二值图像。代码如下所示:
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)。
以下代码可以用来获取轮廓上的点:
sbContour.AppendFormat("{0},", contour[i]);
2.Freeman链码
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链码
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链码上的点
[DllImport("cv200.dll")]
publicstaticexternvoid cvStartReadChainPoints(IntPtr ptrChain,IntPtr ptrReader);
//读取Freeman链码的点
[DllImport("cv200.dll")]
publicstaticextern Point cvReadChainPoint(IntPtr ptrReader);
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Ansi)]
//定义链码读取结构
publicstruct MCvChainPtReader
{
//seqReader
public MCvSeqReader seqReader;
/// char
publicbyte code;
/// POINT->tagPOINT
public Point pt;
/// char[16]
[System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.ByValTStr, SizeConst =16)]
publicstring 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来辅助终止循环,读取了所有的点之后就可以罢手了。
轮廓之间的组织方式
在查找到轮廓之后,不同轮廓是怎么组织的呢?根据不同的选择,它们可能是:(1)列表;(2)双层结构;(3)树型结构。
从纵向上来看,列表只有一层,双层结构有一或者两层,树型结构可能有一层或者多层。
如果要遍历所有的轮廓,可以使用递归的方式,代码如下:
privatevoid TravelContour(Contour<Point> contour,refint total,ref StringBuilder sbContour)
{
if (contour !=null)
{
sbContour.Append("------------------------\r\n");
sbContour.AppendFormat("轮廓{0},右节点:{1},下级节点:{2},外接矩形:({3})\r\n", total, contour.HNext !=null, contour.VNext !=null, contour.BoundingRectangle);
sbContour.AppendFormat("包含{0}个点(面积:{1},周长:{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);
}
}
轮廓的绘制
轮廓的绘制比较简单,用上面提到的方法取得轮廓的所有点,然后把这些点连接成一个多边形即可。
当然,对于用顶点序列表示的轮廓,用Image<TColor,TDepth>.Draw方法或者cvDrawContours函数可以很方便的绘制出轮廓。我发现,如果将参数max_level设置成2,可以绘制出所有的轮廓。
绘制轮廓的代码如下:
int maxLevel =0; //绘制的轮廓深度
int.TryParse(txtMaxLevel.Text, out maxLevel);
imageResult.Draw(contour, new Bgr(lblExternalColor.BackColor), new Bgr(lblHoleColor.BackColor), maxLevel, 2);
轮廓的特性
轮廓的特性有很多,下面一一介绍。
1.轮廓的多边形逼近
轮廓的多边形逼近指的是:使用多边形来近似表示一个轮廓。
多边形逼近的目的是为了减少轮廓的顶点数目。
多边形逼近的结果依然是一个轮廓,只是这个轮廓相对要粗旷一些。
可以使用Contour<Point>.ApproxPoly方法或者cvApproxyPoly函数来对轮廓进行多边形逼近,示例代码如下:
2.轮廓的关键点
轮廓的关键点是:轮廓上包含曲线信息比较多的点。关键点是轮廓顶点的子集。
可以使用cvFindDominantPoints函数来获取轮廓上的关键点,该函数返回的结果一个包含 关键点在轮廓顶点中索引 的序列。再次强调:是索引,不是具体的点。如果要得到关键点的具体坐标,可以用索引到轮廓上去找。
以下代码演示了如何获取轮廓上的关键点:
privatevoid 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}个关键点:\r\n", seq.Total);
for (int i =0; i < seq.Total; i++)
{
int idx = seq[i]; //关键点序列中存储的数据 是 关键点在轮廓中所处位置的索引
Point p = contour[idx]; //得到关键点的坐标
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("在获取关键点时发生异常,错误描述:{0},错误源:{1},错误堆栈:{2}\r\n错误文件:{3},函数名:{4},行:{5},错误内部描述:{6}\r\n", ex.Message, ex.Source, ex.StackTrace, ex.FileName, ex.FunctionName, ex.Line, ex.ErrorStr);
}
catch (Exception e)
{
sbContour.AppendFormat("在获取关键点时发生异常,错误描述:{0},错误源:{1},错误堆栈:{2}\r\n", e.Message, e.Source, e.StackTrace);
}
finally
{
storage.Dispose();
}
}
}
3.轮廓的周长和面积
轮廓的周长可以用Contour<Point>.Perimeter属性或者cvArcLength函数来获取。
轮廓的面积可以用Contour<Point>.Area属性或者cvContourArea函数来获取。
4.轮廓的边界框
有三种常见的边界框:矩形、圆形、椭圆。
(1)矩形:在图像处理系统中提供了一种叫Rectangle的矩形,不过它只能表达边垂直或水平的特例;OpenCv中还有一种叫Box的矩形,它跟数学上的矩形一致,只要4个角是直角即可。
如果要获取轮廓的Rectangle,可以使用Contour<Point>.BoundingRectangle属性或者cvBoundingRect函数。
如果要获取轮廓的Box,可以使用Contour<Point>.GetMinAreaRect方法或者cvMinAreaRect2函数。
(2)圆形
如果要获取轮廓的圆形边界框,可以使用cvMinEnclosingCircle函数。
(3)椭圆
如果要获取轮廓的椭圆边界框,可以使用cvFitEllipse2函数。
下列代码演示了如何获取轮廓的各种边界框:
privatevoid 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);
elseif (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);
}
elseif (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("轮廓点数小于6,不能创建外围椭圆。\r\n");
}
}
5.轮廓的矩
我们可以使用Contour<Point>.GetMoments方法或者cvMoments函数方便的得到轮廓的矩集,然后再相应的方法或函数获取各种矩。
特定的矩:MCvMoments.GetSpatialMoment方法、cvGetSpatialMoment函数
中心矩:MCvMoments.GetCentralMoment方法、cvGetCentralMoment函数
归一化中心矩:MCvMoments.GetNormalizedCentralMoment方法、cvGetNormalizedCentralMoment函数
Hu矩:MCvMoments.GetHuMoment方法、McvHuMoments.hu1~hu7字段、cvGetHuMoments函数
以下代码演示了如何获取轮廓的矩:
privatevoid 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.轮廓的轮廓树
轮廓树用来描述某个特定轮廓的内部特征。注意:轮廓树跟轮廓是一一对应的关系;轮廓树不用于描述多个轮廓之间的层次关系。
可以用函数cvCreateContourTree来构造轮廓树。
7.轮廓的凸包和凸缺陷
轮廓的凸包和凸缺陷用于描述物体的外形。凸包和凸缺陷很容易获得,不过我目前不知道它们到底怎么使用。
如果要判断轮廓是否是凸的,可以用Contour<Point>.Convex属性和cvCheckContourConvexity函数。
如果要获取轮廓的凸包,可以用Contour<Point>.GetConvexHull方法或者cvConvexHull2函数,返回的是包含顶点的序列。
如果要获取轮廓的凸缺陷,可以用Contour<Point>.GetConvexityDefacts方法或者cvConvexityDefects函数。
注意:EmguCv将缺陷的单词拼写错了,defect才是缺陷。
以下代码演示了如何获取轮廓的凸包及凸缺陷:
8.轮廓的成对几何直方图
成对几何直方图的资料比较少,我是这么理解的。
(1)轮廓保存的是一系列的顶点,轮廓是由一系列线段组成的多边形。对于看起来光滑的轮廓(例如圆),只是线段条数比较多,线段长度比较短而已。实际上,电脑中显示的任何曲线都由线段组成。
(2)每两条线段之间都有一定的关系,包括它们(或者它们的延长线)之间的夹角,两条线段的夹角范围是:(0,180)。
(3)每两条线段上的点之间还有距离关系,包括最短(小)距离、最远(大)距离,以及平均距离。最大距离我用了一个偷懒的计算方法,我把轮廓外界矩形的对角线长度看作了最大距离。
(4)成对几何直方图所用的统计数据包括了夹角和距离。
可以用函数cvCalcPGH来计算轮廓的成对几何直方图,示例代码如下:
轮廓的匹配
如果要比较两个物体,可供选择的特征很多。如果要判断某个人的性别,可以根据他(她)头发的长短来判断,这很直观,在长发男稀有的年代准确率也很高。也可以根据这个人尿尿的射程来判断,如果射程大于0.50米,则是男性。总之,方法很多,不一而足。
我们在上文中得到了轮廓的这么多特征,它们也可以用于进行匹配。典型的轮廓匹配方法有:Hu矩匹配、轮廓树匹配、成对几何直方图匹配。
1.Hu矩匹配
轮廓的Hu矩对包括缩放、旋转和镜像映射在内的变化具有不变性。Contour<Point>.MatchShapes方法和cvMatchShapes函数可以很方便的实现对2个轮廓间的匹配。
2.轮廓树匹配
用树的形式比较两个轮廓。cvMatchContourTrees函数实现了轮廓树的对比。
3.成对几何直方图匹配
在得到轮廓的成对几何直方图之后,可以使用直方图对比的方法来进行匹配。如果您和我一样忘记了直方图的对比方式,可以看看我写的另一篇文章《颜色直方图的计算、显示、处理、对比及反向投影(How to Use Histogram? Calculate, Show, Process, Compare and BackProject)》。
各种轮廓匹配的示例代码如下:
通过以上代码,可以计算出两个轮廓对比的值,但是这些值具体代表什么意义呢?实际上,我目前还不清楚,需要进行大量的试验才行。
相关推荐
5. **绘制轮廓**:找到轮廓后,可以使用`drawContours`函数在原图或新图上描绘出来,这对于可视化非常有用。此外,`approxPolyDP`可以用来近似轮廓,降低点的数量,提高处理速度。 6. **轮廓属性**:`contourArea`...
轮廓查找广泛应用于物体识别、运动分析、形状匹配、模板匹配等场景。例如,在自动驾驶中,轮廓查找可以帮助车辆识别行人和其他车辆;在医学图像分析中,它可以用于识别肿瘤或其他病灶。 10. **优化与性能**: ...
轮廓还可以用于获取对象的几何特性,例如外接矩形。`cv.boundingRect()`函数可以找到包围轮廓的最小矩形,并返回矩形的左上角坐标(x, y)以及宽度(w)和高度(h)。使用这些信息,我们可以用`cv.rectangle()`函数...
本篇文章将深入探讨如何使用C#结合Emgu CV进行图像轮廓识别,并计算轮廓之间的匹配度。 首先,我们需要了解基本的图像处理概念。图像通常由像素组成,而轮廓是图像中边界清晰的区域。在图像识别中,找到轮廓是分析...
此外,OpenCV还提供了其他高级功能,如轮廓属性计算(如面积、周长和凸包),以及基于轮廓的形状匹配和对象识别。理解并熟练运用轮廓绘制是进行复杂图像分析和目标检测的基础。 总之,通过OpenCV和C++实现的轮廓...
本项目聚焦于使用OpenCV在C++中实现图像轮廓查找的功能,这在图像分析、对象识别和图像分割等应用中非常关键。下面将详细阐述OpenCV库的基本概念、图像轮廓查找的原理以及如何用C++进行实现。 首先,OpenCV是一个跨...
这个函数返回一个轮廓链码的向量,我们可以遍历这个向量并使用`drawContours`函数将其绘制在图像上。 ```cpp std::vector<std::vector<cv::Point>> contours; cv::findContours(dilated_img, contours, cv::RETR_...
- **轮廓绘制**:用`cv2.drawContours()`函数可以在图像上描绘出轮廓,帮助可视化检测结果。 - **轮廓匹配**:在连续的帧之间,可以通过轮廓的相似性进行目标跟踪。 5. **运动目标检测**:在视频流中,通过比较...
OpenCv提供了函数 findContours()用于对物体轮廓进行检测,该函数实现算法是由S.suzuki K.Abe于1985年发表的。OpenCVSharp封装了这个函数,有2个参数(contours,hierarchy)要做特别的说明。 public static void ...
在许多应用中,如物体识别、形状分析和模板匹配,提取轮廓是非常关键的步骤。 在OpenCV中,`findContours()`函数是用于提取图像轮廓的核心工具。这个函数不仅找出轮廓,还能将它们分层,这对于处理嵌套对象或具有...
此外,还可以通过计算轮廓矩来比较不同轮廓的特性,矩是一种反映形状全局特征的统计量,对于对象识别和形状匹配很有帮助。OpenCV提供了一套计算轮廓矩的工具,包括面积、中心矩等,这些信息可以帮助我们理解图像中的...
检测到的轮廓可以通过OpenCV的轮廓查找功能进一步处理。`findContours()`函数可以从二值图像中提取轮廓,返回一个包含多级轮廓的向量。这些轮廓可以用`drawContours()`函数绘制在原始图像上,或者进行其他后续分析,...
压缩包内的"76】_查找和绘制图片轮廓矩"文件可能是一个程序或者数据集,用于帮助用户找到图像中的轮廓并绘制出来。轮廓是识别物体的重要特征,它可以提供关于物体形状的关键信息。这种工具通常会使用边缘检测算法,...
这个过程通常涉及到边缘检测和轮廓查找,例如使用Canny边缘检测算法或者找到二值图像中的连通组件。在给定的项目中,目标是找到物体的轮廓,并根据这些轮廓创建边界框。 4. **矩形边界框**: 矩形边界框是最常见的...
具体来说,使用`cvFindContours`函数查找图像中的所有轮廓,并通过`cvDrawContours`函数将这些轮廓绘制到另一张图像上。 #### 二、直方图匹配及其应用 **直方图匹配(或直方图规定化)**是一种用于图像增强的技术...
最后,通过查找连通组件,我们可以找到独立的轮廓。这些轮廓可以进一步分析,例如计算其面积、周长,或者进行形状匹配。 对于OCR应用,轮廓分析通常是字符分割的第一步。一旦识别出单个字符的轮廓,我们可以使用...
4. **掩模匹配法**:掩模匹配是一种模板匹配技术,用于在图像中查找与特定模板相似的区域。VB中,可以遍历图像,计算每个位置的模板与图像子区域的相似度,找到最佳匹配点。 5. **彩色变换**:彩色变换通常涉及色彩...
一、查找、绘制轮廓 首先了解一下轮廓的定义。一个轮廓代表一系列的点(像素),这一系列的点构成一个有序的点集,所以可以把一个轮廓理解为一个有序的点集。 1.1 findContour()函数 在OpenCV中,提供了一个...
在轮廓检测与重绘.C中,可能包含了使用OpenCV库的轮廓查找方法,如`findContours`,以及如何使用这些轮廓信息进行对象的重绘和显示。 5. **眼部识别算法实现**: 眼部识别算法是生物识别技术的一部分,通常涉及到...
查找两个分段贝塞尔曲线形状之间的匹配。 这是通过将一种形状的锚点在另一种形状的顶部上移动时的变形最小化来实现的。 我们正在选择顶点列表以进行失真计算。 我们假设两个形状包含相同数量的锚点。 基于动态编程的...