0%

【OpenCv】图像分割——分水岭算法

1 原理

  分水岭分割方法,是一种基于拓扑理论的数学形态学的分割方法,其基本思想是把图像看作是测地学上的拓扑地貌,图像中每一点像素的灰度值表示该点的海拔高度,每一个局部极小值及其影响区域称为集水盆,而集水盆的边界则形成分水岭。分水岭的概念和形成可以通过模拟浸入过程来说明。在每一个局部极小值表面,刺穿一个小孔,然后把整个模型慢慢浸入水中,随着浸入的加深,每一个局部极小值的影响域慢慢向外扩展,在两个集水盆汇合处构筑大坝,即形成分水岭。这种方法也称作泛洪法,对应的还有降雨法。

在这里插入图片描述
  分水岭的计算过程是一个迭代标注过程。分水岭比较经典的计算方法是L. Vincent提出的。在该算法中,分水岭计算分两个步骤,一个是排序过程,一个是淹没过程。首先对每个像素的灰度级进行从低到高排序,然后在从低到高实现淹没过程中,对每一个局部极小值在 $h$ 阶高度的影响域采用先进先出(FIFO)结构进行判断及标注。具体流程如下:

  • 把梯度图像中的像素按照灰度值进行分类,设定一个测地距离阈值(测地线距离(Geodesic Distance):地球表面两点之间的最短路径的距离)。
  • 找到灰度值最小的像素点,让 $threshold$ 从最小值开始增长,这些点为起始点。
  • 水平面在增长的过程中,会碰到周围的邻域像素,测量这些像素到起始点(灰度值最低点)的测地距离,如果小于设定阈值,则将这些像素淹没,否则在这些像素上设置大坝,这样就对这些邻域像素进行了分类。
  • 随着水平面越来越高,会设置更多更高的大坝,直到灰度值的最大值,所有区域都在分水岭线上相遇,这些大坝就对整个图像像素的进行了分区。
    在这里插入图片描述

2 算法改进

  基于梯度图像的直接分水岭算法容易导致图像的过分割,产生这一现象的原因主要是由于输入的图像存在过多的极小区域而产生许多小的集水盆地,从而导致分割后的图像不能将图像中有意义的区域表示出来。

在这里插入图片描述
在 $OpenCv$ 中算法不从最小值开始增长,可以将相对较高的灰度值像素作为起始点(需要用户手动标记),从标记处开始进行淹没,则很多小区域都会被合并为一个区域,这被称为基于图像标(mark)的分水岭算法。其中标记的每个点就相当于分水岭中的注水点,从这些点开始注水使得水平面上升。手动标记太麻烦,我们可是使用距离转换(cv2.distanceTransform函数)的方法进行标记。cv2.distanceTransform计算的是图像内非零值像素点到最近的零值像素点的距离,即计算二值图像中所有像素点距离其最近的值为 0 的像素点的距离。当然,如果像素点本身的值为 0,则这个距离也为 0。在这里插入图片描述

3 API

1
cv2.watershed( InputArray image, InputOutputArray markers )
  • 第一个参数 $image$,必须是一个8bit 3通道彩色图像矩阵序列。
  • 关键是第二个参数 markers:在执行分水岭函数watershed之前,必须对第二个参数 $markers$ 进行处理,它应该包含不同区域的轮廓,每个轮廓有一个自己唯一的编号,轮廓的定位可以通过 $Opencv$ 中 connectedComponents 方法实现,这个是执行分水岭之前的要求。

算法会根据markers传入的轮廓作为种子(也就是所谓的注水点),对图像上其他的像素点根据分水岭算法规则进行判断,并对每个像素点的区域归属进行划定,直到处理完图像上所有像素点。而区域与区域之间的分界处的值被置为“-1”,以做区分。

4 实例

目标是将下图中的硬币和背景分离:

在这里插入图片描述
在编程之前为了更好理解过程,要先介绍几个概念。

  • 背景:不感兴趣的区域,越远离目标图像中心的区域就越是背景
  • 前景:感兴趣的区域,越靠近目标图像中心就越是前景
  • 未知区域:即不确定区域,边界所在的区域

流程图:

在这里插入图片描述
代码:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt

def test_watershed() :
image = cv.imread('images/coins.jpg')
image_gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
#基于直方图的二值化处理
_, thresh = cv.threshold(image_gray, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)

#做开操作,是为了除去白噪声
kernel = np.ones((3, 3), dtype = np.uint8)
opening = cv.morphologyEx(thresh, cv.MORPH_OPEN, kernel, iterations = 2)

#做膨胀操作,是为了让前景漫延到背景,让确定的背景出现
sure_bg = cv.dilate(opening, kernel, iterations = 2)

#为了求得确定的前景,也就是注水处使用距离的方法转化
dist_transform = cv.distanceTransform(opening, cv.DIST_L2, 5)
#归一化所求的距离转换,转化范围是[0, 1]
cv.normalize(dist_transform, dist_transform, 0, 1.0, cv.NORM_MINMAX)
#再次做二值化,得到确定的前景
_, sure_fg = cv.threshold(dist_transform, 0.5 * dist_transform.max(), 255, 0)
sure_fg = np.uint8(sure_fg)

#得到不确定区域也就是边界所在区域,用确定的背景图减去确定的前景图
unknow = cv.subtract(sure_bg, sure_fg)

#给确定的注水位置进行标上标签,背景图标为0,其他的区域由1开始按顺序进行标
_, markers = cv.connectedComponents(sure_fg)

# cv.imshow('markers', markers.astype(np.uint8))
# cv.waitKey(0)

#让标签加1,这是因为在分水岭算法中,会将标签为0的区域当作边界区域(不确定区域)
markers += 1

#是上面所求的不确定区域标上0
markers[unknow == 255] = 0
# print(markers.dtype) int32
markers_copy = markers.copy()

# 使用分水岭算法执行基于标记的图像分割,将图像中的对象与背景分离
markers = cv.watershed(image, markers)

#分水岭算法得到的边界点的像素值为-1
image[markers == -1] = [0, 0, 255]

images = [thresh, opening, sure_bg, dist_transform, sure_fg, unknow, markers_copy, image]

titles = ['thresh', 'opening', 'sure_bg', 'dist_tranform', 'sure_fg', 'unknow', 'markers', 'image']
plt.figure(figsize = (8, 6.1))

for i in range(len(images)) :
if i == 7 :
plt.subplot(2, 4, i + 1)
plt.imshow(cv.cvtColor(images[i], cv.COLOR_BGR2RGB))
plt.title(titles[i])
plt.axis('off')
else :
plt.subplot(2, 4, i + 1)
plt.imshow(images[i], cmap = 'gray')
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.savefig('figure.png')
plt.show()

if __name__ == '__main__':
test_watershed()

效果:
在这里插入图片描述
从上面看来效果还是蛮好的。