口罩识别—分类
一、问题背景
(最重要的目的就是熟悉卷积神经网络进行分类的各个环节)新冠肺炎的爆发,配戴口罩成为防控疫情保护自己的必需措施。不佩戴口罩严禁进入小区、学校、工厂,严禁乘坐公交、地铁等交通工具。但随着近日来,疫情逐渐好转,可能会有一部分降低了对新冠疫情的警惕性,不佩戴口罩出入公众场所,对此我们设计了一个口罩识别系统,把口罩识别问题当做一个分类问题,去检测人脸是否佩戴口罩。(对于口罩识别问题大多都是目标检测问题,其实这里为了只是为了更好的学习深度学习各个部分,就把这当做简单的一个分类问题)。
其实左图才是正常的口罩识别,但是如右图来说,对于特定的情况下,可以是把问题转换为分类问题,但是数据的话应该大多区域都是人脸。
二、数据处理
(一部分爬虫爬大的,一部分截图截的(留下了没技术的眼泪))首先在网络上获取200张图片,然后对获取的数据集进行筛选,最终确定了100张图片,其中50张佩戴口罩的图片,50张未佩戴口罩的图片。(尽量选择人脸占大多部分区域)由于获取的图片大小,文件名都不统一,故对100张图片进行预处理,大小重新调整为250*250,并让图片命名规范。
1、对图片进行重命名
# -*- coding:utf8 -*-
# coding:UTF-8
import os
class BatchRename():
'''
批量重命名文件夹中的图片文件
'''
def __init__(self):
self.path = 'C:\CBSD68'
def rename(self):
filelist = os.listdir(self.path)
total_num = len(filelist)
i = 1
for item in filelist:
if item.endswith('.png'):
src = os.path.join(os.path.abspath(self.path), item)
dst = os.path.join(os.path.abspath(self.path), str(i) + '.png')
try:
os.rename(src, dst)
print('converting %s to %s ...' % (src, dst))
i = i + 1
except:
continue
print('total %d to rename & converted %d jpgs' % (total_num, i))
if __name__ == '__main__':
demo = BatchRename()
demo.rename()
2、图片进行调整大小
# coding:UTF-8
from PIL import Image
import os.path
import glob
def convertjpg(jpgfile, outdir, width=260, height=260):
img = Image.open(jpgfile)
try:
new_img = img.resize((width, height), Image.BILINEAR)
new_img.save(os.path.join(outdir, os.path.basename(jpgfile)))
except Exception as e:
print(e)
for jpgfile in glob.glob(r"C:\Users\86150\Desktop\datasets\ImageNet400\*.png"): # 读取文件
convertjpg(jpgfile, r"C:\Users\86150\Desktop\datasets\IMage280") # 保存文件位置
其次将100张图片进行扩充,对100张图片使用7种变换,分别是左右变换、向左旋转20度、向右旋转20度、颜色增强、对比度增强、亮度增强和随机颜色,通过扩充获得了一共800张图片(其中包括400张佩戴口罩、400张未佩戴口罩)。变换效果如下所示。
然后我们将数据进行划分为训练集、验证集和测试集。
训练集的作用是用来拟合模型,通过设置分类器的参数,训练分类模型。后续结合验证集作用时,会选出同一参数的不同取值,拟合出多个分类器。
验证集的作用是当通过训练集训练出多个模型后,为了能找出效果最佳的模型,使用各个模型对验证集数据进行预测,并记录模型准确率。选出效果最佳的模型所对应的参数,即用来调整模型参数。
测试集通过训练集和验证集得出最优模型后,使用测试集进行模型预测。用来衡量该最优模型的性能和分类能力。即可以把测试集看做从来不存在的数据集,当已经确定模型参数后,使用测试集进行模型性能评价。
from PIL import Image
from PIL import ImageEnhance
import os
import cv2
import numpy as np
def flipLF(root_path,img_name): #左右翻转图像
img = Image.open(os.path.join(root_path, img_name))
filp_img = img.transpose(Image.FLIP_LEFT_RIGHT)
# filp_img.save(os.path.join(root_path,img_name.split('.')[0] + '_flip.jpg'))
return filp_img
def flipTP(root_path,img_name): #上下翻转图像
img = Image.open(os.path.join(root_path, img_name))
filp_img = img.transpose(Image.FLIP_TOP_BOTTOM)
# filp_img.save(os.path.join(root_path,img_name.split('.')[0] + '_flip.jpg'))
return filp_img
def rotation20(root_path, img_name):
img = Image.open(os.path.join(root_path, img_name))
rotation_img = img.rotate(20) #旋转角度
# rotation_img.save(os.path.join(root_path,img_name.split('.')[0] + '_rotation.jpg'))
return rotation_img
def rotation340(root_path, img_name):
img = Image.open(os.path.join(root_path, img_name))
rotation_img = img.rotate(340) #旋转角度
# rotation_img.save(os.path.join(root_path,img_name.split('.')[0] + '_rotation.jpg'))
return rotation_img
def randomColor(root_path, img_name): #随机颜色
"""
对图像进行颜色抖动
:param image: PIL的图像image
:return: 有颜色色差的图像image
"""
image = Image.open(os.path.join(root_path, img_name))
random_factor = np.random.randint(0, 31) / 10. # 随机因子
color_image = ImageEnhance.Color(image).enhance(random_factor) # 调整图像的饱和度
random_factor = np.random.randint(10, 21) / 10. # 随机因子
brightness_image = ImageEnhance.Brightness(color_image).enhance(random_factor) # 调整图像的亮度
random_factor = np.random.randint(10, 21) / 10. # 随机因子
contrast_image = ImageEnhance.Contrast(brightness_image).enhance(random_factor) # 调整图像对比度
random_factor = np.random.randint(0, 31) / 10. # 随机因子
return ImageEnhance.Sharpness(contrast_image).enhance(random_factor) # 调整图像锐度
def contrastEnhancement(root_path, img_name): # 对比度增强
image = Image.open(os.path.join(root_path, img_name))
enh_con = ImageEnhance.Contrast(image)
contrast = 1.5
image_contrasted = enh_con.enhance(contrast)
return image_contrasted
def brightnessEnhancement(root_path,img_name):#亮度增强
image = Image.open(os.path.join(root_path, img_name))
enh_bri = ImageEnhance.Brightness(image)
brightness = 1.5
image_brightened = enh_bri.enhance(brightness)
return image_brightened
def colorEnhancement(root_path,img_name):#颜色增强
image = Image.open(os.path.join(root_path, img_name))
enh_col = ImageEnhance.Color(image)
color = 1.5
image_colored = enh_col.enhance(color)
return image_colored
imageDir="./train/NO" #要改变的图片的路径文件夹
saveDir="./train/NO" #要保存的图片的路径文件夹
for name in os.listdir(imageDir):
#原始图像
# saveName= name[:-4]+"id.jpg"
# image = Image.open(os.path.join(imageDir, name))
# image.save(os.path.join(saveDir,saveName))
#亮度增强
saveName= name[:-4]+"be.jpg"
saveImage=brightnessEnhancement(imageDir,name)
saveImage.save(os.path.join(saveDir,saveName))
#左右变换
saveName= name[:-4]+"fli.jpg"
saveImage=flipLF(imageDir,name)
saveImage.save(os.path.join(saveDir,saveName))
#左旋转20
saveName= name[:-4]+"ro20.jpg"
saveImage=rotation20(imageDir,name)
saveImage.save(os.path.join(saveDir,saveName))
#右旋转20
saveName = name[:-4] + "ro340.jpg"
saveImage = rotation340(imageDir, name)
saveImage.save(os.path.join(saveDir, saveName))
#颜色增强
saveName= name[:-4]+"ce.jpg"
saveImage=colorEnhancement(imageDir,name)
saveImage.save(os.path.join(saveDir,saveName))
#随机颜色
saveName = name[:-4] + "rc.jpg"
saveImage = randomColor(imageDir, name)
saveImage.save(os.path.join(saveDir, saveName))
#对比度增强
saveName = name[:-4] + "cte.jpg"
saveImage = contrastEnhancement(imageDir, name)
saveImage.save(os.path.join(saveDir, saveName))
最后我们对数据进行标记,假设我们将佩戴口罩的标记为1,未佩戴口罩的标记为0,使用一个txt文件保存图片名和所对应的标签。
import os
import copy
def write_txt(path,txt_path):
num =len(os.listdir(path))
file_path = txt_path
file = open (file_path, 'w')
Y = 0
c = os.listdir(path)
for category in c:
C = c.index(category)
for imgs in os.listdir(os.path.join(path,category)):
file.write(category+'/'+imgs+'|'+ str(Y)+ '\n')
Y = Y+1
return
if __name__ == "__main__":
write_txt('data/train','train.txt')
write_txt('data/test', 'test.txt')
write_txt('data/val', 'val.txt')
三、网络相关
1、全连接层
如果是关于一个二维平面上的许多点来进行二分类,那直接使用连接层就足够了。那么对于一个点(x1,x2) 来说,输入层两个神经元,输出层一个神经元(输出它的类别),然后中间加若干隐层就可以完成任务了,当然前提是这些若干点线性可分。神经网络如图2所示。全连接层用公式可以表示为Y=WX+b ,这里Y代表输出标签,W代表权重,X表示输入,b表示偏置。
2、卷积层
卷积层主要进行特征提取,比全连接层的优势在于局部感知、权重共享和参数减少。卷积操作如图3所示。对于彩色图像 来说,经过卷积操作后大小变为 , 。其中F代表卷积核的大小,P代表填充尺寸,S代表卷积核移动的步长。其中,对于空洞卷积来说,F的大小等于F=K+(k-1)(d-1),其中K表示卷积核的大小,d表示空洞因子。
3、池化层
池化层经常使用的是最大池化和平局池化。如图所示为最大池化。对于H*W进行池化,大小变为 , (pytorch中默认步长为F的大小)。池化层降低特征图的尺寸,有助于减少计算量以及特征数量,保留主要特征,增大卷积核感受野,防止过拟合。但我们在做卷积的时候,让conv 层的步长stride = 2同样也可以起到降低尺寸的目的啊,为什么需要pooling 层来降低尺寸,因为池化层不需要保留参数。它采用一个固定的函数进行像素运算,如max pooling filter中采用了max函数,是不需要保留参数的,所以减少了网络的参数量。
4、激活函数
激活函数的作用:激活函数是用来加入非线性因素的,因为线性模型的表达能力不够。激活函数必须是非线性的。计算简单:神经元都要经过激活运算的,在随着网络结构越来越庞大、参数量越来越多,激活函数如果计算量小就节约了大量的资源。各种激活函数如图所示。
四、网络设计
1、网络结构
关于网络结构(如图5所示)的说明,其中Conv表示卷积层,ReLU表示激活层,Pool表示池化层和FC表示全连接层。对于输入为(1*3*255*255),最后经过全连接层大小变为(1*2)。若第一维大,则输出未佩戴口罩,否则返回佩戴口罩。
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=4, kernel_size=3), # input_size=(3*256*256),padding=2
nn.ReLU(), # input_size=(32*256*256)
nn.MaxPool2d((2, 2)))
self.conv2 = nn.Sequential(
nn.Conv2d(in_channels=4, out_channels=16, kernel_size=3),
nn.ReLU(),
nn.MaxPool2d((2, 2)) )
self.conv3 = nn.Sequential(
nn.Conv2d(in_channels=16, out_channels=32, kernel_size=3),
nn.ReLU(),
nn.MaxPool2d((2, 2)))
self.fc1= nn.Sequential(
nn.Linear(29 * 29 * 32, 120),
nn.ReLU(), )
self.fc2 = nn.Sequential(
nn.Linear(120, 60),
nn.ReLU(), )
self.fc3 = nn.Sequential(
nn.Linear(60, 2), )
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = x.view(-1, 29*29*32)
x = self.fc1(x)
x = self.fc2(x)
x = self.fc3(x)
return x
各层参数以及特征图大小
2、损失函数
深度神经网络中的的损失用来度量我们的模型得到的的预测值和数据真实值之间差距,也是一个用来衡量我们训练出来的模型泛化能力好坏的重要指标。对模型进行优化的最终目的是尽可能地在不过拟合的情况下降低损失值。
损失函数(loss function):计算的是一个样本的误差。它是用来估量你模型的预测值 f(x)与真实值 Y的不一致程度,通常用 L(Y,f(x))来表示。
代价函数(cost function):是整个训练集上所有样本误差的平均。本质上看,和损失函数是同一个东西。
目标函数:代价函数 + 正则化项。在实验中主要采用交叉熵损失函数。
3、优化函数
在计算出模型的损失值之后,接下来需要利用损失值进行模型参数的优化。在实践操作最常用到的是一阶优化函数。包括GD,SGD,BGD,Adam等。一阶优化函数在优化过程中求解的是参数的一阶导数,这些一阶导数的值就是模型中参数的微调值。
五、实验结果
训练了40个Epoch,学习率0.001,采用随机梯度下降优化算。然后在验证集中选择最好的一个模型,图7是对图像经过三个卷积层进行特征图可视化,提取特征关注与边缘与口罩部分。
上图显示的是训练阶段的在训练集和验证集上的分类正确率,在30个Epoch时,准确率就可以达到100%。图9是训练过程中的损失曲线,没有大的震荡,最后趋向于收敛。
最后自己选用10张图片进行测试。(其实这个测试不能说明什么问题,应为第二张明明很清楚的,可是却没有识别出来)可能还是和训练集数据有关系,因为原始训练集戴口罩的照片都是在网上获取的,有点偏网红的感觉。
六、代码
先用上面的图像增强和产生数据标签就可以,然后运行下面代码就可以(说实在这样不太好,应该吧模型结构另建一个mode.py,test.py,train.py)但是一切为了方便,都放在一起了。先训练40个·epoch,然后对40个epoch在验证集上进行测试,最后进行一个测试。
import torch
import random
from PIL import Image
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import scipy.io as io
import os
import argparse
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(device)
from torchsummary import summary
class MyData(torch.utils.data.Dataset):
def __init__(self, root, datatxt, transform, target_transform=None):
super(MyData, self).__init__()
file_txt = open(datatxt,'r')
imgs = []
for line in file_txt:
line = line.rstrip()
words = line.split('|')
imgs.append((words[0], words[1]))
self.imgs = imgs
self.root = root
self.transform = transform
self.target_transform = target_transform
def __getitem__(self, index):
#random.shuffle(self.imgs)
name, label = self.imgs[index]
img = Image.open(self.root + name).convert('RGB')
if self.transform is not None:
img = self.transform(img)
label = int(label)
#label_tensor = torch.Tensor([0,0])
#label_tensor[label]=1
return img, label
def __len__(self):
return len(self.imgs)
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=4, kernel_size=3), # input_size=(3*256*256),padding=2
nn.ReLU(), # input_size=(32*256*256)
nn.MaxPool2d((2, 2)))
self.conv2 = nn.Sequential(
nn.Conv2d(in_channels=4, out_channels=16, kernel_size=3),
nn.ReLU(),
nn.MaxPool2d((2, 2)) )
self.conv3 = nn.Sequential(
nn.Conv2d(in_channels=16, out_channels=32, kernel_size=3),
nn.ReLU(),
nn.MaxPool2d((2, 2)))
self.fc1= nn.Sequential(
nn.Linear(29 * 29 * 32, 120),
nn.ReLU(), )
self.fc2 = nn.Sequential(
nn.Linear(120, 60),
nn.ReLU(), )
self.fc3 = nn.Sequential(
nn.Linear(60, 2), )
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = x.view(-1, 29*29*32)
x = self.fc1(x)
x = self.fc2(x)
x = self.fc3(x)
return x
classes = ['NO','OK']
#ToTensor()能够把灰度范围从0-255变换到0-1之间,而后面的transform.Normalize()则把0-1变换到(-1,1).
transform = transforms.Compose(
[transforms.Resize((250, 250)),transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
train_data=MyData(root ='data/train/',datatxt='train.txt', transform=transform)
test_data=MyData(root ='data/test/',datatxt='test.txt',transform=transform)
val_data=MyData(root ='data/val/',datatxt='val.txt',transform=transform)
train_loader = torch.utils.data.DataLoader(dataset=train_data, batch_size=20, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_data, batch_size=11,shuffle=False)
val_loader = torch.utils.data.DataLoader(dataset=val_data, batch_size=20,shuffle=True)
def imshow(img):
img = img / 2 + 0.5 # unnormalize
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()
dataiter = iter(train_loader)
images, labels = dataiter.next()
# 参数设置,使得我们能够手动输入命令行参数,就是让风格变得和Linux命令行差不多
parser = argparse.ArgumentParser(description='PyTorch Training')
parser.add_argument('--model_dir', default='./model/', help='folder to output images and model checkpoints') #输出结果保存路径
args = parser.parse_args()
net = Net()
outf = "./model/"
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
if __name__ == "__main__":
with open("acc.txt", "w") as f:
with open("log.txt", "w")as f2:
for epoch in range(1):
print('\nEpoch: %d' % (epoch + 1))
net.train()
sum_loss = 0.0
correct = 0.0
total = 0.0
correct_epoch = 0.0
for i, data in enumerate(train_loader, 0):
# 准备数据
length = len(train_loader)
inputs, labels = data
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
# forward + backward
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 每训练1个batch打印一次loss和准确率
sum_loss += loss.item()
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += predicted.eq(labels.data).cpu().sum()
print('[epoch:%d, iter:%d] Loss: %.03f '
% (epoch + 1, (i + 1 ), sum_loss / (i + 1)))
#f2.write('%03d %05d |Loss: %.03f '% (epoch + 1, (i + 1 ), sum_loss / (i + 1)))
f2.write('Loss: %.03f ' % ( sum_loss / (i + 1)))
f2.write('\n')
f2.flush()
print('训练分类准确率为:%.3f%%' % (100 * correct / total))
acc = 100. * correct / total
# 将每次测试结果实时写入acc.txt文件中
#f.write("EPOCH=%03d,Train_Accuracy= %.3f%%" % (epoch + 1, acc))
f.write("Train_Accuracy= %.3f" % (acc))
f.write('\n')
f.flush()
print('Saving model......')
torch.save(net.state_dict(), '%s/net_%03d.pth' % (args.model_dir, epoch + 1))
print("Training Finished, TotalEPOCH=%d" % epoch)
# #d打印网络结构及参数和输出形状
# net = net.to(device)
# summary(net, input_size=(3, 250, 250)) #summary(net,(3,250,250))
#训练之后将所有的模型进行测试
for i in range(40):
#print("测试val")
dataiter = iter(val_loader)
images, labels = dataiter.next()
#PATH = 'net' + '{}.pth'.format(39)
net = Net()
model = torch.load(os.path.join(args.model_dir, 'net_%03d.pth' % ( i + 1)))
#model = torch.load(os.path.join('net_%03d.pth' % 39))
net.load_state_dict(model)
correct = 0
total = 0
with torch.no_grad():
for data in val_loader:
net.eval()
images, labels = data
#imshow(torchvision.utils.make_grid(images))
# print('GroundTruth: ', ' '.join('%5s' % classes[labels[j]] for j in range(6)))
# print(images)
#print("真实标签", labels)
outputs = net(images)
# torch.max(a, 1)返回每一行中最大值的那个元素,且返回其索引(返回最大元素在这一行的列索引)
_, predicted = torch.max(outputs.data, 1)
#print("预测标签", predicted)
#print(outputs)
total += labels.size(0)
correct += (predicted == labels).sum().item()
#print('Accuracy of the network on the 10 test images: %d %%' % (100 * correct / total))
print('%d' % (100 * correct / total))
model = torch.load(os.path.join(args.model_dir, 'net_%03d.pth' % ( 40)))
model = torch.load(os.path.join('net_%03d.pth' % 39))
net.load_state_dict(model)
print("测试test")
dataiter = iter(test_loader)
images, labels = dataiter.next()
net = Net()
model = torch.load(os.path.join(args.model_dir, 'net_%03d.pth' % (40)))
net.load_state_dict(model)
correct = 0
total = 0
with torch.no_grad():
for data in test_loader:
net.eval()
images, labels = data
imshow(torchvision.utils.make_grid(images))
# print('GroundTruth: ', ' '.join('%5s' % classes[labels[j]] for j in range(6)))
# print(images)
print("真实标签", labels)
outputs = net(images)
# torch.max(a, 1)返回每一行中最大值的那个元素,且返回其索引(返回最大元素在这一行的列索引)
_, predicted = torch.max(outputs.data, 1)
print("预测标签", predicted)
#print(outputs)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10 test images: %d %%' % (100 * correct / total))