开启成长之旅!这是我参与「日新计划 2 月更文应战」的第 30 天,点击查看活动详情

方针

在本章中,将学习

  • 运用分水岭算法实现依据符号的图画切割
  • 函数:cv2.watershed()

理论

任何灰度图画都能够看作是一个地势外表,其间高强度的像素表示山峰,低强度表示山沟。能够用不同色彩的水(标签)填充每个孤立的山沟(部分最小值)。随着水位的上升,依据邻近的山峰(斜度),来自不同山沟的水明显会开端兼并,色彩也不同。为了避免这种状况,要在水融合的当地建造屏障。继续填满水,建造妨碍,直到一切的山峰都在水下。然后创建的屏障将回来切割成果。这便是Watershed(分水岭算法)背后的“思想”。

可是这种办法会因为图画中的噪声或其他不规矩性而发生过度切割的成果。因而OpenCV实现了一个依据符号的分水岭算法,能够指定哪些是要兼并的山沟点,哪些不是。这是一个交互式的图画切割。所做的是给咱们知道的目标赋予不同的标签用一种色彩(或强度)符号咱们确认为远景或目标的区域,用另一种色彩符号咱们确认为布景或非目标的区域,最终用0符号咱们不确认的区域。 这是咱们的符号。然后运用分水岭算法。然后符号将运用咱们给出的标签进行更新,目标的鸿沟值将为-1。

代码

下面将看到一个有关如何运用间隔改换和分水岭来切割相互触摸的目标的示例。

考虑下面的硬币图画,硬币互相触摸。即使设置阈值,它们也会互相触摸。

OpenCV 28: 分水岭算法的图像分割

先从寻找硬币的近似估计开端。因而,能够运用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()

OpenCV 28: 分水岭算法的图像分割

现在需求去除图画中的白点噪声,能够运用形态学胀大。要去除目标中的任何小孔,能够运用形态学腐蚀。因而,现在能够确认,接近目标中心的区域是远景,而离目标中心很远的区域是布景。不确认的仅有区域是硬币的鸿沟区域。

因而,需求提取可确认为硬币的区域。腐蚀会去除鸿沟像素。因而,无论剩余多少,都能够肯定它是硬币。假如物体互相不触摸,那将起作用。可是,因为它们互相触摸,因而另一个好选择是找到间隔改换并运用恰当的阈值。接下来,需求找到咱们确认它们不是硬币的区域。为此,对其进行了胀大,胀大将目标鸿沟增加到布景。这样,因为鸿沟区域已删除,因而能够保证成果中布景中的任何区域实际上都是布景。

OpenCV 28: 分水岭算法的图像分割
剩下的区域是不确认的区域,无论是硬币仍是布景。分水岭算法应该找到它。这些区域一般位于远景和布景相遇(甚至两个不同的硬币相遇)的硬币鸿沟邻近,咱们称之为鸿沟。能够通过从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()

查看成果。在阈值图画中,得到了一些硬币区域,确认它们是硬币,而且现在已分离它们。(在某些状况下,或许只对远景切割感兴趣,而不对分离相互触摸的目标感兴趣。在那种状况下,无需运用间隔改换,只需侵蚀就足够了。侵蚀只是提取确认远景区域的另一种办法。)

OpenCV 28: 分水岭算法的图像分割

现在能够确认哪些是硬币的区域,哪些是布景。因而,咱们创建了符号(它的巨细与原始图画的巨细相同,但具有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中显现的成果。深蓝色区域显现不知道区域。当然,硬币的色彩不同。剩下,肯定为布景的区域显现在较浅的蓝色,跟不知道区域相比。

OpenCV 28: 分水岭算法的图像分割

现在符号已准备就绪。到了最终一步的时候了,运用分水岭算法。然后符号图画将被修正,鸿沟区域将符号为-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()

能够从成果中看到,对某些硬币,它们触摸的区域被正确地切割,而对于某些硬币,却没有被正确地切割。

OpenCV 28: 分水岭算法的图像分割

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()

OpenCV 28: 分水岭算法的图像分割

附加资源

  • docs.opencv.org/4.1.2/d3/db…

  • CMM page on Watershed Transformation

  • zhuanlan.zhihu.com/p/67741538

  • blog.csdn.net/dcrmg/artic…