B04 - 图像处理 - 图像阈值
将图像转为二值处理,减少干扰区域,目前二值化办法有定值二值化、自适应二值化
按照颜色对图像进行分类,可分为几类?
- 二值图像:只有黑色和白色两种颜色的图像。每个像素点可以用 0/1 表示,0 表示黑色,1 表示白色;
- 灰度图像:只有灰度的图像。每个像素点用 8bit 数字 [0,255] 表示灰度,如:0 表示纯黑,255 表示纯白;
- 彩色图像:彩色图像通常采用红色(R)、绿色(G)和蓝色(B)三个色彩通道的组合表示
什么是图像二值化?
- 将我们感兴趣的像素与其他像素(最终将被拒绝)区分开来,通常根据阈值(根据要解决的问题确定)对每个像素强度值进行比较
- 灰度图片或者彩色图片转为二值化图片的过程
Opencv 如何进行二值化?
- cv: : threshold :固定阈值二值化或大津二值化
- cv: : adaptiveThreshold :自适应阈值二值化
- cv: : inRange :固定阈值区间二值化
什么是阈值处理?
- 根据灰度值和灰度值的限制将图像划分为多个区域,或提取图像中的目标物体,是最基本的阈值处理方法
- 如果图像的直方图存在明显边界,容易找到图像的分割阈值;但如果图像直方图分界不明显,则很难找到合适的阈值,甚至可能无法找到固定的阈值有效地分割图像
- 函数 threshold () 可以将灰度图像转换为二值图像,图像完全由像素 0 和 255 构成,呈现出只有黑白两色的视觉效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16# 生成灰度图像
hImg, wImg = 512, 512
img = np.zeros((hImg, wImg), np.uint8) # # 创建黑色图像 RGB=0
cv2.rectangle(img, (60,60), (450,320), (127,127,127), -1) # -1 表示矩形填充
cv2.circle(img, (256, 256), 120, (205,205,205), -1) # -1 表示圆形填充
# 添加高斯噪声
mu, sigma = 0.0, 25.0
noiseGause = np.random.normal(mu, sigma, img.shape)
imgNoise = img + noiseGause
imgNoise = np.uint8(cv2.normalize(imgNoise, None, 0, 255, cv2.NORM_MINMAX)) # 归一化为 [0,255]
# 阈值处理
ret, imgBin1 = cv2.threshold(img, 63, 255, cv2.THRESH_BINARY) # 阈值分割, thresh=63
ret, imgBin2 = cv2.threshold(img, 125, 255, cv2.THRESH_BINARY) # 阈值分割, thresh=125
ret, imgBin3 = cv2.threshold(img, 175, 255, cv2.THRESH_BINARY) # 阈值分割, thresh=175
show_images([img,imgNoise,imgBin1,imgBin2,imgBin3])
plt.bar(bins[:-1], histNP[:])
什么是全局阈值处理方法?
- 当图像中的目标和背景的灰度分布较为明显时,可以对整个图像使用固定阈值进行全局阈值处理
- 为了获得适当的全局阈值,可以基于灰度直方图进行迭代计算:设定初始阈值 T 分割图像,将图像分为大于 T 和小于 T 的部分;分别计算这两部分的灰度平均值 m1, m2,然后基于 T=(m1+m2)/2 更新阈值,重复迭代,知道 T 变化小于阈值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23img = cv2.imread("../images/Fig0940a.tif", flags=0)
deltaT = 1 # 预定义值
histCV = cv2.calcHist([img], [0], None, [256], [0, 256]) # 灰度直方图
grayScale = range(256) # 灰度级 [0,255]
totalPixels = img.shape[0] * img.shape[1] # 像素总数
totalGary = np.dot(histCV[:,0], grayScale) # 内积, 总和灰度值
T = round(totalGary/totalPixels) # 平均灰度
while True:
numC1, sumC1 = 0, 0
for i in range(T): # 计算 C1: (0,T) 平均灰度
numC1 += histCV[i,0] # C1 像素数量
sumC1 += histCV[i,0] * i # C1 灰度值总和
numC2, sumC2 = (totalPixels-numC1), (totalGary-sumC1) # C2 像素数量, 灰度值总和
T1 = round(sumC1/numC1) # C1 平均灰度
T2 = round(sumC2/numC2) # C2 平均灰度
Tnew = round((T1+T2)/2) # 计算新的阈值
print("T={}, m1={}, m2={}, Tnew={}".format(T, T1, T2, Tnew))
if abs(T-Tnew) < deltaT: # 等价于 T==Tnew
break
else:
T = Tnew
# 阈值处理
ret, imgBin = cv2.threshold(img, T, 255, cv2.THRESH_BINARY) # 阈值分割, thresh=T
基于边缘信息改进全局阈值处理?
- 对于大背景中的小目标,图像的直方图受到背景的大波峰控制,即由于背景内容太多而将目标在直方图中淹没了,使全局阈值处理容易失败
- 如果只利用接近目标和背景之间的边缘的像素,忽略无效的背景区域像素对直方图的贡献,可以改善直方图的分布,从而便于通过阈值处理进行分割
- 步骤:1) 计算图像 f (x, y) 的梯度算子,得到梯度幅值图像;2) 对梯度幅值图像进行二值处理,选取强边缘像素作为遮罩模板;3)基于遮罩模板计算图像 f (x, y) 的直方图分布;4)由基于遮罩模板的直方图找到适当的分割阈值,对图像 f (x, y) 进行全局阈值处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23img = cv2.imread("../images/Fig1041a.tif", flags=0)
# # 全局阈值处理,作为参照比较
histCV1 = cv2.calcHist([img], [0], None, [256], [0, 256]) # 灰度直方图
totalPixels = img.shape[0] * img.shape[1] # 像素总数
totalGray = np.dot(histCV1[:,0], range(256)) # 内积, 总和灰度值
meanGray = round(totalGray/totalPixels) # 平均灰度
ret, imgBin = cv2.threshold(img, meanGray, 255, cv2.THRESH_BINARY) # thresh=meanGray
# (1) 计算 Sobel 梯度算子
SobelX = cv2.Sobel(img, cv2.CV_32F, 1, 0) # 计算 x 轴方向
SobelY = cv2.Sobel(img, cv2.CV_32F, 0, 1) # 计算 y 轴方向
grad = np.sqrt(SobelX**2 + SobelY**2)
gradMax = np.int(np.max(grad))
# (2) 设置阈值 T=0.3*gradMax,对梯度图像进行阈值处理,作为遮罩模板
_, maskBW= cv2.threshold(np.uint8(grad), 0.3*gradMax, 255, cv2.THRESH_BINARY)
# (3) 计算基于遮罩模板的直方图分布,以排除无效背景像素的影响
histCV2 = cv2.calcHist([img], [0], maskBW, [256], [0, 256])
histCV2[0] = 0
# (4) 排除无效背景像素影响后,进行阈值处理
Tmask = 120 # 观察直方图 histCV2,找到分割阈值
_, imgBin2 = cv2.threshold(img, Tmask, 255, cv2.THRESH_BINARY)
show_images([img,imgBin,maskBW,imgBin2])
plt.bar(range(256), histCV1[:,0])
plt.bar(range(256), histCV2[:,0])
基于 Laplace 边缘信息改进全局阈值处理?
- 使用 Laplace 算子计算梯度,可以得到亮点的边缘像素,忽略背景区域像素对直方图的贡献,可以改善直方图的分布,从而便于通过阈值处理进行分割
- 对于酵母细胞图像,希望通过全局阈值处理等等图像中与亮点对应的区域。如果直接使用 OTSU 方法可以分割细胞区域,但不能检测亮点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37img = cv2.imread("../images/Fig1043a.tif", flags=0)
# # 全局阈值处理,作为参照比较
histCV1 = cv2.calcHist([img], [0], None, [256], [0, 256]) # 灰度直方图
ret1, imgOtsu = cv2.threshold(img, 127, 255, cv2.THRESH_OTSU) # 阈值分割, thresh=T
# (1) 计算 Laplacian 梯度算子
laplace = cv2.Laplacian(img, cv2.CV_32F, ksize=3) # Laplace 卷积算子
grad = cv2.convertScaleAbs(laplace)
gradMax = np.int(np.max(grad))
# (2) 以灰度值的 99.5% 分位为阈值, 对边缘图像进行二值处理, 作为遮罩模板
per995 = np.percentile(grad, q=99.5) # 99.5 分位的灰度值, [0, per995] 占比99.5%
_, gradPer995 = cv2.threshold(np.uint8(grad), per995, 1, cv2.THRESH_BINARY) # 对边缘图像二值处理
# (3) 计算基于遮罩模板的直方图分布,以排除无效背景像素的影响
fp = np.uint8(img * gradPer995)
histCV2 = cv2.calcHist([fp], [0], None, [256], [0, 256])
histCV2[0] = 0 # fp 非零像素直方图
# (4) OTSU 算法计算 fp 非零像素的最佳分割阈值
# nonzeroPixels = np.count_nonzero(gradPer995) # 非零像素总数
nonzeroPixels = sum(histCV2[1:]) # 非零像素总数
totalGray = np.dot(histCV2[:,0], range(256)) # 内积, 总和灰度值
mG = totalGray / nonzeroPixels # 平均灰度
icv = np.zeros(256)
numFt, sumFt = 0, 0
for t in range(0, 256): # 遍历灰度值
numFt += histCV2[t,0] # F(t) 像素数量
sumFt += histCV2[t,0] * t # F(t) 灰度值总和
pF = numFt / nonzeroPixels # F(t) 像素数占比
mF = (sumFt/numFt) if numFt>0 else 0 # F(t) 平均灰度
numBt = nonzeroPixels-numFt # B(t) 像素数量
sumBt = totalGray - sumFt # B(t) 灰度值总和
pB = numBt / nonzeroPixels # B(t) 像素数占比
mB = (sumBt/numBt) if numBt>0 else 0 # B(t) 平均灰度
icv[t] = pF * (mF-mG)**2 + pB * (mB-mG)**2 # OTSU 算法: 灰度 t 的类间方差
maxIcv = max(icv) # ICV 的最大值
maxIndex = np.argmax(icv) # 最大值的索引
print(per995, nonzeroPixels, maxIcv, maxIndex)
# 使用 fp 非零像素的最佳分割阈值,对原始图像进行固定阈值处理
ret, imgBin = cv2.threshold(img, maxIndex, 255, cv2.THRESH_BINARY) # 以 maxIndex 作为最优阈值
什么是大津 (Otsu) 二值化?
- OTSU 方法又称大津算法,使用最大化类间方差(intra-class variance)作为评价准则,基于对图像直方图的计算,可以给出类间最优分离的最优阈值
- 任取一个灰度值 T,可以将图像分割为两个集合 F 和 B,集合 F、B 的像素数的占比分别为 pF、pB,集合 F、B 的灰度值均值分别为 mF、mB,图像灰度值为 m,定义类间方差
- 使类间方差 ICV 最大化的灰度值 T 就是最优阈值,只要遍历所有的灰度值,就可以得到使 ICV 最大的最优阈值 T
1
2
3
4
5
6
7
8
9
10
11
12img = cv.imread('noisy2.png',0)
# global thresholding
ret1,th1 = cv.threshold(img,127,255,cv.THRESH_BINARY)
# Otsu's thresholding
ret2,th2 = cv.threshold(img,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
# Otsu's thresholding after Gaussian filtering
blur = cv.GaussianBlur(img,(5,5),0)
ret3,th3 = cv.threshold(blur,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
# plot all the images and their histograms
show_images([img, 0, th1])
show_images([img, 0, th2])
show_images([blur, 0, th3])
多阈值的大津 (Otsu) 二值化处理?
- 大津 (Otsu) 二值化方法使用最大化类间方差(intra-class variance)作为评价准则,基于对图像直方图的计算,可以给出类间最优分离的最优阈值,OTSU 方法可以扩展到任意数量的阈值
- 此时,通常使用双阈值对图像进行二值化,如果是更多阈值,需要采用聚类或启发式方法来获得分割阈值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44def doubleThreshold(img):
histCV = cv2.calcHist([img], [0], None, [256], [0, 256]) # 灰度直方图
grayScale = np.arange(0, 256, 1) # 灰度级 [0,255]
totalPixels = img.shape[0] * img.shape[1] # 像素总数
totalGray = np.dot(histCV[:,0], grayScale) # 内积, 总和灰度值
mG = totalGray / totalPixels # 平均灰度,meanGray
varG = sum(((i-mG)**2 * histCV[i,0]/totalPixels) for i in range(256))
T1, T2, varMax = 1, 2, 0.0
# minGary, maxGray = np.min(img), np.max(img) # 最小灰度,最大灰度
for k1 in range(1, 254): # k1: [1,253], 1<=k1<k2<=254
n1 = sum(histCV[:k1, 0]) # C1 像素数量
s1 = sum((i * histCV[i, 0]) for i in range(k1))
P1 = n1 / totalPixels # C1 像素数占比
m1 = (s1 / n1) if n1 > 0 else 0 # C1 平均灰度
for k2 in range(k1+1, 256): # k2: [2,254], k2>k1
# n2 = sum(histCV[k1+1:k2,0]) # C2 像素数量
# s2 = sum( (i * histCV[i,0]) for i in range(k1+1,k2) )
# P2 = n2 / totalPixels # C2 像素数占比
# m2 = (s2/n2) if n2>0 else 0 # C2 平均灰度
n3 = sum(histCV[k2+1:,0]) # C3 像素数量
s3 = sum((i*histCV[i,0]) for i in range(k2+1,256))
P3 = n3 / totalPixels # C3 像素数占比
m3 = (s3/n3) if n3>0 else 0 # C3 平均灰度
P2 = 1.0 - P1 - P3 # C2 像素数占比
m2 = (mG - P1*m1 - P3*m3)/P2 if P2>1e-6 else 0 # C2 平均灰度
var = P1*(m1-mG)**2 + P2*(m2-mG)**2 + P3*(m3-mG)**2
if var>varMax:
T1, T2, varMax = k1, k2, var
epsT = varMax / varG # 可分离测度
print(totalPixels, mG, varG, varMax, epsT, T1, T2)
return T1, T2, epsT
img = cv2.imread("../images/Fig1043a.tif", flags=0)
# img = cv2.imread("../images/Fig1045a.tif", flags=0)
histCV = cv2.calcHist([img], [0], None, [256], [0, 256]) # 灰度直方图
T1, T2, epsT = doubleThreshold(img)
print("T1={}, T2={}, esp={:.4f}".format(T1, T2, epsT))
binary = img.copy()
binary[binary<T1] = 0
binary[binary>T2] = 255
ret, imgOtsu = cv2.threshold(img, 127, 255, cv2.THRESH_OTSU) # OTSU 阈值分割
ret1, binary1 = cv2.threshold(img, T1, 255, cv2.THRESH_TOZERO) # 小于阈值置 0,大于阈值不变
ret2, binary2 = cv2.threshold(img, T2, 255, cv2.THRESH_TOZERO)
show_images([img,imgOtsu,binary1,binary2,binary3])
plt.bar(range(256), histCV[:,0])
什么是自适应阈值处理?
- 噪声和非均匀光照等因素对阈值处理的影响很大,例如光照复杂时 Otsu 算法等全局阈值分割方法的效果往往不太理想,需要使用可变阈值处理
- 可变阈值是指对于图像中的每个像素点或像素块有不同的阈值,如果该像素点大于其对应的阈值则认为是前景
- 局部阈值分割可以根据图像的局部特征进行处理,根据其邻域的性质计算阈值,标准差和均值是对比度和平均灰度的描述,在局部阈值处理中非常有效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16img = cv2.imread("../images/Fig1050a.tif", flags=0)
# img = cv2.imread("../images/Fig1043a.tif", flags=0)
# OTSU 全局阈值处理
histCV = cv2.calcHist([img], [0], None, [256], [0, 256]) # 灰度直方图
ret, imgOtsu = cv2.threshold(img, 127, 255, cv2.THRESH_OTSU) # 阈值分割, thresh=T
# 自适应局部阈值处理
binaryMean = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 5, 3)
binaryGauss = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 5, 3)
# 自适应局部阈值处理
ratio = 0.2
imgBlur = cv2.boxFilter(img, -1, (5,5)) # 盒式滤波器,均值平滑
localThresh = img - (1.0-ratio) * imgBlur
binaryBox = np.ones_like(img) * 255 # 创建与 img 相同形状的白色图像
binaryBox[localThresh<0] = 0
show_images([img,imgOtsu,binaryMean,binaryGauss,binaryBox])
plt.bar(range(256), histCV[:,0])
什么是基于移动平均的可变阈值处理?
- 灰度遮蔽着点照明(如闪光灯)图像中十分常见。图中被斑点灰度模式遮蔽的手写文本图像,如果使用 OTSU 全局阈值处理,不能克服灰度变化的影响。在不均匀的光照场中,阈值分割的效果不好。使用移动平均的局部阈值,则能很好地进行处理
- 移动平均法是线性的 Z 字形模式扫描整个图片,对每个像素产生一个阈值,然后进行阈值处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17def movingThreshold(img, n, b):
img[1:-1:2, :] = np.fliplr(img[1:-1:2, :]) # 向量翻转
f = img.flatten() # 展平为一维
ret = np.cumsum(f)
ret[n:] = ret[n:] - ret[:-n]
m = ret / n # 移动平均值
g = np.array(f>=b*m).astype(int) # 阈值判断,g=1 if f>=b*m
g = g.reshape(img.shape) # 恢复为二维
g[1:-1:2, :] = np.fliplr(g[1:-1:2, :]) # 交替翻转
return g*255
img1 = cv2.imread("../images/Fig1049a.tif", flags=0)
img2 = cv2.imread("../images/Fig1050a.tif", flags=0)
ret1, imgOtsu1 = cv2.threshold(img1, 127, 255, cv2.THRESH_OTSU) # OTSU 阈值分割
ret2, imgOtsu2 = cv2.threshold(img2, 127, 255, cv2.THRESH_OTSU)
imgMoveThres1 = movingThreshold(img1, 20, 0.5) # 移动平均阈值处理
imgMoveThres2 = movingThreshold(img2, 20, 0.5) # n=20, b=0.5
show_images([img1,imgOtsu1,imgMoveThres1,img2,imgOtsu2,imgMoveThres2])
如何根据掩码快速提取图片的子区域?
- 使用
bitwise_and
快速提取某一张图的子区域1
2
3
4
5
6
7import cv2 as cv
img = cv.imread('messi.jpeg')
img2gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
# 提取mask
ret, mask = cv.threshold(img2gray, 10, 255, cv.THRESH_BINARY)
# mask区域就是要提取的掩码区域
img1_bg = cv.bitwise_and(img,img,mask = mask)
如何融合两张图片?
- 首先截取待操作的 ROI,然后使用 bitwise_and 在准备底图上挖空,去掉顶图未融合部分,然后使用 add 将处理后的两部分相加即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22img1 = cv2.imread("../images/imgLena.tif") # 读取彩色图像(BGR)
img2 = cv2.imread("../images/logoCV.png") # 读取 CV Logo
x, y = (0, 10) # 图像叠加位置
W1, H1 = img1.shape[1::-1]
W2, H2 = img2.shape[1::-1]
if (x + W2) > W1: x = W1 - W2
if (y + H2) > H1: y = H1 - H2
print(W1,H1,W2,H2,x,y)
imgROI = img1[y:y+H2, x:x+W2] # 从背景图像裁剪出叠加区域图像
img2Gray = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY) # img2: 转换为灰度图像
ret, mask = cv2.threshold(img2Gray, 175, 255, cv2.THRESH_BINARY) # 转换为二值图像,生成遮罩,LOGO 区域黑色遮盖
maskInv = cv2.bitwise_not(mask) # 按位非(黑白转置),生成逆遮罩,LOGO 区域白色开窗,LOGO 以外区域黑色
# mask 黑色遮盖区域输出为黑色,mask 白色开窗区域与运算(原图像素不变)
img1Bg = cv2.bitwise_and(imgROI, imgROI, mask=mask) # 生成背景,imgROI 的遮罩区域输出黑色
img2Fg = cv2.bitwise_and(img2, img2, mask=maskInv) # 生成前景,LOGO 的逆遮罩区域输出黑色
# img1Bg = cv2.bitwise_or(imgROI, imgROI, mask=mask) # 生成背景,与 cv2.bitwise_and 效果相同
# img2Fg = cv2.bitwise_or(img2, img2, mask=maskInv) # 生成前景,与 cv2.bitwise_and 效果相同
# img1Bg = cv2.add(imgROI, np.zeros(np.shape(img2), dtype=np.uint8), mask=mask) # 生成背景,与 cv2.bitwise 效果相同
# img2Fg = cv2.add(img2, np.zeros(np.shape(img2), dtype=np.uint8), mask=maskInv) # 生成背景,与 cv2.bitwise 效果相同
imgROIAdd = cv2.add(img1Bg, img2Fg) # 前景与背景合成,得到裁剪部分的叠加图像
imgAdd = img1.copy()
imgAdd[y:y+H2, x:x+W2] = imgROIAdd # 用叠加图像替换背景图像中的叠加位置,得到叠加 Logo 合成图像
参考:
- 【OpenCV 例程 200 篇】21. 图像的叠加_opencv 图像叠加_youcans_的博客 - CSDN 博客
- 【youcans 的 OpenCV 例程 200 篇】158. 阈值处理之固定阈值法_opencv 设置图像固定位置阈值_youcans_的博客 - CSDN 博客
- 【youcans 的 OpenCV 例程 200 篇】159. 图像分割之全局阈值处理_youcans_的博客 - CSDN 博客
- 【youcans 的 OpenCV 例程 200 篇】160. 图像处理之 OTSU 方法_youcans_的博客 - CSDN 博客
- 【youcans 的 OpenCV 例程 200 篇】163. 基于边缘信息改进全局阈值处理_youcans_的博客 - CSDN 博客
- 【youcans 的 OpenCV 例程 200 篇】164. 使用 Laplace 边缘信息改进全局阈值处理_youcans_的博客 - CSDN 博客
- 【youcans 的 OpenCV 例程 200 篇】165. 多阈值 OTSU 处理方法_多阈值 otsu matlab_youcans_的博客 - CSDN 博客
- 【youcans 的 OpenCV 例程 200 篇】166. 自适应阈值处理_自适应阈值处理题目_youcans_的博客 - CSDN 博客
- 【youcans 的 OpenCV 例程 200 篇】167. 基于移动平均的可变阈值处理_基于移动平均的可变阈值处理的过程_youcans_的博客 - CSDN 博客
- 【OpenCV 例程 200 篇】37. 图像的灰度化处理和二值化处理(cv2.threshold)_1. 对获取图像进行合适大小变换后,双边滤波、灰度化、二值化并得到轮廓 (可以结合 hs_youcans_的博客 - CSDN 博客