OpenCV 4.11.0
开源计算机视觉库
加载中…
搜索中…
无匹配项
图像阈值化

目标

简单阈值化

这里,问题很简单。对于每个像素,都应用相同的阈值。如果像素值小于或等于阈值,则将其设置为0,否则将其设置为最大值。函数cv.threshold用于应用阈值化。第一个参数是源图像,它应该是灰度图像。第二个参数是用于对像素值进行分类的阈值。第三个参数是分配给超过阈值的像素值的 最大值。OpenCV 提供不同类型的阈值化,由函数的第四个参数给出。上面描述的基本阈值化是使用类型 cv.THRESH_BINARY 完成的。所有简单的阈值类型为:

请参阅这些类型的文档以了解其区别。

该方法返回两个输出。第一个是使用的阈值,第二个输出是阈值化后的图像

此代码比较了不同的简单阈值类型

import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('gradient.png', cv.IMREAD_GRAYSCALE)
assert img is not None, "文件无法读取,请使用os.path.exists()检查"
ret,thresh1 = cv.threshold(img,127,255,cv.THRESH_BINARY)
ret,thresh2 = cv.threshold(img,127,255,cv.THRESH_BINARY_INV)
ret,thresh3 = cv.threshold(img,127,255,cv.THRESH_TRUNC)
ret,thresh4 = cv.threshold(img,127,255,cv.THRESH_TOZERO)
ret,thresh5 = cv.threshold(img,127,255,cv.THRESH_TOZERO_INV)
titles = ['原始图像','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',vmin=0,vmax=255)
plt.title(titles[i])
plt.xticks([]),plt.yticks([])
plt.show()
CV_EXPORTS_W Mat imread(const String &filename, int flags=IMREAD_COLOR_BGR)
从文件中加载图像。
double threshold(InputArray src, OutputArray dst, double thresh, double maxval, int type)
对每个数组元素应用固定级别的阈值。
注意
为了绘制多幅图像,我们使用了plt.subplot()函数。更多细节请查看matplotlib文档。

代码产生以下结果

图像

自适应阈值化

在上一节中,我们使用了一个全局值作为阈值。但这在所有情况下可能都不好,例如,如果图像在不同区域具有不同的照明条件。在这种情况下,自适应阈值化可以提供帮助。在这里,算法根据其周围的小区域确定像素的阈值。因此,我们为同一图像的不同区域获得不同的阈值,这对于照明变化的图像提供了更好的结果。

除了上面描述的参数外,方法cv.adaptiveThreshold 还有三个输入参数:

adaptiveMethod决定如何计算阈值

blockSize确定邻域区域的大小,而C是从邻域像素的平均值或加权和中减去的常数。

下面的代码比较了具有不同照明的图像的全局阈值化和自适应阈值化。

import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('sudoku.png', cv.IMREAD_GRAYSCALE)
assert img is not None, "文件无法读取,请使用os.path.exists()检查"
img = cv.medianBlur(img,5)
ret,th1 = cv.threshold(img,127,255,cv.THRESH_BINARY)
th2 = cv.adaptiveThreshold(img,255,cv.ADAPTIVE_THRESH_MEAN_C,\
cv.THRESH_BINARY,11,2)
th3 = cv.adaptiveThreshold(img,255,cv.ADAPTIVE_THRESH_GAUSSIAN_C,\
cv.THRESH_BINARY,11,2)
cv.THRESH_BINARY,11,2)
titles = ['原始图像', '全局阈值化 (v = 127)',
'自适应均值阈值化', '自适应高斯阈值化']
images = [img, th1, th2, th3]
for i in range(4)
plt.title(titles[i])
plt.xticks([]),plt.yticks([])
plt.show()
cv::medianBlur
void medianBlur(InputArray src, OutputArray dst, int ksize)
void adaptiveThreshold(InputArray src, OutputArray dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C)
对数组应用自适应阈值。

结果

图像

大津二值化 (Otsu's Binarization)

在全局阈值化中,我们使用任意选择的数值作为阈值。相比之下,大津法避免了选择阈值的过程,而是自动确定阈值。

考虑一幅只有两个不同像素值的图像(*双峰图像*),其直方图只包含两个峰值。一个好的阈值应该在这两个值之间。

类似地,大津法从图像直方图中确定最佳全局阈值。为此,使用cv.threshold()函数,其中cv.THRESH_OTSU作为额外的标志传递。阈值可以任意选择。然后算法找到最佳阈值,并将其作为第一个输出返回。

查看下面的示例。输入图像是一幅噪声图像。在第一种情况下,应用了值为 127 的全局阈值化。在第二种情况下,直接应用了大津阈值化。在第三种情况下,首先使用 5x5 高斯核对图像进行滤波以去除噪声,然后应用大津阈值化。看看噪声滤波如何改善结果。

import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('noisy2.png', cv.IMREAD_GRAYSCALE)
assert img is not None, "文件无法读取,请使用os.path.exists()检查"
# 全局阈值化
ret1,th1 = cv.threshold(img,127,255,cv.THRESH_BINARY)
# 大津阈值化
ret2,th2 = cv.threshold(img,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
# 高斯滤波后的大津阈值化
blur = cv.GaussianBlur(img,(5,5),0)
ret3,th3 = cv.threshold(blur,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
# 绘制所有图像及其直方图
images = [img, 0, th1,
img, 0, th2,
blur, 0, th3]
titles = ['原始噪声图像','直方图','全局阈值化 (v=127)',
'原始噪声图像','直方图','大津阈值化',
'高斯滤波图像','直方图','大津阈值化']
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()
void GaussianBlur(InputArray src, OutputArray dst, Size ksize, double sigmaX, double sigmaY=0, int borderType=BORDER_DEFAULT, AlgorithmHint hint=cv::ALGO_HINT_DEFAULT)
使用高斯滤波器模糊图像。

结果

图像

大津二值化是如何工作的?

本节演示了大津二值化的 Python 实现,以展示其实际工作原理。如果您不感兴趣,可以跳过此部分。

由于我们处理的是双峰图像,大津算法尝试找到一个阈值 (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_2(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_2(t)]^2 \frac{P(i)}{q_2(t)}\]

它实际上找到一个位于两个峰值之间的 t 值,使得两个类的方差最小。它可以用 Python 简单地实现如下:

img = cv.imread('noisy2.png', cv.IMREAD_GRAYSCALE)
assert img is not None, "文件无法读取,请使用os.path.exists()检查"
blur = cv.GaussianBlur(img,(5,5),0)
# 查找归一化直方图及其累积分布函数
hist = cv.calcHist([blur],[0],None,[256],[0,256])
hist_norm = hist.ravel()/hist.sum()
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]) # 概率
q1,q2 = Q[i],Q[255]-Q[i] # 类别的累积和
if q1 < 1.e-6 or q2 < 1.e-6
continue
b1,b2 = np.hsplit(bins,[i]) # 权重
# 查找均值和方差
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
# 计算最小化函数
fn = v1*q1 + v2*q2
if fn < fn_min
fn_min = fn
thresh = i
# 使用 OpenCV 函数查找大津阈值
ret, otsu = cv.threshold(blur,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
print( "{} {}".format(thresh,ret) )
void calcHist(const Mat *images, int nimages, const int *channels, InputArray mask, OutputArray hist, int dims, const int *histSize, const float **ranges, bool uniform=true, bool accumulate=false)
计算一组数组的直方图。

附加资源

  1. 数字图像处理,Rafael C. Gonzalez

练习

  1. 大津二值化有一些可用的优化方法。您可以搜索并实现它。