0. 前言

图像分割是指将图像分成若干互不重叠的子区域,使得同一个子区域内的特征具有一定相似性,不同子区域的特征呈现较为明显的差异。之前介绍了基于阈值的分割方法,比如Otsu法等;基于边缘检测的分割方法,比如Sobel算子、Canny算子等。下面介绍基于区域的分割方法基于图的分割方法

1. 基于区域的分割方法

基于区域的分割算法将具有相似特征的像素集合聚集构成一个区域,这个区域中的相邻像素之间具有相似的性质,主要包括区域生长算法、区域分裂合并算法和分水岭算法等。

1.1 区域生长算法

区域生长的基本思想是将具有相似性质的像素集合起来构成区域。具体先对每个需要分割的区域找一个种子像素作为生长起点,然后将种子像素和周围邻域中与种子像素有相同或相似性质的像素(根据某种事先确定的生长或相似准则来判定)合并到种子像素所在的区域中。将这些新像素当作新的种子继续上面的过程,直到没有满足条件的像素可被包括进来,这样一个区域就生长成了。

区域生长实现步骤如下:

  1. 对图像顺序扫描,找到第1个还没有归属的像素,设该像素为(x0, y0);
  2. 以(x0,y0)为中心,考虑(x0,y0)的4邻域或者8邻域像素(x,y)与种子像素的灰度值之差的绝对值小于某个阈值T,如果满足条件,将(x,y)与(x0,y0)合并(在同一区域内),同时将(x,y)压入堆栈;
  3. 从堆栈中取出一个像素,把它当作(x0, y0)返回到步骤2;
  4. 当堆栈为空时,返回到步骤1;
  5. 重复步骤1-4直到图像中的每个点都有归属时,生长结束。

下图为测试效果(左侧为原图灰度图像,右侧为区域生长图像):

在这里插入图片描述

需要注意的是:当选取的种子不同时,得到的区域生长图像也会不同。

1.2 区域分裂合并算法

区域生长是从某个或者某些像素点出发,最后得到整个区域,进而实现目标提取。而分裂合并可以看做是区域生长的逆过程:从整个图像出发,不断分裂得到各个子区域,然后再把前景区域合并,实现目标提取。分裂合并的假设是对于一幅图像,前景区域由一些相互连通的像素组成。因此,如果把一幅图像分裂到像素级,那么就可以判定该像素是否为前景像素。当所有像素点或者子区域完成判断以后,把前景区域或者像素合并就可得到前景目标。

假定一幅图像分为若干区域,按照有关区域的逻辑词P的性质,各个区域上所有的像素将是一致的。区域分裂合并的算法如下:

  1. 将整幅图像设置为初始区域;
  2. 选择一个区域R,若P(R)错误,则将该区域分为4个子区域;
  3. 考虑图像中任意两个或更多的邻接子区域R1,R2,…,Rn;
  4. 如果P(R1∪R2∪…∪Rn)正确,则将这n个区域合并为一个区域;
  5. 重复上述步骤,直到不能再进行区域分裂合并。

下图为测试效果:

在这里插入图片描述

1.3 分水岭算法

1.3.1 分水岭算法原理

图像的灰度空间很像地球表面的整个地理结构,每个像素的灰度值代表高度。其中的灰度值较大的像素连成的线可以看做山脊,也就是分水岭。其中的水就是用于二值化的gray threshold level,二值化阈值可以理解为水平面,比水平面低的区域会被淹没,刚开始用水填充每个孤立的山谷(局部最小值)。当水平面上升到一定高度时,水就会溢出当前山谷,可以通过在分水岭上修大坝,从而避免两个山谷的水汇集,这样图像就被分成2个像素集,一个是被水淹没的山谷像素集,一个是分水岭线像素集。最终这些大坝形成的线就对整个图像进行了分区,实现对图像的分割。

在该算法中,空间上相邻并且灰度值相近的像素被划分为一个区域。

分水岭算法的整个过程如下:

  1. 把梯度图像中的所有像素按照灰度值进行分类,并设定一个测地距离阈值;
  2. 找到灰度值最小的像素点(默认标记为灰度值最低点),让threshold从最小值开始增长,这些点为起始点;
  3. 水平面在增长的过程中,会碰到周围的邻域像素,测量这些像素到起始点(灰度值最低点)的测地距离,如果小于设定阈值,则将这些像素淹没,否则在这些像素上设置大坝,这样就对这些邻域像素进行了分类;
  4. 随着水平面越来越高,会设置更多更高的大坝,直到灰度值的最大值,所有区域都在分水岭线上相遇,这些大坝就对整个图像像素进行了分区。

用上面的算法对图像进行分水岭运算,由于噪声点或其它因素的干扰,可能会得到密密麻麻的小区域,即图像被分得太细(over-segmented,过度分割),这因为图像中有非常多的局部极小值点,每个点都会自成一个小区域。

其中的解决方法有:

(1)对图像进行高斯平滑操作,抹除很多小的最小值,这些小分区就会合并。

(2)不从最小值开始增长,可以将相对较高的灰度值像素作为起始点(需要用户手动标记),从标记处开始进行淹没,则很多小区域都会被合并为一个区域,这被称为基于图像标记(mark)的分水岭算法

1.3.2 opencv-python中分水岭算法的应用

在OpenCV中,我们需要给不同区域贴上不同的标签。用大于1的整数表示我们确定为前景或对象的区域,用1表示我们确定为背景或非对象的区域,最后用0表示我们无法确定的区域。然后应用分水岭算法,我们的标记图像将被更新,更新后的标记图像的边界像素值为-1。

具体步骤如下:

  1. 对图进行灰度化和二值化得到二值图像;
  2. 通过膨胀得到确定的背景区域,通过距离转换得到确定的前景区域,剩余部分为不确定区域;
  3. 对确定的前景图像进行连接组件处理,得到标记图像;
  4. 根据标记图像对原图像应用分水岭算法,更新标记图像。

测试代码如下:

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

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

# noise removal
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)

sure_bg = cv2.dilate(opening, kernel, iterations=2)  # sure background area
sure_fg = cv2.erode(opening, kernel, iterations=2)  # sure foreground area
unknown = cv2.subtract(sure_bg, sure_fg)  # unknown area

# Perform the distance transform algorithm
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
# Normalize the distance image for range = {0.0, 1.0}
cv2.normalize(dist_transform, dist_transform, 0, 1.0, cv2.NORM_MINMAX)

# Finding sure foreground area
ret, sure_fg = cv2.threshold(dist_transform, 0.5*dist_transform.max(), 255, 0)

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

# Marker labelling
ret, markers = cv2.connectedComponents(sure_fg)
# Add one to all labels so that sure background is not 0, but 1
markers = markers+1
# Now, mark the region of unknown with zero
markers[unknown==255] = 0

markers_copy = markers.copy()
markers_copy[markers==0] = 150  # 灰色表示背景
markers_copy[markers==1] = 0    # 黑色表示背景
markers_copy[markers>1] = 255   # 白色表示前景

markers_copy = np.uint8(markers_copy)

# 使用分水岭算法执行基于标记的图像分割,将图像中的对象与背景分离
markers = cv2.watershed(img, markers)
img[markers==-1] = [0,0,255]  # 将边界标记为红色

plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)), plt.title('Watershed_image'), plt.axis('off')
plt.show()

效果如下:

在这里插入图片描述

2. 基于图的分割方法

此类方法把图像分割问题与图的最小割(min cut)问题相关联。首先将图像映射为带权无向图G=<V,E>,图中每个节点N∈V对应于图像中的每个像素,每条边∈E连接着一对相邻的像素,边的权值表示了相邻像素之间在灰度、颜色或纹理方面的非负相似度。而对图像的一个分割s就是对图的一个剪切,被分割的每个区域C∈S对应着图中的一个子图。而分割的最优原则就是使划分后的子图在内部保持相似度最大,而子图之间的相似度保持最小。基于图的分割方法的本质就是移除特定的边,将图划分为若干子图从而实现分割,常用的方法有GraphCut,GrabCut和Random Walk等。

2.1 Grabcut图像分割

Grabcut是基于图割(graph cut)实现的图像分割算法,它需要用户输入一个bounding box作为分割目标位置,实现对目标与背景的分离/分割。Grabcut分割速度快,效果好,支持交互操作,因此在很多APP图像分割/背景虚化的软件中经常使用。

算法流程如下:

  1. 在图片中定义含有(一个或多个)物体的矩形;
  2. 矩形外的区域被自动认为是背景;
  3. 对于用户定义的矩形区域,可用背景中数据来区分是前景还是背景;
  4. 用高斯混合模型(GMM)来对背景和前景建模,并将未定义的像素标记为可能的前景或背景;
  5. 图像中的每一个像素都被看作通过虚拟边与周围像素连接,而每条边都有一个属于前景或背景的概率,这基于它和周围像素颜色上的相似性;
  6. 每一个像素(即算法中的节点)会与一个前景或背景节点连接;
  7. 在节点完成连接后(可能与背景或前景连接),若节点之间的边属于不同终端(即一个节点属于前景,另一个节点属于背景),则会切断他们之间的边,这就能将图像各部分分割出来。

OpenCV中使用cv2.grabCut()函数来实现图像分割,其函数原型如下:

cv2.grabCut(img, rect, mask, bgdModel, fgdModel, iterCount, mode = GC_EVAL)

参数说明:

  • img:输入的三通道图像;

  • rect:表示roi区域;

  • mask:输入的单通道图像,初始化方式为GC_INIT_WITH_RECT表示ROI区域可以被初始化为:

    GC_BGD:定义为明显的背景像素 0

    GC_FGD:定义为明显的前景像素 1

    GC_PR_BGD:定义为可能的背景像素 2

    GC_PR_FGD:定义为可能的前景像素 3

  • bgdModel:表示临时背景模型数组;

  • fgdModel:表示临时前景模型数组;

  • iterCount:表示图割算法迭代次数, 次数越多,效果越好;

  • mode:当使用用户提供的roi时使用GC_INIT_WITH_RECT。

测试代码如下:

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


img = cv2.imread('lenna.jpg')
r = cv2.selectROI('input', img, False)  # 返回 (x_min, y_min, w, h)
print("input:", r)

# roi区域
roi = img[int(r[1]):int(r[1] + r[3]), int(r[0]):int(r[0] + r[2])]

# 原图mask
mask = np.zeros(img.shape[:2], dtype=np.uint8)

# 矩形roi
rect = (int(r[0]), int(r[1]), int(r[2]), int(r[3]))  # 包括前景的矩形,格式为(x,y,w,h)

bgdmodel = np.zeros((1, 65), np.float64)  # bg模型的临时数组
fgdmodel = np.zeros((1, 65), np.float64)  # fg模型的临时数组

cv2.grabCut(img, mask, rect, bgdmodel, fgdmodel, 11, mode=cv2.GC_INIT_WITH_RECT)

# 提取前景和可能的前景区域
mask2 = np.where((mask == 1) + (mask == 3), 255, 0).astype('uint8')
print(mask2.shape)

result = cv2.bitwise_and(img, img, mask=mask2)

plt.subplot(121), plt.imshow(cv2.cvtColor(roi, cv2.COLOR_BGR2RGB)), plt.title('roi_img'), plt.axis('off')
plt.subplot(122), plt.imshow(cv2.cvtColor(result, cv2.COLOR_BGR2RGB)), plt.title('Grabcut_image'), plt.axis('off')
plt.show()


效果如下:

在这里插入图片描述


3. 源码仓库地址

🌼图像处理、机器学习的常用算法汇总

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐