ModelArts-Lab 第十四期:学习心得及拓展——人脸检测MTCNN和人脸识别Facenet 原理和实践

举报
滕云 发表于 2019/11/10 15:34:08 2019/11/10
【摘要】 本文心得来源学习【ModelArts-Lab AI实战营】第十四期:人脸识别案例 ,通过该次学习,了解人脸检测MTCNN和人脸识别Facenet 原理, 并使用 MTCNN 模型进行人脸检测,使用 FaceNet 模型为每个检测到的人脸创建人脸embedding,然后使用一个线性支持向量机(Linear Support Vector Machine (SVM),SVM)分类器模型来预测给定人脸

本文心得来源学习【ModelArts-Lab AI实战营】第十四期:人脸识别案例 ,通过该次学习,了解人脸检测MTCNN和人脸识别Facenet 原理并进行了实践。

首先我们来看看人脸识别的定义:

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

我们在实践中使用了两大算法:一个是人脸检测;另一个是人脸识别。

  • 人脸检测用的是 MTCNN 算法

  • 人脸识别用的是 FaceNet 算法

       两者都是深度学习算法,在图像领域深度学习算法已经远远领先于传统的机器学习算法,而且视觉领域工业界的应用也是深度学习算法居多。


一、预备知识

在学习人脸检测前需要了解的理论知识:

IoU

       IoU 的全称为交并比(Intersection-over-Union,IoU)目标检测中使用的一个概念,通过这个名称我们大概可以猜到 IoU 的计算方法。

,是产生的候选框(candidate bound)与原标记框(ground truth bound)的交叠率,即它们的交集与并集的比值。最理想情况是完全重叠,即比值为1。

1573370303579720.jpeg

NMS

非极大值抑制,它就是一个寻找局部最大值的过程。

在进行目标检测时一般会采取窗口滑动的方式,同一个人可能有好几个框(每一个框都带有一个分类器得分)

而我们的目标是一个人只保留一个最优的框:

于是我们就要用到非极大值抑制,来抑制那些冗余的框: 抑制的过程是一个迭代-遍历-消除的过程。

(1)将所有框的得分排序,选中最高分及其对应的框:

(2)遍历其余的框,如果和当前最高分框的重叠面积(IOU)大于一定阈值,我们就将框删除。

比如下面这张图,一个人脸生成了 3 个框,红色框是得分最高的:

1573370356584799.png


我们需要将红色的框拿出来,然后用剩下的绿框分别跟它计算 IoU,只要 IoU 大于一定阈值(一般为 0.7),那么我们就将其删掉,最终只剩下一个红色的框。

(3)从未处理的框中继续选一个得分最高的,重复上述过程。

一般会得出一个得分(score),比如人脸检测,会在很多框上都有得分,然后把这些得分全部排序。选取得分最高的那个框,接下来计算其他的框与当前框的重合程度(IoU),如果重合程度大于一定阈值就删除,因为在同一个脸上可能会有好几个高得分的框,都是人脸但是不需要那么框我们只需要一个就够了。

1573370403482174.png


二、MTCNN原理

(一)MTCNN 出现的背景

    MTCNN(Multi-task convolutional neural network) 出现之前,人脸检测主要有传统方法 DPM 和深度学习方法 Faceness、CascadeCNN,但是这些方法在工业级的应用上并不是特别理想。由于人脸检测效果一般,致使建立在检测基础之上的人脸对齐效果也不是很突出。

   在这个大背景下,MTCNN 应运而生,发表在 2016 年 ECCV 上。MTCNN将人脸区域检测与人脸关键点检测放在了一起,它的主题框架类似于cascade。总体可分为P-Net、R-Net、和O-Net三层网络结构。

架构亮点

图像金字塔

在将图像放入模型之前,将图像按照不同尺度缩小成 n 个图像,然后将 n 个不同大小的图像同时放入网络进行训练。可以说跟多尺度训练有异曲同工之妙。在不扩增数据集的基础上,变相的做了一个数据增强操作。

1573370446230471.png


减小卷积核大小,增加模型深度

相比与其他多分类物体检测和分类任务,人脸检测是二分类问题,不需要更大的卷积核而是需要更小的卷积核。因此,将卷积核由 5*5 变为 3*3 并增加深度。这样在减小模型大小的基础上还提高了模型精度。

(二)MTCNN 算法架构

1、数据准备

   在标注好的人脸图片上进行随机切割,根据切割到的边框和真实的人脸框进行 IoU 计算,将 IoU > 0.65 的划分为 Positive 正样本数据,将 IoU < 0.3 的划分为 Negative 负样本数据,将 IoU 在 0.4-0.65 之间的划分为 Part 数据。

将标注好的 5 个特征点划分为 Landmark face 数据。

这四种数据之间的比例为:

negative/positive/part faces/landmark = 3:1:1:2

这四种数据的用途:

  • negative 和 positive 用于人脸分类

  • positive 和 part faces 用于边框回归

  • landmark face 用于人脸特征点定位(人脸对齐/关键点检测)

2、模型结构

整个 MTCNN 有三个不同的 CNN 层组成。

(1)Proposal Net结构图如下:

1573370505274093.jpeg

它的输入数据为:所有训练样本 resize 为 shape=(12x12x3) 的图像,输出的结果有 3 种:

  • face classification:输入图像为人脸图像的概率

  • bounding box:输出矩形框位置信息

  • facial landmark localization:输入人脸样本的 5 个关键点位置

(2)Refinement Net

结构图如下:

1573370549720170.jpeg

它的输入数据为:所有训练样本 resize 为 shape=(24x24x3) 的图像。输出结果跟 PNet 输出结果类型一样, 也是那 3 种。

(3)Output Net

结构图如下:

1573370587273086.jpeg

它的输入数据为:所有训练样本 resize 为 shape=(48x48x3) 的图像。输出结果跟以上两个结构输出结果类型一样,也是那 3 种。

Summary

从 PNet 到 RNet 再到 ONet,网络的输入图像尺寸越来越大,结构越来越深,提取的特征也越具有表现能力。这体现了 coarse-to-fine 的策略:

  • 第一步通过浅层的 CNN 选出候选窗口;

  • 第二步用更复杂的 CNN 过滤掉没有脸部的窗口;

  • 第三步用更强大的 CNN 调优结果。


(三)损失函数

MTCNN 损失有三个:

  • 人脸分类损失

  • 边框定位回归损失

  • 特征点定位回归损失

1、人脸分类损失

一个二分类的交叉熵损失函数:

1573370640645322.png

其中

  • 输入:x

  • 预测:p

  • groundtruth label:y

2、边框定位回归损失

1573370679972437.png

其中

  • y’:预测值边框

  • y:真值框(groundtrue box)

  • y 值为 (y,x,h,w),其中 y,x 为左上角坐标

3、特征点定位回归损失

1573370716826297.png

其中

  • y’:预测值

  • y:真值

  • 包括:左眼、右眼、鼻子、左嘴角、右嘴角 5 个点。$y∈R^{10}$

注意:一般的损失函数之所以用差的平方,主要是通过这种处理可以得到正的损失值,如果有正有负的话两者会相互抵消。由于绝对值不适合求导,所以一般不用它当做损失函数。在人脸算法中和一般的回归算法中,欧几里得损失函数比较常用。

多源训练

在不同的 CNN 层,对不同的损失函数, 比重不同。在 PNet 和 RNet 中, 主要任务是将是不是人脸给区分开来,所以 人脸分类损失 占的比重大;在 ONet 中, 主要目标是将人脸的关键点找出来,所以特征点定位回归损失占的比重稍微大点。

训练技巧:OHSM(Online Hard Example Mining)

在前向传播中,对于全部的样本,根据 Loss 进行由大到小进行排序,选择前 70%。

在反向传播中,只对这前 70% 的样本进行反向传播,这样,忽略了对检测更有帮助的 easy example,加强了对检测更有帮助的 hard example 的惩罚。

训练流程

  • 准备数据,包括人脸检测数据和关键点检测数据

  • 数据生成,通过随机裁剪在数据集上进行剪裁, 然后保存成 12*12 大小的图片,根据 IoU 的不同,划分成 pos、neg、part、landmark

  • 用生成的 4 种数据训练 PNet 网络

  • 用训练好的 PNet 模型生成难训练的人脸检测数据样本,图片大小是 24*24

  • 生成 24*24 大小的 landmark 数据

  • 训练 RNet 网络

  • 用训练好的 PNet 模型生成难训练的人脸检测数据样本,图片大小是 48*48

  • 生成 48*48 大小的 landmark 数据

  • 训练 ONet 网络

也就说整个 MTCNN 需要依次训练 3 个模型结构。

三、faceNet原理

    FaceNet Github地址: https://github.com/davidsandberg/facenet

   参考资料:https://blog.csdn.net/fire_light_/article/details/79592804

       Google工程师Florian Schroff,Dmitry Kalenichenko,James Philbin提出了人脸识别FaceNet模型,该模型没有用传统的softmax的方式去进行分类学习,而是抽取其中某一层作为特征,学习一个从图像到欧式空间的编码方法,然后基于这个编码再做人脸识别、人脸验证和人脸聚类等。

    FaceNet主要用于验证人脸是否为同一个人,通过人脸识别这个人是谁。FaceNet的主要思想是把人脸图像映射到一个多维空间,通过空间距离表示人脸的相似度。同个人脸图像的空间距离比较小,不同人脸图像的空间距离比较大。这样通过人脸图像的空间映射就可以实现人脸识别,FaceNet中采用基于深度神经网络的图像映射方法和基于triplets(三联子)的loss函数训练神经网络,网络直接输出为128维度的向量空间。

    FaceNet的网络结构如下图所示,其中Batch表示人脸的训练数据,接下来是深度卷积神经网络,然后采用L2归一化操作,得到人脸图像的特征表示,最后为三元组(Triplet Loss)的损失函数。

1573370763590422.png


四、人脸识别系统实践

下面,我们实现一个人脸检测系统来预测给定人脸的身份。

我们将使用 MTCNN 模型进行人脸检测,使用 FaceNet 模型为每个检测到的人脸创建人脸embedding,然后使用一个线性支持向量机(Linear Support Vector Machine (SVM),SVM)分类器模型来预测给定人脸的身份。

既然是人脸识别,肯定要有已知人脸的数据库,不想去网上扒拉数据集了,我们自己做一个吧,比如我们

公司新到了两位员工,分别是巩同学和章同学,

把两位大神的人像的收集到 \facenet-ty\face-img\ 文件夹下:

其中\tain目录下收集的是训练集,每新增一个人需新建一个文件夹,巩同学和章同学各有10张单人照片分别放到“gongli”、“zhangziyi”目录下;因为仅用于测试,我们的人脸数据库只有两人的照片,如需新增图库,只需在train目录下继续新建文件夹,然后将单人照片放在里面即可,图片名称可以是任意,注意制作人脸数据库时,必须使用单人照片;

\val 目录下收集的是验证集,建立同样的目录文件,两人各有6张图片;

这些照片提供了各种方向、光照和各种大小的人脸。重要的是,每张照片都包含一张人脸。

我们使用这个数据集作为分类器的基础,对train数据集进行训练,并对val数据集中的人脸进行分类。

1、检测人脸

我们检测数据集中每张照片的人脸,并将数据集缩减为一系列人脸,

首先我们需要创建一个人脸检测器,在\facenet-ty\face-img\ 目录下,我们可以试着检测一下训练集中gongli的10张照片,并创建包含10张人脸的图像,图像有两行,每行5张人脸,具体代码如下:

from os import listdir

from PIL import Image

from numpy import asarray

from matplotlib import pyplot

from mtcnn.mtcnn import MTCNN

#我们创建一个函数 extract_face() ,它将从加载的文件名加载照片,并将提取的人脸调整为输入的尺寸,

#它假定每张照片包含一张人脸,并将返回检测到的第一张人脸。

def extract_face(filename,required_size=(160,160)):

#第一步是以 NumPy 数组的形式加载图像,我们可以使用 PIL 库和 open() 函数来实现。

#并将图像转换为 RGB,以防图像出现 alpha 通道或变成黑白。

   image=Image.open(filename)

   image=image.convert('RGB')

   pixels = asarray(image)

   

#接下来,我们可以创建一个 MTCNN 人脸检测器类detector ,并使用它来检测加载的照片中所有的人脸。

   detector = MTCNN()

   results = detector.detect_faces(pixels)

#结果是一个边界框列表,其中每个边界框定义了边界框的左下角,以及宽度和高度。

#如果我们假设照片中只有一张人脸用于实验,我们可以确定边界框的像素坐标如下。

#有时候库会返回负像素索引,可以通过取坐标的绝对值来解决这一问题。

 

 # extract the bounding box from the first face

   x1, y1, width, height = results[0]['box']  

   x1, y1 = abs(x1), abs(y1)

   x2, y2 = x1 + width, y1 + height

#我们可以使用这些坐标来提取人脸。

   face = pixels[y1:y2, x1:x2]

   # resize pixels to the model size

#然后我们可以使用 PIL 库将这个人脸的小图像调整为所需的尺寸;

#具体而言,模型需要形状为 160x160 的正方形输入。

   image = Image.fromarray(face)

   image = image.resize((160, 160))

   face_array = asarray(image)

   return face_array

folder='./face-img/train/gongli/'

i=1

for filename in listdir(folder):

   path=folder+filename

   face=extract_face(path)

   print(i,face.shape)

   pyplot.subplot(2,5,i)

   pyplot.axis('off')

   pyplot.imshow(face)

   i += 1

pyplot.show()


1573370817896989.png

接下来,我们扩展这个示例代码,遍历给定数据集的每个子目录(例如“train”或“val”),提取出人脸,并为每个检测到的人脸准备一个以名称作为输出标签的数据集。

下面的 load_dataset() 函数输入目录名称,如“face-img/train/”,为每个子目录(人名)检测人脸,为每个检测到的人脸分配标签,标签可以从目录名中提取。

def load_faces(directory):

faces=list()

for filename in listdir(directory):

path = directory+filename

print(path)

face = extract_face(path)

faces.append(face)

return faces

def load_dataset(directory):

x,y = list(),list()

for subdir in listdir(directory):

path=directory+subdir+'/'

if not isdir(path):

continue

print(path)

faces = load_faces(path)

# create labels

labels = [subdir for _ in range(len(faces))]

# summarize progress

print('>loaded %d examples for class: %s' % (len(faces), subdir))

# store

x.extend(faces)

y.extend(labels)

return asarray(x), asarray(y)

trainX, trainy = load_dataset('face-img/train/')


1573370865826578.png


我们通过加载“train”数据集中的所有照片,然后提取人脸,得到 20 个样本,其中正方形人脸输入和类标签字符串作为输出。

然后加载“val ” 数据集,提供 12 个可用作测试数据集的样本。

testX, testy = load_dataset('face-img/val/')

最后将这两个数据集保存到“2-faces-dataset.npz”的压缩 NymPy 数组文件,存放在当前的工作目录中。

savez_compressed('2-faces-dataset.npz', trainX, trainy, testX, testy)

数据准备就绪后,就可提供给人脸检测模型。

2、创建人脸embedding

人脸embedding是一个向量,表示从人脸中提取的特征。并可以将其与为其他人脸生成的向量进行比较,

通过某种标准,如果一个向量与另一个向量距离较近,很可能是同一个人,而距离较远的较量则可能是不同的人。

我们要开发的分类器模型将人脸embedding作为输入,并预测该人脸的身份。FaceNet 模型将为给定的人脸图像生成此embedding。

FaceNet 模型可以用作分类器本身的一部分,或者,我们可以用 FaceNet 模型对人脸进行预处理,创建可以存储并用作分类器模型输入的人脸embedding。因为 FaceNet 模型既大又慢,不利于创建人脸embedding。考虑到速度问题,本次实践我们选择后一种方法,

首先我们可以预先计算训练中所有人脸的人脸embedding。

import numpy as np

from numpy import load

from numpy import expand_dims

from numpy import asarray

from numpy import savez_compressed

from keras.models import load_model

def get_embedding(model, face_pixels):

# scale pixel values

face_pixels = face_pixels.astype('float32')

# standardize pixel values across channels (global)  对图像像素进行标准化,以满足 FaceNet 模型的要求。

mean, std = face_pixels.mean(), face_pixels.std()

face_pixels = (face_pixels - mean) / std

#为了对 Keras 中的每一个样本进行预测,我们必须扩展维数,使人脸数组成为一个样本。

# transform face into one sample

#print(face_pixels.shape)   # 本例中为  (160, 160, 3)

samples = expand_dims(face_pixels, axis=0)

# print(samples.shape)     # 本例中为  (1, 160, 160, 3)

# make prediction to get embedding  然后,利用该模型进行预测,提取嵌入向量。

yhat = model.predict(samples)

return yhat[0]

# load the face dataset

data = np.load('2-faces-dataset.npz')

trainX, trainy, testX, testy = data['arr_0'], data['arr_1'], data['arr_2'], data['arr_3']

print('Loaded: ', trainX.shape, trainy.shape, testX.shape, testy.shape)

#Loaded:  (20, 160, 160, 3) (20,) (12, 160, 160, 3) (12,)

# load the facenet model

model = load_model('facenet_keras.h5')

print('Loaded Model')

# convert each face in the train set to an embedding

newTrainX = list()

for face_pixels in trainX:

   embedding = get_embedding(model, face_pixels)

   newTrainX.append(embedding)

newTrainX = asarray(newTrainX)

print(newTrainX.shape)

# convert each face in the test set to an embedding

newTestX = list()

for face_pixels in testX:

embedding = get_embedding(model, face_pixels)

newTestX.append(embedding)

newTestX = asarray(newTestX)

print(newTestX.shape)

# save arrays to one file in compressed format

savez_compressed('2-faces-embeddings.npz', newTrainX, trainy, newTestX, testy)

我们将训练数据集转换成 20 个人脸embeddings,每个embeddings由 128 个元素向量组成。验证数据集中的 12 个样本也被转换为了人脸embeddings

然后将得到的数据集保存到压缩的 NumPy 数组中,存在当前的工作目录中,名为“2-faces-embeddings.npz”。

3、执行人脸分类

首先,加载人脸嵌入数据集

from numpy import load

from sklearn.metrics import accuracy_score

from sklearn.preprocessing import LabelEncoder

from sklearn.preprocessing import Normalizer

from sklearn.svm import SVC

from matplotlib import pyplot

#load faces

data = load('2-faces-dataset.npz')

#trainX, trainy, testX, testy   取的是testX的faces数据

testX_faces = data['arr_2']

len(testX_faces)

# load face embeddings

data = load('2-faces-embeddings.npz')

trainX, trainy, testX, testy = data['arr_0'], data['arr_1'], data['arr_2'], data['arr_3']

print('Dataset: train=%d, test=%d' % (trainX.shape[0], testX.shape[0]))

#对人脸embeddings向量进行归一化处理

# normalize input vectors

in_encoder = Normalizer(norm='l2')

trainX = in_encoder.transform(trainX)

testX = in_encoder.transform(testX)

#接下来,需要将每个名人姓名的字符串目标变量转换为整数。

#这可以通过 scikit-learn 中的 LabelEncoder 类 来实现。

# label encode targets

out_encoder = LabelEncoder()

out_encoder.fit(trainy)

trainy = out_encoder.transform(trainy)

testy = out_encoder.transform(testy)

接下来,我们可以拟合一个模型。

在处理归一化人脸嵌入输入时,通常使用 线性支持向量机(SVM)。这是因为该方法在分离人脸嵌入向量方面非常有效。我们可以使用 scilit-learn 中的 SVC 类将线性 SVM 拟合到训练数据中,并将“kernel”属性设置为“linear”。我们可能还希望在以后进行预测时使用概率,可以通过将“probability”设置为“True”。

# fit model

model = SVC(kernel='linear',probability=True)

model.fit(trainX, trainy)

接下来,我们就可以对模型进行评估了。

通过使用拟合模型对训练和测试数据集中的每个样本进行预测,然后计算分类正确率。

# predict

yhat_train = model.predict(trainX)

yhat_test = model.predict(testX)

# score

score_train = accuracy_score(trainy, yhat_train)

score_test = accuracy_score(testy, yhat_test)

# summarize

print('Accuracy: train=%.3f, test=%.3f' % (score_train*100, score_test*100))

1573370920198753.png

4、模型预测

我们可以通过显示原始人脸和预测,让它变得更有趣。

首先,加载人脸数据集,特别是测试数据集中的人脸。

从测试集中随机选择一个样本,然后获取嵌入、人脸像素、期望的类预测以及类的相应名称。

from random import choice

# test model on a random example from the test dataset

selection = choice([i for i in range(testX.shape[0])])

random_face_pixels = testX_faces[selection]

random_face_emb=testX[selection]

random_face_class = testy[selection]

random_face_name = out_encoder.inverse_transform([random_face_class])

#进行维度转换

samples = expand_dims(random_face_emb,axis=0)

yhat_class = model.predict(samples)  

yhat_prob = model.predict_proba(samples)

#get name

class_index = yhat_class[0]

class_probability = yhat_prob[0,class_index]*100

predict_names = out_encoder.inverse_transform(yhat_class)

print('Predicted:%s (%.3f)' % (predict_names[0],class_probability))

print('Expected: %s' % random_face_name[0])

1573370964695098.png


#绘制结果

pyplot.imshow(random_face_pixels)

title='%s (%.3f)' % (predict_names[0],class_probability)

pyplot.title(title)

pyplot.show()

1573371008191198.png


每次运行时,都会从测试数据集中选择一个不同的随机样本进行预测。

在例中,选择了章同学的一张照片,并在绘制的图像的标题中正确地显示了预测的名称和概率。


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。