哎呀终于又到了核心技术之一啦 激动惹!
目前比较成熟的图像分割技术有边缘检测方法、阈值分割法、区域分割技术等。近十几年来,又有形态学、小波变换、模糊数学等方法,形态学后面等说开闭变换的时候说,小波变换应该后面会有专门的一节来总结,这一节先说经典的几种方法好啦。。
边缘分割技术是通过边缘检测来实现的,而边缘检测又是通过物体和背景在图像特性上的差异来实现的。常见的边缘检测方法一般都是用滤波器,使用的算子有微分算子、Canny算子、Log算子等。微分算子又分Prewitt算子、Roberts算子、Sobel算子、Laplacian算子等。
前面也说了,对于图像中的间断点,检测模板一般是:
np.array([-1,-1,-1],
[-1, 8,-1],
[-1,-1,-1])
而对于线段,检测模板就有:
np.array([-1,-1,-1],
[ 2, 2, 2],
[-1,-1,-1]) # 水平线段
np.array([-1, 2,-1],
[-1, 2,-1],
[-1, 2,-1]) # 竖直线段
np.array([-1,-1, 2],
[-1, 2,-1],
[ 2,-1,-1]) # +45度线段
np.array([ 2,-1,-1],
[-1, 2,-1],
[-1,-1, 2]) # -45度线段
可以通过滤波函数用这四个滤波器对图像进行滤波(或者说卷积 无所谓啦),然后把得到的结果加在一起,就能覆盖整张图片中大部分的线段。
用于边缘检测时,matlab中有edge
函数直接取,需要自己选择算子,而且这个函数还自动取阈值来得到边缘,输出的是二值图像。
首先有Roberts算子,对于离散的图像f(x,y),边缘检测算子就是用水平和垂直差分逼近梯度算子,这种算子由两个2*2矩阵组成:
np.array([1 , 0],
[0 ,-1])
np.array([0 , 1],
[-1, 0])
由于卷积核一般是3*3,这个可以等价为左边和上边加一排0(能理解吧),滤波之后得到的两个矩阵对应元素求平方和再开方,然后二值化就得到了边缘图像。
对于复杂的图像,用Roberts算子不太能准确的获取边缘。这里用Prewitt算子代替:
np.array([-1,-1,-1],
[ 0, 0, 0],
[ 1, 1, 1])
np.array([-1, 0, 1],
[-1, 0, 1],
[-1, 0, 1])
具体和上面一样,这种方法对于边缘的提取效果还可以,不过也不是最好的方法。
Sobel算子和Scharr的大小和Prewitt相同。算子如下:
np.array([-1,-2,-1],
[ 0, 0, 0],
[ 1, 2, 1])
np.array([-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1])#Sobel算子
np.array([-3,-10,-3],
[ 0, 0, 0],
[ 3, 10, 3])
np.array([-3, 0, 3],
[-10, 0, 10],
[-3, 0, 3])#Scharr算子
都是3*3的时候,据说Scharr算子的效果最好,很多地方都没给出解释。(可能是更符合高斯)
这两种算子算是高斯平滑与微分操作的结合体,所以抗噪声能力较好。
还有这几种(Sobel,Scharr,Prewitt)算子都可以对单方向求边缘。
在python中,操作如下:
sobelx=cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3)
#参数1,0为x方向求一阶导,最高可以求2阶导
#cv2.CV_64F 是数据类型,一般设置高一些,防止截断边界。取绝对值之后可以取回unit8
#ksize可以选择5*5,或者更高
#但当ksize=-1时,Sobel算子会变为3*3的Scharr算子
正常来说,Laplacian作为2阶导数算子,应该是上面的,但在matlab中用fspecial得到的却是下边的:
np.array([ 0,-1,-1],
[-1, 4,-1],
[ 0,-1, 0])
np.array([1/6, 2/3, 1/6],
[2/3,-10/3, 2/3],
[1/6, 2/3, 1/6])
我就以上面的为准啦!(我猜下面的这个是通过平滑滤波得到的结果)
在opencv中,实现Laplacian算子的语句如下(好像是通过调用sobel算子实现的):
laplacian = cv2.Laplacian(img,cv2.CV_64F)
这种方法还是蛮好用的,得到的效果很好。
不过这个算子因为是二阶导,所以对噪声敏感,所以应该事先做过一次高斯滤波(函数内)。在matlab中,好像这个叫做Log算子?而log算子在matlab中用fspecial得到的是一个数值比较奇怪的矩阵,可能是经过高斯平滑滤波过的。
这种方法虽然说是算子,但这种方法其实是一个比较复杂的过程,分几步:
1、先去除噪声,一般用5*5的高斯滤波器;
2、用Sobel算子计算两个方向的一阶导数,然后根据得到的两个梯度图找到边界的梯度和方向:
梯度:平方和开根号;方向:tan-1(Gx/Gy)
3、做非极大值抑制,在获得梯度的方向和大小之后,应该对整幅图像做一个扫描,去除那些非边界上的点。对每一个像素进行检查,看这个点的梯度是不是周围具有相同梯度方向的点中最大的。这样可以得到一个窄边界。
4、做滞后阈值,这时我们需要设置两个阈值:minVal 和 maxVal。当图像的灰度梯度高于 maxVal 时被认为是真的边界,那些低于 minVal 的边界会被抛弃。如果介于两者之间的话,就要看这个点是否与某个被确定为真正的边界点相连,如果是就认为它也是边界点,如果不是就抛弃。
应该说的蛮清楚吧?如果不清楚可以在评论区回复我。。
在opencv中,还是一句话:
edges = cv2.Canny(img,minVal,maxVal,L2gradiant)
#L2gradiant默认是False,False时梯度大小方程是平方和,不开根号
这种方法是最简单的一种分割方法,关键在于阈值是否合适,通常通过直方图选取。(实际上就是二值化啦)
很简单,和最普通的二值化差不多。先取直方图,然后用波谷处的灰度值作为阈值就可以区分物体与背景。当图像有亮暗不同的情况时,这种方法自然gg。
前面二值化说过了,在matlab中,直接用graythresh
得到的阈值就是大津法得到的阈值。
这个方法分几步:
1、设置精度参数T0,然后选择一个初始的阈值T1;
2、用T1分割图像为G1与G2。计算G1与G2的平均值,把新的阈值设定为G1与G2平均值的平均值;
3、如果T2-T1的绝对值小于T0,就推出T2是最佳阈值;反之把T2赋给T1,从2开始重复,直到得到最佳阈值。
感觉这些个方法都有点姜姜的,特定环境下会比较好用把。。
区域分割主要包括区域生长法和分水岭分割法。
顾名思义,区域生长法是一种串行区域分割的图像分割方法。他的根本思想是将具有相似性质的像素归类起来构成区域。从初始区域开始(seed),将相邻的区域/像素归并到当前区域中,并不断扩大区域,直到没有可归并的邻域为止。
当没有先验知识时,这种方法有最佳效果,不过因为是迭代,所以会很慢。
这种方法造成结果的好坏完全取决于seed的选取,归并的条件,终止的条件。
分水岭相当于一个自适应的多阈值分割算法。有好多种实现算法,拓扑学,形态学,浸水模拟和降水模拟等方式。在分割的过程中,它会把跟临近像素间的相似性作为重要的参考依据,从而将在空间位置上相近并且灰度值相近的像素点互相连接起来构成一个封闭的轮廓,封闭性是分水岭算法的一个重要特征。
有点不知道怎么说,引用一下opencv-python中文手册中的一段描述:
任何一副灰度图像都可以被看成拓扑平面,灰度值高的区域可以被看成是
山峰,灰度值低的区域可以被看成是山谷。我们向每一个山谷中灌不同颜色的
水。随着水的位的升高,不同山谷的水就会相遇汇合,为了防止不同山谷的水
汇合,我们需要在水汇合的地方构建起堤坝。不停的灌水,不停的构建堤坝知
道所有的山峰都被水淹没。我们构建好的堤坝就是对图像的分割。这就是分水
岭算法的背后哲理。你可以通过访问网站CMM webpage on watershed来
加深自己的理解。
但是这种方法通常都会得到过度分割的结果,这是由噪声或者图像中其他
不规律的因素造成的。为了减少这种影响, OpenCV 采用了基于掩模的分水岭
算法,在这种算法中我们要设置那些山谷点会汇合,那些不会。这是一种交互
式的图像分割。我们要做的就是给我们已知的对象打上不同的标签。如果某个
区域肯定是前景或对象,就使用某个颜色(或灰度值)标签标记它。如果某个
区域肯定不是对象而是背景就使用另外一个颜色标签标记。而剩下的不能确定
是前景还是背景的区域就用 0 标记。这就是我们的标签。然后实施分水岭算法。
每一次灌水,我们的标签就会被更新,当两个不同颜色的标签相遇时就构建堤
坝,直到将所有山峰淹没,最后我们得到的边界对象(堤坝)的值为 -1。
matlab中代码就是watershed(Img,CONN)
conn取4按4连通,8按8连通。
python中要设定marker,代码如下:
import numpy as np
import cv2
from matplotlib import pyplot as plt
img = cv2.imread('water_coins.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 = np.ones((3,3),np.uint8)
opening = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel, iterations = 2)
# sure background area
sure_bg = cv2.dilate(opening,kernel,iterations=3)
# Finding sure foreground area
# 距离变换的基本含义是计算一个图像中非零像素点到最近的零像素点的距离,也
# 就是到零像素点的最短距离。最常见的距离变换算法就是通过连续的腐蚀操作来
# 实现,腐蚀操作的停止条件是所有前景像素都被完全腐蚀。这样根据腐蚀的先后
# 顺序,我们就得到各个前景像素点到前景中心呗Ⅵ像素点的距离。根据各个像素
# 点的距离值,设置为不同的灰度值。这样就完成了二值图像的距离变换。
#cv2.distanceTransform(src, distanceType, maskSize)
# 第二个参数 0,1,2 分别表示 CV_DIST_L1, CV_DIST_L2 , CV_DIST_C
dist_transform = cv2.distanceTransform(opening,1,5)
ret, sure_fg = cv2.threshold(dist_transform,0.7*dist_transform.max(),255,0)
# Finding unknown region
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg,sure_fg)
# Marker labelling
ret, markers1 = cv2.connectedComponents(sure_fg)
# Add one to all labels so that sure background is not 0, but 1
markers = markers1+1
# Now, mark the region of unknown with zero
markers[unknown==255] = 0
markers3 = cv2.watershed(img,markers)
img[markers3 == -1] = [255,0,0]
OpenCV 自带的示例中也有一个交互式分水岭分割程序: watershed.py。
ok到此为止啦!