上篇文章中,我们重点了解了腐蚀和膨胀这两种最基本的形态学操作,而运用这两个基本操作,我们可以实现更高级的形态学变换。
所以,本文的主角是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 == morphologyDefaultBorderValue(),
17 ocl_morphologyEx(_src, _dst, op, kernel, anchor, iterations, borderType, borderValue))
18 #endif
19
20 //拷贝Mat 数据到临时变量
21 Mat src = _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_Ass