1、算法原理说明
传统分水岭算法:
任何灰阶图像都可以视为地形表面。图像中每一像素的灰度值表示该点的海拔高度,每一个局部极小值及其影响区域称为集水盆地,而集水盆地的边界则形成分水岭。
我们用在局部最小值的地方,打一个小孔,然后把整个地形浸入到水中。那么当水没过高峰时,不同盆地的水,必然会产生融合。为了防止这种情况,我们在水没过高峰时,就在两盆地的边缘线处竖起一座大坝。然后继续此操作,知道最后填满水。所创建的大坝就会形成分水岭。
但是这种做法,由于图像中存在很多的噪声或局部不规则性,会产生很多小的集水盆地,会造成图像的过度分割,无法真正标注出图像有意义的部分。所以需要合并一些图像分割后的相似区域。
这几张图可以很好的解释它的过程:
基于标记的分水岭算法:
在传统的分水岭基础上,opencv实现了基于标记的分水岭算法cv::wathershed(Marker-controlled watershed)
它的原理是:对图像中部分像素做标记,表明它的所属区域是已知的。分水岭算法可以根据这个初始标签确定其他像素所属的区域。
图片来源:IMAGE SEGMENTATION AND MATHEMATICAL MORPHOLOGY
http://www.cmm.mines-paristech.fr/~beucher/wtshed.html
可以根据梯度(一般梯度图像在边缘处有较高的像素值,而在其它地方则有较低的像素值,理想情况下,分山岭恰好在边缘。),也可以根据距离变换互相结合。
2、效果图
3.结果分析
本次算法步骤:
1、加载原始图像
2、阈值分割,将图像分割为黑白两个部分
3、对图像进行开运算,即先腐蚀再膨胀
4、对开运算的结果再进行膨胀,得到大部分是背景的区域
5、通过距离变换 Distance Transform 获取前景区域
6、背景区域sure_bg 和前景区域sure_fg相减,得到即有前景又有背景的重合区域
7、连通区域处理
8、最后使用分水岭算法
这次实验代码使用距离变换的效果在对一些几何体的分割效果还是可以的。
但是像对于人脸的lena图片,关于人脸处细节的分割很差;
效果更好一点:第二次考虑根基梯度。
4.源码
<1>
import cv2
import numpy as np
# 读入图像,并灰度化
img = cv2.imread('test8.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 阈值分割,将图像分为黑白两部分,阈值0,255,第四个参数THRESH_OTSU,它对一幅双峰图像自动根据其直方图计算出合适的阈值
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) #thresh返回图片,ret返回True或False,代表有没有读到图片
# cv2.imshow("thresh", thresh)
# 去除噪声,对图像进行形态学的开运算(先进行腐蚀操作,再进行膨胀操作),使用闭运算可以去除对象中的空洞。
kernel = np.ones((3, 3), np.uint8) #返回一个3*3 的全1数组
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2) #2 顺序为腐蚀-腐蚀-膨胀-膨胀
# cv2.imshow("opening", opening)
# 背景的区域
sure_bg = cv2.dilate(opening, kernel, iterations=3)
#cv2.imshow("sure_bg", sure_bg)
# 距离变换,前景的区域
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5) # DIST_L2 可以为3或者5
ret, sure_fg = cv2.threshold(dist_transform, 0.1 * dist_transform.max(), 255, 0) #0.1时的效果好于其他
#cv2.imshow("sure_fg", sure_fg)
# sure_bg与sure_fg相减,得到既有前景又有背景的重合区域
sure_fg = np.uint8(sure_fg)
unknow = cv2.subtract(sure_bg, sure_fg)
# 连通域处理
ret, markers = cv2.connectedComponents(sure_fg,connectivity=8) #对连通区域进行标号,序号为 0 - N-1
markers = markers + 1 #OpenCV 分水岭算法对物体做的标注必须都 大于1 ,背景为标号 为0,因此对所有markers 加1 变成了 1 - N
#去掉属于背景区域的部分(即让其变为0,成为背景)
markers[unknow==255] = 0
# 分水岭算法
markers = cv2.watershed(img, markers) #分水岭算法后,所有轮廓的像素点被标注为 -1
print(markers)
img[markers == -1] = [255, 0, 0] # 将标注为-1 的像素点标成蓝色
cv2.imshow("result", img)
cv2.imwrite('result8.jpg', img)
cv2.waitKey(0)
<2>
import matplotlib.pyplot as plt
from scipy import ndimage as ndi
#from skimage import morphology,color,data,filter
from skimage import morphology,filters
import cv2
#读图像,并且灰度化
img =cv2.imread('test8.jpg')
image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
denoised = filters.rank.median(image, morphology.disk(2)) #过滤噪声(中值滤波)
#将梯度值低于10的作为开始标记点
markers = filters.rank.gradient(denoised, morphology.disk(5)) <10 # 返回图像的局部梯度(即局部最大值 - 局部最小值)
markers = ndi.label(markers)[0]
gradient = filters.rank.gradient(denoised, morphology.disk(2)) #计算梯度
#基于梯度的分水岭算法
labels =morphology.watershed(gradient, markers, mask=image)
#figsize值:设置子图的宽度和高度;2行2个的子图
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(6, 6))
axes = axes.ravel()
ax0, ax1, ax2, ax3 = axes
ax0.imshow(image, cmap=plt.cm.gray, interpolation='nearest') # interpolation一种插值运算,cmap表示绘图时的样式
ax0.set_title("Original")
ax1.imshow(gradient, cmap=plt.cm.Spectral, interpolation='nearest')
ax1.set_title("Gradient")
ax2.imshow(markers, cmap=plt.cm.Spectral, interpolation='nearest')
ax2.set_title("Markers")
ax3.imshow(labels, cmap=plt.cm.Spectral, interpolation='nearest')
ax3.set_title("Segmented")
for ax in axes:
ax.axis('off') # 关闭坐标轴
fig.tight_layout() # tight_layout会自动调整子图参数,使之填充整个图像区域。
plt.show()