OpenCV学习笔记基础篇(九):形态学滤波:膨胀,腐蚀,开运算,闭运算,形态学梯度,顶帽,黑帽、源码分析

前言:

笔者目前在校本科大二,有志于进行计算机视觉、计算机图形学方向的研究,准备系统性地、扎实的学习一遍OpenCV的内容,故记录学习笔记,同时,由于笔者同时学习数据结构、机器学习等知识,会尽量根据自己的理解,指出OpenCV的应用,并在加上自己理解的前提下进行叙述。
若有不当之处,希望各位批评、指正。


本篇学习内容:

1.形态学滤波
2.源码分析


1.形态学滤波

在上篇中引用了homepages.inf.ed.ac.uk/rbf/CVonline/LOCAL_COPIES/MANDUCHI1/Bilateral_Filtering.html中的一段话,
其中一句写到:In the broadest sense of the term “filtering”, the value of the filtered image at a given location is a function of the values of the input image in a small neighborhood of the same location.
这句话的意思是:在广义的“滤波”里,滤波图像在给定位置的值是输入图像在同一位置的小邻域内的值的函数。

我要说:这句话在本篇内容中仍然是适用的。

1.1 膨胀,腐蚀

膨胀和腐蚀是后面五种运算的基础。其实原理很简单。

膨胀:即取邻域内像素值最大值
腐蚀:即取邻域内像素值最小值

膨胀:dilate()

void cv::dilate	(	
InputArray 	src,
OutputArray dst,
InputArray 	kernel,
Point 	anchor = Point(-1,-1),
int 	iterations = 1,//迭代次数
int 	borderType = BORDER_CONSTANT,
const Scalar & 	borderValue = morphologyDefaultBorderValue() 
)	

腐蚀:erode()

void cv::erode	(	
InputArray 	src,
OutputArray dst,
InputArray 	kernel,
Point 	anchor = Point(-1,-1),
int 	iterations = 1,
int 	borderType = BORDER_CONSTANT,
const Scalar & 	borderValue = morphologyDefaultBorderValue() 
)	

在膨胀和腐蚀中,核的输入是一个InputArray类型的变量。这个变量通常可以通过getStructuringElement()函数得到:

Mat cv::getStructuringElement	(	
int 	shape,//形状,可选RECT,CROSS,ELLIPSE
Size 	ksize,
Point 	anchor = Point(-1,-1) 
)	

它返回的是一个由0,1组成的矩阵。这个矩阵会指导膨胀和腐蚀函数来进行操作。
此外,膨胀和腐蚀可以迭代,通过修改iterations即可。

一个例子:

Mat src = imread("E:/program/image/1.jpg");
Mat dst_erode, dst_dilate;
Mat element = getStructuringElement(MORPH_RECT, Size(5, 5));
erode(src, dst_erode, element);
dilate(src, dst_dilate,element);
imshow("原图", src);
imshow("erode", dst_erode);
imshow("dilate", dst_dilate);
waitKey();

在这里插入图片描述

1.2 开运算,闭运算,形态学梯度,顶帽,黑帽

实际上,膨胀、腐蚀、开运算、闭运算、形态学梯度、顶帽、黑帽都可以用一个函数搞定: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() 
)

源码路径:opencv\sources\modules\imgproc\src\morph.dispatch.cpp 第1157行
函数实现中,在进行了一些准备后,直接进一个大switch:

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" );
    }

op就是需要进行的变换种类。这个switch中也有erode和dilate的选项,所以在进行膨胀和腐蚀时也可以调这个函数来实现。

观察switch中内容,可知:

开运算:先腐蚀后膨胀:dst = dilate(erode(src))
作用:消除小物体,在纤细点处分离物体,且在平滑较大物体边界的同时不明显改变其面积
闭运算:先膨胀后腐蚀:dst = erode(dilate(src))
作用:排除小型黑洞(黑色区域)
形态学梯度:膨胀 - 腐蚀:dst = dilate(src) - erode(src)
作用:保留物体的边缘轮廓
顶帽:原图 - 开运算:dst = src - dilate(erode(src))
作用:分离比邻近点亮一些的斑块
黑帽:闭运算 - 原图:dst = erode(dilate(src)) - src
作用:分离比邻近点暗一些的斑块

一个例子:

Mat src = imread("E:/program/image/1.jpg");
Mat dst_erode, dst_dilate, dst_open, dst_close, dst_gradient, dst_tophat, dst_blackhat;
Mat element = getStructuringElement(MORPH_RECT, Size(5, 5));
erode(src, dst_erode, element);
dilate(src, dst_dilate,element);
morphologyEx(src, dst_open, MORPH_OPEN, element);
morphologyEx(src, dst_close, MORPH_CLOSE, element);
morphologyEx(src, dst_gradient, MORPH_GRADIENT, element);
morphologyEx(src, dst_tophat, MORPH_TOPHAT, element);
morphologyEx(src, dst_blackhat, MORPH_BLACKHAT , element);
imshow("原图", src);
imshow("open", dst_open);
imshow("close", dst_close);
imshow("gradient", dst_gradient);
imshow("tophat", dst_tophat);
imshow("blackhat", dst_blackhat);
waitKey();

在这里插入图片描述
在这里插入图片描述

2.源码分析

而dilate()和erode()又是怎么实现的呢?
源码路径:opencv\sources\modules\imgproc\src\morph.dispatch.cpp 第1004行

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 );
}

dilate()和erode()类似,都调用了morphOp函数。

查morphOp()函数,在同文件的第935行,这个函数在进行了一些准备工作之后,调用namespace hal下的morph()函数。

查morph()函数,在同文件的477行,在进行了一些准备工作之后,调用ocvMorph()函数。

查ocvMorph()函数,在同文件的434行。

static void ocvMorph(int op, int src_type, int dst_type,
                     uchar * src_data, size_t src_step,
                     uchar * dst_data, size_t dst_step,
                     int width, int height,
                     int roi_width, int roi_height, int roi_x, int roi_y,
                     int roi_width2, int roi_height2, int roi_x2, int roi_y2,
                     int kernel_type, uchar * kernel_data, size_t kernel_step,
                     int kernel_width, int kernel_height, int anchor_x, int anchor_y,
                     int borderType, const double borderValue[4], int iterations)
{
    Mat kernel(Size(kernel_width, kernel_height), kernel_type, kernel_data, kernel_step);
    Point anchor(anchor_x, anchor_y);
    Vec<double, 4> borderVal(borderValue);
    Ptr<FilterEngine> f = createMorphologyFilter(op, src_type, kernel, anchor, borderType, borderType, borderVal);
    Mat src(Size(width, height), src_type, src_data, src_step);
    Mat dst(Size(width, height), dst_type, dst_data, dst_step);
    {
        Point ofs(roi_x, roi_y);
        Size wsz(roi_width, roi_height);
        f->apply( src, dst, wsz, ofs );
    }
    {
        Point ofs(roi_x2, roi_y2);
        Size wsz(roi_width2, roi_height2);
        for( int i = 1; i < iterations; i++ )
            f->apply( dst, dst, wsz, ofs );
    }
}

可以看到,又用到了FilterEngine,这次函数是createMorphologyFilter()。这个才是实现OpenCV形态学滤波的真正函数。

查createMorphologyFilter()函数,在同文件90行,其内定义了多个Filter:

    Ptr<BaseRowFilter> rowFilter;
    Ptr<BaseColumnFilter> columnFilter;
    Ptr<BaseFilter> filter2D;

这些filter具体如何实现,本文就暂不介绍了。

参考文献:

  1. OpenCV官方文档:https://docs.opencv.org/4.x/
  2. 《OpenCV3编程入门》毛星云、冷雪飞等编著
  3. https://homepages.inf.ed.ac.uk/rbf/CVonline/LOCAL_COPIES/MANDUCHI1/Bilateral_Filtering.html
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值