二十一 分水岭算法

一、原理

分水岭算法主要用于图像分段,通常是把一副彩色图像灰度化,然后再求梯度图,最后在梯度图的基础上进行分水岭算法,求得分段图像的边缘线。

任意的灰度图像可以被看做是地质学表面,高亮度的地方是山峰,低亮度的地方是山谷。给每个孤立的山谷(局部最小值)不同颜色的水(标签),当水涨起来,根据周围的山峰(梯度),不同的山谷也就是不同的颜色会开始合并,要避免这个,你可以在水要合并的地方建立障碍,直到所有山峰都被淹没。你所创建的障碍就是分割结果,这个就是分水岭的原理,但是这个方法会分割过度,因为有噪点,或者其他图像上的错误。所以OpenCV实现了一个机遇标记的分水岭算法,你可以指定哪些是要合并的点,哪些不是,这是一个交互式的图像分割,我们要做的是给不同的标签。给我们知道是前景或者是目标用一种颜色加上标签,给我们知道是背景或者非目标加上另一个颜色,最后不知道是什么的区域标记为0. 然后使用分水岭算法。

对灰度图的地形学解释,我们我们考虑三类点:

1. 局部最小值点,该点对应一个盆地的最低点,当我们在盆地里滴一滴水的时候,由于重力作用,水最终会汇聚到该点。注意:可能存在一个最小值面,该平面内的都是最小值点。

2. 盆地的其它位置点,该位置滴的水滴会汇聚到局部最小点。

3. 盆地的边缘点,是该盆地和其它盆地交接点,在该点滴一滴水,会等概率的流向任何一个盆地。

image

       假设我们在盆地的最小值点,打一个洞,然后往盆地里面注水,并阻止两个盆地的水汇集,我们会在两个盆地的水汇集的时刻,在交接的边缘线上(也即分水岭线),建一个坝,来阻止两个盆地的水汇集成一片水域。这样图像就被分成2个像素集,一个是注水盆地像素集,一个是分水岭线像素集。

      下面的gif图很好的演示了分水岭算法的效果:

lpe1 (1)ima3 (1)

为了解决过度分割的问题,可以使用基于标记(mark)图像的分水岭算法,就是通过先验知识,来指导分水岭算法,以便获得更好的图像分段效果。通常的mark图像,都是在某个区域定义了一些灰度层级,在这个区域的洪水淹没过程中,水平面都是从定义的高度开始的,这样可以避免一些很小的噪声极值区域的分割。

      下面的gif图很好的演示了基于mark的分水岭算法过程:

ima4lpe2ima5

      上面的过度分段图像,我们通过指定mark区域,可以得到很好的分段效果:

ima8ima9

二、步骤

1、获取灰度图像,二值化图像,进行形态学操作,消除噪点

 1     #去除噪点
 2     blur = cv.pyrMeanShiftFiltering(image,10,100)
 3 
 4     #二值图像
 5     gray = cv.cvtColor(blur,cv.COLOR_BGR2GRAY)
 6     ret,binary = cv.threshold(gray,0,255,cv.THRESH_BINARY_INV|cv.THRESH_OTSU)
 7     cv.imshow('binary',binary)
 8 
 9     #形态学操作,开操作两次,进一步消除噪点
10     kernel = cv.getStructuringElement(cv.MORPH_RECT,(3,3))
11     mb = cv.morphologyEx(binary,cv.MORPH_OPEN,kernel,iterations=2)
12     

2、在距离变换前加上一步操作:通过对上面形态学去噪点后的图像,进行膨胀操作,可以得到大部分都是背景的区域(原黑色不是我们需要的部分是背景)

#膨胀  背景色
dilate_image = cv.dilate(mb,kernel,iterations=3)
cv.imshow('background',dilate_image)

 

3、使用距离变换distanceTransform获取确定的前景色

 (1)距离变换原理

距离变换的定义是计算一个图像中非零像素点到最近的零像素点的距离,也就是到零像素点的最短距离。

这个定义对于初接触距离变换的人来说,完全不知所云啊~那是因为缺乏一些知识背景,下面听我慢慢道来吧!

距离变换的处理图像通常都是二值图像,而二值图像其实就是把图像分为两部分,即背景和物体两部分,物体通常又称为前景目标!通常我们把前景目标的灰度值设为255,即白色,背景的灰度值设为0,即黑色。所以定义中的非零像素点即为前景目标,零像素点即为背景。所以图像中前景目标中的像素点距离背景越远,那么距离就越大,如果我们用这个距离值替换像素值,那么新生成的图像中这个点越亮。具体的应用就是找前景目标的中心~下面给一个具体的例子。

下面这个例子是确认手掌中心的例子:

由于伸出的手指相对于手掌来说比较细(如下图“src”窗口图像所示),也就是说手指上的像素距离零像素距离很短,所以经过距离变换后的图像在手指部位的像素值较小(如下图“dst”窗口图像所示),通过设定合理的阈值对距离变换后的图像进行二值化处理,则可得到去除手指的图像(如下图“bidist”窗口图像所示),手掌重心即为该图像的几何中心。

其它需要注意的:

从定义中我们可以看出距离变换中其实只计算前景目标区域(即非零像素点)的距离值!

(2)distancetransform函数

主要用于计算非零像素到最近零像素点的最短距离。一般用于求解图像的骨骼,得到的是距离图像数组!

def distanceTransform(src, distanceType, maskSize, dst=None, dstType=None): # real signature unknown; restored from __doc__

src:输入的图像,一般为二值图像

distanceType:所用的求解距离的类型,有CV_DIST_L1, CV_DIST_L2 , or CV_DIST_C

mask_size:距离变换掩模的大小,可以是 3 或 5. 对 CV_DIST_L1 或 CV_DIST_C 的情况,参数值被强制设定为 3, 因为 3×3 mask 给出 5×5 mask 一样的结果,而且速度还更快。

(3)若是想骨骼显示(对我们的分水岭流程无影响),我们需要对distanceTransform返回的结果进行归一化处理,使用normalize

distanceTransform(InputArray src, OutputArray dst, int distanceType, int maskSize)

InputArray src:输入的图像,一般为二值图像

 OutputArray dst:输出的图像

int distanceType:所用的求解距离的类型、

It can be CV_DIST_L1, CV_DIST_L2 , or CV_DIST_C

mask_size  距离变换掩模的大小,可以是 3 或 5. 对 CV_DIST_L1 或 CV_DIST_C 的情况,参数值被强制设定为 3, 因为 3×3 mask 给出 5×5 mask 一样的结果,而且速度还更快。 


mask
用户自定义距离情况下的 mask。 在 3×3 mask 下它由两个数(水平/垂直位量,对角线位移量)组成, 5×5 mask 下由三个数组成(水平/垂直位移量,对角位移和 国际象棋里的马步(马走日))
函数 cvDistTransform 二值图像每一个象素点到它最邻近零象素点的距离。对零象素,函数设置 0 距离,对其它象素,它寻找由基本位移(水平、垂直、对角线或knight's move,最后一项对 5×5 mask 有用)构成的最短路径。 全部的距离被认为是基本距离的和。由于距离函数是对称的,所有水平和垂直位移具有同样的代价 (表示为 a ), 所有的对角位移具有同样的代价 (表示为 b), 所有的 knight's 移动具有同样的代价 (表示为 c). 对类型 CV_DIST_C 和 CV_DIST_L1,距离的计算是精确的,而类型 CV_DIST_L2 (欧式距离) 距离的计算有某些相对误差 (5×5 mask 给出更精确的结果), OpenCV 使用 [Borgefors86] 推荐的值:

CV_DIST_C (3×3):a=1, b=1
CV_DIST_L1 (3×3):a=1, b=2
CV_DIST_L2 (3×3):a=0.955, b=1.3693
CV_DIST_L2 (5×5):a=1, b=1.4, c=2.1969

(4)代码实现

 1     #j距离变换,获取前景色
 2     dist = cv.distanceTransform(mb,cv.DIST_L2,5)    #这是我们获取的字段距离数值,对应每个像素都有,所以数组结构和图像数组一致
 3     cv.imshow('distance',dist)
 4     dist_output = cv.normalize(dist,0,1.0,cv.NORM_MINMAX)   #归一化的距离图像数组
 5     cv.imshow('distance_norm',dist_output*100)  #因为是0-1,所以颜色对比不明显,所以需要*100,*50也可以
 6     ret,front = cv.threshold(dist,dist.max()*0.6,255,cv.THRESH_BINARY)
 7     cv.imshow('front',front)
 8 
 9     #开始获取未知区域unknown(栅栏会创建在这一区域),为下一步获取种子做准备
10     front_image = np.uint8(front)
11     unknown = cv.subtract(background,front_image)
12     cv.imshow('unknown',unknown)

4、设置种子:获取了这些区域,我们可以获取种子,这是通过connectedComponents实现,获取masker标签,确定的前景区域会在其中显示为以1开始的数据,这就是我们的种子,会从这里开始漫水

(1)connectedComponents函数

1 def connectedComponents(image, labels=None, connectivity=None, ltype=None): # real signature unknown; restored from __doc__

参数image是需要进行连通域处理的二值图像,其他的这里用不到

ret是连通域处理的边缘条数,是上面提到的确定区域(出去背景外的其他确定区域:就是前景),就是种子数,我们会从种子开始向外涨水

markers是我们创  建的一个标签(一个与原图像大小相同,数据类型为 in32 的数组),其中包含有我们原图像的确认区域的数据(前景区域)

(2)代码实现

根据种子开始漫水,让水漫起来找到最后的漫出点(栅栏边界),越过这个点后各个山谷中水开始合并。注意watershed会将找到的栅栏在markers中设置为-1

1 #获取marker
2 ret,markers = cv.connectedComponents(front_image)
3 # markers = markers + 1  不知道有什么用
4 # markers[unknown==255] = 0
5 markers = cv.watershed(image,markers=markers)
6 image[markers==-1] = [0,0,255]
7 cv.imshow('finally',image)

 

猜你喜欢

转载自www.cnblogs.com/pacino12134/p/9901987.html