博主联系方式:
QQ:1540984562
QQ交流群:892023501
群里会有往届的smarters和电赛选手,群里也会不时分享一些有用的资料,有问题可以在群里多问问。
阈值处理详解
- 基础:
- 基于全局的阈值处理
- 1迭代算法(最小概率误判)
- 2基于Otsu最佳全局阈值方法(非常有效)
- 3用图像平滑改善全局阈值处理
- 4利用边缘改进全局阈值处理
- 基于局部的阈值处理
- 1图像分块可变阈值处理
- 2基于局部图像特性的可变阈值处理
- 3基于移动平均法的可变阈值
基础:
首先将灰度图转化成灰度直方图,横坐标是灰度值(0-255),纵坐标是像素个数。(归一化之后表征的是像素出现的概率)
如下图所示:
灰度直方图性质:
两幅灰度直方图
如图,从图A可以看出,直方图有两个明显的波峰和一个明显的波谷,表明灰度普遍分为两个密集区域。此时将门限设置在两者之间的波谷,则可以很好地分割出背景和物体。
同理,观察图B,有三个明显的波峰和两个明显的波谷,此时可以设置双门限,将图像分割为三类,如下图冰山就是很好的例子,分割为暗背景、冰山的明亮区域和阴影区域。
然而并不是所有图像的直方图都是有明显的多个波峰和波谷的。
单峰型:
无明显波谷型
灰度趋于一致型(被噪声污染过)
灰度阈值取决于波谷的宽度和深度,影响波谷特性的关键因素有:
1、波峰的间隔(波峰离得越远,分离这些模式机会越好)
2、图像中的噪声内容(模式随噪声的增加而展宽)
3、物体和背景的相对尺寸
4、光源的均匀性
5、图像反射的均匀性
接下来的所有的阈值处理方法,其目的都是:将灰度直方图变得好处理 并 找到分割背景和物体的门限灰度值。
基于全局的阈值处理
1迭代算法(最小概率误判)
公式推导:
算法步骤:
代码实现:
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为20Iteration(&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)
公式推导
从一篇博客截来的图,罗列了我们要计算的变量。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;
}
效果:
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; //每一小块的heightint width = (*srcImage).cols/ areas_of_W; //每一小块的widthint 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;
}
全局大津阈值效果
局部阈值法:1*2分割
迭代阈值法:
看来仍然需要改进
2基于局部图像特性的可变阈值处理
算法步骤:
1、计算以某一像素为中心的邻域的灰度标准差和均值
2、设定可变阈值
3、观察是否满足阈值条件
4、二值化
其中a和b都是需要人工整定。
效果图:
3基于移动平均法的可变阈值
有关的链接:(这个算法我还没有理解,等我理解了再来补充)
https://blog.csdn.net/qq_34510308/article/details/93162142