写在前面,防止有傻乎乎的同学直接全文复制出现问题。
调用了 dlib 库的函数实现的人脸检测和人脸识别;
唯一的难点,也确实比较难,就是实现双线程控制,详见报告;
ResNet部分讲解copy的网上资料,当时并没有认真阅读ResNet论文,所以下面的知识点难免出现错误,不要作为论文阅读的参考;
同学人脸涉及隐私所以马赛克掉了,我不要脸所以留下自己的了,别被吓着;本来证件照那张照片是别的照片,非常吓人,所以就换了证件照。

《人工智能技术》课程设计

基于ResNet、dlib+opencv人脸识别系统

指导老师: 王伟

摘要

自建小型数据集,采用Opencv中的级联分类器进行人脸检测,基于ResNet18残差神经网络进行人脸识别;后续采用dlib库中的人脸检测器、特征点检测器和人脸识别模型进行训练和识别,效果相较于前者更佳

关键词:人脸识别、残差神经网络、ResNet18、Opencv、dlib

目录
1 问题描述
2 相关技术介绍
    2.1 ResNet18残差神经网络
        2.1.1 退化现象
        2.1.2 残差块
        2.1.3 为什么叫残差网络
        2.1.4 残差网络的背后原理
        2.1.5 残差网络结构
    2.2 dlib库
        2.2.1 dlib库简介
        2.2.2 两个核心文件和三个核心函数
        2.2.3 人脸识别流程
        2.2.4 人脸识别算法原理
3 系统整体框架
    3.1 整体流程图
    3.2 更新人脸信息库部分
    3.3 人脸识别部分
    3.4 进程切换部分
4 详细设计与实现
    4.1 环境配置
    4.2 dlib实现人脸识别
        4.2.1 系统功能
        4.2.2 设计思路
        4.2.3 结果截图
    4.3 ResNet实现人脸识别
        4.3.1 设计思路
        4.3.2 结果截图
5 总结与展望
    5.1 难点分析
    5.2 局限分析
    5.3 总结
6 代码

1 问题描述

人脸识别技术,是基于人的脸部特征信息进行身份识别的一种生物识别技术。用摄像机或摄像头采集含有人脸的图像或视频流,并自动在图像中检测和跟踪人脸,进而对检测到的人脸进行脸部的一系列相关技术,通常也叫做人像识别、面部识别。

人脸识别技术是一种生物识别技术,可以用来确认用户身份。人脸识别技术相比于传统的身份识别技术有很大的优势,主要体现在方便性上。传统的身份认证方式诸如:密码、PIN码、射频卡片、口令、指纹等,需要用户记住复杂密码或者携带身份认证钥匙。而密码、卡片均存在丢失泄露的风险,相比于人脸识别,交互性于安全性都不够高。人脸识别可以使用摄像头远距离非接触识别,相比于指纹免去了将手指按在识别区域的操作,可由摄像头自动识别。

目前人脸识别技术已经广泛应用于安全、监控、一般身份识别、考勤、走失儿童搜救等领域,对于提升身份认证的效率起到了重要的作用。而且目前还有更深入的人脸识别的研究正在进行,包括性别识别、年龄估计、心情估计等,更高水平和更高准确率的人脸识别技术对于城市安全和非接触式身份认证有巨大的作用。

一个简单的人脸识别系统,包括以下4个方面的内容 :
(1)人脸检测(Detection):即从各种不同的场景中检测出人脸的存在并确定其位置。
(2)人脸规范化(Normalization):校正人脸在尺度、光照和旋转等方面的变化。
(3)人脸校验(Face verification ):采取某种方式表示检测出人脸和数据库中的已知人脸,确认两张脸是否是同一个人。
(4)人脸识别(Recognition):将待识别的人脸与数据库中的已知人脸比较并进行匹配。

虽然人脸识别有很多其他识别无法比拟的优点,但是它本身也会受到多种因素影响,主要分为基础因素、内在因素和外在因素。基础因素是人脸本身就相似,人的五官、轮廓大致相同;内在因素是人的内部属性,如年龄变化、精神状态、化妆等;外部因素是成像质量的问题,比如相片的清晰程度、有无眼镜、口罩等遮挡。对于人类来说,认出一个人是很容易的事情,对于计算机而言,图片是由多维数字矩阵表示的,识别任务难度大。

2 相关技术介绍

2.1 ResNet18残差神经网络

2.1.1 退化现象

在深度学习领域,一直以来从直觉上认为:网络层数越深提取到的特征就应该越高级,从而最终的效果就应该更好。比如从LeNet5的5层网络发展到了VGG的19层网络。于是为了达到更好的网络效果,研究人员又开始加深网络的层数,可这次迎来的确却是网络性能的降低。随着网络加深,训练集的错误率反而更高,这种现象被称作为“网络退化”。

当网络退化时,浅层网络能够达到比深层网络更好的训练效果,这时如果我们把低层的特征传到高层,那么效果应该至少不比浅层的网络效果差,或者说如果一个VGG-100网络在第98层使用的是和VGG-16第14层一模一样的特征,那么VGG-100的效果应该会和VGG-16的效果相同。所以,可以在98层和14层之间添加一条直接映射来达到此效果。

2.1.2 残差块

残差网络是由一系列残差块组成的(图1)。一个残差块可以用表示为:

x
l
+
1
=
x
l
+
F
(
x
l
,
W
l
)
(1)
x_{l+1}=x_l+F(x_l, W_l)\tag{1}
xl+1=xl+F(xl,Wl)(1)

残差块分成两部分直接映射部分和残差部分。
x
l
x_l
xl
是直接映射,反应在图1中是左边的曲线;
F
(
x
l
,
W
l
)
F\left(x_l,W_l\right)
F(xl,Wl)
是残差部分,一般由两个或者三个卷积操作构成,即图1中右侧包含卷积的部分。

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

图 1    残差块

图1中的Weight在卷积网络中是指卷积操作,addition是指单位加操作。

在卷积网络中,
x
l
x_l
xl
可能和
x
l
+
1
x_{l+1}
xl+1
的Feature Map的数量不一样,这时候就需要使用1×1卷积进行升维或者降维(图2)。这时,残差块表示为:

x
l
+
1
=
h
(
x
l
)
+
F
(
x
l
,
W
l
)
(2)
x_{l+1}=h(x_l)+F(x_l, W_l)\tag{2}
xl+1=h(xl)+F(xl,Wl)(2)

其中
h
(
x
l
)
=
W
l

x
h\left(x_l\right)=W_l^\prime x
h(xl)=Wlx
。其中
W

W^\prime
W
是1×1卷积操作,但是实验结果1×1卷积对模型性能提升有限,所以一般是在升维或者降维时才会使用。

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

图 1    1×1残差块

2.1.3 为什么叫残差网络

在统计学中,残差和误差是非常容易混淆的两个概念。误差是衡量观测值和真实值之间的差距,残差是指预测值和观测值之间的差距。对于残差网络的命名原因,作者给出的解释是,网络的一层通常可以看做
y
=
H
(
x
)
y=H\left(x\right)
y=H(x)
,而残差网络的一个残差块可以表示为
H
(
x
)
=
F
(
x
)
+
x
H\left(x\right)=F\left(x\right)+x
H(x)=F(x)+x
,也就是
F
(
x
)
=
H
(
x
)

x
F\left(x\right)=H\left(x\right)-x
F(x)=H(x)x
,在单位映射中,
y
=
x
y=x
y=x
便是观测值,而
H
(
x
)
H\left(x\right)
H(x)
是预测值,所以
F
(
x
)
F\left(x\right)
F(x)
便对应着残差,因此叫做残差网络。

2.1.4 残差网络的背后原理

残差块一个更通用的表示方式是

y
l
=
h
(
x
l
)
+
F
(
x
l
,
W
l
)
x
l
+
1
=
f
(
y
l
)
\begin{align} y_l=h(x_l)+F(x_l,W_l)\tag{3}\\ x_{l+1}=f(y_l)\tag{4}\\ \end{align}
yl=h(xl)+F(xl,Wl)xl+1=f(yl)(3)(4)

现在我们先不考虑升维或者降维的情况,那么
h
(

)
h\left(·\right)
h()
是直接映射,
f
(

)
f\left(·\right)
f()
是激活函数,一般使用ReLU。首先给出两个假设:

那么这时候残差块可以表示为:

x
l
+
1
=
x
l
+
F
(
x
l
,
W
l
)
(5)
x_{l+1}=x_l+F\left(x_l,W_l\right) \tag{5}
xl+1=xl+F(xl,Wl)(5)

对于一个更深的层L,其与l层的关系可以表示为:

x
L
=
x
l
+

i
=
l
L

1
F
(
x
i
,
W
i
)
(6)
x_L=x_l+\sum_{i=l}^{L-1}F\left(x_i,W_i\right)\tag{6}
xL=xl+i=lL1F(xi,Wi)(6)

这个公式反应了残差网络的两个属性:

  1. L层可以表示为任意一个比它浅的l层和他们之间的残差部分之和;

  2. x
    L
    =
    x
    +

    i
    =
    L

    1
    F
    (
    x
    i
    ,
    W
    i
    )
    x_L=x_0+\sum_{i=0}^{L-1}F\left(x_i,W_i\right)
    xL=x0+i=0L1F(xi,Wi)

    L
    L
    L
    是各个残差块特征的单位累和,而MLP是特征矩阵的累积。

根据BP中使用的导数的链式法则,损失函数
ε
\varepsilon
ε
关于
x
l
x_l
xl
的梯度可以表示为


ε

x
l
=

ε

x
L

x
L

x
l
=

ε

x
L
(
1
+


x
l

i
=
l
L

1
F
(
x
i
,
W
i
)
)
=

ε

x
L
+

ε

x
L


x
l

i
=
l
L

1
F
(
x
i
,
W
i
)
(7)
\frac{\partial\varepsilon}{\partial x_l}=\frac{\partial\varepsilon}{\partial x_L}\frac{\partial x_L}{\partial x_l}=\frac{\partial\varepsilon}{\partial x_L}\left(1+\frac{\partial}{\partial x_l}\sum_{i=l}^{L-1}F\left(x_i,W_i\right)\right)=\frac{\partial\varepsilon}{\partial x_L}+\frac{\partial\varepsilon}{\partial x_L}\frac{\partial}{\partial x_l}\sum_{i=l}^{L-1}F\left(x_i,W_i\right)\tag{7}
xlε=xLεxlxL=xLε(1+xli=lL1F(xi,Wi))=xLε+xLεxli=lL1F(xi,Wi)(7)

上面公式反映了残差网络的两个属性:

  1. 在整个训练过程中,


    x
    l

    i
    =
    l
    L

    1
    F
    (
    x
    i
    ,
    W
    i
    )
    \frac{\partial}{\partial x_l}\sum_{i=l}^{L-1}F\left(x_i,W_i\right)
    xli=lL1F(xi,Wi)
    不可能一直为-1,也就是说在残差网络中不会出现梯度消失的问题。


  2. ε

    x
    L
    \frac{\partial\varepsilon}{\partial x_L}
    xLε
    表示
    L
    L
    L
    层的梯度可以直接传递到任何一个比它浅的
    l
    l
    l
    层。

通过分析残差网络的正向和反向两个过程发现,当残差块满足上面两个假设时,信息可以非常畅通的在高层和低层之间相互传导,说明这两个假设是让残差网络可以训练深度模型的充分条件。

下面说明直接映射是最好的选择。
对于假设1,采用反证法,假设
h
(
x
l
)
=
λ
l
x
l
h\left(x_l\right)=\lambda_lx_l
h(xl)=λlxl
,那么这时候,残差块(图3.b)表示为

x
l
+
1
=
λ
l
x
l
+
F
(
x
l
,
W
l
)
(8)
x_{l+1}=\lambda_lx_l+F\left(x_l,W_l\right)\tag{8}
xl+1=λlxl+F(xl,Wl)(8)

对于更深的
L
L
L


x
L
=
(

i
=
l
L

1
λ
i
)
x
l
+

i
=
l
L

1
F
(
x
i
,
W
i
)
(9)
x_L=\left(\prod_{i=l}^{L-1}\lambda_i\right)x_l+\sum_{i=l}^{L-1}F\left(x_i,W_i\right)\tag{9}
xL=(i=lL1λi)xl+i=lL1F(xi,Wi)(9)

为了简化问题,我们只考虑公式的左半部分
x
L
=
(

i
=
l
L

1
λ
i
)
x
l
x_L=\left(\prod_{i=l}^{L-1}\lambda_i\right)x_l
xL=(i=lL1λi)xl
, 损失函数
ε
\varepsilon
ε

x
l
x_l
xl
求偏微分得


ε

x
l
=

ε

x
L
(
(

i
=
l
L

1
λ
i
)
+


x
l
F
^
(
x
i
,
W
i
)
)
(10)
\frac{\partial\varepsilon}{\partial x_l}=\frac{\partial\varepsilon}{\partial x_L}\left(\left(\prod_{i=l}^{L-1}\lambda_i\right)+\frac{\partial}{\partial x_l}\hat{F}\left(x_i,W_i\right)\right)\tag{10}
xlε=xLε((i=lL1λi)+xlF(xi,Wi))(10)

上面公式反映了两个属性:


  1. λ
    >
    1
    \lambda>1
    λ>1
    时,很有可能发生梯度爆炸;

  2. λ
    <
    1
    \lambda<1
    λ<1
    时,梯度变成0,会阻碍残差网络信息的反向传递,从而影响残差网络的训练。

所以
λ
\lambda
λ
必须等1。同理,其他常见的激活函数都会产生和上面的例子类似的阻碍信息反向传播的问题。

对于其它不影响梯度的
h
(

)
h\left(·\right)
h()
,例如LSTM中的门机制(图3.c,图3.d)或者Dropout(图3.f)以及何凯明的“Deep Residual Learning for Image Recognition”中用于降维的1×1卷积(图3.e)也许会有效果,作者采用了实验的方法进行验证,实验结果见图4。

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

图 3    直接映射的变异模型

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

图 4    变异模型(均为110层)在Cifar10数据集上的表现

从图4的实验结果中我们可以看出,在所有的变异模型中,依旧是直接映射的效果最好。下面我们对图3中的各种变异模型的分析:

  1. Exclusive Gating:在LSTM的门机制中,绝大多数门的值为0或者1,几乎很难落到0.5附近。当
    g
    (
    x
    )

    g(x)\rightarrow0
    g(x)0
    时,残差块变成只有直接映射组成,阻碍卷积部分特征的传播;当
    g
    (
    x
    )

    1
    g(x)\rightarrow1
    g(x)1
    时,直接映射失效,退化为普通的卷积网络;
  2. Short-cut only gating:
    g
    (
    x
    )

    g(x)\rightarrow0
    g(x)0
    时,此时网络便是“Deep Residual Learning for Image Recognition”提出的直接映射的残差网络;当
    g
    (
    x
    )

    1
    g(x)\rightarrow1
    g(x)1
    时,退化为普通卷积网络;
  3. Dropout:类似于将直接映射乘以
    1

    p
    1-p
    1p
    ,所以会影响梯度的反向传播;
  4. 1×1 conv:1×1卷积比直接映射拥有更强的表示能力,但是实验效果却不如直接映射,说明该问题更可能是优化问题而非模型容量问题。

因此可以得出结论:假设1成立,即

y
l
=
x
l
+
F
(
x
l
,
W
l
)
(11)
y_l=x_l+F\left(x_l,W_l\right)\tag{11}
yl=xl+F(xl,Wl)(11)


y
l
+
1
=
x
l
+
1
+
F
(
x
l
+
1
,
W
l
+
1
)
=
f
(
y
l
)
+
F
(
f
(
y
l
)
,
W
l
+
1
)
(12)
y_{l+1}=x_{l+1}+F\left(x_{l+1},W_{l+1}\right)=f\left(y_l\right)+F\left(f\left(y_l\right),W_{l+1}\right)\tag{12}
yl+1=xl+1+F(xl+1,Wl+1)=f(yl)+F(f(yl),Wl+1)(12)

论文提出的残差块可以详细展开如图5.a,即在卷积之后使用了BN做归一化,然后在和直接映射单位加之后使用了ReLU作为激活函数。
我们已经说明了“直接映射是最好的选择”,所以我们希望构造一种结构能够满足直接映射,即定义一个新的残差结构
f
^
(

)
\hat{f}\left(·\right)
f()


y
l
+
1
=
y
l
+
F
(
f
^
(
y
l
)
,
W
l
+
1
)
(13)
y_{l+1}=y_l+F\left(\widehat{f}(y_l),W_{l+1}\right)\tag{13}
yl+1=yl+F(f(yl),Wl+1)(13)

上面公式反应到网络里即将激活函数移到残差部分使用,即图5.c,这种在卷积之后使用激活函数的方法叫做post-activation。然后,作者通过调整ReLU和BN的使用位置得到了几个变种,即5.d中的ReLU-only pre-activation和5.d中的 full pre-activation。作者通过对照试验对比了这几种变异模型,结果见图6。

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

图 5    激活函数在残差网络中的使用

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

图 6    基于激活函数位置的变异模型在Cifar10上的实验结果

2.1.5 残差网络结构

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

图 7    ResNet18网络结构

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

图 8    ResNet网络参数

ResNet网络是参考了VGG19网络,在其基础上进行了修改,并通过短路机制加入了残差单元。变化主要体现在ResNet直接使用stride=2的卷积做下采样,并且用global average pool层替换了全连接层。ResNet的一个重要设计原则是:当feature map大小降低一半时,feature map的数量增加一倍,这保持了网络层的复杂度。
ResNet相比普通网络每两层间增加了短路机制,这就形成了残差学习,其中虚线表示feature map数量发生了改变。从图4中可以看到,对于18-layer和34-layer的ResNet,其进行的两层间的残差学习,当网络更深时,其进行的是三层间的残差学习,三层卷积核分别是1×1,3 × 3 和 1 × 1,一个值得注意的是隐含层的feature map数量是比较小的,并且是输出feature map数量的1/4。
如下图所示:图5左对应的是浅层网络,而图5右对应的是深层网络。对于短路连接,当输入和输出维度一致时,可以直接将输入加到输出上。但是当维度不一致时(对应的是维度增加一倍),这就不能直接相加。有两种策略:
(1)采用zero-padding增加维度,此时一般要先做一个downsamp,可以采用strde=2的pooling,这样不会增加参数;
(2)采用新的映射(projection shortcut),一般采用1×1的卷积,这样会增加参数,也会增加计算量。短路连接除了直接使用恒等映射,当然都可以采用projection shortcut。

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

图 9    两种残差块

2.2 dlib库

2.2.1 dlib库简介

dlib 是一个现代C++工具包,包含机器学习算法和工具,用于在C++中创建复杂的软件,以解决现实世界中的问题。按照dlib官网的叙述,其特点主要有:

  1. 丰富的帮助文档:dlib官网为每个类与功能都提供了完整的帮助文档,且官网提供有非常多的例程。作者在官网有说如果有东西文件没记录或者不清楚的可以联系他更改。
  2. 高质量的可移植代码:dlib库不需要第三方库且符合ISO C++标准,支持Windows, Linux, Mac OS X系统。
  3. 丰富的机器学习算法:dlib库中包括深度学习算法、SVM以及一些常用的聚类算法等。
  4. 图像处理:支持读写windows BMP文件、可实现各种色彩空间的图像变换、包括物体检测的一些工具以及高质量的人脸识别功能。
  5. 线程:提供了简单可移植的线程API。

2.2.2 两个核心文件和三个核心函数

  1. shape_predictor_68_face_landmarks.dat”保存人脸68点提取器的数据文件。

  2. dlib_face_recognition_resnet_model_v1.dat”保存基于ResNet的128维特征向量提取器的数据文件。

  3. detector = dlib.get_frontal_face_detector()”获取人脸检测器的函数。

    使用方法:dets = detector(img, 1),返回值是<class 'dlib.dlib.rectangle'>由矩形坐标组成的“列表”,可以通过函数的left, right, top, bottom方法分别获取每个矩形的左边界所在位置,右边界所在位置,上边界所在位置,下边界所在位置。其中“1”表示扫描人脸的精度,该值越大越能识别到小脸,这也导致了执行时间变久,一般取“0”或“1”。

  4. predictor = dlib.shape_predictor(“shape_predictor_68_face_landmarks.dat”)”加载人脸68特征点检测器,返回用于确定人脸68个特征点位置的函数。

    使用方法:“shape = predictor(img, rect)”传入原始图像和detector得到的矩阵信息,返回关键点信息,关键点信息我们是无法直接使用的,但是可以投入下一个函数中。

  5. face_recognition_model = dlib.face_recognition_model_v1(“dlib_face_recognition_resnet_model_v1.dat”)”加载人脸识别模型,返回用于人脸识别的模型。

    使用方法:“face_recognition_model.compute_face_descriptor(img, shape)”传入原始图像和68个特征点,返回128维的特征信息(值),后续将用需要被识别的人脸的128特征信息与信息库中的每张人脸的特征信息计算“距离差”,“距离最近”人脸将被匹配。

2.2.3 人脸识别流程

提取68个特征点

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

图 10    标注前(左)后(右)

获取特征向量写入csv

特征点只是用于标识人脸关键点的坐标而已,如果想要实现人脸识别,那么必须将特征点转换为特征向量。通过dlib函数将特征点转换为特征向量后保存成csv文件。

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

图 11    128维人脸信息(部分)

计算特征数据集的欧氏距离作对比

欧氏距离(也称欧几里得距离)是一个常用的的距离度量,它指的是在空间中两个向量(点)之间的直线距离。欧氏距离越小,说明两个向量越接近,也就是两个向量差异越小。
假设在两个向量分别为
X
(
x
1
,
x
2
,

,
x
n
)
X\left(x_1,x_2,\ldots,x_n\right)
X(x1,x2,,xn)

Y
(
y
1
,
y
2
,

,
y
n
)
Y\left(y_1,y_2,\ldots,y_n\right)
Y(y1,y2,,yn)
,则这两个向量的欧氏距离的计算公式为:
d
i
s
t
(
X
,
Y
)
=

i
=
1
n
(
x
i

y
i
)
2
dist\left(X,Y\right)=\sqrt{\sum_{i=1}^{n}\left(x_i-y_i\right)^2}
dist(X,Y)=i=1n(xiyi)2

体现在人脸识别中,两张人脸特征向量的欧氏距离越小,说明两个人越相似。当欧氏距离小于某一个值时,则可以认为他们是同一个人。基于这个结论,我们可以实现人脸识别。

2.2.4 人脸识别算法原理

dlib 实现的人脸检测方法便是基于图像的Hog特征,综合支持向量机算法实现的人脸检测功能,该算法的大致思路如下:

3 系统整体框架

3.1 整体流程图

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

图 12    系统流程图

3.2 更新人脸信息库部分

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

图 13    图 13 更新人脸信息库部分流程图

3.3 人脸识别部分

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

图 14    人脸识别部分流程图

3.4 进程切换部分

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

图 15    进程切换流程图

4 详细设计与实现

4.1 环境配置

虚拟环境配置:配置GPU环境【Win10基于anaconda虚拟环境】_(本人博客)

Anaconda安装dlib库:“pip install CMake”、“pip install Boost”、“conda install -c menpo dlib==19.8.1”

4.2 dlib实现人脸识别

4.2.1 系统功能

4.2.2 设计思路

双进程

父进程实现视频暂停后对图像中的人脸标记标签的功能,子进程控制摄像头动态捕捉(即视频)和暂停后静态图像的显示。

由于标签的类型需要用户输入,即进行I/O中断,经多次尝试和参考他人资料发现输入中断只能在主进程中进行,所以将标记标签的功能放在父进程中实现。

双进程是实现该系统的基础与核心,如果没有双进程,用户将无法对应着图像标记标签,整个更新人脸信息库的功能将不复存在。

进程间变量的共享

进程间不能通过global或直接传参的方式共享变量,必须要通过multiprocessing库中的Value函数来创建单值变量,列表、字典等数据结构需要使用multiprocessing库的子库Manager库中的函数来创建,且必须要通过函数传递这些变量。

双进程间共享的变量有“系统结束标志”(bool)、“用户输入标志”(bool)、“动态捕捉停止标志”(bool)以及共享列表“截图(暂停后的图像)”。三个布尔类型的共享变量用于控制两个进程的开始、结束和工作顺序;共享列表本质上只用到了一个元素空间,子进程接收到“进行截图”事件后会向父进程传递该共享列表,共享列表存储着截图(本质上是dlib::rectangle类型的迭代器),父进程接收到后将用户输入的每张人脸上的标签和特征信息保存在csv中,实现更新信息库。

更新人脸信息库

人脸信息库包含三部分:带人脸检测标记截图(.jpg文件)、每张人脸(.jpg文件)和每张人脸对应的128维特征向量(.csv文件,含标签等其他信息)。

因为一张图像可能包括多张人脸,所以两部分分开保存。
【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

人脸识别算法

采用欧式距离作为人脸匹配程度。计算目标人脸与选定的人脸信息库中人脸的“距离”,如果“距离”小于0.4,则被认为匹配上;如果“距离”大于0.02,则取最小“距离”对应的标签作为目标人脸的标签;如果“距离”小于0.02,则被认为是完美匹配,无需继续与信息库中剩下的人脸进行匹配。

为了提高匹配的效率,如果目标人脸匹配某个标签超过一定阈值,则可以直接认为匹配成功,结束遍历信息库。阈值选为信息库中人脸数据数量的一定比例。

按下’s’键后的事件

判断当前帧中是否存在人脸?

4.2.3 结果截图

初始界面
【人工智能】人脸识别系统【实验报告与全部代码】(QDU)
【人工智能】人脸识别系统【实验报告与全部代码】(QDU)
【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

更新人脸信息库

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)
【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

信息库更新情况

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)【人工智能】人脸识别系统【实验报告与全部代码】(QDU)
【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

人脸识别

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)
【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

4.3 ResNet实现人脸识别

4.3.1 设计思路

  1. ResNet残差神经网络

    手写ResNet残差神经网络,当然也可以直接调用pytorch中已经实现好的网络模型。
    单纯实现网络结构的ResNet模型不如pytorch自带模型,经过初始化参数和初始化BasicBlock,准确率与自带ResNet一致。

  2. 模型训练

    模型参数:learning_rate=0.1,epoch=5,batch_size=16;
    由于数据量过少,导致epoch过多出现过拟合现象,即训练集效果比较好,但是测试集准确率不升反降。

  3. 保存训练信息并绘制曲线

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)
【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

4.3.2 结果截图

多人人脸识别效果图

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

人脸表情识别

如果我们将标签再添加有关表情的描述,再使用这些标签对模型进行训练,那么就可以得到一个能够对简单表情进行识别的模型。

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)
【人工智能】人脸识别系统【实验报告与全部代码】(QDU)
【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

展示对八种表情的识别效果图

【人工智能】人脸识别系统【实验报告与全部代码】(QDU)
【人工智能】人脸识别系统【实验报告与全部代码】(QDU)

5 总结与展望

5.1 难点分析

5.2 局限分析

5.3 总结

在完成实验之前先比较系统地学习并整理了pytorch的相关知识,对pytoch有了比较完整的了解,但是动手写代码的能力还是比较弱。通过手写ResNet残差神经网络,对ResNet有了一定程度的理解,但是在使用opencv的级联分类器进行人脸检测时会出现人脸必须很大才能检测出来的情况,且使用ResNet网络进行人脸识别需要对参数进行优化,而参数优化的知识我了解的比较少,因此只是进行了简单的网络效果分析,最终分类效果不符合预期,因此重新思考其他实现方式。

询问同学后了解到比较方便且效果比较好的人脸识别库dlib,该库的优势在于集成度高,三个函数解决人脸检测和人脸识别,且效果比较理想,遂采用该库实现人脸识别系统。网上对该库的深入讲解比较少,所以我对该库函数的理解仅停留在会用的程度上,至于底层的实现方式不是很清楚。

整个实验从学习pytorch到学习ResNet18,再到了解dlib并实现系统花费了不少时间,收获颇丰,最主要的收获还是对pytorch库有了一定程度的认识,为之后学习人工智能的相关知识打下基础。

6 代码

绘制 128 维人脸关键点程序

import numpy as np
import cv2
import dlib
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor("./data_dlib/shape_predictor_68_face_landmarks.dat")
# cv2读取图像
img = cv2.imread("C:/Users/23343/Desktop/1.png")
# 取灰度
img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
# 人脸数rects
rects = detector(img_gray, 0)
for i in range(len(rects)):
    landmarks = np.matrix([[p.x, p.y] for p in predictor(img,rects[i]).parts()])
    for idx, point in enumerate(landmarks):
        # 68点的坐标
        pos = (point[0, 0], point[0, 1])
        print(idx,pos)
        # 利用cv2.circle给每个特征点画一个圈,共68个
        cv2.circle(img, pos, 5, color=(0, 255, 0))
        # 利用cv2.putText输出1-68
        font = cv2.FONT_HERSHEY_SIMPLEX
        cv2.putText(img, str(idx+1), pos, font, 0.8, (0, 0, 255), 1,cv2.LINE_AA)
cv2.namedWindow("img", 2)
cv2.imshow("img", img)
cv2.imwrite("C:/Users/23343/Desktop/11.png", img)
cv2.waitKey(0)

人脸识别程序

"""
作者:LJR
日期:2022 04 24
"""
""" 导入库 """
import os
import time
import dlib
import shutil # 就用了这个库的一句话shutil.rmtree(path),删除一个文件夹,无论里面是否有文件或文件夹
import datetime
import cv2 as cv
import numpy as np
import pandas as pd
from multiprocessing import Process, Value, Manager
""" 规定一些参数 """
PICTURE_SIZE = 64
PATH_FACE_INFO = "./Face_128D_Information/"
PATH_WHOLE_PICTURE_SAVE = './Picture_Captured/'
PATH_EACH_FACE_SAVE = './Each_Face/'
SHAPE_PREDICTOR_PATH = "./data_dlib/shape_predictor_68_face_landmarks.dat"
FACE_RECOGNITION_MODEL_PATH = "./data_dlib/dlib_face_recognition_resnet_model_v1.dat"
COLORS = [(0,255,0), (0,0,255), (0,128,255), (0,255,255), (255,0,0), (0,255,255), (255,0,128), (0,0,0), (128,128,128), (255,128,255)] # BGR!!!
COLORS_STRING = ["绿", "红", "橙", "黄", "蓝", "靛", "紫", "黑", "灰", "粉"]
COLORS_STRING_ENIGLISH = ["Green", "Red", "Rrange", "Yellow", "Blue", "Indigo", "Purple", "Black", "Grey", "Pink"]
""""""
detector = dlib.get_frontal_face_detector() # 加载人脸检测器(返回用于确定人脸位置的函数)
predictor = dlib.shape_predictor(SHAPE_PREDICTOR_PATH) # 加载人脸68特征点检测器(返回用于确定人脸68个特征点位置的函数)
face_recognition_model = dlib.face_recognition_model_v1(FACE_RECOGNITION_MODEL_PATH) # 加载人脸识别模型(返回用于人脸识别的模型)
""" 
定义函数: 
函数功能:通过动态捕捉的方式更新人脸信息库
"""
def DynamicUpdateDataSet (flag_over, flag_input, flag_stop_capture, shared_picture) :
    cap = cv.VideoCapture(0, cv.CAP_DSHOW) # 使用自带摄像头 # 由于只会打开一下,所以下面要通过死循环让摄像头一直开着
    while cap.isOpened():
        state, frame = cap.read() # state表示是否读取到图片,frame表示截取到一帧的图片
        dets = detector(frame, 1) # 返回frame上每个人脸(矩形)的位置信息,包括矩形的上边、下边、左边和右边所在像素(投影成点后的像素位置) # 第二个参数代表将原始图像是否进行放大,1表示放大1倍再检查,提高小人脸的检测效果 # 对应的,也就是我们扫描框变小了
        key_pressed = cv.waitKey(1)  # 每 1ms 捕捉一次键入事件
        if key_pressed & 0xFF == 27:  # 如果键入的最后八位为27,则说明是Esc键,跳出,关闭摄像头 # 不加这句话会卡死,无法顺利显示捕捉到的画面
            flag_over.value = True
            break
        elif key_pressed == ord('s'):  # 如果键入的是 's',则保存当前帧(具体操作见文档)
            WhenEnterS (frame, dets, flag_input, flag_stop_capture, shared_picture)
        for idx, rect in enumerate(dets) :
            top = max (rect.top(), 0) # 上
            bottom = max (rect.bottom(), 0) # 下
            left = max (rect.left(), 0) # 左
            right = max (rect.right(), 0) # 右
            # face_image = frame[top:bottom, left:right] # 截取人脸部分的像素(图片) # 这里采用第一象限坐标进行裁剪
            # face_image = cv.resize (face_image, (PICTURE_SIZE, PICTURE_SIZE)) # 调整图片大小
            cv.rectangle(frame, (left, top), (right, bottom), COLORS[idx], 3)  # 在 frame 上标记出识别到的人脸 # 五个参数的含义分别是 图片、矩形左上角像素位置、右下角像素位置、框RGB颜色、粗细 # 注意这里的坐标是图片坐标,不是第一象限坐标了
        cv.imshow("Face Capture", frame)
    cap.release()  # 释放摄像头
    cv.destroyAllWindows() # 销毁所有窗口
"""
定义函数:
函数功能:当在进行动态捕捉图像更新人脸信息库时,按下'S',则执行该函数
"""
def WhenEnterS (frame, dets, flag_input, flag_stop_capture, shared_picture) :
    if len(dets) == 0 :
        print()
        print("捕捉失败,不存在人脸!")
    else :
        print()
        print("捕捉成功,当前帧包含人脸数为 " + str(len(dets)))
        print("请根据矩形框颜色输入每张人脸对应的标签(提示:请输入英文字母,回车表示一个标签输入完成,若对应标签未知则应输入字符串'Unkown',大小写敏感)")
        for idx, rect in enumerate(dets) :
            top = max(rect.top(), 0)  # 上
            bottom = max(rect.bottom(), 0)  # 下
            left = max(rect.left(), 0)  # 左
            right = max(rect.right(), 0)  # 右
            cv.rectangle(frame, (left, top), (right, bottom), COLORS[idx], 3)
        flag_input.value = True # 是否可以输入(标志)
        flag_stop_capture.value = True # 是否暂停画面(标志)
        shared_picture[:] = [] # 清空Listproxy类型的列表
        shared_picture.append((frame, dets)) # 传入整幅图片和图片中每个人人脸
        while flag_stop_capture.value :
            cv.imshow('Face Capture', frame)
            cv.waitKey(100) # 后期可以添加,虽然捕捉到图像,但是允许取消输入的功能
"""
定义函数:
函数功能:控制输入,修改标志
"""
def keepinput(username, database_name, flag_over, flag_input, flag_stop_capture, shared_picture):
    path_whole_picture_save = PATH_WHOLE_PICTURE_SAVE + database_name # 每张图片保存路径(信息库文件夹路径)
    path_each_face_save = PATH_EACH_FACE_SAVE + database_name # 每张人脸图片保存路径(文件夹路径)
    database_name_csv = database_name + ".csv" # csv文件名(信息库名.csv)
    path_face_128D_info = PATH_FACE_INFO + database_name_csv # 每张人脸128D信息保存路径(csv文件路径)
    labels_input = [] # 一幅图中每个人的标签
    while True:
        time.sleep(0.1) # 让摄像头先打开,否则会直接进入IO中断
        while not flag_input.value and not flag_over.value:
            time.sleep(0.1) # 持续等待按下s的事件
        if flag_over.value : # 程序结束了,这个死循环也要结束
            break
        current_time = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S")  # 当前时间
        frame = shared_picture[0][0]
        dets = shared_picture[0][1]
        for i, rect in enumerate(dets):
            label = input(COLORS_STRING[i] + " : ")
            labels_input.append(COLORS_STRING_ENIGLISH[i] + "-" + label)
            top = max(rect.top(), 0)  # 上
            bottom = max(rect.bottom(), 0)  # 下
            left = max(rect.left(), 0)  # 左
            right = max(rect.right(), 0)  # 右
            face = frame[top:bottom, left:right]
            path = path_each_face_save + "/" + current_time + " Creater-" + username + " Label-" + label + ".jpg"
            cv.imwrite(path, cv.resize(face, (PICTURE_SIZE, PICTURE_SIZE))) # 保存单张人脸照片
            print(path)
            # shape = predictor(face, rect)  # 关键点检测
            shape = predictor(frame, rect)
            # face_descriptor = face_recognition_model.compute_face_descriptor(face, shape) # 描述子提取,128D向量
            face_descriptor = face_recognition_model.compute_face_descriptor(frame, shape)
            face_descriptor = np.array(face_descriptor) # 转换为numpy array
            face_info = [path.split('/')[-1], label] # 只需要文件名,不需要路径信息
            face_info.extend(face_descriptor)
            face_info = np.array(face_info)
            pd.DataFrame(face_info.reshape((1, 130))).to_csv(path_face_128D_info, mode='a', header=False, index=False) # 无论是否存在,直接append就行,不存在会新建
        # 保存图片
        # 图片格式为:捕获照片时间 + 图像中人脸框颜色与对应的标签 + .jpg
        cv.imwrite(path_whole_picture_save + "/" + current_time + " Creater-" + username + " " + " ".join(labels_input) + ".jpg", shared_picture[0][0])
        print()
        print("【状态提示】已更新人脸信息")
        flag_input.value = False
        flag_stop_capture.value = False
"""
定义函数:
函数功能:人脸识别
"""
def FaceRecognitioin(path_face_info) :
    face_info = pd.read_csv(path_face_info, header=None)  # 忽略列索引
    descriptors = face_info.iloc[:, 2:].values  # ndarray
    labels = face_info.iloc[:, 1].values
    cap = cv.VideoCapture(0, cv.CAP_DSHOW)  # 使用自带摄像头 # 由于只会打开一下,所以下面要通过死循环让摄像头一直开着
    while cap.isOpened():
        state, frame = cap.read()  # state表示是否读取到图片,frame表示截取到一帧的图片
        dets = detector(frame,
                        1)  # 返回frame上每个人脸(矩形)的位置信息,包括矩形的上边、下边、左边和右边所在像素(投影成点后的像素位置) # 第二个参数代表将原始图像是否进行放大,1表示放大1倍再检查,提高小人脸的检测效果 # 对应的,也就是我们扫描框变小了
        key_pressed = cv.waitKey(1)  # 每 1ms 捕捉一次键入事件
        if key_pressed & 0xFF == 27:  # 如果键入的最后八位为27,则说明是Esc键,跳出,关闭摄像头 # 不加这句话会卡死,无法顺利显示捕捉到的画面
            break
        for faceidx, rect in enumerate(dets):
            top = max(rect.top(), 0)  # 上
            bottom = max(rect.bottom(), 0)  # 下
            left = max(rect.left(), 0)  # 左
            right = max(rect.right(), 0)  # 右
            face_img = frame[top:bottom, left:right]
            # shape = predictor(face_img, rect)  # 传入图片和矩形框位置,返回68个关键点的位置
            shape = predictor(frame, rect)
            # face_descriptor = face_recognition_model.compute_face_descriptor(face_img, shape)  # 人脸描述子
            face_descriptor = face_recognition_model.compute_face_descriptor(frame, shape)
            face_descriptor = np.array(face_descriptor)
            min_dist = 99999
            best_label = None
            for i, des in enumerate(descriptors):  # 遍历人脸信息库
                dist = np.linalg.norm(des - face_descriptor)  # 标准差
                if dist > 0.4:  # 设置阈值 大于0.4表示匹配失败
                    pass
                    # Unkown
                elif dist < min_dist:
                    min_dist = dist
                    best_label = labels[i]
                    # 更新最佳匹配人脸标签
            if best_label == None:
                best_label = "Unkown"
            cv.rectangle(frame, (left, top), (right, bottom), COLORS[faceidx], 2)  # 在img上画矩形
            cv.putText(frame, best_label, ((left + right) // 2 - len(best_label) * 17 // 2, top - 10),
                       cv.FONT_HERSHEY_PLAIN, 2, COLORS[faceidx], 2)  # 各参数依次是:图片,添加的文字,左上角坐标,字体,字体大小,颜色,字体粗细 # 坐标必须为int
        cv.imshow("Face Recognition", frame)
    cap.release()  # 释放摄像头
    cv.destroyAllWindows()  # 销毁所有窗口
"""
定义函数:
函数功能:更新人脸信息库
"""
def UpdateDataset (username, database_name):
    flag_over = Value('b', False)  # 整个程序是否结束
    flag_input = Value('b', False)  # 进程共享变量
    flag_stop_capture = Value('b', False)  # 进程共享变量
    shared_picture = Manager().list()
    Process(target=DynamicUpdateDataSet, args=(flag_over, flag_input, flag_stop_capture, shared_picture)).start()  # 摄像头进程 # PS:进程共享变量必须要传入变量不能使用global
    keepinput(username, database_name, flag_over, flag_input, flag_stop_capture, shared_picture)  # input 函数只能在主进程调用!
if __name__ == "__main__" :
    print()
    print("#### 欢迎进入 LJR 的人脸识别系统 #####")
    print("===================================")
    print()
    username = input("【操作提示】请输入您的用户名(由下划线、字母、数字组成):")
    while True :
        flag_invalid_username = False
        for ch in username:
            if not ch.isdigit() and not ch.isalpha() and ch != "-" :
                flag_invalid_username = True
                break
        if flag_invalid_username :
            print()
            print("【#警 告#】输入非法!")
            username = input("【操作提示】请重新输入您的用户名(由下划线、字母、数字组成):")
        else :
            print()
            print("【状态提示】欢迎 " + username + " 的访问!")
            break
    while True :
        print()
        print("主菜单")
        print("------------------")
        print("0)退出")
        print("1)更新人脸信息库")
        print("2)人脸识别")
        print()
        op = input("【操作提示】请输入您的选择:")
        while op != "0" and op != "1" and op != "2":
            print("【#警 告#】输入非法!")
            op = input("【操作提示】请重新输入您的选择:")
        if op == "1" : # 更新人脸信息库
            print()
            print("【状态提示】已进入更新人脸信息库模式")
            """
            0) 退出
            1) 新建信息库
            2) 向已存在的信息库中添加人脸信息
            3) 删除某一整个信息库
            4) 删除某个信息库中的某些人脸信息
                ① 按时间删除(start_datetime ~ end_datetime)
                ② 按创建者删除 (creater) # 允许输入多个创建者;可以通过空格将多个创建者名称分开(因为输入的时候我们要求创建者的用户名不包含空格)
                ③ 按标签删除 (label) # 存在某个标签的人脸信息删除
            """
            while True:
                print()
                print("菜单")
                print("------------------")
                print("0) 退出")
                print("1) 向信息库中添加人脸信息")
                print("2) 删除信息库")
                op_update = input("【操作提示】请输入您的选择:")
                while op_update != "0" and op_update != "1" and op_update != "2":
                    print("【#警 告#】输入非法!")
                    op_update = input("【操作提示】请重新输入您的选择:")
                if int(op_update) == 1 or int(op_update) == 2: # 添加 or 删除
                    database_name = input("请输入信息库名称:")
                    database = database_name + ".csv"
                    if int(op_update) == 1 : # 添加
                        if database in os.listdir(PATH_FACE_INFO): # 已经存在该信息库
                            print("【状态提示】该信息库已经存在,接下来您将向该信息库中添加人脸信息")
                            print()
                        else : # 不存在该信息库
                            try :
                                os.mkdir(PATH_WHOLE_PICTURE_SAVE + database_name)
                            except FileExistsError :
                                pass
                            try :
                                os.mkdir(PATH_EACH_FACE_SAVE + database_name)
                            except FileExistsError :
                                pass
                            print("【状态提示】该信息库不存在")
                            print("【状态提示】已新建该信息库,接下来您将向该信息库中添加人脸信息")
                            print()
                        print("【操作提示】")
                        print("1)按下'Esc'键,退出更新人脸信息库模式")
                        print("2)按下's'键,将当前摄像头拍摄到的人脸添加到人脸信息库中,标签由用户从控制台输入")
                        UpdateDataset(username, database_name)
                    elif int(op_update) == 2 : # 删除
                        print("【操作提示】删除后将无法恢复,是否确认删除?")
                        op_yes_no = input("【操作提示】是 | 否 :")
                        while op_yes_no != "是" and op_yes_no != "否":
                            print("【#警 告#】输入非法!")
                            op_yes_no = input("【操作提示】是 | 否 :")
                        print()
                        if op_yes_no == "是": # 确定删除
                            if database in os.listdir(PATH_FACE_INFO) : # 已经存在该信息库
                                shutil.rmtree(PATH_WHOLE_PICTURE_SAVE + database_name)
                                shutil.rmtree(PATH_EACH_FACE_SAVE + database_name)
                                os.remove(PATH_FACE_INFO + database)
                                print("【状态提示】成功删除," + database_name + " 已被删除")
                            else : # 不存在该信息库
                                print("【状态提示】删除失败," + database_name + " 不存在")
                        else : # 取消删除
                            print("【状态提示】已取消删除")
                else : # 退出
                    print()
                    print("【状态提示】已退出人脸识别模式")
                    break
            # --------------------------------------------------------------------------------
        elif op == "2" : # 人脸识别
            print()
            print("【状态提示】已进入人脸识别模式")
            database_name = input("【操作提示】请输入您要用于人脸识别人脸信息库名称:")
            database = database_name + ".csv"
            if database in os.listdir(PATH_FACE_INFO):
                print("【状态提示】已选定 " + database_name + " 信息库")
                print("【操作提示】按下'Esc'键,退出人脸识别模式")
                FaceRecognitioin(PATH_FACE_INFO + database)
            else :
                print("【状态提示】" + database_name + " 信息库不存在,无法进行人脸识别")
            print()
            print("【状态提示】已退出人脸识别模式")
            # --------------------------------------------------------------------------------
        elif op == "0" : # 退出
            print()
            print("【状态提示】成功退出,欢迎再次访问!")
            break

发表回复