opencv28:分水岭算法的图像分割

目标

在本章中,将学习

  • 使用分水岭算法实现基于标记的图像分割
  • 函数:cv2.watershed()

理论

任何灰度图像都可以看作是一个地形表面,其中高强度的像素表示山峰,低强度表示山谷。可以用不同颜色的水(标签)填充每个孤立的山谷(局部最小值)。随着水位的上升,根据附近的山峰(坡度),来自不同山谷的水明显会开始合并,颜色也不同。为了避免这种情况,要在水融合的地方建造屏障。继续填满水,建造障碍,直到所有的山峰都在水下。然后创建的屏障将返回分割结果。这就是Watershed(分水岭算法)背后的“思想”。

但是这种方法会由于图像中的噪声或其他不规则性而产生过度分割的结果。因此OpenCV实现了一个基于标记的分水岭算法,可以指定哪些是要合并的山谷点,哪些不是。这是一个交互式的图像分割。所做的是给我们知道的对象赋予不同的标签用一种颜色(或强度)标记我们确定为前景或对象的区域,用另一种颜色标记我们确定为背景或非对象的区域,最后用0标记我们不确定的区域。 这是我们的标记。然后应用分水岭算法。然后标记将使用我们给出的标签进行更新,对象的边界值将为-1。

代码

下面将看到一个有关如何使用距离变换和分水岭来分割相互接触的对象的示例。

考虑下面的硬币图像,硬币彼此接触。即使设置阈值,它们也会彼此接触。
opencv28:分水岭算法的图像分割_第1张图片

先从寻找硬币的近似估计开始。因此,可以使用Otsu的二值化。

import cv2
import numpy
from matplotlib import pyplot as plt

img = cv2.imread('coins.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)  # ret是阈值,thresh是结果
cv2.imshow('coins', thresh)
cv2.waitKey(0)
cv2.destroyAllWindows()

opencv28:分水岭算法的图像分割_第2张图片

现在需要去除图像中的白点噪声,可以使用形态学膨胀。要去除对象中的任何小孔,可以使用形态学腐蚀。因此,现在可以确定,靠近对象中心的区域是前景,而离对象中心很远的区域是背景。不确定的唯一区域是硬币的边界区域。

因此,需要提取可确定为硬币的区域。腐蚀会去除边界像素。因此,无论剩余多少,都可以肯定它是硬币。如果物体彼此不接触,那将起作用。但是,由于它们彼此接触,因此另一个好选择是找到距离变换并应用适当的阈值。接下来,需要找到我们确定它们不是硬币的区域。为此,对其进行了膨胀,膨胀将对象边界增加到背景。这样,由于边界区域已删除,因此可以确保结果中背景中的任何区域实际上都是背景。
opencv28:分水岭算法的图像分割_第3张图片
剩下的区域是不确定的区域,无论是硬币还是背景。分水岭算法应该找到它。这些区域通常位于前景和背景相遇(甚至两个不同的硬币相遇)的硬币边界附近,我们称之为边界。可以通过从sure_bg区域中减去sure_fg区域来获得。

import cv2
import numpy as np
from matplotlib import pyplot as plt

# noise removal
kernel = np.ones((3, 3), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)

# sure background area
sure_bg = cv2.dilate(opening, kernel, iterations=3)

# finding sure foreground area
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
ret, sure_fg = cv2.threshold(dist_transform, 0.7*dist_transform.max(), 255, 0)

# finding unknow region
sure_fg = np.uint8(sure_fg)
unknow = cv2.subtract(sure_bg, sure_fg)

plt.subplot(121)
plt.imshow(dist_transform, cmap='gray')
plt.title('distance transform')
plt.xticks([])
plt.yticks([])

plt.subplot(122)
plt.imshow(thresh, cmap='gray')
plt.title('threshold')
plt.xticks([])
plt.yticks([])

plt.show()

查看结果。在阈值图像中,得到了一些硬币区域,确定它们是硬币,并且现在已分离它们。(在某些情况下,可能只对前景分割感兴趣,而不对分离相互接触的对象感兴趣。在那种情况下,无需使用距离变换,只需侵蚀就足够了。侵蚀只是提取确定前景区域的另一种方法。)
opencv28:分水岭算法的图像分割_第4张图片

现在可以确定哪些是硬币的区域,哪些是背景。因此,我们创建了标记(它的大小与原始图像的大小相同,但具有int32数据类型),并标记其中的区域。肯定知道的区域(无论是前景还是背景)都标有任何正整数,但是带有不同的整数,而不确定的区域则保留为零。为此,使用cv2.connectedComponents()。它用0标记图像的背景,然后其他对象用从1开始的整数标记。

但是,如果背景标记为0,则分水岭会将其视为未知区域。所以我们想用不同的整数来标记它。相反,将未知定义的未知区域标记为0。

# Marker labelling
ret, markers = cv2.connectedComponents(sure_fg)
# Add one to all labels so that sure background is not 0, but 1
markers = markers + 1
# Now, mark the region of unknown with zero
markers[unknow==255] = 0

plt.imshow(markers)
plt.xticks([])
plt.yticks([])
plt.show

参见JET colormap中显示的结果。深蓝色区域显示未知区域。当然,硬币的颜色不同。剩下,肯定为背景的区域显示在较浅的蓝色,跟未知区域相比。

opencv28:分水岭算法的图像分割_第5张图片

现在标记已准备就绪。到了最后一步的时候了,使用分水岭算法。然后标记图像将被修改,边界区域将标记为-1。

void watershed( InputArray image, InputOutputArray markers );
第一个参数 image,必须是一个8bit 3通道彩色图像矩阵序列,第一个参数没什么要说的。关键是第二个参数 markers,Opencv官方文档的说明如下:
Before passing the image to the function, you have to roughly outline the desired regions in the image markers with positive (>0) indices. So, every region is represented as one or more connected components with the pixel values 1, 2, 3, and so on. Such markers can be retrieved from a binary mask using findContours() and drawContours(). The markers are “seeds” of the future image regions. All the other pixels in markers , whose relation to the outlined regions is not known and should be defined by the algorithm, should be set to 0’s. In the function output, each pixel in markers is set to a value of the “seed” components or to -1 at boundaries between the regions.
在执行分水岭函数watershed之前,必须对第二个参数markers进行处理,它应该包含不同区域的轮廓,每个轮廓有一个自己唯一的编号,轮廓的定位可以通过Opencv中findContours方法实现,这个是执行分水岭之前的要求。
接下来执行分水岭会发生什么呢?算法会根据markers传入的轮廓作为种子(也就是所谓的注水点),对图像上其他的像素点根据分水岭算法规则进行判断,并对每个像素点的区域归属进行划定,直到处理完图像上所有像素点。而区域与区域之间的分界处的值被置为“-1”,以做区分。
简单概括一下就是说第二个入参markers必须包含了种子点信息。Opencv官方例程中使用鼠标划线标记,其实就是在定义种子,只不过需要手动操作,而使用findContours可以自动标记种子点。而分水岭方法完成之后并不会直接生成分割后的图像,还需要进一步的显示处理,如此看来,只有两个参数的watershed其实并不简单。

markers = cv2.watershed(img, markers) 
img[markers == -1] = [255,0,0]
plt.subplot(121)
plt.imshow(markers)
plt.title('marker image after segmentation')
plt.xticks([])
plt.yticks([])

plt.subplot(122)
plt.imshow(img)
plt.title('result')
plt.xticks([])
plt.yticks([])
plt.show()

可以从结果中看到,对某些硬币,它们接触的区域被正确地分割,而对于某些硬币,却没有被正确地分割。

opencv28:分水岭算法的图像分割_第6张图片

import cv2
import numpy

img = cv2.imread("coins.jpg")
cv2.imshow("img", img)

# 1.图像二值化
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)

# 2.噪声去除
kernel = numpy.ones((3, 3), dtype=numpy.uint8)
open = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)

# 3.确定背景区域
sure_bg = cv2.dilate(open, kernel, iterations=3)

# 4.寻找前景区域
dist_transform = cv2.distanceTransform(open, 1, 5)
ret, sure_fg = cv2.threshold(dist_transform, 0.5 * dist_transform.max(), 255, cv2.THRESH_BINARY)

# 5.找到未知区域
sure_fg = numpy.uint8(sure_fg)
unknow = cv2.subtract(sure_bg, sure_fg)

# 6.类别标记
ret, markers = cv2.connectedComponents(sure_fg)
# 为所有的标记加1,保证背景是0而不是1
markers = markers + 1
# 现在让所有的未知区域为0
markers[unknow == 255] = 0

# 7.分水岭算法
markers = cv2.watershed(img, markers)
img[markers == -1] = (0, 0, 255)

cv2.imshow("gray", gray)
cv2.imshow("thresh", thresh)
cv2.imshow("open", open)
cv2.imshow("sure_bg", sure_bg)
cv2.imshow("sure_fg", sure_fg)
cv2.imshow("unknow", unknow)
cv2.imshow("img_watershed", img)
cv2.waitKey(0)
cv2.destroyWindow()

opencv28:分水岭算法的图像分割_第7张图片

附加资源

  • https://docs.opencv.org/4.1.2/d3/db4/tutorial_py_watershed.html
  • CMM page on Watershed Transformation
  • https://zhuanlan.zhihu.com/p/67741538
  • https://blog.csdn.net/dcrmg/article/details/52498440

你可能感兴趣的:(#,OpenCv,opencv,算法,图像分割,分水岭算法)