在图像处理的过程中,经常需要从图像中将前景对象作为目标图像分割或者提取出来。图像分割是图像处理过程中一种非常重要的操作。分水岭算法将图像形象地比喻为地理学上的地形表面,实现图像分割,该算法非常有效。
任何一幅灰度图像,都可以被看作是地理学上的地形表面,灰度值高的区域可以被看成是山峰,灰度值低的区域可以被看成是山谷。如下图所示,其中左图是原始图像,右图是其对应的“地形表面”。
如果我们向每一个山谷中“灌注”不同颜色的水(这里采用了OpenCV官网的表述,冈萨雷斯将灌注表述为在山谷中打洞,然后让水穿过洞以均匀的速率上升)。那么,随着水位不断地升高,不同山谷的水就会汇集到一起。在这个过程中,为了防止不同山谷的水交汇,我们需要在水流可能汇合的地方构建堤坝。该过程将图像分成两个不同的集合:集水盆地和分水岭线。我们构建的堤坝就是分水岭线,也即对原始图像的分割。这就是分水岭算法。
灰度图以及其对应的 地形表面”
使用分水岭算法得到的图像分割结果
由于噪声等因素的影响,采用上述基础分水岭算法经常会得到过度分割的结果。过度分割会将图像划分为一个个稠密的独立小块,让分割失去了意义。为了改善图像分割效果,人们提出来了基于掩膜的改进的分水岭算法。改进的分水岭算法允许用户将他认为是同一个分割区域的部分标注出来(被标注的部分就成为掩膜)。其实我们在制作ppt的时候,插入图片后有个删除背景。这个功能就是基于改进后的分水岭算法。
在OpenCV中,可以使用函数cv2.watershed()实现分水岭算法。在具体的实现过程中,还需要借助于形态学函数、距离变换函数cv2.distanceTransform()、cv2.connectedComponents()来完成图像分割。下面对分水岭算法中用到的函数进行简单的说明:
在使用分水岭算法对图像进行分割之前,需要对图像进行简单的形态学处理。这里我们主要使用到了开运算和利用减法获取图像边界,大家可以参考:opencv形态学操作-
当图像内的各个子图没有连接时,可以直接使用形态学的腐蚀操作确定前景对象,但是如果图像内的子图连接在一起时,就很难确定前景对象了。此时,借助于距离变换函数cv2.distanceTransform()可以方便地将前景对象提取出来。
距离变换函数cv2.distanceTransform()计算二值图像内任意点到最近背景点的距离。一般情况下,该函数计算的是图像内非零值像素点到最近的零值像素点的距离,即计算二值图像中所以像素点距离其最近的值为0的像素点的距离。当然,如果有的像素点本身的值为0,则这个距离也为0。
距离变换函数cv2.distanceTransform()的计算结果反映了各个像素与背景(值为0的像素点)的距离关系。通常情况下:
如果对上述计算结果进行阈值化,就可以得到图像内子图的中心、骨架等信息。距离变换函数cv2.distanceTransform()可以用于计算对象的中心,还能细化轮廓、获取图像前景等,有多种功能。距离变换函数cv2.distanceTransform()的语法格式为:
dst = cv2.distanceTransform(src, distanceType, maskSize,[, dstType]])
式中:
使用形态学的膨账操作能够将图像内的前景“ 膨账放大 ”。当图像内的前景被放大后,背
景就会被“ 压缩 ”,所以此时得到的背景信息一定小于实际背景的,不包含前景的“确定背景”。
以下为了方便说明将确定背景称为B。
距离变换函数 cv2.distanceTransform( ) 能够获取图像的“中心”,得到“ 确定前景 ”。为了方
便说明,将确定前景称为F。图像中有了确定前景F 和 确定背景B,剩下区域的就是未知区域UN了。这部分区域正是分水岭算法要进一步明确的区域。
针对一幅图像O,通过以下关系能够得到未知区域UN:
未知区域 UN = 图像O - 确定背景B - 确定前景F
对上述表达式进行整理,可以得到:
未知区域 UN =(图像O - 确定背景B)- 确定前景F
上式中的 “ 图像O - 确定背景 B ” ,可以通过对图像进行形态学的膨胀操作得到。
明确了确定前景后,就可以对确定的确定前景图像进行标注了。在 OpenCV 中,可以使用函数cv2.connectedComponents()进行标注,该函数会将背景标注为0,将其他的对象使用从1开始的正整数标注。函数 cv2.connectedComponents()的语法格式为:
retval, labels = cv2. connectedComponents ( image )
式中:
完成上述处理后,就可以使用分水岭算法对预处理的结果图像进行分割。在OpenCV中,实现分水岭算法的函数是cv2.watershed(),其语法格式为:
markers = cv2.watershed(image,markers)
结合上述的知识,使用分水岭算法进行图像分割时,基本的步骤为:
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()