OpenCV3之——形态学滤波(2):开运算、闭运算、形态学梯度、顶帽、

         概述:形态学的高级形态,往往都是建立在腐蚀和膨胀这两个基本操作之上的,所以本篇主角是OpenCV的morphologyEx函数,它利用基本的膨胀和腐蚀技术,来执行更加高级的形态学变换,如开运算、闭运算、形态学梯度、“顶帽”、“黑帽” 等。

         为了下面对比和演示以及理解的方便,浅墨自己制作了一张毛笔字图,这里先上原图:

 

OpenCV3之——形态学滤波(2):开运算、闭运算、形态学梯度、顶帽、_第1张图片

 

   

    1.1 开运算(Opening Operation)

    开运算(opening operation),其实就是先腐蚀后膨胀过得过程,其数学表达式如下:

                                             

 

   开运算可以用来消除小物体,在纤细点出分离物体,平滑较大物体的边界同时并不明显改变其面积,效果图是这样的:

  

OpenCV3之——形态学滤波(2):开运算、闭运算、形态学梯度、顶帽、_第2张图片

 

   1.2 闭运算(Closing Operation)

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

                                   

   闭运算能够排除小型黑洞(黑色区域),如下所示:

  

OpenCV3之——形态学滤波(2):开运算、闭运算、形态学梯度、顶帽、_第3张图片

  

   

    1.3 形态学梯度(MorphologicalGradient)

    形态学梯度(morphological gradient)为膨胀图与腐蚀图之差,表达式如下:

                                    

 

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

  

OpenCV3之——形态学滤波(2):开运算、闭运算、形态学梯度、顶帽、_第4张图片

 

   

    1.4 顶帽(Top Hat)

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

                                   

  

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

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

 

OpenCV3之——形态学滤波(2):开运算、闭运算、形态学梯度、顶帽、_第5张图片

 

  

  1.5 黑帽(Black Hat)

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

                                           

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

 所以,黑帽运算用来分离比邻近点暗一些的斑块。非常完美的轮廓效果图:(Size(50,50)时)

 

OpenCV3之——形态学滤波(2):开运算、闭运算、形态学梯度、顶帽、_第6张图片

        源码溯源:(我正在使用opencv3.4.1)morphologyEx函数原型位于···\opencv\sources\modules\imgproc\src\morph.cpp中的第2039行,函数原型如下:

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

    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 = src;
            else
                erode(src, e1, k1, anchor, iterations, borderType, borderValue);

            Mat src_complement;
            bitwise_not(src, src_complement);
            if (countNonZero(k2) <= 0)
                e2 = src_complement;
            else
                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函数。

核心API函数:morphologyEx()

void cv::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类型的图像。图像深度为CV_8U、CV_16U、CV_16S、CV_32F或CV_64F其中之一;
  • 第二个参数,OutputArray类型的dst,即目标图像,函数的输出参数需要和源图片有一样的尺寸和类型;
  • 第三个参数,int类型的op,表示形态学运算的类型,参考上面的函数原型,填switch里面的case;
  • 第四个参数,InputArray类型的kernel,膨胀操作的核。当为NULL时,则使用参考点位于中心的3×3的核。我们一般使用getStructuringElement函数配合这个参数使用,getStructuringElement会返回指定形状和尺寸的结构元素(内核矩阵),其中getStructuringElement函数的第一个参数表示内核的形状(三种形状可选),如下:
  1. 矩形:MORPH_RECT
  2. 交叉形:MORPH_CROSS
  3. 椭圆形:MORPH_ELLIPSE

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

      g_nStructElementSize 函数相关的调用相关代码如下。

int g_nStructElementSize = 3;//结构元素(内核矩阵)的尺寸
 
//获取自定义核
Mat element = getStructuringElement( MORPH_RECT,
                Size(2*g_nStructElementSize+1,2*g_nStructElementSize+1 ),
                Point(g_nStructElementSize ,g_nStructElementSize ) );

        调用之后,我们可以在接下来调用erode或dilate函数时,在第三个参数填保存g_nStructElementSize 返回值的Mat类型的element变量。

  • 第五个参数,Point类型的anchor,锚点位置,有默认值(-1,-1),表示锚点位于中心;
  • 第六个参数,int类型的iterations,迭代 使用函数 的次数,默认值为1;
  • 第七个参数,int类型的borderType,默认值BORDER_DEFAULT;
  • 第八个参数,const Scalar&类型的borderValue,边界为常数值时,有默认值morphologyDefaultBorderValue(),一般不用去管它,需要使用时,可以查看官方文档中的createMorphologyFilter()函数。

一、各形态学使用范例一览

        morphologyEx函数几乎可以实现全部的形态学操作,这些代码内容基本一致通过观察,就是改一下morphologyEx函数的第三个参数而已。核都是选源码发现MORPH_RECT(矩形元素结构)。And通过观察发现:最基本的腐蚀和膨胀操作也可以使用morphologyEx函数实现,他们由morphologyEx源码中的switch的前两个case来实现。由于各种形态学操作只是morphologyEx函数的第三个参数的差别,各形态学操作示例:

#include 
using namespace cv;

int main() {
	//载入原图
	Mat srcImage = imread("1.jpg");

	//创建窗口
	namedWindow("原始图");
	namedWindow("效果图");
	//显示原图
	imshow("原始图", srcImage);

	//定义核
	Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
	//进行形态学操作
	//morphologyEx(srcImage, srcImage, MORPH_OPEN, element);//开运算
	//morphologyEx(srcImage, srcImage, MORPH_CLOSE, element);//闭运算
	//morphologyEx(srcImage, srcImage, MORPH_GRADIENT, element);//形态学梯度
	//morphologyEx(srcImage, srcImage, MORPH_TOPHAT, element);//顶帽
	morphologyEx(srcImage, srcImage, MORPH_BLACKHAT, element);//黑帽

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

	waitKey(0);
	return 0;
}

运行结果:

<1>开运算效果

OpenCV3之——形态学滤波(2):开运算、闭运算、形态学梯度、顶帽、_第7张图片

<2>闭运算效果

OpenCV3之——形态学滤波(2):开运算、闭运算、形态学梯度、顶帽、_第8张图片

<3>形态学梯度

OpenCV3之——形态学滤波(2):开运算、闭运算、形态学梯度、顶帽、_第9张图片

 <4>顶帽效果

OpenCV3之——形态学滤波(2):开运算、闭运算、形态学梯度、顶帽、_第10张图片

<5>黑帽效果

OpenCV3之——形态学滤波(2):开运算、闭运算、形态学梯度、顶帽、_第11张图片

二、综合实例:形态学滤波 

#include 
using namespace std;
using namespace cv;

//全局变量声明部分
Mat g_srcImage, g_dstImage;
int g_nElementShape = MORPH_RECT;//0,元素结构的形状

//变量接收的Trackbar位置参数
int g_nMaxIterationNum = 10;
int g_nGradientNum = 20;
int g_nOpenCloseNum = 0;
int g_nErodeDilateNum = 0;
int g_nTopBlackHatNum = 0;

//全局变量声明部分
static void on_OpenClose(int, void *);
static void on_ErodeDilate(int, void *);
static void on_TopBlackHat(int, void *);
static void on_Gradient(int, void *);

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

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

	//创建四个窗口
	namedWindow("【开运算/闭运算】");
	namedWindow("【腐蚀/膨胀】");
	namedWindow("【顶帽/黑帽】");
	namedWindow("【形态学梯度】");

	//参数值初始化
	g_nOpenCloseNum = 9;
	g_nErodeDilateNum = 9;
	g_nTopBlackHatNum = 2;
	g_nGradientNum = 20;

	//分别为四个窗口创建轨迹条
	createTrackbar("迭代值", "【开运算/闭运算】", &g_nOpenCloseNum, g_nMaxIterationNum * 2 + 1, on_OpenClose);
	createTrackbar("迭代值", "【腐蚀/膨胀】", &g_nErodeDilateNum, g_nMaxIterationNum * 2 + 1, on_ErodeDilate);
	createTrackbar("迭代值", "【顶帽/黑帽】", &g_nTopBlackHatNum, g_nMaxIterationNum * 2 + 1, on_TopBlackHat);
	createTrackbar("迭代值", "【形态学梯度】", &g_nGradientNum, g_nMaxIterationNum * 2 + 1, on_Gradient);

	//轮询获取按键信息
	while (1) {
		int c;

		//执行回调函数,重新渲染
		on_OpenClose(g_nOpenCloseNum, 0);
		on_ErodeDilate(g_nErodeDilateNum, 0);
		on_Gradient(g_nGradientNum, 0);
		on_TopBlackHat(g_nTopBlackHatNum, 0);

		//获取按键
		c = waitKey(0);

		//按下q键或者ESC,程序退出
		if ((char)c == 'q' || (char)c == 27)
			break;
		//按下键盘按键1,使用椭圆(Elliptic)结构元素MORPH_ELLIPSE
		if ((char)c == 49) {
			g_nElementShape = MORPH_ELLIPSE;
		}
		else if ((char)c == 50) {//按下键盘按键2
			g_nElementShape = MORPH_RECT;
		}else if((char)c==51){//按下键盘按键3
			g_nElementShape = MORPH_CROSS;
		}
		else if ((char)c == ' ') {
			g_nElementShape = (g_nElementShape + 1) % 3;
		}

	}
	return 0;
}

static void on_OpenClose(int, void *) {
	//偏移量的定义
	int offset = g_nOpenCloseNum - g_nMaxIterationNum;
	int Absolute_offset = offset > 0 ? offset : -offset;
	//自定义核
	Mat element = getStructuringElement(g_nElementShape, Size(Absolute_offset * 2 + 1, Absolute_offset * 2 + 1), Point(Absolute_offset, Absolute_offset));
	
	//进行操作
	if (offset < 0) {//开运算
		morphologyEx(g_srcImage, g_dstImage, MORPH_OPEN, element);
	}
	else {//闭运算
		morphologyEx(g_srcImage, g_dstImage, MORPH_CLOSE, element);
	}
	imshow("【开运算/闭运算】", g_dstImage);
}

static void on_ErodeDilate(int, void *) {
	//偏移量定义
	int offset = g_nErodeDilateNum - g_nMaxIterationNum;
	int Absolute_offset = offset > 0 ? offset : -offset;

	//自定义核
	Mat element = getStructuringElement(g_nElementShape, Size(Absolute_offset * 2 + 1, Absolute_offset*2+1), Point(Absolute_offset, Absolute_offset));

	//进行操作
	if (offset < 0)//腐蚀
		erode(g_srcImage, g_dstImage, element);
	else//膨胀
		dilate(g_srcImage, g_dstImage, element);

	imshow("【腐蚀/膨胀】", g_dstImage);
}

static void on_TopBlackHat(int, void *) {
	//偏移量定义
	int offset = g_nTopBlackHatNum - g_nMaxIterationNum;
	int Absolute_offset = offset > 0 ? offset : -offset;

	//自定义核
	Mat element = getStructuringElement(g_nElementShape, Size(Absolute_offset * 2 + 1, Absolute_offset*2+1), Point(Absolute_offset, Absolute_offset));

	if (offset < 0)//顶帽
		morphologyEx(g_srcImage, g_dstImage, MORPH_TOPHAT, element);
	else//黑帽
		morphologyEx(g_srcImage, g_dstImage, MORPH_BLACKHAT, element);

	imshow("【顶帽/黑帽】", g_dstImage);
}

static void on_Gradient(int, void *) {
	Mat element = getStructuringElement(g_nElementShape, Size(g_nGradientNum*2+1, g_nGradientNum*2+1), Point(g_nGradientNum, g_nGradientNum));
	morphologyEx(g_srcImage, g_dstImage, MORPH_GRADIENT, element);

	imshow("【形态学梯度】", g_dstImage);
}

运行结果截图:

OpenCV3之——形态学滤波(2):开运算、闭运算、形态学梯度、顶帽、_第12张图片

 OpenCV3之——形态学滤波(2):开运算、闭运算、形态学梯度、顶帽、_第13张图片

 

你可能感兴趣的:(OpenCV学习)