最近很多东西都是在不断的去温习与补充,对于形态学这一块确实没咋了解,昨天花了点时间看看一些牛人的博客,还是从原理,目的,例子以及源码这一套流程来分析。
主要参考博客链接: http://blog.csdn.net/dcrmg/article/details/52265949
以及浅墨:http://blog.csdn.net/poem_qianmo/article/details/23710721
还有:http://blog.csdn.net/fred_yang2013/article/details/12029899
形态学算子,一般情况下形态学处理的都是二值图像。这样效果才明显。
常用的图像形态学操作包括膨胀、腐蚀、闭运算、开运算。
膨胀操作会扩大(粗化)图像中物体的轮廓,可以用来弥补(填充)物体间的孔洞,强化离散点,代价是导致物体的面积比原来的面积要大。
腐蚀操作会收缩(细化)图像中物体的轮廓,可以用来断开(分离)物体间的连接,消除离散点,代价是导致物体的面积比原来的面积要小。
闭运算是使用同一结构元对图像进行先膨胀后腐蚀的操作,可以用来弥合较窄的间断和细长的沟壑,消除物体间小的孔洞,填补轮廓线中的断裂。
开运算是使用同一结构元对图像进行先腐蚀后膨胀的操作,可以用来平滑物体的轮廓,断开物体间较窄的连接,消除物体边沿尖锐的突出部分。
下面Samples要用到的图片
腐蚀和膨胀是对白色部分(高亮部分)而言的,不是黑色部分。膨胀就是图像中的高亮部分进行膨胀,“领域扩张”,效果图拥有比原图更大的高亮区域。腐蚀就是原图中的高亮部分被腐蚀,“领域被蚕食”,效果图拥有比原图更小的高亮区域。
这些形态学操作都涉及到一个关键的因子——结构元。结构元基本的形态是矩形、十字形或椭圆形(圆形)。
结构元素就相当于我们在滤波中所涉及到的模板,也就是说它是一个给定像素的矩阵,这个矩阵可以是任意形状的,但是一般情况下都是正方形,圆形或者菱形的但是在结构元素中有一个中心点(也叫做anchor point)。和模板中心一样,处理后的结果赋值给和这个中心点对齐的像素点。处理的过程也是基本相同。
结构元素和卷积模板的区别在于,
膨胀是以集合运算为基础的,卷积是以算数运算为基础的。
膨胀:膨胀就是求局部最大值的操作。
1. 用结构元素,扫描图像的每一个像素
2. 用结构元素与其覆盖的二值图像做“与”操作
3. 如果都为0,结果图像的该像素为0。否则为1
也就是在结构元素覆盖范围下,只要有一个像素符和结构元素像素相同,那么中心点对应点就为1,否则为0
腐蚀:腐蚀就是求局部最小值的操作
1. 用结构元素,扫描图像的每一个像素
2. 用结构元素与其覆盖的二值图像做“与”操作
3. 如果都为1,结果图像的该像素为1。否则为0
也就是查找被处理图像中能不能找到和结构元素相同的矩阵。如果存在那么中心点所对应的点就为1,否则为0.
原文作者还用矩阵输出演示了一张图,十分形象,大家可以去看看
然后彩色图像的处理,以及局部最大值最小值的理解,大家直接去看浅墨的博客;(链接都在文章开头)
但是对于这个膨胀示意过程的图片,我只想说一句,A表示的高亮区域,不是整张图,搞得我还疑惑了一阵子,腐蚀也是类似。
其他的就没什么好说的
代码我对浅墨的稍微改动了一下:增加了对迭代次数的控制
先看看OpenCV的dilate(膨胀参数详解吧! (erode)腐蚀类似
C++: void dilate(
InputArray src,
OutputArray dst,
InputArray kernel,
Point anchor=Point(-1, 1),
int iteration=1,
int borderType=BORDER_CONSTANT,
const Scalar& borderValue=morphologyDefaultBorderValue()
);
/*
参数详解:
第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类对象即可。图像通道的数量可以是任意的,
但图像深度应为CV_8U, CV_16U, CV_16S, CV_32F或CV_64F其中之一.
第二个参数,OutputArray类型的dst,即目标图像,需要和源图片有一样的尺寸和类型.
第三个参数,InputArray类型的kernel,膨胀操作的核。若为NULL时,表示的是使用参考点位于中心3x3的核.
我们一般使用函数getStructuringElemnet配合这个参数的使用。getStructuringElement函数会返回指定形状
和尺寸的结构元素(内核矩阵)。
其中,getStructuringElemnet函数的第一个参数表示内核的形状,我们可以选择如下三种形状之一:
矩形: MORPH_RECT
交叉形: MORPH_CROSS
椭圆形: MORPH_ELLIPSE
而getStructuringElement函数的第二和第三个参数分别是内核的尺寸以及锚点的位置。
我们一般再调用erode以及dilate函数之前,先定义一个Mat类型的变量来获得
getStructuringElement函数的返回值。对于锚点的位置,有默认值Point(-1,-1),表示锚点位于中心.
且需要注意,十字形的element形状唯一依赖于锚点的位置.而在其他情况下,锚点只是影响了形态学运算的结果的
偏移。
getStructuringElement函数相关的调用示例代码如下:
*/
int g_nStructElementSize = 3; //结构元素(内核矩阵)的尺寸
//获取自定义核
Mat element = getStructuringElement(MORPH_RECT,
Size(2*g_nStructElementSize+1,2*g_nStructElementSize+1),
Point( g_nStructElementSize, g_nStructElementSize ));
/*
第四个参数, Point类型的anchor,锚的位置,其由默认值(-1, -1),表示锚位于中心。
第五个参数, int类型的iterations, 迭代使用erode()函数的次数,默认值为1。
第六个参数, int类型的borderType, 用于推断图像外部像素的某种边界模式。注意它有默认值
BORDER_DEFAULT.
第七个参数, const Scale &类型的borderValue, 当边界为常数时的边界值,
有默认值morphologyDefaultBorderValue(),一般我们不用去管他。需要用到它时,
可以看官方文档中的createMorphologyFilter()函数得到更详细的解释.
*/
示例的图片我是从牧野(链接我已给出)那里截图的,很好的比较了不同结构元的优缺点,这张图具有代表性,结论我也验证了,效果可能有点偏差
但是不影响。
结论先Copy如下:
从三个不同结构元的膨胀效果分析:
1. 从膨胀和弥合的有效性看,矩形结构元为优。
可以从第三排中间的闪电形状的孔洞填补上对比出来,其他两个结构元还有孔洞,矩形结构元已经填补完成。
也就是说使用相同尺寸的结构元,相比其他两个结构元,矩形结构元能够执行最少次数的膨胀操作达到膨胀和弥合的目的。
2. 从膨胀之后的外形轮廓上分析:
矩形结构元倾向于使轮廓的拐点处具有水平或垂直分割的特征,上下左右四个方向的边界都是直的。
十字结构元倾向于使轮廓的拐点处具有四分之一十字结构元形状的锯齿。
椭圆结构元倾向于使轮廓的拐点处具有更为平滑和圆润的弧线。
可以简单概括为结构元的形状是什么,就使得膨胀之后的轮廓的拐点处像什么。
3. 从应用场合看,三者各有所长:
矩形结构元膨胀适用于对外形是规则形状、边沿处无太多尖锐突起的物体,这样可以最大程度保持物体原本的轮廓形状。
十字结构元膨胀适用于对外形不规则、边沿处有较多尖锐突起的物体,这样可以最大程度保持物体原本的轮廓形状。
椭圆结构元膨胀适用于要求对物体的轮廓进行平滑圆润处理的物体。
从三个不同结构元的腐蚀效果分析:
1. 从腐蚀的有效性看,矩形结构元为优。
进过同样尺寸同样此时腐蚀后,矩形结构元只剩下部分拐点处的散点没有腐蚀掉,其他两个结构元操作后物体的整 个轮廓仍较为清晰。
2. 从腐蚀之后的外形轮廓上分析:
矩形结构元腐蚀后只剩下部分拐点处的像素点,主要的是非直角的拐点。
十字结构元腐蚀的能力最弱,对所有拐点处都比较敏感。
椭圆结构元也是对所有拐点比较敏感。
3. 从应用场合看,除了矩形结构元所需腐蚀次数较少之外,好像并无其他明显区别。
Samples:
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
//#include <Windows.h>
using namespace std;
using namespace cv;
Mat g_srcImage, g_dstImage; //原始图和效果图
int g_nTrackbarNumer = 0; //0表示腐蚀erode, 1表示膨胀dilate
int g_nStructElementSize = 3; //结构元素(内核矩阵)的尺寸
int iteratorNumber = 3; //迭代次数
void Process(); //膨胀和腐蚀的处理函数
void on_TrackbarNumChange( int, void *); //回调函数
void on_ElementSizeChange( int, void *); //回调函数
void on_IteratorNumChange( int, void *); //回调函数
char *SourceImage = "【原始图】";
char *DstImage = "【效果图】";
int main()
{
system("color 5E");
g_srcImage = imread("source.jpg");
if(g_srcImage.empty()) { cout<< "Where is your picture"<<endl;}
namedWindow(SourceImage);
imshow(SourceImage,g_srcImage);
Mat element = getStructuringElement(MORPH_RECT, Size(2*g_nStructElementSize+1,2*g_nStructElementSize+1));
erode( g_srcImage,g_dstImage,element);
imshow(DstImage,g_dstImage);
createTrackbar("腐蚀/膨胀",DstImage,&g_nTrackbarNumer,1,on_TrackbarNumChange);
createTrackbar("内核尺寸",DstImage,&g_nStructElementSize, 21, on_ElementSizeChange);
createTrackbar("迭代次数",DstImage,&iteratorNumber, 10, on_IteratorNumChange);
cout<<endl<<"\t嗯。运行成功,请调整滚动条观察图像效果~\n\n"
<<"\t按下“q”键时,程序退出~!\n"
<<"\n\n\t\t\t\tby浅墨";
while(char(waitKey(1))!= 'q'){}
return 0;
}
void Process()
{
//获取自定义核
Mat element = getStructuringElement(MORPH_RECT, Size(2*g_nStructElementSize+1,2*g_nStructElementSize+1));
//进行腐蚀或膨胀操作
if(g_nTrackbarNumer == 0){
erode( g_srcImage, g_dstImage, element,Point( g_nStructElementSize,g_nStructElementSize ),iteratorNumber);
}
else{
dilate(g_srcImage,g_dstImage, element,Point( g_nStructElementSize,g_nStructElementSize ),iteratorNumber);
}
//显示效果图
imshow(DstImage,g_dstImage);
}
void on_TrackbarNumChange(int, void *)
{
Process();
}
void on_ElementSizeChange(int, void *)
{
Process();
}
void on_IteratorNumChange( int, void *)
{
Process();
}
效果图如下:可以三个随意控制,然后进行多方位比较
void cv::erode( InputArray src, OutputArray dst, InputArray kernel,
Point anchor, int iterations,
int borderType, const Scalar& borderValue )
{
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 )
{
morphOp( MORPH_DILATE, src, dst, kernel, anchor, iterations, borderType, borderValue );
}
static void morphOp( int op, InputArray _src, OutputArray _dst,
InputArray _kernel,
Point anchor, int iterations,
int borderType, const Scalar& borderValue )
{
Mat src = _src.getMat(), kernel = _kernel.getMat();
Size ksize = kernel.data ? kernel.size() : Size(3,3);
anchor = normalizeAnchor(anchor, ksize);
CV_Assert( anchor.inside(Rect(0, 0, ksize.width, ksize.height)) );
_dst.create( src.size(), src.type() );
Mat dst = _dst.getMat();
if( iterations == 0 || kernel.rows*kernel.cols == 1 )
{
src.copyTo(dst);
return;
}
if( !kernel.data )
{
kernel = getStructuringElement(MORPH_RECT, Size(1+iterations*2,1+iterations*2));
anchor = Point(iterations, iterations);
iterations = 1;
}
else if( iterations > 1 && countNonZero(kernel) == kernel.rows*kernel.cols )
{
anchor = Point(anchor.x*iterations, anchor.y*iterations);
kernel = getStructuringElement(MORPH_RECT,
Size(ksize.width + (iterations-1)*(ksize.width-1),
ksize.height + (iterations-1)*(ksize.height-1)),
anchor);
iterations = 1;
}
int nStripes = 1;
#if defined HAVE_TBB && defined HAVE_TEGRA_OPTIMIZATION
if (src.data != dst.data && iterations == 1 && //NOTE: threads are not used for inplace processing
(borderType & BORDER_ISOLATED) == 0 && //TODO: check border types
src.rows >= 64 ) //NOTE: just heuristics
nStripes = 4;
#endif
parallel_for(BlockedRange(0, nStripes),
MorphologyRunner(src, dst, nStripes, iterations, op, kernel, anchor, borderType, borderType, borderValue));
//Ptr<FilterEngine> f = createMorphologyFilter(op, src.type(),
// kernel, anchor, borderType, borderType, borderValue );
//f->apply( src, dst );
//for( int i = 1; i < iterations; i++ )
// f->apply( dst, dst );
}
当然也可以拿人物彩色图像进行腐蚀,只是有点恐怖片的效果罢了!