尽管这篇文章已经发布一年多了,我现在依旧能零散地收到私信询问有关于文章内容的问题。文章写的比较粗糙,所以在此澄清几个问题:
说来惭愧,研究生转职方向虽然是计算机电路工程,但是无奈学校VLSI课程池太浅,技能树分支偷学点了一些机器学习;好在这两块领域交叉也是热门,下学期可以选到一门机器学习软硬件协同设计(更加偏向于在CPU/GPU/TPU/FPGA优化深度学习模型,而非传统硬件设计)。感觉这个领域在传统VLSI设计之后也可能是一个比较有趣且热门的方向~
文章仅简要描述了一下代码实现,不涉及原理及数学推导~
这个项目是我深度学习课程的Final Project(姑且可以理解为课设)。相比于其他同学对于一个问题提出假设进行研究,我更偏向于做出一个实际可执行、能看到效果的系统。当然这其中我也稍微研究了一下戴口罩对于传统人脸识别系统的影响;由于是展示最终系统效果,研究过程中产生的改版、最终版、打死也不改版我就不放出来了。
做这个项目最大的初衷是想要做一个在当下新冠流行时期具有一定实际意义的课题。在我国(特别是我住的武汉这边)现在几乎人人都会佩戴口罩出行,这非常正确(比美国不知道好到哪里去了)但是在某些场景下还是会有一定的麻烦:比如我从机场转高铁回到武汉,期间就需要反复脱下口罩很多次来完成闸机口的人脸识别和身份证匹配。当然我国现在疫情控制的很好,这样做也无可厚非;然而如果单论这一操作的话,反复脱下口罩一定程度上会加大感染的风险。所以在传统的人脸识别系统的基础上,如何对原有系统配置进行修改升级实现“戴口罩的人脸”检测就成为了我的项目初步目标。
这个项目主要实现了一个基于卷积神经网络的在线口罩人脸识别系统(Online facial recognition system with masked face)。具体实现的功能就是自建数据集、构建并训练CNN模型、调用摄像头实现口罩人脸的二分类(是我的脸、不是我的脸)。
画了一张非常不专业的系统框图大致描述了一下整个系统的构成。整个系统比较重要的三个部分就是构建数据集、训练模型和识别。
推荐一下几门关于ML的课程:
CIS520 Machine Learning - University of Pennsylvania 内容包含了从决策树、线性回归到强化学习等基本所有比较重要的机器学习概念。课程内容重心侧向广度而非深度。
CS229 Machine Learning - Stanford University 大名鼎鼎的吴恩达教授授的课;感觉数学原理推导更多一些。
做项目过程中的一些参考链接如下:
基于卷积神经网络的人脸在线识别系统
How I built a Face Mask Detector for COVID-19 using PyTorch Lightning
Face detection with OpenCV and deep learning
VISUALIZING MODELS, DATA, AND TRAINING WITH TENSORBOARD
除了包含自己面部图片的数据集外,还用了这些数据集:
口罩遮挡人脸数据集(Real-World Masked Face Dataset,RMFD) 来自疫情中心武汉的武汉大学采集的口罩人脸数据集,包含真实口罩人脸和合成口罩人脸,已经经过裁切仅保留面部。
Labeled Faces in the Wild (LFW) 来自马萨诸塞大学的真实人脸数据集,没有经过面部裁切,需要自己进行面部裁切和预处理。
项目代码请查阅这个git repo。
注意: git repo中的数据集链接是属于学校云盘的,所以外部人员无法访问;如果需要数据集可以在这里下载,数据集中不包含“我的脸”,仅有“别人的脸”。
自建人脸数据集、进行在线人脸识别的第一步是快速准确的捕获到图片里的面部。
在第一版程序中,我使用的是Dlib库中的get_frontal_face_detector()
;这个面部检测器是使用现在经典的定向直方图(HOG)功能与线性分类器,图像金字塔和滑动窗口检测方案组合而成的。然而这个检测器的效率非常低,达不到“实时检测”的要求;并且在人戴上口罩时完全失效,无法捕获到任何面部。
在第二版程序中,我将检测器更换为了性能强大的OpenCV中的cv2.dnn.readNetFromCaffe()
。 这是一个“隐藏”在opencv 3.3之后版本中的、基于已经预训练好的DNN模型的检测器。为了使用这个基于Caffe模型预训练完毕的检测器,除了OpenCV库中的dnn模块之外,我们还需要在程序中导入Caffe prototxt文件和Caffe模型权重文件。这个检测器的强大之处在于极高的检测效率以及更高的鲁棒性。在我的测试中,这个检测器应对戴口罩的人脸毫无压力,在640*360尺寸的rgb视频流中的检测速度甚至可以达到40+fps,接近四倍于dlib检测器的速度。
# These two directories need to be in absolute format
path_model = "./deploy.prototxt.txt" # caffe prototxt file
path_weight = "./res10_300x300_ssd_iter_140000.caffemodel" # caffe model weight file
# detect face in the input image
# return the upper left (x,y), width and height
# can detect multiple faces
def face_detector(img):
net = cv2.dnn.readNetFromCaffe(path_model, path_weight) # call OpenCV pretrained DNN model
height, width = img.shape[:2]
blob = cv2.dnn.blobFromImage(cv2.resize(img, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0))
net.setInput(blob)
detections = net.forward()
threshold = 0.5
faces = []
for i in range(0, detections.shape[2]):
confidence = detections[0, 0, i, 2]
if confidence < threshold:
continue
box = detections[0, 0, i, 3:7] * np.array([width, height, width, height])
x_start, y_start, x_end, y_end = box.astype("int")
faces.append(np.array([x_start, y_start, x_end - x_start, y_end - y_start]))
return faces
检测到我的脸之后就可以进行预处理和保存了。考虑到所有我的照片都是在家里拍摄的,画面属性比较单一,所以要对图片进行数据增强。除了在拍摄时我要调整面部朝向、表情、带不带眼镜之外,我还对拍摄的照片的曝光、对比度进行了随机调整,以实现augmentation。
# randomly change the brightness and contrast of the image to augment the data
def img_change(img, light=1, bias=0):
width = img.shape[1]
height = img.shape[0]
for i in range(0, width):
for j in range(0, height):
for k in range(3):
tmp = int(img[j, i, k] * light + bias)
if tmp > 255:
tmp = 255
elif tmp < 0:
tmp = 0
img[j, i, k] = tmp
return img
接着所有面部图片会被调整为64*64*3的图片保存到本地。自建数据集到此完成。
过程与自建数据集基本相同:下载保存读取RMFD及LFW数据集、检测面部、数据增强、调整尺寸并保存。
我所使用的CNN的大致结构示意图。该结构从某一次课堂作业图像分类CNN结构修改而来,主要包括包括三层卷积层、三层池化层、两层全连接层、一层dropout用以防止过拟合、最后一层softmax用于进行逻辑激活、分类输出。
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
# Convolution 1
# input: 64*64*3
# output 64*64*32
self.cnn1 = nn.Conv2d(in_channels=3, out_channels=32, kernel_size=3, stride=1, padding=1)
self.batchnorm1 = nn.BatchNorm2d(32)
self.relu1 = nn.ReLU()
# Avg pool 1
# output: 32*32*32
self.avgpool1 = nn.AvgPool2d(kernel_size=2, stride=2)
# Dropout for regularization
self.dropout = nn.Dropout(p=0.5)
# Convolution 2
# output: 32*32*64
self.cnn2 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, stride=1, padding=1)
self.batchnorm2 = nn.BatchNorm2d(64)
self.relu2 = nn.ReLU()
# Avg pool 2
# output: 16*16*64
self.avgpool2 = nn.AvgPool2d(kernel_size=2, stride=2)
self.dropout = nn.Dropout(p=0.5)
# Convolution 3
# output: 16*16*64
self.cnn3 = nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, stride=1, padding=1)
self.batchnorm3 = nn.BatchNorm2d(64)
self.relu3 = nn.ReLU()
# Avg pool 3
# output: 8*8*64
self.avgpool3 = nn.AvgPool2d(kernel_size=2, stride=2)
self.dropout = nn.Dropout(p=0.5)
# Fully Connected 1
self.fc1 = nn.Linear(8*8*64, 512)
self.batchnorm4 = nn.BatchNorm1d(512)
self.relu4 = nn.ReLU()
# Fully Connected 2
self.fc2 = nn.Linear(512, 2)
self.sigmoid = nn.Softmax(dim=1)
def forward(self, x):
#Convolution 1
out = self.cnn1(x)
out = self.batchnorm1(out)
out = self.relu1(out)
#Avg pool 1
out = self.avgpool1(out)
#Convolution 2
out = self.cnn2(out)
out = self.batchnorm2(out)
out = self.relu2(out)
#Avg pool 2
out = self.avgpool2(out)
#Convolution 3
out = self.cnn3(out)
out = self.batchnorm3(out)
out = self.relu3(out)
#Avg pool 3
out = self.avgpool3(out)
#Resize
out = out.view(out.size(0), -1)
#Dropout
out = self.dropout(out)
#Fully connected 1
out = self.fc1(out)
out = self.batchnorm4(out)
out = self.relu4(out)
#Fully connected 2
out = self.fc2(out)
out = self.sigmoid(out)
return out
在这里安利大家一个工具叫做Google Colab,它是一个基于谷歌云计算引擎的在线Jupyter Notebook;它不需要配置任何环境,连接到服务器即可使用,我们几乎所有的作业和项目都是在colab上完成的,非常好用~ 缺点是有session时长限制,而且似乎只有美区账号才能使用。
考虑到我的数据集包含很多小文件,上传到云端会比较复杂,这份作业我直接选择在本地运行。使用torchsummary预估模型大小:
Device: cuda
Device name: GeForce RTX 2070
----------------------------------------------------------------
Layer (type) Output Shape Param #
================================================================
Conv2d-1 [-1, 32, 64, 64] 896
BatchNorm2d-2 [-1, 32, 64, 64] 64
ReLU-3 [-1, 32, 64, 64] 0
AvgPool2d-4 [-1, 32, 32, 32] 0
Conv2d-5 [-1, 64, 32, 32] 18,496
BatchNorm2d-6 [-1, 64, 32, 32] 128
ReLU-7 [-1, 64, 32, 32] 0
AvgPool2d-8 [-1, 64, 16, 16] 0
Conv2d-9 [-1, 64, 16, 16] 36,928
BatchNorm2d-10 [-1, 64, 16, 16] 128
ReLU-11 [-1, 64, 16, 16] 0
AvgPool2d-12 [-1, 64, 8, 8] 0
Dropout-13 [-1, 4096] 0
Linear-14 [-1, 512] 2,097,664
BatchNorm1d-15 [-1, 512] 1,024
ReLU-16 [-1, 512] 0
Linear-17 [-1, 2] 1,026
Softmax-18 [-1, 2] 0
================================================================
Total params: 2,156,354
Trainable params: 2,156,354
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.05
Forward/backward pass size (MB): 5.32
Params size (MB): 8.23
Estimated Total Size (MB): 13.60
----------------------------------------------------------------
看起来似乎不错,接下来就可以进行导入数据集和训练了~训练的过程中我们使用了Tensorflow库中的Tensorboard进行实时accuracy和loss的监看:
其实可以从图中看出来我设计的CNN结构在训练时大致是收敛趋势,但是实际还是存在overshoot的问题(会导致训练accuracy和loss发生跳动)。作为一个菜鸟调参侠(非常惭愧),现阶段只能在当前结构下调整参数达到一个比较好的效果。有关于模型结构部分我还在优化中。
训练完成后保存模型权重文件到本地备用即可。
path_model = './model'
if not os.path.exists(path_model):
os.makedirs(path_model)
torch.save(net.state_dict(), './model/model.pkl')
开启摄像头进行检测分为两部分:使用检测器检测视频中的人脸、送入网络进行分类。检测器如上文所描述。导入CNN、载入模型文件后即可开始对人脸进行预测(记得将神经网络设置为evaluate模式,不然的话可能会报一些奇怪的错误):
# take in the image ran return a predicted label
def face_recognize(input_image):
path_model = '../model/model.pkl' # load the saved model
model = Net()
model.load_state_dict(torch.load(path_model))
model.eval() # change the behavior of the model
with torch.no_grad():
inputs = torch.from_numpy(input_image)
inputs = inputs.unsqueeze(0)
outputs = model(inputs)
_, predicted = torch.max(outputs.data, 1)
return predicted
而后我们就可以在视频流中根据检测器返回的坐标和网络返回的标签对画面中的人脸进行框选和打标签了:
size = 64
cap = cv2.VideoCapture(0)
while True:
_, img = cap.read()
faces = face_detector(img) # OpenCV DNN face detector
for face in faces:
x, y, w, h = face
x, y = max(x, 0), max(y, 0)
img_face = img[y:y + h, x:x + w]
img_face = cv2.resize(img_face, (size, size))
img_face = img_face.astype('float32') / 255.0
img_face = (img_face - 0.5) / 0.5
img_face = img_face.transpose(2, 0, 1)
if face_recognize(img_face) == 0:
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), thickness=2)
cv2.putText(img, 'Me', (x, y), cv2.FONT_HERSHEY_COMPLEX, 0.5, (255, 255, 255), 1)
else:
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), thickness=2)
cv2.putText(img, 'Others', (x, y), cv2.FONT_HERSHEY_COMPLEX, 0.5, (255, 255, 255), 1)
key = cv2.waitKey(1)
if key == 27:
sys.exit(0)
cv2.imshow('Face recognition v2.0', img)
key = cv2.waitKey(1)
if key == 27:
sys.exit(0)
效果如图:
可以看到即便大家都戴着口罩也可以实现人脸的检测、识别。比较一下新旧版本的区别:
版本 | 识别普通人脸 | 识别口罩人脸 | 识别速度 |
---|---|---|---|
旧版 | √ | × | ~5fps |
新版 | √ | √ | ~20fps |
需要注意的是,这里的识别速度其实都是按照1280*720 rgb视频流为标准进行测试的。其实bottleneck主要还是在人脸检测器这一环节。传统的计算机视觉方案可能确实在我这个项目背景下会有些吃亏;换用DNN方案后不论是速度还是正确率都有了极大的提升,从侧面也体现出了深度学习这一工具的强大之处~
后面模型结构还需要修改;我也会测试和研究一下在现有数据集基础上合成口罩人脸以实现人脸识别这一方案的可行性以及魔改数据集给网络带来的影响。不过寒假就快结束了,还是先把手上的STA教程和Chip Timing Design啃完吧TAT。想想夏天毕业之后就可以成为一名芯片后端社畜了,还是有点激动的~