人脸风控技术落地(一)人脸检测技术原理与最佳工程实践

在上一篇文章中,我们系统地介绍了金融风控领域的人脸反欺诈技术,并概述了人脸检测、人脸对齐、人脸比对、人脸搜索和人脸识别等技术的区别。其中,人脸检测是其他人脸技术的基础和前提。只有在图像中成功检测并定位出人脸,才能在指定位置执行其他任务。
那么,你知道有哪些人脸检测技术吗?如果想在工业级应用中使用这些技术,该如何操作呢?本文将带你深入探讨人脸检测的各个方面,包括数据准备、模型算法、模型训练、评估、测试、部署和使用等一系列内容,确保技术能够成功落地!
传送门:一文读懂金融风控领域的人脸反欺诈技术
▌本文导览
  1. 人脸检测的介绍

  2. 人脸检测数据集

  3. 人脸检测的算法

  4. 模型训练与评估

  5. 模型保存与测试

  6. 模型部署与应用

提示:全文内容1.4w字,包含大量理论和代码实践,建议点赞、收藏后慢慢看~

01

人脸检测技术介绍
▌技术介绍 
人脸检测(Face Detection)是指在图像中识别出是否有人脸,并定位人脸位置的技术,它是计算机视觉AI的一个重要分支。
人脸检测过程包括两个主要任务:首先,识别图像中所有可能是人脸的区域,并为每个区域提供置信度。置信度是一个从0到1的数值,表示识别结果的可靠性,1表示完全确认,0表示完全否定。其次,确定人脸在图像中的具体位置,返回人脸各角位置坐标,以便框选出人脸。如果图像中有多张人脸,系统将分别为每张人脸提供一个框。

【人脸检测:单张人脸】

【人脸检测:多张人脸】
人脸检测接口返回结果如下,可以看到faces数组下多个人脸信息。
# Face Check API response
{
  "faces": [
    {
      "confidence"99.371,
      "face_rectangle": {
        "x"230,
        "y"169,
        "height"546,
        "width"376
      }
    },
    {
      "confidence"99.895,
      "face_rectangle": {
        "x"530,
        "y"369,
        "height"245,
        "width"230
      }
    }
  ]
}

▌应用场景 

人脸检测可以应用于多个下游任务,在不同领域都有广泛的应用。智能手机的面部解锁,相机的自动对焦,照片的美颜优化,娱乐行业的虚拟现实体验,安全监控的身份识别,安检,公共场景人流统计,性别分类,活动集体照找人,相册人脸分类搜索。人脸检测不仅提升了安全性,也在日常生活和娱乐中带来了极大的便利。


【面部解锁】

【相册人脸分类搜索】

【刷脸支付】

【刷脸安检】

【人脸美颜】

▌困难挑战 
人脸检测器的性能受到多种因素的干扰,比如遮挡、光照变化、肤色差异、姿势多样性、面部表情、配饰、妆容、口罩、面部比例和图像质量。此外,背景复杂度、分辨率、运动模糊和摄像头角度也会影响检测结果。这些因素可能导致检测器难以准确识别人脸,影响其在实际应用中的表现。


【人脸检测的挑战】

02

人脸检测数据集

工欲善其事必先利其器,AI领域,优质的数据就是成功的关键。在计算机视觉中的人脸图像检测领域,前人已经构建并开源了许多优秀的数据集供研究和使用。其中,AFW、FDDB和WIDER FACE是推荐最好用的基准数据集,并有相应的评估标准(Benchmark)用于评估和比较人脸检测算法的性能。

▌AFW (Annotated Faces in the Wild)
数据集特点:
  • 包含205个图像,共473个标注的人脸。

  • 图像来自Flickr,具有多样化的背景和姿态。

  • 每个人脸标注了边界框、5个关键点(眼睛、鼻尖、嘴角)和姿态信息(俯仰、偏航、滚动角度)。

应用场景:

用于评估人脸检测和姿态估计算法的性能。
相关论文:
Face detection, pose estimation, and landmark localization in the wild [1]
数据下载地址:
http://vis-www.cs.umass.edu/lfw/index.html
▌FDDB (Face Detection Data Set and Benchmark)
数据集特点:
  • 包含2845张图像,共5171个标注的人脸。

  • 图像来自新闻、网络和个人相册,具有多样化的背景、姿态和光照条件。

  • 标注了椭圆形的边界框 (而不是矩形边界框),更加符合实际人脸的形状。

应用场景:

主要用于评估人脸检测算法的性能。
相关论文:
FDDB: A Benchmark for Face Detection in Unconstrained Settings [2]
数据下载地址:
https://vis-www.cs.umass.edu/fddb/index.html
▌WIDER FACE
数据集特点:
  • 包含32,203张图像,共393,703个标注的人脸。

  • 图像来自互联网,具有极高的多样性,包括不同的姿态、遮挡、光照和分辨率。

  • 标注了矩形的边界框。

应用场景:

用于评估人脸检测算法在复杂场景下的性能。
相关论文:
WIDER FACE: A Face Detection Benchmark [3]

数据下载地址:
http://shuoyang1213.me/WIDERFACE/index.html

▌开源数据集对比选型
数据集 AFW FDDB WIDER FACE
图像数量 205 2845 32,203
人脸数量 473 5171 393,703
标注类型 矩形边界框、5个关键点、姿态信息
椭圆形边界框
矩形边界框
图像来源 Flickr 新闻、网络、个人相册 互联网
多样性 中等 极高
应用场景 人脸检测、姿态估计 人脸检测 人脸检测

综合来看,WIDER FACE数据集因其极高的多样性和复杂性,通常被认为是评估人脸检测算法性能的最佳选择之一。此外还有一些数据集可以根据不同的场景任务需求使用,PASCAL FACE(人脸检测)、LFW(人脸识别)、CelebA(人脸属性)、MAFA(戴口罩人脸检测)等。

▌自建数据集

除了使用开源数据集之外,我们通常还会根据具体任务自建数据集。例如,为了实现身份识别和身份证人脸与活体人脸的比对等风控策略,我们可能需要专门检测身份证上的人脸。这就需要针对身份证人脸的场景进行强化训练或微调训练。为此,首先需要收集大量的身份证图片,并对其中的人脸进行标注,从而自建一个专门的人脸数据集。
可以使用OpenCV和Dlib来自动化建立人脸检测数据集,然后人工校对结果。对自动标注不合格的数据也可以使用Labelme来人工进行标注。
OpenCV
OpenCV(Open Source Computer Vision Library)是一个开源的计算机视觉和机器学习软件库。它提供了数千个优化的算法,用于实时计算机视觉应用。OpenCV提供了丰富的图像处理功能,如图像缩放、旋转、裁剪等。
!pip install opencv-python
import cv2
# 读取图像
image = cv2.imread('path_to_image.jpg')
# 显示图像
cv2.imshow('Image', image)
Dlib
Dlib是一个现代的C++工具包,包含了机器学习算法和工具,可以用于人脸检测及其他一些常见任务。人脸检测方面,Dlib提供了一个基于HOG的前脸检测器。人脸关键点方面,Dlib提供了预训练的68点关键点检测模型。
!pip install dlib
import dlib
# 加载人脸检测器
detector = dlib.get_frontal_face_detector()
# 检测人脸
faces = detector(image)
for face in faces:
    x, y, w, h = face.left(), face.top(), face.width(), face.height()
Labelme
Labelme是一个非常灵活的图形化标注工具,支持多种标注类型,包括矩形框、多边形和关键点。它有图形化的操作界面,可以方便的用来人工标注人脸构建数据集。
!pip install labelme!labelme #打开Labelme GUI

最后save会保存成xx.json文件,一般我们只保留points数据(左上角和右下角坐标)即可。
"points": [        [254.15094339622635,178.188679245283],        [608.8679245283017,712.1509433962264]]#取整xx.jpg 1 254 178 609 712

03

人脸检测算法
▌人脸检测算法的发展
人脸检测算法的发展历程可以说是计算机视觉领域的一段精彩旅程。从最初的简单特征提取到如今的深度学习技术,人脸检测算法经历了多个重要阶段,每个阶段都有其独特的创新和突破。
第一阶段:传统机器学习方法
最早的人脸检测算法依赖于手工特征和传统的机器学习方法。其中,最具代表性的就是 Viola-Jones 算法,于 2001 年由 Paul Viola 和 Michael Jones 提出。Viola-Jones 算法基于 Haar 特征和 AdaBoost 分类器,首次实现了实时的人脸检测。它的出现让计算机能够快速、准确地在图像中定位人脸,成为当时的标杆。
第二阶段:特征工程与统计学习
随着计算能力的提升和数据的积累,研究者开始探索更复杂的特征和模型。此阶段的代表性算法包括基于 HOG(Histogram of Oriented Gradients)特征的 DPM(Deformable Parts Model)等。这些方法通过更精细的特征提取和更复杂的模型结构,提高了检测的准确性,但在速度上仍然受到限制。
第三阶段:深度学习的崛起
进入 2010 年代,深度学习技术迅速崛起,彻底改变了人脸检测的格局。2015 年,R-CNN (Region-based Convolutional Neural Networks) 的出现标志着深度学习在目标检测领域的应用。随后,诸如 Faster R-CNN、YOLO(You Only Look Once)、SSD(Single Shot MultiBox Detector)等算法相继问世,这些算法利用卷积神经网络(CNN)强大的特征提取能力,使得人脸检测的精度和速度大幅提升。
第四阶段:专注于人脸检测的深度学习算法
在深度学习的浪潮中,专门针对人脸检测的算法也不断涌现。MTCNN(Multi-task Cascaded Convolutional Networks)、 RetinaFaceYuNet等算法,通过多任务学习和特定的网络结构,对人脸检测进行了进一步优化。这些算法在公开数据集上取得了优异的成绩,推动了人脸检测技术的应用。接下来重点介绍MTCNN和RetinaFace两种算法。

▌MTCNN

MTCNN于2016年由中科院深圳研究院提出,从名字可以看出,它是由多个卷积神经网络组成,通过级联多个卷积神经网络(CNN)来实现多任务的目标,可以用于人脸检测任务。

相关论文:
Joint face detection and alignment using multitask cascaded convolutional networks [4]
网络结构:


【MTCNN三阶段网络结构】

MTCNN 网络分为三个阶段:

  • P-Net(Proposal Network): 生成初步的人脸候选框。    

  • R-Net(Refine Network): 对 P-Net 生成的候选框进行筛选和精细化。

  • O-Net(Output Network): 最终确认人脸位置并预测关键点。

每个子网络都有自己的卷积层和损失函数,分别进行训练。P-Net 主要负责生成候选框,R-Net 负责筛选和精细化,O-Net 负责最终的精确定位和关键点预测。
▌RetinaFace
RetinaFace 由 InsightFace 团队2019年提出的一种人脸检测的深度学习方法。它通过单级卷积神经网络(CNN)实现高效、精确的人脸定位,并在各种复杂环境下展现出色的性能。
相关论文:
Retinaface: Single-stage dense face localisation in the wild [5]
网络结构:


【RetinaFace网络结构】

RetinaFace 网络由多个层级组成:
1. 输入层这是网络的起点,接收原始图像。
2. 主干网络(Backbone Network)这一部分通常使用预训练的卷积神经网络,它的任务是提取图像的基本特征。可以想象成是一个专门用来识别图像中基本形状和纹理的“专家”。
3. 特征金字塔网络(FPN)接下来,特征会被送入 FPN,这个网络能从不同尺度上分析图像特征,就像是拥有多层放大镜,能同时关注大人脸和小人脸。
4. 检测头(Detection Head):在这个阶段,网络会生成一系列的“候选框”,每个框都可能包含一张人脸。检测头会像一个精明的侦探,逐一检查这些框,判断哪些是真正的人脸。
5. 关键点定位分支:这是一个专门识别人脸关键点的部分。它会在每张被识别出的人脸上标记出眼睛、鼻子、嘴巴等关键位置,确保识别的准确性。
6. 分类和回归层:最后的层负责对每个候选框进行分类(是否是人脸)和回归(调整框的位置和大小),确保最终输出的结果既准确又精确。
主干网络选择:
在选择 RetinaFace 的主干网络时,通常有两种选择:MobileNet 和 ResNet50,它们各自具有不同的特点和适用场景。
MobileNet :是一种轻量级的网络,专为在移动设备和嵌入式系统上高效运行而设计。它通过深度可分离卷积显著减少了参数和计算量,因此在资源受限的环境中能够快速运行,非常适合手机或物联网设备。然而,这种简化也意味着在处理复杂图像时,准确性可能稍逊于更复杂的网络。
参考论文:
MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications [6]
ResNet50 :号称“冠军网络”,它在多个计算机视觉竞赛中取得了优异的成绩。是一个较为深层的网络,拥有 50 层卷积层,并使用残差连接来解决深层网络中的梯度消失问题。这使得 ResNet50 在处理复杂图像时具有更高的准确性,适合在计算资源充足的服务器或高性能计算机上使用,但其对计算资源的需求也更高。
参考论文:
Deep Residual Learning for Image Recognition [7]
▌总结说明

截至目前为止,RetinaFace 被广泛认为是人脸检测领域的顶尖算法之一,也是工业界首选的算法,所以后续模型训练和使用将选择使用RetinaFace

04

模型训练与评估

根据上述知识,这里我们将选用RetinaFace算法 + WIDER FACE数据集进行模型训练与评估。
▌准备工作
首先可以从RetinaFace Github克隆代码,这是一个基于Pytorch实现的Retinaface算法。代码地址:
https://github.com/biubug6/Pytorch_Retinaface
训练环境:
· System RAM 12.7 GB· GPU RAM 15.0GB· Python 3.10· Torch 2.3.1+cu121· Torchversion 0.18.1+cu121
RetinaFace训练模型必须依赖GPU,这里推荐使用Google colab,可以免费薅一些GPU资源,选用T4 GPU型,如果付费可以直接上A100 GPU型。
下载WIDER FACE数据集并放置到指定目录,目录结构如下:
  ./data/widerface/    train/      images/      label.txt    val/      images/      wider_val.txt

▌模型训练

进入模型代码目录Pytorch_Retinaface,其中train.py是我们用来训练的脚本。
parser = argparse.ArgumentParser(description='Retinaface Training')parser.add_argument('--training_dataset', default='./data/widerface/train/label.txt', help='Training dataset directory')parser.add_argument('--network', default='mobile0.25', help='Backbone network mobile0.25 or resnet50')parser.add_argument('--num_workers', default=4, type=int, help='Number of workers used in dataloading')parser.add_argument('--lr', '--learning-rate', default=1e-3, type=float, help='initial learning rate')parser.add_argument('--momentum', default=0.9, type=float, help='momentum')parser.add_argument('--resume_net', default=None, help='resume net for retraining')parser.add_argument('--resume_epoch', default=0, type=int, help='resume iter for retraining')parser.add_argument('--weight_decay', default=5e-4, type=float, help='Weight decay for SGD')parser.add_argument('--gamma', default=0.1, type=float, help='Gamma update for SGD')parser.add_argument('--save_folder', default='./weights/', help='Location to save checkpoint models')

脚本执行有一些默认参数,我们也可以调整,其中重要的几个参数:

  • training_dataset:对应训练的数据集

  • network:选择backbone网络(MobileNet/ResNet50),使用默认的ResNet50网络

  • lr:学习率,学习率决定模型更新的步长

  • momentum:动量因子,用于加速梯度下降并稳定训练过程。

  • weight_decay:SGD优化器的权重衰减。

  • gamma:SGD优化器的学习率衰减因子。

  • save_folder:保存检查点模型的目录。默认保存目录./weights/

还有一些模型训练调整的参数,打开./data/config.py。

cfg_re50 = {    'name': 'Resnet50',    'min_sizes': [[16, 32], [64, 128], [256, 512]],    'steps': [8, 16, 32],    'variance': [0.1, 0.2],    'clip': False,    'loc_weight': 2.0,    'gpu_train': False,    'batch_size': 24,    'ngpu': 0,    'epoch': 100,    'decay1': 70,    'decay2': 90,    'image_size': 840,    'pretrain': True,    'return_layers': {'layer2': 1, 'layer3': 2, 'layer4': 3},    'in_channel': 256,    'out_channel': 256}

这里可以在训练中调整的几个重要参数:

  • batch_size: 设置每次迭代的训练样本数量。较大的批量大小可以更稳定地更新梯度,但需要更多的显存。

  • epoch: 设置训练的总轮数。更多的训练轮数可以让模型更充分地学习数据,但也可能导致过拟合。

  • image_size: 设置输入图像的尺寸。较大的图像尺寸可能提供更精细的特征,但会增加计算量。

调整完参数后即可开启训练:

!python3 train.py
Epoch:1/10 || Epochiter: 1/403 || Iter: 1/4030 || Loc: 4.6140 Cla: 11.4100 Landm: 21.1020 || LR: 0.00100000 || Batchtime: 45.5512 s || ETA: 2 days, 2:59:31 Epoch:1/10 || Epochiter: 2/403 || Iter: 2/4030 || Loc: 4.6274 Cla: 10.6198 Landm: 19.8684 || LR: 0.00100000 || Batchtime: 0.4543 s || ETA: 0:30:30

通过输出日志,我们可以跟踪模型的训练进度、性能指标以及预计完成时间。整个训练过程包含10个周期(Epochs),每个周期由403次迭代(Epochiter)组成,总迭代次数为4030次(403*10)。训练结果包括位置损失(Loc)、分类损失(Cla)和关键点损失(Landm)。在训练过程中,目标是使这些损失逐步减少,以提高模型的性能。

▌评价指标

在人脸检测任务中,常用评价指标有:准确率(Accuracy),精确率(Precision),召回率(Recall),平均精度(Average Precision, AP)。
准确率(Accuracy)是指模型正确检测出人脸的比例,其计算公式:

其中:
  • TP (True Positives):正确检测为人脸的正样本数

  • TN (True Negatives):正确检测为非人脸的负样本数

  • FP (False Positives):错误检测为人脸的负样本数

  • FN (False Negatives):未检测出的人脸正样本数
精确率(Precision)表示在所有被检测为人脸的区域中,实际是人脸的比例,其计算公式:
召回率(Recall)表示在所有实际存在的人脸中,被正确检测出的比例。计算公式:
平均精度(AP)是通过计算不同阈值下的“精确率-召回率”曲线下面积来获得的,它更为全面,它考虑了检测结果的精确性和召回率。其计算公式:
为了方便计算,通常会采用插值法对不同阈值下的精确率和召回率进行处理,再计算其面积。

在实际应用中,平均精度(AP)被视为最重要的指标,因为它不仅能反映模型的整体检测准确性,还能揭示模型在处理不同大小、不同位置的人脸时的稳定性和可靠性。

▌模型评估

模型评估使用widerface val,加载我们刚训练好的模型参数(xx.pth),执行完成后会在widerface_evaluate目录生成result.txt文件。

!python test_widerface.py --trained_model "path/res_epoch_10.pth" --network resnet50

接着进入widerface_evaluate对执行结果进行评估。

%cd ./widerface_evaluate!python setup.py build_ext --inplace!python evaluation.py

【模型在val测试集上评估结果】
在结果中,Val AP(验证集平均精度)用于评估模型在验证集上的检测性能。数据集被划分为三类:
  • Easy:包含清晰、正面、无遮挡的人脸,是最容易检测的类别。

  • Medium:包含一些遮挡、较小尺度或轻微倾斜的人脸,检测难度属于中等。

  • Hard:包含严重遮挡、极小尺度或大角度倾斜的人脸,检测难度最大。

从结果可以看出,模型在Easy和Medium验证集上都达到了约95%的平均精度,这一表现非常出色,几乎可以直接应用于工业领域。在Hard数据集上,模型也表现良好。

通过不断调整参数,反复评估直到达成最佳性能的模型,然后就可以保存和使用了。

05

模型保存与测试

▌模型保存与加载
模型训练完成后,会保存参数权重到文件(一般扩展名为.pth或.pt),下次可以加载模型权重来直接使用训练好的模型,而且PyTorch支持模型加载时选择GPU或CPU环境,只要在torch.load参数map_location=torch.device('cpu')即可。
# 加载预训练模型
def load_model(model, pretrained_path, load_to_cpu):
    print('Loading pretrained model from {}'.format(pretrained_path))
    if load_to_cpu:
        pretrained_dict = torch.load(pretrained_path, map_location=lambda storage, loc: storage)
    else:
        device = torch.cuda.current_device()
        pretrained_dict = torch.load(pretrained_path, map_location=lambda storage, loc: storage.cuda(device))
    if "state_dict" in pretrained_dict.keys():
        pretrained_dict = remove_prefix(pretrained_dict['state_dict'], 'module.')
    else:
        pretrained_dict = remove_prefix(pretrained_dict, 'module.')
    # 加载权重
    model.load_state_dict(pretrained_dict, strict=False)
    return model

def remove_prefix(state_dict, prefix):
    print('remove prefix \'{}\''.format(prefix))
    f = lambda x: x.split(prefix, 1)[-1if x.startswith(prefix) else x
    return {f(key): value for key, value in state_dict.items()}

#预训练模型参数
trained_weigth_path = '/path/resnet50.pth' 
cpu = not torch.cuda.is_available() #当前环境没有gpu使用cpu
model = load_model(net, trained_weigth_path, cpu)
model.eval() #设置模型为评估模式
▌模型测试

为了测试模型的预测效果,我们打开图片,使用模型进行人脸检测,检测完成后,将结果标注在图片上,以便更直观地观察模型的准确性和效果。


【测试人脸检测模型效果】

首先使用OpenCV打开并预处理图片。图片数据被转换为浮点数格式,并进行归一化处理,即从每个像素的 RGB 值中减去固定的均值 (104, 117, 123)。这一归一化步骤有助于提高模型的检测精度。最后,我们将图片调整为模型输入所需的形状。

# 加载图片并预处理
def preprocess_image(image_path):
  img_raw = cv2.imread(image_path, cv2.IMREAD_COLOR)
  if img_raw is None:
    print('image cant open, check please')
    return
  img = np.float32(img_raw) #转为浮点数
  scale = torch.Tensor([img.shape[1], img.shape[0], img.shape[1], img.shape[0]]) #缩放比例 [width, height, width, height]
  img -= (104117123#减去均值进行归一化,RGB 固定104, 117, 123
  img = img.transpose(201#转置图像维度 维度从 (height, width, channels) 转换为 (channels, height, width)
  img = torch.from_numpy(img).unsqueeze(0#添加批量维度 形状从 (channels, height, width) 变为 (1, channels, height, width)
  return img, img_raw, scale
使用训练好的模型进行推理,这个过程非常简单,直接使用model即可。
# 执行Pytorch模型推理
model = load_model(net, trained_weigth_path, cpu)
def inference_pytorch(model, img):
  model.eval() #设置模型为评估模式
  loc, conf, landms = model(img) 
  # loc:位置预测(bounding box location predictions)
  # conf:置信度(confidence scores)
  # landms:关键点(landmark predictions)
  print("loc shape:", loc.shape)
  print("conf shape:", conf.shape)
  print("landms shape:", landms.shape)  
  return loc, conf, landms
对模型预测的结果进行解码,为了对模型预测的结果进行解码,我们需要使用先验框(prior boxes)。这些先验框帮助模型更准确地预测目标的位置。最后,返回解码后的边框、置信度分数和关键点坐标。
#生成先验框
class PriorBox(object):
    def __init__(self, cfg, image_size=None, phase='train'):
        super(PriorBox, self).__init__()
        self.min_sizes = cfg['min_sizes']
        self.steps = cfg['steps']
        self.clip = cfg['clip']
        self.image_size = image_size
        self.feature_maps = [[ceil(self.image_size[0]/step), ceil(self.image_size[1]/step)] for step in self.steps]
        self.name = "s"

    def forward(self):
        anchors = []
        for k, f in enumerate(self.feature_maps):
            min_sizes = self.min_sizes[k]
            for i, j in product(range(f[0]), range(f[1])):
                for min_size in min_sizes:
                    s_kx = min_size / self.image_size[1]
                    s_ky = min_size / self.image_size[0]
                    dense_cx = [x * self.steps[k] / self.image_size[1for x in [j + 0.5]]
                    dense_cy = [y * self.steps[k] / self.image_size[0for y in [i + 0.5]]
                    for cy, cx in product(dense_cy, dense_cx):
                        anchors += [cx, cy, s_kx, s_ky]
        # back to torch land
        output = torch.Tensor(anchors).view(-14)
        if self.clip:
            output.clamp_(max=1, min=0)
        return output

#解码先验框
def py_cpu_nms(dets, thresh):
    """Pure Python NMS baseline."""
    x1 = dets[:, 0]
    y1 = dets[:, 1]
    x2 = dets[:, 2]
    y2 = dets[:, 3]
    scores = dets[:, 4]
    areas = (x2 - x1 + 1) * (y2 - y1 + 1)
    order = scores.argsort()[::-1]
    keep = []
    while order.size > 0:
        i = order[0]
        keep.append(i)
        xx1 = np.maximum(x1[i], x1[order[1:]])
        yy1 = np.maximum(y1[i], y1[order[1:]])
        xx2 = np.minimum(x2[i], x2[order[1:]])
        yy2 = np.minimum(y2[i], y2[order[1:]])
        w = np.maximum(0.0, xx2 - xx1 + 1)
        h = np.maximum(0.0, yy2 - yy1 + 1)
        inter = w * h
        ovr = inter / (areas[i] + areas[order[1:]] - inter)
        inds = np.where(ovr <= thresh)[0]
        order = order[inds + 1]
    return keep

#解码位置
def decode_loc(loc, priors, variances):
    boxes = torch.cat((
        priors[:, :2] + loc[:, :2] * variances[0] * priors[:, 2:],
        priors[:, 2:] * torch.exp(loc[:, 2:] * variances[1])), 1)
    boxes[:, :2] -= boxes[:, 2:] / 2
    boxes[:, 2:] += boxes[:, :2]
    return boxes
#解码关键点
def decode_landm(pre, priors, variances):
    landms = torch.cat((priors[:, :2] + pre[:, :2] * variances[0] * priors[:, 2:],
                        priors[:, :2] + pre[:, 2:4] * variances[0] * priors[:, 2:],
                        priors[:, :2] + pre[:, 4:6] * variances[0] * priors[:, 2:],
                        priors[:, :2] + pre[:, 6:8] * variances[0] * priors[:, 2:],
                        priors[:, :2] + pre[:, 8:10] * variances[0] * priors[:, 2:],
                        ), dim=1)
    return landms
    
# 解码预测结果 
def decode_results(loc, conf, landms, resize, scale, img_shape):
  im_height = img_shape[2]
  im_width = img_shape[3
  #### 解码预测结果 #####
  priorbox = PriorBox(cfg, image_size=(im_height, im_width)) #生成先验框(prior boxes),用于帮助模型预测目标的位置
  priors = priorbox.forward() #生成先验框
  prior_data = priors.data #将先验框数据转换为 NumPy 数组 
  #解码位置预测,将模型的输出解码为实际的边框坐标 
  boxes = decode_loc(torch.tensor(loc[0]), torch.tensor(prior_data), cfg['variance']) #使用先验框和模型的输出(loc[0])解码出实际的边框坐标。cfg['variance'] 是解码过程中使用的方差。
  boxes = boxes * scale / resize #对解码后的边框进行缩放,使其与原图的大小一致。scale 是之前定义的缩放比例张量,resize 是缩放因子,这里为1。
  boxes = boxes.cpu().numpy() #将解码后的边框转换为 NumPy 数组
  #提取置信度 
  scores = conf[0][:, 1#提取了每个预测框的置信度分数
  #解码关键点预测
  landms = decode_landm(torch.tensor(landms[0]), torch.tensor(prior_data), cfg['variance'])
  scale1 = torch.Tensor([img_shape[3], img_shape[2], img_shape[3], img_shape[2],
                        img_shape[3], img_shape[2], img_shape[3], img_shape[2],
                        img_shape[3], img_shape[2]])
  landms = landms * scale1 / resize
  landms = landms.cpu().numpy()
  return boxes, scores, landms
接下来过滤检测结果,我们首先根据置信度阈值筛选出高置信度的边框和关键点。接着,通过非极大值抑制(NMS)算法去除重叠的检测框,以确保每个目标只保留一个最具代表性的检测框。最终,输出经过筛选和去重的高质量检测结果。
# 过滤检测框
def filter_boxes(boxes, scores, landms, confidence_threshold, nms_threshold):
  # 将 PyTorch 张量转换为 NumPy 数组(这里有修改,和原来的代码不一样)
  scores = scores.detach().cpu().numpy()
  # 过滤低置信度检测结果
  inds = np.where(scores > confidence_threshold)[0]
  boxes = boxes[inds] #保留置信度高于阈值的边框
  landms = landms[inds] #保留置信度高于阈值的关键点
  scores = scores[inds] #保留置信度高于阈值的置信度分数
  # 按置信度排序
  order = scores.argsort()[::-1#[::-1] 表示从高到低排序。
  boxes = boxes[order] #按排序后的索引重新排列边框
  landms = landms[order] #按排序后的索引重新排列关键点
  scores = scores[order] #按排序后的索引重新排列置信度分数
  # 将边框和置信度分数水平堆叠在一起,形成一个新的数组 dets。scores[:, np.newaxis] 将置信度分数转换为列向量。
  dets = np.hstack((boxes, scores[:, np.newaxis], landms)).astype(np.float32, copy=False)
  # 使用 NMS 算法去除重叠的检测框。nms_threshold 是 NMS 的阈值,py_cpu_nms 是一个实现 NMS 的函数。keep 是保留下来的检测框的索引。
  keep = py_cpu_nms(dets[:, :5], nms_threshold)
  dets = dets[keep, :] #保留 NMS 后的检测框
  landms = landms[keep] #保留 NMS 后的关键点
  return dets
最后,我们在原图上标注检测结果,根据置信度阈值绘制矩形框,写上置信度分数,然后在图上标注关键点(双眼,鼻子,两边嘴角),保存图片。
# 在图上绘制检测结果
def draw_detection(image, dets, save_path, vis_thres=0.6):
  # 绘制检测结果(可能多张人脸)
  for i, b in enumerate(dets):
      if b[4] < vis_thres: #只展示高置信度人脸
          continue
      text = "{:.4f}".format(b[4])
      b = list(map(int, b))
      #绘制人脸边框
      cv2.rectangle(image, (b[0], b[1]), (b[2], b[3]), (00255), 5)
      cx = b[0]
      cy = b[1] + 12
      #写上置信度
      cv2.putText(image, text, (cx, cy),
                  cv2.FONT_HERSHEY_DUPLEX, 0.5, (255255255))
      # 绘制关键点
      cv2.circle(image, (b[5], b[6]), 3, (00255), 8#1是半径,4是线宽
      cv2.circle(image, (b[7], b[8]), 3, (0255255), 8)
      cv2.circle(image, (b[9], b[10]), 3, (2550255), 8)
      cv2.circle(image, (b[11], b[12]), 3, (02550), 8)
      cv2.circle(image, (b[13], b[14]), 3, (25500), 8)
  # 保存结果图像
  cv2.imwrite(save_path, image)
  print(f"Detection result saved to {save_path}")
完整流程如下:加载图片并预处理,执行推理,解码结果,过滤结果,保存标注。
#人脸检测,使用Pytorch模型
def check_face_use_pytorch(model, image_path):
  # 加载图片并预处理
  img, img_raw, scale = preprocess
  # 执行Pytorch模型推理
  loc, conf, landms = inference_pytorch(model, img)
  # 推理结果解码
  boxes, scores, landms = decode_results(loc, conf, landms, resize, scale, img.shape)
  # 过滤检测框
  dets = filter_boxes(boxes, scores, landms, confidence_threshold, nms_threshold)
  # 在图像上结果标注并保存
  draw_detection(img_raw, dets, save_path, vis_thres)
这里补充说明下NMS,非极大值抑制(NMS)算法可以去除人脸检测过程冗余重叠的检测框。其具体步骤如下:
  • 置信度排序:首先,根据每个检测框的置信度(即模型对该框中目标存在的信任程度)进行排序,从高到低排列。

  • 选择最高置信度框:选择置信度最高的检测框作为当前的最佳框。

  • 计算重叠度:对于剩下的所有检测框,计算它们与当前最佳框的重叠度。重叠度通常使用交并比来衡量。

  • 抑制重叠框:如果某个检测框与当前最佳框的IoU超过预设的阈值(例如0.5),则认为它们重叠过多,应该被抑制,即从候选框列表中移除。

  • 重复过程:从剩下的检测框中再次选择置信度最高的框,重复上述步骤,直到处理完所有框。

    通过NMS过滤冗余重叠的检测框】

交并比(Intersection over Union, IoU)是两个框的交集面积与并集面积之比,数值在0到1之间,可以用来衡量重叠度。其计算公式:

06

模型部署与应用

我们将训练好的模型部署到生产服务器,通过API来调用模型进行推理。为了实现这一点,我们需要为模型开发一套API服务。幸运的是,一些第三方服务器引擎已经为我们简化了这一过程。我们可以利用TensorRT引擎或Triton Inference Server来轻松完成部署工作。接下来,我们将重点介绍如何使用Triton Inference Server进行部署和使用。
▌模型导出ONNX格式
ONNX是一种通用的模型格式,类似于PyTorch,但它具有更好的跨平台兼容性,并提供了一种标准化的模型表示方式。最最重要的是,许多推理引擎(如TensorRT、Triton Inference Server和ONNX Runtime)都对ONNX模型进行了专门优化,可以显著提升推理性能。因此,我们选择将PyTorch模型导出为ONNX格式,再进行部署。
# 创建dummy input,使用任意大小的图像,但批量大小固定为1
dummy_input = torch.randn(13480640)  # 这个大小只是一个示例
# 导出为ONNX模型,设置动态输入形状
torch.onnx.export(
    model,                   # PyTorch 模型
    dummy_input,             # 示例输入张量
    "path/kavin_face_model.onnx",  # 导出ONNX路径
    verbose=True,            # 是否打印导出过程的详细信息
    input_names=['input'],   # 输入的名字
    output_names=['loc''conf''landms'], # 输出的名字
    opset_version=11,        # ONNX 的操作集版本
    dynamic_axes={
        'input': {0'batch_size'2'height'3'width'},  # 设置动态维度
        'loc': {0'batch_size'},  # 批量大小是动态的
        'conf': {0'batch_size'},  # 批量大小是动态的
        'landms': {0'batch_size'}  # 批量大小是动态的
    }
)
print("ONNX模型导出成功")

▌验证ONNX模型

ONNX模型依赖包onnx和onnxruntime需要提前安装好:
!pip install onnx onnxruntime
检测onnx模型是否完整:
import onnx# 加载 ONNX 模型onnx_model = onnx.load("path/kavin_face_model.onnx")# 检查模型的结构和确定模型的正确性,输出None表示符合ONNX规范print(onnx.checker.check_model(onnx_model))
打印onnx模型的输入输出:
import onnxruntime as ort
# 加载 ONNX 模型
onnx_model_path = "/path/kavin_face_model.onnx"
ort_session = ort.InferenceSession(onnx_model_path)
# 打印模型输入名称和形状
input_name = ort_session.get_inputs()[0].name
print(f"Input name: {input_name}")
print("Input shape:", ort_session.get_inputs()[0].shape)
# 打印模型输出名称和形状
output_names = [output.name for output in ort_session.get_outputs()]
for output_name in output_names:
    print(f"Output name: {output_name}")
    print("Output shape:", ort_session.get_outputs()[output_names.index(output_name)].shape)

输出结果如下,可以看到入参批处理数,图片高度,宽度,出参包括人脸位置loc,置信度conf,关键点landms。


Input name: inputInput shape: ['batch_size', 3, 'height', 'width'] Output name: loc Output shape: ['batch_size', 'Concatloc_dim_1', 4] Output name: conf Output shape: ['batch_size', 'Softmaxconf_dim_1', 2] Output name: landms Output shape: ['batch_size', 'Concatlandms_dim_1', 10]
▌使用ONNXRuntime运行模型
ONNXRuntime可以直接加载并执行ONNX模型,推理代码如下,可以获取到位置,置信度和关键点。
import onnxruntime as ort
# 加载 ONNX 模型
onnx_model_path = "model_repository/kavin_face_model/1/model.onnx"
ort_session = ort.InferenceSession(onnx_model_path)

# 执行ONNX模型推理
def inference_onnx(ort_session, img):
  # 前向传播
  input_name = ort_session.get_inputs()[0].name #获取输入名称
  loc, conf, landms = ort_session.run(None, {input_name: img}) #使用 ONNX Runtime 进行模型推理,第一个参数 None 表示我们要获取模型的所有输出,第二个参数是一个字典,键是输入名称,值是输入数据。
  # loc:位置预测(bounding box location predictions)
  # conf:置信度(confidence scores)
  # landms:关键点(landmark predictions)
  print("loc shape:", loc.shape)
  print("conf shape:", conf.shape)
  print("landms shape:", landms.shape)  
  return loc, conf, landms
接着我们使用同样方法加载图片,推理并解码,然后标注结果。
#人脸检测,使用ONNX模型
def check_face_use_onnx(ort_session, image_path):
  # 加载图片并预处理
  img, img_raw, scale = preprocess_image(image_path)
  # 执行ONNX模型推理
  loc, conf, landms = inference_onnx(ort_session, img)  
  # 解码结果,过滤,在图上标注并保存
  # ...同上

【使用ONNX模型推理并绘制结果】
▌Triton Inference Server部署ONNX模型
Triton Inference Server 是由 NVIDIA 开发的高性能推理服务工具。它支持多种深度学习框架,如 TensorRT、TensorFlow 和 PyTorch,能够灵活地部署和管理模型推理服务。Triton 提供动态批处理、模型版本控制和自动扩展功能,提升了推理效率和灵活性,并支持 NVIDIA GPU 和 CPU 的硬件加速。其架构如下图所示。
官方文档链接:
https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/getting_started/quickstart.html

Triton Inference Server 可以使用 Docker 轻松完成部署,简化了环境配置和管理过程。它支持 RESTful 和 gRPC 等多种接口调用方式,方便用户根据不同需求进行集成和访问。其架构图下图所示。


【Triton Inference Server架构】
Triton Inference Server 部署ONNX模型,首先编写配置文件config.pbtxt来描述模型的配置,配置根据模型的具体情况填写正确的入参和出参,配置错误会导致模型无法正常加载和推理(这个非常关键)。
name: "kavin_face_model"
platform: "onnxruntime_onnx"
max_batch_size: 1
input [
  {
    name: "input"
    data_type: TYPE_FP32
    dims: [3-1-1]
  }
]
output [
  {
    name: "loc"
    data_type: TYPE_FP32
    dims: [-14]
  },
  {
    name: "conf"
    data_type: TYPE_FP32
    dims: [-12]
  },
  {
    name: "landms"
    data_type: TYPE_FP32
    dims: [-110]
  }
]
instance_group [
  {
    count: 1
    kind: KIND_CPU
  }
]
然后准备模型仓库目录,结构如下,请注意结构层级关系和文件名都会影响。kavin_face_model目录名,和你config.pbtxt配置的模型name一致,下层级1表示第一版模型,model.onnx是导出的onnx模型,文件名必须为model.onnx,然后和1平级的是配置文件config.pbtxt。
model_repository/└── kavin_face_model/    ├── 1/    │   └── model.onnx    └── config.pbtxt
Docker启动容器,具体启动命令如下:
docker run --name kavin_face_server -p 8000:8000 -p 8001:8001 -p 8002:8002 \    -v $(pwd)/model_repository:/models \       -d nvcr.io/nvidia/tritonserver:24.07-py3 tritonserver --model-repository=/models

--name kavin_face_server容器名

-p 8000:8000:映射 HTTP/REST API 端口
-p 8001:8001:映射 gRPC API 端口
-p 8002:8002:映射 Prometheus 端口,用于监控
-v $(pwd)/model_repository:/models挂载模型到容器,注意路径问题nvcr.io/nvidia/tritonserver:24.07-py3 tritonserver --model-repository=/models :启动服务并指定模型存储目录
这里注意使用的镜像版本,首次拉取远程镜像会比较慢,要解决科学上网问题。
可以打印docker日志来确认服务加载是否有错误,查看容器状态。
docker logs kavin_face_server
docker ps -a
启动正常日志可以看到模型情况:READY

+------------------+---------+--------+

| Model            | Version | Status |

+------------------+---------+--------+

| kavin_face_model | 1       | READY  |

+------------------+---------+--------+

▌部署集群

为了保障服务高可用,通常我们会将服务部署为一个集群,我们创建docker-compose.yml 文件(这里方便本地测试使用docker compose,生产一般使用K8s)。
version: '3.8'
services:
  kavin_face_server:
    image: nvcr.io/nvidia/tritonserver:24.07-py3
    container_name: kavin_face_server
    ports:
      - "8000:8000"
      - "8001:8001"
      - "8002:8002"
    volumes:
      - ./model_repository:/models
    command: tritonserver --model-repository=/models
    deploy:
      replicas: 3  # 设置为3个实例来创建一个集群
      resources:
        limits:
          cpus: '1'
          memory: 2G
    restart: always
使用 docker-compose up -d 命令启动集群。
▌调用模型API完成推理
服务启动后就可以使用curl进行调用模型接口进行测试。
状态检查接口:正常返回200状态,无返回值。
curl -v localhost:8000/v2/health/ready
检测模型状态接口:正常返回模型信息。
curl -v localhost:8000/v2/models/kavin_face_model
模型推理接口,使用模型来完成人脸检测,注意接口返回数据格式和出参顺序。
import requests
def inference_api(img):
  # 构造推理请求的JSON结构
  infer_request = {
        "inputs": [{
                "name""input",
                "shape": img.shape,
                "datatype""FP32",
                "data": img.flatten().tolist()
            }]
  }  
  # 发送推理请求
  try:
    url = 'http://localhost:8000/v2/models/kavin_face_model/infer'
    response = requests.post(url, json=infer_request)
    result = response.json()
    if response.status_code != 200:
      raise Exception(result)
  except Exception as e:
    print("Error in inference request:", e)
    return
  # 解析推理结果,注意这里的返回值顺序
  conf = np.array(result['outputs'][0]['data']).reshape(result['outputs'][0]['shape'])
  landms = np.array(result['outputs'][1]['data']).reshape(result['outputs'][1]['shape'])
  loc = np.array(result['outputs'][2]['data']).reshape(result['outputs'][2]['shape'])
  return loc, conf, landms
#人脸检测,使用API
def check_face_use_api(image_path):
  # 加载图片并预处理
  img, img_raw, scale = preprocess_image(image_path)
  # 执行API推理
  loc, conf, landms = inference_api(img)
  # 解码结果,过滤,在图上标注并保存
  # ...同上

【使用Triton Inference Server API执行模型推理并绘制结果】

▌封装推理服务

一般对外提供服务时,可以使用Python或Java在外面再封装一层API,增加对入参的检测,出参结果的封装(封装成01介绍的接口格式)。为了保障性能,可以通过gRPC来调用Triton Inference Server接口进行推理。这部分并不复杂就不展开赘述了,具体可以参看代码。

07

总结

本文围绕人脸检测任务展开,详细介绍了模型使用的数据集,算法,模型训练和评估过程。然后通过具体示例演示了使用模型检测图片上的人脸以及标注后的效果。最后介绍了模型在生产环境如何部署和使用,可以说完整的带你了解了人脸检测的落地过程。当然实际应用过程还有一些细节需要考虑,根据不同的场景也有一些注意和调整的地方。大家在使用过程中有任何问题欢迎与我交流讨论。

下一篇文章,我们将继续探索人脸风控,通过检测的人脸,我们如何提取出有效特征,进而完成人脸的比对,搜索等更具体的任务。敬请持续关注公众号“技术岁月”。
▌参考文献
[1] Zhu X, Ramanan D. Face detection, pose estimation, and landmark localization in the wild[C]//2012 IEEE conference on computer vision and pattern recognition. IEEE, 2012: 2879-2886.[2] Jain V, Learned-Miller E. Fddb: A benchmark for face detection in unconstrained settings[R]. UMass Amherst technical report, 2010.
[3] Yang S, Luo P, Loy C C, et al. Wider face: A face detection benchmark[C]//Proceedings of the IEEE conference on computer vision and pattern recognition. 2016: 5525-5533.
[4] Zhang K, Zhang Z, Li Z, et al. Joint face detection and alignment using multitask cascaded convolutional networks[J]. IEEE signal processing letters, 2016, 23(10): 1499-1503.
[5] Deng J, Guo J, Zhou Y, et al. Retinaface: Single-stage dense face localisation in the wild[J]. arXiv preprint arXiv:1905.00641, 2019.
[6] Howard A G, Zhu M, Chen B, et al. Mobilenets: Efficient convolutional neural networks for mobile vision applications[J]. arXiv preprint arXiv:1704.04861, 2017.
[7] He K, Zhang X, Ren S, et al. Deep residual learning for image recognition[C]//Proceedings of the IEEE conference on computer vision and pattern recognition. 2016: 770-778.
[8] Wang F, Chen L, Li C, et al. The devil of face recognition is in the noise[C]//Proceedings of the European Conference on Computer Vision (ECCV). 2018: 765-780.
[9] Wu W, Peng H, Yu S. Yunet: A tiny millisecond-level face detector[J]. Machine Intelligence Research, 2023, 20(5): 656-665.

本文涉及代码请在“技术岁月”后台发送关键字“人脸检测代码”获取,涉及论文发送关键字“人脸论文”获取。

如果觉得文章还行,请一定要点赞支持哦,拜托了!最后感谢您的阅读~

请使用浏览器的分享功能分享到微信等