↑↑↑關注後"星標"Datawhale
每日干貨 & 每月組隊學習,不錯過
Datawhale乾貨
作者:姚童,Datawhale優秀學習者,華北電力大學
圖像閾值化分割是一種傳統的最常用的圖像分割方法,因其實現簡單、計算量小、性能較穩定而成爲圖像分割中最基本和應用最廣泛的分割技術。它特別適用於目標和背景佔據不同灰度級範圍的圖像。它不僅可以極大的壓縮數據量,而且也大大簡化了分析和處理步驟,因此在很多情況下,是進行圖像分析、特徵提取與模式識別之前的必要的圖像預處理過程。
閾值處理是指剔除圖像內像素值高於閾值或者低於閾值得像素點。例如,設定閾值爲127,將圖像內所有像素值大於127的像素點的值設爲255;將圖像內所有像素值小於127的像素點的值設爲0。
圖像閾值化的目的是要按照灰度級,對像素集合進行一個劃分,得到的每個子集形成一個與現實景物相對應的區域,各個區域內部具有一致的屬性,而相鄰區域不具有這種一致屬性。這樣的劃分可以通過從灰度級出發選取一個或多個閾值來實現。
學習目標
瞭解閾值分割基本概念
理解最大類間方差法(大津法)、自適應閾值分割的原理
掌握OpenCV框架下上述閾值分割算法API的使用
算法理論介紹
閾值處理
threshold函數
OpenCV使用threshold函數實現閾值化處理。
double cv::threshold ( InputArray src,
OutputArray dst,
double thresh,
double maxval,
int type )
參數:
src — 原圖像,8或32位浮點類型的Mat。
dst — 輸出圖像,與原始圖像具有相同大小和類型。
thresh — 要設定的閾值
maxval — 當type爲THRESH_BINARY或者THRESH_BINARY_INV時,設定的最大值
type — 閾值分割的類型
THRESH_BINARY;二值化閾值處理:灰度值大於閾值的點,將其灰度值設定爲最大值,灰度值小於或等於閾值的點,將其灰度值設定爲0
THRESH_BINARY_INV;反二值化閾值處理:灰度值大於閾值的點,將其灰度值設定爲0,灰度值小於或等於閾值的點,將其灰度值設定爲最大值
THRESH_TRUNC;截斷閾值化處理:灰度值大於閾值的點,將其灰度值設定爲閾值,灰度值小於或等於閾值的點,其灰度值保持不變
THRESH_TOZERO;低閾值零處理:灰度值大於閾值的點,其灰度值保持不變,灰度值小於或等於閾值的點,將其灰度值設定爲0
THRESH_TOZERO_INV;高閾值零處理:灰度值大於閾值的點,將其灰度值設定爲0,灰度值小於或等於閾值的點,其灰度值保持不變
如下表:
OTSU(大津法)
使用threshold進行閾值處理時,需要自定義一個閾值,並以此閾值作爲圖像閾值處理的依據 。通常情況下對於色彩均衡的圖像,直接將閾值設爲127即可,但有時圖像灰度級的分佈是不均衡的,如果此時還將閾值設爲127,那麼閾值處理的結果就是失敗的。所以需要找出圖像的最佳的分割閾值。OTSU就是獲得最佳閾值的方法。
OTSU(大津法)是一種確定圖像二值化分割閾值的算法,由日本學者大津於1979年提出。從大津法的原理上來講,該方法又稱作最大類間方差法,因爲按照大津法求得的閾值進行圖像二值化分割後,前景與背景圖像的類間方差最大。
它被認爲是圖像分割中閾值選取的最佳算法,計算簡單,不受圖像亮度和對比度的影響,因此在數字圖像處理上得到了廣泛的應用。它是按圖像的灰度特性,將圖像分成背景和前景兩部分。因方差是灰度分佈均勻性的一種度量,背景和前景之間的類間方差越大,說明構成圖像的兩部分的差別越大,當部分前景錯分爲背景或部分背景錯分爲前景都會導致兩部分差別變小。因此,使類間方差最大的分割意味着錯分概率最小。
OTSU 是求圖像全局閾值的最佳方法,適用於大部分需要求圖像全局閾值的場合。
缺點:對圖像噪聲敏感;只能針對單一目標分割;當圖像中的目標與背景的面積相差很大時,表現爲直方圖沒有明顯的雙峯,或者兩個峯的大小相差很大,分割效果不佳,或者目標與背景的灰度有較大的重疊時也不能準確的將目標與背景分開。導致這種現象出現的原因是該方法忽略了圖像的空間信息,同時該方法將圖像的灰度分佈作爲分割圖像的依據,因而對噪聲也相當敏感。所以,在實際應用中,總是將其與其他方法結合起來使用。
圖像直方圖
效果:
圖像直方圖:
效果:
OTSU求閾值過程:
假設圖像的像素個數爲M×N。假設存在閾值T將圖像所有像素分爲兩類C1(像素值小於T)和C2(像素值大於T)。假設圖片背景較暗,則C1類爲背景,C2類爲前景。像素被分爲C1和C2類的概率分別爲p1、p2。圖像中屬於C1類的像素個數記作N1,其平均灰度;屬於C2類的的像素個數記作N2,其平均灰度爲。圖像的總平均灰度記爲,類間方差記爲。
因此有如下關係式:
把帶入類間方差公式,化簡,可以得到:
L爲灰度級數,爲灰度級爲的像素點數
小於或等於灰度級K的累加均值爲:
所以,
類間方差公式可以化爲:
求得使最大的灰度級K,就是OTSU的閾值。OTSU方法會遍歷所有灰度級,找到最佳閾值。
自適應閾值處理
前面介紹了OTSU算法,但這算法還屬於全局閾值法,即整張圖片只有一個閾值。所以對於某些光照不均的圖像,這種方法無法得到清晰有效的閾值分割結果圖像,如下圖:
顯然,這樣的閾值處理結果不是我們想要的,所以需要使用變化的閾值對圖像進行分割,這種技術稱爲自適應閾值處理方式。它的思想不是計算全局圖像的閾值,而是根據圖像不同區域亮度分佈,計算其局部閾值,所以對於圖像不同區域,能夠自適應計算不同的閾值,因此被稱爲自適應閾值法。
確定局部閾值的方法:計算每個像素點周圍臨近區域的加權平均值獲得閾值,並使用該閾值對該像素點進行處理。
adaptiveThreshold函數
OpenCV提供了adaptiveThreshold函數實現自適應閾值處理。
void adaptiveThreshold(InputArray src, OutputArray dst,
double maxValue,
int adaptiveMethod,
int thresholdType,
int blockSize, double C)
參數:
src — 原圖像,8或32位浮點類型的Mat。必須爲單通道灰度圖像。
dst — 輸出圖像,與原始圖像具有相同大小和類型。
maxValue — 像素最大值
adaptiveMethod — 自適應方法,只有兩種:THRESH_BINARY 和THRESH_BINARY_INV。
thresholdType — 閾值計算方式,有兩種:
ADAPTIVE_THRESH_MEAN_C:計算方法是計算出領域內的像素平均值再減去C的值
ADAPTIVE_THRESH_GAUSSIAN_C:計算方法是計算出領域內像素的高斯均值再減去C的值
blockSize — 表示一個像素在計算閾值時使用的鄰域尺寸,通常爲3、5、7。
C — 常數,用均值或高斯計算閾值後,再減去C就是最終閾值。
基於OpenCV的實現
c++實現
1. 閾值處理
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
//載入圖像
Mat img = imread("D:\\yt\\picture\\threshold\\s.jpg");
if (img.empty())
{
cout << "Error: Could not load image" << endl;
return 0;
}
Mat gray;
cvtColor(img, gray, COLOR_BGR2GRAY);//先轉爲灰度圖
Mat dst1,dst2,dst3,dst4,dst5;
threshold(gray, dst1, 127, 255, THRESH_BINARY);//二值化閾值處理
threshold(gray, dst2, 127, 255, THRESH_BINARY_INV);//反二值化閾值處理
threshold(gray, dst3, 127, 255, THRESH_TRUNC);//截斷閾值化處理
threshold(gray, dst4, 127, 255, THRESH_TOZERO_INV);//超閾值零處理
threshold(gray, dst5, 127, 255, THRESH_TOZERO);//低閾值零處理
//顯示圖像
imshow("gray_image", gray);
imshow("THRESH_BINARY", dst1);
imshow("THRESH_BINARY_INV", dst2);
imshow("THRESH_TRUNC", dst3);
imshow("THRESH_TOZERO_INV", dst4);
imshow("THRESH_TOZERO", dst5);
waitKey(0);
return 0;
}
效果
二值化閾值處理:
反二值化閾值處理:
截斷閾值化處理:
超閾值零處理:
低閾值零處理:
2. OTSU處理
在OpenCV中,設定參數type爲“THRESH_OTSU”即可實現OTSU方式的閾值分割。且設定閾值thresh爲0。
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
//載入圖像
Mat img = imread("D:\\yt\\picture\\threshold\\s.jpg");
if (img.empty())
{
cout << "Error: Could not load image" << endl;
return 0;
}
Mat gray;
cvtColor(img, gray, COLOR_BGR2GRAY);//先轉爲灰度圖
Mat dst;
threshold(gray, dst, 0, 255, THRESH_OTSU);//OTSU
//顯示圖像
imshow("gray_image", gray);
imshow("THRESH_OTSU", dst);
waitKey(0);
return 0;
}
效果
3. 自適應閾值處理
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
//載入圖像
Mat img = imread("D:\\yt\\picture\\threshold\\1.jpg");
if (img.empty())
{
cout << "Error: Could not load image" << endl;
return 0;
}
Mat gray;
cvtColor(img, gray, COLOR_BGR2GRAY);//先轉爲灰度圖
Mat dst;
adaptiveThreshold(gray, dst, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 7, 10);
//創建窗口,WINDOW_NORMAL使窗口可以自由調節大小
namedWindow("gray_image",WINDOW_NORMAL);
namedWindow("adaptiveThreshold", WINDOW_NORMAL);
//顯示圖像
imshow("gray_image", gray);
imshow("adaptiveThreshold", dst);
waitKey(0);
return 0;
}
效果
進階實現(根據原理自己實現)
實現示例(c++)
1. OTSU處理
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
int Otsu(cv::Mat& src, cv::Mat& dst, int thresh){
const int Grayscale = 256;
int graynum[Grayscale] = { 0 };
int r = src.rows;
int c = src.cols;
for (int i = 0; i < r; ++i){
const uchar* ptr = src.ptr<uchar>(i);
for (int j = 0; j < c; ++j){ //直方圖統計
graynum[ptr[j]]++;
}
}
double P[Grayscale] = { 0 };
double PK[Grayscale] = { 0 };
double MK[Grayscale] = { 0 };
double srcpixnum = r*c, sumtmpPK = 0, sumtmpMK = 0;
for (int i = 0; i < Grayscale; ++i){
P[i] = graynum[i] / srcpixnum; //每個灰度級出現的概率
PK[i] = sumtmpPK + P[i]; //概率累計和
sumtmpPK = PK[i];
MK[i] = sumtmpMK + i*P[i]; //灰度級的累加均值
sumtmpMK = MK[i];
}
//計算類間方差
double Var=0;
for (int k = 0; k < Grayscale; ++k){
if ((MK[Grayscale-1] * PK[k] - MK[k])*(MK[Grayscale-1] * PK[k] - MK[k]) / (PK[k] * (1 - PK[k])) > Var){
Var = (MK[Grayscale-1] * PK[k] - MK[k])*(MK[Grayscale-1] * PK[k] - MK[k]) / (PK[k] * (1 - PK[k]));
thresh = k;
}
}
//閾值處理
src.copyTo(dst);
for (int i = 0; i < r; ++i){
uchar* ptr = dst.ptr<uchar>(i);
for (int j = 0; j < c; ++j){
if (ptr[j]> thresh)
ptr[j] = 255;
else
ptr[j] = 0;
}
}
return thresh;
}
int main(){
cv::Mat src = cv::imread("D:\\yt\\picture\\threshold\\1.jpg");
if (src.empty()){
return -1;
}
if (src.channels() > 1)
cv::cvtColor(src, src, CV_RGB2GRAY);
cv::Mat dst;
int thresh=0;
double t2 = (double)cv::getTickCount();
thresh=Otsu(src , dst, thresh); //Otsu
cv::namedWindow("src", CV_WINDOW_NORMAL);
cv::imshow("src", src);
cv::namedWindow("dst", CV_WINDOW_NORMAL);
cv::imshow("dst", dst);
cv::waitKey(0);
}
2. 自適應閾值處理
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
enum adaptiveMethod{meanFilter,gaaussianFilter,medianFilter};
void AdaptiveThreshold(cv::Mat& src, cv::Mat& dst, double Maxval, int Subsize, double c, adaptiveMethod method = meanFilter){
if (src.channels() > 1)
cv::cvtColor(src, src, CV_RGB2GRAY);
cv::Mat smooth;
switch (method)
{
case meanFilter:
cv::blur(src, smooth, cv::Size(Subsize, Subsize)); //均值濾波
break;
case gaaussianFilter:
cv::GaussianBlur(src, smooth, cv::Size(Subsize, Subsize),0,0); //高斯濾波
break;
case medianFilter:
cv::medianBlur(src, smooth, Subsize); //中值濾波
break;
default:
break;
}
smooth = smooth - c;
//閾值處理
src.copyTo(dst);
for (int r = 0; r < src.rows;++r){
const uchar* srcptr = src.ptr<uchar>(r);
const uchar* smoothptr = smooth.ptr<uchar>(r);
uchar* dstptr = dst.ptr<uchar>(r);
for (int c = 0; c < src.cols; ++c){
if (srcptr[c]>smoothptr[c]){
dstptr[c] = Maxval;
}
else
dstptr[c] = 0;
}
}
}
int main(){
cv::Mat src = cv::imread("D:\\yt\\picture\\threshold\\1.jpg");
if (src.empty()){
return -1;
}
if (src.channels() > 1)
cv::cvtColor(src, src, CV_RGB2GRAY);
cv::Mat dst;
AdaptiveThreshold(src, dst, 255, 21, 10, meanFilter);
cv::namedWindow("src", CV_WINDOW_NORMAL);
cv::imshow("src", src);
cv::namedWindow("dst", CV_WINDOW_NORMAL);
cv::imshow("dst", dst);
cv::waitKey(0);
}
python實現
與c++不同,python中函數cv2.threshold的返回值有兩個
retval,dst = cv2.threshold(src,thresh,maxval,type)
retval — 返回的閾值
dst — 閾值處理的輸出圖像
1. 二值化閾值處理
import cv2
if __name__ == "__main__":
img = cv2.imread('D:/yt/picture/threshold/lena.bmp')
t,dst1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY)
# 顯示圖像
cv2.imshow("origin image", img)
cv2.imshow("THRESH_BINARY", dst1)
# 保存圖像
cv2.imwrite("D:/yt/picture/threshold/THRESH_BINARY.bmp", dst1)
cv2.waitKey(0)
cv2.destroyAllWindows()
效果
2. 反二值化閾值處理
import cv2
if __name__ == "__main__":
img = cv2.imread('D:/yt/picture/threshold/lena.bmp')
t,dst1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY_INV)
# 顯示圖像
cv2.imshow("origin image", img)
cv2.imshow("THRESH_BINARY_INV", dst1)
# 保存圖像
cv2.imwrite("D:/yt/picture/threshold/THRESH_BINARY_INV.bmp", dst1)
cv2.waitKey(0)
cv2.destroyAllWindows()
效果
3. 截斷閾值化處理
import cv2
if __name__ == "__main__":
img = cv2.imread('D:/yt/picture/threshold/lena.bmp')
t,dst1 = cv2.threshold(img,127,255,cv2.THRESH_TRUNC)
# 顯示圖像
cv2.imshow("origin image", img)
cv2.imshow("THRESH_TRUNC", dst1)
# 保存圖像
cv2.imwrite("D:/yt/picture/threshold/THRESH_TRUNC.bmp", dst1)
cv2.waitKey(0)
cv2.destroyAllWindows()
效果
4. 超閾值零處理
import cv2
if __name__ == "__main__":
img = cv2.imread('D:/yt/picture/threshold/lena.bmp')
t,dst1 = cv2.threshold(img,127,255,cv2.THRESH_TOZERO_INV)
# 顯示圖像
cv2.imshow("origin image", img)
cv2.imshow("THRESH_TOZERO_INV", dst1)
# 保存圖像
cv2.imwrite("D:/yt/picture/threshold/THRESH_TOZERO_INV.bmp", dst1)
cv2.waitKey(0)
cv2.destroyAllWindows()
效果
5. 低閾值零處理
import cv2
if __name__ == "__main__":
img = cv2.imread('D:/yt/picture/threshold/lena.bmp')
t,dst1 = cv2.threshold(img,127,255,cv2.THRESH_TOZERO)
# 顯示圖像
cv2.imshow("origin image", img)
cv2.imshow("THRESH_TOZERO", dst1)
# 保存圖像
cv2.imwrite("D:/yt/picture/threshold/THRESH_TOZERO.bmp", dst1)
cv2.waitKey(0)
cv2.destroyAllWindows()
效果
6. OTSU處理
在OpenCV中,給參數type多傳遞一個參數“THRESH_OTSU”即可實現OTSU方式的閾值分割。且設定閾值thresh爲0。
import cv2
import numpy as np
if __name__ == "__main__":
img = cv2.imread('D:/yt/picture/threshold/tiffany.bmp')
img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)#原圖像不是灰度圖,必須先轉換爲灰度圖
#普通二值化閾值處理
t1, dst1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY)
#採用OTSU的處理
t2, dst2 = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
# 創建窗口
cv2.namedWindow("origin image",cv2.WINDOW_NORMAL)#cv2.WINDOW_NORMAL使窗口大小可調整
cv2.namedWindow("THRESH_TOZERO",cv2.WINDOW_NORMAL)
cv2.namedWindow("THRESH_OTSU",cv2.WINDOW_NORMAL)
# 顯示圖像
cv2.imshow("origin image", img)
cv2.imshow("THRESH_TOZERO", dst1)
cv2.imshow("THRESH_OTSU",dst2)
cv2.waitKey(0)
cv2.destroyAllWindows()
效果:
7. 自適應閾值處理
import cv2
import numpy as np
if __name__ == "__main__":
img = cv2.imread('D:/yt/picture/threshold/computer.jpg')
img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)#原圖像不是灰度圖,必須先轉換爲灰度圖
#普通二值化閾值處理
t1, dst1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY)
#自適應閾值處理,採用均值計算閾值
dst2 = cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY,5,3)
#自適應閾值處理,採用高斯均值計算閾值
dst3 = cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C,cv2.THRESH_BINARY,5,3)
# 創建窗口
cv2.namedWindow("origin image",cv2.WINDOW_NORMAL)#cv2.WINDOW_NORMAL使窗口大小可調整
cv2.namedWindow("THRESH_BINARY",cv2.WINDOW_NORMAL)
cv2.namedWindow("MEAN_C",cv2.WINDOW_NORMAL)
cv2.namedWindow("GAUSSIAN_C", cv2.WINDOW_NORMAL)
# 顯示圖像
cv2.imshow("origin image", img)
cv2.imshow("THRESH_BINARY", dst1)
cv2.imshow("MEAN_C",dst2)
cv2.imshow("GAUSSIAN_C", dst3)
cv2.waitKey(0)
cv2.destroyAllWindows()
效果
本文電子版 後臺回覆 圖像分割 獲取
““感謝你的在看,點贊,分享三連↓