我的个人博客:谋仁·Blog
微信公众号:谋仁的麻袋
CSDN:曹谋仁


检测前预处理—-边缘检测二值图

所谓形状/轮廓的检测就是把待检测图像中的边缘轮廓组成的图形识别出来,并检测出轮廓点,再对其进行判断。所以为便于检测轮廓,我们要在检测之前进行一些预处理:
灰度->高斯滤波->Canny边缘检测算法->图像膨胀
代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// <summary>
/// 形状检测前的预处理(灰度->高斯滤波->Canny边缘算法->膨胀)
/// </summary>
/// <param name="imgIn">Mat 类,输入图像</param>
/// <returns>Mat类,预处理后的图像</returns>
Mat preProcessing(Mat imgIn) {
Mat imgGray, imgBlur, imgCanny, imgDila;
//先定义一个内核
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
cvtColor(imgIn, imgGray, COLOR_BGR2GRAY, 0);//灰度变换
GaussianBlur(imgGray, imgBlur, Size(65, 65), 1, 1);//高斯滤波去噪点
Canny(imgBlur, imgCanny, 40, 120);//Canny边缘检测
dilate(imgCanny, imgDila, kernel);//图像膨胀
return imgDila;//返回处理完后的图像
}

findContours函数—-检测轮廓

函数作用:从二值图像中检测轮廓
函数的定义:

1
2
3
4
5
6
7
8
void findContours( 
InputArray image,
OutputArrayOfArrays contours,
OutputArray hierarchy,
int mode,
int method,
Point offset = Point()
);

参数解释:

  • image:8位单通道图像,非零像素视为1,零像素视为0。这里一般输入经过Canny边缘检测、inRange选择、拉普拉斯等变换成01图像。
  • contours:检测到轮廓的变量,每一个轮廓被存储为点向量。定义轮廓的类型为:
1
vector<vector<Point>>

这是一个双重向量,外层向量存放待检测图中所有形状的轮廓;内层向量存放的是某一个轮廓的点集合(一组由连续的Point构成的点的集合的向量)

  • hierarchy:参数定义的类型为:
1
vector<Vec4i> hierarchy

Vec4i的定义:

1
typedef Vec<int, 4> Vec4i

向量内每个元素都包含了4个int型变量,所以从定义上看,hierarchy是一个向量,向量内每个元素都是一个包含4个int型的数组。
向量hierarchy内的元素和轮廓向量contours内的元素是一一对应的,向量的容量相同。hierarchy内每个元素的4个int型变量是hierarchy[i][0] ~ hierarchy[i][3],分别表示当前轮廓 i 的后一个轮廓、前一个轮廓、父轮廓和内嵌轮廓的编号索引。如果当前轮廓没有对应的四者中的其一的话,则相应的hierarchy[i][*]被置为-1。

  • mode:轮廓的检测模式,其取值定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum RetrievalModes {
/** retrieves only the extreme outer contours. It sets `hierarchy[i][2]=hierarchy[i][3]=-1` for
all the contours. */
RETR_EXTERNAL = 0,
/** retrieves all of the contours without establishing any hierarchical relationships. */
RETR_LIST = 1,
/** retrieves all of the contours and organizes them into a two-level hierarchy. At the top
level, there are external boundaries of the components. At the second level, there are
boundaries of the holes. If there is another contour inside a hole of a connected component, it
is still put at the top level. */
RETR_CCOMP = 2,
/** retrieves all of the contours and reconstructs a full hierarchy of nested contours.*/
RETR_TREE = 3,
RETR_FLOODFILL = 4 //!<
};

翻译如下:
RETR_EXTERNAL:只检测最外围轮廓,包含在外围轮廓内的内围轮廓被忽略;
RETR_LIST:检测所有的轮廓,包括内围、外围轮廓,但是检测到的轮廓不建立等级关系,彼此之间独立,没有等级关系。这就意味着这个检索模式下不存在父轮廓或内嵌轮廓,所以hierarchy向量内所有元素的第3、第4个分量都会被置为-1;
RETR_CCOMP:检测所有的轮廓,但所有轮廓只建立两个等级关系,外围为顶层,若外围内的内围轮廓还包含了其他的轮廓信息,则内围内的所有轮廓均归属于顶层;
RETR_TREE:检测所有轮廓,所有轮廓建立一个等级树结构。外层轮廓包含内层轮廓,内层轮廓还可以继续包含内嵌轮廓。
RETR_FLOODFILL:官方没有介绍,暂时不知道

参考:https://www.cnblogs.com/GaloisY/p/11062065.html

  • method:轮廓逼近方法,取值定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
enum ContourApproximationModes {
/** stores absolutely all the contour points. That is, any 2 subsequent points (x1,y1) and
(x2,y2) of the contour will be either horizontal, vertical or diagonal neighbors, that is,
max(abs(x1-x2),abs(y2-y1))==1. */
CHAIN_APPROX_NONE = 1,
/** compresses horizontal, vertical, and diagonal segments and leaves only their end points.
For example, an up-right rectangular contour is encoded with 4 points. */
CHAIN_APPROX_SIMPLE = 2,
/** applies one of the flavors of the Teh-Chin chain approximation algorithm @cite TehChin89 */
CHAIN_APPROX_TC89_L1 = 3,
/** applies one of the flavors of the Teh-Chin chain approximation algorithm @cite TehChin89 */
CHAIN_APPROX_TC89_KCOS = 4
};

翻译如下:
CHAIN_APPROX_NONE:获取每个轮廓的每个像素,相邻的两个点的像素位置差不超过1;
CHAIN_APPROX_SIMPLE:压缩水平方向,垂直方向,对角线方向的元素,值保留该方向的重点坐标,如果一个矩形轮廓只需4个点来保存轮廓信息;
CHAIN_APPROX_TC89_L1和CHAIN_APPROX_TC89_KCOS:使用Teh-Chinl链逼近算法中的一种

参考:https://blog.csdn.net/qq_42887760/article/details/86565601

  • offset:轮廓相对于原始图像的偏移量。例如:如果这里写Size(-10,-20),最终绘制的轮廓相对原图实际轮廓的位置往左偏移了10个像素,往上偏移了20个像素。如果是Size(10,20),就代表往右偏移10个像素,往下偏移20个像素。

findContours函数另一个重载与该定义唯一的区别是少了hierarchy参数,其他参数及含义都一样,故在此不多赘述。

contourArea、arcLength函数—-面积、周长

contourArea函数—-轮廓面积

函数作用:计算图像轮廓的面积
函数的定义:

1
double contourArea( InputArray contour, bool oriented = false );

参数解释:

  • contour:输入轮廓的点集合
  • oriented:表示某一个方向上轮廓的的面积值,顺时针或者逆时针,一般选择默认false

arcLength函数—-轮廓长度

函数作用:计算图像轮廓的周长
函数的定义:

1
double arcLength( InputArray curve, bool closed );

参数解释:

  • curve:输入轮廓的点集合
  • closed:表示曲线是否封闭

approxPolyDP函数—-曲线折线化

函数作用:将曲线或多边形用更少顶点的折现来拟合,即把一个连续光滑曲线折线化
曲线折线化的目的是获得折线顶点个数,进而可以判断出该轮廓大概是什么形状
函数的定义:

1
2
3
4
5
6
void approxPolyDP( 
InputArray curve,
OutputArray approxCurve,
double epsilon,
bool closed
);

参数解释:

  • curve:Mat类或vector类,2D点集,一般由轮廓线点组成的点集
  • approxCurve:输出的折线化后点集
  • epsilon:指定的精度,也即是原始曲线与近似曲线之间的最大距离
  • closed:若为true,则说明拟合曲线是闭合的;反之,若为false,则不为闭合曲线

    drawContours函数—-绘制轮廓

    函数作用:绘制已经找到的图像的轮廓
    函数的定义:
1
2
3
4
5
6
7
8
9
10
11
void drawContours( 
InputOutputArray image,
InputArrayOfArrays contours,
int contourIdx,
const Scalar& color,
int thickness = 1,
int lineType = LINE_8,
InputArray hierarchy = noArray(),
int maxLevel = INT_MAX,
Point offset = Point()
);

参数解释:

  • image:要绘制轮廓的图像
  • contours:轮廓线的点集合
  • contourIdx:指定要绘制轮廓的编号,如果是负数,则绘制所有的轮廓
  • color:轮廓线的颜色,通常用Scalar(blue, green, red)调色
  • thickness:轮廓线的宽度,如果是-1(cv2.FILLED),则为填充模式
  • lineType:线型,取值的定义:
1
2
3
4
5
6
enum LineTypes {
FILLED = -1,
LINE_4 = 4, //!< 4-connected line--4连通线型
LINE_8 = 8, //!< 8-connected line--8连通线型
LINE_AA = 16 //!< antialiased line--抗锯齿线型
};
  • hierarchy:关于层级的可选参数,只有绘制部分轮廓时才会用到
  • maxLevel:绘制轮廓的最高级别,这个参数只有hierarchy有效的时候才有效
    maxLevel=0—–绘制与输入轮廓属于同一等级的所有轮廓即输入轮廓和与其相邻的轮廓
    maxLevel=1—–绘制与输入轮廓同一等级的所有轮廓与其子节点
    maxLevel=2—–绘制与输入轮廓同一等级的所有轮廓与其子节点以及子节点的子节点
  • offset:绘制的轮廓相对于已找到的轮廓位置的偏移量。例如:如果这里写Size(-10,-20),最终绘制的轮廓相对已找到轮廓的位置往左偏移了10个像素,往上偏移了20个像素。如果是Size(10,20),就代表往右偏移10个像素,往下偏移20个像素。

    示例

    源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
#ifdef _DEBUG
#pragma comment(lib,"opencv_world453d.lib")
#else
#pragma comment(lib,"opencv_world453.lib")
#endif // _DEBUG


/// <summary>
/// 对预处理后的图像检测轮廓并标出已定义的简单形状
/// </summary>
/// <param name="imgDil">与处理后的图像</param>
/// <param name="img">待检测轮廓的原图</param>
void GetContour(Mat imgDil, Mat img)
{
vector<vector<Point>> contour;
vector<Vec4i> hierarchy;
findContours(imgDil, contour, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_NONE);
vector<vector<Point>> conPoly(contour.size());//定义用于存放轮廓折线化后的变量
vector<Rect> boundRect(contour.size());
//contour.size()是在该图像中检测到的轮廓个数
for (int i = 0; i < contour.size(); i++)//所有轮廓遍历
{
string objectType;//形状名称
float peri = arcLength(contour[i], true);//轮廓周长
//0.015* peri表示拟合的精度
approxPolyDP(contour[i], conPoly[i], 0.015 * peri, true);//把一个连续光滑曲线折线化
boundRect[i] = boundingRect(conPoly[i]);//获得包覆该轮廓最小矩形
int objCor = (int)conPoly[i].size();//获取折线化后轮廓的顶点个数
#pragma region 定义形状
if (objCor == 3) //三角形
objectType = "Tri";
else if (objCor == 4) {//矩形
float aspRatio = (float)boundRect[i].width / boundRect[i].height;//长宽比来区别正方形与矩形
if (aspRatio > 0.95 && aspRatio < 1.05)
objectType = "Square";
else
objectType = "Rect";
}
else if (objCor > 4) //圆形
objectType = "Circle";
#pragma endregion
drawContours(img, conPoly, i, Scalar(187, 109, 68), 3, LINE_AA);//画出轮廓线
rectangle(img, boundRect[i].tl(), boundRect[i].br(), Scalar(72,255,104), 4);//用矩形框住选出的图形
//标出已定义出的图形名称
putText(img, objectType, { boundRect[i].x,boundRect[i].y - 5 } , 1, FONT_HERSHEY_PLAIN, Scalar(0, 69, 255), 1);
}
}

/// <summary>
/// 形状检测前的预处理(灰度->高斯滤波->Canny边缘算法->膨胀)
/// </summary>
/// <param name="imgIn">Mat 类,输入图像</param>
/// <returns>Mat类,预处理后的图像</returns>
Mat PreProcessing(Mat imgIn) {
Mat imgGray, imgBlur, imgCanny, imgDila;
//先定义一个内核
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
cvtColor(imgIn, imgGray, COLOR_BGR2GRAY, 0);//灰度变换
GaussianBlur(imgGray, imgBlur, Size(65, 65), 1, 1);//高斯滤波去噪点
Canny(imgBlur, imgCanny, 40, 120);//Canny边缘检测
dilate(imgCanny, imgDila, kernel);//图像膨胀
return imgDila;//返回处理完后的图像
}

void main() {
string path = "D:\\My Bags\\图片\\shapes.png";//原图路径
Mat img = imread(path);//读取图片
Mat imgProcessed = PreProcessing(img);//图像预处理
GetContour(imgProcessed , img);//检测轮廓并标出简单图形名称
imshow("Image", img);//显示出来
waitKey(0);
}

运行结果: