《OpenCV3编程入门 》学习笔记 第6章 图像处理(2)

本章学习各种利用OpenCV进行图像处理的方法,包括属于线性滤波的方框滤波、均值滤波与高斯滤波,属于非线性滤波的中值滤波、双边滤波;两种基本形态学操作——膨胀与腐蚀;5种高级形态学滤波操作——开运算、闭运算、形态学梯度、顶帽以及黑帽;此外,还有漫水填充算法、图像金字塔、图像缩放、阈值化。

6.3.4 相关OpenCV源码分析溯源

在…\opencv\sources\modules\imgproc\src\morph.dispatch.cpp路径中,我们可以发现erode(腐蚀)函数和dilate(膨胀)函数的源码,如下。

void erode( InputArray src, OutputArray dst, InputArray kernel,
                Point anchor, int iterations,
                int borderType, const Scalar& borderValue )
{
    CV_INSTRUMENT_REGION();

    CV_Assert(!src.empty());

    morphOp( MORPH_ERODE, src, dst, kernel, anchor, iterations, borderType, borderValue );
}


void dilate( InputArray src, OutputArray dst, InputArray kernel,
                 Point anchor, int iterations,
                 int borderType, const Scalar& borderValue )
{
    CV_INSTRUMENT_REGION();

    CV_Assert(!src.empty());

    morphOp( MORPH_DILATE, src, dst, kernel, anchor, iterations, borderType, borderValue );
}

可以发现,erode和dilate这两个函数内部就是调用了一下morphOp,只是它们调用morphOp时,第一个参数标识符不同:一个为MORPH_ERODE(腐蚀),一个为MORPH_DILATE(膨胀)。

6.3.5 相关核心API函数讲解

1.膨胀:dilate函数

dilate函数使用像素邻域内的局部极大运算符来膨胀一张图片,从src输入,由dst输出。支持就地(in-place)操作。

函数原型如下。

void dilate( InputArray src, OutputArray dst, InputArray kernel,
                          Point anchor = Point(-1,-1), int iterations = 1,
                          int borderType = BORDER_CONSTANT,
                          const Scalar& borderValue = morphologyDefaultBorderValue() );

参数详解如下。

● 第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。图像通道的数量可以是任意的,但图像深度应为CV_8U、CV_16U、CV_16S、CV_32F或CV_64F其中之一。

● 第二个参数,OutputArray类型的dst,即目标图像,需要和源图片有一样的尺寸和类型。

● 第三个参数,InputArray类型的kernel,膨胀操作的核。当为NULL时,表示的是使用参考点位于中心3×3的核。

我们一般使用函数getStructuringElement配合这个参数的使用。getStructuringElement函数会返回指定形状和尺寸的结构元素(内核矩阵)。其中,getStructuringElement函数的第一个参数表示内核的形状,有如下三种形状可以选择。

● 矩形:MORPH_RECT;

● 交叉形:MORPH_CROSS;

● 椭圆形:MORPH_ELLIPSE。

而getStructuringElement函数的第二和第三个参数分别是内核的尺寸以及锚点的位置。

一般在调用erode以及dilate函数之前,先定义一个Mat类型的变量来获得getStructuringElement函数的返回值。对于锚点的位置,有默认值Point(-1,-1),表示锚点位于中心。此外,需要注意,十字形的element形状唯一依赖于锚点的位置,而在其他情况下,锚点只是影响了形态学运算结果的偏移。

getStructuringElement函数相关的调用示例代码如下。

调用之后,我们可以在接下来调用erode或dilate函数时,在第三个参数填保存了getStructuringElement返回值的Mat类型变量。对应于上面的示例,就是element变量。

● 第四个参数,Point类型的anchor,锚的位置,其有默认值(-1,-1),表示锚位于中心。

● 第五个参数,int类型的iterations,迭代使用erode()函数的次数,默认值为1。

● 第六个参数,int类型的borderType,用于推断图像外部像素的某种边界模式。注意它有默认值BORDER_DEFAULT。

● 第七个参数,const Scalar&类型的borderValue,当边界为常数时的边界值,有默认值morphologyDefaultBorderValue(),一般不用去管它。需要用到它时,可以看官方文档中的createMorphologyFilter()函数,以得到更详细的解释。

使用erode函数,一般只需要填前面的三个参数,后面的四个参数都有默认值,而且往往会结合getStructuringElement一起使用。

下面给出一个调用范例。


/*	@File				: 38_dilate.cpp
 *  @Brief				: 示例程序38
 *  @Details			: 图像膨胀dilate函数用法示例
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-04-21
*/


//-----------------------------------【头文件包含部分】---------------------------------------
//	描述:包含程序所依赖的头文件
//---------------------------------------------------------------------------------------------- 
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>

//-----------------------------------【命名空间声明部分】---------------------------------------
//	描述:包含程序所使用的命名空间
//-----------------------------------------------------------------------------------------------  
using namespace std;
using namespace cv;

//-----------------------------------【main( )函数】--------------------------------------------
//	描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main(   )
{

	//载入原图  
	Mat image = imread("1.jpg");

	//创建窗口  
	namedWindow("【原图】膨胀操作");
	namedWindow("【效果图】膨胀操作");

	//显示原图
	imshow("【原图】膨胀操作", image);

	//进行膨胀操作 
	Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
	Mat out;
	dilate(image, out, element);

	//显示效果图 
	imshow("【效果图】膨胀操作", out);

	waitKey(0); 

	return 0;
}

此程序运行截图见前文中图6.22。

2.腐蚀:erode函数

erode函数使用像素邻域内的局部极小运算符来腐蚀一张图片,从src输入,由dst输出。支持就地(in-place)操作。

看一下函数原型,如下。

void erode( InputArray src, OutputArray dst, InputArray kernel,
                         Point anchor = Point(-1,-1), int iterations = 1,
                         int borderType = BORDER_CONSTANT,
                         const Scalar& borderValue = morphologyDefaultBorderValue() );

参数详解如下。

● 第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。图像通道的数量可以是任意的,但图像深度应为CV_8U、CV_16U、CV_16S、CV_32F或CV_64F其中之一。

● 第二个参数,OutputArray类型的dst,即目标图像,需要和源图片有一样的尺寸和类型。

● 第三个参数,InputArray类型的kernel,腐蚀操作的内核。为NULL时,表示的是使用参考点位于中心3x3的核。一般使用函数getStructuringElement配合这个参数的使用。getStructuringElement函数会返回指定形状和尺寸的结构元素(内核矩阵,具体看上文中dilate函数的第三个参数讲解部分。

● 第四个参数,Point类型的anchor,锚的位置。其有默认值(-1,-1),表示锚位于单位(element)的中心,一般不用管它。

● 第五个参数,int类型的iterations,迭代使用erode()函数的次数,默认值为1。

● 第六个参数,int类型的borderType,用于推断图像外部像素的某种边界模式。注意它有默认值BORDER_DEFAULT。

● 第七个参数,const Scalar&类型的borderValue,当边界为常数时的边界值,有默认值morphologyDefaultBorderValue(),一般不用去管它。需要用到它时,可以看官方文档中的createMorphologyFilter()函数以得到更详细的解释。

同样的,使用erode函数,一般只需要填前面的三个参数,后面的四个参数都有默认值。而且往往结合getStructuringElement一起使用。

调用范例如下。


/*	@File				: 39_erode.cpp
 *  @Brief				: 示例程序39
 *  @Details			: 图像腐蚀erode函数用法示例
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-04-21
*/


//---------------------------------【头文件、命名空间包含部分】----------------------------
//		描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------------------------------------
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace cv;

//-----------------------------------【main( )函数】--------------------------------------------
//	描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main(   )
{
	//载入原图  
	Mat srcImage = imread("1.jpg");
	//显示原图
	imshow("【原图】腐蚀操作", srcImage);
	//进行腐蚀操作 
	Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
	Mat dstImage;
	erode(srcImage, dstImage, element);
	//显示效果图 
	imshow("【效果图】腐蚀操作", dstImage);
	waitKey(0); 

	return 0;
}

此程序运行截图见前文中图6.25。

6.3.6 综合示例:腐蚀与膨胀

此示例程序中的效果图窗口中有两个滑动条第一个滑动条“腐蚀/膨胀”用于在腐蚀/膨胀之间进行切换;第二个滚动条”内核尺寸”用于调节形态学操作时的内核尺寸,以得到效果不同的图像,有一定的可玩性。详细注释的示例程序代码如下。


/*	@File				: 40_dilate_And_erode.cpp
 *  @Brief				: 示例程序40
 *  @Details			: 图像腐蚀与膨胀综合示例
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-04-21
*/

//---------------------------------【头文件、命名空间包含部分】-----------------------------
//		描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------------------------------------
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
using namespace std;
using namespace cv;


//-----------------------------------【全局变量声明部分】--------------------------------------
//		描述:全局变量声明
//-----------------------------------------------------------------------------------------------
Mat g_srcImage, g_dstImage;//原始图和效果图
int g_nTrackbarNumer = 0;//0表示腐蚀erode, 1表示膨胀dilate
int g_nStructElementSize = 3; //结构元素(内核矩阵)的尺寸


//-----------------------------------【全局函数声明部分】--------------------------------------
//		描述:全局函数声明
//-----------------------------------------------------------------------------------------------
void Process();//膨胀和腐蚀的处理函数
void on_TrackbarNumChange(int, void *);//回调函数
void on_ElementSizeChange(int, void *);//回调函数
void ShowHelpText();

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

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

	ShowHelpText();

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

	//进行初次腐蚀操作并显示效果图
	namedWindow("【效果图】");
	//获取自定义核
	Mat element = getStructuringElement(MORPH_RECT, Size(2*g_nStructElementSize+1, 2*g_nStructElementSize+1),Point( g_nStructElementSize, g_nStructElementSize ));
	erode(g_srcImage, g_dstImage, element);
	imshow("【效果图】", g_dstImage);

	//创建轨迹条
	createTrackbar("腐蚀/膨胀", "【效果图】", &g_nTrackbarNumer, 1, on_TrackbarNumChange);
	createTrackbar("内核尺寸", "【效果图】", &g_nStructElementSize, 21, on_ElementSizeChange);

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

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

	return 0;
}

//-----------------------------【Process( )函数】------------------------------------
//		描述:进行自定义的腐蚀和膨胀操作
//-----------------------------------------------------------------------------------------
void Process() 
{
	//获取自定义核
	Mat element = getStructuringElement(MORPH_RECT, Size(2*g_nStructElementSize+1, 2*g_nStructElementSize+1),Point( g_nStructElementSize, g_nStructElementSize ));

	//进行腐蚀或膨胀操作
	if(g_nTrackbarNumer == 0) {    
		erode(g_srcImage, g_dstImage, element);
	}
	else {
		dilate(g_srcImage, g_dstImage, element);
	}

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


//-----------------------------【on_TrackbarNumChange( )函数】------------------------------------
//		描述:腐蚀和膨胀之间切换开关的回调函数
//-----------------------------------------------------------------------------------------------------
void on_TrackbarNumChange(int, void *) 
{
	//腐蚀和膨胀之间效果已经切换,回调函数体内需调用一次Process函数,使改变后的效果立即生效并显示出来
	Process();
}


//-----------------------------【on_ElementSizeChange( )函数】-------------------------------------
//		描述:腐蚀和膨胀操作内核改变时的回调函数
//-----------------------------------------------------------------------------------------------------
void on_ElementSizeChange(int, void *)
{
	//内核尺寸已改变,回调函数体内需调用一次Process函数,使改变后的效果立即生效并显示出来
	Process();
}


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

运行截图分别如下所示。

由于篇幅所限,截图就先展示这些,更多的运行效果请大家自行运行书本配套示例程序进行学习和观赏。

6.4 形态学滤波(2):开运算、闭运算、形态学梯度、顶帽、黑帽

上一节中,我们重点了解了腐蚀和膨胀这两种最基本的形态学操作,而运用这两个基本操作,可以实现更高级的形态学变换。

所以,本节的主角是OpenCV中的morphologyEx函数,它利用基本的膨胀和腐蚀技术,来执行更加高级的形态学变换,如开闭运算、形态学梯度、“顶帽”、“黑帽”等。

首先,我们需要知道,形态学的高级形态,往往都是建立在腐蚀和膨胀这两个基本操作之上的。而关于腐蚀和膨胀,概念和细节以及相关代码请参考


/*	@File				: 41_DilationEX.cpp
 *  @Brief				: 示例程序41
 *  @Details			: 用morphologyEx进行图像膨胀
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-04-21
*/

//---------------------------------【头文件、命名空间包含部分】----------------------------
//		描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------------------------------------
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace cv;


//-----------------------------------【main( )函数】--------------------------------------------
//		描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( )
{
	//载入原始图   
	Mat image = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图
	//创建窗口   
	namedWindow("【原始图】膨胀");  
	namedWindow("【效果图】膨胀");  
	//显示原始图  
	imshow("【原始图】膨胀", image);  
	//定义核
	Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));  
	//进行形态学操作
	morphologyEx(image, image, MORPH_DILATE, element);
	//显示效果图  
	imshow("【效果图】膨胀", image);  

	waitKey(0);  

	return 0;  
}

运行效果:

下面来看一个腐蚀的例子:


/*	@File				: 42_ErodeEX.cpp
 *  @Brief				: 示例程序42
 *  @Details			: 用morphologyEx进行图像腐蚀
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-04-21
*/

//-----------------------------------【头文件包含部分】---------------------------------------
//		描述:包含程序所依赖的头文件
//---------------------------------------------------------------------------------------------- 
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

//-----------------------------------【命名空间声明部分】---------------------------------------
//		描述:包含程序所使用的命名空间
//----------------------------------------------------------------------------------------------- 
using namespace cv;
//-----------------------------------【main( )函数】--------------------------------------------
//		描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( )
{
	//载入原始图   
	Mat image = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图
	//创建窗口   
	namedWindow("【原始图】腐蚀");  
	namedWindow("【效果图】腐蚀");  
	//显示原始图  
	imshow("【原始图】腐蚀", image);  
	//定义核
	Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));  
	//进行形态学操作
	morphologyEx(image, image, MORPH_ERODE, element);
	//显示效果图  
	imshow("【效果图】腐蚀", image);  

	waitKey(0);  

	return 0;  
}

 运行结果:

对膨胀和腐蚀心中有数了,接下来的高级形态学操作,应该就不难理解。

6.4.1 开运算

开运算(Opening Operation),其实就是先腐蚀后膨胀的过程。其数学表达式如下:

dst=open(src,element)=dilate(erode(src,element))

开运算可以用来消除小物体,在纤细点处分离物体,并且在平滑较大物体的边界的同时不明显改变其面积。原始图请参考图6.19,而效果图见图6.30和6.31。

 代码示例:


/*	@File				: 43_Opening.cpp
 *  @Brief				: 示例程序43
 *  @Details			: 用morphologyEx进行图像形态学开运算用法示例
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-04-21
*/

//---------------------------------【头文件、命名空间包含部分】----------------------------
//		描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------------------------------------
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace cv;

//-----------------------------------【main( )函数】--------------------------------------------
//		描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( )
{
	//载入原始图   
	Mat image = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图
	//创建窗口   
	namedWindow("【原始图】开运算");  
	namedWindow("【效果图】开运算");  
	//显示原始图  
	imshow("【原始图】开运算", image);  
	//定义核
	Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));  
	//进行形态学操作
	morphologyEx(image, image, MORPH_OPEN, element);
	//显示效果图  
	imshow("【效果图】开运算", image);  

	waitKey(0);  

	return 0;  
}

运行效果:

6.4.2 闭运算

先膨胀后腐蚀的过程称为闭运算(Closing Operation),其数学表达式如下:

dst=clese(src,element)=erode(dilate(src,element))

闭运算能够排除小型黑洞(黑色区域)。效果图如图6.32和图6.33所示。

源码:


/*	@File				: 44_Closing.cpp
 *  @Brief				: 示例程序44
 *  @Details			: 用morphologyEx进行图像闭运算
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-04-21
*/

//---------------------------------【头文件、命名空间包含部分】----------------------------
//		描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------------------------------------
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace cv;


//-----------------------------------【main( )函数】------------------------------------------
//		描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( )
{
	//载入原始图   
	Mat image = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图
	//创建窗口   
	namedWindow("【原始图】闭运算");  
	namedWindow("【效果图】闭运算");  
	//显示原始图  
	imshow("【原始图】闭运算", image);  
	//定义核
	Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));  
	//进行形态学操作
	morphologyEx(image, image, MORPH_CLOSE, element);
	//显示效果图  
	imshow("【效果图】闭运算", image);  

	waitKey(0);  

	return 0;  
}

运行效果:

                                                图6.33 图片闭运算效果图

6.4.3 形态学梯度

形态学梯度(Morphological Gradient)是膨胀图与腐蚀图之差,数学表达式如下:

dst=morph-grad(src,element)=dilate(src,element)-erode(src,element)

对二值图像进行这一操作可以将团块(blob)的边缘突出出来。我们可以用形态学梯度来保留物体的边缘轮廓,如图6.34和图6.35所示。

示例代码:



/*	@File				: 45_Gradient.cpp
 *  @Brief				: 示例程序45
 *  @Details			: 用morphologyEx进行形态学梯度运算
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-04-21
*/

//---------------------------------【头文件、命名空间包含部分】----------------------------
//		描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------------------------------------
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace cv;


//-----------------------------------【main( )函数】------------------------------------------
//		描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( )
{
	//载入原始图   
	Mat image = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图
	//创建窗口   
	namedWindow("【原始图】形态学梯度");  
	namedWindow("【效果图】形态学梯度");  
	//显示原始图  
	imshow("【原始图】形态学梯度", image);  
	//定义核
	Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));  
	//进行形态学操作
	morphologyEx(image, image, MORPH_GRADIENT, element);
	//显示效果图  
	imshow("【效果图】形态学梯度", image);  

	waitKey(0);  

	return 0;  
}

运行效果:

                                                   图6.35 图片形态学梯度效果图

6.4.4 顶帽

顶帽运算(Top Hat)又常常被译为”礼帽“运算,是原图像与上文刚刚介绍的“开运算”的结果图之差,数学表达式如下:

dst=tophat(src,element)=src-open(src,element)

因为开运算带来的结果是放大了裂缝或者局部低亮度的区域。因此,从原图中减去开运算后的图,得到的效果图突出了比原图轮廓周围的区域更明亮的区域,且这一操作与选择的核的大小相关。

顶帽运算往往用来分离比邻近点亮一些的斑块。在一幅图像具有大幅的背景,而微小物品比较有规律的情况下,可以使用顶帽运算进行背景提取。

如图6.36、图6.37所示。

示例代码:


/*	@File				: 46_TopHat.cpp
 *  @Brief				: 示例程序46
 *  @Details			: 用morphologyEx进行形态学顶帽运算
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-04-21
*/

//---------------------------------【头文件、命名空间包含部分】----------------------------
//		描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------------------------------------
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace cv;

//-----------------------------------【main( )函数】------------------------------------------
//		描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( )
{
	//载入原始图   
	Mat image = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图
	//创建窗口   
	namedWindow("【原始图】顶帽运算");  
	namedWindow("【效果图】顶帽运算");  
	//显示原始图  
	imshow("【原始图】顶帽运算", image);  
	//定义核
	Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));  
	//进行形态学操作
	morphologyEx(image, image, MORPH_TOPHAT, element);
	//显示效果图  
	imshow("【效果图】顶帽运算", image);  

	waitKey(0);  

	return 0;  
}

运行效果:

                                        图6.37 图片顶帽效果图

6.4.5 黑帽

黑帽(Black Hat)运算是闭运算的结果图与原图像之差。数学表达式为:

dst=blackhat(src,element)=close(src,element)-src

黑帽运算后的效果图突出了比原图轮廓周围的区域更暗的区域,且这一操作和选择的核的大小相关。

所以,黑帽运算用来分离比邻近点暗一些的斑块,效果图有着非常完美的轮廓。示例如图6.38和图6.39所示。

示例代码:



/*	@File				: 47_BlackHat.cpp
 *  @Brief				: 示例程序47
 *  @Details			: 用morphologyEx进行形态学黑帽运算
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-04-21
*/

//---------------------------------【头文件、命名空间包含部分】----------------------------
//		描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------------------------------------
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace cv;


//-----------------------------------【main( )函数】--------------------------------------------
//		描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( )
{
	//载入原始图   
	Mat image = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图
	//创建窗口   
	namedWindow("【原始图】黑帽运算");  
	namedWindow("【效果图】黑帽运算");  
	//显示原始图  
	imshow("【原始图】黑帽运算", image);  
	//定义核
	Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));  
	//进行形态学操作
	morphologyEx(image, image, MORPH_BLACKHAT, element);
	//显示效果图  
	imshow("【效果图】黑帽运算", image);  

	waitKey(0);  

	return 0;  
}

运行效果:

                                                          图6.39 照片黑帽效果图

6.4.6 形态学滤波OpenCV源码分析溯源

本节的主角是OpenCV中的morphologyEx函数,它利用基本的膨胀和腐蚀技术,来执行更加高级的形态学变换,如开闭运算、形态学梯度、“顶帽”、“黑帽”等。这一节我们来一起看一下morphologyEx函数的源代码。

void morphologyEx( InputArray _src, OutputArray _dst, int op,
                       InputArray _kernel, Point anchor, int iterations,
                       int borderType, const Scalar& borderValue )
{
    CV_INSTRUMENT_REGION();

    CV_Assert(!_src.empty());

    Mat kernel = _kernel.getMat();
    if (kernel.empty())
    {
        kernel = getStructuringElement(MORPH_RECT, Size(3,3), Point(1,1));
    }
#ifdef HAVE_OPENCL
    Size ksize = kernel.size();
    anchor = normalizeAnchor(anchor, ksize);

    CV_OCL_RUN(_dst.isUMat() && _src.dims() <= 2 && _src.channels() <= 4 &&
        anchor.x == ksize.width >> 1 && anchor.y == ksize.height >> 1 &&
        borderType == cv::BORDER_CONSTANT && borderValue == morphologyDefaultBorderValue(),
        ocl_morphologyEx(_src, _dst, op, kernel, anchor, iterations, borderType, borderValue))
#endif

    Mat src = _src.getMat(), temp;
    _dst.create(src.size(), src.type());
    Mat dst = _dst.getMat();

#if !IPP_DISABLE_MORPH_ADV
    //CV_IPP_RUN_FAST(ipp_morphologyEx(op, src, dst, kernel, anchor, iterations, borderType, borderValue));
#endif

    switch( op )
    {
    case MORPH_ERODE:
        erode( src, dst, kernel, anchor, iterations, borderType, borderValue );
        break;
    case MORPH_DILATE:
        dilate( src, dst, kernel, anchor, iterations, borderType, borderValue );
        break;
    case MORPH_OPEN:
        erode( src, dst, kernel, anchor, iterations, borderType, borderValue );
        dilate( dst, dst, kernel, anchor, iterations, borderType, borderValue );
        break;
    case MORPH_CLOSE:
        dilate( src, dst, kernel, anchor, iterations, borderType, borderValue );
        erode( dst, dst, kernel, anchor, iterations, borderType, borderValue );
        break;
    case MORPH_GRADIENT:
        erode( src, temp, kernel, anchor, iterations, borderType, borderValue );
        dilate( src, dst, kernel, anchor, iterations, borderType, borderValue );
        dst -= temp;
        break;
    case MORPH_TOPHAT:
        if( src.data != dst.data )
            temp = dst;
        erode( src, temp, kernel, anchor, iterations, borderType, borderValue );
        dilate( temp, temp, kernel, anchor, iterations, borderType, borderValue );
        dst = src - temp;
        break;
    case MORPH_BLACKHAT:
        if( src.data != dst.data )
            temp = dst;
        dilate( src, temp, kernel, anchor, iterations, borderType, borderValue );
        erode( temp, temp, kernel, anchor, iterations, borderType, borderValue );
        dst = temp - src;
        break;
    case MORPH_HITMISS:
        CV_Assert(src.type() == CV_8UC1);
        if(countNonZero(kernel) <=0)
        {
            src.copyTo(dst);
            break;
        }
        {
            Mat k1, k2, e1, e2;
            k1 = (kernel == 1);
            k2 = (kernel == -1);

            if (countNonZero(k1) <= 0)
                e1 = Mat(src.size(), src.type(), Scalar(255));
            else
                erode(src, e1, k1, anchor, iterations, borderType, borderValue);

            if (countNonZero(k2) <= 0)
                e2 = Mat(src.size(), src.type(), Scalar(255));
            else
            {
                Mat src_complement;
                bitwise_not(src, src_complement);
                erode(src_complement, e2, k2, anchor, iterations, borderType, borderValue);
            }
            dst = e1 & e2;
        }
        break;
    default:
        CV_Error( CV_StsBadArg, "unknown morphological operation" );
    }
}

看上面的源码可以发现,morphologyEx函数其实就是内部的一个大switch而已,根据不同的标识符取不同的操作。比如开运算MORPH_OPEN,按我们上文中讲解的数学表达式,就是先腐蚀后膨胀,即依次调用erode和dilate函数,代码非常简明干净。

6.4.7 核心API函数:morphologyEx()

上面已经讲到,morphologyEx函数利用基本的膨胀和腐蚀技术,来执行更加高级形态学变换,如开闭运算、形态学梯度、“顶帽”、“黑帽”等。下面我们来详细地讲解它的参数意义和使用方法。

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

● 第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。图像位深应该为以下5种之一:CV_8U、CV_16U、CV_16S、CV_32F和CV_64F。

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

● 第三个参数,int类型的op,表示形态学运算的类型,可以是如表6.2中任意之一的标识符。

● 第四个参数,InputArray类型的kernel,形态学运算的内核。若为NULL,表示的是使用参考点位于中心3×3的核。一般使用函数getStructuringElement配合这个参数的使用。getStructuringElement函数会返回指定形状和尺寸的结构元素(内核矩阵)。关于getStructuringElement我们之前有讲到过,这里为了大家参阅方便,再写一遍。

getStructuringElement函数的第一个参数表示内核的形状,我们可以选择如下三种形状之一:

● 矩形——MORPH_RECT

● 交叉形——MORPH_CROSS

● 椭圆形——MORPH_ELLIPSE

而getStructuringElement函数的第二和第三个参数分别是内核的尺寸以及锚点的位置。

一般在调用erode以及dilate函数之前,要先定义一个Mat类型的变量来获得getStructuringElement函数的返回值。对于锚点的位置,有默认值Point(-1,-1),表示锚点位于中心。另外需要注意:十字形的element形状唯一依赖于锚点的位置。而在其他情况下,锚点只是影响形态学运算结果的偏移。

getStructuringElement函数相关的调用示例代码如下。

 之后,便可以在调用erode、dilate或morphologyEx函数时,由kernel参数填保存getStructuringElement返回值的Mat类型变量。对应于上面的示例,就是填element变量。

● 第五个参数,Point类型的anchor,锚的位置,其有默认值(-1,-1),表示锚位于中心。

● 第六个参数,int类型的iterations,迭代使用函数的次数,默认值为1。

● 第七个参数,int类型的borderType,用于推断图像外部像素的某种边界模式。注意它有默认值BORDER_CONSTANT。

● 第八个参数,const Scalar&类型的borderValue,当边界为常数时的边界值,有默认值morphologyDefaultBorderValue(),一般不用去管它。需要用到它时,可以看官方文档中的createMorphologyFilter()函数得到更详细的解释。其中的这些操作都可以进行就地(in-place)操作,且对于多通道图像,每一个通道都单独进行操作。

6.4.8 各形态学操作使用范例一览

核心函数讲解完毕,下面便开始讲解使用范例。为了方便大家需要的时候随时取用,这里提供了利用morphologyEx函数实现的几乎全部的形态学操作:开运算、闭运算、形态学梯度、顶帽、黑帽、腐蚀、膨胀的效果实现简化版完整代码。其实说白了,这些代码基本上内容一致,就是改一下morphologyEx里面的第三个标识符参数而已。核都是选的MORPH_RECT(矩形元素结构)。另外,通过观察源代码发现,最基本的腐蚀和膨胀操作也可以用morphologyEx函数来实现,它们由morphologyEx函数源码中switch的前两个case来实现(虽然在case体内就是简单地各自调用了一下erode和dilation函数,但还是有写出来的必要)。所以在这里,我们也用morphologyEx再重新来实现一遍它们。以下便为实现代码(以形态学梯度为例)。

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

int main( )
{
	//载入原始图   
	Mat image = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图
	//创建窗口   
	namedWindow("【原始图】形态学梯度");  
	namedWindow("【效果图】形态学梯度");  
	//显示原始图  
	imshow("【原始图】形态学梯度", image);  
	//定义核
	Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));  
	//进行形态学操作
	morphologyEx(image, image, MORPH_GRADIENT, element);
	//显示效果图  
	imshow("【效果图】形态学梯度", image);  

	waitKey(0);  

	return 0;  
}

上述代码是以形态学梯度为例的程序,而若要选取其他形态学的操作,只需将morphologyEx函数中的第三个参数MORPH_GRADIENT替换成表6.2中相应的标识符即可。但是方面大家方便查看和使用,在配套示例程序代码包中,还是为大家分别准备了开运算、闭运算、形态学梯度、顶帽、黑帽、腐蚀、膨胀的效果的实现程序。具体程序在本书的配套代码包中的序号为41到47,读者可以进行快速查阅。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值