我们在前面的章节里讲过边缘检测,本章所说的轮廓是基于边缘检测的。我们之前所做的边缘检测,结果只是基于像素的,而很多时候,我们可能需要对轮廓进行一些几何操作,例如分析区域是否连通,求出轮廓的凸包,判断一个点是不是在轮廓内,等等。我们先来看两种生成轮廓的方法,再看对轮廓的一些应用。
一、从边缘得到轮廓
虽然轮廓似乎就是边缘,但实际上,OpenCV只能从二值图得到轮廓,边缘被看成是非常窄的区域(宽1个像素)。所以,从边缘得到轮廓时,会出现两条轮廓基本相同,它们是边缘的内外。
示例代码如下:
Mat src = new Mat(img_circle, ImreadModes.Grayscale);
Mat r1 = new Mat();
Cv2.Canny(src, r1, 40, 120, 3);
Mat r2 = new Mat();
Cv2.FindContours(r1, out Mat[] contours, r2, RetrievalModes.List, ContourApproximationModes.ApproxNone);
Scalar[] colors = new Scalar[] {
new Scalar(255,0,0),
new Scalar(0,255,0),
new Scalar(0,0,255),
new Scalar(255,255,0),
new Scalar(255,0,255),
new Scalar(0,255,255),
new Scalar(180,120,0),
new Scalar(0,120,180),
new Scalar(120,0,180),
new Scalar(60,180,20),
new Scalar(180,120,5),
new Scalar(25,180,255)
};
Mat r3 = new Mat(src.Rows, src.Cols, MatType.CV_8UC3);
for (int i = 0; i < contours.Length; i++)
{
Cv2.DrawContours(r3, contours, i, colors[i], 1, LineTypes.AntiAlias);
}
r3.SaveImage(img_result);
效果如下:
二、从二值图得到轮廓
二值图得到轮廓省了边缘检测一步,示例代码如下:
Mat src = new Mat(img_circle, ImreadModes.Grayscale);
Mat r2 = new Mat();
Cv2.FindContours(src, out Mat[] contours, r2, RetrievalModes.CComp, ContourApproximationModes.ApproxNone);
Scalar[] colors = new Scalar[] {
new Scalar(255,0,0),
new Scalar(0,255,0),
new Scalar(0,0,255),
new Scalar(255,255,0),
new Scalar(255,0,255),
new Scalar(0,255,255),
new Scalar(180,120,0),
new Scalar(0,120,180),
new Scalar(120,0,180),
new Scalar(60,180,20),
new Scalar(180,120,5),
new Scalar(25,180,255)
};
Mat r3 = new Mat(src.Rows, src.Cols, MatType.CV_8UC3);
for (int i = 0; i < contours.Length; i++)
{
Cv2.DrawContours(r3, contours, i, colors[i], 1, LineTypes.AntiAlias);
}
r3.SaveImage(img_result);
效果如下:
三、多边形逼近
多边形逼近的目的是减少轮廓的点数,但看起来轮廓形状差不多。
示例代码如下:
Mat src = new Mat(img_circle, ImreadModes.Grayscale);
Mat r2 = new Mat();
Cv2.FindContours(src, out Mat[] contours, r2, RetrievalModes.External, ContourApproximationModes.ApproxNone);
Mat r3 = new Mat();
Cv2.ApproxPolyDP(contours[0], r3, 5, true);
Mat result = new Mat(src.Rows, src.Cols, MatType.CV_8UC3);
Cv2.DrawContours(result, new Mat[] { r3 }, 0, new Scalar(0, 0, 255), 1, LineTypes.AntiAlias);
result.SaveImage(img_result);
效果如下:
原来的轮廓点数为1828,而多边形逼近的结果是53个点。
四、轮廓长度
简单易懂,就是测量轮廓的像素长度,代码如下:
Mat src = new Mat(img_circle, ImreadModes.Grayscale);
Mat r2 = new Mat();
Cv2.FindContours(src, out Mat[] contours, r2, RetrievalModes.External, ContourApproximationModes.ApproxNone);
double length = Cv2.ArcLength(contours[0], true);
五、矩形包围框
一个刚好把轮廓包围起来的矩形框,代码如下:
Mat src = new Mat(img_circle, ImreadModes.Grayscale);
Mat r2 = new Mat();
Cv2.FindContours(src, out Mat[] contours, r2, RetrievalModes.External, ContourApproximationModes.ApproxNone);
OpenCvSharp.Rect rect = Cv2.BoundingRect(contours[0]);
Mat result = new Mat(src.Rows, src.Cols, MatType.CV_8UC3);
Cv2.DrawContours(result, contours, 0, new Scalar(0, 0, 255), 1, LineTypes.AntiAlias);
Cv2.Rectangle(result, rect, new Scalar(0, 255, 0), 1, LineTypes.AntiAlias);
result.SaveImage(img_result);
效果如下:
六、最小包围矩形
我们在上一点提到的矩形,是平行于x,y轴的。如果矩形能够倾斜,或许存在面积更小的矩形,把轮廓包围起来。代码如下:
Mat src = new Mat(img_circle, ImreadModes.Grayscale);
Mat r2 = new Mat();
Cv2.FindContours(src, out Mat[] contours, r2, RetrievalModes.External, ContourApproximationModes.ApproxNone);
RotatedRect rect = Cv2.MinAreaRect(contours[0]);
OpenCvSharp.Point[] ps = new OpenCvSharp.Point[4];
for (int i = 0; i < 4; i++)
{
ps[i] = new OpenCvSharp.Point(rect.Points()[i].X, rect.Points()[i].Y);
}
Mat result = new Mat(src.Rows, src.Cols, MatType.CV_8UC3);
Cv2.DrawContours(result, contours, 0, new Scalar(0, 0, 255), 1, LineTypes.AntiAlias);
Cv2.Polylines(result, new OpenCvSharp.Point[][] { ps }, true, new Scalar(0, 255, 0), 1, LineTypes.AntiAlias);
result.SaveImage(img_result);
效果如下:
七、最小包围圆
把轮廓包围起来的面积最小的圆。示例代码如下:
Mat src = new Mat(img_circle, ImreadModes.Grayscale);
Mat r2 = new Mat();
Cv2.FindContours(src, out Mat[] contours, r2, RetrievalModes.External, ContourApproximationModes.ApproxNone);
Cv2.MinEnclosingCircle(contours[0], out Point2f center, out float radius);
Mat result = new Mat(src.Rows, src.Cols, MatType.CV_8UC3);
Cv2.DrawContours(result, contours, 0, new Scalar(0, 0, 255), 1, LineTypes.AntiAlias);
Cv2.Circle(result, new OpenCvSharp.Point(center.X, center.Y), (int)radius, new Scalar(0, 255, 0), 1, LineTypes.AntiAlias);
result.SaveImage(img_result);
效果如下:
八、凸包
凸包是一个把轮廓包围起来的凸多边形。跟多边形逼近的一个区别是,这是凸的。示例代码如下:
Mat src = new Mat(img_circle, ImreadModes.Grayscale);
Mat r2 = new Mat();
Cv2.FindContours(src, out Mat[] contours, r2, RetrievalModes.External, ContourApproximationModes.ApproxNone);
Mat r3 = new Mat();
Cv2.ConvexHull(contours[0], r3);
Mat result = new Mat(src.Rows, src.Cols, MatType.CV_8UC3);
Cv2.DrawContours(result, contours, 0, new Scalar(0, 0, 255), 1, LineTypes.AntiAlias);
Cv2.DrawContours(result, new Mat[] { r3 }, 0, new Scalar(0, 255, 0), 1, LineTypes.AntiAlias);
result.SaveImage(img_result);
效果如下:
九、Hu矩匹配
我们在直方图一章里讲过,可以用直方图分布来比较两个图像。其实,用轮廓的形状,也是匹配图像的两种方法。Hu矩是两个轮廓差异的表征,越小表示越相似。
示例代码如下:
Mat src = new Mat(img_letter, ImreadModes.Grayscale);
Mat r2 = new Mat();
Cv2.FindContours(src, out Mat[] contours, r2, RetrievalModes.External, ContourApproximationModes.ApproxNone);
Mat r3 = new Mat(src.Rows, src.Cols, MatType.CV_8UC3);
for (int i = 0; i < contours.Length; i++)
{
Cv2.DrawContours(r3, contours, i, new Scalar(255, 255, 255));
}
r3.SaveImage(img_result);
double m1 = Cv2.MatchShapes(contours[0], contours[5], ShapeMatchModes.I3);
double m2 = Cv2.MatchShapes(contours[1], contours[5], ShapeMatchModes.I3);
double m3 = Cv2.MatchShapes(contours[2], contours[5], ShapeMatchModes.I3);
double m4 = Cv2.MatchShapes(contours[3], contours[5], ShapeMatchModes.I3);
double m5 = Cv2.MatchShapes(contours[4], contours[5], ShapeMatchModes.I3);
我们所采用的测试图像为:
所有字符跟左上角的8进行比较,结果为(数值越小形状越接近):
符号 | 差异结果 |
B | 0.092 |
C | 3.742 |
倾斜的8 | 0.016 |
N | 0.904 |
6 | 0.673 |