一前言
今天看到我之前写的文章进了热榜前100,我是很欣喜的,感谢大家,今天更新早一点,希望大家有一个愉快的周末。这次是周末合刊,内容会多一点。
二主要内容
理论
任何灰度图像都可以看作是地形表面,其中高强度表示峰和丘陵,而低强度表示山谷(对于我一个学地理的理科生这是很熟悉的,可以理解为亮度越高海拔越高)。您开始用不同颜色的水(标签)填充每个孤立的山谷(局部最小值)。随着水的上升,取决于附近的峰值(梯度),来自不同山谷的水,明显具有不同的颜色将开始融合。为避免这种情况,您需要在水合并的位置建立障碍。你继续填补水和建筑障碍的工作,直到所有的山峰都在水下。然后,您创建的障碍为您提供分割结果。这是分水岭背后的"哲学"。
但是这种方法会因图像中的噪声或任何其他不规则性而给出过度调整结果。因此,OpenCV 实施了一个基于标记的分水岭算法,您可以在其中指定要合并的所有谷点,哪些不合并。这是一种交互式图像分割。我们所做的是为我们知道的对象提供不同的标签。用一种颜色(或强度)标记我们确定为前景或对象的区域,用另一种颜色标记我们确定为背景或非对象的区域,最后标记我们不确定任何内容的区域,用 0 标记它。这是我们的标记。然后应用分水岭算法。然后我们的标记将使用我们给出的标签进行更新,对象的边界将具有-1 的值。
代码
下面我们将看到一个如何使用距离变换和分水岭来分割相互接触的物体的示例。考虑下面的硬币图像,硬币互相接触。即使你达到阈值,它也会相互接触。

我们首先找到硬币的近似估计值。为此,我们可以使用 Otsu 的二值化。
python
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
# 读取图像文件
img = cv.imread(r'D:\python_code\pic\coins.jpg')
# 转换为灰度图像
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# Otsu自动阈值二值化(反色处理)
ret, thresh = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)

现在我们需要去除图像中的任何小白噪声。为此,我们可以使用形态开放。要移除对象中的任何小孔,我们可以使用形态学闭合。所以,现在我们确切地知道靠近物体中心的区域是前景,而远离物体的区域是背景。只有我们不确定的区域是硬币的边界区域。
所以我们需要提取我们确定它们是硬币的区域。侵蚀消除了边界像素。所以无论如何,我们可以肯定它是硬币。如果物体没有相互接触,这将起作用。但由于它们相互接触,另一个好的选择是找到距离变换并应用适当的阈值。接下来我们需要找到我们确定它们不是硬币的区域。为此,我们扩大了结果。膨胀将物体边界增加到背景。这样,我们可以确保结果中背景中的任何区域确实是背景,因为边界区域被移除。见下图。

剩下的区域是我们不知道的区域,无论是硬币还是背景。分水岭算法应该找到它。这些区域通常围绕前景和背景相遇的硬币边界(甚至两个不同的硬币相遇)。我们称之为边界。它可以从 sure_bg 区域中减去 sure_fg 区域获得。
sure_bg(确定背景区域)
是什么:
-
定义 :通过形态学膨胀操作确定的100%肯定是背景的区域
-
创建方法:对去噪后的二值图像进行多次膨胀操作
sure_bg = cv.dilate(opening, kernel, iterations=3)
为什么需要:
-
分水岭算法需要明确知道哪些区域是背景
-
多次膨胀确保背景区域足够大,完全包围所有前景物体
特点:
-
比实际背景区域更大
-
包含所有前景物体和它们之间的间隙
-
像素值:255(白色区域)
sure_fg(确定前景区域)
是什么:
-
定义 :通过距离变换确定的100%肯定是前景的区域(物体的中心部分)
-
创建方法:
1. 计算距离变换:每个像素到最近背景像素的距离
dist_transform = cv.distanceTransform(opening, cv2.DIST_L2, 5)
2. 阈值处理:取距离值较大的一定百分比作为确定前景
ret, sure_fg = cv.threshold(dist_transform, 0.7*dist_transform.max(), 255, 0)
距离变换原理:
-
白色像素(前景)离黑色背景越远,距离值越大
-
物体中心距离值最大,边缘距离值较小
特点:
-
比实际物体区域更小
-
只包含物体的中心部分,避免物体边缘的模糊区域
-
像素值:255(白色区域)
python
# noise removal
kernel = np.ones((3,3),np.uint8)
opening = cv.morphologyEx(thresh,cv.MORPH_OPEN,kernel, iterations = 2)
# sure background area
sure_bg = cv.dilate(opening,kernel,iterations=3)
# Finding sure foreground area
dist_transform = cv.distanceTransform(opening,cv.DIST_L2,5)
ret, sure_fg = cv.threshold(dist_transform,0.7*dist_transform.max(),255,0)
# Finding unknown region
sure_fg = np.uint8(sure_fg)
unknown = cv.subtract(sure_bg,sure_fg)
这个代码是接在上一个代码之后的,我这里分开是为了方便讲解,我在最后会放上完整可运行的代码。
看到结果。在阈值图像中,我们得到了一些我们确定硬币的硬币区域,现在它们已经分离。(在某些情况下,你可能只对前景分割感兴趣,而不是分离相互接触的物体。在这种情况下,你不需要使用距离变换,只需要侵蚀就足够了。侵蚀只是提取确定前景区域的另一种方法,那就是所有。)

现在我们确定哪个是硬币区域,哪个是背景和所有。所以我们创建标记(它是一个与原始图像大小相同的数组,但是使用 int32 数据类型)并标记其中的区域。我们确切知道的区域(无论是前景还是背景)都标有任何正整数,但不同的整数,我们不确定的区域只是保留为零。为此,我们使用cv.connectedComponents()。它用 0 标记图像的背景,然后其他对象用从 1 开始的整数标记
retval, labels = cv.connectedComponents(image, labels=None, connectivity=8, ltype=cv.CV_32S)
这个函数用于标记图像中的连通区域。它会找到二值图像中的所有连通组件,并为每个组件分配一个唯一的标签。
参数说明:
-
image:8位单通道二值图像
-
labels:输出的标记图像(可选的输出数组)
-
connectivity:连通性类型
-
4:4连通(上、下、左、右) -
8:8连通(包括对角线,默认)
-
-
ltype:输出标签类型
-
cv.CV_16U:16位无符号整数 -
cv.CV_32S:32位有符号整数(默认)
-
返回值:
-
retval:连通组件的数量(包括背景)
-
labels:标记图像,与原图大小相同,每个像素的值表示其所属组件的标签
python
import cv2
import numpy as np
# 假设 sure_fg 和 unknown 是预先定义的前景和未知区域二值图像
# sure_fg = ... (二值图像,前景为255,背景为0)
# unknown = ... (二值图像,未知区域为255,其他为0)
# 连通组件标记
ret, markers = cv2.connectedComponents(sure_fg)
# 所有标记加1,使背景标记变为1
markers = markers + 1
# 将未知区域标记为0
markers[unknown == 255] = 0
查看 JET 色彩映射中显示的结果。深蓝色区域显示未知区域。确定的硬币用不同的值着色。与未知区域相比,确定背景的剩余区域以浅蓝色显示。

现在我们的标记准备好了。现在是最后一步的时候,应用分水岭。然后将修改标记图像。边界区域将标记为-1。
完整代码:
python
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
# 读取输入图像
img = cv.imread(r'D:\python_code\pic\coins.jpg')
# 转换为灰度图并进行阈值处理
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)
# 使用形态学开运算去除噪声
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)
# 标记连通区域
ret, markers = cv.connectedComponents(sure_fg)
# 调整标记使背景不为0
markers = markers + 1
markers[unknown == 255] = 0
# 应用分水岭算法
markers = cv.watershed(img, markers)
# 标记边界为红色
img[markers == -1] = [255, 0, 0]
# 显示结果
cv.imshow('Watershed Result', img)
cv.waitKey(0)
cv.destroyAllWindows()
效果如下:

我们可以看到效果还是很不错的,所以这个算法还是很有用的,在计算机视觉中:
-
目标检测与分割 - 复杂背景中的目标提取
-
指纹识别 - 指纹脊线分离
-
人脸分析 - 面部特征点定位
-
运动目标分割 - 视频序列中的运动对象分离
-
总之,分水岭算法特别适合于需要精确边界、对象有接触或重叠、且可以接受一定计算时间的应用场景。
三最后一语
这周的合刊就结束了,希望大家能够理解。
我要有能做我自己的自由,和敢做我自己的胆量。
------林语堂《我的愿望》
感谢观看,共勉!!