在图像识别中,如果可以将图像感兴趣的物体或区别分割出来,无疑可以增加我们图像识别的准确率,传统的数字图像处理中的分割方法多数基于灰度值的两个基本性质
由于阈值处理直观、实现简单且计算速度快,因此图像阈值处理在图像分割应用中处于核心地位
下图灰度直方图对应图像f(x,y),然后f(x,y)>T的任何点(x,y)称为一个对象点;否则将该点称为背景点。分割后的图像g(x,y):
g ( x , y ) = { 1 f ( x , y ) > T 0 f ( x , y ) ≤ T g(x,y)=\begin{cases} 1 & f(x,y)>T \\ 0& f(x,y)\leq T \end{cases} g(x,y)={10f(x,y)>Tf(x,y)≤T
当T时一个适用于整个图像的常数时,该公式给出的处理称为全局阈值处理。
但是要求两个以上阈值的分割问题很难解决(通常是不可能的),而较好的结果通常可以用其他方法得到。
我们可以很自然的得出灰度阈值的成功与否直接关系到可区分的直方图模式的谷的宽度和深度,而影响波谷特性的关键因素是:
在大多数应用中,通常图像之间有较大的变化,即使全局阈是一种合适的方法,对每一幅图像有能力自动估计阈值的算法也是需要的。下面的迭代算法可以用于这一目的:
阈值处理可视为一种统计决策理论问题,其目的是在把像素分配给两个或多组(也称分类)的过程中引入的平均误差最小。Otsu方法(Otsu[1979])是另一种有吸引力的方案。
Otsu方法有一个重要的特性,即它完全以在一幅图像的直方图上执行计算为基础。
一幅图像有MxN个像素,L个不同的灰度级, n i n_i ni表示灰度级为i的像素个数。那么图像中像素总数MN为 M N = n 0 + n 1 + ⋯ + n L − 1 MN=n_0+n_1+\dots+n_{L-1} MN=n0+n1+⋯+nL−1
归一化的直方图 p i = n i / M N p_i=n_i/MN pi=ni/MN,由此有
∑ i = 0 L − 1 p i = 1 , p i ≥ 0 \sum_{i=0}^{L-1}p_i=1, p_i \geq 0 i=0∑L−1pi=1,pi≥0
现在,我们假设选择一个阈值 T ( k ) = k , 0 < k < L − 1 T(k)=k, 0< k < L-1 T(k)=k,0<k<L−1,并使用它把输入图像阈值化处理为两类 C 1 C_1 C1和 C 2 C_2 C2,其中, C 1 C_1 C1由图像中灰度值在范围[0,k]内的所有像素组成, C 2 C_2 C2由灰度值在范围[k+1,L-1]内的所有像素组成。用该阈值,像素被分到类 C 1 C_1 C1中的概率 P 1 ( k ) P_1(k) P1(k)由如下的积累和给出:
P 1 ( k ) = ∑ i = 0 k p i P_1(k)=\sum_{i=0}^k p_i P1(k)=i=0∑kpi
因此分配到 C 1 C_1 C1的像素的平均灰度值为:
m 1 ( k ) = ∑ i = 0 k i P ( i / C 1 ) m_1(k)=\sum_{i=0}^kiP(i/C_1) m1(k)=i=0∑kiP(i/C1)
= ∑ i = 0 k i P ( C 1 / i ) P ( i ) / P ( C 1 ) =\sum_{i=0}^kiP(C_1/i)P(i)/P(C_1) =i=0∑kiP(C1/i)P(i)/P(C1)
= 1 P 1 ( k ) ∑ i = 0 k i P ( i ) =\frac{1}{P_1(k)}\sum_{i=0}^kiP(i) =P1(k)1i=0∑kiP(i)
第二行来自贝叶斯公式:
P ( A / B ) = P ( B / A ) P ( A ) / P ( B ) P(A/B)=P(B/A)P(A)/P(B) P(A/B)=P(B/A)P(A)/P(B)
类似的我们也可以得到 C 2 C_2 C2的像素平均灰度值
然后我们可以得到整个图像的平均灰度值
m G = ∑ i = 0 L − 1 i P ( i ) m_G=\sum_{i=0}^{L-1}iP(i) mG=i=0∑L−1iP(i)
为了评价阈值K的“质量”,我们使用归一化的无量纲矩阵
η = σ B 2 σ G 2 \eta =\frac{\sigma^2_B}{\sigma^2_G} η=σG2σB2
其中, σ G 2 \sigma_G^2 σG2是全局方差[即图像中所有像素的灰度方差]
σ G 2 = ∑ i = 0 L − 1 ( i − m G ) 2 p i \sigma_G^2=\sum_{i=0}^{L-1}(i-mG)^2p_i σG2=i=0∑L−1(i−mG)2pi
σ B 2 \sigma_B^2 σB2为类间方差,它定义为
σ B 2 = P 1 ( m 1 − m G ) 2 + P 2 ( m 2 − m G ) 2 \sigma_B^2=P_1(m_1-m_G)^2+P_2(m_2-m_G)^2 σB2=P1(m1−mG)2+P2(m2−mG)2
该表达式还可写为
σ B 2 = P 1 P 2 ( m 1 − m 2 ) 2 \sigma_B^2=P_1P_2(m_1-m_2)^2 σB2=P1P2(m1−m2)2
= ( m G P 1 − m ) 2 P 1 ( 1 − P 1 ) =\frac{(m_GP_1-m)^2}{P_1(1-P_1)} =P1(1−P1)(mGP1−m)2
从上面公式可以看出,两个均值 m 1 m_1 m1和 m 2 m_2 m2彼此隔得越远, σ B 2 \sigma_B^2 σB2越大,这表明类间方差是类之间的可分性度量。因为 σ G 2 \sigma_G^2 σG2是一个常数,由此得出 η \eta η也是一个可分性度量,且最大化这一度量等价于最大化 σ B 2 \sigma_B^2 σB2。
一旦得到可以使 σ B 2 ( k ∗ ) \sigma_B^2(k^*) σB2(k∗)最大的 k ∗ k^* k∗,便可以得到分割图像:
g ( x , y ) = { 1 f ( x , y ) > k ∗ 0 f ( x , y ) ≤ k ∗ g(x,y)=\begin{cases} 1 & f(x,y)>k^* \\ 0& f(x,y)\leq k^* \end{cases} g(x,y)={10f(x,y)>k∗f(x,y)≤k∗
Otsu算法小结如下:
OpenCV同样将这一经典算法封装成了函数
#coding:utf-8
import cv2
import numpy as np
from matplotlib import pyplot as plt
image = cv2.imread("images/gamma.jpg")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
plt.subplot(131), plt.imshow(image, "gray")
plt.title("source image"), plt.xticks([]), plt.yticks([])
plt.subplot(132), plt.hist(image.ravel(), 256)
plt.title("Histogram"), plt.xticks([]), plt.yticks([])
ret1, th1 = cv2.threshold(gray, 0, 255, cv2.THRESH_OTSU) #方法选择为THRESH_OTSU
plt.subplot(133), plt.imshow(th1, "gray")
plt.title("OTSU,threshold is " + str(ret1)), plt.xticks([]), plt.yticks([])
plt.show()
噪声会将简单的阈值处理问题变为不可解决的问题。当噪声不能在源头减少,并且阈值处理又是所选择的分割方法时,通常能增强性能的一种技术是,在阈值处理之前平滑图像。
经平滑和分割后的图像,由于对边界的模糊,会造成物体和背景间的边界稍微有点失真。对一幅图像平滑越多,分割后的结果中的边界误差就越大。
f(x,y) 表示输入图像,利用边缘改进全局阈值处理算法如下:
区域生长的基本方法是从一组“种子”点开始,将于种子预先定义的性质相似的那些领域像素添加到每个种子上来形成这些生长区域(如特定范围的灰度或颜色)
令f(x,y)表示一个输入图像阵列;S(x,y)表示一个种子陈列,陈列中种子点位置处为1,其他位置处为0;Q表示在每个位置(x,y)所用的属性。假设陈列f和s的尺寸相同。基于8连接的一个基本区域生长算法可以说明如下:
import numpy as np
import cv2
class Point(object):
def __init__(self,x,y):
self.x = x
self.y = y
def getX(self):
return self.x
def getY(self):
return self.y
def getGrayDiff(img,currentPoint,tmpPoint):
return abs(int(img[currentPoint.x,currentPoint.y]) - int(img[tmpPoint.x,tmpPoint.y]))
def selectConnects(p):
if p != 0:
connects = [Point(-1, -1), Point(0, -1), Point(1, -1), Point(1, 0), Point(1, 1), \
Point(0, 1), Point(-1, 1), Point(-1, 0)]
else:
connects = [ Point(0, -1), Point(1, 0),Point(0, 1), Point(-1, 0)]
return connects
def regionGrow(img,seeds,thresh,p = 1):
height, weight = img.shape
seedMark = np.zeros(img.shape)
seedList = []
for seed in seeds:
seedList.append(seed)
label = 1
connects = selectConnects(p)
while(len(seedList)>0):
currentPoint = seedList.pop(0)
seedMark[currentPoint.x,currentPoint.y] = label
for i in range(8):
tmpX = currentPoint.x + connects[i].x
tmpY = currentPoint.y + connects[i].y
if tmpX < 0 or tmpY < 0 or tmpX >= height or tmpY >= weight:
continue
grayDiff = getGrayDiff(img,currentPoint,Point(tmpX,tmpY))
if grayDiff < thresh and seedMark[tmpX,tmpY] == 0:
seedMark[tmpX,tmpY] = label
seedList.append(Point(tmpX,tmpY))
return seedMark
img = cv2.imread('images/gamma.jpg',0)
seeds = [Point(10,10),Point(82,150),Point(20,300)]
binaryImg = regionGrow(img,seeds,10)
cv2.imshow('img',img)
cv2.imshow(' ',binaryImg)
cv2.waitKey(0)
这种方法是将一幅图像细分为一组任意不相交区域,然后聚合和/或分裂这些区域。
可总结为:
形态学分水岭分割将前面讨论的分割方法中的许多概念进行了具体化,因此通常会产生更稳定的分割结果,包括连接的分割边界。
分水岭的概念是以三维方式来形象化一幅图像为基础的:两个空间坐标作为灰度的函数,如图2所示。在这种“地形学”解释中,我们考虑三种类型的点:
令 M 1 , M 2 , … , M R M_1,M_2,\dots,M_R M1,M2,…,MR是地图图像 g ( x , y ) g(x,y) g(x,y)中区域最小值点的坐标集。令 C ( M i ) C(M_i) C(Mi)是与区域最小值 M i M_i Mi相关的汇水盆地中的点的坐标集,符号 m i n min min和 m a x max max表示 g ( x , y ) g(x,y) g(x,y)的最小值和最大值,最后令 T [ n ] T[n] T[n]表示满足 g ( s , t ) < n g(s,t)<n g(s,t)<n的坐标(s,t)的集合,即:
KaTeX parse error: Expected 'EOF', got '\right' at position 6: T[n]=\̲r̲i̲g̲h̲t̲\{(s,t)|g(s,t)<…
令 C n ( M i ) C_n(M_i) Cn(Mi)表示汇水盆地中与淹没阶段n的最小值 M i M_i Mi相关联的点的坐标集,即:
C n ( M i ) = C ( M i ) ⋂ T [ n ] C_n(M_i)=C(M_i)\bigcap T[n] Cn(Mi)=C(Mi)⋂T[n]
接下来,令 C [ n ] C[n] C[n]表示阶段n中已被水淹没的汇水盆地的并集:
C [ n ] = ⋃ i = 1 R C n ( M i ) C[n]=\bigcup_{i=1}^RC_n(M_i) C[n]=i=1⋃RCn(Mi)
然后,令 C [ m a x + 1 ] C[max+1] C[max+1]表示所有汇水盆地的并集:
C [ m a x + 1 ] = ⋃ i = 1 R C ( M i ) C[max+1]=\bigcup_{i=1}^RC(M_i) C[max+1]=i=1⋃RC(Mi)
寻找分水先的算法使用 C [ m i n + 1 ] = T [ m i n + 1 ] C[min+1]=T[min+1] C[min+1]=T[min+1]来初始化,然后,该算法进行递归处理,由 C [ n − 1 ] C[n-1] C[n−1]计算 C [ n − 1 ] C[n-1] C[n−1]计算 C [ n ] C[n] C[n]
由 C [ n − 1 ] C[n-1] C[n−1]求得 C [ n ] C[n] C[n]过程如下:令Q表示 T [ n ] T[n] T[n]中的连通分量的集合,然后对于每个连通分量 q ∈ Q [ n ] q\in Q[n] q∈Q[n],有如下三种可能性:
由 C [ n − 1 ] C[n−1] C[n−1]构建$ C[n] 取 决 于 这 三 个 条 件 中 的 哪 个 条 件 成 立 。 遇 到 一 个 新 的 最 小 值 时 , 条 件 1 发 生 , 这 种 情 况 下 , 连 通 分 量 取决于这三个条件中的哪个条件成立。 遇到一个新的最小值时,条件1发生,这种情况下,连通分量 取决于这三个条件中的哪个条件成立。遇到一个新的最小值时,条件1发生,这种情况下,连通分量 q $并入 C [ n − 1 ] C[n−1] C[n−1]中形成$ C[n] 。 当 q 位 于 某 些 局 部 最 小 值 的 汇 水 盆 地 内 时 , 条 件 2 发 生 , 这 种 情 况 下 , q 并 入 。 当 q 位于某些局部最小值的汇水盆地内时,条件2发生,这种情况下,q 并入 。当q位于某些局部最小值的汇水盆地内时,条件2发生,这种情况下,q并入 C[n−1]$中形成 $C[n] $。
当遇到全部或部分分隔两个或多个汇水盆地的山脊线时,条件3发生。进一步淹没会导致这些汇水盆地中的水位聚合。因此,必须在 q 内构筑一个水坝(如果涉及两个以上的汇水盆地,就要构筑多个水坝)以阻止汇水盆地间的水溢出。
import numpy as np
import cv2
from matplotlib import pyplot as plt
img = cv2.imread('images/1.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
dist_transform = cv2.distanceTransform(opening,cv2.DIST_L2,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, 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 = cv2.watershed(img,markers)
img[markers == -1] = [255,0,0]
cv2.imshow('img',img)
cv2.waitKey(0)
cv2.destroyAllWindows()