一、二值化
关于二值化的介绍,以前的博客中有介绍,这里就不再描述了,二值化介绍;二值化分为固定阈值二值化和自适应阈值二值化,固定阈值二值化方式是我们常用的二值化方式,需要自己摸索一个经验阈值,不断调整,直到找到最佳阈值,这种方式在刚刚的链接中已经介绍;而这篇文档主要介绍的就是另一种二值化方式:自适应阈值二值化。
二、自适应阈值二值化
图像进行二值化,且做到自适应阈值参数,有4种自适应阈值二值化方法;先从自适应阈值的作用范围来区分,自适应阈值分为:
- 全局阈值
使用自适应全局阈值的全局二值化方法有:大津法图像二值化、三角法图像二值化;
- 局部阈值
使用自适应局部阈值的局部二值化方法有:局部均值处理、局部高斯处理;
三、大津法图像二值化
OTSU算法也称最大类间差法,有时也称之为大津算法,由大津于1979年提出,被认为是图像分割中阈值选取的最佳算法,计算简单,不受图像亮度和对比度的影响,因此在数字图像处理上得到了广泛的应用。它是按图像的灰度特性,将图像分成背景和前景两部分。因方差是灰度分布均匀性的一种度量,背景和前景之间的类间方差越大,说明构成图像的两部分的差别越大,当部分前景错分为背景或部分背景错分为前景都会导致两部分差别变小。因此,使类间方差最大的分割意味着错分概率最小。
大津法二值化适用于图像直方图中存在双峰的图像(直方图中的双峰就是指背景像素和前景像素),最佳阈值就是双峰之间的某个参数,即将背景像素和前景像素分割开,其原理就是最大类间方差法。
大津法二值化的大致算法思路如下:
- 计算灰度图像的直方图,计算出0 - 255 每个像素值所占的像素个数;
- 遍历阈值 0 - 255,小于或等于阈值的像素为背景,大于阈值的像素为前景;
- 计算背景像素个数所占总像素个数的比例、背景像素的平均值;
- 计算前景像素个数所占总像素个数的比例、前景像素的平均值;
- 计算类间方差或类内方差,当使类间方差最大或者使类内方差最小的阈值,即为最佳阈值;
- 使用最佳阈值,对图像进行二值化处理。
思路细化:
- 图像宽[w],图像高度[h],灰度阈值[T];遍历T,从0 - 255 ;
- 小于阈值T的像素个数[c0],c0为背景像素个数;大于阈值T的像素个数[c1],c1为前景像素个数;c0 + c1 = w * h ;
- 背景像素个数占总像素个数的比例[w0],w0 = c0 / (w * h) ; 前景像素个数占总像素个数的比例[w1],w1 = c1 / (w * h) ; 且w0 + w1 = 1 ;
- 背景的平均像素灰度值[u0],u0 = c0个背景像素灰度值之和 / c0 ;前景的平均像素灰度值[u1],u1 = c1个前景像素灰度值之和 / c1 ;
- 整张图像的像素灰度平均值[u],u = (c0个背景像素灰度值之和 + c1个前景像素灰度值之和) / (w * h) ;
- 类间方差[g],g = w0 * (u0 - u)^2 + w1 * (u1 - u)^2 ;类间方差指的是前景和背景之间的差异,显然该差异越大,说明分离度越好。
- 根据第(6)步,推导后类间方差g = w0 * w1 * (u0 - u1) ^ 2 ;
- 找到最大类间方差对应的灰度阈值T,即是最佳阈值。
除了最大类间方差,也可以通过计算最小类内方差来得到最佳阈值,这里有篇博客介绍到:链接
对于一些噪声较多的图像,可以先使用高斯滤波去噪,再用大津法对图像进行二值化,这样会使二值化的图像效果更好
OpenCV中有大津法二值化的接口:
double threshold(InputArray src, OutputArray dst, double thresh, double maxVal, int thresholdType)
将第5个参数 thresholdType 设置成 THRESH_OTSU 即可,可以将THRESH_OTSU 和THRESH_BINARY等类型配合使用;当使用了THRESH_OTSU,函数threshold()返回值即是找到的最佳阈值,且函数中第三个参数thresh将不起作用。
最大类间方差法 实现代码:
//二值化处理,自适应阈值 大津法
int Binarization::BinaryProcessing_OTSU(Mat& srcImg)
{
//【1】安全性检查
if (!srcImg.data || srcImg.data == NULL)
{
cout << "BinaryProcessing_OTSU() --> srcImg读取失败" << endl;
return MTC_FAIL;
}
//【2】图像灰度化
Mat grayImg;
cvtColor(srcImg, grayImg, CV_BGR2GRAY);
//【3】获取最佳二值化阈值
int nBestTH = 0;
int nRet = GetBestTH_OTSU(grayImg, nBestTH);
if (nRet != MTC_SUCCESS)
{
cout << "BinaryProcessing_OTSU() --> 获取最佳二值化阈值 失败" << endl;
return MTC_FAIL;
}
cout << "BinaryProcessing_OTSU() --> 最佳二值化阈值 = " << nBestTH << endl;
//【4】图像二值化
Mat binaryImg;
threshold(grayImg, binaryImg, nBestTH, 255, CV_THRESH_BINARY);
//【5】显示图像
imshow("二值化图像", binaryImg);
return MTC_SUCCESS;
}
//获取最佳阈值,自适应阈值 大津法(最大类间差法)
int Binarization::GetBestTH_OTSU(Mat& grayImg, int& nBestTH)
{
//【1】安全性检查
if (!grayImg.data || grayImg.data == NULL)
{
cout << "GetBestTH_OTSU() --> grayImg读取失败" << endl;
return MTC_FAIL;
}
if (grayImg.channels() != 1)
{
cout << "GetBestTH_OTSU() --> grayImg不是灰度图像" << endl;
return MTC_FAIL;
}
//【2】参数准备
double sum = 0.0; //所有像素灰度之和
double w0 = 0.0; //背景像素所占比例
double w1 = 0.0; //前景像素所占比例
double u0_temp = 0.0;
double u1_temp = 0.0;
double u0 = 0.0; //背景平均灰度
double u1 = 0.0; //前景平均灰度
double delta_temp = 0.0; //类间方差
double delta_max = 0.0; //最大类间方差
const int GrayScale = 256;
//src_image灰度级
int pixel_count[GrayScale] = { 0 }; //每个灰度级的像素数目
float pixel_pro[GrayScale] = { 0 }; //每个灰度级的像素数目占整幅图像的比例
int height = grayImg.rows;
int width = grayImg.cols;
//统计每个灰度级中像素的个数
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
int index = i * width + j;
pixel_count[(int)grayImg.data[index]]++; //每个灰度级的像素数目
sum += (int)grayImg.data[index]; //灰度之和
}
}
cout << "平均灰度:" << sum / (height * width) << endl;
//计算每个灰度级的像素数目占整幅图像的比例
int imgArea = height * width;
for (int i = 0; i < GrayScale; i++)
{
pixel_pro[i] = (float)pixel_count[i] / imgArea;
}
//遍历灰度级[0,255],寻找合适的threshold
for (int i = 0; i < GrayScale; i++)
{
w0 = w1 = u0_temp = u1_temp = u0 = u1 = delta_temp = 0;
for (int j = 0; j < GrayScale; j++)
{
if (j <= i) //背景部分
{
w0 += pixel_pro[j]; //背景像素比例
u0_temp += j * pixel_pro[j];
}
else //前景部分
{
w1 += pixel_pro[j]; //前景像素比例
u1_temp += j * pixel_pro[j];
}
}
u0 = u0_temp / w0; //背景像素点的平均灰度
u1 = u1_temp / w1; //前景像素点的平均灰度
delta_temp = (float)(w0 * w1 * pow((u0 - u1), 2)); //类间方差 g=w0*w1*(u0-u1)^2
//当类间方差delta_temp最大时,对应的i就是阈值T
if (delta_temp > delta_max)
{
delta_max = delta_temp;
nBestTH = i;
}
}
return MTC_SUCCESS;
}
OpenCV接口,实现代码:
//二值化处理,自适应阈值 大津法 opencv自带接口
int Binarization::BinaryProcessing_OTSU_OpenCV(Mat& srcImg)
{
//【1】安全性检查
if (!srcImg.data || srcImg.data == NULL)
{
cout << "BinaryProcessing_OTSU() --> srcImg读取失败" << endl;
return MTC_FAIL;
}
//【2】图像灰度化
Mat grayImg;
cvtColor(srcImg, grayImg, CV_BGR2GRAY);
//【3】图像二值化
Mat binaryImg;
double dBestTH = threshold(grayImg, binaryImg, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU); //CV_THRESH_OTSU
cout << "BinaryProcessing_OTSU_OpenCV() --> dBestTH = " << dBestTH << endl;
//【4】显示图像
imshow("二值化图像-opencv", binaryImg);
return MTC_SUCCESS;
}
四、三角法图像二值化
三角法二值化,适用于图像直方图中存在单峰的图像,这是一种纯几何的方法来寻找最佳阈值,它的成立条件是假设直方图最大波峰在靠近最亮的一侧,然后通过三角形求得最大直线距离,根据最大直线距离对应的直方图灰度等级即为分割阈值,图示如下:
在直方图上从最高峰处bmx到最暗对应直方图bmin(p=0)%构造一条直线,从bmin处开始计算每个对应的直方图b到直线的垂直距离,直到bmax为止,其中最大距离对应的直方图位置即为图像二值化对应的阈值T。
三角法二值化算法步骤:
(1)图像转灰度
(2)计算图像灰度直方图
(3)寻找直方图中两侧边界
(4)寻找直方图最大值
(5)检测是否最大波峰在亮的一侧,否则翻转
(6)求解到直线的最大值
设灰度级别为L,频率为α,当频率αmax最大的时候设L=L_αmax,当Lmin时,α=α_Lmin
- 求解直线方程:根据点(Lmin,α_Lmin)和点(L_αmax,αmax)可以确定直线l的方程;
- 求解各点到直线的距离:各点(L,α)到直线l的距离d,根据点到直线的距离公式可以求得,用一个列表去存放所有的距离d,然后利用max函数即可求得dmax;
- 找到当点(L,α)到直线l的距离d最大时,灰度级别L的值即为最佳阈值;
(7)确定最佳阈值T,如果翻转则最佳阈值为255 - T
(8)使用最佳阈值,对图像进行二值化处理。
OpenCV中有三角法二值化的接口:
double threshold(InputArray src, OutputArray dst, double thresh, double maxVal, int thresholdType)
将第5个参数 thresholdType 设置成 THRESH_TRIANGLE即可,可以将THRESH_TRIANGLE 和THRESH_BINARY等类型配合使用;当使用了THRESH_TRIANGLE,函数threshold()返回值即是找到的最佳阈值,且函数中第三个参数thresh将不起作用。
几何法 实现代码:
//二值化处理,自适应阈值 三角法
int Binarization::BinaryProcessing_Triangle(Mat& srcImg)
{
//【1】安全性检查
if (!srcImg.data || srcImg.data == NULL)
{
cout << "BinaryProcessing_Triangle() --> srcImg读取失败" << endl;
return MTC_FAIL;
}
//【2】图像灰度化
Mat grayImg;
cvtColor(srcImg, grayImg, CV_BGR2GRAY);
//【3】获取最佳二值化阈值
int nBestTH = 0;
int nRet = GetBestTH_Triangle(grayImg, nBestTH);
if (nRet != MTC_SUCCESS)
{
cout << "BinaryProcessing_Triangle() --> 获取最佳二值化阈值 失败" << endl;
return MTC_FAIL;
}
cout << "BinaryProcessing_Triangle() --> 最佳二值化阈值 = " << nBestTH << endl;
//【4】图像二值化
Mat binaryImg;
threshold(grayImg, binaryImg, nBestTH, 255, CV_THRESH_BINARY);
//【5】显示图像
imshow("二值化图像", binaryImg);
return MTC_SUCCESS;
}
//获取最佳阈值,自适应阈值 三角法
int Binarization::GetBestTH_Triangle(Mat& grayImg, int& nBestTH)
{
//【1】安全性检查
if (!grayImg.data || grayImg.data == NULL)
{
cout << "GetBestTH_Triangle() --> grayImg读取失败" << endl;
return MTC_FAIL;
}
if (grayImg.channels() != 1)
{
cout << "GetBestTH_Triangle() --> grayImg不是灰度图像" << endl;
return MTC_FAIL;
}
//【2】参数准备
const int GrayScale = 256;
int pixel_count[GrayScale] = { 0 }; //每个灰度级的像素数目
int height = grayImg.rows;
int width = grayImg.cols;
int left_bound = 0; //最左边零的位置
int right_bound = 0; //最右边零的位置
int max_mid = 0; //像素数量最多的灰度级位置
bool bIsFlipped = false; //是否将直方图左右翻转
//【3】统计每个灰度级的像素数目
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
int index = i * width + j;
pixel_count[grayImg.data[index]]++;
}
}
//【4】找到最左边零的位置
for (int i = 0; i < GrayScale; i++)
{
if (pixel_count[i] > 0)
{
left_bound = i;
break;
}
}
//位置再移动一个步长,即为最左侧零位置
if (left_bound > 0)
left_bound--;
//【5】找到最右边零的位置
for (int i = GrayScale - 1; i >= 0; i--)
{
if (pixel_count[i] > 0)
{
right_bound = i;
break;
}
}
//位置再移动一个步长,即为最右侧零位置
if (right_bound < GrayScale - 1)
right_bound++;
//【6】找到像素数量最多的灰度级位置
int maxNum = 0;
for (int i = 0; i < GrayScale; i++)
{
if (pixel_count[i] > maxNum)
{
maxNum = pixel_count[i];
max_mid = i;
}
}
//【7】如果最大值(max_mid)位置落在靠左侧这样就无法满足三角法求阈值,所以要检测是否最大值(max_mid)位置是否靠近左侧
//如果靠近左侧则通过翻转到右侧位置
if (max_mid - left_bound < right_bound - max_mid)
{
int i = 0;
int j = GrayScale - 1;
int temp = 0;
while (i < j)
{
temp = pixel_count[i];
pixel_count[i] = pixel_count[j];
pixel_count[j] = temp;
i++;
j--;
}
bIsFlipped = true;
left_bound = GrayScale - 1 - right_bound;
max_mid = GrayScale - 1 - max_mid;
}
//【8】计算求得阈值
nBestTH = left_bound;
int a = maxNum;
int b = left_bound - max_mid;
float maxDist = 0;
for (int i = left_bound + 1; i <= max_mid; i++)
{
//计算距离(点到直线的距离 (Ax + Bx + C) / 根号[A的平方 + B的平方]
//因为只有 Ax+Bx 是变化的,而我们的目的是比较距离大小,所以只计算 Ax+Bx 的值)
float tempDist = a * i + b * pixel_count[i];
if (tempDist > maxDist)
{
maxDist = tempDist;
nBestTH = i;
}
}
nBestTH--;
//【9】对已经得到的最佳阈值,如果前面已经翻转了,则阈值要用 255 - nBestTH
if (bIsFlipped)
nBestTH = GrayScale - 1 - nBestTH;
return MTC_SUCCESS;
}
OpenCV接口,实现代码:
//二值化处理,自适应阈值 三角法 opencv自带接口
int Binarization::BinaryProcessing_Triangle_OpenCV(Mat& srcImg)
{
//【1】安全性检查
if (!srcImg.data || srcImg.data == NULL)
{
cout << "BinaryProcessing_Triangle_OpenCV() --> srcImg读取失败" << endl;
return MTC_FAIL;
}
//【2】图像灰度化
Mat grayImg;
cvtColor(srcImg, grayImg, CV_BGR2GRAY);
//【3】图像二值化
Mat binaryImg;
double dBestTH = threshold(grayImg, binaryImg, 0, 255, CV_THRESH_BINARY | CV_THRESH_TRIANGLE); //CV_THRESH_TRIANGLE
cout << "BinaryProcessing_Triangle_OpenCV() --> dBestTH = " << dBestTH << endl;
//【4】显示图像
imshow("二值化图像-opencv", binaryImg);
return MTC_SUCCESS;
}
五、自适应局部阈值图像二值化
全局阈值图像二值化 只可以对整张图像使用同一个阈值进行二值化,如果图像中亮度分布不均匀,每个区域亮度都有差别,那么再使用全局阈值图像二值化,会导致部分信息缺失。
而自适应局部阈值化能够根据图像不同区域亮度分布,来改变阈值。
OpenCV中集成了这样的方法,接口如下:
void adaptiveThreshold(InputArray src, OutputArray dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C)
参数介绍:
src参数 表示输入图像(8位单通道图像);
maxValue参数 表示使用 THRESH_BINARY 和 THRESH_BINARY_INV 的最大值;
adaptiveMethod参数 表示自适应阈值算法,平均 (ADAPTIVE_THRESH_MEAN_C)或高斯(ADAPTIVE_THRESH_GAUSSIAN_C);
thresholdType参数表示阈值类型,必须为THRESH_BINARY或THRESH_BINARY_INV的阈值类型;
blockSize参数 表示块大小(奇数且大于1,比如3,5,7........ );
C参数是常数,表示从平均值或加权平均值中减去的数。通常情况下,这是正值,但也可能为零或负值。
(1)局部均值法图像二值化
将参数adaptiveMethod 设置为ADAPTIVE_THRESH_MEAN_C,自适应阈值T(x, y),通过计算像素(x, y)周围blockSize x blockSize大小像素块的平均值并减去常量 C 得到。
(2)局部高斯处理图像二值化
将参数adaptiveMethod 设置为ADAPTIVE_THRESH_GAUSSIAN_C,自适应阈值T(x, y),通过计算像素(x, y)周围blockSize x blockSize大小像素块的加权求和(与高斯窗口相关)并减去常量 C 得到。
如果使用平均的方法,则所有像素周围的权值相同;
如果使用高斯的方法,则每个像素周围像素的权值则根据其到中心点的距离通过高斯方程得到。
OpenCV接口 实现代码:
//自适应阈值二值化 均值
int Binarization::AdaptiveThreshold_Mean(Mat& srcImg)
{
//【1】安全性检查
if (!srcImg.data || srcImg.data == NULL)
{
cout << "AdaptiveThreshold_Mean() --> srcImg读取失败" << endl;
return MTC_FAIL;
}
//【2】图像灰度化
Mat grayImg;
cvtColor(srcImg, grayImg, CV_BGR2GRAY);
//【3】自适应阈值二值化
Mat binaryImg;
adaptiveThreshold(grayImg, binaryImg, 255, CV_ADAPTIVE_THRESH_MEAN_C, CV_THRESH_BINARY, 11, 2);
//【4】显示图像
imshow("二值化图像", binaryImg);
return MTC_SUCCESS;
}
//自适应阈值二值化 高斯
int Binarization::AdaptiveThreshold_GAUSSIAN(Mat& srcImg)
{
//【1】安全性检查
if (!srcImg.data || srcImg.data == NULL)
{
cout << "AdaptiveThreshold_GAUSSIAN() --> srcImg读取失败" << endl;
return MTC_FAIL;
}
//【2】图像灰度化
Mat grayImg;
cvtColor(srcImg, grayImg, CV_BGR2GRAY);
//【3】自适应阈值二值化
Mat binaryImg;
adaptiveThreshold(grayImg, binaryImg, 255, CV_ADAPTIVE_THRESH_GAUSSIAN_C, CV_THRESH_BINARY, 11, 2);
//【4】显示图像
imshow("二值化图像", binaryImg);
return MTC_SUCCESS;
}
总结
(1)大津法的优点在于可以快速有效的找到类间分割阈值,但其缺点也很明显,就是只能针对单一目标分割,或者感兴趣的目标都属于同一灰度范围,若需探测目标灰度范围分布较大,则必将有一部分目标探测丢失。
(2)局部分割的优点在于可以进行多目标分割,缺点在于基于局部阈值分割出的目标连结性较差,包含噪声。