前言
上篇《C++ OpenCV自适应阈值Canny边缘检测》中,使用的求中值的方式来获取自适应阈值,有小伙伴留言说一般用大津法OTSU来求自适应阈值,所以这篇就来说说大津法,及两个效果的对比。
实现效果
从上图中可以看出,除了书的那张图两个求出的阈值是完全一样,效果也一样,用大津(OTSU)法的阈值效果会更完整一些,原来的中值过滤掉的东西会更多一些。最后一张手机比较明显。
大津法简介
微卡智享
大津法(OTSU)又名最大类间差法,由日本学者大津于1979年提出。被认为是图像分割中阈值选取的最佳算法,计算简单,不受图像亮度和对比度的影响。
大津法是按图像的灰度特征,把图像分成前景和背景两部分。因方差是灰度分布均匀性的一种度量,背景和前景之间的类间方差越大,说明构成图像的两部分的差别越大,当部分前景错分为背景或部分背景错分为前景都会导致两部分差别变小。因此使用类间方差最大的分割意味着错分概率最小。
对于图像I(x,y),前景(即目标)和背景的分割阈值记作T,
属于前景的像素点数占整幅图像的比例记为ω0,其平均灰度μ0;
背景像素点数占整幅图像的比例为ω1,其平均灰度为μ1。
图像的总平均灰度记为μ,类间方差记为g。
假设图像的背景较暗,并且图像的大小为M×N,
图像中像素的灰度值小于阈值T的像素个数记作N0,
像素灰度大于阈值T的像素个数记作N1,则有:
ω0=N0/ M×N (1)
ω1=N1/ M×N (2)
N0+N1=M×N (3)
ω0+ω1=1 (4)
μ=ω0*μ0+ω1*μ1 (5)
g=ω0(μ0-μ)^2+ω1(μ1-μ)^2 (6)
将式(5)代入式(6),得到等价公式:
g=ω0ω1(μ0-μ1)^2 (7) 这就是类间方差
采用遍历的方法得到使类间方差g最大的阈值T,即为所求。
代码实现
微卡智享
大津法主要函数前半部分和上一篇中求中值的是一样,后半部分就要去计算前景和背景的比例后,再求出类间方差。
核心代码
//使用大津法Mat的阈值
int CvUtils::GetMatOTSU(Mat& img)
{
//判断如果不是单通道直接返回128
if (img.channels() > 1) return 128;
int rows = img.rows;
int cols = img.cols;
//定义数组
float mathists[256] = { 0 };
//遍历计算0-255的个数
for (int row = 0; row < rows; ++row) {
for (int col = 0; col < cols; ++col) {
int val = img.at<uchar>(row, col);
mathists[val]++;
}
}
//定义灰度级像素在整个图像中的比例
float grayPro[256] = { 0 };
int matSize = rows * cols;
for (int i = 0; i < 256; ++i) {
grayPro[i] = (float)mathists[i] / (float)matSize;
}
//大津法OTSU,前景与背景分割,计算出方差最大的灰度值
int calcval;
int calcMax = 0;
for (int i = 0; i < 256; ++i) {
float w0 = 0, w1 = 0, u0tmp = 0, u1tmp = 0, u0 = 0, u1 = 0, u = 0, calctmp = 0;
for (int k = 0; k < 256; ++k) {
float curGray = grayPro[k];
//计算背景部分
if (k <= i) {
//以i为阈值分类,第一类总的概率
w0 += curGray;
u0tmp += curGray * k;
}
//计算前景部分
else {
//以i为阈值分类,第一类总的概率
w1 += curGray;
u1tmp += curGray * k;
}
}
//求出第一类和第二类的平均灰度
u0 = u0tmp / w0;
u1 = u1tmp / w1;
//求出整幅图像的平均灰度
u = u0tmp + u1tmp;
//计算类间方差
calctmp = w0 * pow((u0 - u), 2) + w1 * pow((u1 - u), 2);
//更新最大类间方差,并设置阈值
if (calctmp > calcMax) {
calcMax = calctmp;
calcval = i;
}
}
return calcval;
}
为了做一下两个自适应阈值的对比,把原来的调用方法做了一下改造,加入一个参数来判断调用的什么方法。
//求自适应阈值的最小和最大值
void CvUtils::GetMatMinMaxThreshold(Mat& img, int& minval, int& maxval, int calctype, float sigma)
{
int midval;
switch (calctype)
{
case 1: {
midval = GetMatOTSU(img);
break;
}
default:
midval = GetMatMidVal(img);
break;
}
cout << "midval:" << midval << endl;
// 计算低阈值
minval = saturate_cast<uchar>((1.0 - sigma) * midval);
//计算高阈值
maxval = saturate_cast<uchar>((1.0 + sigma) * midval);
}
上图中可以看出,还是上一篇中那几个图,倒数第二张求的结果是一致的,有个别差异还挺大,总结来说,保留边缘的完整性上大津法的效果要好很多,中值里面过滤掉的会更多一些。
完