基于卷积神经网络的在线口罩人脸识别系统

---------2022年4月更新---------

尽管这篇文章已经发布一年多了,我现在依旧能零散地收到私信询问有关于文章内容的问题。文章写的比较粗糙,所以在此澄清几个问题:

  1. 私信和评论前请务必认真读完文章和评论区;如果你正在做课设,我相信你能自己找到大部分问题的答案。
  2. 所有源代码和数据集均附在文章中。
  3. 评论区有很多高质量的问题,或许能解答你的疑问。
  4. 现在工作比较忙,加上距离文章发布过去了一段时间,部分内容细节可能因为遗忘无法及时准确解答,望各位见谅。

---------2021年1月更新---------

碎碎念

说来惭愧,研究生转职方向虽然是计算机电路工程,但是无奈学校VLSI课程池太浅,技能树分支偷学点了一些机器学习;好在这两块领域交叉也是热门,下学期可以选到一门机器学习软硬件协同设计(更加偏向于在CPU/GPU/TPU/FPGA优化深度学习模型,而非传统硬件设计)。感觉这个领域在传统VLSI设计之后也可能是一个比较有趣且热门的方向~

文章仅简要描述了一下代码实现,不涉及原理及数学推导~


项目背景与介绍

这个项目是我深度学习课程的Final Project(姑且可以理解为课设)。相比于其他同学对于一个问题提出假设进行研究,我更偏向于做出一个实际可执行、能看到效果的系统。当然这其中我也稍微研究了一下戴口罩对于传统人脸识别系统的影响;由于是展示最终系统效果,研究过程中产生的改版、最终版、打死也不改版我就不放出来了。

做这个项目最大的初衷是想要做一个在当下新冠流行时期具有一定实际意义的课题。在我国(特别是我住的武汉这边)现在几乎人人都会佩戴口罩出行,这非常正确(比美国不知道好到哪里去了)但是在某些场景下还是会有一定的麻烦:比如我从机场转高铁回到武汉,期间就需要反复脱下口罩很多次来完成闸机口的人脸识别和身份证匹配。当然我国现在疫情控制的很好,这样做也无可厚非;然而如果单论这一操作的话,反复脱下口罩一定程度上会加大感染的风险。所以在传统的人脸识别系统的基础上,如何对原有系统配置进行修改升级实现“戴口罩的人脸”检测就成为了我的项目初步目标。

这个项目主要实现了一个基于卷积神经网络的在线口罩人脸识别系统(Online facial recognition system with masked face)。具体实现的功能就是自建数据集、构建并训练CNN模型、调用摄像头实现口罩人脸的二分类(是我的脸、不是我的脸)。

基于卷积神经网络的在线口罩人脸识别系统_第1张图片
画了一张非常不专业的系统框图大致描述了一下整个系统的构成。整个系统比较重要的三个部分就是构建数据集、训练模型和识别。


参考资料、数据集与代码

推荐一下几门关于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中的数据集链接是属于学校云盘的,所以外部人员无法访问;如果需要数据集可以在这里下载,数据集中不包含“我的脸”,仅有“别人的脸”。


代码实现

基于OpenCV DNN的人脸检测器

自建人脸数据集、进行在线人脸识别的第一步是快速准确的捕获到图片里的面部。

在第一版程序中,我使用的是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

基于卷积神经网络的在线口罩人脸识别系统_第2张图片
我所使用的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的监看:

基于卷积神经网络的在线口罩人脸识别系统_第3张图片
其实可以从图中看出来我设计的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)

效果如图:

基于卷积神经网络的在线口罩人脸识别系统_第4张图片
可以看到即便大家都戴着口罩也可以实现人脸的检测、识别。比较一下新旧版本的区别:

版本 识别普通人脸 识别口罩人脸 识别速度
旧版 × ~5fps
新版 ~20fps

需要注意的是,这里的识别速度其实都是按照1280*720 rgb视频流为标准进行测试的。其实bottleneck主要还是在人脸检测器这一环节。传统的计算机视觉方案可能确实在我这个项目背景下会有些吃亏;换用DNN方案后不论是速度还是正确率都有了极大的提升,从侧面也体现出了深度学习这一工具的强大之处~

后面模型结构还需要修改;我也会测试和研究一下在现有数据集基础上合成口罩人脸以实现人脸识别这一方案的可行性以及魔改数据集给网络带来的影响。不过寒假就快结束了,还是先把手上的STA教程和Chip Timing Design啃完吧TAT。想想夏天毕业之后就可以成为一名芯片后端社畜了,还是有点激动的~

你可能感兴趣的:(机器学习,深度学习,pytorch,卷积神经网络,人脸识别)