图像分割-阈值处理详解(迭代法、Otsu法、平滑改善法、边缘改进法、分块处理法、局部特性法、移动平均法)

博主联系方式:
QQ:1540984562
QQ交流群:892023501
群里会有往届的smarters和电赛选手,群里也会不时分享一些有用的资料,有问题可以在群里多问问。

基础:

首先将灰度图转化成灰度直方图,横坐标是灰度值(0-255),纵坐标是像素个数。(归一化之后表征的是像素出现的概率)
如下图所示:
灰度直方图性质:
灰度直方图性质
两幅灰度直方图两幅灰度直方图
如图,从图A可以看出,直方图有两个明显的波峰和一个明显的波谷,表明灰度普遍分为两个密集区域。此时将门限设置在两者之间的波谷,则可以很好地分割出背景和物体。
同理,观察图B,有三个明显的波峰和两个明显的波谷,此时可以设置双门限,将图像分割为三类,如下图冰山就是很好的例子,分割为暗背景、冰山的明亮区域和阴影区域。
冰山
然而并不是所有图像的直方图都是有明显的多个波峰和波谷的。
单峰型:
单峰型

无明显波谷型
无明显波谷型

灰度趋于一致型(被噪声污染过)
灰度趋于一致型

灰度阈值取决于波谷的宽度和深度,影响波谷特性的关键因素有:
1、波峰的间隔(波峰离得越远,分离这些模式机会越好)
2、图像中的噪声内容(模式随噪声的增加而展宽)
3、物体和背景的相对尺寸
4、光源的均匀性
5、图像反射的均匀性

接下来的所有的阈值处理方法,其目的都是:将灰度直方图变得好处理 并 找到分割背景和物体的门限灰度值

基于全局的阈值处理

1迭代算法(最小概率误判)

公式推导:
1
2
3
4
算法步骤:
分割前提基于全局的迭代算法
代码实现:

void Iteration(Mat* srcImage, Mat* dstImage, float delta_T)
{
	//【1】求最大灰度和最小灰度
	byte max_his = 0;
	byte min_his = 255;
	int height = (*srcImage).rows;
	int width = (*srcImage).cols;
	for (int j = 0;j < height;j++)
	{
		for (int i = 0;i < width;i++)
		{
			if ((*srcImage).at<uchar>(j, i) > max_his)
			{
				max_his = (*srcImage).at<uchar>(j, i);
			}
			if ((*srcImage).at<uchar>(j, i) < min_his)
			{
				min_his = (*srcImage).at<uchar>(j, i);
			}
		}
	}
	float T = 0.5 * (max_his+ min_his);
	float m1 = 255;		//当m1 m2都取0时,会有错误
	float m2 = 0;
	float old_T = T;
	float new_T = 0.5 * (m1 + m2);
	int times = 10;
	//while (times--)
	while (abs(new_T - old_T) > delta_T)
	{
		int G1 = 0;
		int G2 = 0;
		int timer_G1 = 0;
		int timer_G2 = 0;
		for (int j = 0;j < height;j++)
		{
			for (int i = 0;i < width;i++)
			{
				if ((*srcImage).at<uchar>(j, i) > old_T)
				{
					G1 += (*srcImage).at<uchar>(j, i);
					timer_G1++;
				}
				else
				{
					G2 += (*srcImage).at<uchar>(j, i);
					timer_G2++;
				}
			}
		}
		m1 = G1 * 1.0f / timer_G1;
		m2 = G2 * 1.0f / timer_G2;
		old_T = new_T;
		new_T = 0.5 * (m1 + m2);
	}
	cout << "迭代方法阈值为:" << new_T << endl;	
	//根据得出的阈值二值化图像
	for (int j = 0;j < height;j++)
	{
		for (int i = 0;i < width;i++)
		{
			if ((*srcImage).at<uchar>(j, i) > new_T)
			{
				(*dstImage).at<uchar>(j, i) = 255;

			}
			else
			{
				(*dstImage).at<uchar>(j, i) = 0;

			}
		}
	}
}
int main()
{
		Mat srcImage = imread("D:\\opencv_picture_test\\新垣结衣\\test2.jpg", 0);	//读入的时候转化为灰度图
		namedWindow("原始图", WINDOW_NORMAL);//WINDOW_NORMAL允许用户自由伸缩窗口
		imshow("原始图", srcImage);
	
		Mat dstImage;
		dstImage.create(srcImage.rows, srcImage.cols, CV_8UC1);
		double time0 = static_cast<double>(getTickCount());	//记录起始时间
		//阈值处理+二值化
		//My_P_tile(&srcImage,&dstImage,20);			//设P为20
		Iteration(&srcImage, &dstImage,0.02);
		//一系列处理之后
		time0 = ((double)getTickCount() - time0) / getTickFrequency();
		//cout << "此方法运行时间为:" << time0 << "秒" << endl;	//输出运行时间
		namedWindow("效果图", WINDOW_NORMAL);//WINDOW_NORMAL允许用户自由伸缩窗口
		imshow("效果图", dstImage);
		dstImage = My_Rraw_histogram(&srcImage);
		namedWindow("一维直方图", WINDOW_NORMAL);//WINDOW_NORMAL允许用户自由伸缩窗口
		imshow("一维直方图", dstImage);
		waitKey(0);
		return 0;
}

当直方图存在比较明显的波谷时,这种方法是比较好的。δT控制迭代次数,下面是代码实现效果
原图
二值图
一维直方图
阈值

2基于Otsu最佳全局阈值方法(非常有效)

大津法又叫最大类间方差法、最大类间阈值法(OTSU)。
它的基本思想是,用一个阈值将图像中的数据分为两类,
一类中图像的像素点的灰度均小于这个阈值,另一类中的图像的像素点的灰度均大于或者等于该阈值。 //一般来说使用遍历的方法来求
如果这两个类中像素点的灰度的方差越大,说明获取到的阈值就是最佳的阈值
(方差是灰度分布均匀性的一种度量,背景和前景之间的类间方差越大,说明构成图像的两部分的差别越大,当部分前景错分为背景或部分背景错分为前景都会导致两部分差别变小。因此,使类间方差最大的分割意味着错分概率最小。)。
则利用该阈值可以将图像分为前景和背景两个部分。
而我们所感兴趣的部分一般为前景。
对于灰度分布直方图有两个峰值的图像,大津法求得的T近似等于两个峰值之间的低谷。
(这段阐述转自这里https://www.jianshu.com/p/56b140f9535a)
公式推导
1
2
3
从一篇博客截来的图,罗列了我们要计算的变量。https://blog.csdn.net/u012198575/article/details/81128799
需要计算的变量

代码实现

void My_Ostu(Mat* srcImage, Mat* dstImage)
{
	int height = (*srcImage).rows;
	int width = (*srcImage).cols;
	int Ostu_Threshold = 0; //大津阈值
	int size = height * width;
	float variance;   //类间方差
	float maxVariance = 0, w1 = 0, w2 = 0, avgValue = 0;
	float u0 = 0, u1 = 0, u2 = 0;
	//生成灰度直方图
	int pixels[256];
	float histgram[256];
	for (int i = 0; i < 256; i++)
	{
		pixels[i] = 0;
	}
	for (int j = 0; j < height; j++)
	{
		for (int i = 0; i < width; i++) 
		{
			pixels[(*srcImage).at<uchar>(j, i)]++;
		}
	}
	for (int i = 0; i < 256; i++)
	{
		histgram[i] = pixels[i] * 1.0f / size;
	}
	//遍历找出类间方差最大(maxVariance)的阈值(Ostu_Threshold)
	for (int i = 0;i <= 255;i++)
	{
		w1 = 0;
		w2 = 0;
		u1 = 0;
		u2 = 0;
		//计算背景像素占比,平均灰度
		for (int j = 0;j <= i;j++)
		{
			w1 += histgram[j];
			u1 += histgram[j] * j;
		}
		u1 = u1 / w1;
		//计算前景像素占比,平均灰度
		w2 = 1 - w1;
		if (i == 255)
		{
			u2 = 0;
		}
		else
		{
			for (int j = i + 1;j <= 255;j++)
			{
				u2 += histgram[j] * j;
			}
		}
		u2 = u2 / w2;
		//计算类间方差
		variance = w1 * w2 * (u1 - u2) * (u1 - u2);
		if (variance > maxVariance)
		{ //找到使灰度差最大的值
			maxVariance = variance;
			Ostu_Threshold = i;            //那个值就是阈值
		}
	}
	cout << "大津法阈值为:" << Ostu_Threshold << endl;
	//【3】二值化
	for (int j = 0; j < height; j++)
	{
		for (int i = 0; i < width; i++)
		{
			if ((*srcImage).at<uchar>(j, i) >= Ostu_Threshold)
			{
				(*dstImage).at<uchar>(j, i) = 255;
			}
			else
			{
				(*dstImage).at<uchar>(j, i) = 0;
			}

		}
	}
}
int main()
{
		Mat srcImage = imread("D:\\opencv_picture_test\\新垣结衣\\test2.jpg", 0);	//读入的时候转化为灰度图
		namedWindow("原始图", WINDOW_NORMAL);//WINDOW_NORMAL允许用户自由伸缩窗口
		imshow("原始图", srcImage);
	
		Mat dstImage;
		dstImage.create(srcImage.rows, srcImage.cols, CV_8UC1);
		double time0 = static_cast<double>(getTickCount());	//记录起始时间
		//阈值处理+二值化
		//My_P_tile(&srcImage,&dstImage,20);			//设P为20
		//My_Iteration(&srcImage, &dstImage,0.02);
		My_Ostu(&srcImage, &dstImage);
		//一系列处理之后
		time0 = ((double)getTickCount() - time0) / getTickFrequency();
		cout << "此方法运行时间为:" << time0 << "秒" << endl;	//输出运行时间
		namedWindow("效果图", WINDOW_NORMAL);//WINDOW_NORMAL允许用户自由伸缩窗口
		imshow("效果图", dstImage);
		dstImage = My_Rraw_histogram(&srcImage);
		namedWindow("一维直方图", WINDOW_NORMAL);//WINDOW_NORMAL允许用户自由伸缩窗口
		imshow("一维直方图", dstImage);
		waitKey(0);
		return 0;
}

效果:
1
2

3用图像平滑改善全局阈值处理

总的来说就是在二值化之前先用33或者55之类的均值模板将整个图像处理一下。
不过这样的坏处是使物体与背景的边界变得有些模糊。侵蚀越多,边界误差越大。
在某些极端情况下,这种方法效果并不好。

4利用边缘改进全局阈值处理

这种方法将关注聚焦于物体与背景的边缘像素,在边缘的灰度跳动非常明显,由此得到的灰度直方图将会得到很大的改善。
在这里我们求得边缘的方法主要是梯度算子和拉普拉斯算子。
算法步骤:
算法步骤
一般来说我们确定阈值T是根据,梯度最大值或者拉普拉斯最大值的某百分比来确定的。当有不同需求时,采用不同的占比。

基于局部的阈值处理

这种阈值处理的目的是为了解决光照和反射带来的问题。

1图像分块可变阈值处理

其实就是把一个图片分割为多块,分别使用大津阈值。分块处理
分块处理后的子图像直方图
子图像直方图
上面的是书上的样例,我把原图截下来,试了试自己写的代码,效果并不是很好。
代码实现:

void My_local_adaptive(Mat* srcImage, Mat* dstImage, int areas_of_H, int areas_of_W)		//局部自适应法   基于大津阈值areas_of_H:竖直方向分割的个数  areas_of_W:横坐标方向分割的个数
{
	int height = (*srcImage).rows/ areas_of_H;			//每一小块的height
	int width = (*srcImage).cols/ areas_of_W;			//每一小块的width
	int Ostu_Threshold = 0; //大津阈值
	int size = height * width/ areas_of_H/ areas_of_W;		//每一小块的size
	//一行一行地来
	for (int y = 0; y < areas_of_H; y++)	
	{
		for (int x = 0; x < areas_of_W; x++)
		{
			float variance = 0;   //类间方差
			float maxVariance = 0, w1 = 0, w2 = 0, avgValue = 0;
			float u0 = 0, u1 = 0, u2 = 0;
			//生成areas_of_W*areas_of_H个局部灰度直方图
			int pixels[256];
			float histgram[256];
			for (int i = 0; i < 256; i++)
			{
				pixels[i] = 0;
			}
			//【处理每个小区域并且二值化】
			//【计算直方图】
			for (int j = y* height; j < ((y + 1 == areas_of_H) ? (*srcImage).rows : (y + 1) * height); j++) //? : 是一个三目运算符,也是唯一的一个三目运算符。?前面表逻辑条件,:前面也就是?后面表示条件成立时的值,:后面表条件不成立时的值。例如,当a > b时,x = 1否则x = 0,可以写成x = a > b ? 1 : 0。
			{
				for (int i = x * width; i < ((x + 1 == areas_of_W) ? (*srcImage).cols : (x + 1) * width); i++)
				{
					pixels[(*srcImage).at<uchar>(j, i)]++;
				}
			}
			//【直方图归一化】
			for (int i = 0; i < 256; i++)
			{
				histgram[i] = pixels[i] * 1.0f / size;
			}
			//遍历找出类间方差最大(maxVariance)的阈值(Ostu_Threshold)
			for (int i = 0;i <= 255;i++)
			{
				w1 = 0;
				w2 = 0;
				u1 = 0;
				u2 = 0;
				//计算背景像素占比,平均灰度
				for (int j = 0;j <= i;j++)
				{
					w1 += histgram[j];
					u1 += histgram[j] * j;
				}
				u1=u1/w1;
				//计算前景像素占比,平均灰度
				w2 = 1 - w1;
				if (i == 255)
				{
					u2 = 0;
				}
				else
				{
					for (int j = i + 1;j <= 255;j++)
					{
						u2 += histgram[j] * j;
					}
				}
				u2=u2/w2;
				//计算类间方差
				variance = w1 * w2 * (u1 - u2) * (u1 - u2);
				if (variance > maxVariance)
				{ //找到使灰度差最大的值
					maxVariance = variance;
					Ostu_Threshold = i;            //那个值就是阈值
				}
			}
			cout << "大津法阈值为:" << Ostu_Threshold << endl;
			//【3】二值化
			for (int j = y * height; j < ((y + 1 == areas_of_H) ? (*srcImage).rows : (y + 1) * height); j++) //? : 是一个三目运算符,也是唯一的一个三目运算符。?前面表逻辑条件,:前面也就是?后面表示条件成立时的值,:后面表条件不成立时的值。例如,当a > b时,x = 1否则x = 0,可以写成x = a > b ? 1 : 0。
			{
				for (int i = x * width; i < ((x + 1 == areas_of_W) ? (*srcImage).cols : (x + 1) * width); i++)
				{
					if ((*srcImage).at<uchar>(j, i) >= Ostu_Threshold)
					{
						(*dstImage).at<uchar>(j, i) = 255;
					}
					else
					{
						(*dstImage).at<uchar>(j, i) = 0;
					}
				}
			}
		}
	}
}
int main()
{
		//Mat srcImage = imread("D:\\opencv_picture_test\\新垣结衣\\test2.jpg", 0);	//读入的时候转化为灰度图
		//Mat srcImage = imread("D:\\opencv_picture_test\\miku\\miku2.jpg", 0);	//读入的时候转化为灰度图
		Mat srcImage = imread("D:\\opencv_picture_test\\阈值处理\\带噪声阴影的图.png", 0);	//读入的时候转化为灰度图
		namedWindow("原始图", WINDOW_NORMAL);//WINDOW_NORMAL允许用户自由伸缩窗口
		imshow("原始图", srcImage);
	
		Mat dstImage;
		dstImage.create(srcImage.rows, srcImage.cols, CV_8UC1);
		double time0 = static_cast<double>(getTickCount());	//记录起始时间
		//阈值处理+二值化
		//My_P_tile(&srcImage,&dstImage,20);			//设P为20
		//My_Iteration(&srcImage, &dstImage,0.01);
		//My_Ostu(&srcImage, &dstImage);
		My_local_adaptive(&srcImage, &dstImage, 1, 2);
		//一系列处理之后
		time0 = ((double)getTickCount() - time0) / getTickFrequency();
		cout << "此方法运行时间为:" << time0 << "秒" << endl;	//输出运行时间
		namedWindow("效果图", WINDOW_NORMAL);//WINDOW_NORMAL允许用户自由伸缩窗口
		imshow("效果图", dstImage);
		dstImage = My_Rraw_histogram(&srcImage);
		namedWindow("一维直方图", WINDOW_NORMAL);//WINDOW_NORMAL允许用户自由伸缩窗口
		imshow("一维直方图", dstImage);
		waitKey(0);
		return 0;
}

全局大津阈值效果
all black
2
局部阈值法:1*2分割
2
![2](https://img-blog.csdnimg.cn/202003
迭代阈值法:
1
2
看来仍然需要改进

2基于局部图像特性的可变阈值处理

算法步骤:
1、计算以某一像素为中心的邻域的灰度标准差和均值
计算所需变量
2、设定可变阈值算法步骤
3、观察是否满足阈值条件算法步骤
4、二值化
阈值处理

其中a和b都是需要人工整定。
效果图:
效果图

3基于移动平均法的可变阈值

算法描述
有关的链接:(这个算法我还没有理解,等我理解了再来补充)
https://blog.csdn.net/qq_34510308/article/details/93162142

  • 25
    点赞
  • 149
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拾牙慧者

欢迎请作者喝奶茶

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值