1、二值化的介绍
二值,由字面意思即可得到,我们只希望图片转换为只有两种颜色的图像。因此,我们的处理方式即是对图片的像素值与阈值(分界点)进行比较,然后进行划分,此时阈值的选择就尤为重要。在进行阈值给定的时候,最为常见的有以下几种方式,人为给定阈值、大津法判断阈值、迭代法判断阈值等。在本文中,只记录大津法。
2、大津法的介绍
-
1、原理介绍
相关的原理、公式推导可以参考这篇博客:大津法(OTSU 最大类间方差法)详细数学推导 -
2、利用概率来实现最大类间方差的计算
void binaryOtsu(uint8_t** inArray, uint32_t width, uint32_t height)
{
// 得到总的像素个数
uint32_t pixelNum = width * height;
// 图像直方图,统计每个像素值出现的次数
uint32_t pixelCounts[256] = {0};
for (uint32_t i = 0; i < height; i++) {
for (uint32_t j = 0; j < width; j++) {
uint8_t pixelValue = inArray[i][j];
pixelCounts[pixelValue]++;
}
}
// 计算各个像素值出现的概率
double p[256] = {0}, u_Sum = 0;
for(int i = 0; i < 256; i++) {
p[i] = 1.0 * pixelCounts[i] / pixelNum;
u_Sum += p[i] * i;
}
// 变量定义、计算
int threshold = 0;
double w0 = 0, w1 = 1, u0 = 0, u1 = 0, u_SumTemp = 0, maxVariance = 0;
for(int i = 0; i < 256; i++) {
w0 += p[i]; // 计算前景概率,累加概率和
u_SumTemp += p[i] * i;
w1 = 1 - w0; // 计算背景概率
if(w0 == 0 || w1 == 0) // 防止分母为 0
continue;
u0 = u_SumTemp / w0; // 计算前景的数学期望
u1 = (u_Sum - u_SumTemp) / w1; // 计算背景的数学期望
// 计算最大类间方差
double variance = w0 * w1 * (u1 - u0) * (u1 - u0);
if(variance > maxVariance)
{
maxVariance = variance;
threshold = i;
}
}
cout << "Otsu 计算所得的 threshold = " << threshold << endl;
}
- 3、代码更改,减少数组空间与浮点型运算,经测试与 Matlab、Opencv 中的大津法算得的阈值相同。
/************************************************************
*Function: getOtsuThreshold
*Description: 计算大津法的阈值
*Params: inArray - 8 位灰度图片的二维数组数据
* width - 图像宽度
* height - 图像高度
*Return: threshold - 大津法计算的阈值
************************************************************/
int getOtsuThreshold(uint8_t** inArray, uint32_t width, uint32_t height)
{
// 得到总的像素个数
uint32_t Num = width * height;
// 像素直方图,统计每个像素值出现的次数
uint32_t pixelCounts[256] = {0};
for (uint32_t i = 0; i < height; i++) {
for (uint32_t j = 0; j < width; j++) {
uint8_t pixelValue = inArray[i][j];
pixelCounts[pixelValue]++;
}
}
// 计算直方图的矩,图片的数学期望:E(x) = uSum / Num;
uint32_t uSum = 0;
for(int i = 0; i < 256; i++) {
uSum += i * pixelCounts[i];
}
// 定义变量
int threshold = 0; // 定义结果阈值
uint32_t w0 = 0, w1 = 0; // 定义局部像素和,前景像素和、背景像素和,概率的变形,P(i) = w / Num
double mu0 = 0, u0 = 0, u1 = 0, maxVariance = 0.0; // 定义局部像素直方图的矩,前景像素数学期望、背景像素数学期望,最大类间方差
// 循环遍历得到最大的类间方差
for(int i = 0; i < 256; i++) {
// 计算相关变量
w0 += pixelCounts[i]; // 计算前景像素累加和,前景概率 P(i) = w / Num,实际中 Num 为 定值,不会影响最大类间方差的阈值,故省去除法过程
w1 = Num - w0; // 得到背景像素累加和
mu0 += i * pixelCounts[i]; // 计算矩
if(w0 == 0 || w1 == 0) // 保证分母不为 0
continue;
u0 = mu0 / w0; // 得到前景的数学期望
u1 = (uSum - mu0) * 1.0 / w1; // 得到背景的数学期望
// 计算最大类间方差
double variance = w0 * w1 * (u1 - u0) * (u1 - u0); // 公式计算,实际为 w0 / Num * w1 / Num * (u1 - u0) * (u1 - u0);省去了 / (Num * Num)
if(variance > maxVariance)
{
maxVariance = variance;
threshold = i;
}
}
return threshold;
}
3、代码示例
- 1、二值化方式的枚举说明
// 二值化方式枚举
enum threshMode {
threshBinary = 0, /* value = value > threshold ? max_value : 0 */
threshBinaryInv, /* value = value > threshold ? 0 : max_value */
threshTrunc, /* value = value > threshold ? threshold : value */
threshTozero, /* value = value > threshold ? value : 0 */
threshTozeroInv, /* value = value > threshold ? 0 : value */
threshOtsu, // 大津法判断阈值
};
- 2、二值化函数接口
/************************************************************
*Function: binary
*Description: 将 8 位灰度图片的二维数组数据二值化
*Params: inArray - 8 位灰度图片的二维数组数据
* width - 图像宽度
* height - 图像高度
* mode - 二值化方式,枚举类型,当使用 Otsu 的时候,示例:threshOtsu + threshBinaryInv
*Return: None
************************************************************/
void binary(uint8_t** inArray, uint32_t width, uint32_t height, int threshold, int mode)
{
// 判断是否使用 大津法
if(mode >= threshOtsu) {
threshold = getOtsuThreshold(inArray, width, height);
mode -= threshOtsu;
}
// 遍历更改
for (uint32_t i = 0; i < height; i++) {
for (uint32_t j = 0; j < width; j++) {
// 模式选择
switch (mode)
{
case threshBinary:
inArray[i][j] = (inArray[i][j] > threshold ? 255 : 0);
break;
case threshBinaryInv:
inArray[i][j] = (inArray[i][j] > threshold ? 0 : 255);
break;
case threshTrunc:
inArray[i][j] = (inArray[i][j] > threshold ? threshold : inArray[i][j]);
break;
case threshTozero:
inArray[i][j] = (inArray[i][j] > threshold ? inArray[i][j] : 0);
break;
case threshTozeroInv:
inArray[i][j] = (inArray[i][j] > threshold ? 0 : inArray[i][j]);
break;
default:
break;
}
}
}
}
- 3、函数调用
// 使用大津法判断阈值之后,进行 threshTozero 方式二值
binary(imgArr, width, height, 0, threshOtsu + threshTozero);