本文以实验为导向,使用vgg16+GLCM实现一场精彩的新冠肺炎的分类识别,并且对比不加GLCM后的效果。在这之前,我们需要弄明白一些前缀知识和概念问题:
GLCM(Gray-Level Co-occurrence Matrix),中文称为灰度共生矩阵,是一种用于图像纹理特征提取的统计方法。它是由Tamura等人在1978年首次提出的,用于描述图像中灰度级别之间的相互关系。GLCM在图像处理和计算机视觉领域中广泛应用,特别是在纹理分析、目标识别和图像分类等任务中作为单独特征提取的一环,并与CNN结合达到非凡的效果。
灰度级别:是指图像中每个像素的灰度值的取值范围。在数字图像中,每个像素的灰度值表示了该像素在灰度图像中的亮度程度。灰度级别通常用整数表示,其取值范围取决于图像的位深度。在一个8位灰度图像中,灰度级别的范围为0到255,其中0表示最暗的黑色,255表示最亮的白色。这意味着该图像中每个像素的灰度值可以取256个不同的值,这些值之间以等间距分布。对于一个黑白图像,每个像素只有一个灰度值,而对于彩色图像,每个像素则有多个通道,每个通道都有自己的灰度级别范围。
一、GLCM算法原理🍉
1.1 GLCM计算原理🎈
共生矩阵用两个位置的像素的联合概率密度来定义,它不仅反映亮度的分布特性,也反映具有同样亮度或接近亮度的象素之间的位置分布特性,是有关图象亮度变化的二阶统计特征。它是定义一组纹理特征的基础。一幅图象的灰度共生矩阵能反映出图象灰度关于方向、相邻间隔、变化幅度的综合信息,它是分析图象的局部模式和它们排列规则的基础。
设为一幅二维数字图像,其大小为M × N,灰度级别为,则满足一定空间关系的灰度共生矩阵为:
{(x1,y1),(x2,y2)∈M×N|f(x1,y1)=i, f(x2,y2)=j}
其中表示集合x中的元素个数,显然P为Ng×Ng的矩阵,若(x1,y1)与(x2,y2)间距离为d,两者与坐标横轴的夹角为θ,则可以得到各种间距及角度的灰度共生矩阵:
- i:为f(x,y)中灰度值为i的元素位置列表;
- j:为f(x,y)中灰度值为j的元素位置列表;
- d:为(x1,y1)(x2,y2)之间的距离;
- θ:为(x1,y1)(x2,y2)之间连线与横坐标的夹角;
最后我们遍历i,j列表,找出和(d,θ)所描述的空间位置一样的一对位置(x1,y1)和(x2,y2)有多少个,从而得到P(i,j)的值。也就是说,这其实不是最终形态的灰度共生矩阵,真正的灰度共生矩阵应该是一个大小和一样的,但它可以从中得到:
假设我们令(d=1,θ=0°), 以P(x,y,1,0°)点为例:
- P(1,1,1,0°)= 1 ,即GLCM(1,1),说明左侧原图只有一对灰度为1的像素水平相邻。
- P(1,2,1,0°)= 2, 即GLCM(1,2),说明左侧原图有两对灰度为1和2的像素水平相邻。
根据不同的(d, θ)组合,其实我们可以得到不同纹理的灰度共生矩阵,在GLCM(灰度共生矩阵)中,不同的距离和角度意味着在图像中计算纹理特征时考虑的像素之间的相对位置和方向,从而影响到后续纹理特征的提取。
-
距离(Distance): 距离指的是在GLCM中计算像素对共现频率时像素之间的间隔距离。通常,距离会影响纹理特征的大小和尺度。较小的距离可以捕捉图像中更细小的纹理细节,而较大的距离则能够考虑更广阔的像素关系,提取出更大范围的纹理特征。选择合适的距离取决于应用场景和所关注的纹理细节大小。
-
角度(Angle): 角度是指在GLCM中计算像素对共现频率时相对于水平方向的偏转角度。常用的角度通常是0°、45°、90°和135°。不同的角度能够捕捉图像中不同方向的纹理特征。例如,0°角度对应着水平方向的纹理,能够检测到图像中水平的纹理结构;45°和135°角度对应着对角线方向的纹理,能够检测到图像中的对角线纹理结构;90°角度对应着垂直方向的纹理,能够检测到图像中垂直的纹理结构。因此,选择不同的角度可以从不同方向上提取纹理信息。
在实际应用中,通常会对多个距离和角度进行计算,得到多个GLCM,然后结合这些GLCM来计算一系列的纹理特征,如对比度、能量、相关性等。通过考虑不同的距离和角度,可以全面地捕捉图像中的纹理信息,使得GLCM在纹理分析、图像分类和目标识别等任务中发挥出更强大的能力。
1.2 GLCM纹理特征提取原理🎈
纹理特征提取的一种有效方法是以灰度级的空间相关矩阵即共生矩阵为基础的,因为图像中相距(Δx,Δy)的两个灰度像素同时出现的联合频率分布可以用灰度共生矩阵来表示。若将图像的灰度级定为N级,那么共生矩阵为N×N矩阵,可表示为M(Δx,Δy)(h,k),其中位于(h,k)的元素m(h,k)的值表示一个灰度为h而另一个灰度为k的两个相距为(Δx,Δy)的像素对出现的次数。
对粗纹理的区域,其灰度共生矩阵的m(h,k)值较集中于主对角线附近。因为对于粗纹理,像素对趋于具有相同的灰度。而对于细纹理的区域,其灰度共生矩阵中的mhk值则散布在各处。
为了能更直观地以共生矩阵描述纹理状况,从共生矩阵导出一些反映矩阵状况的参数,典型的有以下几种:
(1)能量:是灰度共生矩阵元素值的平方和,所以也称能量,反映了图像灰度分布均匀程度和纹理粗细度。如果共生矩阵的所有值均相等,则ASM值小;相反,如果其中一些值大而其它值小,则ASM值大。当共生矩阵中元素集中分布时,此时ASM值大。ASM值大表明一种较均一和规则变化的纹理模式。
(2)对比度:反映了图像的清晰度和纹理沟纹深浅的程度。纹理沟纹越深,其对比度越大,视觉效果越清晰;反之,对比度小,则沟纹浅,效果模糊。灰度差即对比度大的象素对越多,这个值越大。灰度公生矩阵中远离对角线的元素值越大,CON越大。
(3)相关:它度量空间灰度共生矩阵元素在行或列方向上的相似程度,因此,相关值大小反映了图像中局部灰度相关性。当矩阵元素值均匀相等时,相关值就大;相反,如果矩阵像元值相差很大则相关值小。如果图像中有水平方向纹理,则水平方向矩阵的COR大于其余矩阵的COR值。
(4)熵:是图像所具有的信息量的度量,纹理信息也属于图像的信息,是一个随机性的度量,当共生矩阵中所有元素有最大的随机性、空间共生矩阵中所有值几乎相等时,共生矩阵中元素分散分布时,熵较大。它表示了图像中纹理的非均匀程度或复杂程度。
(5)逆差距:反映图像纹理的同质性,度量图像纹理局部变化的多少。其值大则说明图像纹理的不同区域间缺少变化,局部非常均匀。
1.3 GLCM代码实践🥒
#import package
from skimage.feature import graycomatrix, graycoprops
import numpy as np
import pandas as pd#Feature Extraction with GLCM
def feature_extractor(images):image_dataset = pd.DataFrame()for image in images:df = pd.DataFrame()#greycomatrix(image, distances, angles, levels=256, symmetric=False, normed=False)#distances - List of pixel pair distance offsets.#angles - List of pixel pair angles in radians.#5 configuration for the grey-level co-occurrence matrix calculationdists = [[1],[3],[5],[3],[3]]angles = [[0],[0],[0],[np.pi/4],[np.pi/2]]for n ,(dist, angle) in enumerate(zip(dists, angles)):GLCM = graycomatrix(image, dist, angle)GLCM_Energy = graycoprops(GLCM, 'energy')[0]df['Energy'+str(n)] = GLCM_EnergyGLCM_corr = graycoprops(GLCM, 'correlation')[0]df['Corr'+str(n)] = GLCM_corrGLCM_diss = graycoprops(GLCM, 'dissimilarity')[0]df['Diss_sim'+str(n)] = GLCM_dissGLCM_hom = graycoprops(GLCM, 'homogeneity')[0]df['Homogen'+str(n)] = GLCM_homGLCM_contr = graycoprops(GLCM, 'contrast')[0]df['Contrast'+str(n)] = GLCM_contrimage_dataset = image_dataset.append(df)return image_dataset
看懂上面的理论知识,相信这份code已经不需要解释了。
二、GLCM+CNN网络构成👑
2.1 数据集构成🎈
2.2.2 CNN网络训练数据集 😊
train_images = torch.tensor(images_train,dtype=torch.float32).unsqueeze(1)#Convert the shape of the training set images to [*, 1, 256, 256].
train_labels = torch.tensor(labels,dtype=torch.int64)
val_images = torch.tensor(images_val,dtype=torch.float32).unsqueeze(1)#Convert the shape of the validation set images to [*, 1, 256, 256].
val_labels = torch.tensor(labels_val,dtype=torch.int64)
test_images = torch.tensor(images_test,dtype=torch.float32).unsqueeze(1)#Convert the shape of the test set images to [*, 1, 256, 256].
test_labels = torch.tensor(labels_test,dtype=torch.int64)
2.2.3 GLCM网络训练数据集😀
NOTE:images_train、images_val、images_test是numpy数组形式的数据集,用于提取GLCM特征。
train_extr_features = feature_extractor(images_train)
val_extr_features = feature_extractor(images_val)
test_extr_features = feature_extractor(images_test)
NOTE: 提取完特征后,变成torch.tensor类型,用于CNN网络训练。
trainFeatures = np.array(train_extr_features)
train_features = torch.tensor(trainFeatures,dtype=torch.float32)
validFeatures = np.array(val_extr_features)
valid_features = torch.tensor(validFeatures,dtype=torch.float32)
testFeatures = np.array(test_extr_features)
test_features = torch.tensor(testFeatures,dtype=torch.float32)
2.2 GLCM_CNN网络结构🎈
class GLCM_CNN(nn.Module):def __init__(self):super(GLCM_CNN,self).__init__()# 构建VGG16网络self.img_output = nn.Sequential(#input is (*,1,256,256)#nn.Conv2d(in_channels=1,out_channels=64,kernel_size=3,stride=1,padding='same'),nn.Conv2d(in_channels=1,out_channels=64,kernel_size=3,stride=1,padding=(1, 1)),# nn.BatchNorm2d(64),nn.ReLU(inplace=True),# nn.Conv2d(in_channels=64,out_channels=64,kernel_size=3,stride=1,padding='same'),nn.Conv2d(in_channels=64,out_channels=64,kernel_size=3,stride=1,padding=(1, 1)),# nn.BatchNorm2d(64),nn.ReLU(inplace=True),nn.MaxPool2d(kernel_size=2,stride=2),# input is (*,64,128,128)# nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding='same'),nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=(1, 1)),# nn.BatchNorm2d(128),nn.ReLU(inplace=True),# nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, stride=1, padding='same'),nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, stride=1, padding=(1, 1)),# nn.BatchNorm2d(128),nn.ReLU(inplace=True),nn.MaxPool2d(kernel_size=2, stride=2),# input is (*,128,64,64)# nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding='same'),nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding=(1, 1)),# nn.BatchNorm2d(256),nn.ReLU(inplace=True),# nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=1, padding='same'),nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=1, padding=(1, 1)),# nn.BatchNorm2d(256),nn.ReLU(inplace=True),# nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=1, padding='same'),nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=1, padding=(1, 1)),# nn.BatchNorm2d(256),nn.ReLU(inplace=True),nn.MaxPool2d(kernel_size=2,stride=2),# input is (*,256,32,32)# nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, stride=1, padding='same'),nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, stride=1, padding=(1, 1)),# nn.BatchNorm2d(512),nn.ReLU(inplace=True),# nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding='same'),nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=(1, 1)),# nn.BatchNorm2d(512),nn.ReLU(inplace=True),# nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding='same'),nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=(1, 1)),# nn.BatchNorm2d(512),nn.ReLU(inplace=True),nn.MaxPool2d(kernel_size=2, stride=2),# input is (*,512,16,16)# nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding='same'),nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=(1, 1)),# nn.BatchNorm2d(512),nn.ReLU(inplace=True),# nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding='same'),nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=(1, 1)),# nn.BatchNorm2d(512),nn.ReLU(inplace=True),# nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding='same'),nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=(1, 1)),# nn.BatchNorm2d(512),nn.ReLU(inplace=True),nn.MaxPool2d(kernel_size=2, stride=2),# input is (*,512,8,8)nn.AdaptiveAvgPool2d((7, 7)),nn.Flatten(),nn.Linear(in_features=512*7*7,out_features=4096),nn.ReLU(inplace=True),nn.Dropout(0.5),nn.Linear(in_features=4096,out_features=4096),nn.ReLU(inplace=True),nn.Dropout(0.5),nn.Linear(in_features=4096,out_features=8))# 构建GLCM特征提取分类网络self.feature_output = nn.Sequential(nn.Linear(in_features=25,out_features=8),nn.ReLU(),nn.Linear(in_features=8,out_features=4),# nn.ReLU(),)# 构建整合网络self.model = nn.Sequential(nn.Linear(in_features=12,out_features=8),nn.ReLU(),nn.Linear(in_features=8,out_features=2),)# 前向传播def forward(self,train_extr_features,imgs):img_output = self.img_output(imgs)feature= self.feature_output(train_extr_features)concat = torch.cat([feature,img_output],dim=1)return self.model(concat)
- self.img_output是一个VGG16的CNN网络,用于提取原数据的特征进行前向传播,得到各个神经元的激活值,也就是 原始数据的各个特征值。
- self.feature_output是一个线性分类器,是用GLCM从源数据中提取的特征,是一个大小为25的向量(通过1.3代码可知),然后投入线性分类器做与上述一样的操作。
- 最后将两者得到的特征值进行合并,传入最后一个融合网络,得到输出层最终的预测结果,用于反向传播,权重更新等进行网络训练。
三、实验结果👑
3.1 GLCM+CNN🎈
3.1.1 前10个epoch🥒
3.1.2 后10个epoch🥒
3.2 CNN🎈
哎呀,实验的时候忘记加验证集了,反正最后结果是valid Accuracy:93.012几来着,哈哈,尴尬了这就,写到这里才发现,,ԾㅂԾ,,