图像分割是指将图像分成若干互不重叠的子区域,使得同一个子区域内的特征具有一定相似性,不同子区域的特征呈现较为明显的差异。之前介绍了基于阈值的分割方法,比如Otsu法等;基于边缘检测的分割方法,比如Sobel算子、Canny算子等。下面介绍基于区域的分割方法和基于图的分割方法。
基于区域的分割算法将具有相似特征的像素集合聚集构成一个区域,这个区域中的相邻像素之间具有相似的性质,主要包括区域生长算法、区域分裂合并算法和分水岭算法等。
区域生长的基本思想是将具有相似性质的像素集合起来构成区域。具体先对每个需要分割的区域找一个种子像素作为生长起点,然后将种子像素和周围邻域中与种子像素有相同或相似性质的像素(根据某种事先确定的生长或相似准则来判定)合并到种子像素所在的区域中。将这些新像素当作新的种子继续上面的过程,直到没有满足条件的像素可被包括进来,这样一个区域就生长成了。
区域生长实现步骤如下:
下图为测试效果(左侧为原图灰度图像,右侧为区域生长图像):
需要注意的是:当选取的种子不同时,得到的区域生长图像也会不同。
区域生长是从某个或者某些像素点出发,最后得到整个区域,进而实现目标提取。而分裂合并可以看做是区域生长的逆过程:从整个图像出发,不断分裂得到各个子区域,然后再把前景区域合并,实现目标提取。分裂合并的假设是对于一幅图像,前景区域由一些相互连通的像素组成。因此,如果把一幅图像分裂到像素级,那么就可以判定该像素是否为前景像素。当所有像素点或者子区域完成判断以后,把前景区域或者像素合并就可得到前景目标。
假定一幅图像分为若干区域,按照有关区域的逻辑词P的性质,各个区域上所有的像素将是一致的。区域分裂合并的算法如下:
下图为测试效果:
图像的灰度空间很像地球表面的整个地理结构,每个像素的灰度值代表高度。其中的灰度值较大的像素连成的线可以看做山脊,也就是分水岭。其中的水就是用于二值化的gray threshold level,二值化阈值可以理解为水平面,比水平面低的区域会被淹没,刚开始用水填充每个孤立的山谷(局部最小值)。当水平面上升到一定高度时,水就会溢出当前山谷,可以通过在分水岭上修大坝,从而避免两个山谷的水汇集,这样图像就被分成2个像素集,一个是被水淹没的山谷像素集,一个是分水岭线像素集。最终这些大坝形成的线就对整个图像进行了分区,实现对图像的分割。
在该算法中,空间上相邻并且灰度值相近的像素被划分为一个区域。
分水岭算法的整个过程如下:
用上面的算法对图像进行分水岭运算,由于噪声点或其它因素的干扰,可能会得到密密麻麻的小区域,即图像被分得太细(over-segmented,过度分割),这因为图像中有非常多的局部极小值点,每个点都会自成一个小区域。
其中的解决方法有:
(1)对图像进行高斯平滑操作,抹除很多小的最小值,这些小分区就会合并。
(2)不从最小值开始增长,可以将相对较高的灰度值像素作为起始点(需要用户手动标记),从标记处开始进行淹没,则很多小区域都会被合并为一个区域,这被称为基于图像标记(mark)的分水岭算法。
在OpenCV中,我们需要给不同区域贴上不同的标签。用大于1的整数表示我们确定为前景或对象的区域,用1表示我们确定为背景或非对象的区域,最后用0表示我们无法确定的区域。然后应用分水岭算法,我们的标记图像将被更新,更新后的标记图像的边界像素值为-1。
具体步骤如下:
测试代码如下:
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()
效果如下:
此类方法把图像分割问题与图的最小割(min cut)问题相关联。首先将图像映射为带权无向图G=
Grabcut是基于图割(graph cut)实现的图像分割算法,它需要用户输入一个bounding box作为分割目标位置,实现对目标与背景的分离/分割。Grabcut分割速度快,效果好,支持交互操作,因此在很多APP图像分割/背景虚化的软件中经常使用。
算法流程如下:
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()
效果如下:
图像处理、机器学习的常用算法汇总