[OpenCV]Watershed算法进行图形分割

52
0
2021年2月16日 09时10分

引言

 

进行图像的分割的方式有很多种,这里我们主要要介绍的是一种叫做分水岭(Watershed)的算法。

 

介绍

 

与GrabCut算法不同的是,GrabCut算法的实现步骤是:
(1)图片中定义(一个或者多个)物体的矩形。
(2)矩形外的区域被自动认为是背景。(先假设)
(3)对于矩形区域内,可以用背景中的数据来区别它里面的前景和背景区域。
(4)之后我们用高斯混合模型(Gaussians Mixture Model,GMM)来对背景和前景建模,并将未定义的像素标记为可能的前景或背景。
(5)图像中的每一个像素都被看做通过虚拟边与周围像素相连接,而每条边都有一个属于前景或背景的概率,这基于它与周围像素颜色上的相似性。(这个与图论中的连接一致)
(6)每一个像素(即算法中的节点)会与一个前景或背景节点进行连接。
(7)在节点完成连接后(可能与前景或背景相连接),若节点之间的边属于不同终端(即:一边是前景一边是背景)则会切断他们的边,从而将图像各部分分割出来。(构成不同的连通分支)
而在分水岭算法中,我们需要将图像中低密度的区域(颜色变化很小的区域)想象成山谷的形状,之后再将图像中高密度的区域(颜色变化程度大的区域)想象成山峰。
之后我们开始“向山谷中注水”,直到不同山谷中的水开始汇聚,为了阻止水汇聚,可以设置一些栅栏,最后得到的栅栏就是我们图像分割的结果。
给不同山谷的水不同颜色的水,水涨起来后,不同颜色的水会有合并的趋势,为了阻止这种情况的发生,我们需要加上栅栏,而我们加上的栅栏就是最后的分割。

 

案例

 

之后我们来进行实例的分析,我们用下图来进行分水岭算法的实现。

 

[OpenCV]Watershed算法进行图形分割插图
 

首先我们先对其进行加载,然后将其从BGR图转换成灰度图,之后再进行阈值处理:

 

import numpy as np
import cv2
img=cv2.imread('1.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
cv2.imshow('img1',thresh)

 

结果如图所示:

 

[OpenCV]Watershed算法进行图形分割插图(1)
 

之后我们进行噪声的去除,对其进行开操作:

 

kernel = np.ones((3,3),np.uint8)
opening = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel, iterations = 2)
cv2.imshow('img2',opening)

 

其结果如下图所示:

 

[OpenCV]Watershed算法进行图形分割插图(2)
 

接下来我们对其使用膨胀,我们所得到的大部分区域都是背景部分:

 

sure_bg = cv2.dilate(opening,kernel,iterations=3)
cv2.imshow('img3',sure_bg)

[OpenCV]Watershed算法进行图形分割插图(3)
 

然后我们需要获取确定的前景区域,也就是最可能是前景的区域,越是远离背景的区域就越是可能是前景,使用下列代码后得到的图中的灰度值表示可能性:

 

dist_transform = cv2.distanceTransform(opening,cv2.DIST_L2,5)
cv2.imshow('img4',dist_transform)

[OpenCV]Watershed算法进行图形分割插图(4)
 

这里可能看起来是二值图 但其实还是一种灰度变化不明显的灰度图。
然后我们使用阈值来将其转变为二值图:

 

ret, sure_fg = cv2.threshold(dist_transform,0,255,0)
cv2.imshow('img5',sure_fg)

 

其结果为如图所示:

 

[OpenCV]Watershed算法进行图形分割插图(5)
 

之后因为之前用了dilate函数,所以前景和背景之间可能会有重合,所以此处我们需要进行这块区域的确认,可以通过sure_bg与sure_fg相减得到这块未知的区域:

 

sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg,sure_fg)
cv2.imshow('img6',unknown)

 

结果如图所示:

 

[OpenCV]Watershed算法进行图形分割插图(6)
 

需要注意的是,下面我们会使用一个cv2.connectedComponents的函数,该函数会创建一个图像与原图像大小相同,不过数据类型会变成int32,他会把图像背景标记为0,其他目标用从1开始标记;但是如果光只是这么做的话,分水岭算法会将标为0的位置认为是unknown,所以我们对里面所有的像素的值加一,这样原本为0的位置上的像素的值就是1了。之后我们设置栅栏来阻止水汇聚,我们把图像堪称有连接边的节点集,给定确定的前景区,其中一些节点会连在一起,而另一些没连在一起的,这意味着他们来自不同的山谷,所以他们之间需要设置一个栅栏:

 

ret, markers = cv2.connectedComponents(sure_fg)
# 所有像素值+1
markers = markers+1
# 这里将所有未知区域的像素设置为0
markers[unknown==255] = 0
# 打开门,让水漫起来
markers = cv2.watershed(img,markers)
# 将原图中的栅栏位置的颜色设置为蓝色并进行显示
img[markers == -1] = [255,0,0]
cv2.imshow('img',img)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

最后我们的结果图如下图所示。

 

[OpenCV]Watershed算法进行图形分割插图(7)
 

总结

 

正如我们开头的时候所说的,所有的灰度图像都可视为拓扑平面,灰度值高的区域看成山峰,灰度值低的区域看成山谷,我们向图像上所有的“山谷”注入不同颜色的水,不断的注水,水位则不断上升,注入的水将灌满山谷,并可能淹没山峰,为了防止不同颜色的山谷中的水溢出汇合,我们可在汇合的地方筑起堤坝,故可将堤坝看作是对图像的分割后形成的边界。

 

而常规的分水岭算法由于图像上噪声和图局部不连续原因常常表现出过度分割。由于噪声的存在以及连接物体的特点,传统的标记分水岭算法对包含连接物体的灰度图像很难取得满意的分割结果;特别是在背景并不连通的情况下,误分割更为常见。为此,在标记分水岭算法的基础上,提出了一种连接物体分割方法;将属于鲁棒统计的Hough变换(也就是统计霍夫变换)用于提取物体标记扩展了标记分水岭算法的应用范围;针对在分割连接物体时,由于背景并非连通,因此允许背景被分别标记,并通过一个后续滤波步骤用以剔除分割后图像中的背景部分,从而得到精确的分割图像;试验证明该算法运算速度快,鲁棒性好,具有广泛的应用价值。感兴趣的读者可以去查阅一些相关的资料。
本文到此结束,谢谢大家。

 

发表评论

后才能评论