图像形态学处理操作:膨胀和腐蚀
膨胀和腐蚀是图像形态学处理的两个最基础的操作,其他的形态学处理,比如开操作和闭操作都是基于膨胀和腐蚀和实现的。在前面的图像滤波部分中有一个卷积核的概念,在形态学处理中也有一个类似卷积核的东西,但是这里不叫卷积核,而是叫做结构元素,结构元素可以自定义成任何几何形状,而不局限于诸如 3x3,5x5 的矩形。
膨胀可以看成是最大值滤波,也就是用结构元素覆盖原图像范围内的最大值替换锚点位置的像素值。腐蚀可以看成是最小值滤波,也就是用结构元素覆盖原图像范围内的最小值替换锚点位置的像素值。为了保证输入图像和输出图像大小一致,OpenCV
提供的膨胀和腐蚀函数会对输入图像进行边界填充,类似于CNN中的padding
。
1.获取结构元素函数
OpenCV
提供的获取结构元素函数原型如下:
Mat getStructuringElement(int shape, Size ksize,
Point anchor = Point(-1,-1));
参数解释:
shape
:结构元素的类型,可选矩形MORPH_RECT
、十字交叉形MORPH_CROSS
以及椭圆MORPH_ELLIPSE
,其中矩形可以特化成正方形,垂直和水平直线,椭圆可以特化成圆。ksize
:结构元素的大小。anchor
:结构元素锚点位置。
不妨用代码输出一下获取到的结构元素内部都是什么:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main(int argc, const char **argv)
{
Mat rect5x5 = getStructuringElement(MORPH_RECT,Size(5,5));
cout<<"----------"<<endl;
cout<<"rect5x5:"<<endl;
for(int i=0;i<rect5x5.rows;++i)
{
for(int j=0;j<rect5x5.rows;++j)
cout<<int(rect5x5.at<uchar>(i,j))<<" ";
cout<<endl;
}
Mat cross5x5 = getStructuringElement(MORPH_CROSS,Size(5,5));
cout<<"----------"<<endl;
cout<<"cross5x5:"<<endl;
for(int i=0;i<cross5x5.rows;++i)
{
for(int j=0;j<cross5x5.rows;++j)
cout<<int(cross5x5.at<uchar>(i,j))<<" ";
cout<<endl;
}
Mat ellipse5x5 = getStructuringElement(MORPH_ELLIPSE,Size(5,5));
cout<<"----------"<<endl;
cout<<"ellipse5x5:"<<endl;
for(int i=0;i<ellipse5x5.rows;++i)
{
for(int j=0;j<ellipse5x5.rows;++j)
cout<<int(ellipse5x5.at<uchar>(i,j))<<" ";
cout<<endl;
}
cout<<"----------"<<endl;
return 0;
}
输出:
----------
rect5x5:
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
----------
cross5x5:
0 0 1 0 0
0 0 1 0 0
1 1 1 1 1
0 0 1 0 0
0 0 1 0 0
----------
ellipse5x5:
0 0 1 0 0
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
0 0 1 0 0
----------
尽管部分结构元素,例如ellipse5x5
中有值为0的元素,实际上用该结构元素在输入二值图像上进行滑动时只关注原图像上与值为1的元素对应的区域内的最大最小值。
2.图像形态学中的膨胀
上面说过,膨胀原理可以看成最大值滤波,也就是用结构元素覆盖原图像范围内的最大值替换锚点位置的像素值。对于膨胀来说,边界补充的时候补充的是较小值,在下面的例子中也就是0:
在上面的图片中元素值1对应二值图像中的前景高像素值,即255,0对应背景像素值,也就是0。
现在用一个 3x3 的矩形结构元素,其锚点位置在中心,来依次在填充后的图像上遍历,以第一行的第一个元素为例,得到的结果如下图:
依次完成上述过程,最终结果如下:
下面通过代码来实现上面的膨胀操作,看看效果是不是一样。
OpenCV
提供的膨胀函数原型如下:
void dilate( InputArray src, OutputArray dst, InputArray kernel,
Point anchor = Point(-1,-1), int iterations = 1,
int borderType = BORDER_CONSTANT,
const Scalar& borderValue = morphologyDefaultBorderValue() );
其参数解释如下:
src
:输入图像,不局限于二值图像,可以是输入尺寸的。dst
:输出膨胀操作结果的图像,大小和通道数跟输入图像一致。anchor
:结构元素锚点位置,默认为中心点。iterations
:膨胀操作次数,比如设为2次,就会在第一次膨胀的输出结果上在进行一次膨胀。borderType
:边界填充方式,默认为BORDER_CONSTANT
,表示用一个固定的常量值填充。borderValue
:上一个参数为BORDER_CONSTANT
时设定的常量值,一般这两个参数就用默认值就可以了。
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main(int argc, const char **argv)
{
//a按照上面的图像自定义出来相同的数据
Mat srcImage = (Mat_<uchar>(6, 6) <<
1,1,1,0,0,0,
1,1,1,1,0,0,
1,1,1,1,1,0,
0,0,1,1,1,0,
0,0,1,1,1,0,
0,0,0,0,0,0);
cout<<endl;
//打印图像信息
for(int i=0;i<srcImage.rows;++i)
{
for(int j=0;j<srcImage.rows;++j)
cout<<int(srcImage.at<uchar>(i,j))<<" ";
cout<<endl;
}
cout<<endl;
//填充边界
Mat borderImage;
//打印出填充边界后的图像
copyMakeBorder(srcImage,borderImage,1,1,1,1,BORDER_CONSTANT,Scalar(0));
cout<<endl;
for(int i=0;i<borderImage.rows;++i)
{
for(int j=0;j<borderImage.rows;++j)
cout<<int(borderImage.at<uchar>(i,j))<<" ";
cout<<endl;
}
cout<<endl;
//获取结构元素
Mat kernel = getStructuringElement(MORPH_RECT,Size(3,3));
//膨胀操作
Mat res;
dilate(srcImage,res,kernel,Point(-1,-1));
//打印膨胀后的图片信息
for(int i=0;i<res.rows;++i)
{
for(int j=0;j<res.rows;++j)
cout<<int(res.at<uchar>(i,j))<<" ";
cout<<endl;
}
cout<<endl;
return 0;
}
输出的原图和填充后的以及膨胀之后的结果如下:
1 1 1 0 0 0
1 1 1 1 0 0
1 1 1 1 1 0
0 0 1 1 1 0
0 0 1 1 1 0
0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 1 1 1 0 0 0 0
0 1 1 1 1 0 0 0
0 1 1 1 1 1 0 0
0 0 0 1 1 1 0 0
0 0 0 1 1 1 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
1 1 1 1 1 0
1 1 1 1 1 1
1 1 1 1 1 1
1 1 1 1 1 1
0 1 1 1 1 1
0 1 1 1 1 1
从上面的结果可以看出,膨胀操作会放大图像中的前景,也就是高像素值区域,可以用膨胀操作来填充图像中的一些背景噪点。
3.图像形态学中的腐蚀
腐蚀操作的过程和膨胀类似,其原理可以看成最小值滤波,也就是用结构元素覆盖原图像范围内的最小值替换锚点位置的像素值。对于腐蚀来说,边界补充的时候补充的是较大值,在下面的例子中也就是1:
最终操作结果如下:
OpenCV
提供的腐蚀函数原型如下:
void erode( InputArray src, OutputArray dst, InputArray kernel,
Point anchor = Point(-1,-1), int iterations = 1,
int borderType = BORDER_CONSTANT,
const Scalar& borderValue = morphologyDefaultBorderValue() );
其参数解释如下:
src
:输入图像,不局限于二值图像,可以是输入尺寸的。dst
:输出腐蚀操作结果的图像,大小和通道数跟输入图像一致。anchor
:结构元素锚点位置,默认为中心点。iterations
:腐蚀操作次数,比如设为2次,就会在第一次腐蚀的输出结果上在进行一次腐蚀。borderType
:边界填充方式,默认为BORDER_CONSTANT
,表示用一个固定的常量值填充。borderValue
:上一个参数为BORDER_CONSTANT
时设定的常量值,一般这两个参数就用默认值就可以了。
同样用代码测试,结果和上面的图一样:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main(int argc, const char **argv)
{
Mat srcImage = (Mat_<uchar>(6, 6) <<
1,1,1,0,0,0,
1,1,1,1,0,0,
1,1,1,1,1,0,
0,0,1,1,1,0,
0,0,1,1,1,0,
0,0,0,0,0,0);
cout<<endl;
for(int i=0;i<srcImage.rows;++i)
{
for(int j=0;j<srcImage.rows;++j)
cout<<int(srcImage.at<uchar>(i,j))<<" ";
cout<<endl;
}
cout<<endl;
Mat borderImage;
copyMakeBorder(srcImage,borderImage,1,1,1,1,BORDER_CONSTANT,Scalar(1));
cout<<endl;
for(int i=0;i<borderImage.rows;++i)
{
for(int j=0;j<borderImage.rows;++j)
cout<<double(borderImage.at<uchar>(i,j))<<" ";
cout<<endl;
}
cout<<endl;
Mat kernel = getStructuringElement(MORPH_RECT,Size(3,3));
Mat res;
erode(srcImage,res,kernel,Point(-1,-1));
for(int i=0;i<res.rows;++i)
{
for(int j=0;j<res.rows;++j)
cout<<int(res.at<uchar>(i,j))<<" ";
cout<<endl;
}
cout<<endl;
return 0;
}
输出:
1 1 1 0 0 0
1 1 1 1 0 0
1 1 1 1 1 0
0 0 1 1 1 0
0 0 1 1 1 0
0 0 0 0 0 0
1 1 1 1 1 1 1 1
1 1 1 1 0 0 0 1
1 1 1 1 1 0 0 1
1 1 1 1 1 1 0 1
1 0 0 1 1 1 0 1
1 0 0 1 1 1 0 1
1 0 0 0 0 0 0 1
1 1 1 1 1 1 1 1
1 1 0 0 0 0
1 1 0 0 0 0
0 0 0 0 0 0
0 0 0 1 0 0
0 0 0 0 0 0
0 0 0 0 0 0
可以看成,腐蚀操作会缩小图像中的前景,可以用腐蚀操作来将图像中的粘连的对象分离开。