使用Keras和OpenCV创建人脸验证系统

使用Keras和OpenCV创建人脸验证系统

什么是人脸验证?

这可以认为是分类问题,使用匹配分数来验证人的身份。如果两个图像属于同一个人,则它们应具有较高的匹配度;如果两个图像属于两个不同的人,则匹配度应较低。

您可能想到的第一件事是,为什么不将捕获的图像与另一个像素逐个匹配呢?如果所捕获图像的像素值与另一个图像的像素值之间的距离(均方或绝对)很小,则它们应该对应于同一个人。但是,由于光线,位置或方向略有变化,图像中的像素值也会发生巨大变化,因此该方法实际上效果不佳。

那我们现在怎么办呢?这就是卷积神经网络(俗称CNN)发挥作用的地方。通过将每个图像嵌入d维向量空间,此类网络可以帮助我们更好地表示图像。然后评估图像嵌入的相似性。

解决问题的一些方法

  1. SVM:这里的想法是为训练集中的每个示例训练一个线性SVM分类器。
  2. DeepID:在这里,验证任务被视为人脸识别的子问题(为每个人分配标签)。
  3. 暹罗网络:这是基于以下思想:个人内部的距离应该比人际之间的距离小得多。

在研究暹罗网络之前,让我们首先讨论暹罗网络所基于的一个非常重要的概念。那就是One Shot Learning。

“One Shot Learning”是一个对象分类问题,主要出现在计算机视觉中,它试图从一个或几个训练样本中学习有关对象分类的信息。通常,在深度学习中,我们需要大量的机器学习数据,我们获得的机器学习数据越多则机器学习模型越好。但在人脸验证的情况下,一个人不可能获得数千张照片。其实,我们的大脑也不需要成千上万的人的照片来识别一个人。

对于面部验证任务,我们希望系统能够从一张/几张图像中判断一个人的身份。

如前所述,卷积神经网络(CNN)有助于图像的向量化表示。但是卷积神经网络(CNN)确实需要训练很多实例。此外,每次将一个新用户的图像添加到机器学习数据集中,都要对机器学习模型进行训练,这很不方便。因此,为什么不建立一个模型来学习两个不同的人之间的距离呢?这正是暹罗网络所要做的。

使用Keras和OpenCV创建人脸验证系统

图像x(1)被馈送给卷积神经网络(CNN),由卷积层和全连接层组成。卷积层提供了一个特征空间,而附加到卷积层的全连接层则在该空间中学习一个函数(大部分是非线性的)。我们最后得到的是一个特征向量(在特征向量中没有添加softmax激活,因为在这个阶段它不会被用于分类)。图像x(2)被输入到一个与上面完全相同的卷积神经网络(CNN)。在我们的例子中,我们有第三张图像x(3)被提供给相同的CNN。

我们选择x(1)作为anchor图像,x(2)作为正图像,x(3)作为负图像。anchor和正图像属于同一人,而负图像是另一个人。因此,我们的目标是最小化正图像和anchor之间的距离,同时最大化anchor和负图像之间的距离。

使用Keras和OpenCV创建人脸验证系统

该目标可以写为:

使用Keras和OpenCV创建人脸验证系统

我们可以添加一个超参数alpha

使用Keras和OpenCV创建人脸验证系统

这里A = x(1),P = x(2),N = x(3)

现在,我们如何将其构造为损失函数。

使用Keras和OpenCV创建人脸验证系统

这称为三重损失函数。如我们所见,此损失函数可确保max函数中的第一项不超过0。

Python实现

创建数据

这是创建机器学习数据集的Python代码。它使用称为Haar frontal face classifier 的预训练分类器,该分类器以级联方式识别人脸。下面的代码存储使用网络摄像头捕获的20张人脸图像,并将其存储在文件夹中。

import osimport cv2# This function can be used during sign-up of a userdef create_dataset():    # The file containing the pretrained classifier     haar_file = 'haarcascade_frontalface_default.xml'    # All the faces data will be present this folder     dataset = './dataset'    if not os.path.exists(dataset):        os.mkdir(dataset)    sub_data = input("Enter your username: ")    # Use the username as path name    path = os.path.join(dataset, sub_data)     # Add a verfication for this step    if not os.path.exists(path):        os.mkdir(path)            # Image to be resized to this shape    (width, height) = (224, 224)             # Make the cascade classifier object    face_cascade = cv2.CascadeClassifier(haar_file)     webcam = cv2.VideoCapture(0)      # The program loops until it has 30 images of the face.     count = 0    while count < 20:        # Read from the webcam        (_, im) = webcam.read()                # Convert to grayscale        gray = cv2.cvtColor(im, cv2.COLOR_BGR2RGB)         # Detect the face        faces = face_cascade.detectMultiScale(gray, 1.3, 4)                 face_resize = None        for (x, y, w, h) in faces:            # The classifier seemed to scrap the chin and hair. Adjustments made to accomodate those.            face = im[y-60 : y+h+60, x-20 : x+w+20]             face_resize = cv2.resize(face, (width, height))             cv2.imwrite('% s/% s.png' % (path, count), face_resize)         count += 1        cv2.imshow('OpenCV', im)         key = cv2.waitKey(100)         if key == 27:             break

使用Keras和OpenCV创建人脸验证系统

创建模型并进行训练

创建数据集时,您可以看到我在现有图像上填充了一些值(此处宽度为22,高度为12)。之所以如此,是因为使用了在ImageNet数据集上预训练的VGG16模型,该神经网络模型期望输入图像的尺寸为(224、224、3),而我们使用的数据集的每个图像都为(200、180、3)。

对于每个人(由数据集文件夹内的文件夹标识的人),我们存储5个(A,P,N)的triplets。然后对它们进行训练,该模型本身包含三个VGG16模型,并由一个实现三重损失函数的Lambda层连接。

# Importing the required modulesfrom keras import backend as K, modelsfrom keras.models import *from keras.layers import *from keras.layers.normalization import BatchNormalizationfrom keras.applications import VGG16from keras.regularizers import l2from keras.activations import reluimport osimport os.path import join as join_import numpy as npfrom PIL import Image# Setting up the datasetSET_DIR = 'dataset'NUM_CLASSES = len(os.listdir('dataset'))# The shape which VGG19 accepts as input and thus each image is resized toimage_shape = (224, 224, 3)# NUM_EXAMPLES is the number of (A,P,N) triplets chosen for the same class (N belongs to a different class of course)NUM_EXAMPLES = 5# Triplets list will contain anchor(A), positive(P) and negative(N) triplets.triplets = []A = P = N = []# creating anchor, positive, negative tripletsfor _ in range(NUM_EXAMPLES):    for direc in os.listdir(SET_DIR):        dir_path = SET_DIR + direc        dir_contents = os.listdir(dir_path)        length = len(dir_contents)                anchor = np.asarray(Image.open(join_(dir_path, dir_contents[np.random.randint(0, length)])))/255        # anchor.shape = (200, 180, 3)                # Padding with zeros for each channel in RGB        anchor = np.array([np.pad(a, ((22,22), (12,12)), 'constant') for a in anchor.T]).T                positive = np.asarray(Image.open(join_(dir_path, dir_contents[np.random.randint(0, length)])))/255        positive = np.array([np.pad(a, ((22,22), (12,12)), 'constant') for a in positive.T]).T                neg_dir = os.listdir(SET_DIR)[np.random.randint(NUM_CLASSES)]        while neg_dir == direc:             neg_dir = os.listdir(SET_DIR)[np.random.randint(NUM_CLASSES)]                    length_negative = len(os.listdir(SET_DIR + neg_dir))        negative = np.asarray(Image.open(                       join_(SET_DIR + neg_dir,                         os.listdir(SET_DIR + neg_dir)[np.random.randint(0, length_negative)])))/255                negative = np.array([np.pad(a, ((22,22), (12,12)), 'constant') for a in negative.T]).T                # append triplet        triplets.append([anchor, positive, negative])        A.append(anchor)        P.append(positive)        N.append(negative)def triplet_function(vects, alpha=0.2):    x, y, z = vects    sum_square_xy = K.sum(K.square(x - y), axis=1, keepdims=True)    sum_square_xz = K.sum(K.square(x - z), axis=1, keepdims=True)    return K.sum(K.maximum(sum_square_xy - sum_square_xz + alpha, 0), axis=0)# Using the VGG16 model defined in keras.applicationsdef VGG():    image_input = Input(shape=(224, 224, 3))    model = VGG16(input_tensor=image_input, weights='imagenet', include_top=True)    model.layers[-1].activation = relu    x_out = Dense(64)(model.layers[-1].output)        new_model = Model(inputs=image_input, outputs=x_out)    return new_modeldef get_model():    anchor = Input(shape=image_shape, name='anchor')    positive = Input(shape=image_shape, name='positive')    negative = Input(shape=image_shape, name='negative')        # Passing each image through the VGG model    anchor_encoding = VGG()(anchor)    positive_encoding = VGG()(positive)    negative_encoding = VGG()(negative)    # Incorporating the triplet loss in the SimVecLayer    SimVecLayer = Lambda(triplet_function, output_shape=(1,))        sim_APN = SimVecLayer([anchor_encoding, positive_encoding, negative_encoding])        return Model(inputs=[anchor, positive, negative], outputs=sim_APN)model = get_model()# Compile the model with a loss and optimizermodel.compile(loss='mean_squared_error', optimizer='adam', metrics=['mae']) model.summary()from IPython.display import SVGfrom keras.utils.vis_utils import model_to_dotSVG(model_to_dot(model).create(prog='dot', format='svg'))# Train the model (done over the intel cloud) model.fit(x = [A, P, N], y = np.zeros((len(triplets),1)),                  epochs=100, verbose=1,                  batch_size=64, validation_split=0.3,                  callbacks=[EarlyStopping(monitor='val_loss', patience=5)])model.save('model.h5')

使用Keras和OpenCV创建人脸验证系统

使用Keras和OpenCV创建人脸验证系统

使用模型进行验证

接收图像img(使用网络摄像头捕获的人的图像)的detect_face函数查找面部并将其裁剪。用验证如下:

a)我们在数据集文件夹中找到命名的文件夹。我们从该文件夹中选择一个图像。

b)我们随机选择另外三个文件夹,并从每个文件夹中选择一个图像。这些将作为negative images。

c)我们找到每个图像的编码,由detect_face返回,一个来自步骤a),三个来自步骤b)。

d)我们求出步骤a)和步骤b中每个图像编码的均方误差。

e)如果从步骤b)获得的任何图像的误差小于从步骤a)获得的图像的误差,我们说该人已被授权,否则就没有授权。

import osfrom os.path import join as jimport numpy as npimport matplotlib.pyplot as pltdef detect_face(img):    # The file containing the pretrained classifier     haar_file = 'haarcascade_frontalface_default.xml'      # Image to be resized to this shape    (width, height) = (224, 224)             # Make the cascade classifier object    face_cascade = cv2.CascadeClassifier(haar_file)            # Convert to grayscale    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)     # Detect the face    faces = face_cascade.detectMultiScale(gray, 1.3, 4)     face_resize = None    for (x, y, w, h) in faces:        # The classifier seemed to scrap the chin and hair. Adjustments made to accomodate those.        face = img[y-60:y + h + 60, x-20:x + w+20]         face_resize = cv2.resize(face, (width, height))        return face_resizedef mean_squared_error(y_true, y_pred):    return K.mean(K.square(y_true - y_pred))def verify_face(img, username):    dir_ = 'dataset'    NUM_CLASSES = len(os.listdir(dir_))        # The directory containing the user's photos    pos_dir = username    P = os.listdir(os.path.join(dir_, pos_dir))[-1]    neg_dirs = [] #os.listdir(dir_)[np.random.randint(NUM_CLASSES)]    for i in range(3):        neg_dir = os.listdir(dir_)[np.random.randint(NUM_CLASSES)]        while neg_dir == pos_dir or neg_dir in neg_dirs:            neg_dir = os.listdir(dir_)[np.random.randint(NUM_CLASSES)]        neg_dirs.append(neg_dir)              P = plt.imread(j(dir_, pos_dir, os.listdir(j(dir_, pos_dir)))[-1])    N1 = plt.imread(dir_ +'/'+ neg_dirs[0] +'/'+ os.listdir(dir_+'/'+neg_dirs[0])[-1])    N2 = plt.imread(dir_ +'/'+ neg_dirs[1] +'/'+ os.listdir(dir_+'/'+neg_dirs[1])[-1])    N3 = plt.imread(dir_ +'/'+ neg_dirs[2] +'/'+ os.listdir(dir_+'/'+neg_dirs[2])[-1])    P = cv2.resize(P, (224,224))    N1 = cv2.resize(N1, (224,224))    N2 = cv2.resize(N2, (224,224))    N3 = cv2.resize(N3, (224,224))        A = np.reshape(img, (1,224,224,3))    P = np.reshape(P, (1,224,224,3))    N1, N2, N3 = [np.reshape(N, (1,224,224,3)) for N in [N1, N2, N3]]    req_model = load_req_model('<path to model.h5>')    enc_anc   = req_model.predict(A)    enc_pos   = req_model.predict(P)    enc_neg_1 = req_model.predict(N1)    enc_neg_2 = req_model.predict(N2)    enc_neg_3 = req_model.predict(N3)        # Normalizing the encodings to avoid large values    maxm = np.max(enc_anc)    enc_anc = enc_anc/maxm    enc_pos = enc_pos/maxm    enc_neg_1, enc_neg_2, enc_neg_3 = [enc/maxm for enc in [enc_neg_1, enc_neg_2, enc_neg_3]]    positive_loss = mean_squared_error(enc_anc, enc_pos).numpy()    negative_losses = [mean_squared_error(enc_anc, enc_neg).numpy()                         for enc_neg in [enc_neg_1, enc_neg_2, enc_neg_3]]    flag = True    for neg_loss in negative_losses:        if positive_loss > neg_loss:            flag = False        return flag

使用Keras和OpenCV创建人脸验证系统

使用Keras和OpenCV创建人脸验证系统