图像形态学滤波是图像滤波的一个重要分支,与之前介绍过的空间域滤波(方框滤波、高斯滤波、中值滤波)以及图像边缘提取过程中的滤波相同的是,他们都是通过滤波算子掩膜计算图像(中心)像素点的过程,不同的是滤波算子(内核)和计算方式,概括如下:
总体来说,图像的形态学操作是基于形状的一系列图像处理操作,在OpenCV中对图像的形态学变换提供了方便快捷的函数,包括基本形态学操作和高级形态学操作。
图像形态学,也叫做数学形态学(Mathematical morphology) 是一门建立在格论和拓扑学基础之上的图像分析学科,是数学形态学图像处理的基本理论,在图像处理中占有相当重要的地位。
在图像处理中,对于图像形态学的应用相当的广泛,主要应用在对于图像的预处理操作当中,尤其是在对二值图像的预处理和分析方面。
结构元素
形态学滤波最主要特征在集合运算,而集合运算是通过由0/1组成的不同形态的结构元素实现的,首先明确结构元素的概念:
结构元素是由元素为1或者0的矩阵组成,结构元素为1的区域定义了图像的邻域,领域内的像素在进行态学操作时要进行考虑。一般来说,二维或者平面结构的结构元素要比处理的图像小得多。结构元素的中心像素,即结构元素的原点,与输入图像中感兴趣的像素值(即要处理的像素值)相对应。
OpenCV中获取结构元素的函数为getStructuringElement,原型为:
Mat getStructuringElement(int shape, //内核的形状
Size esize, //内核的尺寸
Point anchor = Point(-1, -1) //锚点的位置 )
内核的形状,有三种形状可以选择。
基本形态学操作有腐蚀和膨胀,在腐蚀和膨胀上面通过不同运算方式可得到高级形态学操作,其主要作用有:
【注】:OpenCV里面的腐蚀和膨胀操作是针对白色目标区域的
腐蚀:用结构元素的中心点对准当前正在遍历的这个像素,然后取当前结构元素所覆盖下的原图对应区域内的所有像素的最小值,用这个最小值替换当前像素值,删除对象边界的某些像素,使二值图像减小一圈
腐蚀是一种最小值滤波,即用结构元素内的最小值去替换锚点的像素值,具体操作为:
用结构元素,扫描图像的每一个像素
用结构元素与其覆盖的二值图像做 “与”操作
如果都为1,结果图像的该像素为1。否则为0,也就是查找被处理图像中能不能找到和结构元素相同的矩阵。如果存在那么中心点所对应的点就为1,否则为0
简单来说,就是腐蚀图像白色边缘(像素值为1)缩小一圈,图示如下:
若图中原始图像边界点像素为src(x,y),采用结构元素element(x’,y’)处理后得到dst(x,y),公式表述为:
d s t ( x , y ) = m a x ( x ′ , y ′ ) : e l e m e n t ( x ′ , y ′ ) ≠ 0 s r c ( x + x ′ , y + y ′ ) dst(x,y)=\underset{(x',y'):element(x',y')\neq 0}{max}src(x+x',y+y') dst(x,y)=(x′,y′):element(x′,y′)=0maxsrc(x+x′,y+y′)
在OpenCV中进行腐蚀操作可以采用函数erode(),或者采用morphologyEx() 函数并输出需要进行的形态学操作类型。如下为erode()函数原型:
void erode(InputArray src, //原始图像
OutputArray dst, //目标图像,和源图片有一样的尺寸和类型
InputArray kernel, //内核,可以使用getStructuringElement来创建结构元素
Point anchor=Point(-1,-1), //锚点的位置,其有默认值(-1,-1),表示锚位于中心
int iterations=1, //迭代使用该函数的次数,默认值为1
int borderType=BORDER_CONSTANT, //用于推断图像外部像素的某种边界模式,默认值BORDER_DEFAULT
const Scalar& borderValue=morphologyDefaultBorderValue() //当边界为常数时的边界值,有默认值morphologyDefaultBorderValue(),
)
膨胀:用结构元素的中心点对准当前正在遍历的这个像素,然后取当前结构元素所覆盖下的原图对应区域内的所有像素的最大值,用这个最大值替换当前像素值,给图像中的对象边界添加像素,使图像扩大一圈
膨胀是一种最大值滤波,即用结构元素内的最大值去替换锚点的像素值,具体操作:
用结构元素,扫描图像的每一个像素
用结构元素与其覆盖的二值图像做 “与”操作
如果都为0,结果图像的该像素为0。否则为1,
也就是在结构元素覆盖范围下,只要有一个像素符和结构元素像素相同,那么中心点对应点就为1,否则为0
简单来说,就是膨胀图像边缘白色(像素值为1)扩大一圈,图示如下:
类似上,公式表述为:
d s t ( x , y ) = m i n ( x ′ , y ′ ) : e l e m e n t ( x ′ , y ′ ) ≠ 0 s r c ( x + x ′ , y + y ′ ) dst(x,y)=\underset{(x',y'):element(x',y')\neq 0}{min}src(x+x',y+y') dst(x,y)=(x′,y′):element(x′,y′)=0minsrc(x+x′,y+y′)
在OpenCV中进行膨胀操作可以采用函数dilate(),或者采用morphologyEx() 函数并输出需要进行的形态学操作类型。如下为cv::dilate()的函数原型,和erode类似:
void dilate(InputArray src, //原始图像
OutputArray dst, //目标图像,和源图片有一样的尺寸和类型
InputArray kernel, //内核,可以使用getStructuringElement来创建结构元素
Point anchor=Point(-1,-1), //锚点的位置,其有默认值(-1,-1),表示锚位于中心
int iterations=1, //迭代使用该函数的次数,默认值为1
int borderType=BORDER_CONSTANT, //用于推断图像外部像素的某种边界模式,默认值BORDER_DEFAULT
const Scalar& borderValue=morphologyDefaultBorderValue() //当边界为常数时的边界值,有默认值morphologyDefaultBorderValue()
)
【注】腐蚀:删除对象边界的某些像素
~~~~~~~~~~ 膨胀:给图像中的对象边界添加像素
开运算:先腐蚀,再膨胀,可清除一些亮的像素点,放大局部低亮度的区域,在纤细点处分离物体,并且在平滑较大物体的边界的同时不明显改变其面积。
闭运算:先膨胀,再腐蚀,可清除一些暗的像素点,即排除小型黑洞(黑色区域)。
膨胀图与腐蚀图之差,提取物体边缘,对二值图像进行这一操作可以将边缘突出出来。
顶帽:原图像-开运算图,突出原图像中比周围亮的区域
黑帽:闭运算图-原图像,突出原图像中比周围暗的区域
morphologyEx函数利用基本的膨胀和腐蚀技术,来执行更加高级形态学变换,函数原型为:
void morphologyEx(InputArray src, //原始图像
OutputArray dst, //目标图像
int op, //形态学操作
InputArray kernel, //内核
Point anchor=Point(-1,-1), //锚点 ,其有默认值(-1,-1),表示锚位于中心
int iterations=1, //迭代使用该函数的次数,默认值为1
int borderType=BORDER_CONSTANT, //用于推断图像外部像素的某种边界模式,默认值BORDER_DEFAULT
const Scalar& borderValue=morphologyDefaultBorderValue() //当边界为常数时的边界值,有默认值morphologyDefaultBorderValue(),
)
下面这段代码分为两个部分,一部分使用OpenCV自带形态学处理函数实现的,一部分使用自己构造函数实现的。
在实现My_open开运算的同时,使用双滚动条控制结构元素的宽(W)和高(H),具体代码如下:
#include
#include
using namespace cv;
//函数声明
void my_erode(Mat& src,Mat& dst,Size dsize);
void my_dilate(Mat& src,Mat& dst,Size dsize);
void my_open(Mat& src, Mat& dst, Size dsize);
void my_close(Mat& src, Mat& dst, Size dsize);
//全局定义:比较耗内存
int h=1,w=1;
Mat newSrc;
Mat dst_open1;
void on_Track(int ,void*){ //回调函数
my_open(newSrc,dst_open1,Size(2*h+1,2*w+1));
imshow("WIN_NAME",dst_open1);
}
int main(){ //主函数
Mat src=imread("C:/Users/Administrator/Desktop/beauty.jpg");
Size dsize=Size(round(0.3*src.cols),round(0.3*src.rows));
resize(src,src,dsize,0,0,CV_INTER_LINEAR);
//Part1:OpenCV自带函数实现腐蚀膨胀、开闭运算、顶帽底帽等
Mat dst_erode,dst_dilate;
double t_beg=getTickCount();
Mat element=getStructuringElement(MORPH_RECT,Size(7,7)); //矩形内核
erode(src,dst_erode,element); //腐蚀:计时
double t_end=getTickCount();
std::cout<<"OpenCV_erode time:"<<((t_end-t_beg)*1000)/getTickFrequency()<<"ms"<<std::endl;
dilate(src,dst_dilate,element); //膨胀
Mat dst_open;
morphologyEx(src,dst_open,MORPH_OPEN,element); //开运算
/*腐蚀和膨胀也可以应用此模板,对应的参数为
MORPH_OPEN : 开运算
MORPH_CLOSE : 闭运算
MORPH_ERODE : 腐蚀
MORPH_DILATE : 膨胀
MORPH_TOPHAT : 顶帽
MORPH_BLACKHAT : 黑帽
*/
//Part2:C++构造函数实现腐蚀、膨胀、开闭操作
Mat dst_erode1(src.size(),src.type()),dst_dilate1(src.size(),src.type());
//Size dsize(7,7);
double t_beg1=getTickCount();
my_erode(src,dst_erode1,Size(7,7)); //腐蚀:计时
double t_end1=getTickCount();
std::cout<<"My_erode time:"<<((t_end1-t_beg1)*1000)/getTickFrequency()<<"ms"<<std::endl;
my_dilate(src,dst_dilate1,Size(7,7));
//设置滚动条C++实现开运算
int max_value=5; //设置滚动条最大值
src.copyTo(newSrc);
src.copyTo(dst_open1);
namedWindow("WIN_NAME",WINDOW_AUTOSIZE); //创建窗口
createTrackbar("h","WIN_NAME",&h,max_value,on_Track); //h滚动条
createTrackbar("w","WIN_NAME",&w,max_value,on_Track); //w滚动条
on_Track(0,0); //调用回调函数,并显示
//进行显示、写入等操作
waitKey(0);
return 0;
}
void my_erode(Mat& src,Mat& dst,Size wsize){
if((wsize.height%2==0) || (wsize.width%2==0)){
printf("The length and width must be odds!");
exit(-1);
}
int center_y=wsize.height/2;
int center_x=wsize.width/2;
Mat borderDst;
copyMakeBorder(src,borderDst,center_x,center_x,center_y,center_y,BORDER_REFLECT101); //对称扩展边界
for(int row=center_y;row<src.rows+center_y;row++){
for(int col=center_x;col<src.cols+center_x;col++){
//对模板覆盖图像区域处理
if(src.channels() == 1){ //单通道
int min_Value=255;
for(int i=row-center_y;i<row+center_y+1;i++){
for(int j=col-center_x;j<col+center_x+1;j++){
min_Value=MIN(min_Value,borderDst.at<uchar>(i,j)); //掩膜最小值:白色区域(255)缩小一圈
}
}
dst.at<uchar>(row-center_y,col-center_x)=min_Value;
}
else{
for(int channel=0;channel<3;channel++){ //多通道
int min_Value=255;
//对模板覆盖图像区域处理
for(int i=row-center_y;i<row+center_y+1;i++){
for(int j=col-center_x;j<col+center_x+1;j++){
min_Value=MIN(min_Value,borderDst.at<Vec3b>(i,j)[channel]); //掩膜最小值:白色区域(255)缩小一圈
}
}
dst.at<Vec3b>(row-center_y,col-center_x)[channel]=min_Value;
}
}
}
}
}
void my_dilate(Mat& src,Mat& dst,Size dsize){ //类似my_erode处理
if(dsize.width%2 == 0 || dsize.height%2 == 0){
printf("Kernel size do not be odds!");
exit(-1);
}
int h=dsize.height/2; //row
int w=dsize.width/2; //col
Mat borderSrc;
copyMakeBorder(src,borderSrc,h,h,w,w,BORDER_REFLECT101);
//对图像每一个像素遍历
for(int row=h;row<src.rows+h;row++){
for(int col=w;col<src.cols+w;col++){
//对每一个掩膜区域逻辑处理
if(src.channels() == 1){ //单通道
int max_Value=0;
for(int i=row-h;i<row+h+1;i++){
for(int j=col-w;j<col+w+1;j++){
max_Value=MAX(max_Value,borderSrc.at<uchar>(i,j));
}
}
dst.at<uchar>(row-h,col-w)=max_Value;
}
else{
for(int channel=0;channel<3;channel++){ //多通道
int max_Value=0;
for(int i=row-h;i<row+h+1;i++){
for(int j=col-w;j<col+w+1;j++){
max_Value=MAX(max_Value,borderSrc.at<Vec3b>(i,j)[channel]);
}
}
dst.at<Vec3b>(row-h,col-w)[channel]=max_Value;
}
}
}
}
}
void my_open(Mat& src, Mat& dst, Size dsize) { //开运算:先腐蚀再膨胀
Mat dst1;
src.copyTo(dst1);
my_erode(src, dst1,dsize);
my_dilate(dst1, dst,dsize);
}
void my_close(Mat& src, Mat& dst, Size dsize) { //闭运算:先膨胀再腐蚀
Mat dst1;
src.copyTo(dst1);
my_erode(src, dst1,dsize);
my_dilate(dst1, dst,dsize);
}
void my_tophat(Mat src, Mat dst, Size dsize) { //顶帽:原图-开运算(注意防溢出)
Mat dst1;
src.copyTo(dst1);
my_open(src, dst1, dsize);
imshow("open", dst1);
for (int rows = 0; rows < src.rows; rows++) {
for (int cols = 0; cols < src.cols; cols++) {
if(src.channels() == 1){
dst.at<uchar>(rows, cols) = saturate_cast<uchar>(src.at<uchar>(rows, cols) - dst1.at<uchar>(rows, cols));
}
else{
dst.at<Vec3b>(rows, cols)[0] = saturate_cast<uchar>(src.at<Vec3b>(rows, cols)[0] - dst1.at<Vec3b>(rows, cols)[0]);
dst.at<Vec3b>(rows, cols)[1] = saturate_cast<uchar>(src.at<Vec3b>(rows, cols)[1] - dst1.at<Vec3b>(rows, cols)[1]);
dst.at<Vec3b>(rows, cols)[2] = saturate_cast<uchar>(src.at<Vec3b>(rows, cols)[2] - dst1.at<Vec3b>(rows, cols)[2]);
}
}
}
imshow("顶帽", dst);
}
void my_underhat(Mat src, Mat dst, Size dsize) { //黑帽:闭运算-原图
Mat dst1;
src.copyTo(dst1);
my_close(src, dst1, dsize);
imshow("colse", dst1);
for (int rows = 0; rows < src.rows; rows++) {
for (int cols = 0; cols < src.cols; cols++) {
if(src.channels() == 1){
dst.at<uchar>(rows, cols) = saturate_cast<uchar>(dst1.at<uchar>(rows, cols) - src.at<uchar>(rows, cols));
}
else{
dst.at<Vec3b>(rows, cols)[0] = saturate_cast<uchar>(dst1.at<Vec3b>(rows, cols)[0] - src.at<Vec3b>(rows, cols)[0]);
dst.at<Vec3b>(rows, cols)[1] = saturate_cast<uchar>(dst1.at<Vec3b>(rows, cols)[1] - src.at<Vec3b>(rows, cols)[1]);
dst.at<Vec3b>(rows, cols)[2] = saturate_cast<uchar>(dst1.at<Vec3b>(rows, cols)[2] - src.at<Vec3b>(rows, cols)[2]);
}
}
}
namedWindow("黑帽", CV_WINDOW_AUTOSIZE);
imshow("黑帽", dst);
}
滚动条控制内核kernel的W和H得到开运算目标图像,显示如下:
其他效果续更…
参考博客:
【1】【OpenCV(C++)】图像处理:形态学滤波
【2】形态学操作——腐蚀膨胀、开闭运算、顶帽变换底帽变换 c++实现