1.目的
在本教程中:
- 你会学到简单阈值法,自适应阈值法,以及 Otsu 阈值法(俗称大津法)等。
- 你会学到如下函数:**cv.threshold,cv.adaptiveThreshold** 等。
2.简单阈值法
此方法是直截了当的。如果像素值大于阈值,则会被赋为一个值(可能为白色),否则会赋为另一个值(可能为黑色)。使用的函数是 cv.threshold。第一个参数是源图像,它应该是灰度图像。第二个参数是阈值,用于对像素值进行分类。第三个参数是 maxval,它表示像素值大于(有时小于)阈值时要给定的值。opencv 提供了不同类型的阈值,由函数的第四个参数决定。不同的类型有:
- cv.THRESH_BINARY
- cv.THRESH_BINARY_INV
- cv.THRESH_TRUNC
- cv.THRESH_TOZERO
- cv.THRESH_TOZERO_INV
文档清楚地解释了每种类型的含义。请查看文档。
获得两个输出。第一个是 retval,稍后将解释。第二个输出是我们的阈值图像。
代码:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('gradient.png',0)
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 = ['Original Image','BINARY','BINARY_INV','TRUNC','TOZERO','TOZERO_INV']
images = [img, thresh1, thresh2, thresh3, thresh4, thresh5]
for i in xrange(6):plt.subplot(2,3,i+1),plt.imshow(images[i],'gray')plt.title(titles[i])plt.xticks([]),plt.yticks([])
plt.show()
在 OpenCV 中,这些是阈值化操作的类型,用于将灰度图像转换为二值图像或将灰度图像中的像素值根据阈值进行分割。阈值化是一种常见的图像处理技术,它根据像素值与设定阈值的比较结果,将像素值设置为0或最大值。
以下是这些阈值的类型及其作用:
cv.THRESH_BINARY
:如果像素值大于阈值,则将其设置为最大值(例如255),否则设置为0。这种类型的阈值化产生了一个完全黑白的二值图像。
cv.THRESH_BINARY_INV
:与cv.THRESH_BINARY
相反,如果像素值大于阈值,则将其设置为0,否则设置为最大值。这种类型的阈值化也会产生一个完全黑白的二值图像,但与cv.THRESH_BINARY
相比,黑色和白色的区域会互换。
cv.THRESH_TRUNC
:如果像素值大于阈值,则将其截断为阈值,否则保持不变。这种类型的阈值化不会产生二值图像,而是将阈值以上的像素值都设置为相同的值。
cv.THRESH_TOZERO
:如果像素值大于阈值,则保持不变,否则将其设置为0。这种类型的阈值化会将低于阈值的像素值变为黑色,而高于阈值的像素值保持其原始灰度。
cv.THRESH_TOZERO_INV
:与cv.THRESH_TOZERO
相反,如果像素值大于阈值,则将其设置为0,否则保持不变。这种类型的阈值化会将高于阈值的像素值变为黑色,而低于阈值的像素值保持其原始灰度。在使用这些阈值类型时,通常还会指定一个阈值值和一个最大值。阈值值是用于比较的值,而最大值是用于
cv.THRESH_BINARY
和cv.THRESH_BINARY_INV
类型的输出图像中的最大像素值。
3.自适应阈值
在前一节中,我们使用一个全局变量作为阈值。但在图像在不同区域具有不同照明条件的条件下,这可能不是很好。在这种情况下,我们采用自适应阈值。在此,算法计算图像的一个小区域的阈值。因此,我们得到了同一图像不同区域的不同阈值,对于不同光照下的图像,得到了更好的结果。
它有三个“特殊”输入参数,只有一个输出参数。
Adaptive Method-它决定如何计算阈值。
- cv.ADAPTIVE_THRESH_MEAN_C 阈值是指邻近地区的平均值。
- cv.ADAPTIVE_THRESH_GAUSSIAN_C 阈值是权重为高斯窗的邻域值的加权和。
Block Size-它决定了计算阈值的窗口区域的大小。
C-它只是一个常数,会从平均值或加权平均值中减去该值。
代码:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
# img = cv.imread('gradient.png',0)
img = r'D:\study\EmotionDetection_RealTime-master\data\data\te\04.jpg'
img = cv.imread(img,0)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)
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()
img = cv.medianBlur(img,5)
在 OpenCV 中,
cv.medianBlur
函数用于对图像进行中值滤波。中值滤波是一种非线性的数字图像滤波技术,它用像素点邻域内的中值来代替该像素点的值,从而消除图像中的椒盐噪声和斑点噪声,同时保持图像边缘清晰。在 OpenCV 中,可以使用
cv.adaptiveThreshold
函数来实现自适应阈值化。该函数的原型如下:cv.adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C)
参数说明:
src
:输入的灰度图像。maxValue
:用于阈值化操作的最大值,通常设置为 255。adaptiveMethod
:自适应方法,可以是cv.ADAPTIVE_THRESH_MEAN_C
或cv.ADAPTIVE_THRESH_GAUSSIAN_C
。
cv.ADAPTIVE_THRESH_MEAN_C
:计算局部区域的平均值作为阈值。cv.ADAPTIVE_THRESH_GAUSSIAN_C
:计算局部区域的加权平均值作为阈值,权重是一个高斯窗口。thresholdType
:阈值类型,与cv.threshold
函数中的阈值类型相同,例如cv.THRESH_BINARY
。blockSize
:局部区域的大小,必须是奇数,如 3、5、7 等。C
:从平均值或加权平均值中减去的常数,通常是一个正值。
输出图像:
4.otus二值化
在第一部分中,我告诉过您有一个参数 retval。当我们进行 Otsu 二值化时,它的用途就来了。那是什么?
在全局阈值化中,我们使用一个任意的阈值,对吗?那么,我们如何知道我们选择的值是好的还是不好的呢?答案是,试错法。但是考虑一个双峰图像(简单来说,双峰图像是一个直方图有两个峰值的图像)。对于那个图像,我们可以近似地取这些峰值中间的一个值作为阈值,对吗?这就是 Otsu 二值化所做的。所以简单来说,它会自动从双峰图像的图像直方图中计算出阈值。(对于非双峰图像,二值化将不准确。)
为此,我们使用了 cv.threshold 函数,但传递了一个额外的符号 cv.THRESH_OTSU 。对于阈值,只需传入零。然后,该算法找到最佳阈值,并作为第二个输出返回 retval。如果不使用 otsu 阈值,则 retval 与你使用的阈值相同。
查看下面的示例。输入图像是噪声图像。在第一种情况下,我应用了值为 127 的全局阈值。在第二种情况下,我直接应用 otsu 阈值。在第三种情况下,我使用 5x5 高斯核过滤图像以去除噪声,然后应用 otsu 阈值。查看噪声过滤如何改进结果。
代码:
import cv2 as cv
import numpy as np
from skimage import util
from matplotlib import pyplot as plt
# img = cv.imread('gradient.png',0)
img = r'D:\study\EmotionDetection_RealTime-master\data\data\te\01.jpg'
img = cv.imread(img,cv.IMREAD_GRAYSCALE)
cv.imshow('img',img)# 全局阈值
ret1,th1 = cv.threshold(img,127,255,cv.THRESH_BINARY)
# Otsu 阈值
ret2,th2 = cv.threshold(img,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
# 经过高斯滤波的 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 = ['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()
在您的代码中,您使用了 OpenCV 的 `cv.threshold` 函数来应用不同的阈值化方法。这里简要解释一下每个步骤:
1. `ret1, th1 = cv.threshold(img, 127, 255, cv.THRESH_BINARY)`:
- `img` 是输入图像。
- `127` 是阈值。
- `255` 是最大值。
- `cv.THRESH_BINARY` 是阈值类型,它将像素值设置为 0 或 255,具体取决于它们是否大于阈值。
- 返回值 `ret1` 是一个布尔值,表示阈值化操作是否成功。
- `th1` 是阈值化后的输出图像。
2. `ret2, th2 = cv.threshold(img, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)`:
- 与第一个步骤类似,但这次您使用了 `cv.THRESH_OTSU`。
- `cv.THRESH_OTSU` 是一个特殊的阈值类型,它会自动选择一个阈值,使得前景和背景之间的类间方差最大。
- `th2` 是应用 Otsu 阈值后的输出图像。
3. `blur = cv.GaussianBlur(img, (5, 5), 0)`:
- `img` 是输入图像。
- `(5, 5)` 是高斯滤波器的尺寸,它定义了滤波器的宽度和高度。
- `0` 是高斯核的标准差,它决定了滤波器的模糊程度。
- `blur` 是应用高斯滤波后的输出图像。
4. `ret3, th3 = cv.threshold(blur, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)`:
- 与第二个步骤类似,但这次您使用了经过高斯滤波的图像 `blur`。
- `th3` 是应用 Otsu 阈值后的输出图像。
请注意,`cv.threshold` 函数的第二个参数 `0` 通常是不必要的,因为它是默认值。您可以直接使用 `cv.threshold(img, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)`。
最后,`ret1`、`ret2` 和 `ret3` 都是布尔值,表示阈值化操作是否成功。`th1`、`th2` 和 `th3` 是阈值化后的输出图像。
可以看到是没什么区别,因为图像噪声很小,进行高斯滤波后变化不大
添加高斯噪声后,可以看到有明显的效果
加入噪声的代码: img = util.random_noise(img, mode='gaussian', mean=0, var=0.05) img = np.uint8(img * 255)在 OpenCV 中,
util.random_noise(img, mode='gaussian', mean=0, var=0.05)
函数的img
参数是一个图像数据,而util.random_noise
函数是添加高斯噪声的函数。当您调用
util.random_noise
函数并将img
作为第一个参数传递时,它会根据您指定的参数(在这个例子中是mode='gaussian'
、mean=0
和var=0.05
)为图像添加高斯噪声。
mode='gaussian'
指定使用高斯噪声模式。mean=0
指定噪声的均值(中心点),这里设置为 0,意味着噪声的平均值是 0。var=0.05
指定噪声的方差,它决定了噪声的强度。方差值越大,噪声越强。在这个例子中,方差为 0.05,意味着噪声强度较小。添加噪声后,
util.random_noise
函数将返回一个包含噪声的图像。这个图像的数据类型将与输入图像的数据类型相同。如果输入图像是一个浮点数图像,那么添加噪声后的输出也将是一个浮点数图像。如果输入图像是一个整数图像,那么添加噪声后的输出将是一个浮点数图像。因此,
util.random_noise(img, mode='gaussian', mean=0, var=0.05)
的输出结果将是一个浮点数图像,其中包含了根据指定参数添加的高斯噪声。
5. 二值化原理
由于我们使用的是双峰图像,因此 Otsu 的算法试图找到一个阈值(t),该阈值将由下式计算得到的类内加权方差最小化。
它实际上找到一个 T 值,它位于两个峰值之间,使得两个类的方差最小。它可以简单地在 python 中实现,如下所示:
img = cv.imread('noisy2.png',0)
blur = cv.GaussianBlur(img,(5,5),0)
# 找到归一化直方图还有累计分布函数
hist = cv.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 xrange(1,256):p1,p2 = np.hsplit(hist_norm,[i]) # 概率q1,q2 = Q[i],Q[255]-Q[i] # 类别总和b1,b2 = np.hsplit(bins,[i]) # 权重# f 找到均值与方差m1,m2 = np.sum(p1*b1)/q1, np.sum(p2*b2)/q2v1,v2 = np.sum(((b1-m1)**2)*p1)/q1,np.sum(((b2-m2)**2)*p2)/q2# 计算最小函数fn = v1*q1 + v2*q2if fn < fn_min:fn_min = fnthresh = i
# 用 OpenCV 函数的 otsu'阈值
ret, otsu = cv.threshold(blur,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
print( "{} {}".format(thresh,ret) )
Q = hist_norm.cumsum()
在您提供的代码片段中,您正在计算直方图的累积分布函数(CDF)。累积分布函数(CDF)是概率论中的一个概念,它给出了随机变量小于或等于某个值的概率。在图像处理中,累积分布函数通常用于阈值选择,特别是在 Otsu 阈值化方法中。
在您的代码中,`Q = hist_norm.cumsum()` 意味着:
- `hist_norm` 是归一化的直方图,其值范围在 [0, 1]。
- `hist_norm.cumsum()` 计算直方图的累积和,即从左到右遍历直方图,计算累积的概率。
这个累积和形成了一个新的数组 `Q`,其中 `Q[i]` 表示随机变量小于或等于 `i` 的概率。这个数组 `Q` 就是累积分布函数(CDF)。
累积分布函数在 Otsu 阈值化方法中的作用是帮助选择一个阈值,使得背景和前景之间的类间方差最大。Otsu 阈值化的基本思想是找到一个阈值,使得前景和背景之间的类间方差最大。在这个上下文中,类间方差可以通过计算前景和背景的概率分布的方差来估计。
请注意,Otsu 阈值化通常使用 OpenCV 的 `cv.threshold(blur, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)` 函数来实现,而不是手动计算累积分布函数。手动计算可能会导致不准确的结果,并且通常不如 OpenCV 的内置函数高效。
p1,p2 = np.hsplit(hist_norm,[i]) # 概率
在您提供的代码片段中,您似乎正在尝试实现一个自定义的 Otsu 阈值选择方法。在这个方法中,您使用了 NumPy 的 `np.hsplit` 函数来分割直方图。
`np.hsplit` 函数用于将一个数组水平分割成多个子数组。它接受一个数组和一个或多个分割点作为参数,并将数组分割成多个子数组。
在您的代码中,`p1, p2 = np.hsplit(hist_norm, [i])` 意味着:
- `hist_norm` 是归一化的直方图。
- `[i]` 是分割点,表示您想要将直方图分割成两部分,其中一部分包含小于或等于 `i` 的像素,另一部分包含大于 `i` 的像素。
函数 `np.hsplit` 返回两个子数组,分别对应于小于或等于 `i` 的像素和大于 `i` 的像素。这两个子数组分别被赋值给 `p1` 和 `q2`。
请注意,如果您想要使用 OpenCV 的 `cv.threshold` 函数来应用 Otsu 阈值,您应该使用 `cv.threshold(blur, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)`,而不是手动计算阈值。手动计算阈值可能会导致不准确的结果,并且通常不如 OpenCV 的内置函数高效。
q1,q2 = Q[i],Q[255]-Q[i]
在您提供的代码片段中,您似乎正在尝试实现一个自定义的 Otsu 阈值选择方法。在这个方法中,您使用了累积分布函数(CDF)来分割直方图,并计算两个类别的概率。
在 Otsu 阈值化方法中,累积分布函数(CDF)被用来分割直方图,从而得到两个类别的概率分布。具体来说,您正在计算两个类别(前景和背景)的概率。
在您的代码中,`q1, q2 = Q[i], Q[255] - Q[i]` 意味着:
- `Q` 是累积分布函数(CDF)。
- `i` 是分割点,表示您想要将直方图分割成两部分,其中一部分包含小于或等于 `i` 的像素,另一部分包含大于 `i` 的像素。
- `Q[i]` 表示随机变量小于或等于 `i` 的概率,即背景的概率。
- `Q[255] - Q[i]` 表示随机变量大于 `i` 的概率,即前景的概率。
这两个概率值 `q1` 和 `q2` 用于后续的计算,以估计前景和背景的方差。Otsu 阈值化的基本思想是找到一个阈值,使得前景和背景之间的类间方差最大。类间方差可以通过计算前景和背景的概率分布的方差来估计。
请注意,Otsu 阈值化通常使用 OpenCV 的 `cv.threshold(blur, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)` 函数来实现,而不是手动计算累积分布函数。手动计算可能会导致不准确的结果,并且通常不如 OpenCV 的内置函数高效。
b1,b2 = np.hsplit(bins,[i]) # 权重
在您提供的代码片段中,您似乎正在尝试实现一个自定义的 Otsu 阈值选择方法。在这个方法中,您使用了 NumPy 的 `np.hsplit` 函数来分割一个包含像素值的数组。
在您的代码中,`b1, b2 = np.hsplit(bins, [i])` 意味着:
- `bins` 是一个包含像素值的数组,这些像素值代表图像的灰度级。
- `[i]` 是分割点,表示您想要将像素值数组分割成两部分,其中一部分包含小于或等于 `i` 的像素值,另一部分包含大于 `i` 的像素值。
函数 `np.hsplit` 返回两个子数组,分别对应于小于或等于 `i` 的像素值和大于 `i` 的像素值。这两个子数组分别被赋值给 `b1` 和 `b2`。
在 Otsu 阈值化方法中,`b1` 和 `b2` 通常被称为权重,它们代表图像中不同灰度级像素的数量。这些权重被用来计算前景和背景的加权方差,从而找到最佳的阈值。
请注意,Otsu 阈值化通常使用 OpenCV 的 `cv.threshold(blur, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)` 函数来实现,而不是手动计算权重。手动计算可能会导致不准确的结果,并且通常不如 OpenCV 的内置函数高效。
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
在您提供的代码片段中,您似乎正在尝试实现一个自定义的 Otsu 阈值选择方法。在这个方法中,您正在计算两个类别(前景和背景)的均值和方差。
在 Otsu 阈值化方法中,均值和方差被用来估计前景和背景的加权方差,从而找到最佳的阈值。加权方差是类内方差和类间方差的组合,用于评估不同阈值下的分割质量。
在您的代码中,`m1, m2 = np.sum(p1*b1)/q1, np.sum(p2*b2)/q2` 意味着:
- `p1` 和 `p2` 是前景和背景的概率分布。
- `b1` 和 `b2` 是前景和背景的权重,即对应灰度级的像素数量。
- `q1` 和 `q2` 是前景和背景的概率。
这些概率和权重被用来计算两个类别的均值:
- `m1` 是前景的均值,计算方法是将前景的概率分布 `p1` 乘以前景的权重 `b1`,然后除以前景的概率 `q1`。
- `m2` 是背景的均值,计算方法是将背景的概率分布 `p2` 乘以背景的权重 `b2`,然后除以背景的概率 `q2`。
接着,您计算了两个类别的方差:
- `v1` 是前景的方差,计算方法是将前景的权重 `b1` 与前景的均值 `m1` 的差的平方乘以前景的概率分布 `p1`,然后除以前景的概率 `q1`。
- `v2` 是背景的方差,计算方法是将背景的权重 `b2` 与背景的均值 `m2` 的差的平方乘以背景的概率分布 `p2`,然后除以背景的概率 `q2`。
这些方差和均值值用于后续的计算,以估计前景和背景的加权方差,从而找到最佳的阈值。
请注意,Otsu 阈值化通常使用 OpenCV 的 `cv.threshold(blur, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)` 函数来实现,而不是手动计算均值和方差。手动计算可能会导致不准确的结果,并且通常不如 OpenCV 的内置函数高效。