http://blog.csdn.net/u011021773/article/details/53065120
上一篇我讲过什么叫做卷积,而且提到了卷积与滤波其实不完全相同,因为部分滤波是非线性的,比方说中值滤波,就是取每个局部地区的中位数,也就是中间大小的数。如果展开了类比,如果取局部最大值或者局部最小值会是什么样的呢?大值滤波?小值滤波?
哈哈,其实取大值滤波的官方学名叫做膨胀,小值滤波叫做腐蚀。一句话概括,膨胀就是取局部最大值的滤波,腐蚀就是取局部最小值的滤波。
话不多讲,来上一段膨胀的代码:
Mat MyDilate(Mat src, int KernelSize=3,int step=1)
{
Mat dst(src.rows,src.cols,src.type());
src.copyTo(dst);
//半个核的大小,用以计算滤波中心
int KernelHalfSize = (KernelSize - 1) / 2;
if (0 == KernelHalfSize)
{
return dst;
}
int channels = src.channels();
//膨胀是求最大值的滤波
for (int c = 0; c < channels; c++)
{
for (int col = 0; col < src.cols; col+=step)
{
for (int row = 0; row < src.rows; row+=step)
{
int max = 0;
for (int kc = col-KernelHalfSize; kc < col+KernelHalfSize; kc ++)
{
for (int kr = row- KernelHalfSize; kr < row+KernelHalfSize; kr ++)
{
if (src.at<Vec3b>(mid(kr, 0, src.rows-1), mid(kc, 0, src.cols-1))[c] > max)
{
//如果是彩色图
if (3 == channels)
{
//这里求三个数的中位数是为了解决滤波边界的问题,
//如果超出界限的就选边界那一点的值,因为是求最大值所以某个点多算几次也没有关系
max = src.at<Vec3b>(mid(kr, 0, src.rows-1), mid(kc, 0, src.cols-1))[c];
}
//如果是灰度图
else if (1 == channels)
{
max = src.at<uchar>(mid(kr, 0, src.rows-1), mid(kc, 0, src.cols-1));
}
}
}
}
if (3 == channels)
{
dst.at<Vec3b>(row,col)[c] = max;
}
//如果是灰度图
else if (1 == channels)
{
dst.at<uchar>(row,col) = max;
}
}
}
}
return dst;
}
左边为原图,右边为膨胀后的图。这幅图我采用的核的大小为7*7,步长为5,可以看见原本该一起被刮掉的区域现在只有一个个白点被抹掉了,这是因为步长太大很多区域没有被处理,所以只有少部分黑色没有了,变成了白点。
腐蚀就是跟膨胀相反的操作啦,把求最大值改成求最小值就OK了,代码如下:
Mat MyErode(Mat src, int KernelSize = 3, int step = 1)
{
Mat dst(src.rows, src.cols, src.type());
src.copyTo(dst);
//半个核的大小,用以计算滤波中心
int KernelHalfSize = (KernelSize - 1) / 2;
if (0 == KernelHalfSize)
{
return dst;
}
int channels = src.channels();
//腐蚀是求最小值的滤波
for (int c = 0; c < channels; c++)
{
for (int col = 0; col < src.cols; col += step)
{
for (int row = 0; row < src.rows; row += step)
{
int min = 255;
for (int kc = col - KernelHalfSize; kc < col + KernelHalfSize; kc++)
{
for (int kr = row - KernelHalfSize; kr < row + KernelHalfSize; kr++)
{
if (src.at<Vec3b>(mid(kr, 0, src.rows - 1), mid(kc, 0, src.cols - 1))[c] < min)
{
//如果是彩色图
if (3 == channels)
{
//这里求三个数的中位数是为了解决滤波边界的问题,
//如果超出界限的就选边界那一点的值,因为是求最大值所以某个点多算几次也没有关系
min = src.at<Vec3b>(mid(kr, 0, src.rows - 1), mid(kc, 0, src.cols - 1))[c];
}
//如果是灰度图
else if (1 == channels)
{
min = src.at<uchar>(mid(kr, 0, src.rows - 1), mid(kc, 0, src.cols - 1));
}
}
}
}
if (3 == channels)
{
dst.at<Vec3b>(row, col)[c] = min;
}
//如果是灰度图
else if (1 == channels)
{
dst.at<uchar>(row, col) = min;
}
}
}
}
return dst;
}
效果如图:
这里我选用的依然是7 * 7步长为1的模板,可以看到黑色的字明显变粗,中间的小点也都变大,有一块黑色区域的中间的白点几乎消失了,这就是膨胀的效果,消除亮色区域,增大暗色区域。接着我们还是试一试调整一下步长,我把步长调整为2,看看效果图。
可以看见黑色大军还是向外扩展了,但是由于有步长限制了,不能每个点都占领到,所以出现了一个个的小黑点,只能得到点的胜利,不能得到面的胜利。同样,核的大小决定了膨胀的力度,如果核特别大黑色的字将特别粗,可能粗成一个坨。 有了腐蚀和膨胀的基础了,开闭操作就很好理解了,所谓的开操作就是先腐蚀后膨胀,闭操作就是先膨胀后腐蚀,效果如图:
以上两张图分别是开操作和闭操作,核的大小为7*7,步长为1。
开闭操作的意义在哪里呢?
通过观察我们发现开操作的效果和腐蚀很像,都是消除了白色的杂质,这是因为开操作是先腐蚀,后膨胀。通过一次腐蚀就已经将白色杂质消除了,但是此时字也变得很粗了,想要让他还保持原来的面积,就需要再把它膨胀回来,而一些白点会被消除。比方说左侧黑色圆圈中间的白点,还有去字中间的白色缝隙。闭操作是先膨胀后腐蚀,原本的黑字先变粗后变细了,因为黑点已经被消除了,所以再膨胀的时候原来的黑点不会再出现了,但是大面积的黑线依然存在,并且经历了一次变细又变粗的过程,这样画面上就只剩下黑色的字了,而旁边墨水洒落留下的黑点全部都消失了。
为什么黑色圆圈和去字的中间都不是黑色,也不是白色,而是灰色呢?这点开始我也非常奇怪,赶紧检查了程序,发现程序没有问题,0和1的问题不存在有中间值。后来观察图片发现原来图片是经过了有损压缩的,所有的颜色边缘都是渐变的,所以出现了灰色的边缘,而经过开操作的放大就显得非常明显了。
最后再总结一下:
腐蚀就是求局部最小值的滤波操作,目的是去除亮色杂质。
膨胀就是求局部最大值的滤波操作,目的是去除暗色杂质。
开操作是先腐蚀后膨胀的过程,目的是去除亮色杂质并保持重要部分面积不变。
闭操作是先膨胀后腐蚀的过程,目的是去除暗色杂质并保持重要部分面积不变。
最后贴上我的全部代码,以供参考。
#include "iostream"
#include "imgproc.hpp"
#include "highgui.h"
#include "opencv.hpp"
using namespace std;
using namespace cv;
//求三个数的中位数
int mid(int a, int b, int c)
{
int max = a;
int min = b;
if ((a <=b&&b<=c)|| (c <= b&&b <= a))
{
return b;
}
else if ((b <= a&&a <= c) || (c <= a&&a <= b))
{
return a;
}
else
{
return c;
}
}
Mat MyDilate(Mat src, int KernelSize=3,int step=1)
{
Mat dst(src.rows,src.cols,src.type());
src.copyTo(dst);
//半个核的大小,用以计算滤波中心
int KernelHalfSize = (KernelSize - 1) / 2;
if (0 == KernelHalfSize)
{
return dst;
}
int channels = src.channels();
//膨胀是求最大值的滤波
for (int c = 0; c < channels; c++)
{
for (int col = 0; col < src.cols; col+=step)
{
for (int row = 0; row < src.rows; row+=step)
{
int max = 0;
for (int kc = col-KernelHalfSize; kc < col+KernelHalfSize; kc ++)
{
for (int kr = row- KernelHalfSize; kr < row+KernelHalfSize; kr ++)
{
if (src.at<Vec3b>(mid(kr, 0, src.rows-1), mid(kc, 0, src.cols-1))[c] > max)
{
//如果是彩色图
if (3 == channels)
{
//这里求三个数的中位数是为了解决滤波边界的问题,
//如果超出界限的就选边界那一点的值,因为是求最大值所以某个点多算几次也没有关系
max = src.at<Vec3b>(mid(kr, 0, src.rows-1), mid(kc, 0, src.cols-1))[c];
}
//如果是灰度图
else if (1 == channels)
{
max = src.at<uchar>(mid(kr, 0, src.rows-1), mid(kc, 0, src.cols-1));
}
}
}
}
if (3 == channels)
{
dst.at<Vec3b>(row,col)[c] = max;
}
//如果是灰度图
else if (1 == channels)
{
dst.at<uchar>(row,col) = max;
}
}
}
}
return dst;
}
Mat MyErode(Mat src, int KernelSize = 3, int step = 1)
{
Mat dst(src.rows, src.cols, src.type());
src.copyTo(dst);
//半个核的大小,用以计算滤波中心
int KernelHalfSize = (KernelSize - 1) / 2;
if (0 == KernelHalfSize)
{
return dst;
}
int channels = src.channels();
//腐蚀是求最小值的滤波
for (int c = 0; c < channels; c++)
{
for (int col = 0; col < src.cols; col += step)
{
for (int row = 0; row < src.rows; row += step)
{
int min = 255;
for (int kc = col - KernelHalfSize; kc < col + KernelHalfSize; kc++)
{
for (int kr = row - KernelHalfSize; kr < row + KernelHalfSize; kr++)
{
if (src.at<Vec3b>(mid(kr, 0, src.rows - 1), mid(kc, 0, src.cols - 1))[c] < min)
{
//如果是彩色图
if (3 == channels)
{
//这里求三个数的中位数是为了解决滤波边界的问题,
//如果超出界限的就选边界那一点的值,因为是求最大值所以某个点多算几次也没有关系
min = src.at<Vec3b>(mid(kr, 0, src.rows - 1), mid(kc, 0, src.cols - 1))[c];
}
//如果是灰度图
else if (1 == channels)
{
min = src.at<uchar>(mid(kr, 0, src.rows - 1), mid(kc, 0, src.cols - 1));
}
}
}
}
if (3 == channels)
{
dst.at<Vec3b>(row, col)[c] = min;
}
//如果是灰度图
else if (1 == channels)
{
dst.at<uchar>(row, col) = min;
}
}
}
}
return dst;
}
//开操作先腐蚀后膨胀
Mat MyOpen(Mat img, int KernelSize = 3, int step = 1)
{
Mat ErodeImg = MyErode(img, KernelSize, step);
Mat DilateImg = MyDilate(ErodeImg, KernelSize, step);
return DilateImg;
}
//闭操作先膨胀后腐蚀
Mat MyClose(Mat img, int KernelSize = 3, int step = 1)
{
Mat DilateImg = MyDilate(img, KernelSize, step);
Mat ErodeImg = MyErode(DilateImg, KernelSize, step);
return ErodeImg;
}
void main()
{
Mat img=imread("..\\image\\original.png");
Mat ErodeImg = MyErode(img,7);
Mat DilateImg = MyDilate(img, 7);
Mat OpenImg = MyOpen(img, 7);
Mat CloseImg = MyClose(img, 7);
imshow("original", img);
imshow("ErodeImg", ErodeImg);
imshow("DilateImg", DilateImg);
imshow("OpenImg", OpenImg);
imshow("CloseImg", CloseImg);
waitKey(0);
imwrite("..\\image\\ErodeImg7.png", ErodeImg);
imwrite("..\\image\\DilateImg7.png", DilateImg);
imwrite("..\\image\\OpenImg.png", OpenImg);
imwrite("..\\image\\CloseImg.png", CloseImg);
}