《OpenCV3编程入门 》学习笔记 第7章 图像变换

导读

上一章中我们讲解了许多类型的图像处理方法。而迄今为止,所介绍的大多数操作都用于增强、修改或者“处理”图像,使之成为类似但全新的图像。

本章关注的重点是图像变换(image transfrom),即将一幅图像转变成图像数据的另一种表现形式。变换最常见的例子就是傅里叶变换(Fourier transform),即将图像转换成源图像数据的另一种表示形式。这类操作的结果仍然保存为OpenCV图像结构的形式,但是新图像的每个单独像素表示原始输出图像的频谱分量,而不是通常所考虑的空间分量。

其实,计算机视觉中经常会用到许多有用的变换。OpenCV提供了一套完整的实现工具和方法,可以帮助我们实现各种图像变换操作。

本章中,你将学到:

● 基于OpenCV的边缘检测

● 霍夫变换

● 重映射

● 仿射变换

● 直方图均衡化

7.1 基于OpenCV的边缘检测

本节中,我们将一起学习OpenCV中边缘检测的各种算子和滤波器——Canny算子、Sobel算子、Laplacian算子以及Scharr滤波器。

7.1.1 边缘检测的一般步骤

在具体介绍之前,先来一起看看边缘检测的一般步骤。

1.【第一步】滤波

边缘检测的算法主要是基于图像强度的一阶和二阶导数,但导数通常对噪声很敏感,因此必须采用滤波器来改善与噪声有关的边缘检测器的性能。常见的滤波方法主要有高斯滤波,即采用离散化的高斯函数产生一组归一化的高斯核,然后基于高斯核函数对图像灰度矩阵的每一点进行加权求和。

2.【第二步】增强

增强边缘的基础是确定图像各点邻域强度的变化值。增强算法可以将图像灰度点邻域强度值有显著变化的点凸显出来。在具体编程实现时,可通过计算梯度幅值来确定。

3.【第三步】检测

经过增强的图像,往往邻域中有很多点的梯度值比较大,而在特定的应用中,这些点并不是要找的边缘点,所以应该采用某种方法来对这些点进行取舍。实际工程中,常用的方法是通过阈值化方法来检测。

另外,需要注意,下文中讲到的Laplacian算子、sobel算子和Scharr算子都是带方向的,所以,示例中我们分别写了X方向、Y方向和最终合成的的效果图。

7.1.2 canny算子

1.canny算子简介

Canny边缘检测算子是John F.Canny于1986年开发出来的一个多级边缘检测算法。更为重要的是,Canny创立了边缘检测计算理论(Computational theory ofedge detection),解释了这项技术是如何工作的。Canny边缘检测算法以Canny的名字命名,被很多人推崇为当今最优的边缘检测的算法。

其中,Canny的目标是找到一个最优的边缘检测算法,让我们看一下最优边缘检测的三个主要评价标准。

● 低错误率:标识出尽可能多的实际边缘,同时尽可能地减少噪声产生的误报。

● 高定位性:标识出的边缘要与图像中的实际边缘尽可能接近。

● 最小响应:图像中的边缘只能标识一次,并且可能存在的图像噪声不应标识为边缘。

为了满足这些要求,Canny使用了变分法,这是一种寻找满足特定功能的函数的方法。最优检测用4个指数函数项的和表示,但是它非常近似于高斯函数的一阶导数。

2.Canny边缘检测的步骤

(1)【第一步】消除噪声

一般情况下,使用高斯平滑滤波器卷积降噪。以下显示了一个size=5的高斯内核示例:

(2)【第二步】计算梯度幅值和方向

此处,按照Sobel滤波器的步骤来操作。

① 运用一对卷积阵列(分别作用于x和y方向)

② 使用下列公式计算梯度幅值和方向

而梯度方向一般取这4个可能的角度之一——0度,45度,90度,135度。

(3)【第三步】非极大值抑制

这一步排除非边缘像素,仅仅保留了一些细线条(候选边缘)。

(4)【第四步】滞后阈值

这是最后一步,Canny使用了滞后阈值,滞后阈值需要两个阈值(高阈值和低阈值):

① 若某一像素位置的幅值超过高阈值,该像素被保留为边缘像素。

② 若某一像素位置的幅值小于低阈值,该像素被排除。

③ 若某一像素位置的幅值在两个阈值之间,该像素仅仅在连接到一个高于高阈值的像素时被保留。

对于Canny函数的使用,推荐的高低阈值比在2:1到3:1之间。

3.Canny边缘检测:Canny()函数

Canny函数利用Canny算子来进行图像的边缘检测操作。

void Canny( InputArray image, OutputArray edges,
                         double threshold1, double threshold2,
                         int apertureSize = 3, bool L2gradient = false );

● 第一个参数,InputArray类型的image,输入图像,即源图像,填Mat类的对象即可,且需为单通道8位图像。

● 第二个参数,OutputArray类型的edges,输出的边缘图,需要和源图片有一样的尺寸和类型。

● 第三个参数,double类型的threshold1,第一个滞后性阈值。

● 第四个参数,double类型的threshold2,第二个滞后性阈值。

● 第五个参数,int类型的apertureSize,表示应用Sobel算子的孔径大小,其有默认值3。

● 第六个参数,bool类型的L2gradient,一个计算图像梯度幅值的标识,有默认值false。

需要注意的是,这个函数阈值1和阈值2两者中较小的值用于边缘连接,而较大的值用来控制强边缘的初始段,推荐的高低阈值比在2:1到3:1之间。

4.示例程序:Canny边缘检测

OpenCV中调用Canny函数的实例代码如下。


/*	@File				: 56_canny.cpp
 *  @Brief				: 示例程序56
 *  @Details			: canny函数用法示例
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-04-27
 */

#include <opencv2/opencv.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
using namespace cv;

int main( )
{
	//载入原始图  
	Mat srcImage = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图
	Mat srcImage1=srcImage.clone();

	//显示原始图 
	imshow("【原始图】Canny边缘检测", srcImage); 

	
	//----------------------------------------------------------------------------------
	//	二、高阶的canny用法,转成灰度图,降噪,用canny,最后将得到的边缘作为掩码,拷贝原图到效果图上,得到彩色的边缘图
	//----------------------------------------------------------------------------------
	Mat dstImage,edge,grayImage;

	// 【1】创建与src同类型和大小的矩阵(dst)
	dstImage.create( srcImage1.size(), srcImage1.type() );

	// 【2】将原图像转换为灰度图像
	cvtColor( srcImage1, grayImage, COLOR_BGR2GRAY );

	// 【3】先用使用 3x3内核来降噪
	blur( grayImage, edge, Size(3,3) );

	// 【4】运行Canny算子
	Canny( edge, edge, 3, 9,3 );

	//【5】将g_dstImage内的所有元素设置为0 
	dstImage = Scalar::all(0);

	//【6】使用Canny算子输出的边缘图g_cannyDetectedEdges作为掩码,来将原图g_srcImage拷到目标图g_dstImage中
	srcImage1.copyTo( dstImage, edge);

	//【7】显示效果图 
	imshow("【效果图】Canny边缘检测2", dstImage); 


	waitKey(0); 

	return 0; 
}

运行结果:

7.1.3 sobel算子

1.sobel算子的基本概念

Sobel算子是一个主要用于边缘检测的离散微分算子(discrete differentiation operator)。它结合了高斯平滑和微分求导,用来计算图像灰度函数的近似梯度。在图像的任何一点使用此算子,都将会产生对应的梯度矢量或是其法矢量。

2.sobel算子的计算过程

我们假设被作用图像为I然后进行如下操作。

(1)分别在x和y两个方向求导。

① 水平变化:将I与一个奇数大小的内核Gx进行卷积。比如,当内核大小为3时,Gx的计算结果为:

② 垂直变化:将:I与一个奇数大小的内核进行卷积。比如,当内核大小为3时,计算结果为:

(2)在图像的每一点,结合以上两个结果求出近似梯度:

另外有时,也可用下面更简单的公式代替:

3.使用Sobel算子:Sobel()函数

Sobel函数使用扩展的Sobel算子,来计算一阶、二阶、三阶或混合图像差分。

void Sobel( InputArray src, OutputArray dst, int ddepth,
                         int dx, int dy, int ksize = 3,
                         double scale = 1, double delta = 0,
                         int borderType = BORDER_DEFAULT );

(1)第一个参数,InputArray类型的src,为输入图像,填Mat类型即可。

(2)第二个参数,OutputArray类型的dst,即目标图像,函数的输出参数,需要和源图片有一样的尺寸和类型。

(3)第三个参数,int类型的ddepth,输出图像的深度,支持如下src.depth()和ddepth的组合:

● 若src.depth()=CV_8U,取ddepth=-1/CV_16S/CV_32F/CV_64F

● 若src.depth()=CV_16U/CV_16S,取ddepth=-1/CV_32F/CV_64F

● 若src.depth()=CV_32F,取ddepth=-1/CV_32F/CV_64F

● 若src.depth()=CV_64F,取ddepth=-1/CV_64F

(4)第四个参数,int类型dx,x方向上的差分阶数。

(5)第五个参数,int类型dy,y方向上的差分阶数。

(6)第六个参数,int类型ksize,有默认值3,表示Sobel核的大小;必须取1、3、5或7。

(7)第七个参数,double类型的scale,计算导数值时可选的缩放因子,默认值是1,表示默认情况下是没有应用缩放的。可以在文档中查阅getDerivKernels的相关介绍,来得到这个参数的更多信息。

(8)第八个参数,double类型的delta,表示在结果存入目标图(第二个参数dst)之前可选的delta值,有默认值0。

(9)第九个参数,int类型的borderType,边界模式,默认值为BORDER_DEFAULT。这个参数可以在官方文档中borderInterpolate处得到更详细的信息。

一般情况下,都是用ksize(ksize内核来计算导数的。然而,有一种特殊情况——当ksize为1时,往往会使用3 x 1或者1 x 3的内核。且这种情况下,并没有进行高斯平滑操作。

一些补充说明如下。

① 当内核大小为3时,Sobel内核可能产生比较明显的误差(毕竟,Sobel算子只是求取了导数的近似值而已)。为解决这一问题,OpenCV提供了Scharr函数,但该函数仅作用于大小为3的内核。该函数的运算与Sobel函数一样快,但结果却更加精确,其内核是这样的:

② 因为Sobel算子结合了高斯平滑和分化(differentiation),因此结果会具有更多的抗噪性。大多数情况下,我们使用sobel函数时,取【xorder=1,yorder=0,ksize=3】来计算图像X方向的导数,【xorder=0,yorder=1,ksize=3】来计算图像y方向的导数。

计算图像X方向的导数,取【xorder=1,yorder=0,ksize=3】情况对应的内核:

而计算图像Y方向的导数,取【xorder=0,yorder=1,ksize=3】对应的内核:

4.示例程序:Sobel算子的使用调用

Sobel函数的实例代码如下。这里只是教大家如何使用Sobel函数,就没有先用一句cvtColor将原图转化为灰度图,而是直接用彩色图操作。


/*	@File				: 57_sobel.cpp
 *  @Brief				: 示例程序57
 *  @Details			: sobel函数用法示例
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-04-27
*/

#include <opencv2/opencv.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>

using namespace cv;

int main( )
{
	//【0】创建 grad_x 和 grad_y 矩阵
	Mat grad_x, grad_y;
	Mat abs_grad_x, abs_grad_y,dst;

	//【1】载入原始图  
	Mat src = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图

	//【2】显示原始图 
	imshow("【原始图】sobel边缘检测", src); 

	//【3】求 X方向梯度
	Sobel( src, grad_x, CV_16S, 1, 0, 3, 1, 1, BORDER_DEFAULT );
	convertScaleAbs( grad_x, abs_grad_x );
	imshow("【效果图】 X方向Sobel", abs_grad_x); 

	//【4】求Y方向梯度
	Sobel( src, grad_y, CV_16S, 0, 1, 3, 1, 1, BORDER_DEFAULT );
	convertScaleAbs( grad_y, abs_grad_y );
	imshow("【效果图】Y方向Sobel", abs_grad_y); 

	//【5】合并梯度(近似)
	addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, dst );
	imshow("【效果图】整体方向Sobel", dst); 

	waitKey(0); 
	return 0; 
}

 运行截图:

7.1.4 Laplacian算子

1.Laplacian算子简介

Laplacian算子是n维欧几里德空间中的一个二阶微分算子,定义为梯度grad的散度div。因此如果f是二阶可微的实函数,则f的拉普拉斯算子定义如下。

(1)f的拉普拉斯算子也是笛卡儿坐标系xi中的所有非混合二阶偏导数求和。

(2)作为一个二阶微分算子,拉普拉斯算子把C函数映射到C函数。对于k≥2,表达式(1)(或(2))定义了一个算子Δ:C(R)→C(R);或更一般地,对于任何开集Ω,定义了一个算子Δ:C(Ω)→ C(Ω)。

根据图像处理的原理可知,二阶导数可以用来进行检测边缘。因为图像是“二维”,需要在两个方向进行求导。使用Laplacian算子将会使求导过程变得简单。

Laplacian算子的定义:

需要说明的是,由于Laplacian使用了图像梯度,它内部的代码其实是调用了Sobel算子的。

让一幅图像减去它的Laplacian算子可以增强对比度。

2.计算拉普拉斯变换:Laplacian()函数

Laplacian函数可以计算出图像经过拉普拉斯变换后的结果。

void Laplacian( InputArray src, OutputArray dst, int ddepth,
                             int ksize = 1, double scale = 1, double delta = 0,
                             int borderType = BORDER_DEFAULT );

● 第一个参数,InputArray类型的image,输入图像,即源图像,填Mat类的对象即可,且需为单通道8位图像。

● 第二个参数,OutputArray类型的edges,输出的边缘图,需要和源图片有一样的尺寸和通道数。

● 第三个参数,int类型的ddept,目标图像的深度。

● 第四个参数,int类型的ksize,用于计算二阶导数的滤波器的孔径尺寸,大小必须为正奇数,且有默认值1。

● 第五个参数,double类型的scale,计算拉普拉斯值的时候可选的比例因子,有默认值1。

● 第六个参数,double类型的delta,表示在结果存入目标图(第二个参数dst)之前可选的delta值,有默认值0。

● 第七个参数,int类型的borderType,边界模式,默认值为BORDER_DEFAULT。这个参数可以在官方文档中borderInterpolate()处得到更详细的信息。

Laplacian()函数其实主要是利用sobel算子的运算。它通过加上sobel算子运算出的图像x方向和y方向上的导数,来得到载入图像的拉普拉斯变换结果。

其中,sobel算子(ksize>1)如下:

而当ksize=1时,Laplacian()函数采用以下3x3的孔径:

3.示例程序:Laplacian算子的使用

让我们看一看调用实例。



/*	@File				: 58_Laplacian.cpp
 *  @Brief				: 示例程序58
 *  @Details			: Laplacian函数用法示例
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-04-27
*/

#include <opencv2/opencv.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
using namespace cv;

int main( )
{
	//【0】变量的定义
	Mat src,src_gray,dst, abs_dst;

	//【1】载入原始图  
	src = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图

	//【2】显示原始图 
	imshow("【原始图】图像Laplace变换", src); 

	//【3】使用高斯滤波消除噪声
	GaussianBlur( src, src, Size(3,3), 0, 0, BORDER_DEFAULT );

	//【4】转换为灰度图
	cvtColor( src, src_gray, COLOR_RGB2GRAY );

	//【5】使用Laplace函数
	Laplacian( src_gray, dst, CV_16S, 3, 1, 0, BORDER_DEFAULT );

	//【6】计算绝对值,并将结果转换成8位
	convertScaleAbs( dst, abs_dst );

	//【7】显示效果图
	imshow( "【效果图】图像Laplace变换", abs_dst );

	waitKey(0); 

	return 0; 
}

运行结果:

7.1.5 scharr滤波器

我们一般直接称scharr为滤波器,而不是算子。上文已经讲到,它在OpenCV中主要是配合Sobel算子的运算而存在的。下面让我们直接来看看其函数讲解。

1.计算图像差分:Scharr()函数

使用Scharr滤波器运算符计算x或y方向的图像差分。其实它的参数变量和Sobel基本上是一样的,除了没有ksize核的大小。

void Scharr( InputArray src, OutputArray dst, int ddepth,
                          int dx, int dy, double scale = 1, double delta = 0,
                          int borderType = BORDER_DEFAULT );

(1)第一个参数,InputArray类型的src,为输入图像,填Mat类型即可。

(2)第二个参数,OutputArray类型的dst,即目标图像,函数的输出参数,需要和源图片有一样的尺寸和类型。

(3)第三个参数,int类型的ddepth,输出图像的深度,支持如下src.depth()和ddepth的组合:

● 若src.depth()=CV_8U,取ddepth=-1/CV_16S/CV_32F/CV_64F

● 若src.depth()=CV_16U/CV_16S,取ddepth=-1/CV_32F/CV_64F

● 若src.depth()=CV_32F,取ddepth=-1/CV_32F/CV_64F

● 若src.depth()=CV_64F,取ddepth=-1/CV_64F

(4)第四个参数,int类型dx,x方向上的差分阶数。

(5)第五个参数,int类型dy,y方向上的差分阶数。

(6)第六个参数,double类型的scale,计算导数值时可选的缩放因子,默认值是1,表示默认情况下是没有应用缩放的。我们可以在文档中查阅getDerivKernels的相关介绍,来得到这个参数的更多信息。

(7)第七个参数,double类型的delta,表示在结果存入目标图(第二个参数dst)之前可选的delta值,有默认值0。

(8)第八个参数,int类型的borderType,边界模式,默认值为BORDER_DEFAULT。这个参数可以在官方文档中borderInterpolate处得到更详细的信息。

不难理解,如下两者是等价的,即:

2.示例程序:Scharr滤波器


/*	@File				: 59_Scharr.cpp
 *  @Brief				: 示例程序59
 *  @Details			: Scharr函数用法示例
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-04-27
*/


#include <opencv2/opencv.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
using namespace cv;


int main( )
{
	//【0】创建 grad_x 和 grad_y 矩阵
	Mat grad_x, grad_y;
	Mat abs_grad_x, abs_grad_y,dst;

	//【1】载入原始图  
	Mat src = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图

	//【2】显示原始图 
	imshow("【原始图】Scharr滤波器", src); 

	//【3】求 X方向梯度
	Scharr( src, grad_x, CV_16S, 1, 0, 1, 0, BORDER_DEFAULT );
	convertScaleAbs( grad_x, abs_grad_x );
	imshow("【效果图】 X方向Scharr", abs_grad_x); 

	//【4】求Y方向梯度
	Scharr( src, grad_y, CV_16S, 0, 1, 1, 0, BORDER_DEFAULT );
	convertScaleAbs( grad_y, abs_grad_y );
	imshow("【效果图】Y方向Scharr", abs_grad_y); 

	//【5】合并梯度(近似)
	addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, dst );

	//【6】显示效果图
	imshow("【效果图】合并梯度后Scharr", dst); 

	waitKey(0); 
	return 0; 
}

运行效果:

7.1.6 综合示例:边缘检测

本节依然是配给大家一个详细注释的配套示例程序,把这节中介绍的知识点以代码为载体,更形象地展现出来。

这个示例程序中,分别演示了canny边缘检测、sobel边缘检测、scharr滤波器的使用,经过详细注释的的代码如下。


/*	@File				: 60_EdgeDetection.cpp
 *  @Brief				: 示例程序60
 *  @Details			: 边缘检测综合示例——Canny算子,Sobel算子,Laplace算子,Scharr滤波器合辑
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-04-27
*/


#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace cv;


//原图,原图的灰度版,目标图
Mat g_srcImage, g_srcGrayImage,g_dstImage;

//Canny边缘检测相关变量
Mat g_cannyDetectedEdges;
int g_cannyLowThreshold=1;//TrackBar位置参数  

//Sobel边缘检测相关变量
Mat g_sobelGradient_X, g_sobelGradient_Y;
Mat g_sobelAbsGradient_X, g_sobelAbsGradient_Y;
int g_sobelKernelSize=1;//TrackBar位置参数  

//Scharr滤波器相关变量
Mat g_scharrGradient_X, g_scharrGradient_Y;
Mat g_scharrAbsGradient_X, g_scharrAbsGradient_Y;


//-----------------------------------【全局函数声明部分】--------------------------------------
//		描述:全局函数声明
//-----------------------------------------------------------------------------------------------
static void ShowHelpText( );
static void on_Canny(int, void*);//Canny边缘检测窗口滚动条的回调函数
static void on_Sobel(int, void*);//Sobel边缘检测窗口滚动条的回调函数
void Scharr( );//封装了Scharr边缘检测相关代码的函数


//-----------------------------------【main( )函数】--------------------------------------------
//		描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( int argc, char** argv )
{
	//改变console字体颜色
	system("color 2F");  

	//显示欢迎语
	ShowHelpText();

	//载入原图
	g_srcImage = imread("1.jpg");
	if( !g_srcImage.data ) { printf("Oh,no,读取srcImage错误~! \n"); return false; }

	//显示原始图
	namedWindow("【原始图】");
	imshow("【原始图】", g_srcImage);

	// 创建与src同类型和大小的矩阵(dst)
	g_dstImage.create( g_srcImage.size(), g_srcImage.type() );

	// 将原图像转换为灰度图像
	cvtColor( g_srcImage, g_srcGrayImage, COLOR_BGR2GRAY );

	// 创建显示窗口
	namedWindow( "【效果图】Canny边缘检测", WINDOW_AUTOSIZE );
	namedWindow( "【效果图】Sobel边缘检测", WINDOW_AUTOSIZE );

	// 创建trackbar
	createTrackbar( "参数值:", "【效果图】Canny边缘检测", &g_cannyLowThreshold, 120, on_Canny );
	createTrackbar( "参数值:", "【效果图】Sobel边缘检测", &g_sobelKernelSize, 3, on_Sobel );

	// 调用回调函数
	on_Canny(0, 0);
	on_Sobel(0, 0);

	//调用封装了Scharr边缘检测代码的函数
	Scharr( );

	//轮询获取按键信息,若按下Q,程序退出
	while((char(waitKey(1)) != 'q')) {}

	return 0;
}


//-----------------------------------【ShowHelpText( )函数】----------------------------------
//		描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
static void ShowHelpText()
{
	//输出欢迎信息和OpenCV版本
	printf("\n\n\t\t\t非常感谢购买《OpenCV3编程入门》一书!\n");
	printf("\n\n\t\t\t此为本书OpenCV3版的第60个配套示例程序\n");
	printf("\n\n\t\t\t   当前使用的OpenCV版本为:" CV_VERSION );
	printf("\n\n  ----------------------------------------------------------------------------\n");

	//输出一些帮助信息
	printf( "\n\n\t运行成功,请调整滚动条观察图像效果~\n\n"
		"\t按下“q”键时,程序退出。\n");
}


//-----------------------------------【on_Canny( )函数】----------------------------------
//		描述:Canny边缘检测窗口滚动条的回调函数
//-----------------------------------------------------------------------------------------------
void on_Canny(int, void*)
{
	// 先使用 3x3内核来降噪
	blur( g_srcGrayImage, g_cannyDetectedEdges, Size(3,3) );

	// 运行我们的Canny算子
	Canny( g_cannyDetectedEdges, g_cannyDetectedEdges, g_cannyLowThreshold, g_cannyLowThreshold*3, 3 );

	//先将g_dstImage内的所有元素设置为0 
	g_dstImage = Scalar::all(0);

	//使用Canny算子输出的边缘图g_cannyDetectedEdges作为掩码,来将原图g_srcImage拷到目标图g_dstImage中
	g_srcImage.copyTo( g_dstImage, g_cannyDetectedEdges);

	//显示效果图
	imshow( "【效果图】Canny边缘检测", g_dstImage );
}



//-----------------------------------【on_Sobel( )函数】----------------------------------
//		描述:Sobel边缘检测窗口滚动条的回调函数
//-----------------------------------------------------------------------------------------
void on_Sobel(int, void*)
{
	// 求 X方向梯度
	Sobel( g_srcImage, g_sobelGradient_X, CV_16S, 1, 0, (2*g_sobelKernelSize+1), 1, 1, BORDER_DEFAULT );
	convertScaleAbs( g_sobelGradient_X, g_sobelAbsGradient_X );//计算绝对值,并将结果转换成8位

	// 求Y方向梯度
	Sobel( g_srcImage, g_sobelGradient_Y, CV_16S, 0, 1, (2*g_sobelKernelSize+1), 1, 1, BORDER_DEFAULT );
	convertScaleAbs( g_sobelGradient_Y, g_sobelAbsGradient_Y );//计算绝对值,并将结果转换成8位

	// 合并梯度
	addWeighted( g_sobelAbsGradient_X, 0.5, g_sobelAbsGradient_Y, 0.5, 0, g_dstImage );

	//显示效果图
	imshow("【效果图】Sobel边缘检测", g_dstImage); 

}


//-----------------------------------【Scharr( )函数】----------------------------------
//		描述:封装了Scharr边缘检测相关代码的函数
//-----------------------------------------------------------------------------------------
void Scharr( )
{
	// 求 X方向梯度
	Scharr( g_srcImage, g_scharrGradient_X, CV_16S, 1, 0, 1, 0, BORDER_DEFAULT );
	convertScaleAbs( g_scharrGradient_X, g_scharrAbsGradient_X );//计算绝对值,并将结果转换成8位

	// 求Y方向梯度
	Scharr( g_srcImage, g_scharrGradient_Y, CV_16S, 0, 1, 1, 0, BORDER_DEFAULT );
	convertScaleAbs( g_scharrGradient_Y, g_scharrAbsGradient_Y );//计算绝对值,并将结果转换成8位

	// 合并梯度
	addWeighted( g_scharrAbsGradient_X, 0.5, g_scharrAbsGradient_Y, 0.5, 0, g_dstImage );

	//显示效果图
	imshow("【效果图】Scharr滤波器", g_dstImage); 
}

下面放出一些程序运行效果图。

7.2 霍夫变换

本节中,我们将一起探讨OpenCV中霍夫变换相关的知识点,并了解了OpenCV中实现霍夫线变换的HoughLines、HoughLinesP函数的使用方法,以及实现霍夫圆变换的HoughCircles函数的使用方法。

在图像处理和计算机视觉领域中,如何从当前的图像中提取所需要的特征信息是图像识别的关键所在。在许多应用场合中需要快速准确地检测出直线或者圆。其中一种非常有效的解决问题的方法是霍夫(Hough)变换,其为图像处理中从图像中识别几何形状的基本方法之一,应用很广泛,也有很多改进算法。最基本的霍夫变换是从黑白图像中检测直线(线段)。本节就将介绍OpenCV中霍夫变换的使用方法和相关知识。

7.2.1 霍夫变换概述

霍夫变换(Hough Transform)是图像处理中的一种特征提取技术,该过程在一个参数空间中通过计算累计结果的局部最大值得到一个符合该特定形状的集合作为霍夫变换结果。霍夫变换于1962年由PaulHough首次提出,最初的Hough变换是设计用来检测直线和曲线的。起初的方法要求知道物体边界线的解析方程,但不需要有关区域位置的先验知识。这种方法的一个突出优点是分割结果的Robustness,即对数据的不完全或噪声不是非常敏感。然而,要获得描述边界的解析表达常常是不可能的。后于1972年由Richard Duda&Peter Hart推广使用,经典霍夫变换用来检测图像中的直线,后来霍夫变换扩展到任意形状物体的识别,多为圆和椭圆。霍夫变换运用两个坐标空间之间的变换将在一个空间中具有相同形状的曲线或直线映射到另一个坐标空间的一个点上形成峰值,从而把检测任意形状的问题转化为统计峰值问题。

霍夫变换在OpenCV中分为霍夫线变换和霍夫圆变换两种,下面将分别进行介绍。

7.2.2 OpenCV中的霍夫线变换

我们知道,霍夫线变换是一种用来寻找直线的方法.在使用霍夫线变换之前,首先要对图像进行边缘检测的处理,即霍夫线变换的直接输入只能是边缘二值图像。

OpenCV支持三种不同的霍夫线变换,它们分别是:标准霍夫变换(Standard Hough Transform, SHT)、多尺度霍夫变换(Multi-Scale Hough Transform,MSHT)和累计概率霍夫变换(Progressive Probabilistic Hough Transform,PPHT)。

其中,多尺度霍夫变换(MSHT)为经典霍夫变换(SHT)在多尺度下的一个变种。而累计概率霍夫变换(PPHT)算法是标准霍夫变换(SHT)算法的一个改进,它在一定的范围内进行霍夫变换,计算单独线段的方向以及范围,从而减少计算量,缩短计算时间。之所以称PPHT为“概率”的,是因为并不将累加器平面内的所有可能的点累加,而只是累加其中的一部分,该想法是如果峰值如果足够高,只用一小部分时间去寻找它就够了。按照猜想,可以实质性地减少计算时间。

在OpenCV中,可以用HoughLines函数来调用标准霍夫变换(SHT)和多尺度霍夫变换(MSHT)。

而HoughLinesP函数用于调用累计概率霍夫变换PPHT。累计概率霍夫变换执行效率很高,所有相比于HoughLines函数,我们更倾向于使用HoughLinesP函数。

总结一下,OpenCV中的霍夫线变换有如下三种:

● 标准霍夫变换(StandardHough Transform, SHT),由HoughLines函数调用。

● 多尺度霍夫变换(Multi-ScaleHough Transform, MSHT),由HoughLines函数调用。

● 累计概率霍夫变换(ProgressiveProbabilistic Hough Transform, PPHT),由HoughLinesP函数调用。

7.2.3 霍夫线变换的原理

(1)众所周知,一条直线在图像二维空间可由两个变量表示,有以下两种情况。如图7.16所示。

① 在笛卡尔坐标系:可由参数斜率和截距(m, b)表示。

② 在极坐标系:可由参数极径和极角(r, θ)表示。

对于霍夫变换,我们将采用第二种方式极坐标系来表示直线.因此,直线的表达式可为:

(2)一般来说对于点(x0, y0),可以将通过这个点的一族直线统一定义为:

这就意味着每一对(rθ, θ)代表一条通过点(x0, y0)的直线。

(3)如果对于一个给定点(x0, y0),我们在极坐标对极径极角平面绘出所有通过它的直线,将得到一条正弦曲线.例如,对于给定点x0=8和y0=6可以绘出如7.17所示平面图。

只绘出满足下列条件的点r>0和.0<θ<2π

(4)我们可以对图像中所有的点进行上述操作.如果两个不同点进行上述操作后得到的曲线在平面θ-r相交,这就意味着它们通过同一条直线。例如,接上面的例子继续对点x1=9,y1=4和点x2=12,y2=3绘图,得到图7.18。

这三条曲线在平面相交于点(0.925, 9.6),坐标表示的是参数对θ-r或者是说点(x0,y0),点(x1, y1)和点(x2, y2)组成的平面内的的直线。

(5)以上的说明表明,一般来说,一条直线能够通过在平面θ-r寻找交于一点的曲线数量来检测。而越多曲线交于一点也就意味着这个交点表示的直线由更多的点组成。一般来说我们可以通过设置直线上点的阈值来定义多少条曲线交于一点,这样才认为检测到了一条直线。

(6)这就是霍夫线变换要做的。它追踪图像中每个点对应曲线间的交点.如果交于一点的曲线的数量超过了阈值,那么可以认为这个交点所代表的参数对(θ, rθ)在原图像中为一条直线。

7.2.4 标准霍夫变换:HoughLines()函数

此函数可以找出采用标准霍夫变换的二值图像线条。在OpenCV中,我们可以用其来调用标准霍夫变换SHT和多尺度霍夫变换MSHT的OpenCV内建算法。

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 );

● 第一个参数,InputArray类型的image,输入图像,即源图像。需为8位的单通道二进制图像,可以将任意的源图载入进来,并由函数修改成此格式后,再填在这里。

● 第二个参数,InputArray类型的lines,经过调用HoughLines函数后储存了霍夫线变换检测到线条的输出矢量。每一条线由具有两个元素的矢量(ρ, θ)表示,其中,ρ是离坐标原点(0, 0)(也就是图像的左上角)的距离,θ是弧度线条旋转角度(0度表示垂直线,π/2度表示水平线)。

● 第三个参数,double类型的rho,以像素为单位的距离精度。另一种表述方式是直线搜索时的进步尺寸的单位半径。(Latex中/rho即表示ρ)

● 第四个参数,double类型的theta,以弧度为单位的角度精度。另一种表述方式是直线搜索时的进步尺寸的单位角度。

● 第五个参数,int类型的threshold,累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold的线段才可以被检测通过并返回到结果中。

● 第六个参数,double类型的srn,有默认值0。对于多尺度的霍夫变换,这是第三个参数进步尺寸rho的除数距离。粗略的累加器进步尺寸直接是第三个参数rho,而精确的累加器进步尺寸为rho/srn。

● 第七个参数,double类型的stn,有默认值0,对于多尺度霍夫变换,srn表示第四个参数进步尺寸的单位角度theta的除数距离。且如果srn和stn同时为0,就表示使用经典的霍夫变换。否则,这两个参数应该都为正数。

学完函数解析后,看一个以HoughLines为核心的示例程序,就可以全方位了解HoughLines函数的使用方法。


/*	@File				: 61_HoughLines.cpp
 *  @Brief				: 示例程序61
 *  @Details			: HoughLines函数用法示例
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-04-27
*/


#include <opencv2/opencv.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace cv;
using namespace std;


int main( )
{
	//【1】载入原始图和Mat变量定义   
	Mat srcImage = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图
	Mat midImage,dstImage;//临时变量和目标图的定义

	//【2】进行边缘检测和转化为灰度图
	Canny(srcImage, midImage, 50, 200, 3);//进行一此canny边缘检测
	cvtColor(midImage,dstImage, COLOR_GRAY2BGR);//转化边缘检测后的图为灰度图

	//【3】进行霍夫线变换
	vector<Vec2f> lines;//定义一个矢量结构lines用于存放得到的线段矢量集合
	HoughLines(midImage, lines, 1, CV_PI/180, 150, 0, 0 );

	//【4】依次在图中绘制出每条线段
	for( size_t i = 0; i < lines.size(); i++ )
	{
		float rho = lines[i][0], theta = lines[i][1];
		Point pt1, pt2;
		double a = cos(theta), b = sin(theta);
		double x0 = a*rho, y0 = b*rho;
		pt1.x = cvRound(x0 + 1000*(-b));
		pt1.y = cvRound(y0 + 1000*(a));
		pt2.x = cvRound(x0 - 1000*(-b));
		pt2.y = cvRound(y0 - 1000*(a));
		//此句代码的OpenCV2版为:
		//line( dstImage, pt1, pt2, Scalar(55,100,195), 1, CV_AA);
		//此句代码的OpenCV3版为:
		line( dstImage, pt1, pt2, Scalar(55,100,195), 1, LINE_AA);
	}

	//【5】显示原始图  
	imshow("【原始图】", srcImage);  

	//【6】边缘检测后的图 
	imshow("【边缘检测后的图】", midImage);  

	//【7】显示效果图  
	imshow("【效果图】", dstImage);  

	waitKey(0);  

	return 0;  
}


 运行效果:

可以通过调节line(dstImage,pt1,pt2,Scalar(55, 100, 195),1,CV_AA);一句Scalar(55, 100, 195)参数中G、B、R颜色值的数值,得到图中想要的线条颜色。

7.2.5 累计概率霍夫变换:HoughLinesP()函数

此函数在HoughLines的基础上,在末尾加了一个代表Probabilistic(概率)的P,表明它可以采用累计概率霍夫变换(PPHT)来找出二值图像中的直线。

void HoughLinesP( InputArray image, OutputArray lines,
                  double rho, double theta, int threshold,
                  double minLineLength = 0, double maxLineGap = 0 );

● 第一个参数,InputArray类型的image,输入图像,即源图像。需为8位的单通道二进制图像,可以将任意的源图载入进来后由函数修改成此格式后,再填在这里。

● 第二个参数,InputArray类型的lines,经过调用HoughLinesP函数后存储了检测到的线条的输出矢量,每一条线由具有4个元素的矢量(x_1, y_1, x_2, y_2)表示,其中,(x_1, y_1)和(x_2, y_2)是是每个检测到的线段的结束点。

● 第三个参数,double类型的rho,以像素为单位的距离精度。另一种表述方式是直线搜索时的进步尺寸的单位半径。

● 第四个参数,double类型的theta,以弧度为单位的角度精度。另一种表述方式是直线搜索时的进步尺寸的单位角度。

● 第五个参数,int类型的threshold,累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold的线段才可以被检测通过并返回到结果中。

● 第六个参数,double类型的minLineLength,有默认值0,表示最低线段的长度,比这个设定参数短的线段就不能被显现出来。

● 第七个参数,double类型的maxLineGap,有默认值0,允许将同一行点与点之间连接起来的最大的距离。

对于此函数,依然是为大家准备了示例程序,如下。


/*	@File				: 62_HoughLinesP.cpp
 *  @Brief				: 示例程序62
 *  @Details			: HoughLinesP函数用法示例
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-04-27
*/


#include <opencv2/opencv.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace cv;
using namespace std;


int main( )
{
	//【1】载入原始图和Mat变量定义   
	Mat srcImage = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图
	Mat midImage,dstImage;//临时变量和目标图的定义

	//【2】进行边缘检测和转化为灰度图
	Canny(srcImage, midImage, 50, 200, 3);//进行一此canny边缘检测
	cvtColor(midImage,dstImage, COLOR_GRAY2BGR);//转化边缘检测后的图为灰度图

	//【3】进行霍夫线变换
	vector<Vec4i> lines;//定义一个矢量结构lines用于存放得到的线段矢量集合
	HoughLinesP(midImage, lines, 1, CV_PI/180, 80, 50, 10 );

	//【4】依次在图中绘制出每条线段
	for( size_t i = 0; i < lines.size(); i++ )
	{
		Vec4i l = lines[i];
		
		line( dstImage, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(186,88,255), 1, LINE_AA);
	}

	//【5】显示原始图  
	imshow("【原始图】", srcImage);  

	//【6】边缘检测后的图 
	imshow("【边缘检测后的图】", midImage);  

	//【7】显示效果图  
	imshow("【效果图】", dstImage);  

	waitKey(0);  

	return 0;  
}


运行效果:

7.2.6 霍夫圆变换

霍夫圆变换的基本原理和上面讲的霍夫线变化大体上是很类似的,只是点对应的二维极径极角空间被三维的圆心点x、y和半径r空间取代。说“大体上类似”的原因是,如果完全用相同的方法的话,累加平面会被三维的累加容器所代替——在这三维中,一维是x,一维是y,另外一维是圆的半径r。这就意味着需要大量的内存而且执行效率会很低,速度会很慢。

对直线来说,一条直线能由参数极径极角(r, θ)表示.而对圆来说,我们需要三个参数来表示一个圆,也就是:

C:(xcenter, ycenter, r)

这里的(xcenter, ycenter)表示圆心的位置(下图中球心的点),而r表示半径。这样我们就能唯一的定义一个圆了,见下图。

在OpenCV中,我们常常通过一个叫做“霍夫梯度法”的方法来解决圆变换的问题。

7.2.7 霍夫梯度法的原理

霍夫梯度法的原理是这样的:

(1)首先对图像应用边缘检测,比如用canny边缘检测。

(2)然后,对边缘图像中的每一个非零点,考虑其局部梯度,即用Sobel()函数计算x和y方向的Sobel一阶导数得到梯度。

(3)利用得到的梯度,由斜率指定的直线上的每一个点都在累加器中被累加,这里的斜率是从一个指定的最小值到指定的最大值的距离。

(4)同时,标记边缘图像中每一个非0像素的位置。

(5)然后从二维累加器中这些点中选择候选的中心,这些中心都大于给定阈值并且大于其所有近邻。这些候选的中心按照累加值降序排列,以便于最支持像素的中心首先出现。

(6)接下来对每一个中心,考虑所有的非0像素。

(7)这些像素按照其与中心的距离排序。从到最大半径的最小距离算起,选择非0像素最支持的一条半径。

(8)如果一个中心收到边缘图像非0像素最充分的支持,并且到前期被选择的中心有足够的距离,那么它就会被保留下来。

这个实现可以使算法执行起来更高效,或许更加重要的是,能够帮助解决三维累加器中会产生许多噪声并且使得结果不稳定的稀疏分布问题。

人无完人,金无足赤。同样,这个算法也并不是十全十美的,还有许多需要指出的缺点。

7.2.8 霍夫梯度法的缺点

(1)在霍夫梯度法中,我们使用Sobel导数来计算局部梯度,那么随之而来的假设是,它可以视作等同于一条局部切线,这并不是一个数值稳定的做法。在大多数情况下,这样做会得到正确的结果,但或许会在输出中产生一些噪声。

(2)在边缘图像中的整个非0像素集被看做每个中心的候选部分。因此,如果把累加器的阈值设置偏低,算法将要消耗比较长的时间。此外,因为每一个中心只选择一个圆,如果有同心圆,就只能选择其中的一个。

(3)因为中心是按照其关联的累加器值的升序排列的,并且如果新的中心过于接近之前已经接受的中心的话,就不会被保留下来。且当有许多同心圆或者是近似的同心圆时,霍夫梯度法的倾向是保留最大的一个圆。可以说这是一种比较极端的做法,因为在这里默认Sobel导数会产生噪声,若是对于无穷分辨率的平滑图像而言的话,这才是必须的。

7.2.9 霍夫圆变换:HoughCircles()函数

HoughCircles函数可以利用霍夫变换算法检测出灰度图中的圆。它相比之前的HoughLines和HoughLinesP,比较明显的一个区别是不需要源图是二值的,而HoughLines和HoughLinesP都需要源图为二值图像。

void HoughCircles( InputArray image, OutputArray circles,
                  int method, double dp, double minDist,
                  double param1 = 100, double param2 = 100,
                  int minRadius = 0, int maxRadius = 0 );

● 第一个参数,InputArray类型的image,输入图像,即源图像,需为8位的灰度单通道图像。

● 第二个参数,InputArray类型的circles,经过调用HoughCircles函数后此参数存储了检测到的圆的输出矢量,每个矢量由包含了3个元素的浮点矢量(x, y, radius)表示。

● 第三个参数,int类型的method,即使用的检测方法,目前OpenCV中就霍夫梯度法一种可以使用,它的标识符为HOUGH_GRADIENT(OpenCV2中可写作CV_HOUGH_GRADIENT),在此参数处填这个标识符即可。

● 第四个参数,double类型的dp,用来检测圆心的累加器图像的分辨率于输入图像之比的倒数,且此参数允许创建一个比输入图像分辨率低的累加器。例如,如果dp=1时,累加器和输入图像具有相同的分辨率。如果dp=2,累加器便有输入图像一半那么大的宽度和高度。

● 第五个参数,double类型的minDist,为霍夫变换检测到的圆的圆心之间的最小距离,即让算法能明显区分的两个不同圆之间的最小距离。这个参数如果太小的话,多个相邻的圆可能被错误地检测成了一个重合的圆。反之,这个参数设置太大,某些圆就不能被检测出来。

● 第六个参数,double类型的param1,有默认值100。它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示传递给canny边缘检测算子的高阈值,而低阈值为高阈值的一半。

● 第七个参数,double类型的param2,也有默认值100。它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示在检测阶段圆心的累加器阈值。它越小,就越可以检测到更多根本不存在的圆,而它越大的话,能通过检测的圆就更加接近完美的圆形了。

● 第八个参数,int类型的minRadius,有默认值0,表示圆半径的最小值。

● 第九个参数,int类型的maxRadius,也有默认值0,表示圆半径的最大值。

需要注意的是,使用此函数可以很容易地检测出圆的圆心,但是它可能找不到合适的圆半径。我们可以通过第八个参数minRadius和第九个参数maxRadius指定最小和最大的圆半径,来辅助圆检测的效果。或者,可以直接忽略返回半径,因为它们都有着默认值0,只用HoughCircles函数检测出来的圆心,然后用额外的一些步骤来进一步确定半径。

依然是为大家准备了基于此函数的示例程序。


/*	@File				: 63_HoughCircles.cpp
 *  @Brief				: 示例程序63
 *  @Details			: HoughCircles函数用法示例
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-04-27
*/

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace cv;
using namespace std;


int main( )
{
	//【1】载入原始图、Mat变量定义   
	Mat srcImage = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图
	Mat midImage,dstImage;//临时变量和目标图的定义

	//【2】显示原始图
	imshow("【原始图】", srcImage);  

	//【3】转为灰度图并进行图像平滑
	cvtColor(srcImage,midImage, COLOR_BGR2GRAY);//转化边缘检测后的图为灰度图
	GaussianBlur( midImage, midImage, Size(9, 9), 2, 2 );

	//【4】进行霍夫圆变换
	vector<Vec3f> circles;
	HoughCircles( midImage, circles, HOUGH_GRADIENT,1.5, 10, 200, 100, 0, 0 );

	//【5】依次在图中绘制出圆
	for( size_t i = 0; i < circles.size(); i++ )
	{
		//参数定义
		Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));
		int radius = cvRound(circles[i][2]);
		//绘制圆心
		circle( srcImage, center, 3, Scalar(0,255,0), -1, 8, 0 );
		//绘制圆轮廓
		circle( srcImage, center, radius, Scalar(155,50,255), 3, 8, 0 );
	}

	//【6】显示效果图  
	imshow("【效果图】", srcImage);  

	waitKey(0);  

	return 0;  
}

程序运行截图见图7.21、7.22。

7.2.10 综合示例:霍夫变换

这次的综合示例,我们在HoughLinesP函数的基础上,为其添加了用于控制其第五个参数阈值threshold的滚动条,因此可以通过调节滚动条来改变阈值,从而动态地控制霍夫线变换检测的线条多少。

详细注释的程序源代码如下。


/*	@File				: 64_HoughLines.cpp
 *  @Brief				: 示例程序64
 *  @Details			: 霍夫线变换综合示例
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-04-27
*/
 
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace std;
using namespace cv;



Mat g_srcImage, g_dstImage,g_midImage;//原始图、中间图和效果图
vector<Vec4i> g_lines;//定义一个矢量结构g_lines用于存放得到的线段矢量集合
//变量接收的TrackBar位置参数
int g_nthreshold=100;

//-----------------------------------【全局函数声明部分】--------------------------------------
//		描述:全局函数声明
//-----------------------------------------------------------------------------------------------

static void on_HoughLines(int, void*);//回调函数
static void ShowHelpText();


//-----------------------------------【main( )函数】--------------------------------------------
//		描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( )
{
	//改变console字体颜色
	system("color 4F");  

	ShowHelpText();

	//载入原始图和Mat变量定义   
	Mat g_srcImage = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图

	//显示原始图  
	imshow("【原始图】", g_srcImage);  

	//创建滚动条
	namedWindow("【效果图】",1);
	//createTrackbar("值", "【效果图】",&g_nthreshold,200,on_HoughLines);

	//进行边缘检测和转化为灰度图
	Canny(g_srcImage, g_midImage, 50, 200, 3);//进行一次canny边缘检测
	cvtColor(g_midImage,g_dstImage, COLOR_GRAY2BGR);//转化边缘检测后的图为灰度图

	//调用一次回调函数,调用一次HoughLinesP函数
	on_HoughLines(g_nthreshold,0);
	HoughLinesP(g_midImage, g_lines, 1, CV_PI/180, 80, 50, 10 );

	//显示效果图  
	imshow("【效果图】", g_dstImage);  


	waitKey(0);  

	return 0;  

}


//-----------------------------------【on_HoughLines( )函数】--------------------------------
//		描述:【顶帽运算/黑帽运算】窗口的回调函数
//----------------------------------------------------------------------------------------------
static void on_HoughLines(int, void*)
{
	//定义局部变量储存全局变量
	Mat dstImage=g_dstImage.clone();
	Mat midImage=g_midImage.clone();

	//调用HoughLinesP函数
	vector<Vec4i> mylines;
	HoughLinesP(midImage, mylines, 1, CV_PI/180, g_nthreshold+1, 50, 10 );

	//循环遍历绘制每一条线段
	for( size_t i = 0; i < mylines.size(); i++ )
	{
		Vec4i l = mylines[i];
		line( dstImage, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(23,180,55), 1, LINE_AA);
	}
	//显示图像
	imshow("【效果图】",dstImage);
}

//-----------------------------------【ShowHelpText( )函数】----------------------------------
//		描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
static void ShowHelpText()
{
	//输出欢迎信息和OpenCV版本
	printf("\n\n\t\t\t非常感谢购买《OpenCV3编程入门》一书!\n");
	printf("\n\n\t\t\t此为本书OpenCV3版的第64个配套示例程序\n");
	printf("\n\n\t\t\t   当前使用的OpenCV版本为:" CV_VERSION );
	printf("\n\n  ----------------------------------------------------------------------------\n");

	//输出一些帮助信息
	printf("\n\n\n\t请调整滚动条观察图像效果~\n\n");


}

运行效果:

  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值