opencv——轮廓发现与轮廓(二值图像)分析
引言
二值图像分析最常见的一个主要方式就是轮廓发现与轮廓分析,其中轮廓发现的目的是为轮廓分析做准备,经过轮廓分析我们可以得到轮廓各种有用的属性信息。
这里顺带提下边缘检测,和轮廓提取的区别:
边缘检测主要是通过一些手段检测数字图像中明暗变化剧烈(即梯度变化比较大)像素点,偏向于图像中像素点的变化。如canny边缘检测,结果通常保存在和源图片一样尺寸和类型的边缘图中。
轮廓检测指检测图像中的对象边界,更偏向于关注上层语义对象。如OpenCV中的findContours()函数, 它会得到每一个轮廓并以点向量方式存储,除此也得到一个图像的拓扑信息,即一个轮廓的后一个轮廓、前一个轮廓、父轮廓和内嵌轮廓的索引编号。
一,轮廓的发现与绘制
在OpenCV里面利用findContours()函数和drawContours()函数实现这一功能。
- findContours()函数
void findContours( InputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int mode, int method, Point offset = Point() )
参数一: image,输入图像、八位单通道的,背景为黑色的二值图像。(一般是经过Canny、拉普拉斯等边缘检测算子处理过的二值图像)
参数二:contours,输出轮廓图像。是一个向量,向量的每个元素都是一个轮廓。因此,这个向量的每个元素仍是一个向量。即:
vector<vector<Point> > contours;
参数三:hierarchy,输出各个轮廓的继承关系。hierarchy也是一个向量,长度和contours相等,每个元素和contours的元素对应。hierarchy的每个元素是一个包含四个整型数的向量。即:
vector<Vec4i> hierarchy;
参数四:mode,检测轮廓的方法。有四种方法:
- RETR_EXTERNAL:只检测外轮廓。忽略轮廓内部的洞。
- RETR_LIST:检测所有轮廓,但不建立继承(包含)关系。
- RETR_TREE:检测所有轮廓,并且建立所有的继承(包含)关系。
- RETR_CCOMP:检测所有轮廓,但是仅仅建立两层包含关系。
参数五:method,每个轮廓的编码信息。也有四种(常用前两种)
- CHAIN_APPROX_NONE:把轮廓上所有的点存储。
- CHAIN_APPROX_SIMPLE:只存储轮廓上的拐点。
- CHAIN_APPROX_TC89_L1,CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain 近似算法
参数六: Point,偏移量。默认为0
注意:该函数将白色区域当作前景物体。所以findContours()函数是黑色背景下找白色轮廓。(重要!!!)
- drawContours()函数
drawContours( InputOutputArray binImg, // 输出图像 OutputArrayOfArrays contours,// 全部发现的轮廓对象 Int contourIdx// 轮廓索引号,-1表示绘制所有轮廓 const Scalar & color,// 绘制时候颜色 int thickness,// 绘制线宽,-1表示填充轮廓内部 int lineType,// 线的类型LINE_8 InputArray hierarchy,// 拓扑结构图 int maxlevel,// 最大层数, 0只绘制当前的,1表示绘制绘制当前及其内嵌的轮廓 Point offset = Point()// 轮廓位移,可选 )
二,轮廓分析(二值图像分析)
在得到图像的轮廓以后,我们就可以进行轮廓分析。经过轮廓分析我们可以得到轮廓各种有用的属性信息、常见的如下:
-
🧡计算轮廓面积 :
contourArea(contour, oriented = False) //计算轮廓的面积 参数说明:contour为输入的单个轮廓值;oriented:轮廓方向,默认值false。 如果为true,该函数返回一个带符号的面积,其正负取决于轮廓的方向(顺时针还是逆时针)。 如果是默认值false,则面积以绝对值的形式返回. 根据这个特性可以根据面积的符号来确定轮廓的位置。
- 💛计算轮廓周长:
arcLength(contour, closed) // 计算轮廓的周长 参数说明:contour为输入的单个轮廓值,closed表示轮廓是否封闭(true为封闭,false为不封闭)
- 💚计算几何矩与中心距: moments()
Moments m = moments(contours[t]); //获取轮廓的距 //计算轮廓质心 double cx = m.m10 / m.m00; double cy = m.m01 / m.m00;
- 💙轮廓的外接矩形:
轮廓的外接矩形有两种,如下图,绿色的叫外接矩形boundingRect(),表示不考虑旋转并且能包含整个轮廓的矩形。蓝色的叫最小外接矩形minAreaRect(),考虑了旋转
1️⃣外接矩形Rect boundingRect(InputArray points)
输入参数points可以一系列点的集合,对轮廓来说就是该轮廓的点集 返回结果是一个正矩形,包含以下信息:
- 矩形左上角的坐标(rect.x,rect.y)
- 矩形的宽和高(rect.width,rect.height)
Rect rect = boundingRect(Mat(contours[i]));//获取轮廓外接正矩形 rectangle(src, rect, (0, 0, 255), 2, 8, 0);
2️⃣最小外接矩形minAreaRect()
输入参数points可以一系列点的集合,对轮廓来说就是该轮廓的点集 返回结果是一个旋转矩形,包含下面的信息:
- 旋转矩形的中心坐标(rect.center)
- 旋转矩形的宽和高(rect.size.width,rect.size.height)
- 旋转矩形的角度(rect.angle)
RotatedRect rect = minAreaRect(contours[i]);//获取轮廓最小外接矩形 Point2f P[4]; rect.points(P);//获取四顶点坐标 for (int j = 0; j <= 3; j++) { line(src, P[j], P[(j + 1) % 4], Scalar(0,0,255), 1);//依次连线 }
- 💜最小外接圆/拟合圆:minEnclosingCircle()
void minEnclosingCircle(InputArray points, Point2f& center, float& radius); points,输入的二维点集,可以是 vector 或 Mat 类型。 center,圆的输出圆心。 radius,圆的输出半径。 例如: findContours(bin_img, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE); //寻找包裹轮廓的最小圆 vector<Point2f>centers(contours.size());//圆心个数 vector<float>radius(contours.size());//半径个数 for (int i = 0; i < contours.size(); i++)
{ //寻找并绘制最小圆 minEnclosingCircle(contours[i], centers[i], radius[i]); circle(src, centers[i], radius[i], scalar(0,0,255), 2); }
- 🤎拟合椭圆:fitEllipse()
RotatedRect fitEllipse(InputArray points); //唯一一个参数是输入的二维点集,可以是 vector 或 Mat 类型。 例如: // 轮廓发现与绘制 vector<vector<Point>> contours; findContours(binary, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE, Point()); for (size_t t = 0; t < contours.size(); t++) { // 拟合椭圆 RotatedRect rrt = fitEllipse(contours[t]); ellipse(src, rrt, Scalar(0, 0, 255), 2, 8); } imshow("contours", src);
- 🖤拟合直线:fitLine()
OpenCV中直线拟合正是基于最小二乘法实现的。其函数将计算出的直线信息存放在 line 中,(为Vec4f 类型)。line[0]、line[1] 存放的是直线的方向向量,float cosθ = oneline[0]; float sinθ = oneline[1]。line[2]、line[3] 存放的是直线上一个点的坐标。
实现直线拟合的API如下:
void fitLine( InputArray points, //输入待拟合的二维点的数组或vector OutputArray line, //输出直线,Vec4f (2d)或Vec6f (3d)的vector int distType, //距离类型 double param, //距离参数(一般设为0) double reps, //径向的精度参数(一般设为0.01) double aeps //角度精度参数(一般设为0.01) )
distType(距离类型)有六种参数:(DIST_L2就是最小二乘法)
opencv实现:
Mat src = imread("D:/opencv练习图片/直线拟合.png"); imshow("原图片", src); // 去噪声与二值化 Mat dst, gray, binary; Canny(src, binary, 80, 160, 3, false); imshow("canny二值化", binary); Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1)); dilate(binary, binary, k); // 轮廓发现与绘制 vector<vector<Point>> contours; findContours(binary, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE, Point()); for (size_t t = 0; t < contours.size(); t++) { // 最大外接轮廓 Rect rect = boundingRect(contours[t]); int m = max(rect.width, rect.height); if (m < 30) continue; // 直线拟合 Vec4f oneline; fitLine(contours[t], oneline, DIST_L1, 0, 0.01, 0.01); float cosθ = oneline[0]; float sinθ = oneline[1]; float x0 = oneline[2]; float y0 = oneline[3]; // 直线参数斜率k与截矩b float k = sinθ / cosθ; //求tanθ,也就是斜率 float b = y0 - k * x0; float x = 0; float y = k * x + b; line(src, Point(x0, y0), Point(x, y), Scalar(0, 0, 255), 2, 8, 0); } imshow("结果", src);
- 🤍轮廓的凸包:convexHull()
凸包(Convex Hull)是一个计算几何(图形学)中常见的概念。简单来说,给定二维平面上的点集,凸包就是将最外层的点连接起来构成的凸多边形,它是能包含点集中所有点的。
理解物体形状或轮廓的一种比较有用的方法便是计算一个物体的凸包,然后计算其凸缺陷(convexity defects)。
convexHull ( InputArray points, /输入的二维点集,Mat类型数据即可 OutputArray hull, //输出参数,用于输出找到的凸包 bool clockwise = false, //操作方向,为True时,输出的凸包为顺时针方向,否则为逆时针方向 bool returnPoints = true //凸包的返回形式,默认值为true,此时返回点坐标的形式,否则返回对应点的索引值 )
凸包检测原理:
opencv实现:
Mat src = imread("D:/opencv练习图片/凸包检测.jpg"); imshow("原图片", src); // 二值化 Mat dst, gray, binary; cvtColor(src, gray, COLOR_BGR2GRAY); threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU); // 形态学去除干扰 Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1)); morphologyEx(binary, binary, MORPH_OPEN, k); imshow("binary", binary); // 轮廓发现与绘制 vector<vector<Point>> contours; findContours(binary, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE, Point()); for (size_t t = 0; t < contours.size(); t++) { vector<Point> hull; convexHull(contours[t], hull);//凸包检测 bool isHull = isContourConvex(contours[t]);//判断轮廓是否为凸包 printf("test convex of the contours %s", isHull ? "Y" : "N"); int len = hull.size(); //绘制凸包 for (int i = 0; i < hull.size(); i++) { circle(src, hull[i], 4, Scalar(255, 0, 0), 2, 8, 0);//点 line(src, hull[i%len], hull[(i + 1) % len], Scalar(0, 0, 255), 2, 8, 0);//线 } } imshow("凸包检测", src);
-
🧡多边形逼近-逼近真实形状:approxPolyDP()
轮廓的多边形逼近指的是:使用多边形来近似表示一个轮廓。 多边形逼近的目的是为了减少轮廓的顶点数目。 多边形逼近的结果依然是一个轮廓,只是这个轮廓相对要粗旷一些。
void approxPolyDP( InputArray curve, //输入曲线,一般是由图像的轮廓点组成的点集 OutputArray approxCurve, //表示输出的逼近后多边形的点集(类型与输入曲线的类型相同) double epsilon, //轮廓逼近的顶点距离真实轮廓曲线的最大距离,该值越小表示越逼近真实轮廓 bool closed //表示输出的多边形是否封闭 )
opencv实现:
Mat src = imread("D:/opencv练习图片/多边形逼近.png"); Mat dstImage_3(src.size(), CV_8UC3, Scalar(0)); Mat dstImage_6(src.size(), CV_8UC3, Scalar(0)); Mat dstImage_10(src.size(), CV_8UC3, Scalar(0)); imshow("原图片", src); // 二值化 Mat dst, gray, binary; cvtColor(src, gray, COLOR_BGR2GRAY); threshold(gray, binary, 0, 255, THRESH_BINARY_INV | THRESH_OTSU); // 形态学去除干扰 Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1)); morphologyEx(binary, binary, MORPH_OPEN, k); imshow("binary", binary); // 轮廓发现与绘制 vector<vector<Point>> contours; findContours(binary, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE, Point()); vector<vector<Point>> contours_poly(contours.size()); for (int i = 0; i < contours.size(); i++) { //epsilon==3 approxPolyDP(Mat(contours[i]), contours_poly[i], 3, true); drawContours(dstImage_3, contours_poly, i, Scalar(230,130,255), 1, LINE_AA); //epsilon==6 approxPolyDP(Mat(contours[i]), contours_poly[i], 6, true); drawContours(dstImage_6, contours_poly, i, Scalar(255,255,160), 1, LINE_AA); //epsilon==10 approxPolyDP(Mat(contours[i]), contours_poly[i], 10, true); drawContours(dstImage_10, contours_poly, i, Scalar(175, 255, 255), 1, LINE_AA); } imshow("epsilon=3", dstImage_3); imshow("epsilon=6", dstImage_6); imshow("epsilon=10", dstImage_10);
从以上结果可以看出,设置的精度epsilon越小,多边形越拟合。
- 💛检测点是否在轮廓内pointPolygonTest()
OpenCV中实现这个功能的API叫做点多边形测试,它可以准确的得到一个点距离多边形的距离,如果点是轮廓点或者属于轮廓多边形上的点,距离是零,如果是多边形内部的点是是正数,如果是负数返回表示点是外部。
利用这个特性,我们可以巧妙的获取轮廓最大内接圆的半径:
当这个点在轮廓内部(与轮廓距离为正数),其返回的距离是最大值的时候,这个距离就是轮廓的最大内接圆的半径,该点就是最大内接圆的圆心。这样我们就巧妙的获得了圆心的位置与半径,剩下的工作就很容易了完成,绘制一个圆而已,一行代码就可以搞定。
double pointPolygonTest( InputArray contour, //输入轮廓点集合 Point2f pt, //输入图像上任一点 bool measureDist MeasureDist//如果是True,则返回每个点到轮廓的距离,如果是False则返回+1,0,-1三个值,其中+1表示点在轮廓内部,0表示点在轮廓上,-1表示点在轮廓外 )
opencv实现:(绘制轮廓的最大内接圆和最小外接圆)
Mat src = imread("D:/opencv练习图片/图像最大内接圆.png"); imshow("原图片", src); // 二值化 Mat dst, gray, binary; cvtColor(src, gray, COLOR_BGR2GRAY); threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU); imshow("binary", binary); // 轮廓发现与绘制 vector<vector<Point>> contours; findContours(binary, contours, RETR_LIST, CHAIN_APPROX_NONE); Mat dist =Mat::zeros(src.size(), CV_32F);//定义一个Mat对象,存放原图中每个点到该轮廓的距离,为浮点型数据 int dist1 = 0; int maxdist = 0; Point center; //寻找最大内接圆参数 //遍历每个点,计算该点到轮廓距离 for (int row = 0; row < dist.rows; row++) { for (int col = 0; col < dist.cols; col++) { //通过点多边形检测计算获得点到轮廓距离,并存放至dist中 dist1 = pointPolygonTest(contours[0], Point(col, row), true); if (dist1 > maxdist) { maxdist = dist1;//找出dist1中的最大值 center = Point(col, row);//获取最大值的坐标 } } } //寻找最小外接圆参数 vector<Point2f>centers(contours.size());//圆心个数 vector<float>radius(contours.size());//半径个数 minEnclosingCircle(contours[1], centers[1], radius[1]); //绘制最小外接圆 circle(src, centers[1], radius[1], Scalar(0, 0, 255), 2); //绘制最大的内接圆 circle(src, center, maxdist, Scalar(0, 255, 0), 1, LINE_8, 0); imshow("src", src);
需要注意的是:
1️⃣寻找轮廓的findContours的RETR_LIST参数是从里向外找轮廓,因此内部轮廓的索引为contours[0],外轮廓索引为contours[1]。而RETR_TREE正好与之相反,从外向内找轮廓
2️⃣实战发现,在找寻dist1对象的最大值时,用minMaxLoc函数找到好多值。(目前未理解其中缘由)