数字图像处理第九章形态学图像处理(膨胀、腐蚀、开操作、闭操作、击中或击不中、边界提取、连通分量提取)

9.1预备知识

数学形态学的语言是集合;
数学形态学中的集合表示图像中的不同对象;
例如在二值图像中,所有黑色像素的集合是图像完整的形态学描述;
在二值图像中,正在被讨论的集合是二维整数空间z^2的元素,在这个整数二维空间中,集合的每个元素都是一个多元组,是一个黑色(或白色。取决于事先约定)像素在图像中的坐标(x,y)。
一般默认,二值图像中,白色为背景图,黑色为描述图像的元素。
假设存在集合A 和集合B:
集合A的补集合=是不包含集合A元素的所有元素组成A^c.
集合A和B的差表示为 A - B = A ∩ B^c ;
集合B的反射是指关于坐标原点进行翻转, B^ = {W | W = - b , b ∈ B};
集合A平移到点z=(z1,z2)表示为(A)z,定义为:(A)z = {c | c = a + z , a ∈ A}。

9.2膨胀与腐蚀

膨胀与腐蚀是许多形态学算法的基础。
腐蚀和膨胀是针对高亮区域的操作:
图像的像素值越大的地方,图像越亮,而腐蚀和膨胀就是求图像像素局部最小值和局部最大值的过程
膨胀(dilate):就是对图像的高亮部分进行膨胀,相当于高亮部分的领域扩张
腐蚀(erode):就是对图像的高亮部分的侵蚀,也就是经过腐蚀操作之后图像的高亮部分变得更少了

9.2.1 膨胀

假设A与B是Z^2中的集合,则以B为一个结构元,A为被膨胀的集合(图像物体),定义为:
在这里插入图片描述
在这里插入图片描述
可以将B视为一个卷积模板,首先B关于远点翻转旋转,然后逐步移动以滑过整个集合(图像)A,这一过程类似于空间卷积,但是这一过程是非线性操作,而空间卷积是属于线性操作。
膨胀会增长或者粗化二值图像中的物体,这种特殊方式的粗化的宽度由所用的结构元来控制。.
如下图所示:虚线显示了原始集合,实线显示了一个界限,B^ 的原点进一步移动z,若原点不在虚线上,超出了这一界限,会导致B^和A的交集为空,因此处在该边界上或该边界内的所有点就构成了B 对A的膨胀。后面一张图是用来实现垂直方向膨胀比水平方向膨胀更多的结构元。
在这里插入图片描述

9.2.2腐蚀

假设A与B是Z^2中的集合,则以B为一个结构元,A为被腐蚀的集合(图像物体),定义为:
在这里插入图片描述
在这里插入图片描述
上述式子表示为B对A的腐蚀是一个用z平移的B包含在A中所有的点z的集合。
A和B的元素显示为阴影,背景显示为白色。
实线边界是B的原点进一步移动的界限,若超出该界限,则会导致结构元不再完全包含于A中,这样该边界内的点的轨迹(B的原点位置)就构成了对A的腐蚀。
腐蚀是简单满足上述定义式的z值的一个集合。
在这里插入图片描述

9.2.3对偶性

膨胀和腐蚀彼此关于集合求补运算和反射运算是对偶的,即为:
在这里插入图片描述

9.2.4 代码实现

腐蚀是求局部最小值;
膨胀是求局部最大值;
首先,在腐蚀与膨胀的操作过程中,给出一个核窗口,它有一个单独定义出来的参考点,我们称之为锚点,(这个窗口的给出我们一般会结合函数getStructuringElement给出),然后我们把这个窗口在原图像上滑动,计算原图像在这个窗口覆盖部分之下的最值,然后把这个最值赋值给参考点指定的像素
如果这个最值是最大值,那么上述操作就是膨胀dilate,它会使原图像高亮部分的领域逐渐扩张
如果这个最值是最小值,那么上述操作就是腐蚀erode,它会使原图像高亮部分的领域被侵蚀
1、opencv自带函数
(1)膨胀:dilate
此操作将图像 A 与任意形状的内核 (B),通常为正方形或圆形,进行卷积。
内核 B 有一个可定义的锚点, 通常定义为内核中心点。
进行膨胀操作时,将内核 B 划过图像,将内核B 覆盖区域的最大相素值提取,并代替锚点位置的相素。

void dilate( const Mat& src, Mat& dst, const Mat& element,Point anchor=Point(-1,-1), int iterations=1,int borderType=BORDER_CONSTANT,const Scalar& borderValue=morphologyDefaultBorderValue() );

第一个参数:输入图像
第二个参数:输出图像,要和原图像有一样的尺寸和类型
第三个参数:原图像类型的element,膨胀操作的核,当为NULL时表示使用的是参考点位于中心的3x3的核(通常使用函数getStructuringElement来计算)
第四个参数:锚点位置,有默认值Point(-1, -1)
第五个参数:迭代使用dilate的次数,默认值为1
第六个参数:有默认值BORDER_DEFAULT
第七个参数:const Scalar类型的 borderValue,一般不用管他

(2)腐蚀:erode
腐蚀在形态学操作家族里是膨胀操作的孪生姐妹。它提取的是内核覆盖下的相素最小值。
进行腐蚀操作时,将内核 B 划过图像,将内核B 覆盖区域的最小相素值提取,并代替锚点位置的相素。
在这里插入图片描述
在这里插入图片描述
(3)获取锚点模板的函数: getStructuringElement函数

Mat element = getStructuringElement(MORPH_RECT, Size(5, 5), Point(-1, -1));

第一个参数:内核的形状,有下面三种形状可以选择:
MORPH_RECT : 矩形
MORPH_CROSS : 交叉形
MORPH_ELLIPSE : 椭圆形
Size类型的内核的尺寸
Point类型的锚点的位置,有默认值Point(-1, -1)
注意:交叉型的element形状唯一依赖于锚点的位置,其他情况下,锚点的位置只是影响到了形态学运算结果的偏移

2、c++自实现腐蚀膨胀函数

#include<iostream>
#include<opencv/cv.hpp>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
void mydilate( Mat &src,  Mat &dst)
{
	dst = src.clone();
	//Mat element1 = getStructuringElement(MORPH_RECT, ksize, Point(-1, -1));
	for (int i = 0; i < src.rows; i++)
	{
		for (int j = 0; j < src.cols; j++)
		{
			
				uchar maxV = 0;
				for (int k = i - 1; k <= i + 1; k++) //3 x 3模板
				{
					for (int r = j - 1; r <=  j + 1; r++)
					{
						if (k < 0 || k >= src.rows || r < 0 || r >= src.cols)
						{
							continue;
						}
					
						maxV = (std::max<uchar>)(maxV, src.at<uchar>(k, r));//比较两个函数大小
						
					}
				}
				dst.at<uchar>(i, j) = maxV;
		}

	}

}

int main()
{
	Mat src = imread("C:/Users/征途/Desktop/vs-cpp/第九章/01.jpg", IMREAD_GRAYSCALE);
	if (!src.data)
	{
		cout << "输入错误" << endl;
	}
	imshow("原图", src);
	Mat dst;
	//mydilate(src, dst);
	//imshow("膨胀", dst);
	waitKey(0);
	return 0;


}

9.3开操作和闭操作

开操作:一般平滑物体的轮廓、断开较窄的狭颈并消除细的突出物;
闭操作:也会平滑轮廓的一部分,通常会弥合较窄的简断和细长的沟壑,消除小的孔洞,填补轮廓线中的断裂。

9.3.1开操作

结构元B对集合A进行开操作就是先B对A进行腐蚀,紧接着用B对A进行膨胀;定义为:

在这里插入图片描述
假设将结构元B设为一个扁平的转球,然后开操作后的边界由B中的点建立:当B在A边界进行滚动时,B能到达的A边界的最远点就是开操作的边界。
开操作也可以表示为:在这里插入图片描述
该式表明B对A的开操作是通过拟合到A 的B的所有平移的并集得到的。
在这里插入图片描述

开操作性质:
在这里插入图片描述

9.3.2闭操作

结构元B对集合A进行闭操作就是先B对A进行膨胀,紧接着用B对A进行腐蚀;定义为:
在这里插入图片描述
假设B为一个转球,在边界的外侧进行滚动,然后闭操作的边界由B确定;
在这里插入图片描述
闭操作性质:
在这里插入图片描述

9.3.3 对称性

开操作和闭操作彼此关于集合求补和反射也是对偶的,即为:
在这里插入图片描述
用于对二值化后的图像进行处理,属于形态学操作(morphology)

开操作:消除白色的小点,去除小的干扰块
在这里插入图片描述
闭操作:消除黑色的小块,填充闭合区域
在这里插入图片描述

opencv自带函数实现:

cv2.morphologyEX( img 输入图像,
cv2.MORPH_CLOSE(闭操作),cv2.MORPH_OPEN(开操作) 形态学操作
kernel (卷积核)

其中kernel,用 cv2.getStructuringElement(cv2.MORPH_RECT,(5,5)) 获得

c++自定义实现开闭操作:

#include <iostream>
#include <algorithm>
#include <opencv2\opencv.hpp>

using namespace cv;
using namespace std;

//开操作
void open_my(Mat &src, Mat &dst)
{
	Mat dst1;
	dst1.create(src.rows, src.cols, CV_8UC1);
	for (int i = 0; i < src.rows; ++i)
	{
		for (int j = 0; j < src.cols; ++j)
		{
			uchar minV = 255;
			for(int xi = i - 1; xi <= i + 1; xi++)
			{
				for (int yi = j - 1; yi <= j + 1; yi++)
				{
					if (xi < 0 || xi > src.rows || yi < 0 || yi > src.cols)
					{
						continue;
					}
					else
					{
						minV = (std::min<uchar>)(minV, src.at<uchar>(xi, yi));
					}
					
				}
			}
			dst1.at<uchar>(i, j) = minV;
		}
	}
	Mat dst2;
	dst2.create(src.rows, src.cols, CV_8UC1);
	for (int k = 0; k < dst1.rows; ++k)
	{
		for (int r = 0; r < dst1.cols; ++r)
		{
			uchar maxV = 0;
			for (int x2 = k - 1; x2 <= k + 1; x2++)
			{
				for (int y2 = r - 1; y2 <= r + 1; y2++)
				{
					if (x2 < 0 || x2 > dst1.rows || y2 < 0 || y2 > dst1.cols)
					{
						continue;
					}
					else
					{
						maxV = (std::max<uchar>)(maxV, dst1.at<uchar>(x2, y2));
					}

				}
			}
			dst2.at<uchar>(k,r) = maxV;
		}
	}
	
}
//闭操作
void close_my(Mat &src, Mat &dst)
{
	Mat src1;
	Mat dst1;
	dst1.create(src.rows,src.cols,CV_8UC1);
	
	
	for (int k = 0; k < src.rows; ++k)
	{
		for (int r = 0; r < src.cols; ++r)
		{
			uchar maxV = 0;
			for (int x2 = k - 1; x2 <= k + 1; x2++)
			{
				for (int y2 = r - 1; y2 <= r + 1; y2++)
				{
					if (x2 < 0 || x2 > src.rows || y2 < 0 || y2 > src.cols)
					{
						continue;
					}
					else
					{
						maxV = (std::max<uchar>)(maxV, dst1.at<uchar>(x2, y2));
					}

				}
			}
			dst1.at<uchar>(k, r) = maxV;
		}
	}
	Mat dst2;
	dst2.create(dst1.rows, dst1.cols, CV_8UC1);
	for (int i = 0; i < dst1.rows; ++i)
	{
		for (int j = 0; j < dst1.cols; ++j)
		{
			uchar minV = 255;
			for (int xi = i - 1; xi <= i + 1; xi++)
			{
				for (int yi = j - 1; yi <= j + 1; yi++)
				{
					if (xi < 0 || xi > dst1.rows || yi < 0 || yi > dst1.cols)
					{
						continue;
					}
					else
					{
						minV = (std::min<uchar>)(minV, dst1.at<uchar>(xi, yi));
					}

				}
			}
			dst2.at<uchar>(i, j) = minV;
		}
	}
	
}


int main()
{
	Mat img,dst1,dst2;
	img  = imread("C:\\Users\\wj257\\Desktop\\vs2019\\Project2\\1.jpg",IMREAD_GRAYSCALE);
	imshow("1", img);
	open_my(img, dst2);
	imshow("2", dst2);
	close_my(img, dst1);
	imshow("3", dst1);
	waitKey(0);
	return 0;
}

9.4击中或击不中变换

形态学中的击中击不中是一种形状检测工具。
击中击不中变换主要用来检测输入图像的某个特定图像的位置。例如,我们想在A图像内部找到和D图像的位置,可以表示如下:
A⊛D=(A⊖D)∩(Ac⊖(W-D))
其中,Ac表示A的补集,
在这里插入图片描述
击中是指 如果B表示由D及其背景组成的集合,则B在A中的匹配,表示为
在这里插入图片描述
如果令B = (B1, B2),其中B1是代表与一个目标相联系的B的元素构成的集合,B2是由与相应背景相联系的B的元素构成的集合。则有:.
在这里插入图片描述
最终可以化为:
在这里插入图片描述
我们将上面三个公式称为 形态学的击中或击不中变换。
利用D对A进行腐蚀
利用(W-D)对A的补集进行腐蚀
将步骤一和步骤二得到的图像进行与操作,最终得到变换后的操作

C++代码自实现:

在这里插入图片描述
上图是图像A,我们利用击中击不中原理找到其中较大的正方形。
在这里插入图片描述
上述图形为D;

#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main(int argc, char** argv)
{

	Mat a, b, aInv, bInv, hitOrNot, b_erode_a, bInv_erode_aInv;
	a = imread("C:\\Users\\wj257\\Desktop\\vs2019\\Project2\\a.jpg", IMREAD_GRAYSCALE);   
	threshold(a, a, 180, 255, THRESH_BINARY);  //二值化,第五个参数
	//imshow("二值化图像", input_image);

	b = imread("C:\\Users\\wj257\\Desktop\\vs2019\\Project2\\b.jpg", IMREAD_GRAYSCALE);
	threshold(b, b, 180, 1, THRESH_BINARY);
	copyMakeBorder(b, b, 1, 1, 1, 1, BORDER_CONSTANT, Scalar(1));   //利用copyMakeBorder()扩大一圈,新区域的值与原核的值相反;避免使用值全0时的核对图像进行腐蚀(卷积)的操作

	erode(a, b_erode_a, b, Point(-1, -1), 1, BORDER_DEFAULT, 0);
	imshow("b_erode_a", b_erode_a);


	aInv = imread("C:\\Users\\wj257\\Desktop\\vs2019\\Project2\\a.jpg", IMREAD_GRAYSCALE);
	threshold(aInv, aInv, 180, 255, THRESH_BINARY_INV);  //二值化第五个参数与上面的相反取补集

	bInv = imread("C:\\Users\\wj257\\Desktop\\vs2019\\Project2\\b.jpg", IMREAD_GRAYSCALE);
	threshold(bInv, bInv, 180, 1, THRESH_BINARY_INV);	 //二值化第五个参数与上面的相反取补集


	copyMakeBorder(bInv, bInv, 1, 1, 1, 1, BORDER_CONSTANT, Scalar(0));  //利用copyMakeBorder()扩大一圈,新区域的值与原核的值相反;避免使用值全0时的核对图像进行腐蚀(卷积)的操作
	    

	erode(aInv, bInv_erode_aInv, bInv, Point(-1, -1), 1, BORDER_DEFAULT, 0);
	imshow("bInv_erode_aInv", bInv_erode_aInv);

	bitwise_and(b_erode_a, bInv_erode_aInv, hitOrNot);//逻辑与,求交集
	imshow("hitOrNot", hitOrNot);

	waitKey(0);
	return 0;
}

在这里插入图片描述

9.5一些基本的形态学算法

9.5.1 边界提取

表示β(A)的集合A的边界可以通过先用B对A腐蚀,而后执行A和腐蚀结果之间的集合之差得到,即为:
在这里插入图片描述
其中B是一个适当的结构元。
下图说明了二值图像应用边界提取:
在这里插入图片描述
在这里插入图片描述

#include <iostream>
#include <algorithm>
#include <opencv2\opencv.hpp>

using namespace cv;
using namespace std;

int main()
{
	//从文件中读取成灰度图像
	
	Mat img = imread("C:\\Users\\wj257\\Desktop\\vs2019\\Project2\\1.jpg", IMREAD_GRAYSCALE);
	if (img.empty())
	{
		fprintf(stderr, "Can not load image %s\n");
		return -1;
	}

	//OpenCV方法
	Mat dilated_cv;
	dilate(img, dilated_cv, Mat());

	//自定义方法
	Mat dst;
	dst.create(img.rows, img.cols, CV_8UC1);
	for (int i = 0; i < img.rows; ++i)
	{
		for (int j = 0; j < img.cols; ++j)
		{
			//uchar minV = 255;
			uchar maxV = 0;

			//遍历周围最大像素值
			for (int k = i - 1; k <= i + 1; k++)
			{
				for (int r = j - 1; r <= j + 1; r++)
				{
					if (r < 0 || r >= img.cols || k < 0 || k >= img.rows)
					{
						continue;
					}
					//minV = (std::min<uchar>)(minV, img.at<uchar>(yi, xi));
					maxV = (std::max<uchar>)(maxV, img.at<uchar>(k, r));
				}
			}
			dst.at<uchar>(i, j) = maxV;
		}
	}

	//比较两者的结果
	Mat c;
	compare(dilated_cv, dst, c, CMP_EQ);

	//显示
	imshow("原始", img);
	imshow("膨胀_cv", dilated_cv);
	imshow("膨胀_my", dst);
	imshow("比较结果", c);

	waitKey();

	return 0;
}

在这里插入图片描述
在这里插入图片描述

9.5.2 连通分量的提取

令A是包含一个或多个连通分量的集合,并形成一个阵列X0(该阵列与包含A的阵列大小相同),除了在对应于A中每个连通分量中的一个点的已知的每一个位置处我们已置为,其他元素为背景值都为0;如下:
在这里插入图片描述
其中B是一个结构元,当Xk = Xk-1 时,迭代过程结束,Xk包含输入图像中的所有的连通分量。
提取连通域实际上是标记连通域的过程,其算法如下:
在这里插入图片描述
初始点:Bo=某个连通分量中的某点(这里通过遍历图像,知道像素值为255,将此点作为种子点)

循环:

(用3X3d的结构元素对种子点进行膨胀,然后用原图像对膨胀结果取交集)

结束条件:直到膨胀的图像不发生变化

在这里可以用一个模板来存储连通分量,其位置对应原图像的位置。提取完后可以修改原图像的像素值,即一个标签。

 
#include<iostream>  
#include<opencv2\opencv.hpp>  
using namespace std;
using namespace cv;

/*******************************************
功能:标注连通分量
参数:src-输入图像
	  nConn-取值4、8,表示4连通或8连通
********************************************/
void LabelConnRgn(Mat& src, int nConn)
{
	int se[3][3] = { { 1, 1, 1 }, { 1, 1, 1 }, { 1, 1, 1 } };//8连通  
	if (nConn == 4)
	{
		se[0][0] = -1;
		se[0][2] = -1;
		se[2][0] = -1;
		se[2][2] = -1;
	}
	int nConnRgn = 254;//连通分量的标记号  
	Mat dst = Mat::zeros(src.size(), src.type());
	for (int i = 0; i < src.rows; i++)
	{

		uchar* srcData = src.ptr<uchar>(i);
		uchar* dstData = dst.ptr<uchar>(i);
		for (int j = 0; j < src.cols; j++)
		{
			if (srcData[j] == 255)
			{
				dstData[j] = 255;
				while (true)
				{
					Mat temp;
					dst.copyTo(temp);
					dilate(dst, dst, se[3][3]);
					dst = dst & src;
					//如果和上一次处理后的图像相同,说明连通区域已经提取完毕  
					if (dst.data == temp.data) 
					{
						break;
					}
				}
				//标注刚刚找到的连通区域  
				for (int k = 0; k < src.rows; k++)
				{
					uchar* srcData = src.ptr<uchar>(k);
					uchar* dstData = dst.ptr<uchar>(k);
					for (int l = 0; l < src.cols; l++)
					{
						if (dstData[l] == 255)
						{
							//标记原图像上的连通区域  
							srcData[l] = nConnRgn;
						}
					}
				}
				nConnRgn -= 50;//连通区域编号加1(此处为了显示的方便实际为:nConnRgn--;)  
				if (nConnRgn <= 0)
				{
					cout << "连通区域大于254个连通区域" << endl;
					i = src.rows;//强制跳出外循环  
					break;
				}
			}
		}
	}
}
int main()
{
	Mat src = imread("C:\\Users\\wj257\\Desktop\\vs2019\\Project2\\1.jpg", 0);
	imshow("原始图像", src);
	LabelConnRgn(src, 4);
	imshow("连通区域", src);
	waitKey(0);
	return 0;
}
  • 1
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值