人脸检测应用案例

学习任务

  人脸检测是一种在多种应用中使用的计算机技术,可以识别数字图像中的人脸。人脸检测可以视为目标检测的一种特殊情况。在目标检测中,任务是查找图像中给定类的所有对象的位置和大小。通过本章的学习,可以了解如何使用MTCNN提取人脸区域和特征点,为后续例如人脸识别和人脸图片预处理做铺垫。

知识点

   目标识别、卷积神经网络、MTCNN

1.问题描述

人脸检测的目标是找出图像中所有的人脸对应的位置,算法的输出是人脸外接矩形在图像中的坐标,可能还包括姿态如倾斜角度等信息。下面是一张图像的人脸检测结果: OIP-C (1).jpg

以及多人的情况: OIP-C (2).jpg

人脸检测对于我们人类非常容易,出于社会生活的需要,我们大脑中有专门的人脸检测模块,对人脸非常敏感,即使很少量的信息,如简笔画,大脑也能轻易检测出人脸,和各自的表情。人脸检测非常重要,那到底有什么用呢?

自动人脸检测是所有自动人脸图像分析的应用的基础,包括但不限于:人脸识别和验证,监控场合的人脸跟踪,面部表情分析,面部属性识别(性别/年龄识别,颜值评估),面部光照调整和变形,面部形状重建,图像视频检索,数字相册的组织和演示。

人脸检测也是所有基于视觉的人与电脑、人与机器人等交互的系统的初始步骤。例如主流商业数码相机都内嵌了人脸检测,用于辅助自动对焦。很多社交网络,如FaceBook等,也用人脸检测机制实现人物标记。

从问题的领域来看,人脸检测属于目标检测领域,目标检测通常有两大类:

  • 通用目标检测:检测图像中多个类别的目标,比如ILSVRC2017的VID任务要检测200类目标,VOC2012要检测20类目标,通用目标检测核心是n(目标)+1(背景)=n+1分类问题。这类检测通常模型比较大,速度较慢,很少有现成的方法能做到CPU实时性。(注:ILSVRC是一个比赛,全称是ImageNet Large-Scale Visual Recognition Challenge 大规模视觉识别挑战赛,近年来机器视觉领域最受追捧也是最具权威的学术竞赛之一。VOC全称是Visual Object Classes,该项挑战赛的主要目的是对真实场景中的物体进行识别)
  • 特定类别目标检测:仅检测图像中某一类特定目标,如人脸检测、行人检测、车辆检测等等,特定类别目标检测核心是1(目标)+1(背景)=2分类问题。这类检测通常模型比较小,速度要求非常高,这里问题的基本要求就是CPU实时性。

目前在人脸检测中应用较广的算法是MTCNN(Multi-task Cascaded Convolutional Networks,多任务级联卷积网络)。MTCNN算法是一种基于深度学习的人脸检测和人脸对齐方法,它可以同时完成人脸检测和人脸对齐的任务,相比于传统的算法,它的性能更好,检测速度更快。

2.主要模型介绍

总体流程:MTCNN检测识别主要分为三个阶段进行:

第一阶段:人脸检测;

第二阶段:特征提取;

第三阶段:人脸对比。

d01373f082025aafea7f378e3fd6c962024f1ae3.jpeg

从上图可以知道主要包括四个操作,三个步骤。

1、图像金字塔

对图片进行Resize(调整大小)操作,将原始图像缩放成不同的尺度,生成图像金字塔。然后将不同尺度的图像送入到三个子网络中进行训练,目的是为了可以检测到不同大小的人脸,从而实现多尺度目标检测。

2、P-Net(Proposal Network,候选网络)

mtcnn-pnet.png

P-Net是一个人脸区域的候选网络,该网络的输入是一个12x12x3的图像,通过3层的卷积之后,判断这个12x12的图像中是否存在人脸,并且给出人脸框的回归和人脸关键点。

网络的第一部分输出是用来判断该图像是否存在人脸,输出向量大小1x1x2,也就是两个值。

网络的第二部分给出框的精确位置,一般称为框回归。P-Net输入的12×12的图像块可能并不是完美的人脸框的位置,如有的时候人脸并不正好为方形,有可能12×12的图像偏左或偏右,因此需要输出当前框位置相对完美的人脸框位置的偏移。这个偏移大小为1×1×4,即表示框左上角的横坐标的相对偏移、框左上角的纵坐标的相对偏移、框的宽度的误差、框的高度的误差。

网络的第三部分给出人脸的5个关键点的位置。5个关键点分别对应着左眼的位置、右眼的位置、鼻子的位置、左嘴巴的位置、右嘴巴的位置。每个关键点需要两维来表示,因此输出向量大小为1×1×10。

3、R-Net(Refine Network,精细化网络) mtcnn-rnet.png

从网络图可以看到(上图左侧标注),由于该网络结构和P-Net网络结构上具有差异,多了一个全连接层,所以会取得更好的抑制false-positive(假阳性)的作用。在输入R-Net之前,都需要缩放到24x24x3,网络的输出与P-Net相同,R-Net的目的是为了去除大量的非人脸框。

4、O-Net(Output Network,输出网络) mtcnn-onet.png

从网络图可以看到,该层比R-Net层又多了一层卷积层,所以处理的结果会更加精细。输入的图像大小48x48x3,输出包括N个边界框的坐标信息,score得分以及关键点位置。

3.数据描述

该算法训练数据来源于wider和celeba两个公开的数据库,wider提供人脸检测数据,在大图上标注了人脸框事实Ground True的坐标信息,celeba提供了5个关键点的数据。根据参与任务的不同,将训练数据分为四类:

  • 负样本:滑动窗口和Ground True的IOU小于0.3;
  • 正样本:滑动窗口和Ground True的IOU大于0.65;
  • 中间样本:滑动窗口和Ground True的IOU大于0.4小于0.65;
  • 关键点:包含5个关键点坐标的;

4.实验过程

  MTCNN采用多任务级联思想,三个网络可以并行训练,所以三个网络都是单独训练的。P网络训练所使用的图像是12*12尺寸的图像,R网络则是使用24*24的图像,O网络使用48*48的网络。从P-Net、R-Net、再到最后的O-Net,网络输入的图像越来越大,卷积层的通道数越来越多,网络的深度(层数)也越来越深,因此识别人脸的准确率也是越来越高的。三个网络的输出都是5个值,分别为置信度和坐标偏移值。在整个网络的训练学习过程中实际就是训练的偏移框。不能直接学习真实框,如果学习不好就会丢失真实框,所以学习的是偏移框,然后反算回真实框。置信度通过IOU计算得来,它表示窗口滑动到当前位置时,检测到人脸的概率值;我们在训练网络时用正负样本训练是否是人脸的置信度,用正样本和部分样本训练人脸的位置坐标。

4.1获取原始数据集

本次模型训练采用Celeba数据集。 下载好的Celeba的数据集是一个压缩包,解压之后得到以下文件夹,

ce18161c5d287ba6869f4d616f90a95e.png

打开img\img_celeba.7z文件夹,一次性选中所有的的文件夹,将所有的压缩包解压到指定位置,比如存放的路径为E:\CelebA\Img\img_celeba.7z\img_celeba

解压完成后如图: 79457e0b9f910daabf859c2a6dd05bf6.png

4.2准备训练样本

1)训练样本的构成

a.种类

本项目需要准备三种样本:正样本、负样本、部分样本,比例约等于1:1:3,之所以部分样本比较多是因为在级联处理中负样本会大量丢失,这样做的目的是为了保证最后进入R网络和O网络时三种样本的比例均衡。

正样本:整张图全是人脸

负样本:图像为背景

部分样本:一张图中有部分是人脸,另外一部分是非人脸。

b.形状

本项目的训练样本共涉及三种大小:12x12(用于P网络训练),24x24(用于R网络训练),48x48(用于O网络训练)

2)建立数据集样本 建立数据集样本应注意以下几点:

a. 图片路径和标签一一对应,方便训练时读取数据

b. 三个网络是分开训练,不同大小的图片各自有各自的正样本、负样本和部分样本。

c. 部分样本和负样本是在正样本附近偏移得到的

最终得到数据样本是这个样子:

acb4da9eda5d203b55e70d826d82a30d.png

d9a2aad2e147747741890716f7ee1dce.png

92a5905d6f3407bd5cb68335e141754f.png

e7d21fd3a03fe80455d45596ab235d08.png

3)生成样本数据的代码

import os
from PIL import Image
import numpy as np
import utils
import traceback
 
anno_src = r"E:\CelebA\Anno\list_bbox_celeba.txt"          #原来的样本数据(在生成样本时使用)
img_dir = r"E:\CelebA\Img\img_celeba.7z\img_celeba"        #源图片(用于生成新样本)
 
save_path = r"E:\CelebA\MTCN\dataSet"                      #生成样本的总的保存路径
 
float_num = [0.1, 0.5, 0.5, 0.5, 0.9, 0.9, 0.9, 0.9, 0.9]  #控制正负样本比例
 
def gen_sample(face_size,stop_value):
    print("gen size:{} image" .format(face_size))
    positive_image_dir = os.path.join(save_path, str(face_size), "positive")     #仅仅生成路径名
    negative_image_dir = os.path.join(save_path, str(face_size), "negative")
    part_image_dir = os.path.join(save_path, str(face_size), "part")
  
    for dir_path in [positive_image_dir, negative_image_dir, part_image_dir]:     #生成路径
        if not os.path.exists(dir_path):
            os.makedirs(dir_path)
 
    positive_anno_filename = os.path.join(save_path, str(face_size), "positive.txt")
    negative_anno_filename = os.path.join(save_path, str(face_size), "negative.txt")
    part_anno_filename = os.path.join(save_path, str(face_size), "part.txt")
 
    positive_count = 0
    negative_count = 0
    part_count = 0
 
    try:                                                                            
        positive_anno_file = open(positive_anno_filename, "w")
        negative_anno_file = open(negative_anno_filename, "w")
        part_anno_file = open(part_anno_filename, "w")
 
        for i, line in enumerate(open(anno_src)):         #txt文件首行不是路径和标签,需要跳过
            if i < 2:
                continue
            try:
                strs = line.split()                       #列表,包含路径和坐标值
                image_filename = strs[0].strip()          #置信度   
                # print(image_filename)
                image_file = os.path.join(img_dir, image_filename)
                with Image.open(image_file) as img:
                    img_w, img_h = img.size              #原图
                    x1 = float(strs[1].strip())
                    y1 = float(strs[2].strip())
                    w = float(strs[3].strip())           #人脸框
                    h = float(strs[4].strip())
                    x2 = float(x1 + w)
                    y2 = float(y1 + h)
                    px1 = 0#float(strs[5].strip())
                    py1 = 0#float(strs[6].strip())
                    px2 = 0#float(strs[7].strip())
                    py2 = 0#float(strs[8].strip())
                    px3 = 0#float(strs[9].strip())
                    py3 = 0#float(strs[10].strip())
                    px4 = 0#float(strs[11].strip())
                    py4 = 0#float(strs[12].strip())
                    px5 = 0#float(strs[13].strip())
                    py5 = 0#float(strs[14].strip())
                    if x1 < 0 or y1 < 0 or w < 0 or h < 0:  #跳过坐标值为负数的
                        continue
                    boxes = [[x1, y1, x2, y2]]              #当前真实框四个坐标(根据中心点偏移), 二维数组便于IOU计算
                                                            #求中心点坐标
                    cx = x1 + w / 2
                    cy = y1 + h / 2
                    side_len = max(w, h)
                    seed = float_num[np.random.randint(0, len(float_num))]  
                    count = 0
                    for _ in range(4):
                        _side_len = side_len + np.random.randint(int(-side_len * seed), int(side_len * seed)) #生成框
                        _cx = cx + np.random.randint(int(-cx * seed), int(cx * seed))    #中心点作偏移
                        _cy = cy + np.random.randint(int(-cy * seed), int(cy * seed))
                        _x1 = _cx - _side_len / 2       #左上角
                        _y1 = _cy - _side_len / 2
                        _x2 = _x1 + _side_len           #右下角
                        _y2 = _y1 + _side_len
                        if _x1 < 0 or _y1 < 0 or _x2 > img_w or _y2 > img_h:    #左上角的点是否偏移到了框外边,右下角的点大于图像的宽和高
                            continue
 
                        offset_x1 = (x1 - _x1) / _side_len                      #得到四个偏移量
                        offset_y1 = (y1 - _y1) / _side_len
                        offset_x2 = (x2 - _x2) / _side_len
                        offset_y2 = (y2 - _y2) / _side_len
 
                        offset_px1 = 0#(px1 - x1_) / side_len     #offset偏移量
                        offset_py1 = 0#(py1 - y1_) / side_len
                        offset_px2 = 0#(px2 - x1_) / side_len
                        offset_py2 = 0#(py2 - y1_) / side_len
                        offset_px3 = 0#(px3 - x1_) / side_len
                        offset_py3 = 0#(py3 - y1_) / side_len
                        offset_px4 = 0#(px4 - x1_) / side_len
                        offset_py4 = 0#(py4 - y1_) / side_len
                        offset_px5 = 0#(px5 - x1_) / side_len
                        offset_py5 = 0#(py5 - y1_) / side_len
 
                        crop_box = [_x1, _y1, _x2, _y2]
                        face_crop = img.crop(crop_box)       #图片裁剪
                        face_resize = face_crop.resize((face_size, face_size)) #对裁剪后的图片缩放
 
                        iou = utils.iou(crop_box, np.array(boxes))[0]
                        if iou > 0.65:        #可以自己修改
                            positive_anno_file.write(
                                "positive/{0}.jpg {1} {2} {3} {4} {5} {6} {7} {8} {9} {10} {11} {12} {13} {14} {15}\n".format(
                                    positive_count, 1, offset_x1, offset_y1,
                                    offset_x2, offset_y2, offset_px1, offset_py1, offset_px2, offset_py2, offset_px3,
                                    offset_py3, offset_px4, offset_py4, offset_px5, offset_py5))
                            positive_anno_file.flush()   
                            face_resize.save(os.path.join(positive_image_dir, "{0}.jpg".format(positive_count)))
                            # print("positive_count",positive_count)
                            positive_count += 1
                        elif 0.65 > iou > 0.4:
                            part_anno_file.write(
                                "part/{0}.jpg {1} {2} {3} {4} {5} {6} {7} {8} {9} {10} {11} {12} {13} {14} {15}\n".format(
                                    part_count, 2, offset_x1, offset_y1,offset_x2,
                                    offset_y2, offset_px1, offset_py1, offset_px2, offset_py2, offset_px3,
                                    offset_py3, offset_px4, offset_py4, offset_px5, offset_py5))
                            part_anno_file.flush()
                            face_resize.save(os.path.join(part_image_dir, "{0}.jpg".format(part_count)))
                            # print("part_count", part_count)
                            part_count += 1
                        elif iou < 0.1:
                            negative_anno_file.write(
                                "negative/{0}.jpg {1} 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n".format(negative_count, 0))
                            negative_anno_file.flush()
                            face_resize.save(os.path.join(negative_image_dir, "{0}.jpg".format(negative_count)))
                            # print("negative_count", negative_count)
                            negative_count += 1
                        count = positive_count+part_count+negative_count
                        print(count)
                    if count >= stop_value:
                        break
 
            except:
                traceback.print_exc()           #返回错误类型
    finally:
        positive_anno_file.close()
        negative_anno_file.close()
        part_anno_file.close()

gen_sample(12, 50000)
gen_sample(24, 50000)
gen_sample(48, 50000)
 

4.3创建网络模型

  1. PNet

我们可以利用pytorch已经包装好的库(torch.nn)来快速搭建神经网络结构。同时利用已经包装好的包含各种优化算法的库(torch.optim)来优化神经网络中的参数,如权值参数w和阈值参数b。 为了搭建PNet,我们torch.nn中的Sequential模块,torch.nn.Sequential是一个时序容器,我们可以通过调用其构造器,将神经网络模块按照输入层到输出层的顺序传入,以此构造完整的神经网络结构。

由上节的说明可知,PNet需要三个卷积网络,并且输出都要进行非线性处理。在设计神经网络时,对传递数据的形状(shape)有严格的要求,所以代码中将测试点以注释的形式给出,可以在调试的时候打开,在正式运行时关闭。

class PNet(nn.Module):
    def __init__(self):
        super(PNet, self).__init__()
        self.conv1 = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=10, kernel_size=3, stride=1,
                 padding=0, dilation=1, groups=1),     #10*10*10
            nn.BatchNorm2d(10),
            nn.ReLU(),
            nn.MaxPool2d((2, 2), 2)                    #5*5*10
        )
        self.conv2 = nn.Sequential(
            nn.Conv2d(in_channels=10, out_channels=16, kernel_size=3, stride=1,
                 padding=0, dilation=1, groups=1),     #3*3*16
            nn.BatchNorm2d(16),
            nn.ReLU()
        )
        self.conv3 = nn.Sequential(
            nn.Conv2d(in_channels=16, out_channels=32, kernel_size=3, stride=1,
                 padding=0, dilation=1, groups=1),     #1*1*32
            nn.BatchNorm2d(32),
            nn.ReLU()
        )
        self.conv4 = nn.Conv2d(in_channels=32, out_channels=5, kernel_size=1, stride=1,
                 padding=0, dilation=1, groups=1)
 
    def forward(self, x):
        y = self.conv1(x)
        # print(y.shape)
        y = self.conv2(y)
        y = self.conv3(y)
        # y = torch.reshape(y, [y.size(0), -1])
        y = self.conv4(y)
        # print(y)
        category = torch.sigmoid(y[:, 0:1])
        offset = y[:, 1:]
        # print(category.shape)
        # print(offset.shape)
        return category, offset
  1. RNet

RNet的训练数据主要由PNet生成的回归框和面部轮廓关键点组成。其中面部轮廓关键点的数据生成方式类似于PNet,不过对应的回归框大小是24*24。

模型输入为24*24大小的图片,通过28个333的卷积核和3*3(stride=2)的max pooling后生成28个11*11的特征图;通过48个3*3*28的卷积核和3*3(stride=2)的max pooling后生成48个4*4的特征图;通过64个2*2*48的卷积核后,生成64个3*3的特征图;把3*3*64的特征图转换为128大小的全连接层;对回归框分类问题转换为大小为2的全连接层;对bounding box的位置回归问题,转换为大小为4的全连接层;对人脸轮廓关键点转换为大小为10的全连接层。相应代码如下所示:

class RNet(nn.Module):
    def __init__(self):
        super(RNet, self).__init__()
        self.conv1 = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=28, kernel_size=3, stride=1,
                 padding=0, dilation=1, groups=1),   #22*22*28
            nn.BatchNorm2d(28),
            nn.ReLU(),
            nn.MaxPool2d((3, 3), 2, 1)               #11*11*28
        )
        self.conv2 = nn.Sequential(
            nn.Conv2d(in_channels=28, out_channels=48, kernel_size=3, stride=1,
                 padding=0, dilation=1, groups=1),    #9*9*48
            nn.BatchNorm2d(48),
            nn.ReLU(),
            nn.MaxPool2d((3, 3), 2, 0)                #4*4*48
        )
        self.conv3 = nn.Sequential(
            nn.Conv2d(in_channels=48, out_channels=64, kernel_size=2, stride=1,
                 padding=0, dilation=1, groups=1),    #3*3*64
            nn.BatchNorm2d(64),
            nn.ReLU()
        )
        self.fc1 = nn.Linear(3*3*64, 128)
        self.fc2 = nn.Linear(128, 5)
 
    def forward(self, x):
        y = self.conv1(x)
        # print(y.shape)
        y = self.conv2(y)
        # print(y.shape)
        y = self.conv3(y)
        # print(y.shape)
        y = torch.reshape(y, [y.size(0), -1])
        # print(y.shape)
        y = self.fc1(y)
        # print(y.shape)
        y = self.fc2(y)
        # print(y.shape)
 
        category = torch.sigmoid(y[:, 0:1])
        offset = y[:, 1:]
        return category, offset
  1. ONet ONet是MTCNN中的最后一个网络,用于做网络的最后输出。

模型输入是一个48*48*3大小的图片,通过32个3*3*3的卷积核和3*3(stride=2)的max pooling后转换为32个23*23的特征图;通过64个3*3*32的卷积核和3*3(stride=2)的max pooling后转换为64个10*10的特征图;通过64个3*3*64的卷积核和3*3(stride=2)的max pooling后转换为64个4*4的特征图;通过128个2*2*64的卷积核转换为128个3*3的特征图;通过全链接操作转换为256大小的全链接层;最后生成大小为2的回归框分类特征;大小为4的回归框位置的回归特征;大小为10的人脸轮廓位置回归特征。

class ONet(nn.Module):
    def __init__(self):
        super(ONet, self).__init__()
        self.conv1 = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=32, kernel_size=3, stride=1,
                 padding=0, dilation=1, groups=1),   #46*46*32
            nn.BatchNorm2d(32),
            nn.ReLU(),
            nn.MaxPool2d((3, 3), 2, 1)    #23*23*32
        )
        self.conv2 = nn.Sequential(
            nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, stride=1,
                 padding=0, dilation=1, groups=1),    #21*21*64
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.MaxPool2d((3, 3), 2)        #10*10*64
        )
        self.conv3 = nn.Sequential(
            nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, stride=1,
                 padding=0, dilation=1, groups=1),       #8*8*64
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.MaxPool2d((2, 2), 2)            #4*4*64
        )
        self.conv4 = nn.Sequential(
            nn.Conv2d(in_channels=64, out_channels=128, kernel_size=2, stride=1,
                 padding=0, dilation=1, groups=1),  #3*3*128
            nn.BatchNorm2d(128),
            nn.ReLU()
         )
        self.fc1 = nn.Sequential(
            nn.Linear(3*3*128, 256),
            nn.BatchNorm1d(256),
            nn.ReLU()
        )
        self.fc2 = nn.Linear(256, 5)
 
    def forward(self, x):
        y = self.conv1(x)
        # print(y.shape)
        y = self.conv2(y)
        # print(y.shape)
        y = self.conv3(y)
        # print(y.shape)
        y = self.conv4(y)
        y = torch.reshape(y, [y.size(0), -1])
        # print(y.shape)
 
        y = self.fc1(y)
        # print(y.shape)
        y = self.fc2(y)
        # print(y.shape)
        category = torch.sigmoid(y[:, 0:1])
        offset = y[:, 1:]
        return category, offset

4.4检测训练效果

人脸检测的总体流程为: mtcnn-infer.png

由原始图片和PNet生成预测的bounding boxes。输入原始图片和PNet生成的bounding box,通过RNet,生成校正后的bounding box。输入元素图片和RNet生成的bounding box,通过ONet,生成校正后的bounding box和人脸面部轮廓关键点。

import torch
from PIL import Image, ImageDraw, ImageFont
import numpy as np
import utils
import nets
from torchvision import transforms
import time
import os
 
class Detector:
    def __init__(self, pnet_param="./param/p_net.pth", rnet_param="./param/r_net.pth", onet_param="./param/o_net.pth",
                 isCuda=False):
        self.isCuda = isCuda
 
        self.pnet = nets2.PNet()
        self.rnet = nets2.RNet()
        self.onet = nets2.ONet()
 
        if self.isCuda:
            self.pnet.cuda()
            self.rnet.cuda()
            self.onet.cuda()
 
        self.pnet.load_state_dict(torch.load(pnet_param, map_location='cpu'))
        self.rnet.load_state_dict(torch.load(rnet_param, map_location='cpu'))
        self.onet.load_state_dict(torch.load(onet_param, map_location='cpu'))
 
        self.pnet.eval()
        self.rnet.eval()
        self.onet.eval()
 
        self.__image_transform = transforms.Compose([
            transforms.ToTensor(),
            transforms.Normalize(mean=[0.5,0.5,0.5],std=[0.5,0.5,0.5])
        ])
 
    def detect(self, image):
        start_time = time.time()
        pnet_boxes = self.__pnet_detect(image)
        if pnet_boxes.shape[0] == 0:
            return np.array([])
        end_time = time.time()
        t_pnet = end_time - start_time
 
        start_time = time.time()
        rnet_boxes = self.__rnet_detect(image, pnet_boxes)           #p网络输出的框和原图像输送到R网络中,O网络将框扩为正方形再进行裁剪,再缩放
        # print( rnet_boxes)
        if rnet_boxes.shape[0] == 0:
            return np.array([])
        end_time = time.time()
        t_rnet = end_time - start_time
 
        start_time = time.time()
        onet_boxes = self.__onet_detect(image, rnet_boxes)
        if onet_boxes.shape[0] == 0:
            return np.array([])
        end_time = time.time()
 
        t_onet = end_time - start_time
 
        t_sum = t_pnet + t_rnet + t_onet
 
        print("total:{0} pnet:{1} rnet:{2} onet:{3}".format(t_sum, t_pnet, t_rnet, t_onet))
 
        return onet_boxes
 
    def __pnet_detect(self, img):
        boxes = []
        w, h = img.size
        min_side_len = min(w, h)
 
        scale = 1
 
        while min_side_len >= 12:
            img_data = self.__image_transform(img)
            # img_data = img_data.unsqueeze_(0)
            if self.isCuda:
                img_data = img_data.cuda()
            img_data.unsqueeze_(0)  #升维度(新版pytorch可以删掉)
 
 
            _cls, _offest = self.pnet(img_data)  #NCHW
            # print(_cls.shape)    #torch.Size([1, 1, 1290, 1938])
            # print(_offest.shape)    #torch.Size([1, 4, 1290, 1938])
 
            cls, offest = _cls[0][0].cpu().data, _offest[0].cpu().data
            #_cls[0][0].cpu().data去掉NC,  _offest[0]去掉N
            # print(_cls.shape)       #torch.Size([1, 1, 1290, 1938])
            # print(_offest.shape)     #torch.Size([1, 4, 1290, 1938])
 
            idxs = torch.nonzero(torch.gt(cls, 0.6))   #取出置信度大于0.6的索引
            # print(idxs.shape)   #N2     #torch.Size([4639, 2])
 
            for idx in idxs:  #idx里面就是一个h和一个w
                # print(idx)    #tensor([ 102, 1904])
                # print(offest)
                boxes.append(self.__box(idx, offest, cls[idx[0], idx[1]], scale))  #反算
            scale *= 0.709
            _w = int(w * scale)
            _h = int(h * scale)
 
            img = img.resize((_w, _h))
            # print(min_side_len)
            min_side_len = np.minimum(_w, _h)
        return utils.nms(np.array(boxes), 0.3)
 
 
    def __box(self, start_index, offset, cls, scale, stride=2, side_len=12):  #side_len=12建议框大大小
 
        _x1 = int(start_index[1] * stride) / scale#宽,W,x
        _y1 = int(start_index[0] * stride) / scale#高,H,y
        _x2 = int(start_index[1] * stride + side_len) / scale
        _y2 = int(start_index[0] * stride + side_len) / scale
 
        ow = _x2 - _x1    #偏移量
        oh = _y2 - _y1
 
        _offset = offset[:, start_index[0], start_index[1]]   #通道层面全都要[C, H, W]
 
        x1 = _x1 + ow * _offset[0]
        y1 = _y1 + oh * _offset[1]
        x2 = _x2 + ow * _offset[2]
        y2 = _y2 + oh * _offset[3]
 
        return [x1, y1, x2, y2, cls]
 
    def __rnet_detect(self, image, pnet_boxes):
 
        _img_dataset = []
        _pnet_boxes = utils.convert_to_square(pnet_boxes)
        for _box in _pnet_boxes:
            _x1 = int(_box[0])
            _y1 = int(_box[1])
            _x2 = int(_box[2])
            _y2 = int(_box[3])
 
            img = image.crop((_x1, _y1, _x2, _y2))
            img = img.resize((24, 24))
            img_data = self.__image_transform(img)
            _img_dataset.append(img_data)
 
        img_dataset =torch.stack(_img_dataset)
        if self.isCuda:
            img_dataset = img_dataset.cuda()
 
        _cls, _offset = self.rnet(img_dataset)
        _cls = _cls.cpu().data.numpy()
        offset = _offset.cpu().data.numpy()
        boxes = []
 
        idxs, _ = np.where(_cls > 0.6)
        for idx in idxs:                      #只是取出合格的
            _box = _pnet_boxes[idx]
            _x1 = int(_box[0])
            _y1 = int(_box[1])
            _x2 = int(_box[2])
            _y2 = int(_box[3])
 
            ow = _x2 - _x1
            oh = _y2 - _y1
 
            x1 = _x1 + ow * offset[idx][0]
            y1 = _y1 + oh * offset[idx][1]
            x2 = _x2 + ow * offset[idx][2]
            y2 = _y2 + oh * offset[idx][3]
            cls = _cls[idx][0]
 
            boxes.append([x1, y1, x2, y2, cls])
 
        return utils.nms(np.array(boxes), 0.3)
 
    def __onet_detect(self, image, rnet_boxes):
 
        _img_dataset = []
        _rnet_boxes = utils.convert_to_square(rnet_boxes)
        for _box in _rnet_boxes:
            _x1 = int(_box[0])
            _y1 = int(_box[1])
            _x2 = int(_box[2])
            _y2 = int(_box[3])
 
            img = image.crop((_x1, _y1, _x2, _y2))
            img = img.resize((48, 48))
            img_data = self.__image_transform(img)
            _img_dataset.append(img_data)
 
        img_dataset = torch.stack(_img_dataset)
        if self.isCuda:
            img_dataset = img_dataset.cuda()
 
        _cls, _offset = self.onet(img_dataset)
 
        _cls = _cls.cpu().data.numpy()
        offset = _offset.cpu().data.numpy()
 
        boxes = []
        idxs, _ = np.where(_cls > 0.97)
        for idx in idxs:
            _box = _rnet_boxes[idx]
            _x1 = int(_box[0])
            _y1 = int(_box[1])
            _x2 = int(_box[2])
            _y2 = int(_box[3])
 
            ow = _x2 - _x1
            oh = _y2 - _y1
 
            x1 = _x1 + ow * offset[idx][0]
            y1 = _y1 + oh * offset[idx][1]
            x2 = _x2 + ow * offset[idx][2]
            y2 = _y2 + oh * offset[idx][3]
            cls = _cls[idx][0]
 
            boxes.append([x1, y1, x2, y2, cls])
 
        return utils.nms(np.array(boxes), 0.3, isMin=True)
        
if __name__ == '__main__':
    x = time.time()
    with torch.no_grad() as grad:
        path = r"D:\MTCNN\MTCNN\图片1"                            #遍历文件夹内的图片
        for name in os.listdir(path):
            img = os.path.join(path, name)
            image_file = img
            # image_file = r"1.jpg"
            # print(image_file)
            detector = Detector()
 
            with Image.open(image_file) as im:
                boxes = detector.detect(im)
                # print(boxes.shape)
                imDraw = ImageDraw.Draw(im)
                for box in boxes:
                    x1 = int(box[0])
                    y1 = int(box[1])
                    x2 = int(box[2])
                    y2 = int(box[3])
 
                    # print(x1)
                    # print(y1)
                    # print(x2)
                    # print(y2)
 
                    # print(box[4])
                    cls = box[4]
                    imDraw.rectangle((x1, y1, x2, y2), outline='red')
                    font = ImageFont.truetype(r"C:\Windows\Fonts\simhei", size=20)
                    # imDraw.text((x1, y1), "{:.3f}".format(cls), fill="red", font=font)
                y = time.time()
                print(y - x)
                im.show()

效果如图: 8da74543c22c2f3aa1257ebefdd58d53.png

5.小结

   MTCNN是一种多任务的方法,它将人脸区域检测和人脸关键点检测融合在一起,同级联CNN一样也是基于级联框架,但是算法整体思路更加巧妙合理。MTCNN总体来说分为三个部分:PNet、RNet和ONet。它是一个3级联的卷积神经网络,层层递进。PNet的输入是原图经过图像金字塔之后不同尺寸的图片,最后结果由ONet输出。

优点:网络轻量推理时间快,工程部署灵活性大,能够输出人脸关键点5点landmark。

缺点:人脸检测时间受人脸数量影响,模型训练稍显复杂。

说明:卷积神经网络的训练时间较长,另外训练过程可能出现欠拟合或者过拟合,使得检测能力下降。本文代码在pytorch 1.9.0 cpu版上测试通过。