閾值處理詳解
基礎:
首先將灰度圖轉化成灰度直方圖,橫座標是灰度值(0-255),縱座標是像素個數。(歸一化之後表徵的是像素出現的概率)
如下圖所示:
灰度直方圖性質:
兩幅灰度直方圖
如圖,從圖A可以看出,直方圖有兩個明顯的波峯和一個明顯的波谷,表明灰度普遍分爲兩個密集區域。此時將門限設置在兩者之間的波谷,則可以很好地分割出背景和物體。
同理,觀察圖B,有三個明顯的波峯和兩個明顯的波谷,此時可以設置雙門限,將圖像分割爲三類,如下圖冰山就是很好的例子,分割爲暗背景、冰山的明亮區域和陰影區域。
然而並不是所有圖像的直方圖都是有明顯的多個波峯和波谷的。
單峯型:
無明顯波谷型
灰度趨於一致型(被噪聲污染過)
灰度閾值取決於波谷的寬度和深度,影響波谷特性的關鍵因素有:
1、波峯的間隔(波峯離得越遠,分離這些模式機會越好)
2、圖像中的噪聲內容(模式隨噪聲的增加而展寬)
3、物體和背景的相對尺寸
4、光源的均勻性
5、圖像反射的均勻性
接下來的所有的閾值處理方法,其目的都是:將灰度直方圖變得好處理 並 找到分割背景和物體的門限灰度值。
基於全局的閾值處理
1迭代算法(最小概率誤判)
公式推導:
算法步驟:
代碼實現:
void Iteration(Mat* srcImage, Mat* dstImage, float delta_T)
{
//【1】求最大灰度和最小灰度
byte max_his = 0;
byte min_his = 255;
int height = (*srcImage).rows;
int width = (*srcImage).cols;
for (int j = 0;j < height;j++)
{
for (int i = 0;i < width;i++)
{
if ((*srcImage).at<uchar>(j, i) > max_his)
{
max_his = (*srcImage).at<uchar>(j, i);
}
if ((*srcImage).at<uchar>(j, i) < min_his)
{
min_his = (*srcImage).at<uchar>(j, i);
}
}
}
float T = 0.5 * (max_his+ min_his);
float m1 = 255; //當m1 m2都取0時,會有錯誤
float m2 = 0;
float old_T = T;
float new_T = 0.5 * (m1 + m2);
int times = 10;
//while (times--)
while (abs(new_T - old_T) > delta_T)
{
int G1 = 0;
int G2 = 0;
int timer_G1 = 0;
int timer_G2 = 0;
for (int j = 0;j < height;j++)
{
for (int i = 0;i < width;i++)
{
if ((*srcImage).at<uchar>(j, i) > old_T)
{
G1 += (*srcImage).at<uchar>(j, i);
timer_G1++;
}
else
{
G2 += (*srcImage).at<uchar>(j, i);
timer_G2++;
}
}
}
m1 = G1 * 1.0f / timer_G1;
m2 = G2 * 1.0f / timer_G2;
old_T = new_T;
new_T = 0.5 * (m1 + m2);
}
cout << "迭代方法閾值爲:" << new_T << endl;
//根據得出的閾值二值化圖像
for (int j = 0;j < height;j++)
{
for (int i = 0;i < width;i++)
{
if ((*srcImage).at<uchar>(j, i) > new_T)
{
(*dstImage).at<uchar>(j, i) = 255;
}
else
{
(*dstImage).at<uchar>(j, i) = 0;
}
}
}
}
int main()
{
Mat srcImage = imread("D:\\opencv_picture_test\\新垣結衣\\test2.jpg", 0); //讀入的時候轉化爲灰度圖
namedWindow("原始圖", WINDOW_NORMAL);//WINDOW_NORMAL允許用戶自由伸縮窗口
imshow("原始圖", srcImage);
Mat dstImage;
dstImage.create(srcImage.rows, srcImage.cols, CV_8UC1);
double time0 = static_cast<double>(getTickCount()); //記錄起始時間
//閾值處理+二值化
//My_P_tile(&srcImage,&dstImage,20); //設P爲20
Iteration(&srcImage, &dstImage,0.02);
//一系列處理之後
time0 = ((double)getTickCount() - time0) / getTickFrequency();
//cout << "此方法運行時間爲:" << time0 << "秒" << endl; //輸出運行時間
namedWindow("效果圖", WINDOW_NORMAL);//WINDOW_NORMAL允許用戶自由伸縮窗口
imshow("效果圖", dstImage);
dstImage = My_Rraw_histogram(&srcImage);
namedWindow("一維直方圖", WINDOW_NORMAL);//WINDOW_NORMAL允許用戶自由伸縮窗口
imshow("一維直方圖", dstImage);
waitKey(0);
return 0;
}
當直方圖存在比較明顯的波谷時,這種方法是比較好的。δT控制迭代次數,下面是代碼實現效果
2基於Otsu最佳全局閾值方法(非常有效)
大津法又叫最大類間方差法、最大類間閾值法(OTSU)。
它的基本思想是,用一個閾值將圖像中的數據分爲兩類,
一類中圖像的像素點的灰度均小於這個閾值,另一類中的圖像的像素點的灰度均大於或者等於該閾值。 //一般來說使用遍歷的方法來求
如果這兩個類中像素點的灰度的方差越大,說明獲取到的閾值就是最佳的閾值
(方差是灰度分佈均勻性的一種度量,背景和前景之間的類間方差越大,說明構成圖像的兩部分的差別越大,當部分前景錯分爲背景或部分背景錯分爲前景都會導致兩部分差別變小。因此,使類間方差最大的分割意味着錯分概率最小。)。
則利用該閾值可以將圖像分爲前景和背景兩個部分。
而我們所感興趣的部分一般爲前景。
對於灰度分佈直方圖有兩個峯值的圖像,大津法求得的T近似等於兩個峯值之間的低谷。
(這段闡述轉自這裏https://www.jianshu.com/p/56b140f9535a)
公式推導
從一篇博客截來的圖,羅列了我們要計算的變量。https://blog.csdn.net/u012198575/article/details/81128799
代碼實現
void My_Ostu(Mat* srcImage, Mat* dstImage)
{
int height = (*srcImage).rows;
int width = (*srcImage).cols;
int Ostu_Threshold = 0; //大津閾值
int size = height * width;
float variance; //類間方差
float maxVariance = 0, w1 = 0, w2 = 0, avgValue = 0;
float u0 = 0, u1 = 0, u2 = 0;
//生成灰度直方圖
int pixels[256];
float histgram[256];
for (int i = 0; i < 256; i++)
{
pixels[i] = 0;
}
for (int j = 0; j < height; j++)
{
for (int i = 0; i < width; i++)
{
pixels[(*srcImage).at<uchar>(j, i)]++;
}
}
for (int i = 0; i < 256; i++)
{
histgram[i] = pixels[i] * 1.0f / size;
}
//遍歷找出類間方差最大(maxVariance)的閾值(Ostu_Threshold)
for (int i = 0;i <= 255;i++)
{
w1 = 0;
w2 = 0;
u1 = 0;
u2 = 0;
//計算背景像素佔比,平均灰度
for (int j = 0;j <= i;j++)
{
w1 += histgram[j];
u1 += histgram[j] * j;
}
u1 = u1 / w1;
//計算前景像素佔比,平均灰度
w2 = 1 - w1;
if (i == 255)
{
u2 = 0;
}
else
{
for (int j = i + 1;j <= 255;j++)
{
u2 += histgram[j] * j;
}
}
u2 = u2 / w2;
//計算類間方差
variance = w1 * w2 * (u1 - u2) * (u1 - u2);
if (variance > maxVariance)
{ //找到使灰度差最大的值
maxVariance = variance;
Ostu_Threshold = i; //那個值就是閾值
}
}
cout << "大津法閾值爲:" << Ostu_Threshold << endl;
//【3】二值化
for (int j = 0; j < height; j++)
{
for (int i = 0; i < width; i++)
{
if ((*srcImage).at<uchar>(j, i) >= Ostu_Threshold)
{
(*dstImage).at<uchar>(j, i) = 255;
}
else
{
(*dstImage).at<uchar>(j, i) = 0;
}
}
}
}
int main()
{
Mat srcImage = imread("D:\\opencv_picture_test\\新垣結衣\\test2.jpg", 0); //讀入的時候轉化爲灰度圖
namedWindow("原始圖", WINDOW_NORMAL);//WINDOW_NORMAL允許用戶自由伸縮窗口
imshow("原始圖", srcImage);
Mat dstImage;
dstImage.create(srcImage.rows, srcImage.cols, CV_8UC1);
double time0 = static_cast<double>(getTickCount()); //記錄起始時間
//閾值處理+二值化
//My_P_tile(&srcImage,&dstImage,20); //設P爲20
//My_Iteration(&srcImage, &dstImage,0.02);
My_Ostu(&srcImage, &dstImage);
//一系列處理之後
time0 = ((double)getTickCount() - time0) / getTickFrequency();
cout << "此方法運行時間爲:" << time0 << "秒" << endl; //輸出運行時間
namedWindow("效果圖", WINDOW_NORMAL);//WINDOW_NORMAL允許用戶自由伸縮窗口
imshow("效果圖", dstImage);
dstImage = My_Rraw_histogram(&srcImage);
namedWindow("一維直方圖", WINDOW_NORMAL);//WINDOW_NORMAL允許用戶自由伸縮窗口
imshow("一維直方圖", dstImage);
waitKey(0);
return 0;
}
效果:
3用圖像平滑改善全局閾值處理
總的來說就是在二值化之前先用33或者55之類的均值模板將整個圖像處理一下。
不過這樣的壞處是使物體與背景的邊界變得有些模糊。侵蝕越多,邊界誤差越大。
在某些極端情況下,這種方法效果並不好。
4利用邊緣改進全局閾值處理
這種方法將關注聚焦於物體與背景的邊緣像素,在邊緣的灰度跳動非常明顯,由此得到的灰度直方圖將會得到很大的改善。
在這裏我們求得邊緣的方法主要是梯度算子和拉普拉斯算子。
算法步驟:
一般來說我們確定閾值T是根據,梯度最大值或者拉普拉斯最大值的某百分比來確定的。當有不同需求時,採用不同的佔比。
基於局部的閾值處理
這種閾值處理的目的是爲了解決光照和反射帶來的問題。
1圖像分塊可變閾值處理
其實就是把一個圖片分割爲多塊,分別使用大津閾值。
分塊處理後的子圖像直方圖
上面的是書上的樣例,我把原圖截下來,試了試自己寫的代碼,效果並不是很好。
代碼實現:
void My_local_adaptive(Mat* srcImage, Mat* dstImage, int areas_of_H, int areas_of_W) //局部自適應法 基於大津閾值areas_of_H:豎直方向分割的個數 areas_of_W:橫座標方向分割的個數
{
int height = (*srcImage).rows/ areas_of_H; //每一小塊的height
int width = (*srcImage).cols/ areas_of_W; //每一小塊的width
int Ostu_Threshold = 0; //大津閾值
int size = height * width/ areas_of_H/ areas_of_W; //每一小塊的size
//一行一行地來
for (int y = 0; y < areas_of_H; y++)
{
for (int x = 0; x < areas_of_W; x++)
{
float variance = 0; //類間方差
float maxVariance = 0, w1 = 0, w2 = 0, avgValue = 0;
float u0 = 0, u1 = 0, u2 = 0;
//生成areas_of_W*areas_of_H個局部灰度直方圖
int pixels[256];
float histgram[256];
for (int i = 0; i < 256; i++)
{
pixels[i] = 0;
}
//【處理每個小區域並且二值化】
//【計算直方圖】
for (int j = y* height; j < ((y + 1 == areas_of_H) ? (*srcImage).rows : (y + 1) * height); j++) //? : 是一個三目運算符,也是唯一的一個三目運算符。?前面表邏輯條件,:前面也就是?後面表示條件成立時的值,:後面表條件不成立時的值。例如,當a > b時,x = 1否則x = 0,可以寫成x = a > b ? 1 : 0。
{
for (int i = x * width; i < ((x + 1 == areas_of_W) ? (*srcImage).cols : (x + 1) * width); i++)
{
pixels[(*srcImage).at<uchar>(j, i)]++;
}
}
//【直方圖歸一化】
for (int i = 0; i < 256; i++)
{
histgram[i] = pixels[i] * 1.0f / size;
}
//遍歷找出類間方差最大(maxVariance)的閾值(Ostu_Threshold)
for (int i = 0;i <= 255;i++)
{
w1 = 0;
w2 = 0;
u1 = 0;
u2 = 0;
//計算背景像素佔比,平均灰度
for (int j = 0;j <= i;j++)
{
w1 += histgram[j];
u1 += histgram[j] * j;
}
u1=u1/w1;
//計算前景像素佔比,平均灰度
w2 = 1 - w1;
if (i == 255)
{
u2 = 0;
}
else
{
for (int j = i + 1;j <= 255;j++)
{
u2 += histgram[j] * j;
}
}
u2=u2/w2;
//計算類間方差
variance = w1 * w2 * (u1 - u2) * (u1 - u2);
if (variance > maxVariance)
{ //找到使灰度差最大的值
maxVariance = variance;
Ostu_Threshold = i; //那個值就是閾值
}
}
cout << "大津法閾值爲:" << Ostu_Threshold << endl;
//【3】二值化
for (int j = y * height; j < ((y + 1 == areas_of_H) ? (*srcImage).rows : (y + 1) * height); j++) //? : 是一個三目運算符,也是唯一的一個三目運算符。?前面表邏輯條件,:前面也就是?後面表示條件成立時的值,:後面表條件不成立時的值。例如,當a > b時,x = 1否則x = 0,可以寫成x = a > b ? 1 : 0。
{
for (int i = x * width; i < ((x + 1 == areas_of_W) ? (*srcImage).cols : (x + 1) * width); i++)
{
if ((*srcImage).at<uchar>(j, i) >= Ostu_Threshold)
{
(*dstImage).at<uchar>(j, i) = 255;
}
else
{
(*dstImage).at<uchar>(j, i) = 0;
}
}
}
}
}
}
int main()
{
//Mat srcImage = imread("D:\\opencv_picture_test\\新垣結衣\\test2.jpg", 0); //讀入的時候轉化爲灰度圖
//Mat srcImage = imread("D:\\opencv_picture_test\\miku\\miku2.jpg", 0); //讀入的時候轉化爲灰度圖
Mat srcImage = imread("D:\\opencv_picture_test\\閾值處理\\帶噪聲陰影的圖.png", 0); //讀入的時候轉化爲灰度圖
namedWindow("原始圖", WINDOW_NORMAL);//WINDOW_NORMAL允許用戶自由伸縮窗口
imshow("原始圖", srcImage);
Mat dstImage;
dstImage.create(srcImage.rows, srcImage.cols, CV_8UC1);
double time0 = static_cast<double>(getTickCount()); //記錄起始時間
//閾值處理+二值化
//My_P_tile(&srcImage,&dstImage,20); //設P爲20
//My_Iteration(&srcImage, &dstImage,0.01);
//My_Ostu(&srcImage, &dstImage);
My_local_adaptive(&srcImage, &dstImage, 1, 2);
//一系列處理之後
time0 = ((double)getTickCount() - time0) / getTickFrequency();
cout << "此方法運行時間爲:" << time0 << "秒" << endl; //輸出運行時間
namedWindow("效果圖", WINDOW_NORMAL);//WINDOW_NORMAL允許用戶自由伸縮窗口
imshow("效果圖", dstImage);
dstImage = My_Rraw_histogram(&srcImage);
namedWindow("一維直方圖", WINDOW_NORMAL);//WINDOW_NORMAL允許用戶自由伸縮窗口
imshow("一維直方圖", dstImage);
waitKey(0);
return 0;
}
全局大津閾值效果
局部閾值法:1*2分割
迭代閾值法:
看來仍然需要改進
2基於局部圖像特性的可變閾值處理
算法步驟:
1、計算以某一像素爲中心的鄰域的灰度標準差和均值
2、設定可變閾值
3、觀察是否滿足閾值條件
4、二值化
其中a和b都是需要人工整定。
效果圖:
3基於移動平均法的可變閾值
有關的鏈接:(這個算法我還沒有理解,等我理解了再來補充)
https://blog.csdn.net/qq_34510308/article/details/93162142