引言

二值图像分析最常见的一个主要方式就是轮廓发现轮廓分析,其中轮廓发现的目的是为轮廓分析做准备,经过轮廓分析我们可以得到轮廓各种有用的属性信息。

这里顺带提下边缘检测,和轮廓提取的区别:

边缘检测主要是通过一些手段检测数字图像中明暗变化剧烈(即梯度变化比较大)像素点,偏向于图像中像素点的变化。如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,检测轮廓的方法。有四种方法:

  1. RETR_EXTERNAL:只检测外轮廓。忽略轮廓内部的洞。
  2. RETR_LIST:检测所有轮廓,但不建立继承(包含)关系。
  3. RETR_TREE:检测所有轮廓,并且建立所有的继承(包含)关系。
  4. RETR_CCOMP:检测所有轮廓,但是仅仅建立两层包含关系。

参数五:method,每个轮廓的编码信息。也有四种(常用前两种)

  1. CHAIN_APPROX_NONE:把轮廓上所有的点存储。
  2. CHAIN_APPROX_SIMPLE:只存储轮廓上的拐点。
  3. 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函数找到好多值。(目前未理解其中缘由)

opencv——轮廓发现与轮廓(二值图像)分析的更多相关文章

  1. Python+OpenCV图像处理(十六)—— 轮廓发现

    简介:轮廓发现是基于图像边缘提取的基础寻找对象轮廓的方法,所以边缘提取的阈值选定会影响最终轮廓发现结果. 代码如下: import cv2 as cv import numpy as np def c ...

  2. 【python+opencv】轮廓发现

    python+opencv---轮廓发现 轮廓发现---是基于图像边缘提取的基础寻找对象轮廓的方法, 所有边缘提取的阈值选定会影响最终轮廓发现的结果. 介绍两种API使用: -cv.findConto ...

  3. opencv python:轮廓发现

    example import cv2 as cv import numpy as np def edge_demo(image): blurred = cv.GaussianBlur(image, ( ...

  4. opencv::轮廓发现(find contour in your image)

    轮廓发现(find contour) 轮廓发现是基于图像边缘提取的基础寻找对象轮廓的方法. 所以边缘提取的阈值选定会影响最终轮廓发现结果 //发现轮廓 cv::findContours( InputO ...

  5. Opencv笔记(十八)——轮廓的更多函数及其层次结构

    凸缺陷 前面我们已经学习了轮廓的凸包,对象上的任何凹陷都被成为凸缺陷.OpenCV 中有一个函数 cv.convexityDefect() 可以帮助我们找到凸缺陷.函数调用如下: hull = cv2 ...

  6. OpenCV计算机视觉学习(8)——图像轮廓处理(轮廓绘制,轮廓检索,轮廓填充,轮廓近似)

    如果需要处理的原图及代码,请移步小编的GitHub地址 传送门:请点击我 如果点击有误:https://github.com/LeBron-Jian/ComputerVisionPractice 1, ...

  7. python实现轮廓发现

    目录: (一)轮廓发现的介绍 (二)代码实现 (1)使用直接使用阈值方法threshold方法获取二值化图像来选择轮廓 (2)使用canny边缘检测获取二值化图像 (一)轮廓发现的介绍与API的介绍 ...

  8. OpenCV学习代码记录——轮廓(contour)检测

    很久之前学习过一段时间的OpenCV,当时没有做什么笔记,但是代码都还在,这里把它贴出来做个记录. 代码放在码云上,地址在这里https://gitee.com/solym/OpenCVTest/tr ...

  9. OpenCV函数:提取轮廓相关函数使用方法

    opencv中提供findContours()函数来寻找图像中物体的轮廓,并结合drawContours()函数将找到的轮廓绘制出.首先看一下findContours(),opencv中提供了两种定义 ...

随机推荐

  1. windows 以管理员身份运行 代码

    1 // 以管理员身份运行本进程 2 // 1 获取本进程的文件路径. 3 TCHAR path[MAX_PATH] = { 0 }; // 需要初始化 4 DWORD dwPathSize = MA ...

  2. 7、MyBatis教程之分页实现

    8.分页实现 1.limit实现分页 思考:为什么需要分页? 在学习mybatis等持久层框架的时候,会经常对数据进行增删改查操作,使用最多的是对数据库进行查询操作,如果查询大量数据的时候,我们往往使 ...

  3. 如何配置Nginx,实现http访问重定向到https?

    现在越来越多的网站,当我们输入域名时,会自动重定向到https,我们只需要简单修改下Nginx配置文件/usr/local/nginx/conf/nginx.conf(根据个人的实际存储路径)即可. ...

  4. 使用Azure API Management, Functions, Power Apps和Logic App构建应用

    ASP.NET OpenAPI 可以非常方便的将我们的Web API项目自动文档化,除了自动文档化以外,我们还可以利用Azure API Management将Open API自动文档化了的Web A ...

  5. Makefile基本用法

    来源 https://www.gnu.org/software/make/manual/make.pdf 简单的例子 其中的cc通过链接,间接指向/usr/bin/gcc. Makefile文件中列出 ...

  6. 发送请求时携带了参数,但是浏览器network没有显示的排错思路

    发送请求时携带了参数,但是浏览器network没有显示的排错思路 不知道大家有没有遇到这样子的情况就是发送请求的时候明明携带了参数,但是浏览器的network中就是没有!请看下图和代码! 我发送请求用 ...

  7. maven中心仓库OSSRH使用简介

    目录 简介 为什么使用中心仓库 发布到中心仓库前的准备工作 使用OSSRH 使用Sonatype创建ticket 中央仓库中的组件要求 提供Javadoc 和源代码 使用GPG/PGP给文件签名 Me ...

  8. C++运算符重载的一些困惑

    一.背景 在复习<C++基础与提高>时,自己实现运算符重载(i++)时,几次都报错.其实还是自己对运算符重载这一部分内容理解得不够透彻,于是再次看了下书上的内容,理解算是加深了一些,于是提 ...

  9. IDEA通过Maven打包JavaFX工程(OpenJFX11)

    1 概述 最近研究JFX,写出来了但是打包不了,这...尴尬... IDEA的文档说只支持Java8打成jar包: 尝试过直接使用Maven插件的package,不行,也尝试过Build Artifa ...

  10. The Dole Queue UVA - 133

     In a serious attempt to downsize (reduce) the dole queue, The New National Green Labour Rhinoceros ...