2019-6-26 opencv图像分割(image segmentation)

**图像分割(image segmentation)**是图像预处理的重要步骤之一,它的主要目标是将图像划分为不同的区域,这些区域与真实世界中的物体具有一定的关联成分。

图像分割的方法大体分为三种:基于阈值的分割、基于边缘的分割和基于区域的分割。
理论说明参见https://www.jianshu.com/p/4ffdf060fe57

基于阈值的分割是一种传统的最常用的图像分割方法,因其实现简单、计算量小、性能较稳定而成为图像分割中最基本和应用最广泛的分割技术。它特别适用于目标和背景占据不同灰度级范围的图像。它不仅可以极大的压缩数据量,而且也大大简化了分析和处理步骤,因此在很多情况下,是进行图像分析、特征提取与模式识别之前的必要的图像预处理过程。
图像阈值化的目的是要按照灰度级,对像素集合进行一个划分,得到的每个子集形成一个与现实景物相对应的区域,各个区域内部具有一致的属性,而相邻区域不具有这种一致属性。这样的划分可以通过从灰度级出发选取一个或多个阈值来实现。
基本原理:通过设定不同的特征阈值,把图像象素点分为若干类。

基于边缘的分割,是通过边缘检测,即检测灰度级或者结构具有突变的地方,表明一个区域的终结,也是另一个区域开始的地方。这种不连续性称为边缘。
不同的图像灰度不同,边界处一般有明显的边缘,利用此特征可以分割图像。
图像中边缘处像素的灰度值不连续,这种不连续性可通过求导数来检测到。对于阶跃状边缘,其位置对应一阶导数的极值点,对应二阶导数的过零点(零交叉点)。因此常用微分算子进行边缘检测。常用的一阶微分算子有Roberts算子、Prewitt算子和Sobel算子,二阶微分算子有Laplace算子和Kirsh算子等。在实际中各种微分算子常用小区域模板来表示,微分运算是利用模板和图像卷积来实现。这些算子对噪声敏感,只适合于噪声较小不太复杂的图像。
由于边缘和噪声都是灰度不连续点,在频域均为高频分量,直接采用微分运算难以克服噪声的影响。因此用微分算子检测边缘前要对图像进行平滑滤波。LoG算子和Canny算子是具有平滑功能的二阶和一阶微分算子,边缘检测效果较好,

基于区域分割是将图像按照相似性准则分成不同的区域,主要包括区域增长,区域分裂合并和分水岭等几种类型。

OpenCV提供了 分水岭算法函数watershed 和 GrabCut算法函数grabCut,可以快速实现图像的分割。

分水岭算法函数watershed

以下解释来自百度百科
分水岭分割方法,是一种基于拓扑理论的数学形态学的分割方法,其基本思想是把图像看作是测地学上的拓扑地貌,图像中每一点像素的灰度值表示该点的海拔高度,每一个局部极小值及其影响区域称为集水盆,而集水盆的边界则形成分水岭。分水岭的概念和形成可以通过模拟浸入过程来说明。在每一个局部极小值表面,刺穿一个小孔,然后把整个模型慢慢浸入水中,随着浸入的加深,每一个局部极小值的影响域慢慢向外扩展,在两个集水盆汇合处构筑大坝,即形成分水岭。

分水岭计算分两个步骤,一个是排序过程,一个是淹没过程。首先对每个像素的灰度级进行从低到高排序,然后在从低到高实现淹没过程中,对每一个局部极小值在h阶高度的影响域采用先进先出(FIFO)结构进行判断及标注。

分水岭变换得到的是输入图像的集水盆图像,集水盆之间的边界点,即为分水岭。显然,分水岭表示的是输入图像极大值点。因此,为得到图像的边缘信息,通常把梯度图像作为输入图像。

分水岭算法会把跟临近像素间的相似性作为重要的参考依据,从而将在空间位置上相近并且灰度值相近的像素点互相连接起来构成一个封闭的轮廓,封闭性是分水岭算法的一个重要特征。
分水岭算法对微弱边缘具有良好的响应,是得到封闭连续边缘的保证。
分水岭算法所得到的封闭的集水盆,为分析图像的区域特征提供了可能。

但是,图像中的噪声、物体表面细微的灰度变化,都会让算法产生过度分割的现象。
为消除分水岭算法产生的过度分割,通常可以采用两种处理方法

  • 利用先验知识去除无关边缘信息
  • 修改梯度函数使得集水盆只响应想要探测的目标。一个简单的方法是对梯度图像进行阈值处理,以消除灰度的微小变化产生的过度分割。

程序可采用方法:
用阈值限制梯度图像以达到消除灰度值的微小变化产生的过度分割,获得适量的区域。
再对这些区域的边缘点的灰度级进行从低到高排序,然后在从低到高实现淹没的过程。
梯度图像用Sobel算子计算获得。
对梯度图像进行阈值处理时,选取合适的阈值对最终分割的图像有很大影响,因此阈值的选取是图像分割效果好坏的一个关键。
缺点:实际图像中可能含有微弱的边缘,灰度变化的数值差别不是特别明显,选取阈值过大可能会消去这些微弱边缘。

代码实现

# -*- coding: cp936 -*-
import cv2
import numpy as np

# Step1. 加载图像
img = cv2.imread('3.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Step2.阈值分割,将图像分为黑白两部分
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
cv2.imshow("thresh", thresh)

# Step3. 对图像进行“开运算”,先腐蚀再膨胀
kernel = np.ones((3, 3), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)
cv2.imshow("opening", opening)

# Step4. 对“开运算”的结果进行膨胀,得到大部分都是背景的区域
sure_bg = cv2.dilate(opening, kernel, iterations=3)
cv2.imshow("sure_bg", sure_bg)

# Step5.通过distanceTransform获取前景区域
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
ret, sure_fg = cv2.threshold(dist_transform, 0.6 * dist_transform.max(), 255, 0)
cv2.imshow("sure_fg", sure_fg)

# Step6. sure_bg与sure_fg相减,得到既有前景又有背景的重合区域
sure_fg = np.uint8(sure_fg)
unknow = cv2.subtract(sure_bg, sure_fg)

# Step7. 连通区域处理
ret, markers = cv2.connectedComponents(sure_fg)
markers = markers + 1
markers[unknow==255] = 0

# Step8.分水岭算法
markers = cv2.watershed(img, markers)
img[markers == -1] = [0, 255, 0] #绿色标记分水岭
cv2.imshow("dst", img)
cv2.waitKey(0)

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

代码运行中间结果显示如上,最后一张是最终分割效果。

分水岭函数说明如下,网上参见https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_watershed/py_watershed.html

watershed( InputArray image, InputOutputArray markers )
  • image,8bit 3通道彩色图像矩阵序列
  • markers ,32位单通道图。这个参数说明需要参考https://docs.opencv.org/3.4.3/d7/d1b/group__imgproc__misc.html#ga3267243e4d3f95165d55a618c65ac6e1

Before passing the image to the function, you have to roughly outline the desired regions in the image markers with positive (>0) indices. So, every region is represented as one or more connected components with the pixel values 1, 2, 3, and so on. Such markers can be retrieved from a binary mask using findContours and drawContours (see the watershed.cpp demo). The markers are “seeds” of the future image regions. All the other pixels in markers , whose relation to the outlined regions is not known and should be defined by the algorithm, should be set to 0’s. In the function output, each pixel in markers is set to a value of the “seed” components or to -1 at boundaries between the regions.

markers 参数中包含的是轮廓信息。用这些轮廓信息可以把图划分成一些区域(region)。
这些轮廓(或者叫连接组件connected components)信息是大于0的值,每个区域轮廓都用一个像素值1,2,3来标记,图像上除此之外其他地方被设置为0。
轮廓可以通过轮廓函数findContours 来找到,然后用drawContours 画出。
这些轮廓信息也就是种子(seeds)信息,以它们为基础用分水岭算法对图像中其他像素进行划分,决定其属于哪个区域。
算法输出结果,图像中区域边界用’-1’标记,其余则是设置为种子信息的值(像素值1,2,3等)。

注意:说明中指出找出轮廓可以用findContours函数,但是代码中并没有如此操作。代码中用前景和背景重叠区域边界作为轮廓。

GrabCut算法函数grabCut

GrabCut算法利用了图像中的纹理(颜色)信息和边界(反差)信息,只要小量的用户交互操作即可得到比较好的分割效果。

grabCut(img, mask, rect, bgdModel, fgdModel, iterCount, mode=None)
  • img: 输入图像,必须是8位3通道图像,在处理过程中不会被修改

  • 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: 包含前景的矩形,格式为(x, y, w, h)

  • bdgModel,fgdModel: 算法内部使用的数组,只需要创建两个大小为(1,65),数据类型为np.float64的数组

  • iterCount: 算法迭代的次数

  • mode: 用来指示grabCut函数进行什么操作:
    cv.GC_INIT_WITH_RECT (=0),用矩形窗初始化GrabCut;
    cv.GC_INIT_WITH_MASK (=1),用掩码图像初始化GrabCut。

实现代码如下

# -*- coding: cp936 -*-

import cv2
import numpy as np

# Step1. 加载图像
img = cv2.imread('5.jpg')

# Step2. 创建掩模、背景图和前景图
mask = np.zeros(img.shape[:2], np.uint8)# 创建大小相同的掩模
bgdModel = np.zeros((1,65), np.float64)# 创建背景图像
fgdModel = np.zeros((1,65), np.float64)# 创建前景图像

# Step3. 初始化矩形区域
# 这个矩形必须完全包含前景
rect = (50,0,450,333) #格式为(x, y, w, h)

# Step4. GrubCut算法,迭代5次
# mask的取值为0,1,2,3
cv2.grabCut(img, mask, rect, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_RECT) # 迭代5次

# Step5. mask中,值为2和0的统一转化为0, 1和3转化为1
mask2 = np.where((mask == 2) | (mask == 0), 0, 1).astype('uint8')
img = img * mask2[:,:,np.newaxis] # np.newaxis 插入一个新维度,相当于将二维矩阵扩充为三维

cv2.imshow("dst", img)
cv2.waitKey(0)

在这里插入图片描述

结果如上图,左边是grabCut结果,右边是原图。
这个算法的关键就是,在开始用户画一个矩形方块把前景图圈起来,前景区域应该完全在矩形内,然后算法反复进行分割以达到最好效果。
图中的向日葵就是前景。
这个图大小是500*333,所以把矩形框设置为(50,0,450,333),这样可以把整个向日葵包含其中。注意:矩形框格式为(x, y, w, h)。

感觉GrabCut算法实现比分水岭算法简单多了,效果也很好。

进阶应用。
观察左边图片中,可以注意到在右边还是有一小部分区域(2个花瓣之间)没有被识别为背景。我们可以继续利用上面的方法把它也识别出来。
再设置一个矩形框

rect1 = (450,0,50,333) #格式为(x, y, w, h)

对这个区域进行GrabCut,然后显示结果
在这里插入图片描述
现在简单了。因为mask中前景为1,背景为0,所以可以把第二次的结果反转一下,前景为0,背景为1,然后和第一次计算的结果按照元素一一相乘,就能得到最终需要的效果。
类似PS中增加一层效果。

# -*- coding: cp936 -*-

import cv2
import numpy as np

# Step1. 加载图像
img = cv2.imread('5.jpg')

# Step2. 创建掩模、背景图和前景图
mask = np.zeros(img.shape[:2], np.uint8)# 创建大小相同的掩模
mask3 = np.zeros(img.shape[:2], np.uint8)# 创建大小相同的掩模
bgdModel = np.zeros((1,65), np.float64)# 创建背景图像
fgdModel = np.zeros((1,65), np.float64)# 创建前景图像

# Step3. 初始化矩形区域
# 这个矩形必须完全包含前景
rect =  (50,0,450,333) #格式为(x, y, w, h)
rect1 = (450,0,50,333) #格式为(x, y, w, h)#去除右边一小部分第一次没有被识别区域

# Step4. GrubCut算法,迭代5次
# mask的取值为0,1,2,3
#第一次识别
cv2.grabCut(img, mask, rect, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_RECT) # 迭代5次

#第二次识别
cv2.grabCut(img, mask3, rect1, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_RECT) # 迭代5次

# Step5. mask中,值为2和0的统一转化为0, 1和3转化为1
mask2 = np.where((mask == 2) | (mask == 0), 0, 1).astype('uint8')
mask4 = np.where((mask3 == 2) | (mask3 == 0), 0, 1).astype('uint8')
mask5 = np.where( mask4 == 0, 1, 0).astype('uint8')
mask6 =np.multiply(mask2,mask5)
                 
 # np.newaxis 插入一个新维度,相当于将二维矩阵扩充为三维
img = img * mask6[:,:,np.newaxis]

在这里插入图片描述
结果不错吧。复杂的图像都可以利用这个思路来解决。

猜你喜欢

转载自blog.csdn.net/weixin_42555985/article/details/93713477