人脸识别

学习任务

人脸识别可以说是人工智能领域中成熟较早、落地较广的技术之一,从机场、火车站的安检闸机,到平常用户手机中的“刷脸”支付,人脸识别技术已经深入到我们的生活当中。

人脸识别作为一种用于从图像或视频中识别人脸的系统,它还在许多其他领域发挥着重要的作业,如帮助新闻机构在重大事件报道中识别名人,为移动应用程序提供二次身份验证,为媒体和娱乐公司自动索引图像和视频文件,允许人道主义团体识别和营救人口贩卖受害者等。

通过本章的学习,我们将尝试构建一个人脸识别系统,该系统将一个人的图像与数据集中的护照大小的照片相匹配,并输出该图像是否匹配。

知识点

本节重点介绍人脸分类器部分,涉及图像处理、分类算法、OpenCV。

1.问题描述

  人脸识别问题可以描述为,给定某一场景下的静态图象或者动态序列,根据预先存储的人脸数据库识别或者认证场景中一个或者多个人的身份。

人脸识别系统主要包括4个组成部分:

1)人脸图像采集及检测

对要检测的目标对象进行概率统计,从而得到待检测对象的一些特征,建立起目标检测模型。

2)人脸图像预处理

基于人脸检测的结果,对图像进行处理,为后面的特征提取服务,系统获取的人脸图像可能受到各种条件的限制和随机干扰,需要进行缩放、旋转、拉伸、光线补偿、灰度变换、直方图均衡化、规范化、几何校正、过滤以及锐化等图像预处理。

3)人脸图像特征提取

就是将人脸图像信息数字化,将一张人脸图像转变为一串数字(一般称为特征向量)。例如,对一张脸,找到它的眼睛左边、嘴唇右边、鼻子、下巴等位置,利用特征点间的欧氏距离、曲率和角度等提取出特征分量,最终把相关的特征连接成一个长的特征向量。

4)人脸图像匹配与识别

就是把提取的人脸图像的特征数据与数据库中存储的人脸特征模板进行搜索匹配,根据相似程度对身份信息进行判断,设定一个阀值,当相似度超过这一阀值,则把匹配得到的结果输出。这一过程又分为两类:一类是确认,是一对一进行图像比较,换句话说就是证明”你就是你“,一般用在金融的核实身份和信息安全领域;另一类是辨认,是一对多进行图像匹配,也就是说在N个人中找到你,一般的N可以是一个视频流,只要人走进识别范围就完成识别工作,一般用在安防领域。

2.数据集介绍

本实训采用CASIA-FaceV5亚洲人脸数据集,用该数据集在预训练模型上进行再训练,也就是迁移学习训练模型,以得到较高的识别准确率。

CASIA-FaceV5亚洲人脸数据集有500人、每个人5张图片,共2500张图片,图片大小为640*480。数据集共有500个文件夹,文件夹名称为:000~499;一个文件夹表示一个人,里面有5张图片,图片名称为:文件夹名_0.bmp~文件夹名_4.bmp,具体如下图所示。 Screenshot from 2021-09-07 05-20-08.png

Screenshot from 2021-09-07 05-20-56.png

以CASIA-FaceV5数据集作为测试集时,不仅要考虑把同一个人识别出来,也需要考虑把不同的人区分开,这就需要生成同一人和不同人对应的pairs文件。

同一人时,每个人有5张图片,共有500人,故总的对数为5000。

不同人时,总对数为3118750。

因同一人和不同人的对数差距较大,故分别生成pairs文件。生成的同一人的成对TXT文件中,每一行格式为:文件夹名\t图片1名\t图片2名;生成的不同人的成对TXT文件中,每一行格式为:文件夹1名\t图片1名\t文件夹2名\t图片2名。

3.主要模型介绍

3.1 人脸检测

我们首先介绍基于AdaBoost算法的人脸检测,在深度卷积神经网络用于此问题之前,AdaBoost算法在视觉目标检测领域的实际应用上一直处于主导地位。

在2001年,Viola和Jones设计了一种人脸检测算法。它使用简单的Haar特征和级联AdaBoost分类器构造检测器,检测速度较之前的方法有2个数量级的提高,并且有很高的精度,我们称这种方法为VJ框架。VJ框架是人脸检测历史上有里程碑意义的一个成果。

用级联AdaBoost分类器进行目标检测的思想是:用多个AdaBoost分类器合作完成对候选框的分类,这些分类器组成一个流水线,对滑动窗口中的候选框图像进行判定,确定它是人脸还是非人脸。在这些AdaBoost分类器中,前面的分类器很简单,包含的弱分类器很少,可以快速排除掉大量非人脸窗口,但也可能会把一些不是人脸的图像判定为人脸。如果一个候选框通过了第一级分类器的筛选即被它判定为人脸,则送入下一级分类器中进行判定,否则丢弃掉,以此类推。如果一个检测窗口通过了所有的分类器,则认为是人脸,否则是非人脸。

出于性能的考虑,弱分类器使用了简单的Haar(哈尔)特征。这种特征源自于小波分析中的Haar小波变换,Haar小波是最简单的小波函数,用于对信号进行均值、细节分解。

用于人脸检测的Haar特征分为三类:边缘特征、线性特征和对角线特征,组合成特征模板。特征模板内有白色和黑色两种矩形,并定义该模板的特征值为白色矩形像素和减去黑色矩形像素和。Haar特征值反映了图像的灰度变化情况。例如:脸部的一些特征能由矩形特征简单的描述,如:眼睛要比脸颊颜色要深,鼻梁两侧比鼻梁颜色要深,嘴巴比周围颜色要深等。但矩形特征只对一些简单的图形结构,如边缘、线段较敏感,所以只能描述特定走向(水平、垂直、对角)的结构。

OIP-C.hbFPsfsCqV8rf1MV8b8p5wHaGR.jpeg

对于图中的a和c这类特征,特征数值计算公式为:v=Σ白-Σ黑,而对于b来说,计算公式如下:v=Σ白-2*Σ黑;之所以将黑色区域像素和乘以2,是为了使两种矩形区域中像素数目一致。我们希望当把矩形放到人脸区域计算出来的特征值和放到非人脸区域计算出来的特征值差别越大越好,这样就可以用来区分人脸和非人脸。

通过改变特征模板的大小和位置,可在图像子窗口中穷举出大量的特征。上图的特征模板称为“特征原型”;特征原型在图像子窗口中扩展(平移伸缩)得到的特征称为“矩形特征”;矩形特征的值称为“特征值”。

1328274-20180815213149483-1209887019.png

上图中两个矩形特征,表示出人脸的某些特征。比如中间一幅表示眼睛区域的颜色比脸颊区域的颜色深,右边一幅表示鼻梁两侧比鼻梁的颜色要深。同样,其他目标,如眼睛等,也可以用一些矩形特征来表示。使用特征比单纯地使用像素点具有很大的优越性,并且速度更快。

矩形特征可位于图像任意位置,大小也可以任意改变,所以矩形特征值是矩形模版类别、矩形位置和矩形大小这三个因素的函数。故类别、大小和位置的变化,使得很小的检测窗口含有非常多的矩形特征,如:在24*24像素大小的检测窗口内矩形特征数量可以达到16万个。这样就有两个问题需要解决了,即(1)如何快速计算那么多的特征?(2)哪些矩形特征才是对分类器分类最有效的?在VJ框架中分别采用积分图和AdaBoost算法来就解决这两个问题。

3.2 人脸识别

通过前一节的介绍,我们可以得到图片中的人脸及位置了,可是光有人脸有啥用啊,咱得知道这人是谁啊,这部分工作交由facenet完成。

facenet是谷歌的人脸检测算法,发表于CVPR 2015,利用相同人脸在不同角度等姿态的照片下有高内聚性、不同人脸有低耦合性,提出使用cnn+triplet mining方法,在LFW数据集上准确度达到 99.63%。通过CNN将人脸映射到欧式空间的特征向量上,使得不同人的图片特征的距离较大;通过相同个体的人脸的距离,总是小于不同个体的人脸这一先验知识训练网络。

测试时只需要计算人脸特征EMBEDDING,然后计算距离使用阈值即可判定两张人脸照片是否属于相同的个体。 facenet中有许多个用于分类的深度学习模型,我们将使用其中的Inception-ResNetV1模型,它是目前使用比较广泛的一个模型。

如图所示为整个网络的主干结构:

20191219190418800.png

可以看到里面的结构分为几个重要的部分

4.实验过程

我们将尝试从图像中提取人脸。 OpenCV已经包含了很多已经训练好的分类器,其中包括:面部,眼睛,微笑等。这些XML文件保存在/opencv/data/haarcascades/文件夹中。

首先,我们需要加载haarcascade_frontalface_default XML分类器。然后以灰度模式加载我们的输入图像(或视频)。如果找到人脸,则将检测到的人脸的位置返回为Rect(x,y,w,h)。然后,将这些位置用于为人脸创建ROI。

import fnmatch
import os
from matplotlib import pyplot as plt
import cv2

# Load the cascade
face_cascade = cv2.CascadeClassifier('/haarcascade_frontalface_default.xml')

paths="/data/"

for root,_,files in os.walk(paths):
    for filename in files: 
        file = os.path.join(root,filename)
        if fnmatch.fnmatch(file,'*.jpg'):
            
            img = cv2.imread(file)        
            gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            # Detect faces
            faces = face_cascade.detectMultiScale(gray, 1.1, 4)
            # Draw rectangle around the faces
            for (x, y, w, h) in faces:
              crop_face = img[y:y+h, x:x+w]
            path = os.path.join(root,filename)
            cv2.imwrite(path,crop_face)

这会将目录中的所有图像替换为图像中检测到的人脸。分类器的数据准备部分现已完成。

现在,我们将加载该数据集。先要准备导入一些工具包:

from torch import nn, optim, as_tensor
from torch.utils.data import Dataset, DataLoader
import torch.nn.functional as F
from torch.optim import lr_scheduler
from torch.nn.init import *
from torchvision import transforms, utils, datasets, models
import cv2
from PIL import Image
from pdb import set_trace
import time
import copy
from pathlib import Path
import os
import sys
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from skimage import io, transform
from tqdm import trange, tqdm
import csv
import glob
import dlib
import pandas as pd
import numpy as np

看起上有点多,是吧?原因是为了增加数据集的大小,应用了各种数据扩充。

data_transforms = {
    'train': transforms.Compose([
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Scale((224,224)),
        transforms.ColorJitter(brightness=0.4, contrast=0.4, saturation=0.4, hue=0.4),
        transforms.RandomRotation(5, resample=False,expand=False, center=None),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
    'val': transforms.Compose([
        transforms.ToTensor(),
        transforms.Scale((224,224)),
       transforms.ColorJitter(brightness=0.4, contrast=0.4, saturation=0.4, hue=0.4),
        transforms.RandomRotation(5, resample=False,expand=False, center=None),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
    ]),
}
data_dir = '/content/drive/MyDrive/AttendanceCapturingSystem/data/'
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
                                          data_transforms[x])
                  for x in ['train', 'val']}
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x],
                                              batch_size=8, 
                                             shuffle=True)
              for x in ['train', 'val']}
dataset_sizes = {x: len(image_datasets[x]) for x in ['train','val']}
class_names = image_datasets['train'].classes
class_names

现在让我们可视化数据集。

def imshow(inp, title=None):
    """Imshow for Tensor."""
    inp = inp.numpy().transpose((1, 2, 0))
    mean = np.array([0.485, 0.456, 0.406])
    std = np.array([0.229, 0.224, 0.225])
    inp = std * inp + mean
    inp = np.clip(inp, 0, 1)
    plt.imshow(inp)
    if title is not None:
        plt.title(title)
    plt.pause(0.001)  # pause a bit so that plots are updated
# Get a batch of training data
inputs, classes = next(iter(dataloaders['train']))
# Make a grid from batch
out = utils.make_grid(inputs)
imshow(out, title=[class_names[x] for x in classes])

现在让我们建立分类器模型。在这里,我们将使用在VGGFace2数据集上预训练的InceptionResnetV1作为基础模型。

from models.inception_resnet_v1 import InceptionResnetV1
print('Running on device: {}'.format(device))

model_ft = InceptionResnetV1(pretrained='vggface2', classify=False, num_classes = len(class_names))

list(model_ft.children())[-6:]

layer_list = list(model_ft.children())[-5:] # all final layers

model_ft = nn.Sequential(*list(model_ft.children())[:-5])

for param in model_ft.parameters():
    param.requires_grad = False
    
class Flatten(nn.Module):
    def __init__(self):
        super(Flatten, self).__init__()
        
    def forward(self, x):
        x = x.view(x.size(0), -1)
        return x
      
class normalize(nn.Module):
    def __init__(self):
        super(normalize, self).__init__()
        
    def forward(self, x):
        x = F.normalize(x, p=2, dim=1)
        return x    
model_ft.avgpool_1a = nn.AdaptiveAvgPool2d(output_size=1)

model_ft.last_linear = nn.Sequential(
    Flatten(),
    nn.Linear(in_features=1792, out_features=512, bias=False),
    normalize()
)

model_ft.logits = nn.Linear(layer_list[2].out_features, len(class_names))
model_ft.softmax = nn.Softmax(dim=1)
model_ft = model_ft.to(device)
criterion = nn.CrossEntropyLoss()

# Observe that all parameters are being optimized
optimizer_ft = optim.SGD(model_ft.parameters(), lr=1e-2, momentum=0.9)

# Decay LR by a factor of *gamma* every *step_size* epochs
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)  

model_ft = model_ft.to(device)
criterion = nn.CrossEntropyLoss()
# Observe that all parameters are being optimized
optimizer_ft = optim.SGD(model_ft.parameters(), lr=1e-2, momentum=0.9)
# Decay LR by a factor of *gamma* every *step_size* epochs
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)

model_ft

现在我们将训练模型。

def train_model(model, criterion, optimizer, scheduler,
                num_epochs=25):
    since = time.time()
    FT_losses = []
    best_model_wts = copy.deepcopy(model.state_dict())
    best_acc = 0.0
    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch, num_epochs - 1))
        print('-' * 10)
    # Each epoch has a training and validation phase
        for phase in ['train', 'val']:
            if phase == 'train':
                model.train()  # Set model to training mode
            else:
                model.eval()   # Set model to evaluate mode
            running_loss = 0.0
            running_corrects = 0
            # Iterate over data.
            for inputs, labels in dataloaders[phase]:
                inputs = inputs.to(device)
                labels = labels.to(device)
                # zero the parameter gradients
                optimizer.zero_grad()
                # forward
                # track history if only in train
                with torch.set_grad_enabled(phase == 'train'):
                    outputs = model(inputs)
                    _, preds = torch.max(outputs, 1)
                    loss = criterion(outputs, labels)
                    # backward + optimize only if in training phase
                    if phase == 'train':
                        loss.backward()
                        optimizer.step()
                        scheduler.step()
                
                FT_losses.append(loss.item())
                # statistics
                running_loss += loss.item() * inputs.size(0)
                running_corrects += torch.sum(preds == labels.data)
            epoch_loss = running_loss / dataset_sizes[phase]
            epoch_acc = running_corrects.double() / dataset_sizes[phase]
            print('{} Loss: {:.4f} Acc: {:.4f}'.format(
                phase, epoch_loss, epoch_acc))
            # deep copy the model
            if phase == 'val' and epoch_acc > best_acc:
                best_acc = epoch_acc
                best_model_wts = copy.deepcopy(model.state_dict())
    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(
        time_elapsed // 60, time_elapsed % 60))
    print('Best val Acc: {:4f}'.format(best_acc))
    # load best model weights
    model.load_state_dict(best_model_wts)
    return model, FT_losses

最后,我们将评估模型并保存。

model_ft, FT_losses = train_model(model_ft, criterion, optimizer_ft, exp_lr_scheduler, num_epochs=200)
plt.figure(figsize=(10,5))
plt.title("FRT Loss During Training")
plt.plot(FT_losses, label="FT loss")
plt.xlabel("iterations")
plt.ylabel("Loss")
plt.legend()
plt.show()

torch.save(model, "/model.pt")

现在,我们将输入图像输入已保存的模型并检查匹配情况。

import fnmatch
import os
from matplotlib import pyplot as plt
import cv2
from facenet_pytorch import MTCNN, InceptionResnetV1
resnet = InceptionResnetV1(pretrained='vggface2').eval()
# Load the cascade
face_cascade = cv2.CascadeClassifier('/haarcascade_frontalface_default.xml')

def face_match(img_path, data_path): # img_path= location of photo, data_path= location of data.pt 
    # getting embedding matrix of the given img
    img = cv2.imread(img_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # Detect faces
    faces = face_cascade.detectMultiScale(gray, 1.1, 4)
    # Draw rectangle around the faces
    for (x, y, w, h) in faces:
        crop_face = img[y:y+h, x:x+w]
            
    img = cv2.imwrite(img_path,crop_face)
    emb = resnet(img.unsqueeze(0)).detach() # detech is to make required gradient false
    
    saved_data = torch.load('model.pt') # loading data.pt file
    embedding_list = saved_data[0] # getting embedding data
    name_list = saved_data[1] # getting list of names
    dist_list = [] # list of matched distances, minimum distance is used to identify the person
    
    for idx, emb_db in enumerate(embedding_list):
        dist = torch.dist(emb, emb_db).item()
        dist_list.append(dist)
        
    idx_min = dist_list.index(min(dist_list))
    return (name_list[idx_min], min(dist_list))


result = face_match('trainset/0006/0006_0000546/0006_0000546_script.jpg', '/model.pt')

print('Face matched with: ',result[0], 'With distance: ',result[1])

部分测试结果 aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8yMjQ5ODQwNS1kMTA2MzI3ZDFiZmNmNjM2LnBuZw.png

5.小结

在本章中,我们主要介绍了人脸识别的基本逻辑流程,其基本思想是要将图像中的特征提取出来,转换到一个合适的子空间里,然后在这个子空间里衡量类似性或分类学习。但问题在于,对客观世界采用怎样协调统一且有成效的表示法?我们要找到怎样合适的子空间,怎样去分类,才能区分不同类,聚集相似的类别?这些都是当下学术界和工业界热烈讨论的内容。