@[TOC](OpenCV入门(十六)快速学会OpenCV 15 图像分割)
作者:Xiou
图像分割主要是指将图像分成各具特性的区域并提取出感兴趣目标的技术。图像分割是数字图像分析中的重要环节,在整个研究中起着承前启后的作用,既是对所有图像预处理效果的一个检验,也是后续进行图像分析与解译的基础。
图像阈值化分割是一种传统的、最常用的图像分割方法,因其实现简单、计算量小、性能较稳定,成为图像分割中最基本和应用最广泛的分割技术。它特别适用于目标和背景占据不同灰度级范围的图像,在很多情况下是进行图像分析、特征提取与模式识别之前必要的图像预处理过程。
图像阈值化的目的是要按照灰度级对像素集合进行一个划分,得到的每个子集形成一个与现实景物相对应的区域,各个区域内部具有一致的属性,而相邻区域不具有这种一致属性。这样的划分可以通过从灰度级出发选取一个或多个阈值来实现。
阈值分割的基本原理是通过设定不同的特征阈值把图像像素点分为若干类。常用的特征包括直接来自原始图像的灰度或彩色特征、由原始灰度或彩色值变换得到的特征。
测试图片:
1.彩色图像分割
灰度图像大多通过算子寻找边缘和区域生长融合来分割图像。彩色图像增加了色彩信息,可以通过不同的色彩值来分割图像,常用彩色空间HSV/HIS、RGB、LAB等都可以用于分割。本节将使用inRange函数来实现阈值化,跟前面的阈值化方法一样,只不过在实现时用阈值范围来替代固定阈值。inRange函数提供了一种物体检测的手段,用基于像素值范围的方法,在HSV色彩空间检测物体从而达到分割的效果。
HSV(Hue、Saturation、Value的首字母,表示颜色的色相、饱和度、强度)色彩空间是一种类似于RGB的颜色表示方式。hue通道是颜色类型,在需要根据颜色来分割物体的应用中非常有效。如图所示,saturation的变化从不饱和到完全饱和,对应图中灰色过渡到阴影(没有白色成分)。value描述了颜色的强度或者亮度。图中显示的是HSV圆柱体,表示HSV的颜色空间。
HSV是一种比较直观的颜色模型,所以在许多图像编辑工具中应用比较广泛,这个模型中颜色的参数分别是色调(H)、饱和度(S)、明度(V)。
由于RGB色彩空间是由三个通道来编码颜色的,因此难以根据颜色来分割物体,而HSV中只有Hue一个通道表示颜色。此时可以用函数cvtColor将BGR转换到HSV色彩空间,然后利用函数inRange根据HSV设置的范围检测目标。该函数声明如下:
inRange(src, lowerb, upperb[, dst])
其中,
src表示输入图像;
lowerb表示H、S、V的最小值;
upperb表示H、S、V的最大值;
dst表示输出图像,要和输入图像有相同的尺寸且为CV_8U类。
直接用HSV体系进行颜色分割
代码实例:
import cv2 as cv
import numpy as np
def color_seperate(image):
hsv = cv.cvtColor(image, cv.COLOR_BGR2HSV) #对目标图像进行色彩空间转换
lower_hsv = np.array([100, 43, 46]) #设定蓝色下限
upper_hsv = np.array([124, 255, 255]) #设定蓝色上限
#依据设定的上下限对目标图像进行二值化转换
mask = cv.inRange(hsv, lowerb=lower_hsv, upperb=upper_hsv)
#将二值化图像与原图进行“与”操作;实际是提取前两个frame的“与”结果,
#然后输出mask为1的部分
dst = cv.bitwise_and(src, src, mask=mask) #注意:括号中要写mask=xxx
cv.imshow('result', dst) #输出
src = cv.imread('test.jpg') #导入目标图像,获取图像信息
color_seperate(src)
cv.imshow('image', src)
cv.waitKey(0)
cv.destroyAllWindows()
输出结果:
2.grabCut算法分割
可以使用grabCut算法来分割前景或使用最小程度的用户交互来分解前景。
OpenCV中的grabCut算法是Graph_Cut算法的改进,Graph_Cut是一种直接基于图割算法的图像分割技术,仅仅需要确认前景和背景输入就可以完成前景和背景的最优分割。
该算法利用了图像中的纹理(颜色)信息和边界(反差)信息,只要少量的用户交互操作即可得到比较好的分割结果,和分水岭算法比较相似,但是计算速度比较慢,得到的结果比较精确。如果要从静态图像中提取前景物体(如从一个图像剪切物体到另一个图像),采用grabCut算法是最好的选择。
grabCut函数声明如下:
grabCut(img, mask, rect, bgdModel, fgdModel, iterCount[, mode])
参数
img表示输入原图像。
mask表示输出掩码,如果使用掩码进行初始化,那么mask保存初始化掩码信息,在执行分割的时候也可以将用户交互所设定的前景与背景保存到mask中再传入grabCut函数。在处理结束之后,mask中会保存结果。
mask只能取四种值:
GCD_BGD(=0)表示背景;
GCD_FGD(=1)表示前景;
GCD_PR_BGD(=2)表示可能的背景;
GCD_PR_FGD(=3)表示可能的前景。
如果没有手工标记GCD_BGD或者GCD_FGD,那么结果只能是GCD_PR_BGD或GCD_PR_FGD。rect表示用户选择的前景矩形区域,包含分割对象的矩形ROL,矩形外部的像素为背景,矩形内部的像素为前景,当参数mode=GC_INIT_WITH_RECT时使用这个参数。bgModel表示输出背景图像。fgdModel表示输出前景图像。iterCount表示迭代次数。mode表示用于指示grabCut函数进行什么操作,可选的值有GC_INIT_WITH_RECT(=0)表示用矩形窗初始化grabCut;GC_INIT_WITH_MASK(=1)表示用掩码图像初始化grabCut;GC_EVAL(=2)表示执行分割。
利用grabCut函数做图像分割时,通常还需要和compare函数联合使用。compare函数主要用于在两个图像之间进行逐像素的比较,并输出比较的结果,函数声明如下:
cv2.compare(src1, src2, cmpop[, dst])
其中,参数
src1表示原始图像1(必须是单通道)或者一个数值,比如是一个Mat或者一个单纯的数字n;
src2表示原始图像2(必须是单通道)或者一个数值,比如是一个Mat或者一个单纯的数字n;
dst表示结果图像,类型是CV_8UC1,即单通道8位图,大小和src1和src2中最大的一样,比较结果为真的地方值为255,否则为0;
cmpop表示操作类型,有以下几种类型:
利用grabCut做图像前景分割
代码实例:
import numpy as np
import cv2
from matplotlib import pyplot as plt
import warnings
warnings.filterwarnings("ignore", module="matplotlib")
imgpath = "girl.jpg"
img = cv2.imread(imgpath)
Coords1x, Coords1y = 'NA', 'NA'
Coords2x, Coords2y = 'NA', 'NA'
def OnClick(event):
#获取当鼠标"按下"的时候,鼠标的位置
global Coords1x, Coords1y
if event.button == 1:
try:
Coords1x = int(event.xdata)
Coords1y = int(event.ydata)
except:
Coords1x = event.xdata
Coords1y = event.ydata
print("####左上角坐标:", Coords1x, Coords1y)
def OnMouseMotion(event):
#获取当鼠标"移动"的时候,鼠标的位置
global Coords2x, Coords2y
if event.button == 3:
try:
Coords2x = int(event.xdata)
Coords2y = int(event.ydata)
except:
Coords2x = event.xdata
Coords2y = event.ydata
print("#### 右下角坐标:", Coords2x, Coords2x)
def OnMouseRelease(event):
if event.button == 2:
fig = plt.gca()
img = cv2.imread(imgpath)
#创建一个与所加载图像同形状的 Mask
mask = np.zeros(img.shape[:2], np.uint8)
#算法内部使用的数组,你必须创建两个np.float64类型的0数组,大小是(1, 65)
bgdModel = np.zeros((1, 65), np.float64)
fgdModel = np.zeros((1, 65), np.float64)
#计算人工前景的矩形区域 (rect.x,rect.y,rect.width,rect.height)
if (Coords2x - Coords1x) > 0 and (Coords2y - Coords1y) > 0:
try:
rect = (Coords1x, Coords1y, Coords2x - Coords1x, Coords2y -Coords1y)
print('#### 分割区域: ', rect)
print('#### 等会儿 有点慢 ...')
iterCount = 5
cv2.grabCut(img, mask, rect, bgdModel, fgdModel, iterCount, cv2.GC_INIT_WITH_RECT)
mask2 = np.where((mask == 2) | (mask == 0), 0, 1).astype('uint8')
img = img * mask2[:, :, np.newaxis]
plt.subplot(121), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.subplot(122), plt.imshow(cv2.cvtColor(cv2.imread(imgpath), cv2.COLOR_BGR2RGB))
fig.figure.canvas.draw()
print('May the force be with me!')
except:
print('#### 先左键 后右键 ')
else:
print('#### 左下角坐标值必须大于右上角坐标 ')
#预先绘制图片
fig = plt.figure()
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
#鼠标左键,选取分割区域(长方形)的左上角点
fig.canvas.mpl_connect('button_press_event', OnClick)
#鼠标右键,选取分割区域(长方形)的右下角点
fig.canvas.mpl_connect('button_press_event', OnMouseMotion)
#鼠标中键,在所选区域执行分割操作
fig.canvas.mpl_connect('button_press_event', OnMouseRelease)
plt.show()
代码实现的基本步骤如下:
(1)在图片中定义含有(一个或者多个)物体的矩形。
(2)矩形外的区域被自动认为是背景。
(3)对于用户定义的矩形区域,可用背景中的数据来区别它里面的前景和背景区域。(4)用高斯混合模型来对背景和前景建模,并将未定义的像素标记为可能的前景或背景。
(5)图像中的每一个像素都被看作通过虚拟边与周围像素相连接,而每条边都有一个属于前景或背景的概率,这基于它与周围颜色上的相似性。
(6)每一个像素(算法中的节点)会与一个前景或背景节点链接。
(7)在节点完成链接后,若节点之间的边属于不同终端,则会切断它们之间的边,这就能将图像各部分分割出来。
(8)保存工程并运行,这时用鼠标左键单击左上角、用鼠标右键单击右下角,之后单击鼠标中键进行生成。
运行结果如图所示。可以看到,前景、背景都已经被分离出来了。
3.floodFill漫水填充分割
漫水填充法是一种用特定的颜色填充联通区域,通过设置可连通像素的上下限以及连通方式来达到不同的填充效果的方法。漫水填充经常被用来标记或分离图像的一部分,以便对其进行进一步处理或分析,也可以用来从输入图像获取掩码区域,掩码会加速处理过程,或只处理掩码指定的像素点,操作的结果总是某个连续的区域。
漫水填充法的原理很简单,就是从一个点开始遍历附近的像素点,填充成新的颜色,直到封闭区域内所有像素点都被填充成新颜色为止。floodFill填充的实现方法常见的有4邻域像素填充法、8邻域像素填充法、基于扫描线的像素填充法等。
在OpenCV中,漫水填充算法由floodFill函数实现,其作用是用我们指定的颜色从种子点开始填充一个连接域,连通性由像素值的接近程度来衡量。在OpenCV中,有两个C++重写版本的floodFill,函数声明如下:
floodFill(image, mask, seedPoint, newVal[, loDiff[, upDiff[, flags]]])
参数:
image: 输入图片
mask: 掩码, 比 image 长宽高 2
seedPoint: 泛洪算法的起始点
newVal: 重绘区域的新值 (颜色)
loDiff: seePoint – loDiff 下限
upDiff: seePoint – upDiff 上限
flags: 操作标志符
LOODFILL_FIXED_RANGE: 改变图像, 泛洪填充
FLOODFILL_MASK_ONLY: 不改变图像, 只填充 mask 本身, 忽略新的颜色值参数。
原图:
代码实例1:
import numpy as np
import cv2
def flood_fill(image):
"""泛洪"""
# 深拷贝
image_copy = image.copy()
# 获取高宽
h, w = image.shape[:2]
# 获取mask
mask = np.zeros([h + 2, w + 2], np.uint8)
# 泛洪
cv2.floodFill(image_copy, mask, (175, 20), (0, 25, 0), (30, 30, 30), (50, 50, 50), cv2.FLOODFILL_FIXED_RANGE)
# 图片展示
cv2.imshow("flood_fill", image_copy)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 返回
return image_copy
if __name__ == "__main__":
# 读取图片
img = cv2.imread("test.jpg")
# 获取泛洪图像
flood_fill = flood_fill(img)
# 保存图片
cv2.imwrite("flood_fill.jpg", flood_fill)
输出结果:
4.分水岭分割
分水岭算法是一种图像区域分割法,在分割的过程中会把跟临近像素间的相似性作为重要的参考依据,从而将在空间位置上相近并且灰度值相近的像素点互相连接起来,构成一个封闭的轮廓。封闭性是分水岭算法的一个重要特征。
在OpenCV中,分水岭算法的函数是watershed,声明如下:
void cv::watershed ( InputArray image, InputOutputArray markers )
参数
image必须是一个8位3通道彩色图像矩阵序列;
参数markers表示必须包含种子点信息,在执行分水岭函数watershed之前,必须对第二个参数markers进行处理,它应该包含不同区域的轮廓,每个轮廓有一个唯一的编号,轮廓的定位可以通过OpenCV中findContours方法实现。
使用watershed函数实现图像自动分割的基本步骤如下:
(1)图像灰度化、滤波、Canny边缘检测。
(2)查找轮廓,把轮廓信息按照不同的编号绘制到watershed的第二个入参markers上,相当于标记注水点。
(3)watershed分水岭运算。
(4)绘制分割出来的区域。另外,还可以使用随机颜色填充,或者跟原始图像融合一下,以得到更好的显示效果。
代码实例:
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('coin.png')
if img is None:
print('Could not open or find the image ')
exit(0)
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)
# cv.imshow("threshold", thresh) #阈值处理后会有紧挨着(粘连)的情况
# 去噪处理
kernel = np.ones((3, 3), np.uint8)
opening = cv.morphologyEx(thresh, cv.MORPH_OPEN, kernel, iterations=2) # 开运算
# sure background area
sure_bg = cv.dilate(opening, kernel, iterations=3) # 膨胀操作
# Finding sure foreground area
dist_transform = cv.distanceTransform(opening, cv.DIST_L2, 5)
ret, sure_fg = cv.threshold(dist_transform, 0.7 * dist_transform.max(), 255, 0) # 距离背景点足够远的点认为是确定前景
# Finding unknown region
sure_fg = np.uint8(sure_fg)
unknown = cv.subtract(sure_bg, sure_fg) # 确定未知区域:减法运算
# Marker labelling
ret, markers = cv.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 = cv.watershed(img, markers)
img[markers == -1] = [255, 0, 0]
plt.imshow(img)
plt.show()
输出结果:
其中,threshold函数的作用是进行阈值处理,morphologyEx函数的作用是去除噪声,dilate函数的作用是用膨胀的方式获取背景。
distanceTransform函数进行距离变换,cv.DIST_L2代表采用欧几里得的距离计算公式,5代表掩膜尺寸,用来确定前景,然后通过阈值处理得到核心的区域,超过最大值的70%才留下来。
在分水岭算法中,标注0代表未知区域,所以需要对上面的标注结果进行调整。最后用函数watershed实现分水岭算法。