最大類間方差法(大津法OTSU)
演算法介紹
最大類間方差法是1979年由日本學者大津提出的,是一種自適應閾值確定的方法,又叫大津法,簡稱OTSU,是一種基於全域性的二值化演算法,它是根據影象的灰度特性,將影象分為前景和背景兩個部分。當取最佳閾值時,兩部分之間的差別應該是最大的,在OTSU演算法中所採用的衡量差別的標準就是較為常見的最大類間方差。前景和背景之間的類間方差如果越大,就說明構成影象的兩個部分之間的差別越大,當部分目標被錯分為背景或部分背景被錯分為目標,都會導致兩部分差別變小,當所取閾值的分割使類間方差最大時就意味著錯分概率最小[1]。
記T為前景與背景的分割閾值,前景點數佔影象比例為w 0 ,平均灰度為u 0 ;背景點數佔影象比例為w
g=w 0 ×(u 0 −u) 2 +w 1 ×(u 1 −u) 2
聯立上面兩式可得:
g=w 0 ×w 1 ×(u 0 −u 1 ) 2
或
g=w 0 1−w 0 ×(u 0 −u) 2
當方差g 最大時,可以認為此時前景和背景差異最大,此時的灰度T是最佳閾值。類間方差法對噪聲以及目標大小十分敏感,它僅對類間方差為單峰的影象產生較好的分割效果。當目標與背景的大小比例懸殊時(例如受光照不均、反光或背景複雜等因素影響),類間方差準則函式可能呈現雙峰或多峰,此時效果不好。直接用OTSU演算法處理自然場景銘牌圖片的部分結果例項如下:
圖1.最大類間方差法二值化結果
程式碼實現
(C語言版,VS2012+opencv249)
#include "stdio.h"
#include "cv.h"
#include "highgui.h"
#include "Math.h"
int Otsu(IplImage* src);
int main()
{
IplImage* img = cvLoadImage("lena.jpg",0); //獲取灰度影象img
IplImage* dst = cvCreateImage(cvGetSize(img), 8, 1);
int threshold = Otsu(img); //呼叫大津法求出最佳閾值
printf("otsu threshold = %d\n", threshold);
cvThreshold(img, dst, threshold, 255, CV_THRESH_BINARY); //用otsu的閾值二值化
cvNamedWindow( "img", 1 );
cvNamedWindow( "dst", 1 );
cvShowImage("img", img);
cvShowImage("dst", dst);
cvWaitKey(-1);
cvReleaseImage(&img);
cvReleaseImage(&dst);
cvDestroyWindow( "img" );
cvDestroyWindow( "dst" );
return 0;
}
int Otsu(IplImage* src)
{
int height=src->height;
int width=src->width;
//histogram
float histogram[256] = {0};
for(int i=0; i < height; i++)
{
unsigned char* p=(unsigned char*)src->imageData + src->widthStep * i;
for(int j = 0; j < width; j++)
{
histogram[*p++]++;
}
}
//normalize histogram & average pixel value
int size = height * width;
float u =0;
for(int i = 0; i < 256; i++)
{
histogram[i] = histogram[i] / size;
u += i * histogram[i]; //整幅影象的平均灰度
}
int threshold;
float maxVariance=0;
float w0 = 0, avgValue = 0;
for(int i = 0; i < 256; i++)
{
w0 += histogram[i]; //假設當前灰度i為閾值, 0~i 灰度畫素所佔整幅影象的比例即前景比例
avgValue += i * histogram[i]; //avgValue/w0 = u0
float t = avgValue/w0 - u; //t=u0-u
float variance = t * t * w0 /(1 - w0);
if(variance > maxVariance)
{
maxVariance = variance;
threshold = i;
}
}
return threshold;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
程式碼執行結果:
參考資料:
[1]Otsu N. A threshold selection method from gray-level histograms[J]. Automatica, 1975, 11(285-296): 23-27.
OTSU演算法:就是計算出灰度圖最佳閾值的演算法
1.先對灰度圖進行直方圖計算並歸一化處理,得到0-255之間每個畫素在灰度圖中出現的概率,即表示為某個畫素在灰度圖中出現了n個,灰度圖總的畫素點為N個,則這個畫素的出現概率為Pi=n/N
2.每個灰度圖可以由閾值k將灰度圖分為A,B兩大類,很容易得到A,B類在灰度圖中的出現概率以及灰度均值
3.計算灰度圖A,B類得類間方差,在最佳閾值K處,求得的類間方差最大,也就是類間方差最大的那個時刻的閾值就為灰度圖的最佳閾值
程式碼實現:
以下三種演算法時間複雜度依次降低,前兩種演算法自己所寫,最後一種演算法從網上看到大神寫的,膜拜!當然如果你想偷懶,可是更直接點用cvThreshold一步實現cvThreshold(srcCopy,thresholdImage,0,255,CV_THRESH_OTSU),花費時間與演算法三相當。
演算法一:
處理一張640*480的圖片花費時間大概在8000毫秒左右,非常低效,但是是一種非常直接的,顯而易見的實現方法。
[cpp] view plain copy print?- int otsu(IplImage* src)
- {
- CvScalar cs = cvSum(src); //影象總灰度值
- int Width = src->width;
- int Height = src->height;
- double U_t = 1.0*cs.val[0]/(Width*Height); //影象的平均灰度
- int threshold = 0;
- double delta = 0;
- for(int k=0; k<256; k++)
- {
- double sumOfGray = 0;
- int num = 0;
- for(int i=0; i < Width; i++)
- {
- for(int j=0; j < Height;j++)
- {
- int t = cvGet2D(src, j,i).val[0];
- if(t < k)
- {
- num++; //統計灰度小於k的畫素點的個數
- sumOfGray += t; //灰度值小於k的畫素點的灰度和
- }
- }
- }
- double w0 = 1.0*num/(Width*Height); //灰度值小於閾值k的畫素的概率
- double w1 = 1.0 - w0; //灰度值大於k畫素的概率
- double u0 = 1.0*sumOfGray/num; //灰度值小於閾值k的畫素的平均灰度值
- double u1 = 1.0*(cs.val[0]-sumOfGray)/(Width*Height-num); //灰度值大於閾值k的畫素的平均灰度值
- double delta_tmp = w0*pow(u0-U_t,2) + w1*pow(u1-U_t,2); //類間方差
- if(delta_tmp > delta)
- {
- delta = delta_tmp;
- threshold = k;
- }
- }
- cout<<cs.val[0]<<endl;
- cout<<U_t<<endl;
- return threshold;
- }
int otsu(IplImage* src)
{
CvScalar cs = cvSum(src); //影象總灰度值
int Width = src->width;
int Height = src->height;
double U_t = 1.0*cs.val[0]/(Width*Height); //影象的平均灰度
int threshold = 0;
double delta = 0;
for(int k=0; k<256; k++)
{
double sumOfGray = 0;
int num = 0;
for(int i=0; i < Width; i++)
{
for(int j=0; j < Height;j++)
{
int t = cvGet2D(src, j,i).val[0];
if(t < k)
{
num++; //統計灰度小於k的畫素點的個數
sumOfGray += t; //灰度值小於k的畫素點的灰度和
}
}
}
double w0 = 1.0*num/(Width*Height); //灰度值小於閾值k的畫素的概率
double w1 = 1.0 - w0; //灰度值大於k畫素的概率
double u0 = 1.0*sumOfGray/num; //灰度值小於閾值k的畫素的平均灰度值
double u1 = 1.0*(cs.val[0]-sumOfGray)/(Width*Height-num); //灰度值大於閾值k的畫素的平均灰度值
double delta_tmp = w0*pow(u0-U_t,2) + w1*pow(u1-U_t,2); //類間方差
if(delta_tmp > delta)
{
delta = delta_tmp;
threshold = k;
}
}
cout<<cs.val[0]<<endl;
cout<<U_t<<endl;
return threshold;
}
演算法二:
借用opencv中的直方圖函式,大大減少迴圈數,同一張圖片花費時間大概在16毫秒左右。
[cpp] view plain copy print?- int otsu(IplImage* src)
- {
- int hist_size = 256; //直方圖尺寸
- int hist_height = 256;
- float range[] = {0,255}; //灰度級的範圍
- float* ranges[]={range};
- //建立一維直方圖,統計影象在[0 255]畫素的均勻分佈
- CvHistogram* gray_hist = cvCreateHist(1,&hist_size,CV_HIST_ARRAY,ranges,1);
- //計算灰度影象的一維直方圖
- cvCalcHist(&src,gray_hist,0,0);
- //歸一化直方圖
- cvNormalizeHist(gray_hist,1.0);
- int Width = src->width;
- int Height = src->height;
- int threshold = 0;
- double delta = 0;
- CvScalar sumOfGray = cvSum(src);
- double U_t = sumOfGray.val[0]/(Width*Height);
- for(int k=0; k<256; k++)
- {
- double u0 = 0, u1 = 0, w0 = 0, w1 = 0, delta_tmp = 0, u00 = 0, u11 = 0;
- for(int i=0; i<k; i++)
- {
- u00 += cvQueryHistValue_1D(gray_hist,i)*i; //灰度小於閾值k的畫素的平均灰度值
- w0 += cvQueryHistValue_1D(gray_hist,i); //灰度小於閾值k的畫素的概率
- }
- u0 = u00/w0;
- for(int j=k; j<256; j++)
- {
- u11 += cvQueryHistValue_1D(gray_hist,j)*j; //灰度大於閾值k的畫素的平均灰度值
- w1 += cvQueryHistValue_1D(gray_hist,j); //灰度小於閾值k的畫素的概率
- }
- u1 = u11/w1;
- delta_tmp = w0*pow(u0-U_t,2) + w1*pow(u1-U_t,2); //類間方差
- if(delta_tmp > delta)
- {
- delta = delta_tmp;
- threshold = k;
- }
- }
- cvReleaseHist(&gray_hist);
- return threshold;
- }
int otsu(IplImage* src)
{
int hist_size = 256; //直方圖尺寸
int hist_height = 256;
float range[] = {0,255}; //灰度級的範圍
float* ranges[]={range};
//建立一維直方圖,統計影象在[0 255]畫素的均勻分佈
CvHistogram* gray_hist = cvCreateHist(1,&hist_size,CV_HIST_ARRAY,ranges,1);
//計算灰度影象的一維直方圖
cvCalcHist(&src,gray_hist,0,0);
//歸一化直方圖
cvNormalizeHist(gray_hist,1.0);
int Width = src->width;
int Height = src->height;
int threshold = 0;
double delta = 0;
CvScalar sumOfGray = cvSum(src);
double U_t = sumOfGray.val[0]/(Width*Height);
for(int k=0; k<256; k++)
{
double u0 = 0, u1 = 0, w0 = 0, w1 = 0, delta_tmp = 0, u00 = 0, u11 = 0;
for(int i=0; i<k; i++)
{
u00 += cvQueryHistValue_1D(gray_hist,i)*i; //灰度小於閾值k的畫素的平均灰度值
w0 += cvQueryHistValue_1D(gray_hist,i); //灰度小於閾值k的畫素的概率
}
u0 = u00/w0;
for(int j=k; j<256; j++)
{
u11 += cvQueryHistValue_1D(gray_hist,j)*j; //灰度大於閾值k的畫素的平均灰度值
w1 += cvQueryHistValue_1D(gray_hist,j); //灰度小於閾值k的畫素的概率
}
u1 = u11/w1;
delta_tmp = w0*pow(u0-U_t,2) + w1*pow(u1-U_t,2); //類間方差
if(delta_tmp > delta)
{
delta = delta_tmp;
threshold = k;
}
}
cvReleaseHist(&gray_hist);
return threshold;
}
演算法三:
一次迴圈實現,其中的公式需要自己用筆推導一下,非常精妙!完成一次運算的時間毫秒已經不能計量了,顯示為0!
[cpp] view plain copy print?- int otsu(IplImage* src)
- {
- int hist_size = 256; //直方圖尺寸
- int hist_height = 256;
- float range[] = {0,255}; //灰度級的範圍
- float* ranges[]={range};
- //建立一維直方圖,統計影象在[0 255]畫素的均勻分佈
- CvHistogram* gray_hist = cvCreateHist(1,&hist_size,CV_HIST_ARRAY,ranges,1);
- //計算灰度影象的一維直方圖
- cvCalcHist(&src,gray_hist,0,0);
- //歸一化直方圖
- cvNormalizeHist(gray_hist,1.0);
- int Width = src->width;
- int Height = src->height;
- int threshold = 0;
- double delta = 0;
- double U_t = 0;
- for(int m=0; m<256; m++)
- {
- U_t += cvQueryHistValue_1D(gray_hist,m)*m;
- }
- double u = 0, w = 0;
- for(int k=0; k<256; k++)
- {
- u += cvQueryHistValue_1D(gray_hist,k)*k; //
- w += cvQueryHistValue_1D(gray_hist,k); //灰度大於閾值k的畫素的概率
- double t = U_t * w - u;
- double delta_tmp = t * t / (w * (1 - w) );
- if(delta_tmp > delta)
- {
- delta = delta_tmp;
- threshold = k;
- }
- }
- cvReleaseHist(&gray_hist);
- return threshold;
- }