图像分割作为计算机视觉的基石领域,历经数十年的演进与革新,从传统的图像处理方法到如今蓬勃发展的深度学习技术,始终推动着计算机视觉应用的边界拓展。本系列文章将通过三篇深度技术博客,分别对三种极具代表性的图像分割技术展开全面剖析 —— 基于 OpenCV 的经典分水岭算法、基于 PyTorch 框架实现的 UNet 深度学习模型,以及当下最先进(SOTA)的图像分割模型。在本篇文章中,我们将率先聚焦于分水岭算法,深入探讨其原理、OpenCV 实现细节,以及优化技巧。后续文章中,我们还将在人体分割数据集上训练 UNet 模型,直观展现深度学习在图像分割任务中的强大性能与广泛适用性。
什么是图像分割?
图像分割,简单来说,就是将一幅图像划分为多个具有独特特征的区域或片段的过程。每个区域内的像素集合具有相似的属性,如颜色、纹理、灰度值等。这项技术的核心目标,是将原始图像转化为更简洁、更具语义信息的表达形式,从而显著降低图像分析的复杂度,为后续诸如目标识别、场景理解、医学图像诊断、自动驾驶中的障碍物检测等任务奠定坚实基础。
在医学领域,图像分割能够精准提取 X 光、CT、MRI 等医学影像中的器官、肿瘤等感兴趣区域,助力医生进行疾病诊断与治疗方案制定;在智能安防领域,通过分割监控视频中的目标物体,实现异常行为检测与人员追踪;在工业质检中,对产品图像进行分割,可快速识别瑕疵与缺陷。可见,图像分割技术已深度融入众多行业,成为推动技术发展与应用落地的关键力量。
经典路线:使用 OpenCV 的分水岭算法
2.1 算法核心思想
分水岭算法源于地形学的启发,将灰度图像巧妙类比为三维地形。在这个地形模型中,图像的灰度值对应地形的高度 —— 灰度值高的区域如同巍峨的山峰与起伏的丘陵,灰度值低的区域则恰似深邃的山谷。算法的执行过程宛如一场 “洪水漫灌” 的模拟:从每个山谷的最低点开始注入 “洪水”,随着水位不断上升,来自不同山谷的洪水逐渐蔓延。当两股不同来源的洪水即将汇合时,就在它们的交界处筑起 “堤坝”。持续这一过程,直至整个地形都被 “淹没”,最终形成的这些 “堤坝”,便勾勒出了图像中不同对象的边界,实现图像分割。
然而,在实际应用中,由于图像不可避免地存在噪声、纹理细节以及复杂光照等干扰因素,直接应用上述简单的分水岭算法,往往会导致严重的过度分割问题 —— 原本完整的对象被错误地划分成大量细小的子区域,极大地影响分割效果。为解决这一难题,基于标记的分水岭算法应运而生。
2.2 基于标记的分水岭算法优化
基于标记的分水岭算法引入了 Marker 这一关键概念。标记可视为对图像中已知前景和背景区域的先验标注,通过人工交互、阈值处理、形态学操作或其他启发式方法生成。在算法执行前,将前景对象标记为不同的正整数(如 1、2、3...,每个数字对应一个独立对象),背景区域标记为 0,而尚未确定归属的区域则暂不标记。
在后续的 “洪水漫灌” 过程中,洪水只会从已标记的区域开始蔓延,并且只有来自不同标记区域的洪水相遇时,才会构建 “堤坝”。如此一来,有效避免了因噪声等因素导致的过度分割,显著提升了算法的准确性与实用性。
分水岭算法和OpenCV
-
阈值处理:在分水岭算法中,阈值处理在识别图像的某些部分方面起着重要作用。将图像转换为灰度后,该算法对灰度图像应用阈值处理,以获得二值图像,该二值图像有助于分离前景(待分割的对象)和背景。
# 加载图像
img = cv2.imread( 'water_coins.jpg' )
imshow( "Original image" , img)
# 灰度
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 使用 OTSU 进行阈值处理
ret, thresh = cv2.threshold(gray, 0 , 255 , cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
imshow( "Thresholded" , thresh)
2.开运算(先腐蚀,后膨胀):在此步骤中,执行开运算,即先腐蚀运算,再膨胀运算。此步骤的主要目的是为了去除噪声。腐蚀运算可以去除图像中的小白噪声,但同时也会缩小物体的尺寸。之后进行膨胀运算,可以让我们保留物体的大小,同时去除噪声。
让我们了解一下侵蚀和扩张
- 腐蚀:此操作会腐蚀掉前景对象的边界。其工作原理是创建一个卷积核并将其传入图像。如果核下方区域中的任何像素为黑色,则核中间的像素将被设置为黑色。此操作可有效去除细小的白噪声。
- 膨胀:腐蚀之后进行膨胀,本质上与腐蚀相反。膨胀操作会将像素添加到图像中对象的边界。如果核下方区域中的任何像素为白色,则核中间的像素将被设置为白色。
# 噪声消除
kernel = np.ones(( 3 , 3 ), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN,kernel, iterations = 2 )
让我们分解一下:
- 创建核:
np.ones((3,3),np.uint8)
创建一个 3x3 矩阵,所有元素均为“1”。该矩阵将用作形态学运算的“结构元素”。它可以是各种形状(正方形、圆形等),但在本例中,我们使用的是正方形。 - 应用开运算:
cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations = 2)
应用开运算。'thresh' 是我们在阈值化后获得的二值图像,cv2.MORPH_OPEN
表示我们要进行开运算,'kernel' 是我们的结构元素,'iterations = 2' 表示我们要执行两次操作。
3. 膨胀用于背景识别:在此步骤中,膨胀操作用于识别图像的背景区域。上一步的结果(已去除噪声)将进行膨胀。膨胀后,物体(或前景)周围的很大一部分预计将成为背景区域(因为膨胀会扩大物体)。这个“确定的背景”区域有助于分水岭算法的后续步骤,我们的目标是识别不同的片段/物体。
# sure background area
sure_bg = cv2.dilate(opening, kernel, iterations=3)
4. 距离变换:分水岭算法涉及应用距离变换来识别可能成为前景的区域。此步骤的代码如下:
# 寻找确定的前景区域
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5 )
ret, sure_fg = cv2.threshold(dist_transform, 0.7 * dist_transform.max ( ), 255 , 0 )
在此步骤中,我们要做两件事:
- 应用距离变换:该
cv2.distanceTransform
函数使用欧氏距离计算每个二值图像像素到最近零像素的距离cv2.DIST_L2
。距离变换有助于我们识别可能位于前景的区域。该函数cv2.distanceTransform(opening, cv2.DIST_L2, 5)
计算此变换。 - 对距离变换进行阈值处理:计算距离变换后,我们对变换后的图像应用阈值处理,以获得确定的前景区域。
cv2.threshold(dist_transform, 0.7*dist_transform.max(), 255, 0)
函数调用应用了阈值处理。第二个参数0.7*dist_transform.max()
将阈值设置为距离变换找到的最大距离的 70%。距离变换值高于此阈值的像素将被设置为确定的前景。
5. 识别未知区域:我们识别未知区域,即既不是确定前景也不是确定背景的区域。首先,我们将确定前景(sure_fg
)转换为无符号的8位整数。然后,用确定背景(sure_bg
)减去确定前景,得到未知区域。未知区域是分水岭算法的关键,因为它表示不同对象之间或对象与背景之间的过渡区域。
# 查找未知区域
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg, sure_fg)
imshow( "SureFG" , sure_fg)
imshow( "SureBG" , sure_bg)
imshow( "unknown" , unknown)
1. 确定前景(Sure Foreground):硬币所占据的区域,更确切地说,由于使用了距离变换和后续的阈值处理,硬币的中心区域会被识别为确定前景。这些区域是算法明确认定属于硬币本身的部分,是分割中较为确定的前景部分。
2. 确定背景(Sure Background):围绕硬币的区域,以及硬币内部那些足够大、未被形态学操作去除的区域,会被标记为确定背景。从本质上讲,这些区域是不存在硬币的区域。算法通过形态学操作等手段,将这些明显不包含硬币的区域准确识别并标记为背景部分。
3. 未知区域(Unknown Region):这些区域既不属于确定前景,也不属于确定背景。它们主要位于硬币边缘附近,在这些区域,算法没有足够的把握将其归为前景(硬币)或背景(硬币周围的区域)。由于硬币边缘的模糊性、噪声干扰或图像特征的复杂性等因素,使得算法难以准确判断这些区域的归属,因此将其标记为未知区域。
6. 标记 sure_bg、sure_fg 和未知区域:这 需要创建一个标记并标记其中的区域。我们标记的区域包括确定的背景 ( sure_bg
)、确定的前景 ( sure_fg
) 和未知区域。以下是此步骤的代码片段:
# 标记标记
# 连通分量确定二值图像中斑点状区域的连通性。
ret, markers = cv2.connectedComponents(sure_fg)
# 为所有标签加 1,以确保背景不为 0,而是 1
markers = markers+ 1
# 现在,用零标记未知区域
markers[unknown== 255 ] = 0
另外,我们希望确定的背景与确定的前景的标签不同,我们将标记图像中的所有标签加 1。经过此操作后,确定的背景像素被标记为 1,而确定的前景像素的标签从 2 开始。
7.应用分水岭算法
下一步是将分水岭算法应用于标记(在前面步骤中找到的标记区域)
markers = cv2.watershed(img,markers)
img[markers == -1] = [255,0,0]
imshow("img", img)
该cv2.watershed()
函数修改标记图像(markers
)本身。图像中物体的边界用 -1 标记markers
。图像中不同的物体用不同的正整数标记。因此,不确定是背景还是前景的区域由分水岭算法确定——它们要么被分配到背景,要么被分配到某个物体,从而在物体和背景之间形成清晰的边界划分。
分水岭算法是如何工作的?
分水岭算法中的“Flooding”和“Dam Construction”概念本质上是一种描述算法如何工作的隐喻方式
- Flooding: “泛洪”过程是指基于图像梯度对每个标记区域(标记)进行扩展。在这种情况下,梯度代表地形高程,高强度像素值代表峰顶,低强度像素值代表谷底。泛洪从谷底(即强度值最低的区域)开始。泛洪过程的执行方式是,为图像中的每个像素分配一个标签。它获得的标签取决于哪个标记的“泛洪”首先到达它。如果一个像素与多个标记的距离相等,则它暂时仍属于未知区域。
- Dam Construction:随着洪水过程的持续,来自不同标记(代表图像中不同区域)的洪水最终会开始汇合。汇合后,便会形成一座“大坝”。从算法角度来看,这种筑坝相当于在标记图像中创建边界。这些边界会被赋予一个特殊标签(通常为 -1)。大坝建在不同标记洪水汇合的位置,这些位置通常是图像中强度快速变化的区域——标志着图像中不同区域之间的边界。
应用分水岭算法后,我们的标记图像(最初包含确定前景、确定背景和未知区域的标签)现在包含图像中每个不同对象的标签。我们有效地将图像分割成不同的对象(硬币)和背景。
结论
分水岭算法凭借直观的地形模拟思想,为图像分割领域提供了极具效率的解决方案。该算法通过模拟洪水漫灌与堤坝构建的过程,能够从复杂的图像场景中精准提取关键特征,在目标识别、医学影像分析等众多领域展现出独特的应用价值。借助 Python 生态中的 OpenCV 库,分水岭算法的实现流程得到了极大简化,开发者能够快速搭建起图像分割模型,将理论算法高效转化为实际应用。
尽管分水岭算法在概念和基础实现上优势显著,但其原生形态容易受图像噪声、纹理细节等因素影响,导致过度分割问题,即单一对象被错误划分成多个子区域。不过,通过针对性的图像预处理操作(如形态学滤波、阈值优化)以及参数精细化调整,这一缺陷能够得到有效克服。例如,结合距离变换与阈值分割确定前景区域,利用形态学操作明确背景范围,可显著提升算法的分割精度与稳定性,使其成为图像分析任务中的可靠利器。
值得强调的是,图像分割技术的选择本质上是对项目需求、数据特性与计算资源的综合考量。分水岭算法虽然强大,但在面对语义复杂、目标边界模糊的图像时,可能需要与深度学习算法或其他传统方法结合使用。未来,随着多技术融合趋势的深化,分水岭算法有望在更多复杂场景中焕发新的活力,持续为图像分割技术的发展贡献力量。