Python基于均值漂移算法和分水岭算法实现图像分割

Ruby
Ruby
发布时间:2026-10-16 | 4人浏览过
原创

本文详解Python中利用OpenCV实现图像分割的两种核心算法:均值漂移与分水岭。通过pyrMeanShiftFiltering进行色彩平滑滤波,结合分水岭算法处理粘连物体轮廓,提供完整代码示例与参数解析,帮助开发者有效分割图像前景与背景。

一、基于均值漂移算法的图像分割

均值漂移(Mean Shift)是一种通用的聚类分析方法,最初由Fukunaga等人于1975年在探讨概率密度梯度函数估计的论文中提出[1]。作为一种非参数估计算法,它通过沿概率密度梯度上升的方向来寻找分布的峰值。具体而言,Mean Shift算法首先计算当前点的偏移均值,将该点移动至其偏移均值的位置,然后以新位置为起点重复此过程,直至满足预设的终止条件。

在图像分割领域,可以利用均值漂移算法的特性来实现彩色图像的分割。OpenCV中提供的对应函数为pyrMeanShiftFiltering()。严格来说,该函数执行的是色彩层面的平滑滤波操作,而非直接的图像分割。它能够中和色彩分布相近的区域,平滑色彩细节,并侵蚀掉面积较小的颜色区域。因此,在OpenCV中该函数的后缀为“Filter”(滤波),而非“Segment”(分割)。该函数的原型定义如下:

dst = pyrMeanShiftFiltering(src, sp, sr[, dst[, maxLevel[, termcrit]]])

– src表示输入图像,需为8位三通道的彩色图像。

– dst表示输出图像,其大小和类型需与输入图像保持一致。

– sp表示定义漂移物理空间半径的大小。

– sr表示定义漂移色彩空间半径的大小。

– maxLevel表示定义金字塔的最大层数。

– termcrit表示定义的漂移迭代终止条件,可以设置为迭代次数满足终止,迭代目标与中心点偏差满足终止,或者两者的结合。

均值漂移函数pyrMeanShiftFiltering()的执行过程主要包含以下步骤:

  • 构建迭代空间。以输入图像上任一点P0为圆心,建立以sp为物理空间半径、sr为色彩空间半径的球形空间。物理空间坐标为x和y,色彩空间坐标为RGB或HSV,从而构成一个空间球体。其中x和y代表图像的长和宽,色彩空间R、G、B的值范围在0至255之间。
  • 求迭代空间的向量并移动迭代空间球体重新计算向量,直至收敛。在上一步构建的球形空间中,求出所有点相对于中心点的色彩向量之和,将迭代空间的中心点移动至该向量的终点,并再次计算该球形空间中所有点的向量之和。如此迭代,直到在最后一个空间球体中所求得向量和的终点即为该空间球体的中心点Pn,此时迭代结束。
  • 更新输出图像dst上对应的初始原点P0的色彩值为本轮迭代的终点Pn的色彩值,从而完成一个点的色彩均值漂移处理。
  • 对输入图像src上的其他点,依次执行上述三个步骤,直至遍历完所有像素点后,整个均值偏移色彩滤波过程完成。

下面的代码展示了图像均值漂移的具体实现过程:

# -*- coding: utf-8 -*-
# By: Eastmount
import cv2
import numpy as np
import matplotlib.pyplot as plt

#读取原始图像灰度颜色
img = cv2.imread('scenery.png') 

spatialRad = 50 #空间窗口大小
colorRad = 50 #色彩窗口大小
maxPyrLevel = 2 #金字塔层数

#图像均值漂移分割
dst = cv2.pyrMeanShiftFiltering( img, spatialRad, colorRad, maxPyrLevel)

#显示图像
cv2.imshow('src', img)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()

当漂移物理空间半径设置为50,漂移色彩空间半径设置为50,金字塔层数为2时,输出的效果图如图1所示。

当漂移物理空间半径设置为20,漂移色彩空间半径设置为20,金字塔层数为2时,输出的效果图如图2所示。对比可以发现,半径为20时,图像色彩细节大部分得以保留;而半径为50时,森林和水面的色彩细节基本已经丢失。

至此,均值偏移算法对彩色图像的分割平滑操作已完成。为了达到更好的分割效果,需借助漫水填充函数进行下一步处理。关于漫水填充函数的详细介绍将在下一篇文章中展开,此处仅引入该函数。完整代码如下所示:

# -*- coding: utf-8 -*-
# By: Eastmount
import cv2
import numpy as np
import matplotlib.pyplot as plt

#读取原始图像灰度颜色
img = cv2.imread('scenery.png') 

#获取图像行和列
rows, cols = img.shape[:2]

#mask必须行和列都加2且必须为uint8单通道阵列
mask = np.zeros([rows+2, cols+2], np.uint8) 

spatialRad = 100 #空间窗口大小
colorRad = 100   #色彩窗口大小
maxPyrLevel = 2  #金字塔层数

#图像均值漂移分割
dst = cv2.pyrMeanShiftFiltering( img, spatialRad, colorRad, maxPyrLevel)

#图像漫水填充处理
cv2.floodFill(dst, mask, (30, 30), (0, 255, 255),
              (100, 100, 100), (50, 50, 50),
              cv2.FLOODFILL_FIXED_RANGE)

#显示图像
cv2.imshow('src', img)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()

输出的效果图如图3所示,它将天空区域染成了黄色。

二、基于分水岭算法的图像分割

图像分水岭算法(Watershed Algorithm)是一种将图像的边缘轮廓转换为“山脉”,将均匀区域转换为“山谷”,从而提升分割效果的算法[3]。该算法基于拓扑理论的数学形态学,将灰度图像根据灰度值把像素之间的关系看作山峰和山谷的关系:高亮度(灰度值高)的地方被视为山峰,低亮度(灰度值低)的地方被视为山谷。接着,给每个孤立的山谷(局部最小值)注入不同颜色的水(Label)。当水位上涨时,根据周围的山峰(梯度),不同的山谷(即不同颜色的像素点)开始合并。为了避免这种合并现象,可以在水即将合并的地方建立障碍,直到所有山峰都被淹没。所创建的障碍即为分割结果,这就是分水岭算法的基本原理[3]。

分水岭算法的计算过程是一个迭代标注过程,主要包括排序和淹没两个步骤。由于图像中可能存在噪声或缺失等问题,该方法容易造成过度分割。OpenCV提供了watershed()函数来实现图像分水岭算法,并且能够指定需要合并的点,其函数原型如下所示:

markers = watershed(image, markers)

– image表示输入图像,需为8位三通道的彩色图像。

– markers表示用于存储函数调用之后的运算结果,输入/输出为32位单通道图像的标记结构,输出结果需和输入图像的尺寸和类型一致。

下面是分水岭算法实现图像分割的具体过程。假设存在一幅彩色硬币图像,如图4所示,硬币相互之间紧密挨着。

第一步,通过图像灰度化和阈值化处理提取图像灰度轮廓,采用OTSU二值化处理获取硬币的轮廓。

# -*- coding: utf-8 -*-
# By: Eastmount
import numpy as np
import cv2
from matplotlib import pyplot as plt

#读取原始图像
img = cv2.imread('coin.jpg')

#图像灰度化处理
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

#图像阈值化处理
ret, thresh = cv2.threshold(gray, 0, 255, 
cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)

#显示图像
cv2.imshow('src', img)
cv2.imshow('res', thresh)
cv2.waitKey()
cv2.destroyAllWindows()

输出结果如图5所示。

第二步,通过形态学开运算过滤掉小的白色噪声。同时,由于图像中的硬币是紧挨着的,所以不能采用图像腐蚀去掉边缘的像素,而是选择距离转换,配合一个适当的阈值进行物体提取。这里引入一个图像膨胀操作,将目标边缘扩展到背景,以确定结果的背景区域。

# -*- coding: utf-8 -*-
# By: Eastmount
import numpy as np
import cv2
from matplotlib import pyplot as plt

#读取原始图像
img = cv2.imread('coin.jpg')

#图像灰度化处理
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

#图像阈值化处理
ret, thresh = cv2.threshold(gray, 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.7*dist_transform.max(), 255, 0)

#寻找未知区域
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg, sure_fg)

#用来正常显示中文标签
plt.rcParams['font.sans-serif']=['SimHei']

#显示图像
titles = ['原始图像', '阈值化', '开运算',
          '背景区域', '前景区域', '未知区域']  
images = [img, thresh, opening, sure_bg, sure_fg, unknown]  
for i in range(6):  
   plt.subplot(2,3,i+1), plt.imshow(images[i], 'gray')  
   plt.title(titles[i])  
   plt.xticks([]),plt.yticks([])  
plt.show()

输出结果如图6所示,包括原始图像、阈值化处理、开运算、背景区域、前景区域、未知区域等。由图可知,在使用阈值过滤的图像里,确认了图像的硬币区域。而在有些情况下,如果对前景分割更感兴趣,而不关心目标是否需要分开或挨着,那时可以采用腐蚀操作来求解前景区域。

第三步,当前处理结果中,已经能够区分出前景硬币区域和背景区域。接着我们创建标记变量,在该变量中标记区域,已确认的区域(前景或背景)用不同的正整数标记出来,不确认的区域保持0。使用cv2.connectedComponents()函数来将图像背景标记成0,其他目标用从1开始的整数标记。注意,如果背景被标记成0,分水岭算法会认为它是未知区域,所以要用不同的整数来标记。

最后,调用watershed()函数实现分水岭图像分割,标记图像会被修改,边界区域会被标记成0,完整代码如下所示。

# -*- coding: utf-8 -*-
# By: Eastmount
import numpy as np
import cv2
from matplotlib import pyplot as plt

#读取原始图像
img = cv2.imread('coin.jpg')

#图像灰度化处理
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

#图像阈值化处理
ret, thresh = cv2.threshold(gray, 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.7*dist_transform.max(), 255, 0)

#寻找未知区域
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg, sure_fg)

#标记变量
ret, markers = cv2.connectedComponents(sure_fg)

#所有标签加一,以确保背景不是0而是1
markers = markers+1

#用0标记未知区域
markers[unknown==255]=0

#分水岭算法实现图像分割
markers = cv2.watershed(img, markers)
img[markers == -1] = [255,0,0]

#用来正常显示中文标签
plt.rcParams['font.sans-serif']=['SimHei']

#显示图像
titles = ['标记区域', '图像分割']  
images = [markers, img]  
for i in range(2):  
   plt.subplot(1,2,i+1), plt.imshow(images[i], 'gray')  
   plt.title(titles[i])  
   plt.xticks([]),plt.yticks([])  
plt.show()

最终分水岭算法的图像分割如图7所示,它将硬币的轮廓成功提取。

图8是采用分水岭算法提取图像Windows中心轮廓的效果图。

分水岭算法对微弱边缘具有良好的响应,图像中的噪声、物体表面细微的灰度变化,都会产生过度分割的现象。但同时应当看出,分水岭算法对微弱边缘具有良好的响应,是得到封闭连续边缘的保证。另外,分水岭算法所得到的封闭的集水盆,为分析图像的区域特征提供了可能。

三、总结

本文主要讲解了图像分割方法,包括基于均值漂移算法的图像分割方法、基于分水岭算法的图像分割方法,通过这些处理能有效分割图像的背景和前景,识别某些图像的区域。

以上就是Python基于均值漂移算法和分水岭算法实现图像分割的详细内容,更多关于Python图像分割的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表