首页 > 代码库 > 学习 opencv---(10)形态学图像处理(2):开运算,闭运算,形态学梯度,顶帽,黒帽合辑
学习 opencv---(10)形态学图像处理(2):开运算,闭运算,形态学梯度,顶帽,黒帽合辑
上篇文章中,我们重点了解了腐蚀和膨胀这两种最基本的形态学操作,而运用这两个基本操作,我们可以实现更高级的形态学变换。
所以,本文的主角是OpenCV中的morphologyEx函数,它利用基本的膨胀和腐蚀技术,来执行更加高级的形态学变换,如开闭运算、形态学梯度、“顶帽”、“黑帽”等等。
第二件事,是浅墨想跟大家做一个关于OpenCV系列文章的书写内容和风格的思想汇报。
是这样的,浅墨发现最近几期写出来的文章有些偏离自己开始开这个专栏的最初的愿望——原理和概念部分占的比重有些大,有些弱化OpenCV实际的使用。
写这些博文的初心是教大家如何使用OpenCV来写代码,原理部分我想很多朋友应该多少都懂,就算某些同学对某些概念有些模糊,大家也完全可以带着关键词句去google或者百度。
浅墨的想法是,以后的专栏文章原理部分尽量从简,“深入”的源码剖析部分也是从简,重点突出“浅出”部分,让大家快速上手OpenCV函数的使用,这样浅墨的工作量也会小很多,更新也会更勤。
PS:浅墨其实每次在写图像处理原理部分的时候都特纠结,因为浅墨其实感兴趣的和大家一样,也是如何写代码,而不是那些多多少少让人提不起兴趣来的图像处理公式和概念。这往往就照成了博文更新的拖延症。
所以呢,在浅墨以后写的OpenCV文章中,原理和深入部分我们就点到为止,文章的拳头内容是“浅出”部分,重点教大家如何快速上手OpenCV API。我想这也是大家一直期待和想要看到的浅墨出品的文章的样子吧。:)
OK,大概就是这些。我们开始今天的正题。
一、理论与概念讲解——从现象到本质
首先呢,要知道形态学的高级形态,往往都是建立在腐蚀和膨胀这两个基本操作之上的。而关于腐蚀和膨胀,概念和细节以及相关代码可以看浅墨之前写的这篇文章:【OpenCV入门教程之十】 形态学图像处理(一):膨胀与腐蚀
对膨胀和腐蚀心中有数了,接下来的高级形态学操作,应该就不难理解。
另外,为了下面对比和演示以及理解的方便,浅墨自己制作了一张毛笔字图,这里先上原图:
1.1 开运算(Opening Operation)
开运算(opening operation),其实就是先腐蚀后膨胀过得过程,其数学表达式如下:
**** 括号的优先级大,所以先运算erode
开运算可以用来消除小物体,在纤细点出分离物体,平滑较大物体的边界同时并不明显改变其面积,效果图是这样的:
1.2 闭运算(Closing Operation)
先膨胀后腐蚀的过程称为闭运算(closing operation),数学表达式如下:
闭运算能够排除小型黑洞(黑色区域),如下所示:
1.3 形态学梯度(MorphologicalGradient)
形态学梯度(morphological gradient)为膨胀图与腐蚀图之差,表达式如下:
对二值图像进行这一操作可以将团块(blob)的边缘突出来。我们可以用形态学梯度来保留物体的边缘轮廓,如下所示:
1.4 顶帽(Top Hat)
顶帽运算(top hat) 又常常被译为“礼帽”运算,为原图像与上文刚介绍的“开运算”的结果图之差,表达式如下:
因为开运算带来的结果是放大了裂缝或者局部低亮度的区域,因此,从原图中减去开运算后的图,得到的效果图突出了比原型轮廓周围的区域更明亮的区域。且这一操作和选择的核的大小有关
顶帽运算往往用来分离比邻近点亮一些的斑块。当一幅图像具有大幅的背景的时候,而微小物品比较有规律的时候,可以用顶帽运算进行背景提取。
1.5 黑帽(Black Hat)
黑帽(Black Hat)运算为”闭运算“的结果图与原图像之差。数学表达式为:
黑帽运算后的效果图突出了比原图轮廓周围的区域更暗的区域,且这一操作和选择的核的大小相关。
所以,黑帽运算用来分离比邻近点暗一些的斑块。非常完美的轮廓效果图:
二、深入——OpenCV源码分析溯源
本文的主角是OpenCV中的morphologyEx函数,它利用基本的膨胀和腐蚀技术,来执行更加高级的形态学变换,
如开闭运算,形态学梯度,“顶帽”、“黑帽”等等。这一节我们来一起看一下morphologyEx函数的源代码。
1 void cv::morphologyEx( InputArray _src, OutputArray _dst, int op,
2 InputArray _kernel, Point anchor, int iterations,
3 int borderType, const Scalar& borderValue )
4 {
5 Mat kernel = _kernel.getMat();
6 if (kernel.empty())
7 {
8 kernel = getStructuringElement(MORPH_RECT, Size(3,3), Point(1,1));
9 }
10 #ifdef HAVE_OPENCL
11 Size ksize = kernel.size();
12 anchor = normalizeAnchor(anchor, ksize);
13
14 CV_OCL_RUN(_dst.isUMat() && _src.dims() <= 2 && _src.channels() <= 4 &&
15 anchor.x == ksize.width >> 1 && anchor.y == ksize.height >> 1 &&
16 borderType == cv::BORDER_CONSTANT && borderValue =http://www.mamicode.com/= morphologyDefaultBorderValue(),
17 ocl_morphologyEx(_src, _dst, op, kernel, anchor, iterations, borderType, borderValue))
18 #endif
19
20 //拷贝Mat 数据到临时变量
21 Mat src =http://www.mamicode.com/ _src.getMat(), temp;
22 _dst.create(src.size(), src.type());
23 Mat dst = _dst.getMat();
24
25 Mat k1, k2, e1, e2; //only for hit and miss op
26
27 //一个大switch,根据不同的标识符获取不同的操作
28 switch( op )
29 {
30 case MORPH_ERODE:
31 erode( src, dst, kernel, anchor, iterations, borderType, borderValue );
32 break;
33 case MORPH_DILATE:
34 dilate( src, dst, kernel, anchor, iterations, borderType, borderValue );
35 break;
36 case MORPH_OPEN:
37 erode( src, dst, kernel, anchor, iterations, borderType, borderValue );
38 dilate( dst, dst, kernel, anchor, iterations, borderType, borderValue );
39 break;
40 case CV_MOP_CLOSE:
41 dilate( src, dst, kernel, anchor, iterations, borderType, borderValue );
42 erode( dst, dst, kernel, anchor, iterations, borderType, borderValue );
43 break;
44 case CV_MOP_GRADIENT:
45 erode( src, temp, kernel, anchor, iterations, borderType, borderValue );
46 dilate( src, dst, kernel, anchor, iterations, borderType, borderValue );
47 dst -= temp;
48 break;
49 case CV_MOP_TOPHAT:
50 if( src.data != dst.data )
51 temp = dst;
52 erode( src, temp, kernel, anchor, iterations, borderType, borderValue );
53 dilate( temp, temp, kernel, anchor, iterations, borderType, borderValue );
54 dst = src - temp;
55 break;
56 case CV_MOP_BLACKHAT:
57 if( src.data != dst.data )
58 temp = dst;
59 dilate( src, temp, kernel, anchor, iterations, borderType, borderValue );
60 erode( temp, temp, kernel, anchor, iterations, borderType, borderValue );
61 dst = temp - src;
62 break;
63 case MORPH_HITMISS:
64 CV_Assert(src.type() == CV_8UC1);
65 k1 = (kernel == 1);
66 k2 = (kernel == -1);
67 if (countNonZero(k1) <= 0)
68 e1 = src;
69 else
70 erode(src, e1, k1, anchor, iterations, borderType, borderValue);
71 if (countNonZero(k2) <= 0)
72 e2 = src;
73 else
74 {
75 Mat src_complement;
76 bitwise_not(src, src_complement);
77 erode(src_complement, e2, k2, anchor, iterations, borderType, borderValue);
78 }
79 dst = e1 & e2;
80 break;
81 default:
82 CV_Error( CV_StsBadArg, "unknown morphological operation" );
83 }
84 }
看上面的源码可以发现,其实morphologyEx函数其实就是内部一个大switch而已。根据不同的标识符取不同的操作。
比如开运算MORPH_OPEN,按我们上文中讲解的数学表达式,就是先腐蚀后膨胀,即依次调用erode和dilate函数,为非常简明干净的代码。
三、浅出——API函数快速上手
3.1 morphologyEx函数详解
上面我们已经讲到,morphologyEx函数利用基本的膨胀和腐蚀技术,来执行更加高级形态学变换,如开闭运算,形态学梯度,“顶帽”、“黑帽”等等。这一节我们来了解它的参数意义和使用方法。
1 void morphologyEx(
2 InputArray src,
3 OutputArray dst,
4 int op,
5 InputArraykernel,
6 Pointanchor=Point(-1,-1),
7 intiterations=1,
8 intborderType=BORDER_CONSTANT,
9 constScalar& borderValue=http://www.mamicode.com/morphologyDefaultBorderValue() );
第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。图像位深应该为以下五种之一:CV_8U, CV_16U,CV_16S, CV_32F 或CV_64F。
- 第二个参数,OutputArray类型的dst,即目标图像,函数的输出参数,需要和源图片有一样的尺寸和类型。
- 第三个参数,int类型的op,表示形态学运算的类型,可以是如下之一的标识符:
MORPH_OPEN – 开运算(Opening operation
MORPH_CLOSE – 闭运算(Closing operation)
MORPH_GRADIENT -形态学梯度(Morphological gradient)
MORPH_TOPHAT - “顶帽”(“Top hat”)
MORPH_BLACKHAT - “黑帽”(“Black hat“)
- 第四个参数,InputArray类型的kernel,形态学运算的内核。若为NULL时,表示的是使用参考点位于中心3x3的核。我们一般使用函数 getStructuringElement配合这个参数的使用。getStructuringElement函数会返回指定形状和尺寸的结构元素(内核矩阵)。关于getStructuringElement我们上篇文章中讲过了,这里为了大家参阅方便,再写一遍:
其中,getStructuringElement函数的第一个参数表示内核的形状,我们可以选择如下三种形状之一:
矩形: MORPH_RECT
交叉形: MORPH_CROSS
椭圆形: MORPH_ELLIPSE
而getStructuringElement函数的第二和第三个参数分别是内核的尺寸以及锚点的位置。
我们一般在调用erode以及dilate函数之前,先定义一个Mat类型的变量来获得getStructuringElement函数的返回值。对于锚点的位置,有默认值Point(-1,-1),表示锚点位于中心。且需要注意,十字形的element形状唯一依赖于锚点的位置。而在其他情况下,锚点只是影响了形态学运算结果的偏移。
getStructuringElement函数相关的调用示例代码如下:
1 int g_nStructElementSize = 3; //结构元素(内核矩阵)的尺寸
2
3 //获取自定义核
4 Mat element =getStructuringElement(MORPH_RECT,
5 Size(2*g_nStructElementSize+1,2*g_nStructElementSize+1),
6 Point(g_nStructElementSize, g_nStructElementSize ));
调用这样之后,我们便可以在接下来调用erode、dilate或morphologyEx函数时,kernel参数填保存getStructuringElement返回值的Mat类型变量。对应于我们上面的示例,就是填element变量。
其中的这些操作都可以进行就地(in-place)操作。且对于多通道图像,每一个通道都是单独进行操作。
OK,讲解完毕,下面就是使用的范例。
高能预警!高能预警!高能预警!
一大波示例代码正在逼近。
为了方便大家需要的时候随时取用。下面我们依次列举出开运算,闭运算,形态学梯度,顶帽,黑帽,腐蚀,膨胀的效果实现简化版完整代码。
其实说白了,这些代码基本上内容一致,其实就是改一下morphologyEx里面的第三个标识符参数而已。核都是选的MORPH_RECT,矩形元素结构。
另外,通过看源码我们发现,最基本的腐蚀和膨胀操作也可以用morphologyEx函数来实现,他们由morphologyEx函数源码中switch的前两个case来实现(虽然在case体内就是简单地各自调用了一下erode和dilation函数,但还是有写出来的必要)。所以在这里,我们也用morphologyEx再重新来实现一遍他们。
按着顺序来列出吧,就直接列详细注释好的代码和运行结果了。
3.2 开运算示例程序
未完待续。。。。。。。。。。。。。。。。。
学习 opencv---(10)形态学图像处理(2):开运算,闭运算,形态学梯度,顶帽,黒帽合辑