此系列的其他文章:
OpenCV算法學(xué)習(xí)筆記之初識(shí)OpenCV
OpenCV算法學(xué)習(xí)筆記之幾何變換
OpenCV算法學(xué)習(xí)筆記之對(duì)比度增強(qiáng)
OpenCV算法學(xué)習(xí)筆記之平滑算法
OpenCV算法學(xué)習(xí)筆記之形態(tài)學(xué)處理
OpenCV算法學(xué)習(xí)筆記之邊緣檢測(cè)(一)
OpenCV算法學(xué)習(xí)筆記之邊緣處理(二)
OpenCV算法學(xué)習(xí)筆記之形狀檢測(cè)
更多文章可以訪(fǎng)問(wèn)我的博客Aengus | Blog
閾值分割也被稱(chēng)為二值化處理宾濒,簡(jiǎn)單來(lái)說(shuō)就是將圖像灰度值大于(或小于)閾值的像素調(diào)整為255可都,將小于(或大于)閾值的像素灰度值調(diào)整為0,這樣最終輸出圖像只有兩種灰度值(255和0)截碴。對(duì)對(duì)比度比較弱的圖像進(jìn)行閾值分割前常常要進(jìn)行對(duì)比度增強(qiáng)肴楷。常見(jiàn)的閾值分割技術(shù)有全局閾值分割和自適應(yīng)局部閾值分割。
全局閾值分割
原理
顧名思義,全局閾值分割就是將整個(gè)圖像灰度值大于閾值(thresh)的像素調(diào)為白色,小于或等于閾值的調(diào)整為黑色拧略,也可以反過(guò)來(lái)。用公式表達(dá)就是:
其中代表輸出圖像位于第
行第
列的像素的灰度值瘪弓,
代表輸入圖像位于第
行第
列的像素的灰度值。
實(shí)現(xiàn)
用Python實(shí)現(xiàn)就是
import cv2 as cv
import numpy as np
src = cv.imread("inputImage.png")
thresh = int(input())
dst = src.copy() # 一般不處理原圖
dst[dst>thresh] = 255
dst[dst<=thresh] = 0
OpenCV提供函數(shù)threshold(InputArray src, OutputArray dst, double thresh, double maxval, int type)
實(shí)現(xiàn)了全局閾值分割禽最,其中maxval
是圖像二值化顯示時(shí)的最大值腺怯,一般設(shè)置為255;type
為類(lèi)型川无,有如下取值:
enum ThresholdTypes {
THRESH_BINARY = 0, // 大于thresh的設(shè)為maxval
THRESH_BINARY_INV = 1, // 大于thresh的設(shè)為0
THRESH_TRUNC = 2, // 大于thresh的使用threshold函數(shù)呛占,小于等于的不處理
THRESH_TOZERO = 3, // 大于thresh的不處理,小于等于的設(shè)為0
THRESH_TOZERO_INV = 4, // 大于thresh的設(shè)為0懦趋,小于等于的不處理
THRESH_MASK = 7,
THRESH_OTSU = 8, // 使用OTSU算法計(jì)算出閾值
THRESH_TRIANGLE = 16 // 使用TRIANGLE算法計(jì)算出閾值晾虑,與下文的直方圖技術(shù)法原理類(lèi)似
};
最后兩個(gè)的算法之后再解釋?zhuān)覀兺ǔS眠@兩個(gè)與THRESH_BINARY
結(jié)合使用,如type=THRESH_OTSU+THRESH_BINARY
代表先用OTSU算法計(jì)算出閾值后再根據(jù)THRESH_BINARY
規(guī)則進(jìn)行分割仅叫。
自動(dòng)求取閾值
閾值分割的核心是如何選取閾值帜篇,選取正確的閾值是分割成功的關(guān)鍵。常用的自動(dòng)求全局閾值的算法有直方圖技術(shù)法诫咱,Otsu算法笙隙,熵算法等。
直方圖技術(shù)法
原理
一般來(lái)說(shuō)坎缭,一幅圖如果前景和背景對(duì)比比較明顯的話(huà)竟痰,那么它的直方圖則會(huì)有兩個(gè)比較明顯的峰值签钩。兩個(gè)峰值對(duì)應(yīng)物體內(nèi)部和外部較多數(shù)目的點(diǎn),兩個(gè)峰值之間的波谷對(duì)應(yīng)物體邊緣附近相對(duì)較少數(shù)目的點(diǎn)坏快。直方圖技術(shù)就是首先找到這兩個(gè)峰值铅檩,然后取兩個(gè)峰值之間的波谷位置對(duì)應(yīng)的灰度值,取其作為閾值莽鸿。
上述過(guò)程可能會(huì)有干擾昧旨。由于灰度值在直方圖中的隨機(jī)波動(dòng),兩個(gè)波峰(局部最大值)和它們之間的波谷不能很好的確定富拗,比如兩個(gè)峰值之間可能出現(xiàn)兩個(gè)最小值臼予,為了解決這種干擾, 一般對(duì)直方圖進(jìn)行高斯平滑啃沪,逐漸增加高斯濾波的標(biāo)準(zhǔn)差粘拾,直到能從平滑后的直方圖只存在兩個(gè)唯一的波峰和它們之間唯一的最小值,但這種方式需要手動(dòng)調(diào)節(jié)创千,可以通過(guò)以下方式進(jìn)行自動(dòng)選取波峰和波谷:
第一步:找到灰度直方圖的第一個(gè)峰值缰雇,并找到其對(duì)應(yīng)的灰度值。顯然追驴,灰度直方圖的最大值就是第一個(gè)峰值械哟,將其對(duì)應(yīng)的灰度值用fristPeak表示;
第二步:找到直方圖的第二個(gè)峰值殿雪,并找到其對(duì)應(yīng)的灰度值暇咆,第二個(gè)峰值不一定是第二大值,因?yàn)樗芸赡艹霈F(xiàn)第一個(gè)峰值的附近丙曙,可以用以下公式計(jì)算:
也可以用絕對(duì)值的形式:
其中代表灰度值等于
的像素的數(shù)量爸业;
第三步:找到這兩個(gè)峰值之間的波谷,如果出現(xiàn)兩個(gè)或多個(gè)波谷亏镰,則取左側(cè)的波谷即可扯旷,其對(duì)應(yīng)灰度值即為閾值。
Python實(shí)現(xiàn)
在利用直方圖計(jì)算閾值時(shí)索抓,會(huì)計(jì)算一個(gè)直方圖最大值的位置索引钧忽,可以利用numpy的where()
函數(shù)返回值等于某個(gè)數(shù)的索引,如
import numpy as np
hist = np.array([1, 3, 9, 2, 8])
max_loc = np.where(hist == np.max(hist)) # max_loc = (array([2], dtype=int64))
如果出現(xiàn)多個(gè)最大值逼肯,那么會(huì)返回存儲(chǔ)最大值的所有位置索引的一維數(shù)組組成的元組耸黑。
下面的函數(shù)是利用直方圖法自動(dòng)求取閾值:
def thresh_two_peaks(image):
"""
:param image: 輸入圖像
:return 閾值和閾值分割后結(jié)果組成的二元元組
"""
# 計(jì)算灰度直方圖
histogram = calcGrayHist(image)
# 找到灰度直方圖的最大峰值對(duì)應(yīng)的灰度值
max_loc = np.where(histogram == np.max(histogram))
first_peak = max_loc[0][0]
# 尋找灰度直方圖第二個(gè)峰值對(duì)應(yīng)的灰度值
measure_dists = np.zeros([256], np.float32)
for k in range(256):
measure_dists[k] = pow(k-first_peak, 2) * histogram[k]
max_loc2 = np.where(measure_dists == np.max(measure_dists))
second_peak = max_loc2[0][0]
# 找到兩個(gè)峰之間的最小值對(duì)應(yīng)的灰度值,作為閾值
thresh = 0
# 第一個(gè)峰值在第二個(gè)峰值的右邊
if first_peak > second_peak:
temp = histogram[int(second_peak): int(first_peak)]
min_loc = np.where(temp == np.min(temp))
thresh = second_peak + min_loc[0][0] + 1
else:
temp = histogram[int(first_peak): int(second_peak)]
min_loc = np.where(temp == np.min(temp))
thresh = first_peak + min_loc[0][0] + 1
# 找到閾值后進(jìn)行閾值處理汉矿,得到二值圖
thresh_img = image.copy()
thresh_img[thresh_img > thresh] = 255
thresh_img[thresh_img <= thresh] = 0
return (thresh, thresh_img)
上面使用的函數(shù)calcGrayHist()
是在《OpenCV算法學(xué)習(xí)筆記之對(duì)比度增強(qiáng)》中的函數(shù)崎坊。
需要注意的是在求兩個(gè)峰值之前的波谷時(shí),需要判斷第二個(gè)峰值是在第一個(gè)峰值的右側(cè)還是左側(cè)洲拇。
C++實(shí)現(xiàn)
int threshTwoPeaks(const Mat &image, Mat &threshOut)
{
// 計(jì)算灰度直方圖
Mat histtogram = calcGrayHist(image);
// 找到灰度直方圖中最大峰值對(duì)應(yīng)的灰度值
Point firstPeakLoc;
minMaxLoc(histogram, NULL, NULL, NULL, &firstPeakLoc);
int firstPeak = firstPeakLoc.x;
// 尋找灰度直方圖第二個(gè)峰值對(duì)應(yīng)的灰度值
Mat measureDists = Mat::zeros(Size(256, 1), CV_32FC1);
for (int k=0; k<256; k++){
int histK = histogram.at<int>(0, k);
measureDists.at<float>(0, k) = pow(float(k - firstPeak), 2)*histK;
}
Point secondPeakLoc;
minMaxLoc(measureDists, NULL, NULL, NULL, &secondPeakLoc);
int secondPeak = secondPeakLoc.x;
// 找到兩個(gè)峰值之間最小值對(duì)應(yīng)的灰度值奈揍,作為閾值
Point threshLoc;
int thresh = 0;
// 第一個(gè)峰值在第二個(gè)峰值左側(cè)
if (firstPeak < secondPeak){
minMaxLoc(histogram.colRange(firstPeak, secondPeak), NULL, NULL, &threshLoc);
thresh = firstPeak + threshLoc.x + 1;
}else{
minMaxLoc(histogram.colRange(secondPeak, firstPeak), NULL, NULL, &threshLoc);
thresh = secondPeak + threshLoc.x + 1;
}
// 閾值分割
threshold(image, threshOut, thresh, 255, THRESH_BINARY);
return thresh;
}
Otsu算法
原理
Otsu其實(shí)是利用最大方差法曲尸。假設(shè)輸入圖像的高為
,寬為
男翰,
代表其歸一化所獲得的圖像灰度直方圖另患,
代表灰度值等于
的像素點(diǎn)的個(gè)數(shù)在圖像中占的比例,
蛾绎,則步驟為:
第一步:計(jì)算灰度直方圖的零階累積矩昆箕,也稱(chēng)為累加直方圖:
第二步:計(jì)算灰度直方圖的一階累積矩:
第三步:計(jì)算圖像總體的灰度平均值
,其實(shí)就是
時(shí)的一階累積矩租冠,即:
第四步:計(jì)算每一個(gè)灰度級(jí)作為閾值時(shí)鹏倘,前景區(qū)域的平均灰度、背景區(qū)域的平均灰度與整幅圖像的平均灰度的方差顽爹,對(duì)方差的衡量采用以下度量:
第五步:找到上述最大的纤泵,然后對(duì)應(yīng)的
即為Otsu自動(dòng)選取的閾值,即:
C++實(shí)現(xiàn)
需要注意的是在求方差時(shí)可能出現(xiàn)分母為0的情況镜粤。
/**
* Otsu算法
*
* @param image 輸入的單通道8位圖
* @param otsuThreshImage Otsu算法閾值分割后的圖像
* @return Otsu得到的閾值
*/
int otsu(const Mat &image, Mat &otsuThreshImage)
{
// 計(jì)算灰度直方圖
Mat histogram = calcGrayHist(image);
// 歸一化灰度直方圖
Mat normHist;
histogram.convertTo(normHist, CV_32FC1, 1.0/(image.rows*image.cols), 0.0);
// 計(jì)算累加直方圖(零階累積矩)和一階累積矩
Mat zeroCumuMoment = Mat::zeros(Size(256, 1), CV_32FC1);
Mat oneCumuMoment = Mat::zeros(Size(245, 1), CV_32FC1);
for (int i = 0; i < 256; i++)
{
if (i == 0)
{
zeroCumuMoment.at<float>(0, i) = normHist.at<float>(0, i);
oneCumuMoment.at<float>(0, i) = i * normHist.at<float>(0, i);
} else {
zeroCumuMoment.at<float>(0, i) = normHist.at<float>(0, i-1) + normHist.at<float>(0, i);
oneCumuMoment.at<float>(0, i) = normHist.at<float>(0, i-1) + i * normHist.at<float>(0, i);
}
}
// 計(jì)算類(lèi)間方差
Mat variance = Mat::zeros(Size(256, 1), CV_32FC1);
// 總平均值
float mean = oneCumuMoment.at<float>(0, 255);
for (int i = 0; i < 255; i++)
{
if (zeroCumuMoment.at<float>(0, i) == 0 || zeroCumuMoment.at<float>(0, i) == 1)
{
variance.at<float>(0, i) = 0;
} else {
float cofficient = zeroCumuMoment.at<float>(0, i) * (1.0-zeroCumuMoment.at<float>(0, i));
variance.at<float>(0, i) = pow(mean*zeroCumuMoment.at<float>(0, i)-oneCumuMoment.at<float>(0, i), 2.0) / cofficient;
}
}
// 找到閾值
Point maxLoc;
mimMaxLoc(variance, NULL, NULL, NULL, &maxLoc);
int otsuThresh = maxLoc.x;
// 閾值處理
threshold(image, otsuThreshImage, otsuThresh, 255, THRESH_BINARY);
return otsuThresh;
}
對(duì)于我們?cè)谧铋_(kāi)始的全局閾值分割里提到的OpenCV提供的函數(shù)threshold
中捏题,也可以將參數(shù)type
設(shè)置為THRESH_OTSU
以實(shí)現(xiàn)OTSU算法求取閾值。
熵算法
原理
信息熵的概念來(lái)源于信息論肉渴。
假設(shè)某個(gè)符號(hào)有
種取值公荧,分別為
,每種符號(hào)出現(xiàn)的概率分別為
同规,那么該符號(hào)的信息熵為
將8位圖圖片看作一種符號(hào)循狰,那么圖片就有256種灰度取值,設(shè)為圖片歸一化后的灰度直方圖券勺,每一種取值的概率為
晤揣,
,利用熵計(jì)算閾值的步驟為:
第一步:計(jì)算圖片的累加概率直方圖朱灿,也稱(chēng)為零階累積矩,記為:
第二步:計(jì)算各個(gè)灰度值的熵:
第三步:計(jì)算使最大值的
值钠四,該值記為需要的閾值盗扒,其中:
Python實(shí)現(xiàn)
在第二步的實(shí)現(xiàn)中,由于對(duì)數(shù)的自變量不能為0缀去,如果判斷侣灶,那么直接令
即可。具體代碼如下:
def thresh_entropy(image):
rows, cols = image.shape
# 計(jì)算灰度直方圖
gray_hist =calcGrayHist(image)
# 歸一化灰度直方圖缕碎,即概率直方圖
norm_gray_hist = gray_hist / float(rows*cols)
# 第一步:計(jì)算累加直方圖褥影,也稱(chēng)為零階累積矩
zero_cumu_moment = np.zeros([256], np.float32)
for k in range(256):
if k == 0:
zero_cumu_moment[k] = norm_fray_hist[k]
else:
zero_cumu_moment[k] = zero_cumu_moment[k-1] + norm_gray_hist[k]
# 第二步:計(jì)算各個(gè)灰度級(jí)的熵
entropy = np.zeros([256], np.float32)
for k in range(256):
if k == 0:
if norm_gray_hist[k] == 0:
entropy[k] = 0
else:
entropy[k] = -norm_gray_hist[k] * math.log10(norm_gray_hist[k])
else:
if norm_hist_gray[k] == 0:
entropy[k] = entropy[k-1]
else:
entropy[k] = entropy[k-1] -
norm_gray_hist[k]*math.log10(norm_hist_gray[k])
# 第三步:找閾值
f_t = np.zeros([256], np.float32)
ft1, ft2 = 0.0, 0.0
total_entropy = entropy[255]
for k in range(255):
# 找最大值
max_front = np.max(norm_gray_hist[0:k+1])
max_back = np.max(norm_gray_hist[k+1:256])
if max_front == 0 or zero_cumu_moment[k] == 0 or
max_front == 1 or zero_cumu_moment[k] ==1 or total_entropy == 0:
ft1 = 0
else:
ft1 = entropy[k] / total_entropy*(math.log10(zero_cumu_moment[k]) / math.log10(max_front))
if max_back == 0 or 1-zero_cumu_moment[k] == 0 or max_back == 1
or 1-zero_cumu_moment[k] == 1:
ft2 = 0
else:
if total_entropy == 0:
ft2 = (math.log10(1-zero_cumu_moment[k]) / math.log10(max_back))
else:
ft2 = (1-entropy[k]/total_entropy)*(math.log10(1-zero_cumu_moment[k])/math.log10(max_back))
f_t[k] = ft1 + ft2
# 找到最大值的索引,作為閾值
thresh_loc = np.where(ft == np.max(f_t))
thresh = thresh_loc[0][0]
# 閾值處理
threshold = np.copy(image)
threshold[threshold > thresh] = 255
threshold[threshold < thrseh] = 0
return threshold
自適應(yīng)閾值
對(duì)于不均勻光照的圖片咏雌,如果設(shè)定一個(gè)全局的閾值凡怎,可能只會(huì)得到一張局部分割效果好但是光照不足(充分)的地方效果不好校焦,為此我們可以利用自適應(yīng)閾值針對(duì)圖片的不同部分得到不同的閾值,這樣整張圖片的分割效果就會(huì)好很多统倒。
原理
利用不同的平滑算子可以計(jì)算出當(dāng)前像素為中心的鄰域的灰度“平均值”寨典,所以使用平滑處理后的輸出結(jié)果作為每個(gè)像素設(shè)置閾值的參考值。在自適應(yīng)閾值處理中房匆,平滑算子的尺寸決定了分割出來(lái)的物體尺寸耸成,如果濾波器的尺寸太小,那么估計(jì)出的局部閾值將不理想浴鸿。一般來(lái)說(shuō)井氢,平滑算子的寬度必須大于被識(shí)別物體的寬度,平滑算子尺寸越大岳链, 則結(jié)果能更好的的作為每個(gè)像素閾值的參考花竞,當(dāng)然也不能無(wú)限大。
假設(shè)輸入圖像為宠页,高為
左胞,寬為
,平滑算子的尺寸記為
且都為奇數(shù)举户。步驟如下:
第一步:對(duì)圖像進(jìn)行平滑處理烤宙,平滑后的結(jié)果記為,平滑可以使用均值平滑俭嘁、高斯平滑躺枕、中值平滑;
第二步:自適應(yīng)閾值矩陣供填,一般令
拐云;
第三步:利用局部閾值分割的規(guī)則
進(jìn)行閾值分割。
Python實(shí)現(xiàn)
def adaptive_thresh(image, win_size, ratio=0.15):
"""
自適應(yīng)閾值分割
:param image: 輸入圖像
:param win_size: 平滑算子尺寸
:param ratio: 比例系數(shù)
:return: 自適應(yīng)閾值分割后的結(jié)果
"""
# 第一步:對(duì)圖像進(jìn)行均值平滑近她,這里使用的是OpenCV提供的函數(shù)
image_mean = cv2.boxFilter(image, cv2.CV_32FC1, win_size)
# 第二步:原圖像與平滑結(jié)果做差
out = image - (1.0-ratio)*image_mean
# 第三步:當(dāng)差值大于或等于0時(shí)叉瘩,輸出值為255;反之為0
out[out >= 0] = 255
out[out < 0] = 0
out = out.astype(np.uint8)
return out
OpenCV提供自適應(yīng)閾值分割函數(shù)void adaptiveThreshold(InputArray src, OutputArray dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C)
粘捎,其參數(shù)如下表所示:
參數(shù) | 解釋 |
---|---|
src | 單通道矩陣薇缅,數(shù)據(jù)類(lèi)型為CV_8U |
dst | 輸出矩陣 |
maxValue | 與函數(shù)threshold類(lèi)似,一般取255 |
adaptiveMethod | ADAPTIVE_THRESH_MEAN_C: 采用均值平滑攒磨;ADAPTIVE_THRESH_GAUSSIAN_C: 采用高斯平滑 |
thresholdType | THRESH_BINARY, THRESH_BINARY_INV |
blockSize | 平滑算子的尺寸且為奇數(shù) |
C | 比例系數(shù) |
二值圖的邏輯運(yùn)算
“與”和“或”運(yùn)算
“與”運(yùn)算可以理解為集合的交集泳桦,結(jié)果是兩幅圖白色相交的部分;
“或”運(yùn)算可以理解為集合的并集娩缰,結(jié)果是兩幅圖白色區(qū)域的并集灸撰;
OpenCV提供函數(shù)bitwise_and
和bitwise_or
分別實(shí)現(xiàn)了這兩種運(yùn)算;
參考
《OpenCV算法精解——基于Python和C++》(張平)第六章