文章目录
- Otsu算法简介
- Otsu 算法的逻辑
- 源码实现
欢迎访问个人网络日志🌹🌹知行空间🌹🌹
Otsu算法简介
Otsu阈值法发表于1979年,论文为A threshold selection method from gray level histograms,作者是日本东京大学的Nobuyuki Otsu(大津 展之)。
自动全局阈值算法通常包括如下几步
- 1.对输入图像进行预处理,如高斯平滑
- 2.获取图像的灰度直方图
- 3.计算阈值T
- 4.对原图像二值化,小于阈值T的位置像素值设为0,大于阈值T的像素值设为255
一般,各种阈值处理算法的区别主要在第3步,即确定阈值的逻辑不同。
Otsu 算法的逻辑
其核心思想是,将图像的像素根据某个像素值分成两簇,并使得这两簇之间的像素值的类间方差最大化。
所以Otsu算法适合用于像素直方图表现为双峰图像的阈值处理。
双峰图像(bimodal images)是指具有如下形式像素直方图的图像:
如下就是一个双峰图像的示例:
假设一副灰度图,像素值灰度级为
L
L
L,如我们常见的灰度图像,灰度级是256。
像素值为第 i i i个灰度级的像素点有 n i n_i ni个,则这幅图像总的像素点个数为 N = n 1 + n 2 + . . . n L N=n_1+n_2+...n_L N=n1+n2+...nL
基于上述假设,某个像素点为灰度级 i i i的概率可表示为:
p i = n i N p_i=\frac{n_i}{N} pi=Nni
p i p_i pi满足以下条件: p i > 0 , ∑ i = 1 L p i = 1 p_i\gt0,\sum_{i=1}^Lp_i=1 pi>0,i=1∑Lpi=1
取灰度级 t t t为阈值将这幅图像的像素点分成 C 1 C_1 C1和 C 2 C_2 C2两簇,
- C 1 C_1 C1包含像素级为 [ 1 , 2 , . . . , t ] [1,2,...,t] [1,2,...,t]的像素
- C 2 C_2 C2包含像素级为 [ t + 1 , . . . , L ] [t+1,...,L] [t+1,...,L]的像素
对于图像中某个像素属于 C 1 / C 2 C_1/C_2 C1/C2类的概率可表示为:
ω
1
(
t
)
=
∑
i
=
1
t
p
i
\omega_1(t) = \sum^t_{i=1}p_i
ω1(t)=i=1∑tpi
ω
2
(
t
)
=
∑
i
=
t
+
1
L
p
i
\omega_2(t) = \sum^L_{i=t+1}p_i
ω2(t)=i=t+1∑Lpi
ω 1 ( t ) , ω 2 ( t ) \omega_1(t),\omega_2(t) ω1(t),ω2(t)满足关系 ω 1 ( t ) = 1 − ω 2 ( t ) \omega_1(t) = 1 - \omega_2(t) ω1(t)=1−ω2(t)
求 C 1 / C 2 C_1/C_2 C1/C2每个簇对应的像素均值:
μ 1 ( t ) = ∑ i = 1 t i ∗ n i ∑ i = 1 t n i = ∑ i = 1 t i ∗ n i N ∑ i = 1 t n i N = ∑ i = 1 t i p i ω 1 ( t ) \mu_1(t)=\frac{\sum\limits_{i=1}^ti*n_i}{\sum\limits_{i=1}^tn_i}=\frac{\sum\limits_{i=1}^t\frac{i*n_i}{N}}{\sum\limits_{i=1}^t\frac{n_i}{N}}=\sum_{i=1}^t\frac{ip_i}{\omega_1(t)} μ1(t)=i=1∑tnii=1∑ti∗ni=i=1∑tNnii=1∑tNi∗ni=i=1∑tω1(t)ipi
同样可推导:
μ 2 ( t ) = ∑ i = t + 1 L i p i ω 2 ( t ) \mu_2(t)=\sum_{i=t+1}^L\frac{ip_i}{\omega_2(t)} μ2(t)=i=t+1∑Lω2(t)ipi
整幅图像的像素均值记为:
μ T = ∑ i L i ∗ p i = ω 1 ( t ) μ 1 ( t ) + ω 2 ( t ) μ 2 ( t ) \mu_T=\sum_i^Li*p_i=\omega_1(t)\mu_1(t)+\omega_2(t)\mu_2(t) μT=i∑Li∗pi=ω1(t)μ1(t)+ω2(t)μ2(t)
求
C
1
/
C
2
C_1/C_2
C1/C2每个簇对应的像素值方差:
σ
1
2
(
t
)
=
∑
i
=
1
t
[
i
−
μ
1
(
t
)
]
2
∗
n
i
∑
i
=
1
t
n
i
=
∑
i
=
1
t
[
i
−
μ
1
(
t
)
]
2
∗
n
i
N
∑
i
=
1
t
n
i
N
=
∑
i
=
1
t
[
i
−
μ
1
(
t
)
]
2
p
i
ω
1
(
t
)
\sigma^2_1(t)=\frac{\sum\limits_{i=1}^t[i-\mu_1(t)]^2*n_i}{\sum\limits_{i=1}^tn_i}=\frac{\sum\limits_{i=1}^t\frac{[i-\mu_1(t)]^2*n_i}{N}}{\sum\limits_{i=1}^t\frac{n_i}{N}}=\frac{\sum\limits_{i=1}^t[i-\mu_1(t)]^2p_i}{\omega_1(t)}
σ12(t)=i=1∑tnii=1∑t[i−μ1(t)]2∗ni=i=1∑tNnii=1∑tN[i−μ1(t)]2∗ni=ω1(t)i=1∑t[i−μ1(t)]2pi
同样可推导:
σ 2 2 ( t ) = ∑ i = t + 1 L [ i − μ 2 ( t ) ] 2 p i ω 2 ( t ) \sigma^2_2(t)=\frac{\sum\limits_{i=t+1}^L[i-\mu_2(t)]^2p_i}{\omega_2(t)} σ22(t)=ω2(t)i=t+1∑L[i−μ2(t)]2pi
为了衡量所取阈值 t t t的二值化效果,作者定义了三种方差,分别是:
类内方差:
σ
W
2
=
ω
1
σ
1
2
+
ω
2
σ
2
2
\sigma_W^2=\omega_1\sigma_1^2 + \omega_2\sigma_2^2
σW2=ω1σ12+ω2σ22
类间方差:
σ
B
2
=
ω
1
(
μ
1
−
μ
T
)
2
+
ω
2
(
μ
2
−
μ
T
)
2
=
ω
1
ω
2
(
μ
1
−
μ
2
)
2
\sigma_B^2=\omega_1(\mu_1-\mu_T)^2 + \omega_2(\mu_2-\mu_T)^2=\omega_1\omega_2(\mu_1-\mu_2)^2
σB2=ω1(μ1−μT)2+ω2(μ2−μT)2=ω1ω2(μ1−μ2)2
图像总的像素值方差:
σ T 2 = ∑ i = 1 L ( i − μ T ) 2 p i \sigma_T^2=\sum_{i=1}^L(i-\mu_T)^2p_i σT2=i=1∑L(i−μT)2pi
可以推导三者之间有如下关系:
σ W 2 + σ B 2 = σ T 2 \sigma_W^2 + \sigma_B^2 = \sigma_T^2 σW2+σB2=σT2
从上面的定义可以发现, σ W 2 / σ B 2 \sigma_W^2/ \sigma_B^2 σW2/σB2于阈值 t t t有关,而 σ T 2 \sigma_T^2 σT2与阈值 t t t无关。
上面 σ W 2 \sigma_W^2 σW2是 t t t的二阶函数, σ B 2 \sigma_B^2 σB2是 t t t的一阶函数,更易优化。最后,求阈值 t t t可以变成最大化类间方差 σ B 2 \sigma_B^2 σB2
σ B 2 ( t ∗ ) = max 1 ≤ t ≤ L σ B 2 ( t ) \sigma_B^2(t^*)=\max_{1\le t\le L}\sigma_B^2(t) σB2(t∗)=1≤t≤LmaxσB2(t)
欢迎访问个人网络日志🌹🌹知行空间🌹🌹
源码实现
// Include Libraries
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>
using namespace std;
using namespace cv;
int main(){
// read the image in BGR format
Mat testImage = imread("boat.jpg", 0);
int bins_num = 256;
// Get the histogram
long double histogram[256];
// initialize all intensity values to 0
for(int i = 0; i < 256; i++)
histogram[i] = 0;
// calculate the no of pixels for each intensity values
for(int y = 0; y < testImage.rows; y++)
for(int x = 0; x < testImage.cols; x++)
histogram[(int)testImage.at<uchar>(y,x)]++;
// Calculate the bin_edges
long double bin_edges[256];
bin_edges[0] = 0.0;
long double increment = 0.99609375;
for(int i = 1; i < 256; i++)
bin_edges[i] = bin_edges[i-1] + increment;
// Calculate bin_mids
long double bin_mids[256];
for(int i = 0; i < 256; i++)
bin_mids[i] = (bin_edges[i] + bin_edges[i+1])/2;
// Iterate over all thresholds (indices) and get the probabilities weight1, weight2
long double weight1[256];
weight1[0] = histogram[0];
for(int i = 1; i < 256; i++)
weight1[i] = histogram[i] + weight1[i-1];
int total_sum=0;
for(int i = 0; i < 256; i++)
total_sum = total_sum + histogram[i];
long double weight2[256];
weight2[0] = total_sum;
for(int i = 1; i < 256; i++)
weight2[i] = weight2[i-1] - histogram[i - 1];
// Calculate the class means: mean1 and mean2
long double histogram_bin_mids[256];
for(int i = 0; i < 256; i++)
histogram_bin_mids[i] = histogram[i] * bin_mids[i];
long double cumsum_mean1[256];
cumsum_mean1[0] = histogram_bin_mids[0];
for(int i = 1; i < 256; i++)
cumsum_mean1[i] = cumsum_mean1[i-1] + histogram_bin_mids[i];
long double cumsum_mean2[256];
cumsum_mean2[0] = histogram_bin_mids[255];
for(int i = 1, j=254; i < 256 && j>=0; i++, j--)
cumsum_mean2[i] = cumsum_mean2[i-1] + histogram_bin_mids[j];
long double mean1[256];
for(int i = 0; i < 256; i++)
mean1[i] = cumsum_mean1[i] / weight1[i];
long double mean2[256];
for(int i = 0, j = 255; i < 256 && j >= 0; i++, j--)
mean2[j] = cumsum_mean2[i] / weight2[j];
// Calculate Inter_class_variance
long double Inter_class_variance[255];
long double dnum = 10000000000;
for(int i = 0; i < 255; i++)
Inter_class_variance[i] = ((weight1[i] * weight2[i] * (mean1[i] - mean2[i+1])) / dnum) * (mean1[i] - mean2[i+1]);
// Maximize interclass variance
long double maxi = 0;
int getmax = 0;
for(int i = 0;i < 255; i++){
if(maxi < Inter_class_variance[i]){
maxi = Inter_class_variance[i];
getmax = i;
}
}
cout << "Otsu's algorithm implementation thresholding result: " << bin_mids[getmax];
- 1.https://learnopencv.com/otsu-thresholding-with-opencv/