主页:https://kpzhang93.github.io/MTCNN_face_detection_alignment/index.html论文:https://arxiv.org/abs/1604.02878代码:官方matlab版、C++ caffe版第三方训练代码:tensorflow、mxnet

MTCNN,恰如论文标题《Joint Face Detection and Alignment using Multi-task Cascaded Convolutional Networks》所言,采用级联CNN结构,通过多任务学习,同时完成了两个任务——人脸检测和人脸对齐,输出人脸的Bounding Box以及人脸的关键点(眼睛、鼻子、嘴)位置。

MTCNN 又好又快,提出时在FDDB、WIDER FACE和AFLW数据集上取得了当时(2016年4月)最好的结果,速度又快,现在仍被广泛使用作为人脸识别的前端,如InsightFace和facenet。

MTCNN效果为什么好,文中提了3个主要的原因:

  1. 精心设计的级联CNN架构(carefully designed cascaded CNNs architecture)
  2. 在线困难样本挖掘(online hard sample mining strategy)
  3. 人脸对齐联合学习(joint face alignment learning)

下面详细介绍。

 

模型概述

多任务卷积神经网络(MTCNN)实现人脸检测与对齐是在一个网络里实现了人脸检测与五点标定的模型,主要是通过CNN模型级联实现了多任务学习网络。

整个模型分为三个阶段,第一阶段通过一个浅层的CNN网络快速产生一系列的候选窗口;第二阶段通过一个能力更强的CNN网络过滤掉绝大部分非人脸候选窗口;第三阶段通过一个能力更加强的网络找到人脸上面的五个标记点;

总体而言,MTCNN方法可以概括为:图像金字塔+3阶段级联CNN,如下图所示

mtcnn 关键点 mtcnn算法改进_多任务

 

对输入图像建立金字塔是为了检测不同尺度的人脸,通过级联CNN完成对人脸 由粗到细(coarse-to-fine) 的检测,所谓级联指的是 前者的输出是后者的输入,前者往往先使用少量信息做个大致的判断,快速将不是人脸的区域剔除,剩下可能包含人脸的区域交给后面更复杂的网络,利用更多信息进一步筛选,这种由粗到细的方式在保证召回率的情况下可以大大提高筛选效率。下面为MTCNN中级联的3个网络(P-Net、R-Net、O-Net),可以看到它们的网络层数逐渐加深,输入图像的尺寸(感受野)在逐渐变大12→24→48,最终输出的特征维数也在增加32→128→256,意味着利用的信息越来越多。

 

工作流程

mtcnn 关键点 mtcnn算法改进_人脸检测_02

首先,对原图通过双线性插值构建图像金字塔,可以参看前面的博文《人脸检测中,如何构建输入图像金字塔》。构建好金字塔后,将金字塔中的图像逐个输入给P-Net。

  • P-Net:其实是个全卷积神经网络(FCN),前向传播得到的特征图在每个位置是个32维的特征向量,用于判断每个位置处约12×1212×12大小的区域内是否包含人脸,如果包含人脸,则回归出人脸的Bounding Box,进一步获得Bounding Box对应到原图中的区域,通过NMS保留分数最高的Bounding box以及移除重叠区域过大的Bounding Box。(
  • 网络是全卷积神经网络是一个推荐网络简称 P-Net, 主要功能是获得脸部区域的窗口与边界Box回归,获得的脸部区域窗口会通过BB回归的结果进行校正,然后使用非最大压制(NMS)合并重叠窗口。)
  •  
  • R-Net:是单纯的卷积神经网络(CNN),先将P-Net认为可能包含人脸的Bounding Box 双线性插值到24×2424×24,输入给R-Net,判断是否包含人脸,如果包含人脸,也回归出Bounding Box,同样经过NMS过滤。(
  • 网络模型称为优化网络R-Net,大量过滤非人脸区域候选窗口,然后继续校正BB回归的结果,使用NMS进行合并。)
  •  
  • O-Net:也是纯粹的卷积神经网络(CNN),将R-Net认为可能包含人脸的Bounding Box 双线性插值到48×4848×48,输入给O-Net,进行人脸检测和关键点提取。( 网络模型称为O-Net,输入第二阶段数据进行更进一步的提取,最终输出人脸标定的5个点位置。)

需要注意的是:

  1. face classification判断是不是人脸使用的是softmax,因此输出是2维的,一个代表是人脸,一个代表不是人脸
  2. bounding box regression回归出的是bounding box左上角和右下角的偏移𝑑𝑥1,𝑑𝑦1,𝑑𝑥2,𝑑𝑦2dx1,dy1,dx2,dy2,因此是4维的
  3. facial landmark localization回归出的是左眼、右眼、鼻子、左嘴角、右嘴角共5个点的位置,因此是10维的
  4. 在训练阶段,3个网络都会将关键点位置作为监督信号来引导网络的学习, 但在预测阶段,P-Net和R-Net仅做人脸检测,不输出关键点位置(因为这时人脸检测都是不准的),关键点位置仅在O-Net中输出。
  5. Bounding box和关键点输出均为归一化后的相对坐标,Bounding Box是相对待检测区域(R-Net和O-Net是相对输入图像),归一化是相对坐标除以检测区域的宽高,关键点坐标是相对Bounding box的坐标,归一化是相对坐标除以Bounding box的宽高,这里先建立起初步的印象,具体可以参看后面准备训练数据部分和预测部分的代码细节。

MTCNN效果好的第1个原因是精心设计的级联CNN架构,其实,级联的思想早已有之,而使用级联CNN进行人脸检测的方法是在2015 CVPR《A convolutional neural network cascade for face detection》中被率先提出,MTCNN与之的差异在于:

  • 减少卷积核数量(层内)
  • 将5×55×5的卷积核替换为3×33×3
  • 增加网络深度

这样使网络的表达能力更强,同时运行时间更少。

 

 

多任务学习与在线困难样本挖掘

4种训练数据参与的训练任务如下:

  • Negatives和Positives用于训练face classification
  • Positives和Part faces用于训练bounding box regression
  • landmark faces用于训练facial landmark localization

据此来设置𝛽𝑗𝑖βij,对每一个样本看其属于那种训练数据,对其能参与的任务将𝛽β置为1,不参与的置为0。

至于在线困难样本挖掘,仅在训练face/non-face classification时使用,具体做法是:对每个mini-batch的数据先通过前向传播,挑选损失最大的前70%作为困难样本,在反向传播时仅使用这70%困难样本产生的损失。文中的实验表明,这样做在FDDB数据级上可以带来1.5个点的性能提升。

具体怎么实现的?这里以MTCNN-Tensorflow / train_models / mtcnn_model.py代码为例,用label来指示是哪种数据,下面为代码,重点关注valid_inds和loss(square_error)的计算(对应𝛽𝑗𝑖βij),以及cls_ohem中的困难样本挖掘。

# in mtcnn_model.py]
# pos=1, neg=0, part=-1, landmark=-2
# 通过cls_ohem, bbox_ohem, landmark_ohem来计算损失
num_keep_radio = 0.7 # mini-batch前70%做为困难样本

# face/non-face 损失,注意在线困难样本挖掘(前70%)
def cls_ohem(cls_prob, label):
    zeros = tf.zeros_like(label)
    #label=-1 --> label=0net_factory

    #pos -> 1, neg -> 0, others -> 0
    label_filter_invalid = tf.where(tf.less(label,0), zeros, label)
    num_cls_prob = tf.size(cls_prob)
    cls_prob_reshape = tf.reshape(cls_prob,[num_cls_prob,-1])
    label_int = tf.cast(label_filter_invalid,tf.int32)
    # get the number of rows of class_prob
    num_row = tf.to_int32(cls_prob.get_shape()[0])
    #row = [0,2,4.....]
    row = tf.range(num_row)*2
    indices_ = row + label_int
    label_prob = tf.squeeze(tf.gather(cls_prob_reshape, indices_))
    loss = -tf.log(label_prob+1e-10)
    zeros = tf.zeros_like(label_prob, dtype=tf.float32)
    ones = tf.ones_like(label_prob,dtype=tf.float32)
    # set pos and neg to be 1, rest to be 0
    valid_inds = tf.where(label < zeros,zeros,ones)
    # get the number of POS and NEG examples
    num_valid = tf.reduce_sum(valid_inds)

    ###### 困难样本数量 #####
    keep_num = tf.cast(num_valid*num_keep_radio,dtype=tf.int32)
    #FILTER OUT PART AND LANDMARK DATA
    loss = loss * valid_inds
    loss,_ = tf.nn.top_k(loss, k=keep_num) ##### 仅取困难样本反向传播 #####
    return tf.reduce_mean(loss)

# bounding box损失
#label=1 or label=-1 then do regression
def bbox_ohem(bbox_pred,bbox_target,label):
    '''

    :param bbox_pred:
    :param bbox_target:
    :param label: class label
    :return: mean euclidean loss for all the pos and part examples
    '''
    zeros_index = tf.zeros_like(label, dtype=tf.float32)
    ones_index = tf.ones_like(label,dtype=tf.float32)
    # keep pos and part examples
    valid_inds = tf.where(tf.equal(tf.abs(label), 1),ones_index,zeros_index)
    #(batch,)
    #calculate square sum
    square_error = tf.square(bbox_pred-bbox_target)
    square_error = tf.reduce_sum(square_error,axis=1)
    #keep_num scalar
    num_valid = tf.reduce_sum(valid_inds)
    #keep_num = tf.cast(num_valid*num_keep_radio,dtype=tf.int32)
    # count the number of pos and part examples
    keep_num = tf.cast(num_valid, dtype=tf.int32)
    #keep valid index square_error
    square_error = square_error*valid_inds
    # keep top k examples, k equals to the number of positive examples
    _, k_index = tf.nn.top_k(square_error, k=keep_num)
    square_error = tf.gather(square_error, k_index)

    return tf.reduce_mean(square_error)

# 关键点损失
def landmark_ohem(landmark_pred,landmark_target,label):
    '''
    :param landmark_pred:
    :param landmark_target:
    :param label:
    :return: mean euclidean loss
    '''
    #keep label =-2  then do landmark detection
    ones = tf.ones_like(label,dtype=tf.float32)
    zeros = tf.zeros_like(label,dtype=tf.float32)
    valid_inds = tf.where(tf.equal(label,-2),ones,zeros) ##### 将label=-2的置为1,其余为0 #####
    square_error = tf.square(landmark_pred-landmark_target)
    square_error = tf.reduce_sum(square_error,axis=1)
    num_valid = tf.reduce_sum(valid_inds)
    #keep_num = tf.cast(num_valid*num_keep_radio,dtype=tf.int32)
    keep_num = tf.cast(num_valid, dtype=tf.int32)
    square_error = square_error*valid_inds # 在计算landmark_ohem损失时只计算beta=1的 #####
    _, k_index = tf.nn.top_k(square_error, k=keep_num)
    square_error = tf.gather(square_error, k_index)
    return tf.reduce_mean(square_error)

 

多任务学习的代码片段如下:

# in train.py
if net == 'PNet':
    image_size = 12
    radio_cls_loss = 1.0;radio_bbox_loss = 0.5;radio_landmark_loss = 0.5;
elif net == 'RNet':
    image_size = 24
    radio_cls_loss = 1.0;radio_bbox_loss = 0.5;radio_landmark_loss = 0.5;
else:
    radio_cls_loss = 1.0;radio_bbox_loss = 0.5;radio_landmark_loss = 1;
    image_size = 48

# ...
# 多任务联合损失
total_loss_op  = radio_cls_loss*cls_loss_op + radio_bbox_loss*bbox_loss_op + radio_landmark_loss*landmark_loss_op + L2_loss_op
train_op, lr_op = train_model(base_lr, total_loss_op, num)

def train_model(base_lr, loss, data_num):
    """
    train model
    :param base_lr: base learning rate
    :param loss: loss
    :param data_num:
    :return:
    train_op, lr_op
    """
    lr_factor = 0.1
    global_step = tf.Variable(0, trainable=False)
    #LR_EPOCH [8,14]
    #boundaried [num_batch,num_batch]
    boundaries = [int(epoch * data_num / config.BATCH_SIZE) for epoch in config.LR_EPOCH]
    #lr_values[0.01,0.001,0.0001,0.00001]
    lr_values = [base_lr * (lr_factor ** x) for x in range(0, len(config.LR_EPOCH) + 1)]
    #control learning rate
    lr_op = tf.train.piecewise_constant(global_step, boundaries, lr_values)
    optimizer = tf.train.MomentumOptimizer(lr_op, 0.9)
    train_op = optimizer.minimize(loss, global_step)
    return train_op, lr_op

 

 

参考文献:

1.

2.如何应用MTCNN和FaceNet模型实现人脸检测及识别