这次主要介绍两方面的内容,一部分是形态学操作,另一部分是工具条。
先说形态学操作。这里只介绍4种简单的:腐蚀、膨胀、开、闭。最基本的形态学操作是腐蚀和膨胀。其他的操作可以通过腐蚀和膨胀推导出来。
用集合论的观点介绍他们非常麻烦。这里换一种思路:我们先做一定的假设:对于一幅图像:前景(我们感兴趣的部分)是白色的;背景(不感兴趣的部分)是黑色的。然后就可以望文生义一下了:腐蚀操作会使得前景变小,而膨胀会使得前景变大。这主要是当结构元(用来对图像处理的基本模版)作用于图像的边沿时,两种操作的定义引起的。腐蚀操作时只有当整个结构元都在图像边沿内时,锚点(结构元与图像中每个像素对齐的点,通常取作结构元的几何中心)对准的像素才会被保留,判为前景;否则这个点判为背景。膨胀操作则是只要结构元与图像有交集时,锚点对准的像素就会被保留,判为前景。腐蚀可以用来消除一些小的误检测的前景;而膨胀则可以填充一些小洞。
注意到,用3*3的模板腐蚀3次与用7*7的模板腐蚀一次效果是相同的。膨胀的结果可以类推。
有了这两个运算的基本定义,我们就可以定义开运算和闭运算了:开运算就是膨胀以后再腐蚀,而闭运算是腐蚀以后再膨胀。闭运算使得小洞被填上,临近的目标连接到了一起(任何结构元容纳不下的小洞或者缝隙都会被填充)。而开运算则会除去一些小的斑点。
对一幅图像多次使用开运算而闭运算是没有意义的。这与腐蚀和膨胀不同。
在OpenCV中,使用erode和dilate实现腐蚀与膨胀。要实现开运算或者闭运算,当然可以通过腐蚀完以后膨胀之类的方法完成,但是OpenCV通过了一个函数morphologyEx,通过改变函数中的参数来实现不同的形态学运算。下面的代码对这4种运算进行了简单的说明:
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace cv;
int main( void )
{
Mat image = imread("D:/picture/images/binary.bmp");
if(!image.data)
return -1;
imshow("源图像",image);
//腐蚀操作
Mat eroded;
//默认情况下,结构元为3*3
erode(image,eroded,Mat());
erode(eroded,eroded,Mat());
erode(eroded,eroded,Mat());
imshow("腐蚀结果",eroded);
//使用自定义的结构元
Mat element(7,7,CV_8U,Scalar(1));
erode(image,eroded,element);
imshow("7*7结构元腐蚀结果",eroded);
//使用默认的结构元重复操作也能得到相同的结果
erode(image,eroded,Mat(),Point(-1,-1),3);
imshow("使用默认结构元重复3次",eroded);
//膨胀操作
Mat dilated;
dilate(image,dilated,Mat());
imshow("膨胀结果",dilated);
//使用闭运算
Mat element5(5,5,CV_8U,Scalar(1));
Mat closed;
morphologyEx(image,closed,cv::MORPH_CLOSE,element5);
imshow("5*5结构元闭运算",closed);
//通过先膨胀再腐蚀的方法得到闭运算
Mat result;
dilate(image,result,element5);
erode(result,result,element5);
imshow("先膨胀,再腐蚀等于闭运算",result);
//使用开运算
Mat opened;
morphologyEx(image,opened,cv::MORPH_OPEN,element5);
imshow("5*5结构元开运算",opened);
//先闭运算再开运算
morphologyEx(image,result,cv::MORPH_CLOSE,element5);
morphologyEx(result,result,cv::MORPH_OPEN,element5);
imshow("先闭运算,在开运算",result);
waitKey(0);
return 0;
}
下面我们着重看如何使用工具条。
在OpenCV中,使用createTrackbar函数来创建工具条。第一个参数是工具条的名字,第二个参数是工具条要放到哪个窗口上(说来惭愧,我以前一直觉得namedWindow这句话很多余,直接在imshow的时候填上窗口名就行了,干嘛非要namedWindow呢?现在才发现了它的作用),第三个参数是工具条改变的是哪个值(顺带设定了初始位置也是这个值),第四个参数是改变的值的最大值。第五个参数是这个值是作用于哪个函数的。第六个值是用户传给回调函数的数据。不过因为我这里使用的是全局变量,所以没有使用最后一个参数。
下面看看程序:
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
using namespace cv;
using namespace std;
//定义全局变量:
Mat image;
Mat eroded;
Mat dilated;
Mat opened;
Mat closed;
//测试的形态学运算:腐蚀、膨胀、开、闭
//为每个形态学运算创建一个控件
//控件需要的参数:
//每种形态学操作需要单独确定的参数:
//结构元的种类:
int erosion_element = 0;
int dilation_element = 0;
int open_element = 0;
int close_element = 0;
//结构元的大小
int erosion_size = 0;
int dilation_size = 0;
int open_size = 0;
int close_size = 0;
//四种形态学操作共用的参数
//结构元种类的最大值:一个3种:正方形、十字、椭圆
int const max_elements = 2;
//结构元的最大值(四种共用一个)
int const max_kernel_size = 5;
//工具条的回调函数
void Erosion(int,void*);
void Dilation(int,void*);
void Open(int, void*);
void Close(int,void*);
int main()
{
cout<<"结构元类型:0-矩形,1-十字,2-椭圆"<<endl;
cout<<"结构元大小:2*n+1"<<endl;
image = imread("D:/picture/images/binary.bmp");
if(!image.data)
return -1;
imshow("源图像",image);
eroded.create(image.rows,image.cols,image.type());
dilated.create(image.rows,image.cols,image.type());
namedWindow("腐蚀");
namedWindow("膨胀");
namedWindow("开运算");
namedWindow("闭运算");
//创建工具条
//腐蚀操作:
createTrackbar("核函数类型","腐蚀",&erosion_element,max_elements,Erosion);
createTrackbar("核函数大小","腐蚀",&erosion_size,max_kernel_size,Erosion);
//调用腐蚀函数
Erosion(0,0);
//膨胀操作
createTrackbar("结构元类型","膨胀",&dilation_element,max_elements,Dilation);
createTrackbar("结构元大小","膨胀",&dilation_size,max_kernel_size,Dilation);
Dilation(0,0);
//开操作
createTrackbar("结构元类型","开运算",&open_element,max_elements,Open);
createTrackbar("核函数大小","开运算",&open_size,max_kernel_size,Open);
Open(0,0);
//闭操作
createTrackbar("结构元类型","闭运算",&close_element,max_elements,Close);
createTrackbar("核函数大小","闭运算",&close_size,max_kernel_size,Close);
Close(0,0);
waitKey(0);
return 0;
}
//回调函数:
//腐蚀函数:不需要输入参数
void Erosion(int,void*)
{
int erosion_type;
if(erosion_element == 0)
{
erosion_type = MORPH_RECT;
}
else if(erosion_element == 1)
{
erosion_type = MORPH_CROSS;
}
else if(erosion_element == 2)
{
erosion_type = MORPH_ELLIPSE;
}
//获取腐蚀元素:参数为:类型、大小、锚点
Mat element = getStructuringElement(erosion_type,Size(2*erosion_size+1,2*erosion_size+1),Point(-1,-1));
//进行腐蚀运算:参数为:原图像、结果、腐蚀元素
erode(image,eroded,element);
//显示腐蚀结果
imshow("腐蚀",eroded);
}
//膨胀回调函数
void Dilation(int,void*)
{
int dilation_type;
if(dilation_element == 0)
{
dilation_type = MORPH_RECT;
}
else if(dilation_element == 1)
{
dilation_type = MORPH_CROSS;
}
else if(dilation_element == 2)
{
dilation_type = MORPH_ELLIPSE;
}
//获取腐蚀元素:参数为:类型、大小、锚点
Mat element = getStructuringElement(dilation_type,Size(2*dilation_size+1,2*dilation_size+1),Point(-1,-1));
//进行腐蚀运算:参数为:原图像、结果、腐蚀元素
dilate(image,dilated,element);
//显示腐蚀结果
imshow("膨胀",dilated);
}
void Open(int, void*)
{
int open_type;
if(open_element == 0)
{
open_type = MORPH_RECT;
}
else if(open_element == 1)
{
open_type = MORPH_CROSS;
}
else if(open_element == 2)
{
open_type = MORPH_ELLIPSE;
}
//获取腐蚀元素:参数为:类型、大小、锚点
Mat element = getStructuringElement(open_type,Size(2*open_size+1,2*open_size+1),Point(-1,-1));
//进行开运算
morphologyEx(image,opened,cv::MORPH_OPEN,element);
imshow("开运算",opened);
}
//闭操作
void Close(int,void*)
{
int close_type;
if(close_element == 0)
{
close_type = MORPH_RECT;
}
else if(close_element == 1)
{
close_type = MORPH_CROSS;
}
else if(close_element == 2)
{
close_type = MORPH_ELLIPSE;
}
//获取腐蚀元素:参数为:类型、大小、锚点
Mat element = getStructuringElement(close_type,Size(2*close_size+1,2*close_size+1),Point(-1,-1));
//进行开运算
morphologyEx(image,closed,cv::MORPH_CLOSE,element);
imshow("闭运算",closed);
}
美中不足的是,为了改变形态学操作的结构元种类和大小,我定义了许多的全局变量。
如果想知道形态学操作的更多内容,还是看冈萨雷斯的那本经典的《数字图像处理》吧!