基于分水岭算法的图像分割
本节主要介绍基于分水岭算法的图像分割技术,用到以下函数: cv.watershed()
- 用法如下:cv.watershed( image, markers ) -> markers
一、概念
任何灰度图像都可以被视为一个地形表面,其中高强度表示峰和丘陵,而低强度表示低谷。 您开始用不同颜色的水(标签)填充每个孤立的山谷(局部最小值)。 随着水的上升,根据附近的山峰(梯度),来自不同山谷的水(显然具有不同的颜色)将开始合并。 为避免这种情况,您可以在水合并的位置建立障碍。 你继续注水和建造障碍物,直到所有的山峰都被淹没。 然后,您创建的障碍会为您提供分割结果。 这就是分水岭背后的“哲学”。 您可以访问分水岭上的 CMM 网页,借助一些动画来了解它。cmm.ensmp.fr/~beucher/wt…
但是由于图像中的噪声或任何其他不规则性,这种方法会给您带来过度分割的结果。 因此,OpenCV 实现了一个基于标记的分水岭算法,您可以在其中指定要合并哪些谷点,哪些不合并。 它是一种交互式图像分割。 我们所做的是为我们所知道的对象赋予不同的标签。 用一种颜色(或强度)标记我们确定为前景或对象的区域,用另一种颜色标记我们确定为背景或非对象的区域,最后标记我们不确定的区域, 用 0 标记它。 然后应用分水岭算法。 然后我们的标记将使用我们提供的标签进行更新,并且对象的边界将具有 -1 的值。
二、具体编码
下面我们将看到一个示例,说明如何使用距离变换和分水岭来分割相互接触的对象。 考虑下面的硬币图像,硬币相互接触。 即使你把它设为阈值,它也会相互接触。
我们首先找到硬币的近似估计。 为此,我们可以使用 Otsu 的二值化。
import cv2 as cv
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
def cv_show(name, img):
cv.imshow(name, img)
cv.waitKey(0)
cv.destroyAllWindows()
def compare(imgs):
# for i in range(len(imgs)):
# imgs[i][:,-3:-1,:] = [255,255,255]
res = np.hstack(imgs)
cv_show('Compare', res)
img = cv.imread('water_coins.jpg')
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(gray,0,255,cv.THRESH_BINARY_INV+cv.THRESH_OTSU)
compare([gray, thresh])
现在我们需要去除图像中的任何小的白噪声。 为此,我们可以使用形态学开场。 要移除对象中的任何小孔,我们可以使用形态闭合。 所以,现在我们确定靠近物体中心的区域是前景,远离物体的区域是背景。 只有我们不确定的区域是硬币的边界区域。
所以我们需要提取我们确定它们是硬币的区域。 侵蚀去除了边界像素。 所以无论剩下什么,我们可以确定它是硬币。 如果物体不相互接触,这将起作用。 但由于它们相互接触,另一个不错的选择是找到距离变换并应用适当的阈值。 接下来我们需要找到我们确定它们不是硬币的区域。 为此,我们扩大了结果。 膨胀增加对象边界到背景。 这样,我们可以确保结果中背景中的任何区域都是真正的背景,因为边界区域已被移除。 见下图。
使用腐蚀操作或者距离变换找到硬币ROI,也就是纯前景区域,利用膨胀操作找到纯背景区域。
剩下的区域是我们不知道的区域,无论是硬币还是背景。 那么分水岭算法就派上了用场。 这些区域通常位于前景和背景相遇的硬币边界周围(甚至两个不同的硬币周围)。 我们称之为边界。 可以通过从sure_bg 背景区域中减去sure_fg 前景区域得到。
# 两轮开操作来滤除噪声
kernel = np.ones((3,3),np.uint8)
opening = cv.morphologyEx(thresh,cv.MORPH_OPEN,kernel, iterations = 2)
# 使用腐蚀操作确定背景区域
sure_bg = cv.dilate(opening,kernel,iterations=3)
# 使用距离变换确定前景区域
dist_transform = cv.distanceTransform(opening,cv.DIST_L2,5)
ret, sure_fg = cv.threshold(dist_transform,0.7*dist_transform.max(),255,0)
# 滤出未知区域,也就是分水岭算法需要处理的地方
sure_fg = np.uint8(sure_fg)
unknown = cv.subtract(sure_bg,sure_fg)
compare([opening, sure_fg, sure_bg, unknown])
查看结果。 在阈值图像中,我们得到了一些硬币区域,我们可以确定这些区域是硬币,现在它们已经分离。 (在某些情况下,您可能只对前景分割感兴趣,而不是对相互接触的对象进行分离。在这种情况下,您不需要使用距离变换,只需侵蚀就足够了。侵蚀只是另一种提取确定前景区域的方法,即全部。)
现在我们确定哪些是硬币区域,哪些是背景和所有。 所以我们创建标记(它是一个与原始图像大小相同的数组,但具有 int32 数据类型)并标记其中的区域。 我们确定知道的区域(无论是前景还是背景)都标有任何正整数,但整数不同,而我们不确定的区域则保留为零。 为此,我们使用 cv.connectedComponents()。 它用 0 标记图像的背景,然后用从 1 开始的整数标记其他对象。
但是我们知道,如果背景标记为 0,分水岭就会将其视为未知区域。 所以我们想用不同的整数来标记它。 相反,我们将用 0 标记由 unknown 定义的未知区域。
# 利用确定的前景图像为marker阵列标记赋值,前景 1 背景0
ret, markers = cv.connectedComponents(sure_fg)
# 将marker阵列全部加上 1 做偏移量, 因为 前景背景应该是有确定的部分,
# cv.watershed函数会处理marker = 0 的地方,为了避免背景被处理,我们要加上偏移量
markers = markers+1
# 让真正应该有watershed处理的未知处赋值为0
# marker和unkonwn阵列同等大小
markers[unknown==255] = 0
fig = plt.figure(figsize = (20, 8))
plt.imshow(markers)
plt.xticks([]),plt.yticks([])
plt.show()
查看 JET 颜色图中显示的结果。 深蓝色区域显示未知区域。 当然硬币的颜色不同。 与未知区域相比,确定背景的剩余区域以浅蓝色显示。
现在我们的标记已经准备好了。 现在是最后一步的时候了,应用分水岭。 然后标记图像将被修改。 边界区域将标记为 -1。
markers = cv.watershed(img,markers)
# 边界区域将标记为 -1,然后我们为原图像的边界区域上色
img[markers == -1] = [255,0,0]
fig = plt.figure(figsize = (20, 30))
plt.subplot(121),plt.imshow(markers)
plt.xticks([]),plt.yticks([])
plt.subplot(122),plt.imshow(img)
plt.xticks([]),plt.yticks([])
plt.show()
三、练习
# 一幅图像是由不同大小的灰度级像素值构成的,可以把不同的大小想象成不同高度的山脉,
# 接着在地表(就是从像素灰度级0开始)向这个山脉地脉注入水,
# 那么当一个山脉与另一个山脉将要融合的线上就是图像的边界,当水注入最高山脉后形成的现象就是整幅图的边界。
def water_shed(image):
blurred = cv.pyrMeanShiftFiltering(image, 10, 30)
gray = cv.cvtColor(blurred, cv.COLOR_BGR2GRAY)
ret, binary = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV | cv.THRESH_OTSU)
cv.imshow('binary', binary)
kernel = cv.getStructuringElement(cv.MORPH_RECT, (3, 3))
open_binary = cv.morphologyEx(binary, cv.MORPH_OPEN, kernel, iterations=2) # 2次开操作,去除噪声
cv.imshow('open_binary', open_binary)
dilate_op = cv.dilate(open_binary, kernel, iterations=3) # 3次膨胀。
# 获得掩码需要知道图像的前景和背景,在这里我的理解是:前景就是有硬币的图像,
# 背景就是除硬币之前的区域。我们要确保我们获得的背景图中不包含前景图的区域,
# 通过腐蚀操作,将硬币的区域放大,那么剩下的就一定是背景区域了,即可以得到一定是背景图的区域
cv.imshow('dilate_op', dilate_op)
dist = cv.distanceTransform(open_binary, cv.DIST_L2, 3) # 可用来实现目标细化、骨架提取、形状插值及匹配、粘连物体的分离等
# 距离变换是针对二值图像的一种变换。在二维空间中,
# 一幅二值图像可以认为仅仅包含目标和背景两种像素,目标的像素值为1,背景的像素值为0;
# 距离图像是图像中每个像素的灰度值为该像素与距其最近的背景像素间的距离
# (就是原二值图像中像素值为1的像素点与最近的像素点为0的像素点之间的距离)
# DIST_L1:曼哈顿距离,DIST_L2:欧氏距离,masksize:跟卷积一样
dist_norm = cv.normalize(dist, 0, 1.0, cv.NORM_MINMAX) # 归一化,其中第二三个参数分别是上下限,第四个参数是归一化选择的数学公式
cv.imshow('distance', dist_norm*50)
# 由于掩码是一幅二值图像,所以经过距离变化后还需要将图像进行二值化
# 这样子的结果图像中像素点为1的区域就一定是硬币的位置,即前景图
ret, surface_image = cv.threshold(dist, dist.max()*0.65, 255, cv.THRESH_BINARY)
cv.imshow('surface_image', surface_image)
# 现在我们已经知道原图像的前景图和背景图了,但是还有一些区域是我们所不知道的就是原图像中硬币与硬币之间相连的那些或者叠加的区域,即边界,通过背景图减去前景图可以大概的获得这些未知的边界
surface = np.uint8(surface_image)
fringe = cv.subtract(dilate_op, surface)
cv.imshow('fringe', fringe)
ret, markers = cv.connectedComponents(surface) # connectedComponents函数可以使图像中标记背景像素点为0,非背景像素点从1开始累加分别标记
print(ret)
#cv.imshow('markers', markers)
markers = markers+1 # 在所有的标签上加1,以确保背景不是0,而是1
markers[fringe == 255] = 0 # 将边缘区域标记为0,便于分水岭算法探测
markers = cv.watershed(image, markers) # 分水岭算法
image[markers == -1] = [0, 0, 255] # 经过分水岭分割算法后,边界处会标记为-1
cv.imshow('result', image)
src = cv.imread('water_coins.jpg')
cv.imshow("input_image", src)
water_shed(src)
cv.waitKey(0)
cv.destroyAllWindows()
25
# 过一遍流程
img = cv.imread('xy.png')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
thresh = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV | cv.THRESH_OTSU )[1]
kernel = np.ones((3,3), np.uint8)
thresh = cv.morphologyEx(thresh, cv.MORPH_OPEN,kernel, iterations = 2 )
# cv_show('Test', thresh)
sure_fg = cv.erode(thresh,kernel, iterations = 2 )
sure_bg = cv.dilate(thresh, kernel, iterations = 2)
unknowns = sure_bg - sure_fg
# cv_show('Test', unknowns)
marker = np.zeros(unknowns.shape, np.uint8)
# 此处会将前景标记为1,背景标记为0,
# connectedComponents 的第二个返回值才是我们所需
marker = cv.connectedComponents(sure_fg)[1]
# 偏移
marker = marker + 1
marker[unknowns == 255] = 0
# 分水岭
marker = cv.watershed(img, marker)
# 在原图像上作画
res = img.copy()
res[marker == -1] = [255,0,0]
compare([img, res])