本教程将学习如下内容:
- 学习简单阈值化、自适应阈值化、Otsu阈值化等
- 介绍两个关键函数:
cv2.threshold
,cv2.adaptiveThreshold
等
简单阈值化
接下来处理的事情很简单。如果像素值大于阈值,则分配一个值(可能是白色),否则分配另一个值,可能是黑色。
使用的函数是cv2.threshold
。第一个参数是源图像,它应该是灰度图像。
第二个参数是用于对像素值进行分类的阈值。第三个参数是maxVal
,它表示像素值大于(有时小于)阈值时要给出的值。
OpenCV提供了不同风格的阈值处理,由函数的第四个参数决定。不同类型有:
cv2.THRESH_BINARY
cv2.THRESH_BINARY_INV
cv2.THRESH_TRUNC
cv2.THRESH_TOZERO
cv2.THRESH_TOZERO_INV
文档清楚地解释了每种类型的含义。 获得两个输出。第一个是返回,稍后将对此进行解释。第二个输出是阈值图像。
代码:
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('/data/cvdata/gradient.png',0)
ret,thresh1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY)
ret,thresh2 = cv2.threshold(img,127,255,cv2.THRESH_BINARY_INV)
ret,thresh3 = cv2.threshold(img,127,255,cv2.THRESH_TRUNC)
ret,thresh4 = cv2.threshold(img,127,255,cv2.THRESH_TOZERO)
ret,thresh5 = cv2.threshold(img,127,255,cv2.THRESH_TOZERO_INV)
titles = ['Original Image','BINARY','BINARY_INV','TRUNC','TOZERO','TOZERO_INV']
images = [img, thresh1, thresh2, thresh3, thresh4, thresh5]
for i in range(6):
plt.subplot(2,3,i+1),plt.imshow(images[i],'gray')
plt.title(titles[i])
plt.xticks([]),plt.yticks([])
plt.show()
注意: 为了绘制多个图像,使用 $plt.subplot()$函数。 请查看Matplotlib文档以了解更多详细信息。
结果如下:
自适应阈值
在上一节中使用全局值作为阈值。然而在图像在不同区域具有不同照明条件的所有条件下,它可能并不都是好的。 在这种情况下,采用自适应阈值。该算法计算图像小区域的阈值。 为同一图像的不同区域获得了不同的阈值,这为具有不同光照的图像提供了更好的结果。
它有三个“特殊”输入参数,只有一个输出参数。
- 自适应方法:决定如何计算阈值。
cv2.ADAPTIVE_THRESH_MEN_C
:阈值是邻域的平均值。cv2.ADAPTIVE_THRESH_GASSIAN_C
:阈值是邻域值的加权和,其中权重是高斯窗口。
Block Size
: 决定了街区的大小。C
: 只是一个常数,从计算的平均值或加权平均值中减去。
下面的代码比较了具有不同光照的图像的全局阈值和自适应阈值:
%matplotlib inline
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('/data/cvdata/dave.jpg',0)
# img = cv2.imread('/data/cvdata/apple.jpg', 0)
img = cv2.medianBlur(img,5)
ret,th1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY)
th2 = cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_MEAN_C,\
cv2.THRESH_BINARY,11,2)
th3 = cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C,\
cv2.THRESH_BINARY,11,2)
titles = ['Original Image', 'Global Thresholding (v = 127)',
'Adaptive Mean Thresholding', 'Adaptive Gaussian Thresholding']
images = [img, th1, th2, th3]
for i in range(4):
plt.subplot(2,2,i+1),plt.imshow(images[i],'gray')
plt.title(titles[i])
plt.xticks([]),plt.yticks([])
plt.show()
结果如下:
Otsu的二值化
在第一部分中已讲述过第二个参数retVal。 当使用Otsu的二进制化时,用途就出现了。
在全局阈值化中,使用任意值作为阈值, 那么如何才能知道选择的价值观好不好呢? 答案是,试错法。但是考虑一个双峰图像(简单地说,双峰图像是一个直方图有两个峰值的图像)。 对于该图像,可以近似地将这些峰值中间的一个值作为阈值,这就是Otsu二值化所做的。 因此,简单地说,它会根据双峰图像的图像直方图自动计算阈值。(对于非双峰图像,二值化将不准确。)
为此使用了cv2.threshold()
函数,但传递了一个额外的标志,
$cv2.THRESH_OTSU$。
对于阈值,只需传递零。然后,该算法找到最佳阈值,并作为第二个输出retVal返回。
如果不使用Otsu阈值,retVal与使用的阈值相同。
看看下面的例子。输入图像是有噪声的图像。在第一种情况下,对值127应用了全局阈值处理。 在第二种情况下,直接应用了Otsu的阈值处理。 在第三种情况下,使用5x5高斯核对图像进行滤波以去除噪声,之后应用Otsu阈值。 查看噪声滤波如何改善结果。
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('/data/cvdata/noisy2.png',0)
# img = cv2.imread('/data/cvdata/apple.jpg',0)
# global thresholding
ret1,th1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY)
# Otsu's thresholding
ret2,th2 = cv2.threshold(img,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
# Otsu's thresholding after Gaussian filtering
blur = cv2.GaussianBlur(img,(5,5),0)
ret3,th3 = cv2.threshold(blur,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
# plot all the images and their histograms
images = [img, 0, th1,
img, 0, th2,
blur, 0, th3]
titles = ['Original Noisy Image','Histogram','Global Thresholding (v=127)',
'Original Noisy Image','Histogram',"Otsu's Thresholding",
'Gaussian filtered Image','Histogram',"Otsu's Thresholding"]
for i in range(3):
plt.subplot(3,3,i*3+1),plt.imshow(images[i*3],'gray')
plt.title(titles[i*3]), plt.xticks([]), plt.yticks([])
plt.subplot(3,3,i*3+2),plt.hist(images[i*3].ravel(),256)
plt.title(titles[i*3+1]), plt.xticks([]), plt.yticks([])
plt.subplot(3,3,i*3+3),plt.imshow(images[i*3+2],'gray')
plt.title(titles[i*3+2]), plt.xticks([]), plt.yticks([])
plt.show()
结果如下:
Otsu的二元化如何运作
本节演示了Otsu二值化的Python实现,以展示它的实际工作原理。 如果不感兴趣可以跳过。 由于使用的是双峰图像,Otsu的算法试图找到一个阈值(t),该阈值最小化了由以下关系给出的加权类内方差:
$$\sigma_w^2(t) = q_1(t)\sigma_1^2(t)+q_2(t)\sigma_2^2(t)$$
其中:
$$q_1(t) = \sum_{i=1}^{t} P(i) \quad \& \quad q_1(t) = \sum_{i=t+1}^{I} P(i)$$$$\mu_1(t) = \sum_{i=1}^{t} \frac{iP(i)}{q_1(t)} \quad \& \quad \mu_2(t) = \sum_{i=t+1}^{I} \frac{iP(i)}{q_2(t)}$$$$\sigma_1^2(t) = \sum_{i=1}^{t} [i-\mu_1(t)]^2 \frac{P(i)}{q_1(t)} \quad \& \quad \sigma_2^2(t) = \sum_{i=t+1}^{I} [i-\mu_1(t)]^2 \frac{P(i)}{q_2(t)}$$
实际上找到了一个位于两个峰值之间的t值,使得两个类别的方差都是最小的。 可以简单地在Python中实现,如下所示:
img = cv2.imread('/data/cvdata/noisy2.png',0)
blur = cv2.GaussianBlur(img,(5,5),0)
# find normalized_histogram, and its cumulative distribution function
hist = cv2.calcHist([blur],[0],None,[256],[0,256])
hist_norm = hist.ravel()/hist.max()
Q = hist_norm.cumsum()
bins = np.arange(256)
fn_min = np.inf
thresh = -1
for i in range(1,256):
p1,p2 = np.hsplit(hist_norm,[i]) # probabilities
q1,q2 = Q[i],Q[255]-Q[i] # cum sum of classes
b1,b2 = np.hsplit(bins,[i]) # weights
# finding means and variances
m1,m2 = np.sum(p1*b1)/q1, np.sum(p2*b2)/q2
v1,v2 = np.sum(((b1-m1)**2)*p1)/q1,np.sum(((b2-m2)**2)*p2)/q2
# calculates the minimization function
fn = v1*q1 + v2*q2
if fn < fn_min:
fn_min = fn
thresh = i
# find otsu's threshold value with OpenCV function
ret, otsu = cv2.threshold(blur,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
print (thresh,ret)
118 117.0
/tmp/ipykernel_171/346094610.py:20: RuntimeWarning: invalid value encountered in scalar divide m1,m2 = np.sum(p1*b1)/q1, np.sum(p2*b2)/q2 /tmp/ipykernel_171/346094610.py:20: RuntimeWarning: divide by zero encountered in scalar divide m1,m2 = np.sum(p1*b1)/q1, np.sum(p2*b2)/q2 /tmp/ipykernel_171/346094610.py:21: RuntimeWarning: invalid value encountered in multiply v1,v2 = np.sum(((b1-m1)**2)*p1)/q1,np.sum(((b2-m2)**2)*p2)/q2