OpenCV-Python——第22章:分水岭算法实现图像分割

版权声明:版权声明:本文为博主原创文章,未经博主允许不得转载。如有错误,欢迎指出,我会及时修改 https://blog.csdn.net/yukinoai/article/details/88575861

目录

0 原理    1 举例说    1)二值化    2)去除图像中的所有的白噪声   3)提取肯定是硬币的区域   4)获得边界区域

5)标记区域   6)实施分水岭算法


0 原理

在地理学中,分水岭是一个山脊,该山脊通过不同的水系来区分排水区域。集水盆地是把水排入河流或水库的地理区域。分水岭变换把这些概念应用到灰度图像处理中,从而解决许多图像分割问题。

理解分水岭变换要求我们把灰度图像视为一个拓扑表面,表面中f(x, y)的值被解释为高度。例如, 我们可以把下图(a)中的简单图像形象化为下图(b)中的三维表面。如果雨水降落到该表面上,那么雨水明显会流人两个集水盆地中。正好降落到分水岭脊线上的雨水会等概率地流到两个集水盆地中。 分水岭变换将找到灰度图像中的集水盆地和脊线。在解决图像分割问题方面,关键概念是把起始图像 变为另一幅图像,在变换后的图像中,集水盆地就是我们要识别的物体或区域。

OpenCV 采用了基于掩模的分水岭 算法,在这种算法中我们要设置那些山谷点会汇合,那些不会。这是一种交互 式的图像分割。我们要做的就是给我们已知的对象打上不同的标签。如果某个 区域肯定是前景或对象,就使用某个颜色(或灰度值)标签标记它。如果某个 区域肯定不是对象而是背景就使用另外一个颜色标签标记。而剩下的不能确定 是前景还是背景的区域就用 0 标记。这就是我们的标签。然后实施分水岭算法。 每一次灌水,我们的标签就会被更新,当两个不同颜色的标签相遇时就构建堤 坝,直到将所有山峰淹没,最后我们得到的边界对象(堤坝)的值为 -1。

1 举例说明

首先看下面这个例子,再具体解释里面的含义:

 1)二值化

我们从找到硬币的近似估计开始。我们可以使用 Otsu's 二值化。

import numpy as np
import cv2
from matplotlib import pyplot as plt

src = cv2.imread('test27.jpg')
img = src.copy()
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(
    gray, 0, 255, cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)

2)去除图像中的所有的白噪声

这就需要使用形态学中的开运算。为了去除对象上小的空洞我们需要使用形态学闭运算。所以我们现在知道靠近 对象中心的区域肯定是前景,而远离对象中心的区域肯定是背景。而不能确定的区域就是硬币之间的边界。

kernel = np.ones((3, 3), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)

开运算可参考:https://blog.csdn.net/yukinoai/article/details/86762342

3)提取肯定是硬币的区域

a.  当硬币之间没有接触时

腐蚀操作可以去除边缘像素。剩下就可以肯定是硬币了。

b. 硬币之间是相互接触

距离变换再加上合适的阈值。接下来我们要找到肯定不是硬币的区域。这是就需要进行膨胀操作了。 膨胀可以将对象的边界延伸到背景中去。这样由于边界区域被去处理,我们就可以知道那些区域肯定是前景,那些肯定是背景。

# 膨胀
sure_bg = cv2.dilate(opening, kernel, iterations=3)

# 距离变换
dist_transform = cv2.distanceTransform(opening, 1, 5)
ret, sure_fg = cv2.threshold(dist_transform, 0.7*dist_transform.max(), 255, 0)

 膨胀操作可参考:https://blog.csdn.net/yukinoai/article/details/86762342

        

距离变换函数:

cv2.distanceTransform(src, distanceType, maskSize)

  • src:输入图像
  • distanceType:计算距离的方式,自带7种

                        DIST_L1      = 1,   //!< distance = |x1-x2| + |y1-y2|

                        DIST_L2      = 2,   //!< the simple euclidean distance

                        DIST_C       = 3,   //!< distance = max(|x1-x2|,|y1-y2|)

                        DIST_L12     = 4,   //!< L1-L2 metric: distance = 2(sqrt(1+x*x/2) - 1))

                        DIST_FAIR    = 5,   //!< distance = c^2(|x|/c-log(1+|x|/c)), c = 1.3998

                        DIST_WELSCH  = 6,   //!< distance = c^2/2(1-exp(-(x/c)^2)), c = 2.9846

                        DIST_HUBER   = 7    //!< distance = |x|<c ? x^2/2 : c(|x|-c/2), c=1.345

  • maskSize:蒙板尺寸,3种

                        DIST_MASK_3 = 3, //!< mask=3

                        DIST_MASK_5 = 5, //!< mask=5

                        DIST_MASK_PRECISE = 0 //!< mask=0

4)获得边界区域

剩下的区域就是我们不知道该如何区分的了。这就是分水岭算法要做的。这些区域通常是前景与背景的交界处(或者两个前景的交界)。我们称之为边界。

sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg, sure_fg)

5)标记区域

现在知道了那些是背景那些是硬币了。那我们就可以创建标签(一个与原图像大小相同,数据类型为 in32 的数组),并标记其中的区域了。对我们已经 确定分类的区域(无论是前景还是背景)使用不同的正整数标记,对我们不确 定的区域使用 0 标记。我们可以使用函数 cv2.connectedComponents() 来做这件事。它会把将背景标记为 0,其他的对象使用从 1 开始的正整数标记。 但是,我们知道如果背景标记为 0,那分水岭算法就会把它当成未知区域了。所以我们想使用不同的整数标记它们。而对不确定的区域(函数 cv2.connectedComponents 输出的结果中使用 unknown 定义未知区域)标记为 0。

# 标记
ret, markers1 = cv2.connectedComponents(sure_fg)

# 确保背景是1不是0
markers = markers1 + 1

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

结果使用 JET 颜色地图表示。深蓝色区域为未知区域。肯定是硬币的区域 使用不同的颜色标记。其余区域就是用浅蓝色标记的背景了。 

其中connectedComponents()函数:

cv2.connectedComponents(image, labels, connectivity, ltype)

  • image:输入8位单通道图像
  • labels:输出标签地图
  • connectivity:连通性,默认8,还可以取4
  • ltype:输出标签类型 ,默认 CV_32S, 还可以取CV_16U

6)实施分水岭算法

标签图像将会被修改,边界区域的标记将变为 -1

markers3 = cv2.watershed(img, markers)
img[markers3 == -1] = [0, 0, 255]

其中分水岭算法函数:

cv2.watershed(img, markers)

  • img:输入图像
  • markers:标记

最后所有程序如下:

import numpy as np
import cv2
from matplotlib import pyplot as plt

src = cv2.imread('test27.jpg')
img = src.copy()
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, 1, 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, markers1 = cv2.connectedComponents(sure_fg)

# 确保背景是1不是0
markers = markers1 + 1

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

markers3 = cv2.watershed(img, markers)
img[markers3 == -1] = [0, 0, 255]

plt.subplot(241), plt.imshow(cv2.cvtColor(src, cv2.COLOR_BGR2RGB)),
plt.title('Original'), plt.axis('off')
plt.subplot(242), plt.imshow(thresh, cmap='gray'),
plt.title('Threshold'), plt.axis('off')
plt.subplot(243), plt.imshow(sure_bg, cmap='gray'),
plt.title('Dilate'), plt.axis('off')
plt.subplot(244), plt.imshow(dist_transform, cmap='gray'),
plt.title('Dist Transform'), plt.axis('off')
plt.subplot(245), plt.imshow(sure_fg, cmap='gray'),
plt.title('Threshold'), plt.axis('off')
plt.subplot(246), plt.imshow(unknown, cmap='gray'),
plt.title('Unknow'), plt.axis('off')
plt.subplot(247), plt.imshow(np.abs(markers), cmap='jet'),
plt.title('Markers'), plt.axis('off')
plt.subplot(248), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)),
plt.title('Result'), plt.axis('off')

plt.show()

结果如下:

猜你喜欢

转载自blog.csdn.net/yukinoai/article/details/88575861