欢迎关注公众号:Python大视界,
上周在组会学习中小师妹介绍了分水岭算法和阈值分割算法,并用opencv实现相关代码,借用实现的代码,加之一些优秀的博客,对于图像分割做一个简单的介绍。在这里,将从以下四个方面对于图像分割做一个简单的介绍,图像分割简介,阈值分割算法,分水岭算法,及代码实现。
(一)图像分割简介
图像识别来自于模板匹配,从人类自身的视觉识别中演变而来,将眼前的物体和脑海中的印象进行比对,完成眼前物体的定义。图像分割在图像识别中有很重要的作用。简单的讲,图像分割就是根据图像的某些特征或者特征相似的集合,对图像进行分组聚类,将图像分成若干个特定的有意义的区域,并提取出感兴趣的区域,在一幅图像中,把目标从背景中分离出来,保留图像的结构信息,便于图像分析。常规的图像分割根据灰度,色彩,几何形状等特征将细胞图像划分为若干个互不相交的区域,使得这些特征在同一区域中,表现出一致性或相似性,而在不同区域表现出明显的不同。
简单的说就是在一副图像中,把目标从背景中分离出来。对于灰度图像来说,不同区域的像素有不同的特征,区域内部的像素一般具有灰度相似性,而在区域的边界上一般具有灰度不连续性。
图像分割算法包括传统图像分割算法如阈值分割算法,分水岭算法,基于边缘检测的分割方法,基于图论的分割方法,和深度学习分割方法,如基于U-NET,R-CNN等去训练,模型来做图像分割,下面这张图展示了常见图像分割算法的之间的比较。
在这里,我们将简单的学习传统图像分割算法,阈值分割算法,分水岭算法,基于图的分割算法,基于边缘检测的算法。
(二)阈值分割算法
阈值分割算法是一种基于区域像素的图像分割算法,它利用图像中要提取的目标与背景在灰度上的差异,通过设置阈值来把像素级分成若干类,从而实现目标与背景的分离,特别适用于目标和背景占据不同灰度级范围的图像
其基本原理是:利用图像中要提取的目标区域与其背景在灰度特性上的差异,把图像看作具有不同灰度级的两类区域(目标区域和背景区域)的组合,选取一个比较合理的阈值,以确定图像中每个像素点应该属于目标区域还是背景区域,从而产生相应的二值图像。 阈值分割法的特点是:适用于目标与背景灰度有较强对比的情况,重要的是背景或物体的灰度比较单一,而且可以得到封闭且连通区域的边界。
图像本质为矩阵,我们将图像转化为灰度图像,并计算其灰度直方图,根据直方图中像素的分布来设计阈值,基于图像的灰度特征来计算一个或多个灰度阈值,并将图像中每个像素的灰度值与阈值作比较,最后将像素根据比较结果分到合适的类别中。根据阈值将图像分割为两个图片,背景图和目标图。
图像若只有目标和背景两大类,那么只需要选取一个阈值进行分割,此方法成为单阈值分割;但是如果图像中有多个目标需要提取,单一阈值的分割就会出现作物,在这种情况下就需要选取多个阈值将每个目标分隔开,这种分割方法相应的成为多阈值分割。常见的图像分割算法包括全阈值分割,自适应阈值分割,Ostu阈值法(大津法)。
阈值分割算法能够较快的完成图像分割,考虑灰度信息而没有考虑空间信息,不适用于多通道图片,也不适用于特征值相差不大的图像,并对于噪声和灰度不均匀敏感,阈值法在实际应用中主要存在两个问题:(1)该方法只考虑到图像中像素点本身的灰度值,没有考虑到图像中像素点的空间分布,容易对噪声敏感,(2)该方法对于背景与目标区域灰度差异较小的图像分割效果不好。
(1)全阈值分割
全阈值分割指将灰度值大于thresh(阈值)的像素设为一种颜色,小于或等于阈值的像素设为另外一种颜色,通常,根据自定义阀值对图像进行二值化处理,即灰度值大于阀值时设改像素灰度值为255,灰度值小于阈值时设该像素灰度值为0。
那在OPenCV中实现全阈值分割使用的API 是:
ret,th = threshold(src, thresh, maxval, type)
各个参数代表:
src: 要处理的图像,一般是灰度图
thresh: 设定的阈值
maxval: 灰度中的最大值,一般为255,用来指明阈值分割中最大值的取值,主要指阈值二值化和阈值反二值化中
type:阈值分割的方式,取值主要有以下五种:
那么,针对于同一张图片,怎样实现不同的全局阈值分割呢?
import cv2 as cv
import matplotlib.pyplot as plt
# 1.读取图像
img = cv.imread('01.png', 0)
# 2. 阈值分割,采取五种不同的阈值分割算法
ret, th1 = cv.threshold(img, 127, 255, cv.THRESH_BINARY)
ret, th2 = cv.threshold(img, 127, 255, cv.THRESH_BINARY_INV)
ret, th3 = cv.threshold(img, 127, 255, cv.THRESH_TRUNC)
ret, th4 = cv.threshold(img, 127, 255, cv.THRESH_TOZERO)
ret, th5 = cv.threshold(img, 127, 255, cv.THRESH_TOZERO_INV)
# 3. 图像显示
titles = ['raw', 'THRESH_BINARY', 'THRESH_BINARY_INV', 'THRESH_TRUNC', 'THRESH_TOZERO', 'THRESH_TOZERO_INV']
images = [img, th1, th2, th3, th4, th5]
plt.figure(figsize=(10,6))
# 使用Matplotlib显示
for i in range(6):
plt.subplot(2, 3, i + 1)
plt.imshow(images[i], 'gray')
plt.title(titles[i], fontsize=8)
plt.xticks([]), plt.yticks([]) # 隐藏坐标轴
plt.show()
上述代码的运行结果为:
从上面的结果可以看出,不同阈值处理方式,
<1>threshold binary 阈值二值化
红色部分是当前图像的灰度值,蓝色线对应值为选定的阈值。所有像素值小于这一值的设定为0,否则设定为最大值1。
<2>阈值反二值化 (threshold binary Inverted)
红色部分是当前图像的灰度值,蓝色线对应值为选定的阈值。所有像素值小于这一值的设定为1,否则设定为0。
<3>截断(truncate)
像素值大于阈值的就设定为阈值大小,否则保持原有的灰度值不变。
<4>阈值取零(threshold to zero)
像素值小于阈值的全部设为0,大于的保持不变。
<5>阈值反取零(threshold to zero inverted)
(2)自适应阈值
对于一张图片,根据图像不同区域亮度分布,计算其局部阈值,对于图像不同区域,能够自适应计算不同的阈值。根据像素的邻域块的像素值分布来确定该像素位置上的阈,阈值由其周围邻域像素的分布来决定的。亮度较高的图像区域的阈值通常会较高,而亮度较低的图像区域的阈值则会相适应地变小。不同亮度、对比度、纹理的局部图像区域将会拥有相对应的局部阈值。常用的局部自适应阈值有:1)局部邻域块的均值;2)局部邻域块的高斯加权和。
在OPenCV中实现自适应阈值分割的API是:
dst = cv.adaptiveThreshold(src, maxval, thresh_type, type, Block Size, C)
各参数的含义为:
参数:
src: 输入图像,一般是灰度图
Maxval:灰度中的最大值,一般为255,用来指明像素超过或小于阈值(与type类型有关),赋予的最大值
thresh_type : 阈值的计算方法,主要有以下两种: cv2.ADAPTIVE_THRESH_MEAN_C:邻域内像素值取均值
cv2.ADAPTIVE_THRESH_GAUSSIAN_C:邻域内像素值进行高斯核加权求和
type: 阈值方式,与threshold中的type意义相同
block_size: 计算局部阈值时取邻域的大小,如果设为11,就取11*11的邻域范围,一般为奇数。
C: 阈值计算方法中的常数项,即最终的阈值是邻域内计算出的阈值与该常数项的差值
用代码实现局部阈值分割算法,
import cv2 as cv
import matplotlib.pyplot as plt
# 1. 图像读取
img = cv.imread('02.png', 0)
# 2.固定阈值
ret, th1 = cv.threshold(img, 127, 255, cv.THRESH_BINARY)
# 3.自适应阈值,阈值的计算有两种方式,求均值或高斯加权
# 3.1 邻域内求均值
th2 = cv.adaptiveThreshold(
img, 255, cv.ADAPTIVE_THRESH_MEAN_C, cv.THRESH_BINARY, 11, 4)
# 3.2 邻域内高斯加权
th3 = cv.adaptiveThreshold(
img, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 17, 6)
# 4 结果绘制
titles = ['raw', 'THRESH_BINARY(v = 127)', 'ADAPTIVE_THRESH_MEAN_C', 'ADAPTIVE_THRESH_GAUSSIAN_C']
images = [img, th1, th2, th3]
plt.figure(figsize=(10,6))
for i in range(4):
plt.subplot(2, 2, i + 1), plt.imshow(images[i], 'gray')
plt.title(titles[i], fontsize=8)
plt.xticks([]), plt.yticks([])
plt.show()
采用全局阈值,局部阈值求平均和局部阈值求高斯的方法比较,结果如下:
(3)Ostu实现(大津法)
大津法按图像的灰度特性,将图像分成背景和前景两部分。因方差是灰度分布均匀性的一种度量,背景和前景之间的类间方差越大,说明构成图像的两部分的差别越大,当部分前景错分为背景或部分背景错分为前景都会导致两部分差别变小。因此,使类间方差最大的分割意味着错分概率最小。
大津法按照图像上灰度值的分布,将图像分成背景和前景两部分看待,前景就是我们要按照阈值分割出来的部分。背景和前景的分界值就是我们要求出的阈值。遍历不同的阈值,计算不同阈值下对应的背景和前景之间的类内方差,当类内方差取得极大值时,此时对应的阈值就是大津法(OTSU算法)所求的阈值。
在求阈的过程中,采用遍历的方法得到使类间方差g最大的阈值T,即为所求的阈值结果。
在opencv中,对于全局阈值分割和Ostu算法,代码如下,
import cv2 as cv
import matplotlib.pyplot as plt
# 1. 图像读取
img = cv.imread('ssDNA20', 0)
# 2.固定阈值
ret, th1 = cv.threshold(img, 12, 255, cv.THRESH_BINARY)
# 3. OSTU阈值
ret2,th2 = cv.threshold(img,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
# 4. 结果绘制
plt.figure(figsize=(10,8),dpi=100)
plt.subplot(131),plt.imshow(img,'gray'),plt.title('RAW')
plt.xticks([]), plt.yticks([])
plt.subplot(132),plt.imshow(th1,'gray'),plt.title('THRESH_BINARY')
plt.xticks([]), plt.yticks([])
plt.subplot(133),plt.imshow(th2,'gray'),plt.title('OStu')
plt.xticks([]), plt.yticks([])
plt.show()
运行结果为:
(三)分水岭算法
水岭算法由Vincent于1991年提出,该方法模拟地质学中的地貌,将图像中像素点的灰度值模拟为海拔高度,像素灰度值中的局部极小值模拟为谷底。局部极大值模拟为顶峰,谷底之间的边界即为分水岭。
如果将图片的像素看成类似于山的高低,那么,一张图片可以近似看成下面这个形状。
分水岭分割算法是一种基于拓扑理论的数学形态学的分割的算法,其基本思想是将图像看作测地学上的拓扑地貌,图像中的每一点像素的灰度值表示该点的海拔高度,每一个局部值及其影响区域成为聚水盆,而聚水盆的边界则形成分水岭,分水岭的算法可以通过模拟浸入过程来说明,在每一个局部最小值表面,刺穿一个小孔,然后将整个模型浸入到水中,随着进入的加深,每一个局部极小值的影响慢慢向外扩展,在两个聚水盆之间汇合处构筑大坝,即形成分水岭。
假设在盆地的最小值点,打一个洞,然后往盆地里面注水,并阻止两个盆地的水汇集,我们会在两个盆地的水汇集的时刻,在交接的边缘线上(也即分水岭线),建一个坝,来阻止两个盆地的水汇集成一片水域。这样图像就被分成2个像素集,一个是注水盆地像素集,一个是分水岭线像素集。那图像就被我们分割开了。如下图所示:
分水岭的计算过程是一个迭代标注过程。在该算法中,分水岭计算分两个步骤,一个是排序过程,一个是淹没过程。首先对每个像素的灰度级进行从低到高排序,然后在从低到高实现淹没过程中,对每一个局部极小值在h阶高度的影响域采用先进先出(FIFO)结构进行判断及标注。
分水岭算法的整个过程:
在实际应用中,由于噪声点或者其它干扰因素的存在,使用分水岭算法常常存在过度分割的现象,这是因为过多局部极值点的存在而产生许多小的集水盆地,从而导致分割后的图像不能将图像中有意义的区域表示出来。
为了解决过分割的问题,提出了改进算法:基于标记(mark)图像的分水岭算法,本质上就是通过先验知识,来指导分水岭算法,以便获得更好的图像分割效果。通常的mark图像,都是在某个区域定义了一些灰度层级,在这个区域的洪水淹没过程中,水平面都是从定义的高度开始的,这样可以避免一些很小的噪声极小值区域的分割。
分水岭算法的API:
res = watershed(image,markers)
参数:
image: 输入图像,必须是8位的3通道彩色图像
marker: 标记图像,32位单通道图像,它包括种子点信息,使用轮廓信息作为种子点。在进行分水岭算法之前,必须设置好marker信息,它包含不同区域的轮廓,每个轮廓有唯一的编号,使用findCountours方法确定轮廓位置,不同区域的交界位置为-1。
对于maker的解读:
它应该包含不同区域的轮廓,每个轮廓有一个自己唯一的编号,这个是执行分水岭之前的要求。
算法会根据markers传入的轮廓作为种子(也就是所谓的注水点),对图像上其他的像素点根据分水岭算法规则进行判断,并对每个像素点的区域归属进行划定,直到处理完图像上所有像素点。而区域与区域之间的分界处的值被置为“-1”,以做区分。
用代码实现分水岭算法。
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
#1.读入图片
img = cv.imread('03.png')
gray_img = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
#2.canny边缘检测
canny = cv.Canny(gray_img,80,150)
#3.轮廓检测并设置标记图像
#寻找图像轮廓 返回修改后的图像 图像的轮廓 以及它们的层次
contours,hierarchy = cv.findContours(canny,cv.RETR_TREE,cv.CHAIN_APPROX_SIMPLE)
#32位有符号整数类型,
marks = np.zeros(img.shape[:2],np.int32)
#findContours检测到的轮廓
imageContours = np.zeros(img.shape[:2],np.uint8)
#轮廓颜色
compCount = 0
index = 0
#绘制每一个轮廓
for index in range(len(contours)):
#对marks进行标记,对不同区域的轮廓使用不同的亮度绘制,相当于设置注水点,有多少个轮廓,就有多少个轮廓
#图像上不同线条的灰度值是不同的,底部略暗,越往上灰度越高
marks = cv.drawContours(marks,contours,index,(index,index,index),1,8,hierarchy)
#绘制轮廓,亮度一样
imageContours = cv.drawContours(imageContours,contours,index,(255,255,255),1,8,hierarchy)
#4 使用分水岭算法,并给不同的区域随机填色
marks = cv.watershed(img,marks)
afterWatershed = cv.convertScaleAbs(marks)
#生成随机颜色
colorTab = np.zeros((np.max(marks)+1,3))
#生成0~255之间的随机数
for i in range(len(colorTab)):
aa = np.random.uniform(0,255)
bb = np.random.uniform(0,255)
cc = np.random.uniform(0,255)
colorTab[i] = np.array([aa,bb,cc],np.uint8)
bgrImage = np.zeros(img.shape,np.uint8)
#遍历marks每一个元素值,对每一个区域进行颜色填充
for i in range(marks.shape[0]):
for j in range(marks.shape[1]):
#index值一样的像素表示在一个区域
index = marks[i][j]
#判断是不是区域与区域之间的分界,如果是边界(-1),则使用白色显示
if index == -1:
bgrImage[i][j] = np.array([255,255,255])
else:
bgrImage[i][j] = colorTab[index]
#5 图像显示
plt.figure(figsize=(10,8),dpi=100)
plt.subplot(131),plt.imshow(img,'gray'),plt.title('RAW')
plt.xticks([]), plt.yticks([])
plt.subplot(132),plt.imshow(bgrImage[:,:,::-1] ,'gray'),plt.title('watershed')
plt.show()
运行结果如下:
(四)代码实现
详见GitHub账号,正在创建中。
链接:https://pan.baidu.com/s/1qG-saa4aT8ZYC8b6N7-AEQ
提取码:vfgn
参考博客:
1)OpenCV:图像分割、阈值分割、全阈值分割、自适应阈值分割、Otsu 阈值(大津法)、分水岭算法、GrabCut算法_あずにゃん梓喵的博客-CSDN博客2
2)【拜小白opencv】22-自适应阈值化操作:adaptiveThreshold()函数_拜小白的成长之路,告别小白-CSDN博客_adaptivethreshold函数的参数