Bootstrap

OpenCV图像与视频分析笔记 ——图像部分


前言

  本篇文章是作者在学习OpenCV图像与视频分析的笔记,或许还会有视频部分。本笔记包括了大部分的图像分析案例,包括图像分割,轮廓操作,霍夫检测API和图像的形态学操作。
  本文所例举的17个部分,包括了OpenCV图像处理相关API的详细解读和使用例子


图像部分

1. 图像的五种阈值分割方法

​ OpenCV提供了5种阈值分割的方法,一般的,我们对图像进行二值分割比较多,所以前面两种阈值分割的方法用的比较多,需要注意的是,自定义阈值T的分割法支持多通道,以通道为单位操作。

//阈值法1,二值化,if > T 255 else 0
threshold(gray, binary1, 127, 255, THRESH_BINARY);

//阈值法2,二值化,if < T 255 else 0 
threshold(gray, binary2, 127, 255, THRESH_BINARY_INV);	//invert 反转

//阈值法3,if < T 原值 else T
threshold(gray, binary3, 127, 255, THRESH_TRUNC);		//truncate 截断

//阈值法4,if < T 0 else 原值
threshold(gray, binary4, 127, 255, THRESH_TOZERO);

//阈值法5,if < T 原值 else 0
threshold(gray, binary5, 127, 255, THRESH_TOZERO_INV);

2. 图像的全局阈值分割法

​ 利用OpenCV提供的API确定阈值T,然后基于T进行二值分割,常见的全局阈值分割法有:均值法,OTSU阈值法,三角阈值法

//均值法,自己求灰度图像的像素均值
Scalar s = mean(gray);		
double t1 = threshold(gray, binary1, s[0], 255,THRESH_BINARY);	//返回阈值T

//OTSU阈值,求出每个像素点的最小类内(以像素点为基准,将图像像素点分为两类)方差,作为阈值
double t2 = threshold(gray, binary2, 0, 255, THRESH_BINARY | THRESH_OTSU);


//三角阈值,原理基于最多像素值(只对单峰图像效果较好,如医学生物类图像)和最大像素值
double t3 = threshold(gray, binary3, 0, 255, THRESH_BINARY | THRESH_TRIANGLE);

3. 自适应阈值(局部阈值)分割法

自适应阈值分割法特适用于光照不均匀或背景复杂的图像,在这些情况下,全局阈值分割法可能无法有效地分割目标和背景。

/*
	自适应阈值分割法函数原型
	
	src: 单通道图像
	dst: 输出的目标图像
	maxValue: 大于0的最大像素值
	adaptiveMethod: 使用的自适应阈值方法,常见的有 ADAPTIVE_THRESH_GAUSSIAN_C 和			ADAPTIVE_THRESH_MEAN_C
	thresholdType: 所使用阈值的方法
	blockSize: 像素邻居的块大小,可以理解为局部块的大小
	C: 常数C,为整数,计算出局部块的阈值之后,所减去的值
*/
void adaptiveThreshold(InputArray src, OutputArray dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C);

/*
		使用例子
*/


4. 图像中联通组件的发现

​ 在OpenCV的图像中,联通组件可以简单的理解为是图像中每一个元素(比如一个风景图像,花,树木等都属于一个联通组件)一般图像的背景也是一个联通组件,在检测联通组件的时候,背景一般是第一个联通组件,通常被赋值为0

/*
	联通组件发现函数原型1(第2个原型在第五部分介绍)

	param image: input image.
    param labels: output result, storage the label value of connected components.
    param connectivity: can be understood as the method of finding connected components, normal value is 8 or 4.
    param ltype: output image label type. Currently CV_32S and CV_16U are supported.
    param ccltype: connected components algorithm type, normal value is CCL_DEFAULT.
    
    return: the number of connected components.
*/
int connectedComponents(InputArray image, OutputArray labels, int connectivity, int ltype, int ccltype);

/*
	使用例子
	binary1:一个二值图像,调用联通组件API前,应该先将图像进行二值化
	labels:存储了联通组件信息,同一个联通组件在图像中覆盖的位置,值相同,其中,背景的值为0
*/
Mat labels = Mat::zeros(binary1.size(), CV_32S);
int num_labels = connectedComponents(binary1, labels, 8, CV_32S, CCL_DEFAULT);	//返回联通组件个数

5. 续4

​ 在OpenCV中,提供的另一个联通组件发现的API,connectedComponentsWithStats,该API不仅可以实现联通组件的发现,还可以对每一个联通组件的状态信息(联通组件的中心位置,联通组件的矩形范围)进行输出

/*
	联通组件发现函数原型2
	
    param image: input image.
    param labels: output result, storage the label value of connected components.
    param stats: the status of connected component, including backgroud label.
    param centroids: the central position of connected component, including x and y axis
    param connectivity: can be understood as the method of finding connected components, normal value is 8 or 4.
    param ltype: output image label type. Currently CV_32S and CV_16U are supported.
    param ccltype: connected components algorithm type, normal value is CCL_DEFAULT.
    
    return: the number of connected components.
	
*/

int connectedComponentsWithStats(InputArray image, OutputArray labels, OutputArray stats, OutputArray centroids, int connectivity, int ltype, int ccltype);

/*
	使用例子
	labels:每一个联通组件的取值
	stats:每一个联通组件的矩形范围信息,比如最左上角点的信息,矩形的宽和高,矩形面积等
	centroids:每一个联通组件的中心位置
*/
Mat labels = Mat::zeros(binary.size(), CV_32S);		//存储联通组件标签,同一个联通组件的像素标签相同
Mat stats, centroids;	//stats存储了每个联通组件的矩形范围,centroids存储了每个联通组件中心位置

int num_labels = connectedComponentsWithStats(binary, labels, stats, centroids, 8, CV_32S, CCL_DEFAULT);	//CV_32S: 32位有符号整数

6. 轮廓发现和绘制

​ 在OpenCV中,图像轮廓的定义很好理解,就是每一个图像元素的边缘(比如风景图,花的边缘和树的边缘等),轮廓发现和绘制就是通过OpenCV提供的API,将图像所有的元素轮廓进行检测,并且绘制出来,其中,轮廓发现是基于二值图的

/*
	轮廓发现函数原型
	
	param image: 输入的二值图像
	param contours: 存储检测到的每一个轮廓点集合(vector<vector<cv::Point>>).
	param heirarchy: 可选参数vector(vector<cv::Vec4i>), 存储内外轮廓之间的嵌套关系,hierarchy[i][0]~hierarchy[i][3]分别存储了后一个轮廓、前一个轮廓、父轮廓和内嵌轮廓的索引编号。
	mode: 轮廓检测的模式, 常见的两种模式为RETR_TREE(检测所有轮廓,并且构建所有轮廓的拓扑关系)和RETR_EXTERNAL(只检测外部轮廓).
	method: 轮廓逼近的方法,常见的取值为CHAIN_APPROX_SIMPLE(只取能体现轮廓的少部分点)和CHAIN_APPROX_NONE(取轮廓所有的点,效率较慢)
	param offset: 默认即可.
*/
void findContours( InputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int mode, int method, Point offset = Point());

/*
	使用例子
	binary:二值图
*/
vector<vector<Point>> contours;		//存储每一个轮廓所构成的点集合
vector<Vec4i> hierachy;				//存储图像的层次结构
findContours(binary, contours, hierachy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point());

/*
	轮廓绘制函数原型,解释一些比较关键的参数

	image:与上面findContours()不同的是,这里image是多通道图像,很好理解,我们绘制的轮廓一般是带颜色的
	contourIdx:轮廓数组contours的索引,大于0时绘制一个轮廓,小于零时绘制轮廓数组contours的所有轮廓
	hierarchy:存储图像轮廓层次的数组,默认即可
*/
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());

/*
	使用例子
	src:需要绘制轮廓的多通道图像
	第三个参数为-1表示绘制所有轮廓,也可以使用循环一个一个轮廓的绘制
*/
drawContours(src, contours, -1, Scalar(0, 255, 255), 2, LINE_AA);	//第三个参数为负数,绘制所有轮廓

for (size_t i = 0; i < contours.size(); ++i) {
	drawContours(src, contours, i, Scalar(0, 0, 255), 2, LINE_AA);	//第三个参数为正数,绘制一个轮廓
}

7. 续6. 轮廓面积与周长,最大 or 最小外接矩形

​ 如6所示,当我们进行轮廓发现之后,会得到轮廓数组contours,通过该轮廓数组,利用OpenCV提供的一些轮廓相关的API,可以输出轮廓相关的信息。

/*
	计算轮廓面积函数原型
	contour:轮廓的点集合
	oriented:是否返回带有方向的值(矢量),默认false返回标量即可
*/
double contourArea( InputArray contour, bool oriented = false);

/*
	计算轮廓周长函数原型
	curve:轮廓点集合
	closed:曲线(轮廓)是否闭合,findContours返回的轮廓一般是闭合的,这里true即可
*/
double arcLength( InputArray curve, bool closed);

/*
	轮廓最大外接矩形函数原型
	array:轮廓的点集合
	
	返回值为矩形,可以绘制
*/
Rect boundingRect( InputArray array);

/*
	轮廓最小外接矩形函数原型
	points:轮廓的点集合
	
	返回值为旋转矩形,可以绘制
*/
RotatedRect minAreaRect( InputArray points);


/*
	续6,例子,当我们进行轮廓发现之后,得到contours数组,对该数组进行遍历
*/
for (size_t i = 0; i < contours.size(); ++i) {
    double area = contourArea(contours[i]);		//计算轮廓面积
    double size = arcLength(contours[i], true);	//计算轮廓周长
    cout << area << ", " << size << endl;

    //绘制最大ROI矩形区域,矩形规则摆放
    Rect box = boundingRect(contours[i]);		
    rectangle(src, box, Scalar(0, 0, 255), 2, 8);

    //绘制最小ROI矩形区域,矩形不是规则摆放的
    RotatedRect rotatedRect = minAreaRect(contours[i]);
    vector<Point2f> pts(4);		
    rotatedRect.points(pts);	//旋转矩形的四个点,提供了绘制不规则矩形的方法
    for (int i = 0; i < 4; ++i) {
        line(src, pts[i], pts[(i + 1) % 4], Scalar(150, 75, 0), 2, 8);
    }
    cout << rotatedRect.angle << endl;

    //绘制椭圆形状的ROI区域
    ellipse(src, rotatedRect, Scalar(0, 150, 75), 2, 8);
}

8. 轮廓匹配

​ 在OpenCV中,提供了API对两幅图像的相同轮廓进行匹配,轮廓匹配的基本原理是基于图像的矩实现的,那么,什么是矩呢?其实,可以理解为用数学公式的方式来表达图像的几何信息。矩又分为很多种,几何矩、中心矩、归一化中心矩和Hu不变矩,通过对矩的理解,才能知道OpenCV进行轮廓匹配的原理

几何矩是基于图像像素强度的统计量,用于描述图像的几何特性。对于一个二值图像或灰度图像,其几何矩由下式定义:
G e o m e t r i c M o m e n t s ( p , q ) = ∑ x = 0 X − 1 ∑ y = 0 Y − 1 x p y q f ( x , y ) Geometric Moments_{(p,q)} = \sum_{x=0}^{X-1} \sum_{y=0}^{Y-1} x^{p} y^{q} f(x,y) GeometricMoments(p,q)=x=0X1y=0Y1xpyqf(x,y)
其中, ( p + q ) (p+q) (p+q)表示图像矩的阶数, f ( x , y ) f(x,y) f(x,y)表示图像中某一像素点的灰度值(二值图像的 f ( x , y ) = 0 o r 1 f(x,y) = 0 or 1 f(x,y)=0or1),在图像处理中,比较常用的是0阶矩、一阶矩和二阶矩

  • 0阶矩 ( p + q = 0 ) (p+q=0) (p+q=0)对于图像处理,有什么几何特性呢?由二值图像可知,我们的图像像素灰度值不是0就是1,所以,将其带入到0阶矩的公式中,为0的像素灰度值表示背景,为1的像素灰度值表示前景轮廓,0阶矩公式即表示为所有轮廓的像素灰度值和(轮廓的总面积)
  • 同理,对于一阶矩 ( p + q = 1 ) (p + q = 1) (p+q=1)的几何特性, G e o m e t r i c M o m e n t s ( 1 , 0 ) GeometricMoments_{(1,0)} GeometricMoments(1,0)表示图像灰度值中心点(质心)在x方向上的值 G e o m e t r i c M o m e n t s ( 0 , 1 ) GeometricMoments_{(0,1)} GeometricMoments(0,1)表示图像质心在y方向上的值
  • 二阶矩的几何意义是,描述图像的惯性,也称为惯性矩,它描述了图像的形状,方向和对称信息,二阶矩有三个 G e o m e t r i c M o m e n t s ( 2 , 0 ) GeometricMoments_{(2,0)} GeometricMoments(2,0) G e o m e t r i c M o m e n t s ( 0 , 2 ) GeometricMoments_{(0,2)} GeometricMoments(0,2) G e o m e t r i c M o m e n t s ( 1 , 1 ) GeometricMoments_{(1,1)} GeometricMoments(1,1) G e o m e t r i c M o m e n t s ( 2 , 0 ) GeometricMoments_{(2,0)} GeometricMoments(2,0)为x轴方向上的惯性矩,描述了图像在x轴上的扩散程度, G e o m e t r i c M o m e n t s ( 0 , 2 ) GeometricMoments_{(0,2)} GeometricMoments(0,2)为y轴方向上的惯性矩,描述了图像在y轴上的扩散程度,其中,这两个较大的值称为长轴,较小的值称为短轴(想象一下椭圆的长短轴) G e o m e t r i c M o m e n t s ( 1 , 1 ) GeometricMoments_{(1,1)} GeometricMoments(1,1)为x轴和y轴之间的联合惯性矩,它衡量了图像在两个正交方向上的耦合程度,即图像是否倾向于某个特定的角度,如果该值较大,就说明图像存在明显的倾斜(非对称)

中心矩:在介绍Hu不变矩之前,先理解一下中心矩与归一化中心矩,因为Hu不变矩是基于归一化中心矩计算得到的。中心矩增加了图像的质心,使矩的值不随图像轮廓位置的改变而改变。这个好理解,举个例子,在图像的变换中,原图像轮廓的所在位置,在新图像上发生了变化,根据几何矩的计算公式,像素点所在位置 ( x , y ) (x,y) (x,y)发生了改变,矩的值也就随之发生了改变,但是,中心矩的计算方式使矩的值不会因轮廓位置的改变而改变
C e n t r a l M o m e n t s ( p , q ) = ∑ x = 0 X − 1 ∑ y = 0 Y − 1 ( x − x ˉ ) p ( y − y ˉ ) q f ( x , y ) CentralMoments_{(p,q)} = \sum_{x=0}^{X-1} \sum_{y=0}^{Y-1} (x-\bar{x}) ^{p} (y-\bar{y})^{q} f(x,y) CentralMoments(p,q)=x=0X1y=0Y1(xxˉ)p(yyˉ)qf(x,y)
归一化中心距:就是在中心距的基础上,除以一个适当的尺度因子(通常是零阶矩的幂次),对比中心矩,归一化中心矩保证了矩的值在图像缩放的时候也保持不变
N o r m a l i z e d C e n t r a l M o m e n t s ( p , q ) = C e n t r a l M o m e n t s ( p , q ) G e o m e t r i c M o m e n t s ( 0 , 0 ) r , r = p + q + 2 2 NormalizedCentralMoments_{(p,q)}= \frac{CentralMoments_{(p,q)}}{GeometricMoments_{(0,0)}^{r}}, r= \frac{p+q+2}{2} NormalizedCentralMoments(p,q)=GeometricMoments(0,0)rCentralMoments(p,q),r=2p+q+2
Hu不变矩:Hu不变矩通过对归一化中心矩的线性组合,构成了7个值,保证了图像矩的值在图像旋转时也保持不变,它是归一化中心矩的进一步优化,使得Hu不变矩能够成为最好的图像几何信息数学表达方式,在图像进行平移、缩放和旋转时,都能够保证矩的值不变,下列公式,用 N N N表示归一化中心矩, M M M表示Hu不变矩。
KaTeX parse error: Unknown column alignment: e at position 17: …\begin{array}{le̲ft} M_{1}=N_{20…
​ 通过对几何矩、中心矩和归一化中心矩的介绍,我们应该对图像的矩有一个清晰的认识。不同阶数的几何矩,它能表达图像中不同的几何信息,但是,随着图像的变化(如平移,缩放,旋转),其矩的值也会变化。所以,就出现了中心矩,中心距在几何矩的基础上,解决了平移导致图像距变化的问题,归一化中心矩在中心矩的基础上,解决了缩放导致图像矩变化的问题,Hu不变矩在归一化中心矩的基础上,解决了旋转导致图像矩变化的问题。

/*
		轮廓匹配函数原型
		
		contour1: 待匹配轮廓的Hu不变矩
		contour2:匹配轮廓的Hu不变矩
		method:主要的取值有CONTOURS_MATCH_I1、CONTOURS_MATCH_I2和CONTOURS_MATCH_I3,分别表示使用Hu不变矩的欧几里得距离作为相似度度量、使用Hu不变矩的曼哈顿距离作为相似度度量和使用Hu不变矩的切比雪夫距离作为相似度度量
		return: 返回值表示两个轮廓之间的形状相似度,这个值越小,表示两个轮廓越相似
*/
double matchShapes( InputArray contour1, InputArray contour2,int method, double parameter );


/*
		使用例子
*/

void contours_find_info(Mat& image, vector<vector<Point>>& contours) {
	Mat gray;
	Mat binary;
	//灰度化 & 二值化
	cvtColor(image, gray, COLOR_BGR2GRAY);
	threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);

	findContours(binary, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point());
}

void QuickDemo::ContoursMatchDemo() {
	Mat src1 = imread("D:\\Photo\\images\\abc.png");
	Mat src2 = imread("D:\\Photo\\images\\a.png");
	if (src1.empty() || src2.empty()) {
		return;
	}
  
  //src1图像中有ABC三个字母,src2图像中只有A字母,需要通过src2匹配src1中的A字母轮廓,并且绘制
	imshow("src1", src1);
	imshow("src2", src2);

	//轮廓发现
	vector<vector<Point>> contours1;		//存储src1图像轮廓
	vector<vector<Point>> contours2;		//存储src2图像轮廓

	contours_find_info(src1, contours1);	
	contours_find_info(src2, contours2);

	//计算src2图像中A字母轮廓的几何距
	Moments mm2 = moments(contours2[0]);

	
	//计算src2图像中A字母轮廓的Hu距
	Mat Hu2;
	HuMoments(mm2, Hu2);

	//遍历src1图像中的所有字母轮廓,与src2图像中的A字母轮廓进行匹配
	for (int i = 0; i < contours1.size(); ++i) {
		//计算src1图像中轮廓的几何距
		Moments mm1 = moments(contours1[i]);

		//通过质心和像素总和求出轮廓的中心位置
    cout << mm1.m00 << endl;		//得出0阶距的值就是轮廓的像素总和	
    
		double cx = mm1.m10 / mm1.m00;
		double cy = mm1.m01 / mm1.m00;

		cout << cx << " " << cy << endl;

		//通过src1图像中字母轮廓的几何距,计算Hu距
		Mat Hu1;
		HuMoments(mm1, Hu1);

		//通过Hu距进行轮廓匹配
		double dist = matchShapes(Hu1, Hu2, CONTOURS_MATCH_I1, 0);

		if (dist < 1) {
			cout << dist << endl;	
      
			//在src1图像中绘制出从src2图像匹配到的轮廓
      drawContours(src1, contours1, i, Scalar(255, 0, 255), 2, LINE_AA);
		}
	}

	imshow("contours_match_demo", src1);
}

9. 轮廓逼近

​ 在谈轮廓逼近之前,想先聊一下什么是边缘检测和轮廓检测

边缘检测主要是通过一些手段检测数字图像中明暗变化剧烈(即梯度值比较大)的像素点偏向于图像的像素点变化。如canny边缘检测,结果通常保存在和源图像一样尺寸和类型的边缘图中。轮廓检测指检测图像中的对象边界,更偏向于关注上层语义对象。如OpenCV中的findContours()函数, 它会得到每一个轮廓并以点向量方式存储。图像的边缘检测更加注重于图像的像素点,而图像的轮廓检测更加注重于图像中的对象

轮廓逼近,就是通过对轮廓外形无限逼近,删除非关键点、得到轮廓的关键点,不断逼近轮廓的真实形状。

/*
		轮廓逼近函数原型
		
		curve: 需要进行逼近的轮廓
		approxCurve:存储轮廓逼近的结果,源码对这个参数的要求是,类型需要和curve一致
		epsilon:逼近精度,表示原始轮廓与近似轮廓之间的最大允许距离(最大误差)
		closed:轮廓是否闭合,选择默认true即可
*/
void approxPolyDP( InputArray curve,OutputArray approxCurve,double epsilon, bool closed );

/*
		使用例子,通过轮廓逼近API找到轮廓的关键点,并且绘制出结果
*/
void QuickDemo::ContoursApproxDemo() {
	Mat src = imread("D://Photo//images//contours.png",IMREAD_COLOR);

	Mat gray;
	Mat binary;
	cvtColor(src, gray, COLOR_BGR2GRAY);	//转灰度图像
	threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);	//转二值图像

	//轮廓发现
	vector<vector<Point>> contours;	//存储每一个轮廓的信息
	vector<Vec4i> hierarchy;		//存储轮廓层次
	findContours(binary, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point());

	//轮廓逼近
	for (size_t i = 0; i < contours.size(); ++i) {
		vector<Point> results;		
		approxPolyDP(contours[i], results, 4, true);		//轮廓逼近
    
    //绘制轮廓逼近的结果
    for (auto result: results) {
			circle(src, result, 2, Scalar(255, 200, 0), 2, LINE_AA);
		}

	imshow("src", src);
	imshow("binary", binary);
}

10. 轮廓拟合

​ 生成最相似的圆或者椭圆,读者可以对轮廓拟合、轮廓的最小外接矩形(续7.)三个方法进行对比,实现的需求是差不多的,这里对比一下轮廓拟合和轮廓最小外接矩形(ROI)的区别

  • 轮廓拟合:轮廓拟合的主要目标是用一个简单的几何形状描述轮廓的形状特征,更多的用于形状分析和识别,OpenCV提供了三个轮廓拟合的APIfitEllipse();fitLine();approxPolyDP();,分别使用椭圆、直线和多边形进行拟合(多边形拟合也叫轮廓逼近)。
  • 最小外接矩形:最小外接矩形侧重于定义轮廓的边界和位置,主要用于ROI区域的定义和图像预处理阶段(减少图像分析的范围),OpenCV提供了一个轮廓最小外接矩形的APIRotatedRect minAreaRect( InputArray points);
/*
		椭圆轮廓拟合原函数
		
		points: 轮廓的点集合,通常是Mat对象或者是vector<Point>对象
		return: 返回椭圆对应的旋转矩阵
*/
RotatedRect fitEllipse( InputArray points );

/*
		直线轮廓拟合原函数
		
		points: 轮廓的点集合
		line: 拟合输出的的结果,是包含四个元素的向量,(vx, vy, x0, y0),(vx, vy)表示直线对应的归一化向量,(x0, y0)表示直线通过的点
		distType: 计算点到直线距离的方式,常见的有最小二乘法、最小中值误差或最小均值误差法,详细可以见DistanceTypes类
		param: 额外参数,这里使用0即可,让fitLine()自动匹配最优的参数
		reps: 半径精度,表示轮廓点到拟合直线的距离
		aeps: 角度精度,OpenCV官方建议将reps和aeps的值设置为0.1
*/
void fitLine( InputArray points, OutputArray line, int distType,double param, double reps, double aeps);
/*
		多边形轮廓拟合原函数
    
    curve: 拟合轮廓的点集合,vector<Point>或者Mat对象
    approxCurve:轮廓拟合的结果,参数类型与curve一致
    epsilon: 拟合精度,表示原始轮廓与近似轮廓之间的最大允许距离(最大误差)
		closed: 轮廓是否闭合,默认true即可
*/
void approxPolyDP( InputArray curve,OutputArray approxCurve,double epsilon, bool closed);

/*
		这里主要演示椭圆轮廓拟合例子
*/
void QuickDemo::ContoursFittingDemo() {
	Mat src = imread("D://Photo//images//self_circle.png", IMREAD_COLOR);
	Mat gray;
	Mat binary;
	cvtColor(src, gray, COLOR_BGR2GRAY);	//转灰度图像
	threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);	//转二值图像

	//轮廓发现
	vector<vector<Point>> contours;	
	vector<Vec4i> hierarchy;		//存储轮廓拓扑信息
	findContours(binary, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point());

	//轮廓拟合
	for (size_t i = 0; i < contours.size(); ++i) {
		RotatedRect rrt = fitEllipse(contours[i]);	//椭圆轮廓拟合,基于二值图
		Point center = rrt.center;	//旋转矩阵的中心
		circle(src, center, 2, Scalar(0, 0, 255), 2, 8);	
		ellipse(src, rrt, Scalar(255, 0, 0), 2, 8);		//根据旋转矩阵绘制椭圆
	}

	imshow("src", src);
	imshow("binary", binary);
}

11. 霍夫直线检测HoughLines()

霍夫直线检测,通俗的理解就是,对图像中的点集合进行直线检测,其基本原理是:将图像看成一个直角坐标系,像素点 ( x , y ) (x,y) (x,y)看作已知参数,组成极坐标方程 ρ ( θ ) = x ⋅ c o s ( θ ) + y ⋅ s i n ( θ ) \rho(\theta) = x \cdot cos(\theta) + y \cdot sin(\theta) ρ(θ)=xcos(θ)+ysin(θ),此时就构成了一个, θ \theta θ作为自变量, ρ \rho ρ作为因变量的三角函数曲线。同理,对其他的 n − 1 n-1 n1个像素点进行该变换,就组成了 n n n条不同的 ρ i ( θ ) = x ⋅ c o s ( θ ) + y ⋅ s i n ( θ ) , i ⊆ [ 1 , n ] , θ ⊆ [ 0 , π ) \rho_{i}(\theta) = x \cdot cos(\theta) + y \cdot sin(\theta),i\subseteq[1,n],\theta\subseteq[0,\pi) ρi(θ)=xcos(θ)+ysin(θ),i[1,n],θ[0,π)三角函数曲线,其中, θ \theta θ的取值范围跟图像中直线与x轴正向的夹角有关。霍夫检测认为,如果这 n n n个点作为已知参数构成的 n n n条三角函数曲线相交于一个点,那么这 n n n个点就属于同一条直线上 n n n越多,就说明越多的点在一条直线上,直线就越平滑,至此,霍夫对一条直线的检测就完成了。

/*
		霍夫直线检测HoughLines()函数原型
		
		image: 输入的单通道二值图像
		lines:存储检测到的直线几何信息,vector<Vec3f>或者Mat对象存储,其中,lines[i][0]存储图像原点到直线的距离;lines[i][1]存储直线的法线(方向指向lines[i]),与x轴正向的夹角;lines[i][3]存储图像中有多少个像素点在lines[i]中
		rho:直线法线的像素精度,默认取1即可
		theta:直线法线(方向指向lines[i]),与x轴正向夹角的角度精度,如果以度数为精度,默认取值CV_PI/180.0
		threshold:检测直线的阈值,只有超过threshold个像素单位才认为是一条直线
		srn:rho方向上的高斯滤波器的标准差,默认0即可
		stn:theta方向上的高斯滤波器的标准差,默认0即可
		min_theta:指定theta的最小值,可以过滤一些不需要检测的直线
		max_theta:指定theta的最大值,同上
*/
void HoughLines( InputArray image, OutputArray lines,double rho, double theta, int threshold,double srn = 0, double stn = 0,double min_theta = 0, double max_theta = CV_PI);

/*
		霍夫直线检测HoughLines()使用例子
*/
void QuickDemo::HoughLinesDemo() {
	Mat src = imread("D://Photo//images//lines1.png", IMREAD_COLOR);
	//二值化
	Mat gray;
	Mat binary;
	GaussianBlur(src, src, Size(3, 3), 0); 
	cvtColor(src, gray, COLOR_BGR2GRAY);	//转灰度图像
	threshold(gray,binary,0,255,THRESH_BINARY|THRESH_OTSU);	//转二值图像

	vector<Vec3f> lines;	
	HoughLines(binary, lines, 1, (CV_PI / 180.0), 200, 0, 0, 0);

	//绘制直线
	for (size_t i = 0; i < lines.size(); ++i) {
		float rho = lines[i][0];	//距离,左上角为起始点
		float theta = lines[i][1];	//角度
		float acc = lines[i][2];	//转换为极坐标曲线,经过同一个点的曲线个数
		double x0, y0;	//存储直角坐标系上原始的点

		x0 = rho * cos(theta);
		y0 = rho * sin(theta);
		//输出lines[i]里面的内容
		printf("rho: %.2lf, theta: %.2lf, acc: %.2lf\n", rho, theta, acc);
		printf("x0 = %.2lf, y0 = %.2lf\n", x0, y0);

		Point pt1, pt2;			//找到直线上延长的两个点
		float new_rho = CV_PI - theta;
		pt1.x = cvRound(x0 + 1000 * sin(new_rho));		
		pt1.y = cvRound(y0 + 1000 * cos(new_rho));
		pt2.x = cvRound(x0 - 1000 * sin(new_rho));
		pt2.y = cvRound(y0 - 1000 * cos(new_rho));

		line(src, pt1, pt2, Scalar(255, 150, 0), 2, LINE_AA);
	}

	imshow("src", src);
	imshow("binary", binary);
}

12. 霍夫直线检测HoughLinesP()

​ 与11的霍夫检测直线不同的是,OpenCV提供的第二个霍夫直线检测APIHoughLinesP()更加注重于图像中直线段的检测(从API的参数也可以看出来),跟传统的霍夫检测HoughLines()不同的是HoughLinesP()只对局部的像素点进行统计,保证准确率的同时,直线检测的效率也提高了。

/*
		霍夫直线检测HoughLinesP()函数原型
		
		image:输入的单通道二值图像
		lines:存储检测到直线的几何信息,<vector<Vec4i>>或者Mat数据类型,lines[i][0]、lines[i][1]、lines[i][2]和lines[i][3]分别表示直线起始坐标(x1, y1, x2, y2)
		rho:直线法线(方向指向直线)的像素精度(也叫步长或者距离分辨率),默认取1
		theta:直线法线(方向指向直线)与x轴正向夹角的角度精度(也叫角度分辨率),默认CV_PI/180.0
		threshold:检测直线的阈值,只有超过threshold个像素单位才认为是一条直线
		minLineLength:检测直线的最小线段长度
		maxLineGap:检测到的两条直线最大允许间隙(单位像素,直线距离)
*/
void HoughLinesP( InputArray image, OutputArray lines, double rho, double theta, int threshold, double minLineLength = 0, double maxLineGap = 0)

/*
		霍夫直线检测HoughLinesP()使用例子
*/
void QuickDemo::HoughLinesPDemo() {
	Mat src = imread("D://Photo//images//morph01.png", IMREAD_COLOR);
	Mat gray;
	Mat binary;

	GaussianBlur(src, src, Size(3, 3), 0);	//高斯模糊
	cvtColor(src, gray, COLOR_BGR2GRAY);
	Canny(gray, binary, 80, 160, 3, false);	//利用边缘检测进行二值化

	vector<Vec4i> lines;		//存储四个值为整数的向量
	HoughLinesP(binary, lines, 1, CV_PI / 180.0, 80, 200, 10);
	Mat result = Mat::zeros(src.size(), src.type());
	for (size_t i = 0; i < lines.size(); ++i) {
		int x1 = lines[i][0];
		int y1 = lines[i][1];
		int x2 = lines[i][2];
		int y2 = lines[i][3];
		line(result, Point(x1, y1), Point(x2, y2), Scalar(255, 0, 0), 2, LINE_AA);	//gap 间隔
	}

	imshow("src", src);
	imshow("result", result);
	imshow("binary", binary);

}

13. 霍夫圆检测

霍夫圆检测,和霍夫直线检测原理类似,对于 X − Y X-Y XY坐标系下(对应于我们的图像空间)的圆方程 ( x − a ) 2 + ( y − b ) 2 = r 2 (x-a)^{2}+(y-b)^{2} = r^{2} (xa)2+(yb)2=r2,我们进行霍夫空间变换到 a − b a-b ab坐标系中,将 x x x y y y r r r看作已知参数, a − b a-b ab坐标系下,新圆的方程为 ( a − x ) 2 + ( b − y ) 2 = r 2 (a-x)^{2}+(b-y)^{2} = r^{2} (ax)2+(by)2=r2,同理,对 X − Y X-Y XY坐标系下圆的 n n n个点转换到 a − b a-b ab坐标系中,就能得到半径为 r r r n n n个圆,着 a − b a-b ab坐标系下的 n n n个圆必相交于一个点,至此,霍夫圆检测就完成了。对图像中所有的边缘点,设定 r r r的范围,就能检测一定条件下所有的点了。

/*
		霍夫圆检测函数原型
		
		image:经过灰度化和边缘检测处理的单通道八位图像
		circles:存储检测到的圆的几何信息,vector<Vec4f>,circles[i][0]表示检测到第i个圆圆心的x值,circles[i][1]表示检测到第i个圆圆心的y值,circles[i][2]表示检测到第i个圆的半径,circles[i][3]表示检测到第i个圆所拥有的像素单位个数
		method:检测圆的方法,常见的两个检测方法是HOUGH_GRADIENT和HOUGH_GRADIENT_ALT
		dp:这个参数很重要,说一下,dp = 图像分辨率 / 累计数组分辨率,图像分辨率好理解,累计数组是什么呢?霍夫圆检测算法运行的过程中,霍夫参数空间为(a, b, r),累计数组的作用就是,记录每一对参数空间(a0, b0, r0)中有多少个曲线H(a, b, r)经过,统计累计数组的局部最大值,就能检测到一个圆了(累计数组局部最大值就是检测圆边缘点的个数)。知道了累计数组是什么,累计数组分辨率就好理解了,指的就是a,b,r的分辨率,a,b代表圆心,r代表半径。dp值越接近1,说明a,b,r的分辨率越接近图像分辨率,检测的精度就越高,dp值越大,检测精度就越低。所以,dp最好的取值范围是[1,2],dp不可能小于1,因为图像局部的分辨率不可能超过整个图像的分辨率
		minDist:检测到的各个圆之间,其圆心的最小距离
		param1:边缘检测的高阈值,默认100即可
		param2:累加器阈值,类似于HoughLines()的threshold,只有累计数组中向量(a0, b0, r0)的累加值高于param2,才会被认为(a0, b0, r0)是检测到的一个圆
		minRadius:检测圆的最小半径,限制r,提高检测效率
		maxRadius:检测圆的最大半径,限制r,提高检测效率
*/
void HoughCircles( InputArray image, OutputArray circles,int method, double dp, double minDist, double param1 = 100, double param2 = 100, int minRadius = 0, int maxRadius = 0 );

/*
		霍夫圆检测使用例子
*/
void QuickDemo::HoughCircleDemo() {
	Mat src = imread("D://Photo//images//coins.jpg", IMREAD_COLOR);
	Mat gray;
	Mat binary;

	GaussianBlur(src, src, Size(9, 9), 2);	//高斯模糊
	cvtColor(src, gray, COLOR_BGR2GRAY);
	Canny(gray, binary, 40, 160, 3, false);	//利用边缘检测进行二值化

	vector<Vec4f> circles;

	int dp = 2;		//图像分辨率与累加器数组circles分辨率的ratio
	double minDist = 40;		//检测到的圆心最小距离,正常来说是最小半径的两倍
	double minRadius = 20;	//检测圆半径的最小值	
	double maxRadius = 70;	//检测圆半径的最大值
	HoughCircles(binary, circles, HOUGH_GRADIENT, dp, minDist, 100, 100, minRadius, maxRadius);

	//绘制检测到的圆
	for (size_t i = 0; i < circles.size(); ++i) {
		Point center(circles[i][0], circles[i][1]);		//圆心
		int radius = cvRound(circles[i][2]);				//半径
		cout << circles[i][3] << endl;						//像素单位个数
		circle(src, center, radius, Scalar(0, 0, 255), 4, LINE_AA, 0);	//绘制圆
		circle(src, center, 2, Scalar(255, 0, 0), 2, LINE_AA, 0);		//绘制圆心
	}

	imshow("src", src);
	imshow("gray", gray);
	imshow("binary", binary);
}

14. 图像形态学操作,腐蚀(erosion)与膨胀(dilate)

​ 图像的腐蚀(erosion)和膨胀(dilation)是形态学操作的基础,常用于图像处理和分析中,特别是在二值图像(前景一般为白色,背景一般为黑色)中用于改变图像中对象的形状和大小

腐蚀,一般用来缩小图像的前景区域(将轮廓加以腐蚀),具体的,定义一个核(结构元素),然后将核沿着图像滑动(有点类似于卷积操作),当核每经过图像局部区域,都会将局部区域像素最小值替代核中的某一个像素值(具体位置在定义核的时候表明)

膨胀,膨胀与腐蚀刚好相反,一般用来放大图像的前景区域(将轮廓加以膨胀),膨胀操作与腐蚀操作的原理几乎一致,只是像素最大值替代核中某一个像素值

/*
		腐蚀操作函数原型
		
    src:任意通道数的图像,常见的BGR, GRAY, BINARY图像都可以进行腐蚀操作
		dst:腐蚀操作输出的目标图像
		kernel:腐蚀操作用到的核(结构元素)
		anchor:腐蚀操作替代核中像素值的位置,Point(-1, -1)表示默认替代核的中心位置像素值
		iterations:表示进行腐蚀操作的迭代次数,默认1即可
		borderType:处理边界的类型,默认即可,具体类型可以在BorderTypes枚举中查看
		borderValue:指定边界的填充值,默认即可
*/
void erode( InputArray src, OutputArray dst, InputArray kernel, oint anchor = Point(-1,-1), int iterations = 1, int borderType = BORDER_CONSTANT, const Scalar& borderValue = morphologyDefaultBorderValue());

/*
		膨胀操作函数原型
		
		src:任意通道数的图像,常见的BGR, GRAY, BINARY图像都可以进行膨胀操作
		dst:膨胀操作输出的目标图像
		kernel:膨胀操作用到的核(结构元素)
		anchor:膨胀操作替代核中像素值的位置,Point(-1, -1)表示默认替代核的中心位置像素值
		iterations:表示进行膨胀操作的迭代次数,默认1即可
		borderType:处理边界的类型,默认即可,具体类型可以在BorderTypes枚举中查看
		borderValue:指定边界的填充值,默认即可
*/
void dilate( InputArray src, OutputArray dst, InputArray kernel, Point anchor = Point(-1,-1), int iterations = 1, int borderType = BORDER_CONSTANT, const Scalar& borderValue = morphologyDefaultBorderValue());

/*
		通常对图像进行形态学操作的时候,我们会提前定义好核(结构元素),定义核的函数原型如下
		
		shape:核的形状,OpenCV提供了三种核的形状,分别是矩形(MORPH_RECT = 0, 八领域),十字形(MORPH_CROSS = 1, 四领域),椭圆形(MORPH_ELLIPSE = 2)
		ksize:size()数据类型,表示核的大小
		anchor:替代核中的像素值位置,Point(-1, -1)默认替代核的中心位置
*/
Mat getStructuringElement(int shape, Size ksize, Point anchor = Point(-1,-1));

/*
		腐蚀与膨胀操作使用例子
*/
void QuickDemo::ErodeAndDilateDemo() {
	Mat src = imread("D://Photo//images//cells.png", IMREAD_COLOR);
	Mat gray;
	Mat binary;
	//二值化
	GaussianBlur(src, src, Size(9, 9), 2);	//高斯模糊
	cvtColor(src, gray, COLOR_BGR2GRAY);
	threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);

	//定义腐蚀与膨胀的核,最后一个参数-1表示代替中心位置的像素
	Mat kernel = getStructuringElement(MORPH_RECT, Size(5, 5), Point(-1, -1));
	Mat result1 = Mat::zeros(src.size(), src.type());
	Mat result2 = Mat::zeros(src.size(), src.type());
  
	//对二值图像的腐蚀操作
	erode(binary, result1, kernel);		//erode:腐蚀,缩小轮廓
	//对二值图像的膨胀操作	
	dilate(binary, result2, kernel);	//dilate:膨胀,扩大轮廓

	imshow("binary", binary);
	imshow("erode result1", result1);
	imshow("dilate result2", result2);
}

15. 图像形态学操作,开操作(erosion)与闭操作(dilate)

开操作,开操作是先腐蚀,再膨胀,开操作的目的是用来消除小物体、在纤细点处分离物体、平滑较大物体的边界,这个很好理解,因为先进行的腐蚀操作(缩小轮廓),会去除轮廓的一些小凸起,然后进行膨胀操作,会对较大物体的轮廓进行放大,使得看起来更加平滑。

闭操作,闭操作是先膨胀,再腐蚀,闭操作的目的是将图像中靠近的图块进行连通,这个也很好理解,因为先进行的膨胀操作,靠的较近的图块由于轮廓会被放大,这时,两个图块的轮廓就会重合,达到图块连通的目的,然后再进行腐蚀操作,将连通部分的突起进行缩小

/*
		OpenCV提供了形态学操作的API,morphologyEx,该API可以执行erode, dilate, open, close等形态学操作,函数原型如下
		src:任意通道的图像,常见的BGR, GRAY, BINARY图像都可以进行形态学操作
		dst:形态学操作输出的结果,图像大小、类型和src一致
		op: 形态学操作,MorphTypes提供了8种形态学操作,这里先知道四种,后续篇幅会有其他四种的介绍,MORPH_OPEN = 2, MORPH_CLOSE = 3, MORPH_ERODE = 0, MORPH_DILATE = 1
    kernel:形态学操作用到的核(结构元素)
		anchor:形态学操作替代核中像素值的位置,Point(-1, -1)表示默认替代核的中心位置像素值
		iterations:形态学操作迭代次数
    borderType:处理边界的类型,默认即可,具体类型可以在BorderTypes枚举中查看
		borderValue:指定边界的填充值,默认即可
*/
void morphologyEx( InputArray src, OutputArray dst, int op, InputArray kernel, Point anchor = Point(-1,-1), int iterations = 1, int borderType = BORDER_CONSTANT, const Scalar& borderValue = morphologyDefaultBorderValue());

/*
		开操作和闭操作使用例子
*/
void QuickDemo::MorphologyOpenAndCloseDemo() {
	Mat src = imread("D://Photo//images//fill.png", IMREAD_COLOR);
	Mat gray, binary;

	//灰度且二值化
	cvtColor(src, gray, COLOR_BGR2GRAY);
	threshold(gray, binary, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);

	Mat dst1, dst2;

	//定义开闭操作的卷积核,第一个参数为卷积核的形状
	Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));

	//开操作,删除小的干扰块,一定要进行二值化,并且使背景通道值为0,因为腐蚀是用通道小的值替代
	morphologyEx(binary, dst1, MORPH_OPEN, kernel, Point(-1, -1), 1);
	
	//闭操作,填充闭合区域
	morphologyEx(binary, dst2, MORPH_CLOSE, kernel, Point(-1, -1), 1);

	imshow("src", src);
	imshow("binary", binary);
	imshow("dst1_open", dst1);
	imshow("dst2_close", dst2);
}

16. 图像形态学梯度

形态学梯度,形态学梯度的主要目的是将图像中图块的轮廓提取出来,为什么梯度可以提取出轮廓呢?这个很好理解,还记得我们在进行腐蚀和膨胀操作的时候,就是对图块边缘轮廓进行缩小或放大。这样,腐蚀的结果、膨胀的结果和原图的像素差别就是轮廓部分的大小不一样,又因为梯度就是在这三者之间做像素差,所以结果就把轮廓给提取出来了。具体的有基本梯度,内梯度,外梯度三种

  • 基本梯度:膨胀减去腐蚀之后的结果,MorphTypes种提供了MORPH_GRADIENT实现
  • 内梯度:原图减去腐蚀之后的结果,需要自己做像素差
  • 外梯度:膨胀减去原图之后的结果,需要自己做像素差

/*
		形态学梯度原函数也是和上述15一样,用OpenCV提供的morphologyEx执行即可,将第三个参数op赋值为MORPH_GRADIENT即可实现基本梯度的需求
*/
void morphologyEx( InputArray src, OutputArray dst, int op, InputArray kernel, Point anchor = Point(-1,-1), int iterations = 1, int borderType = BORDER_CONSTANT, const Scalar& borderValue = morphologyDefaultBorderValue());


/*
		形态学梯度使用例子
		
		细心的读者会发现,我这里形态学操作跟上面不同的是,使用GRAY类型的图像,这也验证了,形态学操作支持任意类型的图像
*/
void QuickDemo::MorphologyGradientDemo() {
	Mat src = imread("D://Photo//images//LinuxLogo.jpg", IMREAD_COLOR);
	Mat gray, binary;

	//灰度且二值化
	cvtColor(src, gray, COLOR_BGR2GRAY);
	threshold(gray, binary, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);

	//定义形态学操作的卷积核
	Mat kernel = getStructuringElement(MORPH_ELLIPSE, Size(3, 3), Point(-1, -1));
	Mat basic_grad, inter_grad, exter_grad;
	
	//基本梯度
	morphologyEx(gray, basic_grad, MORPH_GRADIENT, kernel, Point(-1, -1), 1);

	Mat dst1, dst2;
	erode(gray, dst1, kernel);		//腐蚀
	dilate(gray, dst2, kernel);		//膨胀
	
	//内梯度
	subtract(gray, dst1, inter_grad);
	//外梯度
	subtract(dst2, gray, exter_grad);

	imshow("src", src);
	imshow("gray", gray);
	imshow("binary", binary);
	imshow("basic_grad", basic_grad);
	imshow("inter_grad", inter_grad);
	imshow("exter_grad", exter_grad);
}

17. 图像形态学操作,顶帽、黑帽

顶帽,顶帽操作的主要目的是提取图像中的噪声,突出原图像中,局部区域比周围亮的部分,它是原图减去开操作的结果。这个好理解,我们知道,开操作的主要目的是用来消除小物体、在纤细点处分离物体、平滑较大物体的边界,用原图减去开操作之后,我们就能提取出那些消除的小物体(噪声),以及大物体没有平滑的部分(局部区域比周围亮的部分)

黑帽,黑帽操作的主要目的是提取图像中,局部区域比周围暗的部分,它是闭操作减去原图的结果。这个也好理解,我们知道,闭操作的主要目的是将两个靠近的图块进行连通,原图这两个图块之间本来是不连通的(局部区域比周围暗的部分),所以用闭操作减去原图,就能把这个比周围暗的区域提取出来

/*
		顶帽与黑帽操作,直接通过OpenCV提供的形态学操作API执行即可,第三个参数 op = MORPH_TOPHAT为顶帽操作,op = MORPH_BLACKHAT为黑帽操作
*/
void morphologyEx( InputArray src, OutputArray dst, int op, InputArray kernel, Point anchor = Point(-1,-1), int iterations = 1, int borderType = BORDER_CONSTANT, const Scalar& borderValue = morphologyDefaultBorderValue());

/*
		顶帽与黑帽操作使用例子
*/
void QuickDemo::OtherMorphologyDemo() {
	Mat src = imread("D://Photo//images//cross.png", IMREAD_COLOR);
	Mat gray, binary;

	//灰度且二值化
	cvtColor(src, gray, COLOR_BGR2GRAY);
	threshold(gray, binary, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);

	//定义形态学操作卷积核
	Mat kernel = getStructuringElement(MORPH_ELLIPSE, Size(15, 15), Point(-1, -1));

	//顶帽
	Mat dst1;
	morphologyEx(binary, dst1, MORPH_TOPHAT, kernel, Point(-1, -1), 1);

	//黑帽
	Mat dst2;
	morphologyEx(binary, dst2, MORPH_BLACKHAT, kernel, Point(-1, -1), 1);

	imshow("gray", gray);
	imshow("binary", binary);
	imshow("top_hat demo", dst1);
	imshow("black_hat demo", dst2);
}
;