OpenCV进行图像分割:分水岭算法(相关函数介绍以及项目实现)

 

一、简介

在图像处理的过程中,经常需要从图像中将前景对象作为目标图像分割或者提取出来。图像分割是图像处理过程中一种非常重要的操作。分水岭算法将图像形象地比喻为地理学上的地形表面,实现图像分割,该算法非常有效。

二、算法原理

任何一幅灰度图像,都可以被看作是地理学上的地形表面,灰度值高的区域可以被看成是山峰,灰度值低的区域可以被看成是山谷。如下图所示,其中左图是原始图像,右图是其对应的“地形表面”。
如果我们向每一个山谷中“灌注”不同颜色的水(这里采用了OpenCV官网的表述,冈萨雷斯将灌注表述为在山谷中打洞,然后让水穿过洞以均匀的速率上升)。那么,随着水位不断地升高,不同山谷的水就会汇集到一起。在这个过程中,为了防止不同山谷的水交汇,我们需要在水流可能汇合的地方构建堤坝。该过程将图像分成两个不同的集合:集水盆地和分水岭线。我们构建的堤坝就是分水岭线,也即对原始图像的分割。这就是分水岭算法。

灰度图以及其对应的 地形表面”

使用分水岭算法得到的图像分割结果 

 由于噪声等因素的影响,采用上述基础分水岭算法经常会得到过度分割的结果。过度分割会将图像划分为一个个稠密的独立小块,让分割失去了意义。为了改善图像分割效果,人们提出来了基于掩膜的改进的分水岭算法。改进的分水岭算法允许用户将他认为是同一个分割区域的部分标注出来(被标注的部分就成为掩膜)。其实我们在制作ppt的时候,插入图片后有个删除背景。这个功能就是基于改进后的分水岭算法。

三、相关函数介绍

在OpenCV中,可以使用函数cv2.watershed()实现分水岭算法。在具体的实现过程中,还需要借助于形态学函数、距离变换函数cv2.distanceTransform()、cv2.connectedComponents()来完成图像分割。下面对分水岭算法中用到的函数进行简单的说明:

3.1 形态学函数回顾

在使用分水岭算法对图像进行分割之前,需要对图像进行简单的形态学处理。这里我们主要使用到了开运算和利用减法获取图像边界,大家可以参考:opencv形态学操作-

3.2 距离变换函数 distance Transform

       当图像内的各个子图没有连接时,可以直接使用形态学的腐蚀操作确定前景对象,但是如果图像内的子图连接在一起时,就很难确定前景对象了。此时,借助于距离变换函数cv2.distanceTransform()可以方便地将前景对象提取出来。

      距离变换函数cv2.distanceTransform()计算二值图像内任意点到最近背景点的距离。一般情况下,该函数计算的是图像内非零值像素点到最近的零值像素点的距离,即计算二值图像中所以像素点距离其最近的值为0的像素点的距离。当然,如果有的像素点本身的值为0,则这个距离也为0。

距离变换函数cv2.distanceTransform()的计算结果反映了各个像素与背景(值为0的像素点)的距离关系。通常情况下:

  • 如果前景对象的中心(质心)距离 值为0的像素点 距离较远,会得到一个较大的值
  • 如果前景对象的边缘 距离 值为0的像素点 较近,会得到一个较小的值。

       如果对上述计算结果进行阈值化,就可以得到图像内子图的中心、骨架等信息。距离变换函数cv2.distanceTransform()可以用于计算对象的中心,还能细化轮廓、获取图像前景等,有多种功能。距离变换函数cv2.distanceTransform()的语法格式为:

dst = cv2.distanceTransform(src, distanceType, maskSize,[, dstType]])

 式中:

  • src是8位单通道的二值图像
  • distanceType 为距离类型参数,其具体值和含义如下图所示:

  •  maskSize 为掩膜的大小,其可能的值如下表所示。需要注意的是,当 distanceType = cv2.DIST_L1 或 cv2.DIST_C时,maskSize强制为3(因为设置为3和设置为5以及更大值没什么区别)

  • dstType 为目标图像的类型,默认值为CV_32F
  • dst 表示计算得到的目标图像,可以是8位或32位浮点数,尺寸和src相同

3.3  确定未知区域

使用形态学的膨账操作能够将图像内的前景“ 膨账放大 ”。当图像内的前景被放大后,背
景就会被“ 压缩 ”,所以此时得到的背景信息一定小于实际背景的,不包含前景的“确定背景”。
以下为了方便说明将确定背景称为B。
距离变换函数 cv2.distanceTransform( ) 能够获取图像的“中心”,得到“ 确定前景 ”。为了方
便说明,将确定前景称为F。图像中有了确定前景F 和 确定背景B,剩下区域的就是未知区域UN了。这部分区域正是分水岭算法要进一步明确的区域。

针对一幅图像O,通过以下关系能够得到未知区域UN:

未知区域 UN = 图像O - 确定背景B - 确定前景F

对上述表达式进行整理,可以得到:
未知区域 UN =(图像O  -  确定背景B)- 确定前景F
上式中的 “ 图像O -  确定背景 B ” ,可以通过对图像进行形态学的膨胀操作得到。

3.4  函数 connectedComponents

明确了确定前景后,就可以对确定的确定前景图像进行标注了。在 OpenCV 中,可以使用函数cv2.connectedComponents()进行标注,该函数会将背景标注为0,将其他的对象使用从1开始的正整数标注。函数 cv2.connectedComponents()的语法格式为:

retval, labels = cv2. connectedComponents ( image  )

式中:

  • image 为8位单通道的待标注图像。
  • retval 为返回的标注的数量。
  • labels 为标注的结果图像。

3.5 函数cv2.watershed()

完成上述处理后,就可以使用分水岭算法对预处理的结果图像进行分割。在OpenCV中,实现分水岭算法的函数是cv2.watershed(),其语法格式为:

markers = cv2.watershed(image,markers)
  • image是输入图像,必须是 8位三通道的图像。在对图像使用 cv2.watershed()函数处理之前,必须先用正数大致勾画出图像中的期望分割区域。每一个分割的区域会被标注为1、2、3等。对于尚未确定的区域,需要将它们标注为0。我们可以将标注区域理解为进行分水岭算法分割的“种子”区域。
  • markers 是32 位单通道的标注结果,它应该和 image具有相等大小。在markers 中,每一个像素要么被设置为初期的“ 种子值 ”,要么被设置为“ -1 ” 表示边界。markers 可以省略。

四、分水岭算法图像分割示例

结合上述的知识,使用分水岭算法进行图像分割时,基本的步骤为:

  1. 通过形态学开运算对原始图像O去噪
  2. 通过腐蚀操作获取 “ 确定背景B ” 。 需要注意的是,这里得到 “ 原始图像O - 确定背景B ”即可。
  3. 利用距离变换函数cv2.distanceTransform()对原始图像进行运算,并对其进行阈值处理,得到 “ 确定背景F ”。
  4. 计算未知区域 UN (UN = O - B - F)。
  5. 利用函数cv2.connectedComponents()对原始图像进行标注。
  6.  对标注的结果进行修正。
  7. 使用分水岭函数完成对图像的分割。
import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread('black.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
ishow = img.copy()
ret, thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
kernel = np.ones((3,3), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)
sure_bg = cv2.dilate(opening, kernel, iterations=3)
dist_transform = cv2.distanceTransform(opening,cv2.DIST_L2,5)
ret, sure_fg = cv2.threshold(dist_transform, 0.7*dist_transform.max(),255,0)
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg, sure_fg)
ret, markers = cv2.connectedComponents(sure_fg)
markers = markers + 1
markers = cv2.watershed(img, markers)
img[markers == -1] = [0, 255, 0]
plt.subplot(121)
plt.imshow(ishow)
plt.axis("off")
plt.subplot(122)
plt.imshow(img)
plt.axis('off')
plt.show()


cv2.waitKey()
cv2.destroyAllWindows()

猜你喜欢

转载自blog.csdn.net/m0_62128864/article/details/124541624