本章学习各种利用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,读者可以进行快速查阅。