OpenCV学习笔记-分水岭算法
一、分水岭算法
分水岭算法是一种图像区域分割法,在分割的过程中,它会把跟临近像素间的相似性作为重要的参考依据,从而将在空间位置上相近并且灰度值相近的像素点互相连接起来构成一个封闭的轮廓,封闭性是分水岭算法的一个重要特征。
其他图像分割方法,如阈值,边缘检测等都不会考虑像素在空间关系上的相似性和封闭性这一概念,彼此像素间互相独立,没有统一性。分水岭算法较其他分割方法更具有思想性,更符合人眼对图像的印象。
任意的灰度图像可以被看做是地质学表面,高亮度的地方是山峰,低亮度的地方是山谷。给每个孤立的山谷(局部最小值)不同颜色的水(标签),当水涨起来,根据周围的山峰(梯度),不同的山谷也就是不同的颜色会开始合并,要避免这个,你可以在水要合并的地方建立障碍,直到所有山峰都被淹没。你所创建的障碍就是分割结果,这个就是分水岭的原理,但是这个方法会分割过度,因为有噪点,或者其他图像上的错误。所以OpenCV实现了一个基于掩模的分水岭算法,你可以指定哪些是要合并的点,哪些不是,这是一个交互式的图像分割,我们要做的是给不同的标签。给我们知道是前景或者是目标用一种颜色加上标签,给我们知道是背景或者非目标加上另一个颜色,最后不知道是什么的区域标记为0. 然后使用分水岭算法。
二、代码
我们就距离变换和分水岭算法对紧挨在一起的对象进行分割。
1、我们找到硬币的近似估计
gray = cv.cvtColor(blurred, cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(gray, 0, 255, cv.THRESH_BINARY | cv.THRESH_OTSU)
cv.imshow('thresh', thresh)
我们发现效果并不好,所以再加上一个边缘保留滤波
#去噪声
blurred = cv.pyrMeanShiftFiltering(img, 10, 100)
这时效果好了很多,但是还是有噪声。现在我们用形态学开运算去除图像中的所有白噪声,用闭运算去除对象上的小空洞。所以现在我们知道了靠近对象中心的区域是前景,远离对象中心的区域是背景,不能确定的区域就是硬币间的边界。
所以我们需要提取确定是硬币的区域。腐蚀可以去掉边缘像素,剩下的就是硬币了。当硬币之间没有接触时,这种方法有效,但是由于硬币之间有接触,所以我们换一个更好的选择:距离变换再加上合适的阈值,就能找到硬币区域。
接下来是找到确定不是硬币的区域。我们队结果进行膨胀操作,膨胀可以将对象的边界延伸到背景中。这样由于边界区域被处理了,所以我们可以肯定哪些区域是前景,哪些区域是背景。
kernel = cv.getStructuringElement(cv.MORPH_RECT, (3, 3)) opening = cv.morphologyEx(thresh, cv.MORPH_OPEN, kernel, iterations=2) cv.imshow('opening ', opening) sure_bg = cv.dilate(opening, kernel, iterations=3) cv.imshow('mor-opt', sure_bg)
剩下的区域我们不知道该如何区分了,这就是分水岭算法要做的。这些区域通常是前景与背景的交界处,我们称之为边界。从sure-bg里面减去surface-fg得到。
距离变换的基本含义是计算一个图像中非零像素点到最近的零像素点的距离,也就是到零像素点的最短距离。一个最常见的距离变换算法就是通过连续的腐蚀操作来实现,腐蚀操作的停止条件是所有前景像素都被完全腐蚀,这样根据腐蚀的先后顺序,我们就得到各个前景像素点到前景中心骨架像素点的距离。根据各个像素点的距离值,设置为不同的灰度值。这样就完成了二值图像的距离变换。
#距离变换 dist = cv.distanceTransform(opening, cv.DIST_L2, 3) dist_output = cv.normalize(dist, 0, 1.0, cv.NORM_MINMAX) cv.imshow('distance-t', dist_output * 50) ret, surface = cv.threshold(dist, dist.max()*0.6, 255, cv.THRESH_BINARY) cv.imshow('surface', surface) #Finding unknown region surface_fg = np.uint8(surface) cv.imshow('surface_bin', surface_fg) unknown = cv.subtract(sure_bg,surface_fg)
现在我们知道哪些是背景哪些是硬币了。那我们创建标签(和原图像大小相等,数据类型是int32的数组),并标记其中的区域。我们对已经确定分类的区域用不同的正整数标记,对我们不确定的区域使用0标记。我们用connectedComponents()来做这个,它把背景标记为0,其他目标是从1开始的整数标记。
但是,如果背景被标记为0,分水岭算法会认为它是未知区域,所以我们要用不同的整数标记,不确定的区域用0标记。
# Marker labelling ret, markers = cv.connectedComponents(surface_fg) # Add one to all labels so that sure background is not 0, but 1 markers = markers + 1 # now,make thre region of unknown with zero markers[unknown==255] = 0
现在我们的标记准备好,使用分水岭算法cv.watershed(),标记图像会被修改,边界区域会被标记成-1.
markers = cv.watershed(img, markers) img[markers==-1] = [255, 0, 0]
结果:
完整代码:
import cv2 as cv import numpy as np from matplotlib import pyplot as plt def watershed_demo(img): print(img.shape) #去噪声 blurred = cv.pyrMeanShiftFiltering(img, 10, 100) # gray\binary image gray = cv.cvtColor(blurred, cv.COLOR_BGR2GRAY) ret, thresh = cv.threshold(gray, 0, 255, cv.THRESH_BINARY | cv.THRESH_OTSU) cv.imshow('thresh', thresh) # 有很多的黑点,所以我们去黑点噪声 kernel = cv.getStructuringElement(cv.MORPH_RECT, (3, 3)) opening = cv.morphologyEx(thresh, cv.MORPH_OPEN, kernel, iterations=2) cv.imshow('opening ', opening) sure_bg = cv.dilate(opening, kernel, iterations=3) cv.imshow('mor-opt', sure_bg) #距离变换 dist = cv.distanceTransform(opening, cv.DIST_L2, 3) dist_output = cv.normalize(dist, 0, 1.0, cv.NORM_MINMAX) cv.imshow('distance-t', dist_output * 50) ret, surface = cv.threshold(dist, dist.max()*0.6, 255, cv.THRESH_BINARY) cv.imshow('surface', surface) #Finding unknown region surface_fg = np.uint8(surface) cv.imshow('surface_bin', surface_fg) unknown = cv.subtract(sure_bg,surface_fg) # Marker labelling ret, markers = cv.connectedComponents(surface_fg) # Add one to all labels so that sure background is not 0, but 1 markers = markers + 1 # now,make thre region of unknown with zero markers[unknown==255] = 0 markers = cv.watershed(img, markers) img[markers==-1] = [255, 0, 0] cv.imshow('result', img) img = cv.imread('img/target.jpg') cv.namedWindow('img',cv.WINDOW_AUTOSIZE) cv.imshow('img',img) watershed_demo(img) cv.waitKey(0) cv.destroyAllWindows()