MTCNN算法與代碼理解—人臉檢測和人臉對齊聯合學習


博客:blog.shinelee.me | 博客園 | CSDN

寫在前面

主頁https://kpzhang93.github.io/MTCNN_face_detection_alignment/index.html
論文https://arxiv.org/abs/1604.02878
代碼官方matlab版C++ caffe版
第三方訓練代碼tensorflowmxnet

MTCNN,恰如論文標題《Joint Face Detection and Alignment using Multi-task Cascaded Convolutional Networks》所言,采用級聯CNN結構,通過多任務學習,同時完成了兩個任務——人臉檢測和人臉對齊,輸出人臉的Bounding Box以及人臉的關鍵點(眼睛、鼻子、嘴)位置

MTCNN 又好又快,提出時在FDDBWIDER FACEAFLW數據集上取得了當時(2016年4月)最好的結果,速度又快,現在仍被廣泛使用作為人臉識別的前端,如InsightFacefacenet

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

  1. 精心設計的級聯CNN架構(carefully designed cascaded CNNs architecture)
  2. 在線困難樣本挖掘(online hard sample mining strategy)
  3. 人臉對齊聯合學習(joint face alignment learning)

下面詳細介紹。

算法Pipeline詳解

總體而言,MTCNN方法可以概括為:圖像金字塔+3階段級聯CNN,如下圖所示

MTCNN Pipeline

對輸入圖像建立金字塔是為了檢測不同尺度的人臉,通過級聯CNN完成對人臉 由粗到細(coarse-to-fine) 的檢測,所謂級聯指的是 前者的輸出是后者的輸入,前者往往先使用少量信息做個大致的判斷,快速將不是人臉的區域剔除,剩下可能包含人臉的區域交給后面更復雜的網絡,利用更多信息進一步篩選,這種由粗到細的方式在保證召回率的情況下可以大大提高篩選效率。下面為MTCNN中級聯的3個網絡(P-Net、R-Net、O-Net),可以看到它們的網絡層數逐漸加深輸入圖像的尺寸(感受野)在逐漸變大12→24→48最終輸出的特征維數也在增加32→128→256,意味着利用的信息越來越多。

the architectures of P-Net, R-Net, and O-Net

工作流程是怎樣的?
首先,對原圖通過雙線性插值構建圖像金字塔,可以參看前面的博文《人臉檢測中,如何構建輸入圖像金字塔》。構建好金字塔后,將金字塔中的圖像逐個輸入給P-Net。

  • P-Net:其實是個全卷積神經網絡(FCN),前向傳播得到的特征圖在每個位置是個32維的特征向量,用於判斷每個位置處約\(12\times12\)大小的區域內是否包含人臉,如果包含人臉,則回歸出人臉的Bounding Box,進一步獲得Bounding Box對應到原圖中的區域,通過NMS保留分數最高的Bounding box以及移除重疊區域過大的Bounding Box。
  • R-Net:是單純的卷積神經網絡(CNN),先將P-Net認為可能包含人臉的Bounding Box 雙線性插值\(24\times24\),輸入給R-Net,判斷是否包含人臉,如果包含人臉,也回歸出Bounding Box,同樣經過NMS過濾。
  • O-Net:也是純粹的卷積神經網絡(CNN),將R-Net認為可能包含人臉的Bounding Box 雙線性插值\(48\times 48\),輸入給O-Net,進行人臉檢測和關鍵點提取。

需要注意的是:

  1. face classification判斷是不是人臉使用的是softmax,因此輸出是2維的,一個代表是人臉,一個代表不是人臉
  2. bounding box regression回歸出的是bounding box左上角和右下角的偏移\(dx1, 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\times 5\)的卷積核替換為\(3\times 3\)
  • 增加網絡深度

這樣使網絡的表達能力更強,同時運行時間更少。

MTCNN效果好的后面2個原因在線困難樣本挖掘人臉對齊聯合學習將在下一節介紹。

如何訓練

損失函數

MTCNN的多任務學習有3個任務,1個分類2個回歸,分別為face classification、bounding box regression以及facial landmark localization,分類的損失函數使用交叉熵損失,回歸的損失函數使用歐氏距離損失,如下:

MTCNN loss function

對於第\(i\)個樣本,\(L_i^{det}\)為判斷是不是人臉的交叉熵損失,\(L_i^{box}\)為bounding box回歸的歐式距離損失,\(L_i^{landmark}\)為關鍵點定位的歐氏距離損失,任務間權重通過\(\alpha_j\)協調,配置如下:

Ft9d1O.png

同時,訓練數據中有含人臉的、有不含人臉的、有標注了關鍵點的、有沒標注關鍵點的,不同數據能參與的訓練任務不同,比如不含人臉的負樣本自然無法用於訓練bounding box回歸和關鍵點定位,於是有了\(\beta_i^j \in \{ 0, 1\}\)指示每個樣本能參與的訓練任務,例如對於不含人臉的負樣本其\(\beta_i^{det}=1, \beta_i^{box}=0, \beta_i^{landmark}=0\)

至此,我們已經清楚了MTCNN多任務學習的損失函數。

訓練數據准備

MTCNN准備了4種訓練數據:

  1. Negatives:與ground-truth faces的\(IOU < 0.3\)的圖像區域,lable = 0
  2. Positives:與ground-truth faces的\(IOU \ge 0.65\)的圖像區域,lable = 1
  3. Part faces:與ground-truth faces的\(0.4 \le IOU < 0.65\)的圖像區域,lable = -1
  4. Landmark faces:標記了5個關鍵點的人臉圖像,lable = -2

這4種數據是如何組織的呢?以MTCNN-Tensorflow為例:

Since MTCNN is a Multi-task Network,we should pay attention to the format of training data.The format is:
[path to image] [cls_label] [bbox_label] [landmark_label]
For neg sample, cls_label=0, bbox_label=[0,0,0,0], landmark_label=[0,0,0,0,0,0,0,0,0,0].
For pos sample, cls_label=1, bbox_label(calculate), landmark_label=[0,0,0,0,0,0,0,0,0,0].
For part sample, cls_label=-1, bbox_label(calculate), landmark_label=[0,0,0,0,0,0,0,0,0,0].
For landmark sample, cls_label=-2, bbox_label=[0,0,0,0], landmark_label(calculate).

數量之比依次為\(3:1:1:2\),其中,Negatives、Positives和Part faces通過WIDER FACE數據集crop得到,landmark faces通過CelebA數據集crop得到,先crop區域,然后看這個區域與哪個ground-truth face的IOU最大,根據最大IOU來生成label,比如小於0.3的標記為negative。

P-Net訓練數據的准備可以參見gen_12net_data.py、gen_landmark_aug_12.py、gen_imglist_pnet.py和gen_PNet_tfrecords.py,代碼很直觀,這里略過crop過程,重點介紹bounding box label和landmark label的生成。下面是gen_12net_data.py和gen_landmark_aug_12.py中的代碼片段,bounding box 和 landmark 的label為歸一化后的相對坐標offset_x1, offset_y1, offset_x2, offset_y2為bounding box的label,使用crop區域的size進行歸一化rv為landmark的label,使用bbox的寬高進行歸一化,注意兩者的歸一化是不一樣的,具體見代碼:

## in gen_12net_data.py
# pos and part face size [minsize*0.8,maxsize*1.25]
size = npr.randint(int(min(w, h) * 0.8), np.ceil(1.25 * max(w, h)))

# delta here is the offset of box center
if w<5:
    print (w)
    continue
#print (box)
delta_x = npr.randint(-w * 0.2, w * 0.2)
delta_y = npr.randint(-h * 0.2, h * 0.2)

#show this way: nx1 = max(x1+w/2-size/2+delta_x)
# x1+ w/2 is the central point, then add offset , then deduct size/2
# deduct size/2 to make sure that the right bottom corner will be out of
nx1 = int(max(x1 + w / 2 + delta_x - size / 2, 0))
#show this way: ny1 = max(y1+h/2-size/2+delta_y)
ny1 = int(max(y1 + h / 2 + delta_y - size / 2, 0))
nx2 = nx1 + size
ny2 = ny1 + size

if nx2 > width or ny2 > height:
    continue 
crop_box = np.array([nx1, ny1, nx2, ny2])
#yu gt de offset
##### x1 y1 x2 y2 為 ground truth bbox, nx1 ny1 nx2 ny2為crop的區域,size為crop的區域size ######
offset_x1 = (x1 - nx1) / float(size) 
offset_y1 = (y1 - ny1) / float(size)
offset_x2 = (x2 - nx2) / float(size)
offset_y2 = (y2 - ny2) / float(size)
#crop
cropped_im = img[ny1 : ny2, nx1 : nx2, :]
#resize
resized_im = cv2.resize(cropped_im, (12, 12), interpolation=cv2.INTER_LINEAR)
##########################################################################

## in gen_landmark_aug_12.py
#normalize land mark by dividing the width and height of the ground truth bounding box
# landmakrGt is a list of tuples
for index, one in enumerate(landmarkGt):
    # (( x - bbox.left)/ width of bounding box, (y - bbox.top)/ height of bounding box
    rv = ((one[0]-gt_box[0])/(gt_box[2]-gt_box[0]), (one[1]-gt_box[1])/(gt_box[3]-gt_box[1]))
    # put the normalized value into the new list landmark
    landmark[index] = rv

需要注意的是,對於P-Net,其為FCN,預測階段輸入圖像可以為任意大小,但在訓練階段,使用的訓練數據均被resize到\(12\times 12\),以便於控制正負樣本的比例(避免數據不平衡)。

因為是級聯結構訓練要分階段依次進行,訓練好P-Net后,用P-Net產生的候選區域來訓練R-Net,訓練好R-Net后,再生成訓練數據來訓練O-Net。P-Net訓練好之后,根據其結果准備R-Net的訓練數據,R-Net訓練好之后,再准備O-Net的訓練數據,過程是類似的,具體可以參見相關代碼,這里就不贅述了。

多任務學習與在線困難樣本挖掘

4種訓練數據參與的訓練任務如下:

  • Negatives和Positives用於訓練face classification
  • Positives和Part faces用於訓練bounding box regression
  • landmark faces用於訓練facial landmark localization

據此來設置\(\beta_i^j\),對每一個樣本看其屬於那種訓練數據,對其能參與的任務將\(\beta\)置為1,不參與的置為0。

至於在線困難樣本挖掘,僅在訓練face/non-face classification時使用,具體做法是:對每個mini-batch的數據先通過前向傳播,挑選損失最大的前70%作為困難樣本,在反向傳播時僅使用這70%困難樣本產生的損失。文中的實驗表明,這樣做在FDDB數據級上可以帶來1.5個點的性能提升。

具體怎么實現的?這里以MTCNN-Tensorflow / train_models / mtcnn_model.py代碼為例,用label來指示是哪種數據,下面為代碼,重點關注valid_indslosssquare_error)的計算(對應\(\beta_i^j\)),以及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

以上對應論文中的損失函數。

預測過程

預測過程與算法Pipeline詳解一節講述的一致,直接看一下官方matlab代碼,這里重點關注P-Net FCN是如何獲得Bounding box的,以及O-Net最終是如何得到landmark的,其余部分省略。

將圖像金字塔中的每張圖像輸入給P-Net,若當前輸入圖像尺寸為\(M\times N\),在bounding box regression分支上將得到一個3維張量\(m\times n \times 4\),共有\(m\times n\)個位置,每個位置對應輸入圖像中一個\(12\times 12\)的區域,而輸入圖像相對原圖的尺度為scale,進一步可以獲得每個位置對應到原圖中的區域范圍,如下所示:

MTCNN generateBoundingBox

而每個位置處都有個\(4\)維的向量,其為bounding box左上角和右下角的偏移dx1, dy1, dx2, dy2,通過上面的訓練過程,我們知道它們是歸一化之后的相對坐標,通過對應的區域以及歸一化后的相對坐標就可以獲得原圖上的bounding box,如下所示,dx1, dy1, dx2, dy2為歸一化的相對坐標,求到原圖中的bounding box坐標的過程為生成訓練數據bounding box label的逆過程。
FNPOqe.png

landmark位置通過O-Net輸出得到,將人臉候選框resize到\(48\times 48\)輸入給O-Net,先獲得bounding box(同上),因為O-Net輸出的landmark也是歸一化后的相對坐標,通過bounding box的長寬和bounding box左上角求取landmark 在原圖中的位置,如下所示:

FNFdts.png

至此,預測過程中的要點也介紹完畢了,以上。

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM