第5章〓图像分割 本章学习目标  理解图像分割的基本概念。  解析阈值分割的几种算法。  解析边缘检测的常用算子。  掌握霍夫变换的原理和函数。  掌握轮廓检测和绘制函数。  掌握区域分割的几种方法。  了解基于特定理论的图像分割方法。 本章首先对图像分割进行概述; 然后重点介绍了阈值分割、边缘分割和区域分割3种传统图像分割技术及利用Python语言实现分割效果; 最后介绍了几种基于特定理论的图像分割方法。 5.1图像分割概述 图像分割是把图像划分成若干具有独特性质的区域,并提出感兴趣目标的技术和过程。这些区域是互不相交的,分割后所得区域的总和应覆盖整个图像。具有独特性质是指满足像素的灰度、颜色、纹理等某种特征的相似性准则。具有同一性质的区域可以是单个区域,也可以是多个区域。 图像分割是图像分析过程中最重要的步骤之一,是图像识别和图像理解的前提。分割质量的好坏直接影响目标物特征提取和描述,关系到目标物识别、分类与理解及整个图像处理与分析系统的结果。 图像分割的应用非常广泛,几乎应用在有关图像处理的所有领域。例如,应用于医学领域,图像分割是病变区域提取、临床试验、特定组织测量以及实现三维重建的基础,可以帮助医生分析病情,进行组织器官的重建等; 应用于交通领域,可用于桥梁裂缝检测、智能车辆导航、红外行人检测等; 应用于军事领域,可以为军事目标的识别和跟踪提供特征参数,为导航与制导提供依据; 应用于服装领域,可从服装图像中快速提取服装款式信息、结构元素,提高款式设计和制版效率,促进服装行业智能化、一体化发展。遥感图像可以提供真实、丰富的地面信息和资料。应用于遥感图像,可以进行城乡的建设与规划、地图的绘制与更新、考古和旅游资源的开发、森林资源及环境的监测与管理、农产品长势的检测与产量估计、海岸区域的环境监测等。 图像分割算法发展几十年来,一直备受关注,借助各种理论,至今已提出了上千种各种类型的分割算法,但目前尚无普遍适用的最优分割算法,现有算法都是针对具体问题的。 图像分割方法主要分为以下几类: 基于阈值的分割方法、基于边缘的分割方法、基于区域的分割方法以及基于特定理论的分割方法等。 针对单色图像的分割算法,通常基于灰度值的两个基本性质,即不连续性和相似性。不连续性是基于灰度的不连续变化分割图像,如图像边缘; 相似性是依据一组预定义的准则将图像分割为相似区域,如阈值处理、区域生成、区域分裂合并等。这两类方法是互补的,有时需要结合使用,以求得更好的分割效果。 5.2阈值分割原理与实现 阈值分割是一种常用的传统图像分割方法,因其处理直观、实现简单、计算速度快、性能较稳定而成为图像分割中最基本和应用最广泛的分割技术。 阈值分割的基本思想是利用图像中灰度值的差异,通过把图像中每个像素的灰度值与设置阈值进行比较,实现对像素的划分。如果只设置一个阈值,就把像素分为两类,高于阈值的分为一类,其他的分为另一类,也就是将图像分成两部分,即前景区和背景区。这种对整幅图像采用统一的阈值进行分割的方法叫全局阈值分割,适用于目标与背景有较强对比的图像。分割后习惯上将图像设置为黑白两色,就是所谓的图像二值化。当阈值在一幅图像上不再单一而是有改变时,就称为可变阈值分割。可变阈值分割适合于较复杂的图像情况。 基于阈值的分割算法中最核心的问题是如何找到合适的阈值,这一步骤直接影响分割的准确性以及由此产生的图像描述、分析的正确性。 阈值分割的一般步骤可总结如下。 (1) 确定阈值。 (2) 图像中像素的灰度值和阈值做比较。 (3) 像素划分。 本节重点介绍全局阈值分割的常用方法,包括固定阈值法、直方图双峰法、最大类间方差法、迭代法、最大熵法等。可变阈值分割在本节中不做重点介绍,仅在最后介绍一种自适应法。 5.2.1固定阈值法 固定阈值法就是不使用相关算法去计算阈值,而是由用户自行设定一个阈值作为划分像素的依据。根据各像素点的灰度值与阈值的关系,将图像的灰度值分为两类。习惯上,一类灰度值为0,另一类灰度值为255,使整幅图像呈现出黑白效果。 OpenCV提供的阈值处理函数为ret,dst=cv2.threshold(src,thresh,maxval,type)。其中,src是源图像; thresh是进行分类的阈值; maxval是图像中像素的灰度值大于(或小于等于,根据type决定)阈值时赋予的新值(习惯上设置为255,可以不为255); type是一个方法选择参数,常用的方法有以下几个。 ① cv2.THRESH_BINARY(当前灰度值大于阈值时,设置为maxval; 否则设置为0)。 ② cv2.THRESH_BINARY_INV(当前灰度值大于阈值时,设置为0; 否则设置为maxval)。 ③ cv2.THRESH_TRUNC (当前灰度值大于阈值时,设置为阈值; 否则不改变)。 ④ cv2.THRESH_TOZERO(当前灰度值大于阈值时,不改变; 否则设置为0)。 ⑤ cv2.THRESH_TOZERO_INV(当前灰度值大于阈值时,设置为0; 否则不改变)。 该函数有两个返回值,第一个是输入的阈值ret,第二个是阈值化后的图像dst。 【例5.1】利用cv2.threshold()函数完成固定阈值法的阈值分割。 图5.1是对一幅灰度图像使用cv2.threshold()函数进行阈值分割,type分别取cv2.THRESH_BINARY和cv2.THRESH_BINARY_INV时图像效果图及直方图,此程序中阈值设置为100。从图中可见,灰度图像被划分成了黑白二值图像,cv2.THRESH_BINARY_INV是cv2.THRESH_BINARY的反相效果。 图5.1threshold()函数进行阈值分割 程序参考代码如下: import cv2#导入opencv库 import numpy as np #导入numpy库 import matplotlib.pyplot as plt #导入matplotlib库的pyplot模块 #以灰度模式读入原始图像 original=cv2.imread("flower.jpg",0) #threshold函数,type值不同 ret,dst1=cv2.threshold(original,100,255,cv2.THRESH_BINARY) ret,dst2=cv2.threshold(original,100,255,cv2.THRESH_BINARY_INV) #反相效果 #显示 plt.rcParams["font.sans-serif"]=["SimHei"] plt.rcParams.update({"font.size": 10}) titles1=["Original","BINARY","BINARY_INV"] titles2=["Original直方图","BINARY直方图","BINARY_INV直方图"] images=[original,dst1,dst2] plt.figure(figsize=(9,6)) #显示图像 for i in range(3): plt.subplot(2,3,i+1) plt.imshow(images[i],"gray") plt.title(titles1[i]) plt.axis("off") #显示直方图 for i in range(3,6): plt.subplot(2,3,i+1) a=np.array(images[i-3]).flatten() plt.hist(a,bins=64,color="blue") plt.title(titles2[i-3]) plt.xlim(0,255) plt.ylim(0,20000) plt.show() 有关type参数的其他效果参见实训4。 5.2.2直方图双峰法 图5.2具有双峰分布的直方图 1996年,Prewitt提出了直方图双峰法。该方法的基本思想是假设图像中有明显的目标和背景,则其灰度直方图呈双峰分布。当灰度级直方图具有双峰特性时,选取两峰之间的谷底对应的灰度级作为阈值, 如图5.2所示。该方法不适合直方图中双峰差别很大或双峰间的谷比较宽广且平坦的图像以及单峰直方图的情况。对于有多个峰值的直方图,可以选择多个阈值,要根据实际情况具体分析。 5.2.3最大类间方差法 最大类间方差法是由日本学者大津(Nobuyuki Otsu)于1979年提出,是一种不需人为设定其他参数,自动选择阈值的方法,也称为Otsu算法或大津法。该算法计算简单,不受图像亮度和对比度的影响,被认为是阈值分割算法中阈值选取的最佳方法。 最大类间方差法的基本思想是通过阈值将图像分为前景和背景两部分,当取最佳阈值时,前景和背景之间的差别应该是最大的。方差是灰度分布均匀性的一种度量,衡量差别的标准采用最大类间方差。类间方差越大,说明构成图像的前景和背景之间的差别越大。若某些像素被错分,都会导致两部分差别变小。当使用所取阈值进行阈值分割,使类间方差最大就意味着错分概率最小。该算法的缺点是对图像噪声敏感,只能针对单一目标分割,当目标和背景大小比例悬殊时效果不理想。 设图像f(x,y)的灰度范围是0,L-1,灰度值为i的像素数为ni,图像的总像素数为N,各灰度值出现的概率pi为 pi=niN,且有∑L-1i=0pi=1 阈值T将图像像素按灰度值与T的关系分为两部分,即C0和C1,C0由图像中灰度值在0,T内的所有像素组成,C1由灰度值在T+1,L-1内的所有像素组成,两部分的概率分别为P0和P1,即 P0=∑Ti=0pi,P1=∑L-1i=T+1pi=1-P0 两部分的平均灰度分别为μ0和μ1,整幅图像的总平均灰度为μ,有 μ0=1P0∑Ti=0ipi,μ1=1P1∑L-1i=T+1ipi μ=∑L-1i=0ipi=∑Ti=0ipi+∑L-1i=T+1ipi=P0μ0+P1μ1 两部分的类间方差g为 g=P0μ0-μ2+P1μ1-μ2 将μ的表达式代入上式,可得简化公式为 g=P0P1(μ0-μ1)2 T在0,L-1内依次取值,使g最大的值就是需要的阈值。 OpenCV提供的阈值处理函数cv2.threshold(src,thresh,maxval,type),type取THRESH_OTSU,thresh取0(屏蔽thresh)时,就会使用Otsu算法得到的阈值(函数第一个返回值)分割图像。type取THRESH_OTSU时也可以同时搭配THRESH_BINARY、THRESH_BINARY_INV、THRESH_TRUNC、THRESH_TOZERO及THRESH_TOZERO_INV使用。 【例5.2】利用cv2.threshold()函数完成Otsu算法的阈值分割。 图5.3是利用cv2.threshold()函数对一幅灰度图像用Otsu算法进行分割的效果图和直方图,并显示了分割阈值。 图5.3Otsu算法进行阈值分割 程序参考代码如下: import cv2 from matplotlib import pyplot as plt original=cv2.imread("boy.jpg",0) ret1,dst=cv2.threshold(original,0,255,cv2.THRESH_OTSU) plt.rcParams["font.sans-serif"]=["SimHei"] plt.figure(figsize=(9,3)) plt.subplot(131) plt.imshow(original,"gray") plt.title("原始图像") plt.xticks([]) plt.yticks([]) plt.subplot(132) plt.hist(original.ravel(),256)# original.ravel()将数组拉成一维数组 plt.title("直方图") plt.xlim(0,255) plt.ylim(0,10000) plt.subplot(133) plt.imshow(dst,"gray") plt.title("Otsu算法分割图像,阈值"+str(ret1)) plt.xticks([]) plt.yticks([]) plt.show() 如需得到分割后的反相效果,只需使用语句: ret1,dst=cv2.threshold(original,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU) 效果如图5.4所示。 图5.4Otsu算法进行阈值分割后反相效果 5.2.4迭代法 迭代法的基本思想是设定一个阈值T作为初始估计值,然后根据某种规则不断更新这一估计值,直到满足给定条件。迭代算法的优劣取决于迭代规则的确定。好的迭代规则既能快速收敛又能产生优于上次迭代的结果。迭代法适合直方图有明显波谷的图像。 下面是一种应用较广泛的迭代算法,步骤如下。 (1) 用图像的最大灰度值和最小灰度值的平均值作为初始估计阈值T。 (2) 将图像像素按灰度值与阈值T的关系分为两部分,即C0和C1,两部分的平均灰度分别为μ0和μ1。 (3) 计算新的阈值T,T=12(μ0+μ1)。 (4) 重复步骤(2)和(3),直到连续迭代中的T值间的差小于一个预定义参数为止。 【例5.3】利用迭代法对图像进行阈值分割。 图5.5是利用迭代法对图5.3所示的原始图像进行阈值分割的效果。 图5.5迭代法阈值分割效果 程序参考代码如下: import cv2 import numpy as np import matplotlib.pyplot as plt def iteration_threshold(gray): gray=np.array(gray).astype(np.float32) zmax=np.max(gray) zmin=np.min(gray) t0=(zmax+zmin)/2 #设置初始阈值 m,n=gray.shape fnum=0 #初始化前景像素个数 bnum=0 #初始化背景像素个数 fsum=0 #初始化前景像素灰度值的和 bsum=0 #初始化背景像素灰度值的和 for i in range(m): for j in range(n): tmp=gray[i][j] if tmp>t0: fnum=fnum+1 #前景像素的个数 fsum=fsum+int(tmp) #前景像素灰度值的总和 else: bnum=bnum+1 #背景像素的个数 bsum=bsum+int(tmp) #背景像素灰度值的总和 #计算前景和背景的平均灰度 zf=int(fsum/fnum) zb=int(bsum/bnum) t=(zf+zb)/2 #求出新的阈值 while(abs(t-t0)>1): #设置两次阈值差值不大于1 t0=t fnum=0 bnum=0 fsum=0 bsum=0 for i in range(m): for j in range(n): tmp=gray[i][j] if tmp>t0: fnum=fnum+1 fsum=fsum+int(tmp) else: bnum=bnum+1 bsum=bsum+int(tmp) zf=int(fsum/fnum) zb=int(bsum/bnum) t=(zf+zb)/2 return t original=cv2.imread("boy.jpg",0) threshold=iteration_threshold(original) ret,dst=cv2.threshold(original,threshold,255,cv2.THRESH_BINARY) #显示 plt.rcParams["font.sans-serif"]=["SimHei"] plt.subplot(121) plt.imshow(original,"gray") plt.title("原始图像") plt.xticks([]) plt.yticks([]) plt.subplot(122) plt.imshow(dst,"gray") plt.title("迭代法分割图像,阈值"+str(ret)) plt.xticks([]) plt.yticks([]) plt.show() 5.2.5最大熵法 熵是信息论中的一个术语,是所研究对象平均信息量的表征。假设离散随机变量x的概率分布是px,则其熵为 H(p)=-∑xpxlogp(x) 式中,log的底数可以取2、e或10。取不同的底数,熵的单位不一样。熵的定义使随机变量的不确定性得到了度量。熵越大,随机变量越不确定,也就是随机变量越随机,意味着添加的约束和假设越少,这时求出的分布越自然、偏差越小、越具有均匀分布。 最大熵算法就是找出一个最佳阈值把图像分成前景和背景两部分,使得两部分熵之和最大。从信息论角度来说,这样选择的阈值分割出的图像信息量最大,最不确定,分布偏差最小。 设图像f(x,y)的灰度范围是0,L-1,灰度值为i的像素数为ni,图像的总像素数为N,各灰度值出现的概率pi为 pi=niN,且有∑L-1i=0pi=1 以灰度值T分割图像,图像中不大于灰度值T的像素点构成背景B,高于灰度值T的像素点构成目标物体O,各灰度在本区的概率分布为 B区: pipT,i=0,1,2,…,T O区: pi1-pT,i=T+1,T+2,…,L-1 其中 pT=∑Ti=0pi 背景区域和目标区域的熵分别为 HB=-∑Ti=0pipTlgpipT HO=-∑L-1i=T+1pi1-pTlgpi1-pT 图像总熵为 H=HB+HO 使H最大的T值就是寻找的最佳阈值。 【例5.4】利用最大熵法对图像进行阈值分割。 图5.6是一幅灰度图像使用最大熵法实现阈值分割的效果图。 图5.6最大熵法阈值分割效果 程序参考代码如下: import cv2 import numpy as np import matplotlib.pyplot as plt def calentropy(T): #初始化 PT=0 #概率和 entropyB=0 #背景熵 entropyO=0 #前景熵 for i in range(T+1): PT+=p[i] #0~T灰度级的概率和 for i in range(256): if i<=T: if p[i]!=0: entropyB-=(p[i]/PT)*np.log10(p[i]/PT)#背景熵 else: if p[i]!=0: entropyO-=(p[i]/(1-PT))*np.log10(p[i]/(1-PT)) #前景熵 entropy=entropyB+entropyO #背景熵和前景熵的和 return entropy def maxentropy(image): Entropy=[] for i in range(256): entropy=calentropy(i) #将返回的熵赋给entropy Entropy.append(entropy) #各个entropy形成列表,列表索引为各灰度级 return Entropy.index(max(Entropy)) #Entropy列表中最大值的索引号就是所求阈值 original=cv2.imread("turtles.jpg",0) hist=cv2.calcHist([original],[0],None,[256],[0,255])#返回直方图各个灰度级的像素个数 m,n=original.shape sum=m*n p=[] for i in range(256): p.append(hist[i]/sum) #计算概率 threshold=maxentropy(original) ret,dst=cv2.threshold(original,threshold,255,cv2.THRESH_BINARY) #显示 plt.rcParams["font.sans-serif"]=["SimHei"] plt.figure(figsize=(6,3)) plt.subplot(121) plt.imshow(original,"gray") plt.title("原始图像") plt.xticks([]) plt.yticks([]) plt.subplot(122) plt.imshow(dst,"gray") plt.title("最大熵法分割图像,阈值"+str(ret)) plt.xticks([]) plt.yticks([]) plt.show() 5.2.6自适应法 在绝大多数情况下,目标和背景的对比度在图像中各处不是完全一样的,很难用一个统一的阈值将目标和背景区分开,这时往往需要通过控制阈值选取范围的方法实现局部分割阈值的选择。自适应法对于图像不同区域,能够自适应计算不同的阈值。 OpenCV提供dst=cv2.adaptiveThreshold(src, maxValue,adaptiveMethod,thresholdType,blockSize,C)函数,该函数可以通过计算某个邻域(局部)的均值、高斯加权平均来确定阈值。 ① src: 表示源图像,8位单通道图像。 ② dst: 表示输出图像,与源图像大小一致。 ③ maxValue: 表示大于(小于等于)阈值时赋予的新值。 ④ adaptiveMethod: 表示在一个邻域内计算阈值所采用的算法,有以下两个取值。  ADAPTIVE_THRESH_MEAN_C的计算方法是计算出邻域的平均值再减去常量C的值作为阈值;  ADAPTIVE_THRESH_GAUSSIAN_C的计算方法是计算出邻域的高斯均值再减去常量C的值作为阈值。 ⑤ thresholdType: 表示阈值类型,只有两个取值,即THRESH_BINARY 和THRESH_BINARY_INV。 ⑥ blockSize: 表示计算阈值的像素邻域大小,可取3、5、7等奇数,取值越大,结果越表现为阈值分割效果(可取21、31、41),取值越小,结果越表现为边缘检测效果。 ⑦ C: 表示偏移值调整量,值越大越能抑制噪声。用均值或高斯均值计算阈值后,再减这个值就是最终阈值。 【例5.5】利用cv2.adaptiveThreshold()函数体会自适应法进行阈值分割。 图5.7是一幅灰度图像使用自适应法实现阈值分割的效果图。 图5.7自适应法阈值分割效果 程序参考代码如下: import cv2 import Matplotlib.pyplot as plt original=cv2.imread("characters.jpg",0) dst=cv2.adaptiveThreshold(original,255,cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY,41,3) plt.rcParams["font.sans-serif"]=["SimHei"] plt.subplot(121) plt.imshow(original,"gray") plt.title("原始图像") plt.xticks([]) plt.yticks([]) plt.subplot(122) plt.imshow(dst,"gray") plt.title("自适应法分割图像") plt.xticks([]) plt.yticks([]) plt.show() 有关全局阈值分割(选用Otsu算法)与可变阈值分割(选用自适应法)对同一幅图像进行阈值分割的效果对比参见实训4。 5.3边缘分割原理与实现 边缘检测是图像分割的一种重要途径。图像边缘是图像最基本也是最重要的特征。边缘指图像局部特性不连续,总是以某种图像特征所对应的数值发生突变的形式出现,如灰度值、颜色分量、纹理结构的突变都会形成图像边缘,它标志着一个区域的终结和另一个区域的开始。根据灰度变换的特点,常见的边缘可分为阶跃型、斜坡型和屋顶型,如图5.8所示。 图5.8常见的边缘类型 图像边缘有方向和幅度两个属性,沿边缘方向像素变化平缓,垂直于边缘方向像素变化剧烈。边缘上的这种变化可以用微分算子检测出来,在第4章中曾讲过微分算子在图像锐化处理中的应用,本节将它应用于图像分割。基于微分算子的边缘检测是经典的边缘检测方法,通常用一阶或二阶导数来实现。一阶导数的最大值对应图像边缘,二阶导数以过零点对应图像边缘。在数字图像处理中,经证实,一阶导数和二阶导数具有以下性质: 一阶导数通常在图像中产生较粗的边缘; 二阶导数对精细细线,如细线、孤立点和噪声有较强的响应; 二阶导数在灰度斜坡和灰度台阶过渡处会产生双边缘响应; 二阶导数的符号可用于确定边缘的过渡是从亮到暗还是从暗到亮。由于边缘和噪声都是灰度不连续点,检测边缘的同时噪声会对其产生影响,因此用微分算子检测边缘前要对图像进行平滑滤波。 基于边缘检测的图像分割主要通过以下4步实现。 (1) 图像平滑。 对图像进行平滑处理以降低噪声,但是降低噪声的平滑能力越强,边缘强度的损失越大。这一步通过平滑滤波实现。 (2) 边缘点检测。 将邻域中灰度有显著变化的点突出显示,这些点是边缘点的候选者。这一步通过锐化滤波实现。 (3) 边缘判断。 根据具体情况,选择边缘点,去除没有意义的边缘点。实际工程中,这一步通过阈值化实现。 (4) 边缘连接。 前3步操作后得到的仅是边缘像素点,这些边缘像素点很少能完整地描绘实际的一条边缘,因此需要紧接着使用连接方法将这些边缘像素点形成有意义的边缘。 5.3.1常用边缘检测算子 目前常用的边缘检测算子主要有Roberts算子、Prewitt算子、Sobel算子、LoG算子和Canny算子等,其中Roberts算子、Prewitt算子和Sobel算子都是基于一阶导数的,都是梯度算子; LoG是基于二阶导数的算子。有关梯度算子的知识已在4.4.2小节空间邻域锐化中介绍过,本节简单回顾。 1. 梯度算子 图像在计算机中以数值矩阵的形式存在,形成了离散的数值信号。图像灰度函数f(x,y)在点(x,y)的梯度是一个具有方向和大小的矢量。梯度的方向就是函数f(x,y)最大变化率的方向,梯度幅值作为变化率大小的度量,可以反映图像灰度局部变化的强弱,因此可以作为检测边缘点的依据。 梯度表示为 f=gradf=gxgy=fxfy 式中,gx和gy分别为x方向和y方向的梯度。 梯度幅值为 Mx,y=magf=g2x+g2y=fx2+fy2 可以用下式近似表示,即 Mx,y≈gx+gy=fx+fy 梯度的方向角为(相对于x 轴而言) α(x,y)=arctangygx 将fx和fy用差分近似表示,图像的梯度幅值就可以通过对原始图像进行空间滤波求得。图5.9是一个3×3空间滤波器,滤波的过程就是求图像中各像素点的新像素值的过程,模板系数与被该模板覆盖区域的灰度值的乘积之和作为模板覆盖区域下图像中心点处的新灰度值。图5.10是一幅图像被模板覆盖的3×3区域,zk表示各灰度值,z5的新值=w1z1+w2z2+…+w9z9=∑9k=1wkzk。对原始图像进行滤波可分别求得gx和gy,通过Mx,y≈gx+|gy|,求得Mx,y。 常用的梯度算子如下。 (1) Roberts算子。 对于图5.10中的3×3区域,Roberts算子以求对角像素之差为基础,有 图5.93×3空间滤波器 图5.10图像的3×3区域 gx=fx=z9-z5 gy=fy=z8-z6 gx和gy分别使用模板-1001和0-110对图像滤波求得。 Roberts算子常用来处理具有陡峭边缘的低噪声图像,当边缘接近±45°时,处理效果更好,但其边缘定位较差,对噪声敏感,提取的边缘比较粗,常需对检测出的边缘图像做细化处理。 (2) Prewitt算子。 还是对于图5.10中的3×3区域,有 gx=fx=z7+z8+z9-z1+z2+z3 gy=fy=z3+z6+z9-z1+z4+z7 gx和gy分别使用模板-1-1-1000111水平和-101-101-101垂直对图像滤波求得。 这两种算子可通过调用OpenCV的filter2D()函数实现。该函数在4.4.2小节中已有介绍,此处不再赘述。 (3) Sobel算子。 同样对于图5.10中的3×3区域,有 gx=fx=z7+2z8+z9-z1+2z2+z3 gy=fy=z3+2z6+z9-z1+2z4+z7 gx和gy分别使用模板-1-2-1000121水平和-101-202-101垂直对图像滤波求得。 Sobel算子在OpenCV中用Sobel()函数实现。该函数在4.4.2小节中已有介绍,此处不再赘述。 Prewitt算子、Sobel算子因考虑到邻域信息,相当于对图像先做加权平滑处理,再做微分运算,因此对噪声有一定的抑制能力,适合具有较多噪声且灰度渐变图像的分割。 【例5.6】用梯度算子进行边缘检测。 图5.11是对一幅灰度图像用不同梯度算子进行边缘检测的效果图。处理时先进行高斯平滑; 然后进行边缘检测; 最后进行阈值处理。 图5.11不同梯度算子边缘检测效果 程序参考代码如下: import cv2 import numpy as np import matplotlib.pyplot as plt #读取图像并进行高斯平滑处理 original=cv2.imread("building.jpg",0) original1=cv2.GaussianBlur(original,(3,3),0) #Roberts算子 kernelxr=np.array([[-1,0],[0,1]]) kernelyr=np.array([[0,-1],[1,0]]) xr=cv2.filter2D(original1,cv2.CV_16S,kernelxr) yr=cv2.filter2D(original1,cv2.CV_16S,kernelyr) #转回CV_8U absXr=cv2.convertScaleAbs(xr) absYr=cv2.convertScaleAbs(yr) #融合 Roberts=cv2.addWeighted(absXr,0.5,absYr,0.5,0) #Prewitt算子 kernelxp=np.array([[-1,-1,-1],[0,0,0],[1,1,1]]) kernelyp=np.array([[-1,0,1],[-1,0,1],[-1,0,1]]) xp=cv2.filter2D(original1,cv2.CV_16S,kernelxp) yp=cv2.filter2D(original1,cv2.CV_16S,kernelyp) #转回CV_8U absXp=cv2.convertScaleAbs(xp) absYp=cv2.convertScaleAbs(yp) #融合 Prewitt=cv2.addWeighted(absXp,0.5,absYp,0.5,0) #Sobel算子 xs=cv2.Sobel(original1,cv2.CV_16S,1,0) ys=cv2.Sobel(original1,cv2.CV_16S,0,1) #转回CV_8U absXs=cv2.convertScaleAbs(xs) absYs=cv2.convertScaleAbs(ys) #融合 Sobel=cv2.addWeighted(absXs,0.5,absYs,0.5,0) #阈值化 Roberts_ret1,Roberts1=cv2.threshold(Roberts,0,255,cv2.THRESH_OTSU) Prewitt_ret1,Prewitt1=cv2.threshold(Prewitt,0,255,cv2.THRESH_OTSU) Sobel_ret1,Sobel1=cv2.threshold(Sobel,0,255,cv2.THRESH_OTSU) #显示 plt.rcParams["font.sans-serif"]=["SimHei"] titles=["原始图像","Roberts算子边缘检测","Prewitt算子边缘检测","Sobel算子边缘检测"] images=[original,Roberts1,Prewitt1,Sobel1] plt.figure(figsize=(14,3)) for i in range(4): plt.subplot(1,4,i+1) plt.imshow(images[i],"gray") plt.title(titles[i]) plt.axis("off") plt.show() 2. LoG算子 图像函数f(x,y)的拉普拉斯算子是根据数字图像中阶跃型边缘点对应二阶导数的过零点而设计的一种与方向无关的边缘检测算子,定义为 2f=2fx2+2fy2 其中 2fx2=fx+1,y+fx-1,y-2fx,y 2fy2=fx,y+1+fx,y-1-2fx,y 故 2fx,y=fx+1,y+fx-1,y+fx,y+1+fx,y-1-4fx,y 拉普拉斯算子的缺点是对噪声非常敏感,而且会产生双边缘,因此一般不用其原始形式作边缘检测。 针对拉普拉斯算子抗噪声能力差的缺点,Marr和Hildreth提出先对图像进行高斯平滑,再用拉普拉斯算子进行边缘检测的改进方法。这一过程可表示为 2[Gx,y★f(x,y)] 式中: ★表示卷积; f(x,y)为图像; Gx,y为高斯函数,即 Gx,y=12πσ2e-x2+y22σ2 σ是标准差,直接影响检测结果。一般来说σ越大,抗噪声能力越强,但是会导致一些变化细微的边缘检测不到。 线性系统中,卷积与微分的次序可以交换,先卷积后微分和先微分后卷积是相等的,所以 2[Gx,y★f(x,y)]=2Gx,y★f(x,y) 即先对高斯算子做拉普拉斯变换,得到2Gx,y,再与图像卷积,等同于先对图像进行高斯平滑,再用拉普拉斯算子进行边缘检测。 2Gx,y=2Gx,yx2+2Gx,yy2 =12πσ4x2σ2-1e-x2+y22σ2+12πσ4y2σ2-1e-x2+y22σ2 =-1πσ41-x2+y22σ2e-x2+y22σ2 将2Gx,y称为LoG(Laplacian of Gaussian,高斯拉普拉斯)算子。 LoG算子形如草帽,也称墨西哥草帽算子,00-1000-1-2-10-1-216-2-10-1-2-1000-100(实践中,用该模板的负模板)是一个对LoG算子近似的5×5模板,这种近似并不是唯一的。任意尺寸的模板可以通过对2Gx,y取样,并标定系数以使系数之和为零来生成。LoG算子本质的形状是: 一个正的中心项由紧邻的负区域包围着,中心项的值以距原点的距离为函数而增大,而外层区域的值为零。系数之和为零,以便在灰度级恒定区域中模板的响应为零。在应用LoG算子时,标准差参数σ的值对边缘检测效果有很大的影响,图像不同,选择的参数值不同。 LoG是一个对称滤波器,所以使用相关或卷积的空间滤波将产生相同的结果。关于LoG的参考文献都采用卷积说法,为保持一致,也使用卷积来表示线性滤波。 在具体实现时,直接用LoG算子与图像卷积或先高斯平滑图像,再求平滑后图像的拉普拉斯这两种方法都是可以的,但是后者要进行两次卷积,图像较大时一般不建议采用。 另外,Scipy库的ndimage子模块专门用于图像处理,提供了gaussian_laplace( )函数用于进行LoG边缘检测。 【例5.7】用LoG算子进行边缘检测。 图5.12是对一幅灰度图像用LoG算子进行边缘检测的效果图。本例先进行高斯平滑; 然后用拉普拉斯算子进行边缘点检测; 最后进行阈值处理。阈值处理时以LoG边缘点图像最大灰度值的50%为阈值,保留了大多数的主要边缘点,过滤掉了一些“无关”的特征。 图5.12LoG算子边缘检测效果 程序参考代码如下: import cv2 import matplotlib.pyplot as plt #读取图像并进行高斯平滑处理 original=cv2.imread("building.jpg",0) original1=cv2.GaussianBlur(original,(3,3),0) #拉普拉斯算子 Laplace=cv2.Laplacian(original1,cv2.CV_16S,ksize=3) #转回原来的uint8形式; 否则将无法显示图像,而只是一幅灰色的窗口图像 LoG=cv2.convertScaleAbs(Laplace) #阈值化 m,n=LoG.shape max=0 for i in range(m): for j in range(n): if LoG[i][j]>max: max=LoG[i][j] ret,LoG1=cv2.threshold(LoG,max*0.5,255,cv2.THRESH_BINARY) #显示 plt.rcParams["font.sans-serif"]=["SimHei"] titles=["原始图像","LoG算子得到的边缘点图像","LoG算子边缘检测"] images=[original,LoG,LoG1] plt.figure(figsize=(12,3)) for i in range(3): plt.subplot(1,3,i+1) plt.imshow(images[i],"gray") plt.title(titles[i]) plt.xticks([]) plt.yticks([]) plt.show() 3. Canny算子 Canny边缘检测算法是一种多级检测算法,它在抑制噪声和边缘精确定位之间取得了较好的平衡,是边缘检测中最经典、最先进的算法之一。它基于3个基本目标。 (1) 低错误率。 检测算法应该尽可能检测出图像的真实边缘,尽可能减少漏检和误检。 (2) 最优定位。 检测的边缘点应该精确地定位于边缘的中心。 (3) 单边缘响应。 对于同一边缘要有尽可能低的响应次数,即对单边缘最好只有一个响应。 Canny边缘检测算法较为复杂,主要通过以下4步实现。 (1) 高斯滤波。 用高斯滤波器平滑图像,以降低错误率。 (2) 计算图像的梯度幅值和方向。 用一阶偏导的有限差分计算梯度的幅值和方向。 (3) 非极大值抑制。 为确定边缘,需将非局部极大值点置零以得到细化的边缘。 (4) 双阈值筛选边缘。 使用两个阈值T1和T2(T10): #取出第一个种子 currentPoint=seedList.pop(0) #检测种子点8邻域内满足生长准则的情况 for i in range(8): tmpX=currentPoint[0]+connects[i][0] tmpY=currentPoint[1]+connects[i][1] #如果出边界就检测下一点 if tmpX<0 or tmpX>=height or tmpY<0 or tmpY>=weight: continue #没出边界的就检测是否满足生长准则并且没被标记过 if abs(int(img[currentPoint[0],currentPoint[1]])-int(img[tmpX,tmpY]))< threshold and seedMark[tmpX,tmpY]==0: #满足条件的设为白色并将其作为种子加入到检测列表中 seedMark[tmpX,tmpY]=255 seedList.append((tmpX,tmpY)) return seedMark original=cv2.imread("lung window.jpg",0) #初始种子坐标 seeds=[(180,100),(300,370)] #调用regionGrow()函数 threshold=7 neighbourhood=1 result=regionGrow(original,seeds,threshold,neighbourhood) #显示 plt.rcParams["font.sans-serif"]=["SimHei"] titles=["原始图像","区域生长结果图像"] images=[original,result] plt.figure(figsize=(14,6)) for i in range(2): plt.subplot(1,2,i+1) plt.imshow(images[i],"gray") plt.title(titles[i]) plt.xticks([]) plt.yticks([]) plt.show() 5.4.2区域分裂合并法 区域分裂合并法的基本思想是,先确定一个分裂合并的相似性准则,然后通过分裂将不同特征的区域分离,再通过合并将相同特征的区域并到一起,从而实现图像的分割。 算法的具体过程为: 令R表示整幅图像区域,P表示某种相似性准则。假设以像素的灰度值为特征,制定相似性准则P为灰度值差小于阈值T。如果 P(R)=False,即区域R不满足相似性准则,就将图像等分为4个区域。分别检测分得的4个区域的P(Ri)值,如果某个区域的值是False,就将该区域再次分为4个区域,如此不断继续下去。直到对任何区域Ri,有P(Ri)=True,表示区域Ri已经满足相似性准则或是已经为单个像素,此时分裂结束。该过程可以用四叉树形式表示,树的根对应于整幅图像区域,每个节点对应于划分的子区域,如图5.20所示,图中只对R2进行了细分。分裂后,如果有满足相似性准则的邻近区域,即P(Rj∪Rk)=True时,两个相邻的区域Rj和Rk就进行合并。合并的区域可以大小不同。当无法再进行合并时操作停止。 图5.20四叉树算法示意图 总结,区域分裂合并法主要有以下4个步骤。 (1) 制定相似性准则P。 (2) 分裂: 满足P(Ri)=False的任何区域Ri,分裂为4个相等的不相交区域,直到不能进一步分裂为止。 (3) 合并: 对满足条件的P(Rj∪Rk)=True的任意两个相邻的区域Rj和Rk进行合并,直到无法进一步合并为止。 (4) 结束。 图5.21是一个分裂合并的实例。假设相似性准则P为区域内灰度值相等。对于整幅图像,如图5.21(a)所示,有黑色和白色,灰度值不相等,不满足相似性准则,进行第一次分裂,把图像等分成了4个区域,如图5.21(b)所示; 分得的4个区域,每个区域都不满足灰度值相等这一准则,所以每一区域又都等分为4个区域,如图5.21(c)所示; 对前述分得的区域,发现只有右侧中间两个区域不满足相似性准则,需要继续分裂,其余都满足,都不需要再进行分裂,如图5.21(d)所示; 对不满足相似性准则的两个区域4等分之后,发现所有区域都满足相似性准则了,则分裂结束。接下来开始进行合并,对满足相似性准则的相邻区域进行合并,得到图像分割的结果,如图5.21(e)所示。 图5.21区域分列合并实例 区域分裂合并算法的难点在于分裂与合并的准则不易确定,选用合适的准则P对于提高图像分割质量十分重要。目前,均方误差最小、F检测等是最常用的准则。 5.4.3分水岭算法 分水岭算法是一种基于拓扑理论的数学形态学的分割方法,它根据分水岭的构成来考虑图像的分割。分水岭算法把图像看作测地学上的拓扑地貌,图像中每一点像素的灰度值表示该点的海拔高度,每个局部极小值及其影响区域称为集水盆,分水岭就是集水盆的边界形成的,对应图像边缘。分水岭变换可以保证分割区域的连续性和封闭性。 下面采用模拟浸入过程来说明分水岭的概念和形成。在每一个局部极小值表面,打一个小孔,然后让模型慢慢浸入水中,随着浸入的加深,水淹没极小值及周围的区域,各个极小值及其影响域就是相应的集水盆,两个集水盆相遇的界限,就是分水岭。 图5.22显示了形成分水岭的过程。图5.22(a)是水刚刚从一个极小值处涌出,在第一个集水盆慢慢上升,图5.22(d)是其俯视图。图5.22(b)是水涌出到一定程度,在两个集水盆中上升,图5.22(e)是其俯视图。图5.22(c)是两个集水盆将连未连时的截面图,图5.22(f)是其俯视图。此时两个集水盆的边界就是分水岭,就是希望得到的原始图像的边缘。 图5.22分水岭形成示意图 传统的分水岭算法对噪声等非常敏感,图像中的噪声点或其他干扰因素等的存在,常产生过度分割现象。过度分割就是原本完整的物体被分割成很多细小的区域,从而导致分割后的图像不能将图像中有意义的区域表示出来。 为了减少过度分割造成的影响,OpenCV采用了一个基于掩模的分水岭算法,使用cv2.watershed(img,markers)函数实现。img表示输入8位3通道图像; markers参数表示与原始图像大小相同的标记图,用于标记图像不同区域,每个区域有一个自己唯一的编号,确定区域的编号用正整数值1、2、3、……分别表示,作为后续区域分割的种子,而不确定的区域用0表示。在使用分水岭函数之前,必须先通过一定的操作得到第二个参数markers,它是OpenCV分水岭算法的关键。函数依据种子信息,对图像上的像素点根据分水岭算法规则进行判断,并对每个像素点的区域归属进行划定,直到处理完图像上所有像素点。函数运行后,返回值是一个与输入大小相同的图像,其中灰度值为-1的像素点就是想要的边界。 利用OpenCV分水岭算法实现图像自动分割的步骤如下。 (1) 加载原始图像。 (2) 阈值分割,将图像分割为黑白两部分。 (3) 对图像进行开运算,即先腐蚀再膨胀(简单理解腐蚀就是使目标区域“变小”,膨胀就是使目标区域“变大”),目的是消除噪声。 (4) 对上一步的结果进行膨胀,得到确定背景区域。 (5) 通过距离变换 cv2.distanceTransform( )函数,获取确定前景区域。距离变换的结果是一幅灰度级图像,即距离图像,图像中每个像素的灰度值为该像素与距其最近的背景像素间的距离。由于目标中心像素距离背景最远,所以值最大,就最亮。再通过设定合理的阈值对距离变换后的图像进行二值化处理,就可获得确定前景区域。 (6) 背景区域和前景区域相减,得到不确定是背景还是前景的区域,即未知区域,要求的分水岭就在这一区域。 (7) 得到OpenCV分水岭函数需要的markers。OpenCV分水岭函数需要的markers对确定分类的区域(无论是前景还是背景)使用不同的正整数标记,对不确定的区域使用0标记。而利用cv2.connectedComponents( )函数得到的标记图会把背景标记为 0,其他的对象用从 1 开始的正整数标记,所以要通过操作将其转换为OpenCV分水岭函数需要的markers的标记形式。 图5.23分水岭算法效果 (8) 最后使用分水岭函数cv2.watershed(img,markers)。返回图像中灰度值为-1的像素点就是所求边界。 【例5.13】利用分水岭算法实现图像分割。 图5.23是对一幅彩色图像利用分水岭算法实现图像分割的效果图。图中用红色标出了图像中两部分的分割线。 程序参考代码如下: import cv2 import numpy as np original=cv2.imread("landscape.jpg") #转换为灰度图 gray=cv2.cvtColor(original,cv2.COLOR_BGR2GRAY) #阈值分割 ret,dst=cv2.threshold(gray,0,255,cv2.THRESH_OTSU) #对图像进行"开运算"(先腐蚀再膨胀),迭代3次,目的是消除噪声 kernel=np.ones((3,3),np.uint8) opening=cv2.morphologyEx(dst,cv2.MORPH_OPEN,kernel,iterations=3) #对"开运算"的结果进行膨胀,得到确定背景区域 sure_bg=cv2.dilate(opening,kernel,iterations=5) #通过distanceTransform,得到确定前景区域 dist_transform=cv2.distanceTransform(opening,cv2.DIST_L2,5) ret,sure_fg=cv2.threshold(dist_transform,0.2*dist_transform.max(),255,cv2.THRESH_BINARY) #用sure_bg与sure_fg相减,得到未知区域,边界就在这一区域 sure_fg=np.uint8(sure_fg) unknow=cv2.subtract(sure_bg,sure_fg) #得到OpenCV分水岭函数需要的markers ret,markers_connect=cv2.connectedComponents(sure_fg,connectivity=8) markers_OpenCV=markers_connect+1 #转换为OpenCV分水岭函数使用的markers形式 markers_OpenCV[unknow==255]=0 #调用分水岭函数 markers_watershed=cv2.watershed(original,markers_OpenCV) original[markers_watershed==-1]=[0,0,255] #markers_watershed中灰度值为-1的像素点即边界,标为红色 #显示效果图 cv2.imshow("Result",original) cv2.waitKey(0) cv2.destroyAllWindows()#销毁所有窗口 也可设置提取目标,如想要提取图5.23中的草地和树木,效果图及参考代码见实训6。 5.5基于特定理论的图像分割 随着各学科新理论和新方法的提出,出现了与一些特定理论、方法相结合的图像分割方法,主要有基于聚类分析的图像分割方法、基于模糊集理论的图像分割方法、基于神经网络的图像分割方法、基于图论的图像分割方法、基于遗传算法的图像分割方法、基于小波变换的图像分割方法等。 5.5.1基于聚类分析的图像分割方法 聚类分析是数据挖掘的一个重要研究领域。在众多的分割算法中,基于聚类分析的图像分割方法是图像分割领域中一类极其重要和应用相当广泛的算法。长久以来,专家学者们关于聚类算法及应用的探讨从未间断。聚类算法是将一组分布未知的数据进行分类,使得同一类中的数据相似度尽可能大,而不同类的数据相似度尽可能小。聚类算法主要分为两大类,即硬聚类和软聚类。硬聚类是指数据集中每个样本只能划分到一个簇的算法,最具代表性的如K均值(KMeans)算法。软聚类也称模糊聚类,指算法可以将一个样本划分到一个或者多个簇。常见的算法是模糊C均值(Fuzzy CMeans,FCM)算法。软聚类建立了样本对类别的不确定描述,更能真实反映客观世界,成为聚类分析的主流。 K均值算法的基本思想是以空间中的K个点为中心进行聚类,将空间中的每个点归类到离自己距离最近的中心所在的类,形成K个类。采用迭代方式,逐次更新各聚类中心的值,直至得到最好的聚类结果。最终实现各聚类本身尽可能紧凑,而各聚类之间尽可能分开。具体步骤如下。 (1) 从n个数据对象任选K个对象作为初始聚类中心。 (2) 对于所剩其他对象,根据它们与这些聚类中心的距离,分别将它们归类给与其距离最近的聚类。 (3) 计算每个所获新聚类的聚类中心(新聚类中所有点的坐标平均值)。 (4) 如果新的聚类中心与老的聚类中心的距离小于某一阈值,就表示新的聚类中心位置变化不大,收敛稳定,认为聚类已达到了期望的结果,算法终止。 (5) 否则就认为新的聚类中心与老的聚类中心变化很大,就继续迭代步骤(2)~(4)。 该算法的优点是原理简单、实现容易; 处理大数据集时非常高效且伸缩性较好。缺点主要是K值难以估计; 不同的初始聚类中心可能导致完全不同的聚类结果; 结果只能保证局部最优; 对于离群点和孤立点敏感; 对于非凸数据集或类别规模差异太大的数据效果不好; 需样本存在均值(限定数据种类)等。 模糊C均值算法是在模糊数学基础上对K均值算法的推广,是一种柔性的模糊划分。它不像K均值聚类那样非此即彼地认为每个点只能属于某一类,而是赋予每个点一个对各类的隶属度,通过隶属度值大小将样本点归类。具体操作时通过不断迭代优化各点与所有类中心相似性的目标函数,得到每个数据点对所有类中心的隶属度,用隶属度获取局部极小值,确定每个数据点属于哪个聚类,从而得到最优聚类。 模糊C均值算法因算法简单、收敛速度快、能处理大数据集、解决问题范围广、易于应用计算机实现等特点,在图像分割领域得到广泛应用。但在应用FCM时,聚类个数和模糊指数的选取至关重要,只有选取正确才能得到好的聚类效果。 5.5.2基于模糊集理论的图像分割方法 模糊集合论作为描述和处理具有不确定性(即模糊性)事物和现象的一种数学手段,更接近人类真实思维和决策,更符合客观实际,非常适合处理图像分割问题。这是由于人的视觉特性和数字图像本身所具有的不确定性使得图像分割问题是典型的结构不良问题,将模糊集理论应用于图像分割是针对图像不确定性的非常有效的方法,能得到更好的分割效果。 模糊技术在图像分割中常常和现有的许多图像分割技术相结合,形成基于模糊理论的图像分割方法,主要包括模糊阈值分割方法、模糊聚类分割方法、模糊神经网络分割方法等。 模糊阈值分割法通过计算图像的模糊率或模糊熵来选取图像分割阈值。模糊程度由模糊率函数确定,当模糊率最低时,分割效果最好。其中模糊率与隶属函数相关,模糊数学的基本思想是隶属度的思想。应用模糊数学方法建立数学模型的关键是建立符合实际的隶属函数。利用不同的S形隶属度函数来定义模糊目标,通过优化过程最后选择一个具有最小模糊率的S形函数。用该函数增强目标及属于该目标的像素之间的关系,这样得到的S形函数的交叉点为阈值分割需要的阈值。传统的模糊熵阈值分割通过计算图像的最大模糊熵确定阈值。然而,将图像分为黑白两色很多时候并不能很好地区分这两色内有区别的目标,这时就需要多阈值分割。 5.5.3基于神经网络的图像分割方法 人工神经网络简称为神经网络或连接模型,是对人脑或自然神经网络若干基本特性的抽象和模拟。人工神经网络以对大脑的生理研究成果为基础,其目的在于模拟大脑的某些机理与机制,实现某个方面的功能。近年来,人工神经网络识别技术引起广泛关注,并应用于图像分割。基于神经网络的图像分割方法的基本思想是通过训练多层感知机得到线性决策函数,然后用决策函数对像素进行分类来达到分割图像的目的。神经网络存在巨量连接,容易引入空间信息,解决了图像中的噪声和不均匀问题。 基于神经网络的分割算法主要有BP(Back Propagation)神经网络算法、RBF(Radial Basis Function)神经网络算法、Hopfield神经网络算法等。 5.5.4基于图论的图像分割方法 图论理论能够充分考虑图像像素之间的空间位置关系,可以兼顾边界和区域两方面的信息,减少图像离散化造成的误差,因而能够得到良好的分割效果。近年来,基于图论的图像分割方法受到广泛关注。该方法的基本思想是将图像表示成图论中的网络图,利用图论的相关特性并结合有关技术,通过对网络图进行一系列操作和计算处理,完成对图像的分割。网络图和图像之间有很好的对应关系,网络图的节点对应图像的像素,网络图的边对应图像中像素的相邻性,网络图边的权对应图像中相邻像素的关系,可以表示相邻两个像素的相似性,也可表示相邻两个像素间边的连接强度。图论的相关特性与图像分割具有某种共性,因此可以基于图论的特性对图像进行分割。图论中节点间的最短路径可完成类似基于边缘的图像分割,图论中的最小分割可以实现类似基于区域的图像分割效果。 5.5.5基于遗传算法的图像分割方法 遗传算法又叫基因算法,是模拟达尔文生物进化论的自然选择和遗传学机理的生物进化过程的计算模型,是一种通过模拟自然进化过程搜索最优解的方法,具有简单、实用性强、稳定性好、适用于并行处理等显著特点,在很多领域得到广泛应用,成功地解决了各种类型的优化问题。在分割复杂图像时,人们往往采用多参量进行信息融合,在多参量参与最优值的求取过程中,优化计算是最重要的。把自然进化的特征应用到计算机算法中,能解决很多困难。遗传算法的出现为解决这类问题提供了有效的方法,它不仅可以得到全局最优解,而且大大缩短了计算时间。 5.5.6基于小波变换的图像分割方法 小波变换是一种时、频两域分析工具,它在时间域和频率域都具有良好的局部特性,能有效地从信号中提取信息。小波变换具有多分辨率(也叫多尺度)特性,能够在不同分辨率上对信号进行分析,因此在图像处理和分析等许多方面得到应用。 基于小波变换的图像阈值分割方法的基本思想是首先由二进制小波变换将图像的直方图分解为不同层次的小波系数; 然后依据给定的分割准则和小波系数选择阈值; 最后利用阈值标出图像分割的区域。基于小波变换的边缘检测图像分割方法的基本思想是取小波函数作为平滑函数的一阶导数或二阶导数,利用信号的小波变换的模值在信号突变点处取局部极大值或过零点的性质来提取信号的边缘点。 实训4阈值分割 (1) 通过编写程序体会OpenCV提供的阈值处理函数cv2.threshold( )的type参数,观察type参数不同时得到的图像效果。type分别取cv2.THRESH_BINARY、cv2.THRESH_BINARY_INV、cv2.THRESH_TRUNC、cv2.THRESH_TOZERO和cv2.THRESH_TOZERO_INV。效果如图5.24所示。 图5.24threshold()函数的type参数取不同值时的效果图和直方图 程序参考代码如下: import cv2#导入opencv库 import numpy as np #导入numpy库 import matplotlib.pyplot as plt #导入matplotlib库的pyplot模块 from matplotlib.colors import NoNorm #以灰度模式读入原始图像 original=cv2.imread("pig.jpg",0) #threshold函数,type值不同 ret,dst1=cv2.threshold(original,170,255,cv2.THRESH_BINARY) ret,dst2=cv2.threshold(original,170,255,cv2.THRESH_BINARY_INV) ret,dst3=cv2.threshold(original,170,255,cv2.THRESH_TRUNC) ret,dst4=cv2.threshold(original,170,255,cv2.THRESH_TOZERO) ret,dst5=cv2.threshold(original,170,255,cv2.THRESH_TOZERO_INV) #显示 plt.rcParams["font.sans-serif"]=["SimHei"] plt.rcParams.update({"font.size": 8}) titles1=["Original","BINARY","BINARY_INV","TRUNC","TOZERO","TOZERO_INV"] titles2=["Original直方图","BINARY直方图","BINARY_INV直方图","TRUNC直方图","TOZERO直方图","TOZERO_INV直方图"] images=[original,dst1,dst2,dst3,dst4,dst5] plt.figure(figsize=(18,6)) #显示图像 for i in range(6): plt.subplot(2,6,i+1) plt.imshow(images[i],"gray",norm=NoNorm()) plt.title(titles1[i]) plt.axis("off") #显示直方图 for i in range(6,12): plt.subplot(2,6,i+1) a=np.array(images[i-6]).flatten() plt.hist(a,bins=64,color="blue") plt.title(titles2[i-6]) plt.xlim(0,255) plt.ylim(0,20000) plt.show() (2) 利用Otsu算法和自适应法对同一图像进行阈值分割,体会全局阈值分割和可变阈值分割的效果对比。 Otsu算法进行阈值分割时,只有一个阈值,把图像中每个像素的灰度值与该阈值进行比较,这样往往会过滤掉很多信息,导致细节不明显。而自适应法对每一个像素点单独计算阈值,即每个像素点的阈值都是不同的,就是将该像素点周围blockSize* blockSize区域内的像素取均值或高斯加权平均,然后减去一个常数C,从而得到该点的阈值,这样能避免整张图像亮度分布不均,能保留更多的细节性信息。效果如图5.25所示。 图5.25Otsu算法和自适应法阈值分割效果对比 程序参考代码如下: import cv2 import matplotlib.pyplot as plt original=cv2.imread("basket.jpg",0) ret1,dst1=cv2.threshold(original,0,255,cv2.THRESH_OTSU+cv2.THRESH_BINARY_INV) dst2=cv2.adaptiveThreshold(original,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C,cv2.THRESH_BINARY_INV,41,16) #显示 plt.rcParams["font.sans-serif"]=["SimHei"] image=[original,dst1,dst2] title=["原始图像","Otsu算法阈值分割后","自适应法阈值分割后"] plt.figure(figsize=(14,4)) for i in range(3): plt.subplot(1,3,i+1) plt.imshow(image[i],"gray") plt.title(title[i]) plt.axis("off") plt.show() 实训5边缘分割 利用累计概率霍夫变换函数检测彩色图像building1.jpg中的线段。 图5.26是检测后的效果。 图5.26累计概率霍夫变换效果 程序参考代码如下: import cv2 import numpy as np import matplotlib.pyplot as plt #读入原始图像 original=cv2.imread("building-1.jpg") #复制原始图像 original_hough=original.copy() #将原始图像转为灰度图 gray=cv2.cvtColor(original,cv2.COLOR_BGR2GRAY) #Canny算子进行边缘检测 edges=cv2.Canny(gray,50,150) #复制检测结果图 edges_hough=edges.copy() #进行累计概率霍夫变换 lines=cv2.HoughLinesP(edges,1,np.pi/180,200,minLineLength=200,maxLineGap=10) for line in lines: x1,y1,x2,y2=line[0] cv2.line(original_hough,(x1,y1),(x2,y2),(0,255,0),4)#在原始图像副本上用绿色4像素画检测到的线段 cv2.line(edges_hough,(x1,y1),(x2,y2),(255,255,255),4) #显示 plt.figure(figsize=(14,4)) plt.rcParams["font.sans-serif"]=["SimHei"] #显示原始图像及画线段后的副本 plt.subplot(141),plt.imshow(cv2.cvtColor(original,cv2.COLOR_BGR2RGB)),plt.title("原始图像") plt.xticks([]),plt.yticks([]) plt.subplot(142),plt.imshow(cv2.cvtColor(original_hough,cv2.COLOR_BGR2RGB)),plt.title("检测到的线段画到原始图像后") plt.xticks([]),plt.yticks([]) #显示Canny算子边缘检测结果图及画线段后的副本 titles=["Canny算子边缘检测结果图","检测到的线段画到Canny算子边缘检测结果图上"] images=[edges,edges_hough] for i in range(2): plt.subplot(1,4,i+3) plt.imshow(images[i],"gray") plt.title(titles[i]) plt.xticks([]) plt.yticks([]) plt.show() 实训6区域分割 利用分水岭算法实现图像中目标的提取。原始图像是一幅风景图,假设要提取草地和树木,只需先使用分水岭算法分割图像,对于非目标区域用其他颜色填充即可。程序代码与利用分水岭算法实现图像分割基本相同,只需在最后利用original[markers_watershed==2]=[255,255,255]语句将背景区设成白色或其他颜色即可,效果如图5.27所示。 图5.27分水岭算法提取目标 程序参考代码如下: import cv2 import numpy as np import matplotlib.pyplot as plt original=cv2.imread("landscape.jpg") original_copy=original.copy() #转换为灰度图 gray=cv2.cvtColor(original,cv2.COLOR_BGR2GRAY) #阈值分割 ret,dst=cv2.threshold(gray,0,255,cv2.THRESH_OTSU) #对图像进行"开运算"(先腐蚀再膨胀),迭代3次,目的是消除噪声 kernel=np.ones((3,3),np.uint8) opening=cv2.morphologyEx(dst,cv2.MORPH_OPEN,kernel,iterations=3) #对"开运算"的结果进行膨胀,得到确定背景区域 sure_bg=cv2.dilate(opening,kernel,iterations=5) #通过distanceTransform得到确定前景区域 dist_transform=cv2.distanceTransform(opening,cv2.DIST_L2,5) ret,sure_fg=cv2.threshold(dist_transform,0.2*dist_transform.max(),255,cv2.THRESH_BINARY) #用sure_bg与sure_fg相减,得到未知区域,边界就在这一区域 sure_fg=np.uint8(sure_fg) unknow=cv2.subtract(sure_bg,sure_fg) #得到OpenCV分水岭函数需要的markers ret,markers_connect=cv2.connectedComponents(sure_fg,connectivity=8) markers_OpenCV=markers_connect+1 #转换为OpenCV分水岭函数使用的markers形式 markers_OpenCV[unknow==255]=0 #调用分水岭函数 markers_watershed=cv2.watershed(original,markers_OpenCV) original[markers_watershed==2]=[255,255,255] #显示 plt.figure(figsize=(12,4)) plt.rcParams["font.sans-serif"]=["SimHei"] titles=["原始图像","提取目标后的图像"] images=[original_copy,original] for i in range(2): plt.subplot(1,2,i+1) plt.imshow(cv2.cvtColor(images[i],cv2.COLOR_BGR2RGB)) plt.title(titles[i]) plt.xticks([]) plt.yticks([]) plt.show()