OpenCV:详解阈值分割的处理

本文深入探讨了图像处理中的阈值分割技术,包括固定阈值和自适应阈值的方法,展示了如何使用OpenCV进行图像二值化,并对比了不同场景下的分割效果。

阈值分割是图像处理中非常实用的操作,对我们提取目标区域,使图像信息更加简单(0和1)来加速后续的处理速度。

Targets

  • 使用固定阈值、自适应阈值”二值化”图像
  • OpenCV函数:cv2.threshold(), cv2.adaptiveThreshold()

Simple Thresholding 固定阈值

固定阈值分割很直接,一句话说就是像素点值大于阈值一个值,小于阈值是另外一个值。


cv2.threshold()用来实现阈值分割,retreturn value缩写,代表当前的阈值,暂时不用理会。函数有4个参数:

cv2.threshold(src, thresh, maxval, type[, dst]) → retval, dst
@param: src – input array (single-channel, 8-bit or 32-bit floating point).
@param: thresh – threshold value. 因为是固定阈值分割,所以必须选择一个固定的值作为分界线
@param: maxval – 在使用THRESH_BINARY和THRESH_BINARY_INV的时候,需要指定最大值
                maximum value to use with the THRESH_BINARY and
                THRESH_BINARY_INV thresholding types.
@param: type – thresholding type (see the details below).

具体的函数定义如下:

 


更形象一些理解这些函数可以参见下图,下图第一行是原图图像,然后余下各行均为结果:

源码:

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('grey-gradient.jpg', 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()

运行结果:

经验之谈:很多人误以为阈值分割就是二值化。从上图中可以发现,两者并不等同,阈值分割结果是两种值,而不是两个值,所以教程开头我把二值化加了引号。

Conclusion

固定阈值是 最不常用 的阈值分割方法,其最大的缺陷就是“固定”:

  • 固定阈值需要人工不断寻找,一张图片甚至要尝试5~10个阈值才能决定最适合的。
  • 图片与图片之间的阈值都不一样,比如说这个阈值可能适用于这张图片,但是那张图片就不能适用了,如果要批量处理图片的话,总不至于一张一张图片去找一下阈值
  • 固定的阈值会施加在整张图片上,但是每张图片不同区域之间适合的阈值都不一定相同,可能这个全局阈值这一批效果比较好,那一片就不行了

我们拿下面的图片为例,详细阐释一下为什么固定阈值不好。

Example 2

现在有一张300x300大小左右的,40倍镜下的外周血样本:

我们需要将细胞和背景的组织液分开,就是说要把所有的细胞涂成白色,把背景涂成黑色,我们先上固定阈值的代码:

我们需要将细胞和背景的组织液分开,就是说要把所有的细胞涂成白色,把背景涂成黑色,我们先上固定阈值的代码:

# -*- coding: utf-8 -*-
# @Time    : 2020/9/4 20:58
# @Author  : jhys
# @FileName: 阈值分割2.py

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('cell.jpg', 0)

# 应用5种不同的阈值方法
_, th1 = cv2.threshold(img, 40,  255, cv2.THRESH_BINARY_INV)
_, th2 = cv2.threshold(img, 90,  255, cv2.THRESH_BINARY_INV)
_, th3 = cv2.threshold(img, 140, 255, cv2.THRESH_BINARY_INV)
_, th4 = cv2.threshold(img, 190, 255, cv2.THRESH_BINARY_INV)
_, th5 = cv2.threshold(img, 240, 255, cv2.THRESH_BINARY_INV)

titles = ['Original', 'BINARY_40', 'BINARY_90', 'BINARY_140', 'BINARY_190', 'BINARY_210']
images = [img, th1, th2, th3, th4, th5]

plt.figure(figsize=(8, 4))

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.savefig("2_out.png")
plt.show()

可以看到,我们尝试了5次,只有BINARY_190的图像比较合格,但是仍然有很多地方都失败了;接下来我们展示一下不用固定阈值的版本,这个的工序就非常复杂了:聚类->柱状图均衡->中心填充,详细代码就不上了。

 

Adaptive Thresholding 自适应阈值

看得出来固定阈值是在整幅图片上应用一个阈值进行分割,它并不适用于明暗分布不均的图片。cv2.adaptiveThreshold()自适应阈值会每次取图片的一小部分计算阈值,这样图片不同区域的阈值就不尽相同。

cv2.adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C[, dst]) → dst
@param: src – Source 8-bit single-channel image.
@param: maxValue –  Non-zero value assigned to the pixels for which the condition is satisfied. See the details below.
                    最大阈值,一般为255。
@param: adaptiveMethod – Adaptive thresholding algorithm to use,
        ADAPTIVE_THRESH_MEAN_C:小区域内取均值
        ADAPTIVE_THRESH_GAUSSIAN_C:小区域内加权求和,权重是个高斯核
@param: thresholdType – Thresholding type that must be
        THRESH_BINARY: 过了临界值的取最大值,没到临界值的取最小值
        THRESH_BINARY_INV:过了临界值的取最小值,没到临界值的取最大值
@param: blockSize – 小区域的面积,如11就是11*11的小块;这里不能填1以及所有偶数
@param: C – 最终阈值等于小区域计算出的阈值再减去此值

如果你没看懂上面的参数也不要紧,暂时会用就行,当然我建议你调整下参数看看不同的结果。

Example 3

我们这里有一张图片,尝试阈值分割之后能看得清整个数独棋盘。

先上代码:

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('test.jpg', 0)

# 固定阈值
_, th1 = cv2.threshold(img, 40,  255, cv2.THRESH_BINARY )
_, th2 = cv2.threshold(img, 90,  255, cv2.THRESH_BINARY )
_, th3 = cv2.threshold(img, 140, 255, cv2.THRESH_BINARY )
_, th4 = cv2.threshold(img, 190, 255, cv2.THRESH_BINARY )
_, th5 = cv2.threshold(img, 240, 255, cv2.THRESH_BINARY )

# 自适应阈值
th6 = cv2.adaptiveThreshold(
    img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY , 11, 4)
th7 = cv2.adaptiveThreshold(
    img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY , 17, 6)
th8 = cv2.adaptiveThreshold(
    img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY , 5, 6)

titles = ['Original', 'BINARY_40', 'BINARY_90', 'BINARY_140', 'BINARY_190', 'BINARY_210', 'MEAN_C_11','GAUSSIAN_C_17','GAUSSIAN_C_5']
images = [img, th1, th2, th3, th4, th5, th6, th7, th8]

# 开始画图
plt.figure(figsize = (8,8))

for i in range(9):
    plt.subplot(3,3,i+1)
    plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])

# 保存并展示图片
plt.savefig('3_out.png')
plt.show()

Conclusion

自适应阈值的本质依然是阈值分割,就是过了临界值的取最大值,没到临界值的取最小值。只不过他做的就是能在小区域内自动去算这个临界值。也就是说他虽然已经很智能的可以算阈值,但是还是逃脱不了阈值的本质缺陷:

  • 不能保证他自动算出来的阈值就是最好的,其实本来也没有什么评判标准去判断阈值的好坏。而这也是阈值法被诟病的主要原因。
  • 阈值分割的本质:过了临界值的取最大值,没到临界值的取最小值。这个本质的想法就是很naive的,很多情况下不是这么简单一刀切能解决问题的。

我们可以再来看一下之前提到的细胞分割。

Example 4

还是之前的细胞分割问题,直接上代码:


# 导入库
import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('cell.jpg', 0)

# 自适应阈值
th1 = cv2.adaptiveThreshold(
    img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV , 3, 0)
th2 = cv2.adaptiveThreshold(
    img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV , 33, 0)
th3 = cv2.adaptiveThreshold(
    img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV , 53, 0)
th4 = cv2.adaptiveThreshold(
    img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV , 73, 0)
th5 = cv2.adaptiveThreshold(
    img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV , 3, 0)
th6 = cv2.adaptiveThreshold(
    img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV , 33, 0)
th7 = cv2.adaptiveThreshold(
    img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV , 53, 0)
th8 = cv2.adaptiveThreshold(
    img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV , 73, 0)

titles = ['Original', 'MEAN_3', 'MEAN_33','MEAN_53','MEAN_73','GAUSSIAN_3','GAUSSIAN_33','GAUSSIAN_53','GAUSSIAN_73']
images = [img, th1, th2, th3, th4, th5, th6, th7, th8]

# 开始画图
plt.figure(figsize = (8,8))

for i in range(9):
    plt.subplot(3,3,i+1)
    plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])

# 保存并展示图片
plt.savefig('4_out.png')
plt.show()

 

Conclusion

我们可以总结一下了,就是固定阈值和自适应阈值可以解决很小一部分的问题,但是现实生活和科学研究中的复杂图像二值化几乎不可能以简单的阈值分割解决。

现今有专门用于语义分割的神经网络 —— U-net来完成这个任务,就拿之前的细胞分割为例,外周血的分割上尚且不需要用到U-net,但是更为复杂的组织切片的分割就是U-net的天下了。

还有用于物体检测的神经网络 —— RCNNFaster-RCNNYOLOSSD,可以更好地完成之前的外周血细胞分割问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值