第十五节、韦伯局部描述符(WLD,附源码)
纹理作为一种重要的视觉线索,是图像中普遍存在而又难以描述的特征,图像的纹理特征一般是指图像上地物重复排列造成的灰度值有规则的分布。纹理特征的关键在于纹理特征的提取方法。目前,用于纹理特征提取的方法有很多,最具有代表性的是有基于二阶概率密度的灰度共生矩阵、符合人眼视觉特性的小波变换、纹理谱法以及基于图像结构基元的纹理元方法等。
为了更有效地描述图像局部纹理特征,又先后提取了局部二值模式(Local Binary Pattern,LBP)和韦伯局部描述符(Weber Local Descriptor,WLD),这两种方法都是基于邻域像素点间的灰度变化特征来描述图像纹理的。 因两者易于理解、便于计算且具有较好的局部特征描述能力而被广泛地应用于纹理分类 、目标检测、人脸识别 、合成孔径雷达(Synthetic Aperture Radar,SAR) 图像索引 、指纹活性检测 、图像伪造检测等众多领域中。
一 WLD原理
韦伯定律是反映心理量和物理量之间关系的定律,它表明能够引起感觉差异的差别阈值与原始刺激的强度之比是一个常量,即:
$$\frac{ΔI}{I}=k$$
其中$k$是一个常量;$ΔI$表示差别阈值;$I$表示原始刺激的强度。由此可以推知,刺激的变化所引起的感觉差异不仅与刺激变化的大小有关,还与原始刺激的强度有关。局部图像描述符WLB就是根据该定律提出的,它包含两个算子:差分激励算子和方向算子。WLD计算除边缘像素点外的每个像素点的差分激励和方向,并以其二维分布直方图来联合表征图像的纹理特征。
1、差分激励算子
差分激励反映局部窗内灰度变化的强度信息。通过计算局部窗内邻域像素点与中心像素点间的灰度差值和与中心像素点灰度值的比值$G_{ratio}(x_c)$,再利用反正切变换将分布在$[-P,+∞]$范围内的$G_{ratio}(x_c)$映射到区间$(-\frac{\pi}{2},\frac{\pi}{2})$内得到,差分激励$ξ(x_c)$的计算式为:
$$ξ(x_c)=arctan(G_{ratio}(x_c))=arctan(\sum\limits_{i=0}^{P-1}\frac{(x_i-x_c)}{x_c})$$
其中,$x_c$和$x_i,i=0,1,...,P-1$分别表示中心像素点和邻域像素点的灰度值;$P$表示邻域像素点个数。
2、方向算子
方向反映局部窗内灰度变化的空间分布信息。通过局部窗内水平方向与垂直方向上邻域像素点的灰度差值比值的反正切变换来描述。 其计算式为:
$$Φ(x_c)=arctan(\frac{D_V}{D_H})$$
其中,$D_H$和$D_V$分别表示水平方向上和垂直方向上中心像素点两侧的邻域像素点间的灰度差异,若对于$3\times3$像素的局部窗口如下图所示,则$D_H=x_7-x_3$,$D_V=x_5-x_1$。
为了能够更加有效的区分局部窗口的灰度分布变换,进一步将方向由$Φ(x_c)\in(-\frac{\pi}{2},\frac{\pi}{2})$变换到了$Φ'(x_c)\in[0,2\pi]$,其变换公式为:
$$Φ'(x_c)=\begin{cases} Φ(x_c) \qquad D_H>0,D_V>0 \\ \pi+Φ(x_c) \qquad D_H<0,D_V>0 \\\pi+Φ(x_c) \qquad D_H<0,D_V<0 \\ 2\pi+Φ(x_c) \qquad D_H>0,D_V<0 \end{cases}$$
3、WLD直方图
WLD采用均匀量化技术,将方向$Φ'(x_c)$均匀地量化为$T$个方向,将差分激励均匀地划分为$M$个频段,分别对应于图像中的高频、中频和低频变化,再将划分的每个频段上将差分激励均匀地量化为$S$格,形成一个$T\times{C}=T\times(M\times{S})$的二维直方图,并通过编码将其转化为一维向量用于表示图像的纹理特征。
二维直方图如上图所示,横轴表示方向,纵轴表示差分激励。每个小矩形表示在该方向下所在差分激励区间像素的数量,数量不同,颜色不同。
下图是WLD直方图得到的过程:
1、在每个主方向上得到差分激励子直方图,得到$H(0)$至$H(T-1)$;
2、将$H(k)$分成$M$个子区间,即$l_m,m=0,...,M-1$,将$H(k)$中的${l_m}$对应放置在$t=k$和$m=i$处;
3、将$m=i$的一行子直方图拼接成一个直方图,即$H_i$;
4、将$H_i$组合成一个直方图,即WLD直方图。
然后可以把WLD直方图用于后续分类。
4、缺点
我们来看下图所示的3个局部灰度分布示例。从3个局部窗内的灰度分布看,(a)~(c)分别表示了高频、中频和低频3种变换。按照WLD的计算方法,他们的$ΔI$都等于0,即差分激励等于0,而且,在垂直方向上的灰度值差值也为0,表明方向也等于0.这意味着,WLD将无法区分这3个纹理模式。
存在上述问题的主要原因有:
- WLD的差分激励算子是利用各向同性的边缘检测滤波器———拉普拉斯算子统计局部窗内$P$个邻域像素点与中心像素点间的灰度差值之和$ΔI$,导致了灰度变化的正负差值相互抵消,换言之,局部窗内的灰度变化信息没有充分体现;
- WLD 的方向算子仅表达了水平方向和垂直方向上邻域像素点间灰度变化梯度的空间分布方位,不能充分反映局部窗内灰度变化的空间分布结构信息,难以体现纹理的内在变化特征;
5、改进
纹理特征是指与空间分布相关的图像灰度等级的变化。 这意味着灰度图像的纹理既与各像素点间灰度变化的梯度幅值有关,也与其梯度的空间分布密切相关,两者是有机的一体。 针对WLD 存在的问题,提出一种基于正负梯度改进的 WLD(WLD-PNG)。
其核心思想是:
基于局部窗内邻域像素点与中心像素点间灰度变化的正负梯度信息,分离计算正、负差分激励以保留灰度等级的变化特征;
利用 LBP 模式提取正负梯度分布的结构信息,以反映灰度等级变化的空间分布特征。 最后,采用均匀量化和编码技术,将两者有机联合建立图像的纹理特征。
二 代码实现
由于opencv没有提供WLD算法的实现,但是从网上我们可以找到基于Weber local descriptors的人脸识别的代码和基于OPENCV的WLD特征提取程序,我们作为参考,修改自己的代码如下:
# -*- coding: utf-8 -*- """ Created on Sat Sep 1 19:08:10 2018 @author: zy """ ''' WLD 韦伯局部描述符 ''' import cv2 import numpy as np class WLD(object): def __init__(self): #差分激励由中心像素与周围像素强度的差异以及中心像素强度组成,分别由f1和f2滤波器得出。 self.__f1 = np.array([[1,1,1], [1,-8,1], [1,1,1]]) self.__f2 = np.array([[0,0,0], [0,1,0], [0,0,0]]) #方向反映局部窗内灰度变化的空间分布信息。通过局部窗内水平方向与垂直方向上邻域像素点 #的灰度差值比值的反正切变换来描述。 方向分为竖直方向和水平方向,由f3和f4滤波器得出。 self.__f3 = np.array([[0,-1,0], [0,0,0], [0,1,0]]) self.__f4 = np.array([[0,0,0], [1,0,-1], [0,0,0]]) #方向量化个数 如果修改,也需要修改__classify_fai函数 self.__T = 12 #差分激励量化个数 如果修改,也需要修改__classify_epc函数 self.__M= 8 #每个频段上将差分激励均匀地量化为S格 可以修改 self.__S = 4 def calc_hist(self,image,concatenate=True): ''' 输出统计直方图 形状[M,T,S]或[MxTxS,] args: image:输入灰度图 concatenate:表明输出直方图是否合并为一维 ''' his = np.zeros((self.__M,self.__T,self.__S),np.float32) rows,cols = image.shape[:2] #计算像素点个数 sum_pix = rows*cols epc,theta = self.__compute(image) for i in range(rows): for j in range(cols): #把差分激励分为8个区间,分别对应一个类别 m = self.__classify_epc(epc[i][j]) t = theta[i][j] s = self.__classify_s(epc[i][j],m) his[m-1][t-1][s-1] += 1 #归一化 his /= sum_pix if concatenate: his = np.reshape(his,-1) return his def __compute(self,image): ''' 计算每个像素点的差分激励和方向 ''' rows,cols = image.shape[:2] #用于保存每个像素点对应的差分激励算子 epc = np.zeros((rows,cols),dtype=np.float32) #用于保存每个像素点对应的方向算子 theta = np.zeros((rows,cols),dtype=np.float32) ''' 计算差分激励ξ ''' v1 = cv2.filter2D(image,cv2.CV_16SC1,self.__f1) v2 = cv2.filter2D(image,cv2.CV_16SC1,self.__f2) for i in range(rows): for j in range(cols): #-π/2~π/2 epc[i][j] = np.arctan(v1[i][j]/(v2[i][j]+0.0001)) ''' 计算每个像素点的方向Φ,把方向[0,2pi]均匀地量化为12个区间 ''' v3 = cv2.filter2D(image,cv2.CV_16SC1,self.__f3) v4 = cv2.filter2D(image,cv2.CV_16SC1,self.__f4) for i in range(rows): for j in range(cols): theta[i][j] = np.arctan(v3[i][j]/((v4[i][j])+0.0001)) if v3[i][j]>0 and v4[i][j]>0: pass elif v3[i][j]>0 and v4[i][j]<0: theta[i][j] = theta[i][j]+2*np.pi else: theta[i][j]=theta[i][j]+np.pi theta[i][j] = self.__classify_fai(theta[i][j]) theta = theta.astype(np.uint8) return epc,theta def __classify_fai(self,value): ''' 把方向值value分类 类别为1、2、3、4、5、6、7、8、9、10、11、12 args: value:数值 0~2π之间 ''' if value >= 0 and value < 0.15*np.pi: return 1 elif value >= np.pi*0.15 and value < 0.35*np.pi: return 2 elif value >= np.pi*0.35 and value < 0.5*np.pi: return 3 elif value >= np.pi*0.5 and value < 0.65*np.pi: return 4 elif value >= 0.65*np.pi and value < 0.85*np.pi: return 5 elif value >= np.pi*0.85 and value < np.pi: return 6 elif value >= np.pi and value < 1.15*np.pi: return 7 elif value >= 1.15*np.pi and value < 1.35*np.pi: return 8 elif value >= np.pi*1.35 and value < 1.5*np.pi: return 9 elif value >= 1.5*np.pi and value < 1.65*np.pi: return 10 elif value >= 1.65*np.pi and value < 1.85*np.pi: return 11 else: return 12 def __classify_epc(self,value): ''' 把差分激励值value分类,划分为8个区间 类别为1、2、3、4、5、6、7、8 args: value:数值 -π/2~π/2之间 ''' if value >= np.pi*(-0.5) and value < (-0.3)*np.pi: return 1 elif value >= np.pi*(-0.3) and value < (-0.15)*np.pi: return 2 elif value >= np.pi*(-0.15) and value < (-0.05)*np.pi: return 3 elif value >= np.pi*(-0.05) and value < 0: return 4 elif value >= 0 and value < 0.05*np.pi: return 5 elif value >= np.pi*0.05 and value < 0.15*np.pi: return 6 elif value >= np.pi*0.15 and value < 0.3*np.pi: return 7 else: return 8 def __classify_s(self,value,label): ''' 将每个区间的差分激励再次划分为S格 args: value:差分激励值 -π/2~π/2之间 label:当前所属区间 1,2,3,...,8 ''' if label == 1: space = ((-0.3)*np.pi - (-0.5)*np.pi)/self.__S return int((value - (-0.5)*np.pi)/space)+1 elif label == 2: space = ((-0.15)*np.pi - (-0.3)*np.pi)/self.__S return int((value - (-0.3)*np.pi)/space)+1 elif label == 3: space = ((-0.05)*np.pi - (-0.15)*np.pi)/self.__S return int((value - (-0.15)*np.pi)/space)+1 elif label == 4: space = 0-(-0.05)*np.pi/self.__S return int((value - (-0.05)*np.pi)/space)+1 elif label == 5: space = 0.05*np.pi /self.__S return int(value /space)+1 elif label == 6: space = (0.15*np.pi - 0.05*np.pi)/self.__S return int((value - 0.05*np.pi)/space)+1 elif label == 7: space = (0.3*np.pi - 0.15*np.pi)/self.__S return int((value - 0.15*np.pi)/space)+1 else: space = (0.5*np.pi - 0.3*np.pi)/self.__S n = int((value - 0.3*np.pi)/space)+1 if n == self.__S+1: n = self.__S return n def draw_hist(hist): ''' 绘制直方图 ''' #首先先创建一个黑底的图像,为了可以显示彩色,所以该绘制图像是一个8位的3通道图像 #图像高为100,宽为直方图的长度 width = len(hist) height = 100 draw_image = np.zeros((height,width,3),dtype= np.uint8) #获取最大值 max_value = np.max(hist) #数值量化 value = np.asarray((hist*0.9/max_value*height),dtype=np.int8) for i in range(width): cv2.line(draw_image,(i,height-1),(i,height-1-value[i]),(255,0,0)) #显示 cv2.imshow('hist',draw_image) cv2.waitKey(0) cv2.destroyAllWindows() if __name__=='__main__': image = cv2.imread('./image/match1.jpg') image = cv2.resize(image,dsize=(600,400)) imgray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) wld = WLD() hist = wld.calc_hist(imgray) print(hist.shape) print(hist) print(np.sum(hist)) draw_hist(hist)
运行结果如下:
三 代码下载
参考文章