转载请注明出处:https://blog.csdn.net/AndrExpert/article/details/80377629
在OpenCV4Android开发实录(4):图像去噪与线性滤波(均值、方框、高斯)和OpenCV4Android开发实录(5):图像边缘处理与非线性滤波(中值、双边)文章中,我们较为详细地介绍了几种常见线性和非线性图像滤波技术,本文将再接再厉继续学习另外一种图像滤波技术-
形态学滤波
,它是近年来出现的一类重要的非线性滤波器,被广泛应用于形状识别、边缘检测、纹理分析、图像恢复和增强等领域。
形态学滤波是以数学形态学为理论基础的一种图像滤波技术,它充分利用了数学形态学运算所具有的的几个特征和良好的代数性质,在保护图像边缘细节和特征提取等方面有较为广泛的应用。数学形态学是由一组形态学的代数运算子组成的,它的基本运算包括膨胀、腐蚀、开运算和闭运算,基于这四种运算还可以推导出和组合成各种数学形态学算法,文章的后面我们会介绍到。
数学形态学的基本思想是利用具有一定形态的结构元素
(Structure Element)去量度和提取图像中的对应形状,通过对图像数据进行简化,保留它们的基本形态特性并除去不相干的结构,从而达到对图像分析和识别的目的。所谓结构元素,可以简单地定义为像素的结构(形态)和一个原点(或称锚点),即上文提到的核,处理过程参照前文,只是形态学滤波核的形态可谓是多种多样。通常,不同的目的,使用结构元素的形态可能会不一样,常见的有方形、圆形和菱形等,且基于效率考虑结构元素的原点一般为位于中心位置。简单来说,形态学操作就是基于形状的一系列图像处理操作。
1. 基本理论
腐蚀(erode)和膨胀(dilate)是最基本的形态学操作,其他一系列形态学运算大部分是在腐蚀或膨胀的基础上推导而来。腐蚀是求局部最小值运算,它利用核(结构元素)与图像进行卷积,这个核会计算它所覆盖的区域的像素点的最小值,并把这个最小值赋值给参考点(与锚点有关),这样图像中的白色区域(高亮区)就会减小,黑色区域会变大。也就是说,腐蚀是黑色区域扩大的过程
,常应用于分割出独立的像素元素、连接相邻像素、消除图像中的小白点(噪声)等。数学表达式如下:
膨胀与腐蚀是一对相反操作,它是求局部最大值元素,即用局部像素中最大值赋值给参考点,通过膨胀操作处理的图像黑色区域会减小,白色区域(高亮区)增大。也就是说,膨胀是白色区域扩大的过程
,常应用于分割出独立的像素元素、连接相邻像素、消除图像中的小黑点灯。数学表达式如下:
2. 源码解析
/**erode函数原型:对图像进行腐蚀操作
* src:输入图像,要求图像深度为CV_8U, CV_16U, CV_16S, CV_32F or CV_64F之一
* dst:输出图像,要求图像尺寸、类型与输入图像一致;
* kernel:图像腐蚀结构元素,即核,通常使用函数getStructuringElement获得;
* anchor:锚点,默认值为Point(-1,-1),表示锚点在核中心;
* iterations:迭代执行腐蚀操作erode的次数,默认执行1次
* borderType:图像边缘处理方法,默认使用BORDER_CONSTANT模式
* borderValue:指定BORDER_CONSTANT处理图像边缘的颜色值,默认通过morphologyDefaultBorderValue方法获得;
*/
void erode( InputArray src, OutputArray dst, InputArray kernel,
Point anchor = Point(-1,-1), int iterations = 1,
int borderType = BORDER_CONSTANT,
const Scalar& borderValue = morphologyDefaultBorderValue() );
/**dilate函数原型:对图像进行膨胀操作
* 注:参数说明同上
*/
void dilate( InputArray src, OutputArray dst, InputArray kernel,
Point anchor = Point(-1,-1), int iterations = 1,
int borderType = BORDER_CONSTANT,
const Scalar& borderValue = morphologyDefaultBorderValue() );
erode函数位于...\opencv-3.3.0-win-sdk\sources\modules\imgproc\src目录下morph.cpp
源文件中,通过查看erode和dilate函数源码可知,这两个函数均调用了morphOp函数,且调用的方式基本一致,源码如下:
void cv::erode( InputArray src, OutputArray dst, InputArray kernel,
Point anchor, int iterations,
int borderType, const Scalar& borderValue ){
CV_INSTRUMENT_REGION()
// 选择MORPH_ERODE执行腐蚀操作
morphOp( MORPH_ERODE, src, dst, kernel, anchor, iterations, borderType, borderValue );
}
void cv::dilate( InputArray src, OutputArray dst, InputArray kernel,
Point anchor, int iterations,
int borderType, const Scalar& borderValue ){
CV_INSTRUMENT_REGION()
// 选择MORPH_DILATE执行膨胀操作
morphOp( MORPH_DILATE, src, dst, kernel, anchor, iterations, borderType, borderValue );
}
接下来,我们继续分析morphOp函数,该函数会根据输入参数作相应的边界处理,具体工作如下:首先
,morphOp函数会判断传入的kernal参数是否为空,如果为空,则默认将核大小设置为Size(3,3),并执行”getStructuringElement(MORPH_RECT, Size(1+iterations*2,1+iterations*2))”、”Point(iterations, iterations)”来获得核核锚点的位置;其次
,判断使用的图像边界处理方法是否为BORDER_ISOLATED,如果不是,需要调用locateROI( Size& wholeSize, Point& ofs)方法获取图像ROI区域的起始点;最后
,调用hal::morph函数进行进一步处理。考虑到调用的层级较深,具体的算法实现我们就不继续分析下去了,目前来说参考意义不是很大。morphOp函数源码如下:
static void morphOp( int op, InputArray _src, OutputArray _dst,
InputArray _kernel,
Point anchor, int iterations,
int borderType, const Scalar& borderValue )
{
CV_INSTRUMENT_REGION()
// 获取核
Mat kernel = _kernel.getMat();
// 判断核大小,如果为NULL,则默认核大小为Size(3,3)
Size ksize = !kernel.empty() ? kernel.size() : Size(3,3);
anchor = normalizeAnchor(anchor, ksize);
CV_OCL_RUN(_dst.isUMat() && _src.dims() <= 2 && _src.channels() <= 4 &&
borderType == cv::BORDER_CONSTANT && borderValue == morphologyDefaultBorderValue() &&
(op == MORPH_ERODE || op == MORPH_DILATE) &&
anchor.x == ksize.width >> 1 && anchor.y == ksize.height >> 1,
ocl_morphOp(_src, _dst, kernel, anchor, iterations, op, borderType, borderValue) )
// 如果iterations=0或者kernel.rows*kernel.cols == 1
// 直接将输入图像复制到目标图像,结束操作
if (iterations == 0 || kernel.rows*kernel.cols == 1)
{
_src.copyTo(_dst);
return;
}
// 如果核为NULL,计算核及锚点
if (kernel.empty())
{
kernel = getStructuringElement(MORPH_RECT, Size(1+iterations*2,1+iterations*2));
anchor = Point(iterations, iterations);
iterations = 1;
}
...
Mat src = _src.getMat();
_dst.create( src.size(), src.type() );
Mat dst = _dst.getMat();
Point s_ofs;
Size s_wsz(src.cols, src.rows);
Point d_ofs;
Size d_wsz(dst.cols, dst.rows);
// 判断图像边缘处理方法是否为BORDER_ISOLATED
bool isolated = (borderType&BORDER_ISOLATED)?true:false;
borderType = (borderType&~BORDER_ISOLATED);
if(!isolated)
{
// 获取ROI区域起始点位置,存储到s_ofs、d_ofs
src.locateROI(s_wsz, s_ofs);
dst.locateROI(d_wsz, d_ofs);
}
// 进一步调用
hal::morph(op, src.type(), dst.type(),
src.data, src.step,
dst.data, dst.step,
src.cols, src.rows,
s_wsz.width, s_wsz.height, s_ofs.x, s_ofs.y,
d_wsz.width, d_wsz.height, d_ofs.x, d_ofs.y,
kernel.type(), kernel.data, kernel.step, kernel.cols, kernel.rows, anchor.x, anchor.y,
borderType, borderValue.val, iterations,
(src.isSubmatrix() && !isolated));
}
3. 代码实战
在下面的实例中,我们使用getStructuringElement函数来构造图像膨胀和腐蚀处理的结构元素(核),它的函数原型如下:
Mat getStructuringElement(int shape, Size ksize, Point anchor = Point(-1,-1));
其中,shape表示结构元素(核)的形状,包括MORPH_RECT(矩形)、MORPH_CROSS(十字线)以及MORPH_ELLIPSE(椭圆)三种;ksize表示核的大小;anchor表示锚点位置,默认为核中心(-1,-1),需要注意的是,除了十字形的element形状唯一依赖于锚点的位置,其他情况锚点只是影响形态学结果的偏移。
代码如下:
// 形态学腐蚀操作
Mat ImageMorphology::erodeImage(Mat srcImage, int ksize) {
Mat dstImage;
Mat element = getStructuringElement(MORPH_RECT, Size(ksize, ksize));
erode(srcImage, dstImage, element);
return dstImage;
}
// 形态学膨胀操作
Mat ImageMorphology::dilateImage(Mat srcImage, int ksize) {
Mat dstImage;
Mat element = getStructuringElement(MORPH_RECT, Size(ksize, ksize));
dilate(srcImage, dstImage, element);
return dstImage;
}
1 基本理论
(1) 开/闭运算
开运算和闭运算是一对相反的操作,它们都是基于膨胀和腐蚀实现的,其中,开运算
是先对图像进行腐蚀后膨胀过程,由于先腐蚀,图像中的黑色区域增大,然后膨胀黑色区域会减小但是程度会比腐蚀操作要小,所以图像整体的黑色区域会增大,图像中原本细小的白色区域会在一开始的腐蚀中消失,在之后的膨胀过程中不起作用,因此会比单纯的腐蚀操作改变图像的程度稍微温和些,常用于消除小物体、连接细小连接部分;闭运算
是先对图像进行膨胀后腐蚀过程,与开运算相反,图像整体的白色区域会增大,图像中原本细小的黑色区域会在一开始的膨胀中消失,对之后的腐蚀造成不起作用,常用于排除小型黑洞。开运算、闭运算伪代码如下所示:
// 开运算(erode:腐蚀 dilate:膨胀)
open = dst = erode(src,tmp) = dilate(tmp,dst)
// 闭运算
close = dst = dilate(src,tmp) = erode(tmp,dst)
(2) 形态学梯度
形态学梯度(Morphological Gradient)是膨胀图与腐蚀图之差。在图像灰度变化比较小的部分,其膨胀与腐蚀操作不会明显改变图像,但是到了图像梯度大的地方,膨胀与腐蚀会明显改变图像,相减之后剩下的部分便是图像梯度大的部分,即图像轮廓。形态学梯度伪代码如下:
erode( src, temp);
dilate( src, dst);
gradient = dst - temp;
(3) 顶帽
原图像与开运算结果之差。开运算结果是图像黑色部分增大,与原图像相减,就能得到那些变化了的区域,由于开运算对亮的区域影响较大,即能突出图像中比较亮的斑块。伪代码如下:
tophat = src - open
(4) 黑帽
闭运算结果与原图像之差。与顶帽是相似运算,能突出图像中比较暗的斑块。伪代码如下:
blackhat = close- src
2 源码解析
(1) 函数原型
//
/**形态学滤波操作函数
* src:输入图像,要求图像深度为CV_8U, CV_16U, CV_16S, CV_32F or CV_64F之一
* dst:输出图像,要求图像尺寸、类型与输入图像一致;
* op:形态学算法选项,可为MorphTypes::MORPH_ERODE(腐蚀)、MorphTypes::MORPH_DILATE(膨胀)等8种。
* kernel:图像腐蚀结构元素,即核,通常使用函数getStructuringElement获得;
* anchor:锚点,默认值为Point(-1,-1),表示锚点在核中心;
* iterations:迭代执行腐蚀操作erode的次数,默认执行1次
* borderType:图像边缘处理方法,默认使用BORDER_CONSTANT模式
* borderValue:指定BORDER_CONSTANT处理图像边缘的颜色值,默认通过morphologyDefaultBorderValue方法获得;
*/
void 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() );
(2) OpenCV3.3源码解析
从上述基本理论可知,形态学的高级滤波是在腐蚀、膨胀操作的基础上结果,也就是我可以通过erode函数和dilate函数运算获得。实际上,OpenCV已经帮我们封装好,请看morch.pp源文件的cv::morphlogogyEx函数:
void cv::morphologyEx( InputArray _src, OutputArray _dst, int op,
InputArray _kernel, Point anchor, int iterations,
int borderType, const Scalar& borderValue )
{
CV_INSTRUMENT_REGION()
// 如果kernal为空,默认核为大小3x3的矩形
Mat kernel = _kernel.getMat();
if (kernel.empty())
{
kernel = getStructuringElement(MORPH_RECT, Size(3,3), Point(1,1));
}
// 如果HAVE_OPENCL被宏定义,执行ifdef...endif模块
#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
// 根据op值完成对应的形态学滤波算法
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 CV_MOP_CLOSE:
dilate( src, dst, kernel, anchor, iterations, borderType, borderValue );
erode( dst, dst, kernel, anchor, iterations, borderType, borderValue );
break;
// 形态学梯度:腐蚀-膨胀
case CV_MOP_GRADIENT:
erode( src, temp, kernel, anchor, iterations, borderType, borderValue );
dilate( src, dst, kernel, anchor, iterations, borderType, borderValue );
dst -= temp;
break;
// 顶帽:源图像-开运算,即temp为先膨胀后腐蚀的结果
case CV_MOP_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;
// 黑帽:源图像-闭运算,即temp为先腐蚀后膨胀的结果
case CV_MOP_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" );
}
}
morphlogogyEx函数代码很简单,主要做了三件事情:首先
,判断kerne是否为空,如果是则默认使用大小为3x3的核,核的形状为矩形;其次
,将目标图像的大小和类型规定与源图像一致;最后
,根据传入参数op的值,执行相应的形态学滤波算法。
3 代码实战
Mat ImageMorphology::morphologyOperator(int option,Mat srcImage,int ksize) {
Mat dstImage;
Mat element = getStructuringElement(MORPH_RECT, Size(ksize, ksize));
// 形态学滤波操作
// 其他参数使用默认值
morphologyEx(srcImage,dstImage,option,element);
return dstImage;
}