概述:形态学的高级形态,往往都是建立在腐蚀和膨胀这两个基本操作之上的,所以本篇主角是OpenCV的morphologyEx函数,它利用基本的膨胀和腐蚀技术,来执行更加高级的形态学变换,如开运算、闭运算、形态学梯度、“顶帽”、“黑帽” 等。
为了下面对比和演示以及理解的方便,浅墨自己制作了一张毛笔字图,这里先上原图:
开运算(opening operation),其实就是先腐蚀后膨胀过得过程,其数学表达式如下:
开运算可以用来消除小物体,在纤细点出分离物体,平滑较大物体的边界同时并不明显改变其面积,效果图是这样的:
先膨胀后腐蚀的过程称为闭运算(closing operation),数学表达式如下:
闭运算能够排除小型黑洞(黑色区域),如下所示:
形态学梯度(morphological gradient)为膨胀图与腐蚀图之差,表达式如下:
对二值图像进行这一操作可以将团块(blob)的边缘突出来。我们可以用形态学梯度来保留物体的边缘轮廓,如下所示:
顶帽运算(top hat) 又常常被译为“礼帽”运算,为原图像与上文刚介绍的“开运算”的结果图之差,表达式如下:
因为开运算带来的结果是放大了裂缝或者局部低亮度的区域,因此,从原图中减去开运算后的图,得到的效果图突出了比原型轮廓周围的区域更明亮的区域。且这一操作和选择的核的大小有关
顶帽运算往往用来分离比邻近点亮一些的斑块。当一幅图像具有大幅的背景的时候,而微小物品比较有规律的时候,可以用顶帽运算进行背景提取。
黑帽(Black Hat)运算为”闭运算“的结果图与原图像之差。数学表达式为:
黑帽运算后的效果图突出了比原图轮廓周围的区域更暗的区域,且这一操作和选择的核的大小相关。
所以,黑帽运算用来分离比邻近点暗一些的斑块。非常完美的轮廓效果图:(Size(50,50)时)
源码溯源:(我正在使用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函数。
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())
参数详解:
而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变量。
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>开运算效果
<2>闭运算效果
<3>形态学梯度
<4>顶帽效果
<5>黑帽效果
#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);
}
运行结果截图: