目录
[1. 基本概念](#1. 基本概念)
[2. 算法思想](#2. 算法思想)
[3. 应用场景](#3. 应用场景)
[1. 图像的梯度](#1. 图像的梯度)
[2. 水域和分水岭的形成](#2. 水域和分水岭的形成)
[3. 算法步骤](#3. 算法步骤)
[2. 参数说明](#2. 参数说明)
[3. 关键步骤详解](#3. 关键步骤详解)
[1. 处理噪声和过分割](#1. 处理噪声和过分割)
[2. 手动标记的分水岭算法](#2. 手动标记的分水岭算法)
[3. 基于标记的交互式分水岭](#3. 基于标记的交互式分水岭)
[2. 细胞分割](#2. 细胞分割)
[3. 图像分割与目标提取](#3. 图像分割与目标提取)
[1. 图像预处理的重要性](#1. 图像预处理的重要性)
[2. 标记的选择](#2. 标记的选择)
[3. 处理过分割问题](#3. 处理过分割问题)
[4. 性能优化](#4. 性能优化)
[5. 选择合适的距离变换](#5. 选择合适的距离变换)
一、分水岭算法概述
分水岭算法(Watershed Algorithm)是一种基于区域的图像分割方法,由Vincent和Soille于1991年提出。它将图像视为地形地貌,通过模拟水从高海拔区域流向低海拔区域的过程来分割图像。
1. 基本概念
像素的海拔:通常使用图像的灰度值或梯度值表示
局部极小值:图像中灰度值最小的像素点,对应地形中的"盆地"
水域:从局部极小值开始,水逐渐淹没周围区域形成的区域
分水岭线:不同水域之间的边界线,对应地形中的"山脊"
2. 算法思想
分水岭算法的核心思想是:
将图像的梯度视为地形高度图
从每个局部极小值开始,逐渐提高水位
当不同水域的水相遇时,在它们之间构建"大坝"
最终,这些"大坝"形成的边界就是图像的分割结果
3. 应用场景
图像分割与目标提取
医学图像处理(如细胞分割、肿瘤检测)
工业检测(如缺陷检测、产品分类)
计算机视觉(如目标识别、图像分析)
二、分水岭算法的数学原理
1. 图像的梯度
分水岭算法通常使用图像的梯度作为地形高度图,因为梯度值大的地方对应图像中的边缘。
在OpenCV中,可以使用Sobel算子计算图像的梯度:
//python
python
import cv2
import numpy as np
from matplotlib import pyplot as plt
#读取图像
img = cv2.imread('coins.jpg')
#转换为灰度图像
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
#计算梯度
grad_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
grad_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
grad = cv2.magnitude(grad_x, grad_y)
#显示结果
plt.figure(figsize=(12, 6))
plt.subplot(121), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)), plt.title('Original Image')
plt.subplot(122), plt.imshow(grad, cmap='gray'), plt.title('Gradient Magnitude')
plt.tight_layout()
plt.show()
2. 水域和分水岭的形成
假设我们从图像的局部极小值开始,逐渐提高水位:
初始阶段:水只填充局部极小值区域(盆地)
扩展阶段:随着水位上升,水逐渐淹没周围区域,形成水域
合并阶段:当不同水域的水相遇时,在它们之间构建大坝
完成阶段:所有可能的合并都被大坝阻止,最终的大坝就是图像的分割边界
3. 算法步骤
经典的分水岭算法步骤如下:
计算梯度图像:使用Sobel、Laplacian等算子计算图像的梯度
确定局部极小值:找到梯度图像中的局部极小值点
初始化标记:为每个局部极小值分配唯一的标记
迭代洪水填充:从每个局部极小值开始,逐渐扩大水域
构建分水岭线:当不同水域相遇时,在它们之间标记分水岭线
三、OpenCV中的分水岭实现
在OpenCV中,分水岭算法通过cv2.watershed()函数实现。该函数需要一个标记图像作为输入,标记图像中包含了我们对分割的先验知识。
- 基本用法
//python
python
import cv2
import numpy as np
from matplotlib import pyplot as plt
#读取图像
img = cv2.imread('coins.jpg')
#转换为灰度图像
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
#高斯模糊
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
#阈值分割
ret, thresh = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
#形态学操作
kernel = np.ones((3, 3), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)
#确定背景区域
sure_bg = cv2.dilate(opening, kernel, iterations=3)
#距离变换
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
#确定前景区域
ret, sure_fg = cv2.threshold(dist_transform, 0.7dist_transform.max(), 255, 0)
#找到未知区域
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg, sure_fg)
#标记连通区域
ret, markers = cv2.connectedComponents(sure_fg)
#为所有标记加1,确保背景是1而不是0
markers = markers + 1
#标记未知区域为0
markers[unknown == 255] = 0
#应用分水岭算法
markers = cv2.watershed(img, markers)
#将分水岭线标记为红色
img[markers == 1] = [255, 0, 0]
#显示结果
plt.figure(figsize=(15, 10))
plt.subplot(231), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)), plt.title('Watershed Result')
plt.subplot(232), plt.imshow(thresh, cmap='gray'), plt.title('Threshold')
plt.subplot(233), plt.imshow(sure_bg, cmap='gray'), plt.title('Sure Background')
plt.subplot(234), plt.imshow(dist_transform, cmap='gray'), plt.title('Distance Transform')
plt.subplot(235), plt.imshow(sure_fg, cmap='gray'), plt.title('Sure Foreground')
plt.subplot(236), plt.imshow(unknown, cmap='gray'), plt.title('Unknown Region')
plt.tight_layout()
plt.show()
2. 参数说明
//python
markers = cv2.watershed(image, markers)
参数说明:
image:输入图像(必须是彩色图像)
markers:标记图像,必须是32位整数类型
标记为0的像素:未知区域
标记为正整数的像素:已知区域(不同的整数表示不同的区域)
返回值:
markers:修改后的标记图像
标记为1的像素:分水岭线
标记为1的像素:背景区域
标记为2及以上的像素:前景区域(不同的整数表示不同的前景对象)
3. 关键步骤详解
(1)图像预处理
//python
#高斯模糊
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
#阈值分割
ret, thresh = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
#形态学操作
kernel = np.ones((3, 3), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)
图像预处理的目的是去除噪声,突出目标区域。通常使用高斯模糊、阈值分割和形态学操作。
(2)确定背景和前景
//python
#确定背景区域
sure_bg = cv2.dilate(opening, kernel, iterations=3)
#距离变换
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
#确定前景区域
ret, sure_fg = cv2.threshold(dist_transform, 0.7dist_transform.max(), 255, 0)
背景区域:通过对二值图像进行膨胀操作获得
前景区域:通过距离变换和阈值分割获得,距离变换计算每个像素到最近背景像素的距离
(3)找到未知区域
//python
找到未知区域
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg, sure_fg)
未知区域是背景和前景之间的过渡区域,这些区域将由分水岭算法自动分割。
(4)标记连通区域
//python
#标记连通区域
ret, markers = cv2.connectedComponents(sure_fg)
#为所有标记加1,确保背景是1而不是0
markers = markers + 1
#标记未知区域为0
markers[unknown == 255] = 0
使用cv2.connectedComponents()函数标记前景区域中的连通分量
为所有标记加1,确保背景是1而不是0
将未知区域标记为0
(5)应用分水岭算法
//python
应用分水岭算法
markers = cv2.watershed(img, markers)
将分水岭线标记为红色
img[markers == 1] = [255, 0, 0]
cv2.watershed()函数根据标记图像分割图像
分割结果中,分水岭线被标记为1,通常将其显示为红色
四、分水岭算法的优化
1. 处理噪声和过分割
经典的分水岭算法容易受到噪声影响,导致过分割问题。可以通过以下方法优化:
(1)图像预处理
使用高斯模糊(cv2.GaussianBlur())减少噪声
使用形态学操作(如开运算、闭运算)去除小的噪点
(2)标记控制
手动标记前景和背景区域
使用距离变换和阈值分割自动确定前景区域
合并相似的区域
2. 手动标记的分水岭算法
在某些情况下,我们可以手动标记前景和背景区域,提高分割的准确性。
//python
python
import cv2
import numpy as np
from matplotlib import pyplot as plt
#读取图像
img = cv2.imread('cell.jpg')
#转换为灰度图像
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
#高斯模糊
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
#阈值分割
ret, thresh = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
#形态学操作
kernel = np.ones((3, 3), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)
#创建标记图像
markers = np.zeros((gray.shape[0], gray.shape[1]), dtype=np.int32)
#手动标记背景区域(可选)
markers[0:50, 0:50] = 1
#自动标记前景区域
#距离变换
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
ret, sure_fg = cv2.threshold(dist_transform, 0.5dist_transform.max(), 255, 0)
sure_fg = np.uint8(sure_fg)
#标记前景区域
ret, fg_markers = cv2.connectedComponents(sure_fg)
markers[sure_fg > 0] = fg_markers[sure_fg > 0] + 1
#应用分水岭算法
markers = cv2.watershed(img, markers)
#将分水岭线标记为红色
img[markers == 1] = [255, 0, 0]
#显示结果
plt.figure(figsize=(12, 6))
plt.subplot(121), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)), plt.title('Watershed Result')
plt.subplot(122), plt.imshow(markers, cmap='tab10'), plt.title('Markers')
plt.tight_layout()
plt.show()
3. 基于标记的交互式分水岭
在实际应用中,我们可以使用鼠标交互来标记前景和背景区域,提高分割的准确性。
//python
python
import cv2
import numpy as np
#鼠标回调函数
markers = None
current_marker = 1
def draw_circle(event, x, y, flags, param):
global markers, current_marker
if event == cv2.EVENT_LBUTTONDOWN:
#标记前景区域
cv2.circle(img, (x, y), 10, (255, 0, 0), 1)
cv2.circle(markers, (x, y), 10, current_marker, 1)
elif event == cv2.EVENT_RBUTTONDOWN:
#标记背景区域
cv2.circle(img, (x, y), 10, (0, 0, 255), 1)
cv2.circle(markers, (x, y), 10, 1, 1)
#读取图像
img = cv2.imread('cell.jpg')
original = img.copy()
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
#初始化标记图像
markers = np.zeros((gray.shape[0], gray.shape[1]), dtype=np.int32)
#创建窗口
cv2.namedWindow('Image')
cv2.setMouseCallback('Image', draw_circle)
while True:
cv2.imshow('Image', img)
k = cv2.waitKey(1) & 0xFF
if k == 27: ESC键退出
break
elif k == ord('c'): #'c'键清除标记
img = original.copy()
markers = np.zeros((gray.shape[0], gray.shape[1]), dtype=np.int32)
elif k == ord('s'): #'s'键应用分水岭算法
#克隆标记图像
temp_markers = markers.copy()
#将背景标记为1
temp_markers[temp_markers == 1] = 1
#将前景标记为2
temp_markers[temp_markers > 1] = 2
#应用分水岭算法
temp_markers = cv2.watershed(original, temp_markers)
#显示结果
result = original.copy()
result[temp_markers == 1] = [255, 0, 0]
cv2.imshow('Watershed Result', result)
cv2.waitKey(0)
cv2.destroyAllWindows()
五、实际应用案例
- 硬币分割
//python
python
import cv2
import numpy as np
from matplotlib import pyplot as plt
#读取图像
img = cv2.imread('coins.jpg')
#转换为灰度图像
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
#高斯模糊
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
#阈值分割
ret, thresh = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
#形态学操作
kernel = np.ones((3, 3), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)
#确定背景区域
sure_bg = cv2.dilate(opening, kernel, iterations=3)
#距离变换
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
#确定前景区域
ret, sure_fg = cv2.threshold(dist_transform, 0.7dist_transform.max(), 255, 0)
#找到未知区域
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg, sure_fg)
#标记连通区域
ret, markers = cv2.connectedComponents(sure_fg)
#为所有标记加1,确保背景是1而不是0
markers = markers + 1
#标记未知区域为0
markers[unknown == 255] = 0
#应用分水岭算法
markers = cv2.watershed(img, markers)
#为每个硬币分配不同的颜色
colors = np.random.randint(0, 255, size=(markers.max()+1, 3), dtype=np.uint8)
colors[0] = [0, 0, 0] #背景为黑色
colors[1] = [255, 255, 255] #背景为白色
colors[1] = [255, 0, 0] #分水岭线为红色
segmented = colors[markers]
#显示结果
plt.figure(figsize=(15, 10))
plt.subplot(231), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)), plt.title('Original Image')
plt.subplot(232), plt.imshow(thresh, cmap='gray'), plt.title('Threshold')
plt.subplot(233), plt.imshow(sure_bg, cmap='gray'), plt.title('Sure Background')
plt.subplot(234), plt.imshow(dist_transform, cmap='gray'), plt.title('Distance Transform')
plt.subplot(235), plt.imshow(markers, cmap='tab10'), plt.title('Markers')
plt.subplot(236), plt.imshow(cv2.cvtColor(segmented, cv2.COLOR_BGR2RGB)), plt.title('Segmented Result')
plt.tight_layout()
plt.show()
#统计硬币数量
print(f"检测到的硬币数量: {np.max(markers) 1}")
2. 细胞分割
//python
python
import cv2
import numpy as np
from matplotlib import pyplot as plt
#读取图像
img = cv2.imread('cells.jpg')
#转换为灰度图像
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
#高斯模糊
blurred = cv2.GaussianBlur(gray, (7, 7), 0)
#阈值分割
ret, thresh = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
#形态学操作
kernel = np.ones((5, 5), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)
#确定背景区域
sure_bg = cv2.dilate(opening, kernel, iterations=3)
#距离变换
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
#确定前景区域
ret, sure_fg = cv2.threshold(dist_transform, 0.5dist_transform.max(), 255, 0)
#找到未知区域
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg, sure_fg)
#标记连通区域
ret, markers = cv2.connectedComponents(sure_fg)
#为所有标记加1,确保背景是1而不是0
markers = markers + 1
#标记未知区域为0
markers[unknown == 255] = 0
#应用分水岭算法
markers = cv2.watershed(img, markers)
#将分水岭线标记为红色
img[markers == 1] = [255, 0, 0]
#为每个细胞分配不同的颜色
colors = np.random.randint(0, 255, size=(markers.max()+1, 3), dtype=np.uint8)
colors[0] = [0, 0, 0] #背景为黑色
colors[1] = [255, 255, 255] #背景为白色
colors[1] = [255, 0, 0] #分水岭线为红色
segmented = colors[markers]
#显示结果
plt.figure(figsize=(15, 10))
plt.subplot(231), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)), plt.title('Original Image')
plt.subplot(232), plt.imshow(thresh, cmap='gray'), plt.title('Threshold')
plt.subplot(233), plt.imshow(dist_transform, cmap='gray'), plt.title('Distance Transform')
plt.subplot(234), plt.imshow(markers, cmap='tab10'), plt.title('Markers')
plt.subplot(235), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)), plt.title('Watershed Result')
plt.subplot(236), plt.imshow(cv2.cvtColor(segmented, cv2.COLOR_BGR2RGB)), plt.title('Segmented Result')
plt.tight_layout()
plt.show()
#统计细胞数量
print(f"检测到的细胞数量: {np.max(markers) 1}")
3. 图像分割与目标提取
//python
python
import cv2
import numpy as np
from matplotlib import pyplot as plt
#读取图像
img = cv2.imread('objects.jpg')
#转换为灰度图像
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
#高斯模糊
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
#Canny边缘检测
edges = cv2.Canny(blurred, 50, 150)
#形态学操作
kernel = np.ones((3, 3), np.uint8)
dilated = cv2.dilate(edges, kernel, iterations=2)
#确定背景区域
sure_bg = cv2.dilate(dilated, kernel, iterations=3)
#距离变换
dist_transform = cv2.distanceTransform(dilated, cv2.DIST_L2, 5)
#确定前景区域
ret, sure_fg = cv2.threshold(dist_transform, 0.6dist_transform.max(), 255, 0)
#找到未知区域
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg, sure_fg)
#标记连通区域
ret, markers = cv2.connectedComponents(sure_fg)
#为所有标记加1,确保背景是1而不是0
markers = markers + 1
#标记未知区域为0
markers[unknown == 255] = 0
#应用分水岭算法
markers = cv2.watershed(img, markers)
#创建结果图像
result = img.copy()
#为每个区域分配不同的颜色
colors = np.random.randint(0, 255, size=(markers.max()+1, 3), dtype=np.uint8)
colors[0] = [0, 0, 0] #背景为黑色
colors[1] = [255, 255, 255] #背景为白色
colors[1] = [255, 0, 0] #分水岭线为红色
segmented = colors[markers]
#显示结果
plt.figure(figsize=(15, 10))
plt.subplot(231), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)), plt.title('Original Image')
plt.subplot(232), plt.imshow(edges, cmap='gray'), plt.title('Canny Edges')
plt.subplot(233), plt.imshow(dilated, cmap='gray'), plt.title('Dilated Edges')
plt.subplot(234), plt.imshow(dist_transform, cmap='gray'), plt.title('Distance Transform')
plt.subplot(235), plt.imshow(markers, cmap='tab10'), plt.title('Markers')
plt.subplot(236), plt.imshow(cv2.cvtColor(segmented, cv2.COLOR_BGR2RGB)), plt.title('Segmented Result')
plt.tight_layout()
plt.show()
#提取每个目标
for i in range(2, np.max(markers) + 1):
#创建掩码
mask = np.zeros_like(gray, dtype=np.uint8)
mask[markers == i] = 255
#提取目标
object_img = cv2.bitwise_and(img, img, mask=mask)
#显示目标
plt.figure(figsize=(6, 6))
plt.imshow(cv2.cvtColor(object_img, cv2.COLOR_BGR2RGB)), plt.title(f'Object {i1}')
plt.tight_layout()
plt.show()
六、注意事项与技巧
1. 图像预处理的重要性
噪声处理:使用高斯模糊(cv2.GaussianBlur())或中值滤波(cv2.medianBlur())去除噪声
对比度增强:使用直方图均衡化(cv2.equalizeHist())增强图像对比度
边缘增强:使用形态学操作(如膨胀、腐蚀)增强边缘
2. 标记的选择
背景标记:确保背景标记覆盖整个背景区域
前景标记:确保每个前景对象至少有一个标记
未知区域:未知区域应该包含图像中的所有边缘和不确定区域
3. 处理过分割问题
过分割是分水岭算法的常见问题,解决方法包括:
增加图像预处理步骤,去除噪声
减少形态学操作的迭代次数
调整距离变换的阈值
手动合并相似的区域
4. 性能优化
对小图像应用分水岭算法,再将结果缩放回原尺寸
使用ROI(感兴趣区域)只处理图像的部分区域
减少形态学操作的迭代次数
5. 选择合适的距离变换
OpenCV提供了三种距离变换方法:
cv2.DIST_L1:曼哈顿距离
cv2.DIST_L2:欧几里得距离
cv2.DIST_C:棋盘距离
通常使用cv2.DIST_L2(欧几里得距离)获得最佳效果。
七、总结
分水岭算法是一种强大的图像分割工具,通过模拟水从高海拔区域流向低海拔区域的过程来分割图像。它在医学图像处理、工业检测和计算机视觉等领域有广泛的应用。
主要内容回顾
算法原理:基于地形地貌的模拟,通过洪水填充和大坝构建来分割图像
数学推导:使用图像的梯度作为地形高度图,通过寻找局部极小值和构建分水岭线来分割图像
OpenCV实现:使用cv2.watershed()函数,需要先创建标记图像
关键步骤:图像预处理、确定背景和前景、找到未知区域、标记连通区域、应用分水岭算法
优化方法:噪声处理、标记控制、手动标记和交互式标记
实际应用:硬币分割、细胞分割和图像分割
注意事项:图像预处理、标记选择、过分割处理和性能优化
使用建议
对于简单图像,使用自动标记方法即可获得良好的分割结果
对于复杂图像,结合手动标记或交互式标记提高分割准确性
总是先进行图像预处理,去除噪声和增强边缘
根据实际情况调整参数,如阈值、形态学操作的迭代次数等
通过合理使用分水岭算法,可以在各种应用场景中获得准确的图像分割结果。它是图像处理和计算机视觉领域中不可或缺的工具之一。