这次主要介绍两方面的内容,一部分是形态学操作,另一部分是工具条。
先说形态学操作。这里只介绍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); }
美中不足的是,为了改变形态学操作的结构元种类和大小,我定义了许多的全局变量。
如果想知道形态学操作的更多内容,还是看冈萨雷斯的那本经典的《数字图像处理》吧!