参考代码:https://github.com/bubbliiiing/mtcnn-keras
参考视频:https://www.bilibili.com/video/BV1fJ411C7AJ/?spm_id_from=333.788.recommend_more_video.1
参考博客:https://blog.csdn.net/qq_36782182/article/details/83624357
https://zhuanlan.zhihu.com/p/58825924
MTCNN工作原理
MTCNN是什么
MTCNN,Multi-task convolutional neural network(多任务卷积神经网络),将人脸区域检测与人脸关键点检测放在了一起,它的主题框架类似于cascade。总体可分为P-Net、R-Net、和O-Net三层网络结构。
它是2016年中国科学院深圳研究院提出的用于人脸检测任务的多任务神经网络模型,该模型主要采用了三个级联的网络,采用候选框加分类器的思想,进行快速高效的人脸检测。这三个级联的网络分别是快速生成候选窗口的P-Net、进行高精度候选窗口过滤选择的R-Net和生成最终边界框与人脸关键点的O-Net。和很多处理图像问题的卷积神经网络模型,该模型也用到了图像金字塔、边框回归、非最大值抑制等技术。
MTCNN实现流程
构建图像金字塔
首先将图像进行不同尺度的变换,构建图像金字塔,以适应不同大小的人脸的进行检测(应对测试的图片)。
def calculateScales(img):
pr_scale = 1.0
h,w,_ = img.shape
#--------------------------------------------#
# 将最大的图像大小进行一个固定
# 如果图像的短边大于500,则将短边固定为500
# 如果图像的长边小于500,则将长边固定为500
#--------------------------------------------#
if min(w,h)>500:
pr_scale = 500.0/min(h,w)
w = int(w*pr_scale)
h = int(h*pr_scale)
elif max(w,h)<500:
pr_scale = 500.0/max(h,w)
w = int(w*pr_scale)
h = int(h*pr_scale)
#------------------------------------------------#
# 建立图像金字塔的scales,防止图像的宽高小于12
#------------------------------------------------#
scales = []
factor = 0.709
factor_count = 0
minl = min(h,w)
while minl >= 12:
scales.append(pr_scale*pow(factor, factor_count))
minl *= factor
factor_count += 1
return scales
#500/719
# [0.6954102920723226, 0.4930458970792767, 0.3495695410292072, 0.24784480458970787, 0.17572196645410287, 0.12458687421595895, 0.08833209381911489, 0.06262745451775245, 0.044402865253086475, 0.03148163146443831, 0.022320476708286765]
P-Net
全称为Proposal Network,其基本的构造是一个全卷积网络。对上一步构建完成的图像金字塔,通过一个FCN进行初步特征提取与标定边框,并进行Bounding-Box Regression调整窗口与NMS进行大部分窗口的过滤。
P-Net是一个人脸区域的区域建议网络,该网络的将特征输入结果三个卷积层之后,通过一个人脸分类器判断该区域是否是人脸,同时使用边框回归和一个面部关键点的定位器来进行人脸区域的初步提议,该部分最终将输出很多张可能存在人脸的人脸区域,并将这些区域输入R-Net进行进一步处理。
这一部分的基本思想是使用较为浅层、较为简单的CNN快速生成人脸候选窗口。
# -----------------------------#
# 粗略获取人脸框
# 输出bbox位置和是否有人脸
# -----------------------------#
def create_Pnet(weight_path):
# h,w,3
inputs = Input(shape=[None, None, 3])
# h,w,3->h/2,2/w,10
x = Conv2D(10, (3, 3), strides=1, padding='valid', name='conv1')(inputs)
x = PReLU(shared_axes=[1, 2], name='PReLU1')(x)
x = keras.layers.MaxPool2D(pool_size=2)(x)
# h/2,2/w,10->h/2,2/w,16
x = Conv2D(16, (3, 3), strides=1, padding='valid', name='conv2')(x)
x = PReLU(shared_axes=[1, 2], name='PReLU2')(x)
# h/2,2/w,16->h/2,2/w,32
x = Conv2D(32, (3, 3), strides=1, padding='valid', name='conv3')(x)
x = PReLU(shared_axes=[1, 2], name='PReLU3')(x)
# h/2,2/w,2
classifier = Conv2D(2, (1, 1), activation='softmax', name='conv4-1')(x)
# 无激活函数,线性。
# h/2,2/w,4
bbox_regress = Conv2D(4, (1, 1), name='conv4-2')(x)
model = Model([inputs], [classifier, bbox_regress])
model.load_weights(weight_path, by_name=True)
return model
#-------------------------------------#
# 对pnet处理后的结果进行处理
# 为了方便理解,我将代码进行了重构
# 具体代码与视频有较大区别
#-------------------------------------#
def detect_face_12net(cls_prob,roi,out_side,scale,width,height,threshold):
#-------------------------------------#
# 计算特征点之间的步长
#-------------------------------------#
# stride代表压缩比例 原图500 890 经过Pnet 后变为(245, 440)
stride = 0
if out_side != 1:
stride = float(2*out_side-1)/(out_side-1)
#-------------------------------------#
# 获得满足得分门限的特征点的坐标
#-------------------------------------#
(y,x) = np.where(cls_prob >= threshold)
#-----------------------------------------#
# 获得满足得分门限的特征点得分
# 最终获得的score的shape为:[num_box, 1]
#-------------------------------------------#
score = np.expand_dims(cls_prob[y, x], -1)
#-------------------------------------------------------#
# 将对应的特征点的坐标转换成位于原图上的先验框的坐标
# 利用回归网络的预测结果对先验框的左上角与右下角进行调整
# 获得对应的粗略预测框
# 最终获得的boundingbox的shape为:[num_box, 4]
#-------------------------------------------------------#
boundingbox = np.concatenate([np.expand_dims(x, -1), np.expand_dims(y, -1)], axis = -1)
top_left = np.fix(stride * boundingbox + 0)
bottom_right = np.fix(stride * boundingbox + 11)
boundingbox = np.concatenate((top_left,bottom_right), axis = -1)
# 找到框的位置(从pnet->scale后的图像转换)
# 框的位置加上点的位置,再乘上scale获得到原图的位置
boundingbox = (boundingbox + roi[y, x] * 12.0) * scale
#-------------------------------------------------------#
# 将预测框和得分进行堆叠,并转换成正方形
# 最终获得的rectangles的shape为:[num_box, 5]
#-------------------------------------------------------#
rectangles = np.concatenate((boundingbox, score), axis = -1)
rectangles = rect2square(rectangles)
rectangles[:, [1,3]] = np.clip(rectangles[:, [1,3]], 0, height)
rectangles[:, [0,2]] = np.clip(rectangles[:, [0,2]], 0, width)
return rectangles
R-Net
全称为Refine Network,其基本的构造是一个卷积神经网络,相对于第一层的P-Net来说,增加了一个全连接层,因此对于输入数据的筛选会更加严格。在图片经过P-Net后,会留下许多预测窗口,我们将所有的预测窗口送入R-Net,这个网络会滤除大量效果比较差的候选框,最后对选定的候选框进行Bounding-Box Regression和NMS进一步优化预测结果。
因为P-Net的输出只是具有一定可信度的可能的人脸区域,在这个网络中,将对输入进行细化选择,并且舍去大部分的错误输入,并再次使用边框回归和面部关键点定位器进行人脸区域的边框回归和关键点定位,最后将输出较为可信的人脸区域,供O-Net使用。对比与P-Net使用全卷积输出的1x1x32的特征,R-Net使用在最后一个卷积层之后使用了一个128的全连接层,保留了更多的图像特征,准确度性能也优于P-Net。
R-Net的思想是使用一个相对于P-Net更复杂的网络结构来对P-Net生成的可能是人脸区域区域窗口进行进一步选择和调整,从而达到高精度过滤和人脸区域优化的效果。
# -----------------------------#
# mtcnn的第二段
# 精修框
# -----------------------------#
def create_Rnet(weight_path):
inputs = Input(shape=[24, 24, 3])
# 24,24,3 -> 22,22,28 -> 11,11,28
x = Conv2D(28, (3, 3), strides=1, padding='valid', name='conv1')(inputs)
x = PReLU(shared_axes=[1, 2], name='prelu1')(x)
x = keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')(x)
# 11,11,28 -> 9,9,48 -> 4,4,48
x = Conv2D(48, (3, 3), strides=1, padding='valid', name='conv2')(x)
x = PReLU(shared_axes=[1, 2], name='prelu2')(x)
x = keras.layers.MaxPool2D(pool_size=3, strides=2)(x)
# 4,4,48 -> 3,3,64
x = Conv2D(64, (2, 2), strides=1, padding='valid', name='conv3')(x)
x = PReLU(shared_axes=[1, 2], name='prelu3')(x)
# 3,3,64 -> 64,3,3
x = Permute((3, 2, 1))(x)
x = Flatten()(x)
# 576 -> 128
x = Dense(128, name='conv4')(x)
x = PReLU(name='prelu4')(x)
# 128 -> 2
classifier = Dense(2, activation='softmax', name='conv5-1')(x)
# 128 -> 4
bbox_regress = Dense(4, name='conv5-2')(x)
model = Model([inputs], [classifier, bbox_regress])
model.load_weights(weight_path, by_name=True)
# 得到每个框的修正值
return model
#-------------------------------------#
# 对Rnet处理后的结果进行处理
# 为了方便理解,我将代码进行了重构
# 具体代码与视频有较大区别
#-------------------------------------#
def filter_face_24net(cls_prob, roi, rectangles, width, height, threshold):
#-------------------------------------#
# 利用得分进行筛选
#-------------------------------------#
pick = cls_prob[:, 1] >= threshold
score = cls_prob[pick, 1:2]
# 原始的人脸框
rectangles = rectangles[pick, :4]
# 对原始人脸框的调整
roi = roi[pick, :]
#-------------------------------------------------------#
# 利用Rnet网络的预测结果对粗略预测框进行调整
# 最终获得的rectangles的shape为:[num_box, 4]
#-------------------------------------------------------#
# x2 -x1
w = np.expand_dims(rectangles[:, 2] - rectangles[:, 0], -1)
# y2-y1
h = np.expand_dims(rectangles[:, 3] - rectangles[:, 1], -1)
rectangles[:, [0,2]] = rectangles[:, [0,2]] + roi[:, [0,2]] * w
rectangles[:, [1,3]] = rectangles[:, [1,3]] + roi[:, [1,3]] * h
#-------------------------------------------------------#
# 将预测框和得分进行堆叠,并转换成正方形
# 最终获得的rectangles的shape为:[num_box, 5]
#-------------------------------------------------------#
rectangles = np.concatenate((rectangles,score), axis=-1)
rectangles = rect2square(rectangles)
rectangles[:, [1,3]] = np.clip(rectangles[:, [1,3]], 0, height)
rectangles[:, [0,2]] = np.clip(rectangles[:, [0,2]], 0, width)
return np.array(NMS(rectangles, 0.7))
O-Net
全称为Output Network,基本结构是一个较为复杂的卷积神经网络,相对于R-Net来说多了一个卷积层。O-Net的效果与R-Net的区别在于这一层结构会通过更多的监督来识别面部的区域,而且会对人的面部特征点进行回归,最终输出五个人脸面部特征点。
是一个更复杂的卷积网络,该网络的输入特征更多,在网络结构的最后同样是一个更大的256的全连接层,保留了更多的图像特征,同时再进行人脸判别、人脸区域边框回归和人脸特征定位,最终输出人脸区域的左上角坐标和右下角坐标与人脸区域的五个特征点。O-Net拥有特征更多的输入和更复杂的网络结构,也具有更好的性能,这一层的输出作为最终的网络模型输出。
O-Net的思想和R-Net类似,使用更复杂的网络对模型性能进行优化
# -----------------------------#
# mtcnn的第三段
# 精修框并获得五个点
# -----------------------------#
def create_Onet(weight_path):
inputs = Input(shape=[48, 48, 3])
# 48,48,3 -> 46,46,32 -> 23,23,32
x = Conv2D(32, (3, 3), strides=1, padding='valid', name='conv1')(inputs)
x = PReLU(shared_axes=[1, 2], name='prelu1')(x)
x = keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')(x)
# 23,23,32 -> 21,21,64 -> 10,10,64
x = Conv2D(64, (3, 3), strides=1, padding='valid', name='conv2')(x)
x = PReLU(shared_axes=[1, 2], name='prelu2')(x)
x = keras.layers.MaxPool2D(pool_size=3, strides=2)(x)
# 8,8,64 -> 4,4,64
x = Conv2D(64, (3, 3), strides=1, padding='valid', name='conv3')(x)
x = PReLU(shared_axes=[1, 2], name='prelu3')(x)
x = keras.layers.MaxPool2D(pool_size=2)(x)
# 4,4,64 -> 3,3,128
x = Conv2D(128, (2, 2), strides=1, padding='valid', name='conv4')(x)
x = PReLU(shared_axes=[1, 2], name='prelu4')(x)
# 3,3,128 -> 128,12,12
x = Permute((3, 2, 1))(x)
x = Flatten()(x)
# 1152 -> 256
x = Dense(256, name='conv5')(x)
x = PReLU(name='prelu5')(x)
# 256 -> 2
classifier = Dense(2, activation='softmax', name='conv6-1')(x)
# 256 -> 4
bbox_regress = Dense(4, name='conv6-2')(x)
# 256 -> 10
landmark_regress = Dense(10, name='conv6-3')(x)
model = Model([inputs], [classifier, bbox_regress, landmark_regress])
model.load_weights(weight_path, by_name=True)
# 进一步修正框,返回五个人脸特征点(锚点)的坐标
return model
#-------------------------------------#
# 对onet处理后的结果进行处理
# 为了方便理解,我将代码进行了重构
# 具体代码与视频有较大区别
#-------------------------------------#
def filter_face_48net(cls_prob, roi, pts, rectangles, width, height, threshold):
#-------------------------------------#
# 利用得分进行筛选
#-------------------------------------#
pick = cls_prob[:, 1] >= threshold
score = cls_prob[pick, 1:2]
rectangles = rectangles[pick, :4]
pts = pts[pick, :]
roi = roi[pick, :]
w = np.expand_dims(rectangles[:, 2] - rectangles[:, 0], -1)
h = np.expand_dims(rectangles[:, 3] - rectangles[:, 1], -1)
#-------------------------------------------------------#
# 利用Onet网络的预测结果对预测框进行调整
# 通过解码获得人脸关键点与预测框的坐标
# 最终获得的face_marks的shape为:[num_box, 10]
# 最终获得的rectangles的shape为:[num_box, 4]
#-------------------------------------------------------#
face_marks = np.zeros_like(pts)
face_marks[:, [0,2,4,6,8]] = w * pts[:, [0,1,2,3,4]] + rectangles[:, 0:1]
face_marks[:, [1,3,5,7,9]] = h * pts[:, [5,6,7,8,9]] + rectangles[:, 1:2]
rectangles[:, [0,2]] = rectangles[:, [0,2]] + roi[:, [0,2]] * w
rectangles[:, [1,3]] = rectangles[:, [1,3]] + roi[:, [1,3]] * h
#-------------------------------------------------------#
# 将预测框和得分进行堆叠
# 最终获得的rectangles的shape为:[num_box, 15]
#-------------------------------------------------------#
rectangles = np.concatenate((rectangles,score,face_marks),axis=-1)
rectangles[:, [1,3]] = np.clip(rectangles[:, [1,3]], 0, height)
rectangles[:, [0,2]] = np.clip(rectangles[:, [0,2]], 0, width)
return np.array(NMS(rectangles,0.3))
集成架构及系统思想
MTCNN为了兼顾性能和准确率,避免滑动窗口加分类器等传统思路带来的巨大的性能消耗,先使用小模型生成有一定可能性的目标区域候选框,然后在使用更复杂的模型进行细分类和更高精度的区域框回归,并且让这一步递归执行,以此思想构成三层网络,分别为P-Net、R-Net、O-Net,实现快速高效的人脸检测。在输入层使用图像金字塔进行初始图像的尺度变换,并使用P-Net生成大量的候选目标区域框,之后使用R-Net对这些目标区域框进行第一次精选和边框回归,排除大部分的负例,然后再用更复杂的、精度更高的网络O-Net对剩余的目标区域框进行判别和区域边框回归。
下面是整个系统的工作流图
技术细节
FCN(全卷积网络)
全卷积网络就是去除了传统卷积网络的全连接层,然后对其进行反卷积对最后一个卷积层(或者其他合适的卷积层)的feature map进行上采样,使其恢复到原有图像的尺寸(或者其他),并对反卷积图像的每个像素点都可以进行一个类别的预测,同时保留了原有图像的空间信息。 同时,在反卷积对图像进行操作的过程中,也可以通过提取其他卷积层的反卷积结果对最终图像进行预测,合适的选择会使得结果更好、更精细。
IoU
对于某个图像的子目标图像和对这个子目标图像进行标定的预测框,把最终标定的预测框与真实子图像的自然框(通常需要人工标定)的某种相关性叫做IOU(Intersection over Union),经常使用的标准为两个框的交叉面积与合并面积之和。
Bounding-Box regression:
解决的问题: 当IOU小于某个值时,一种做法是直接将其对应的预测结果丢弃,而Bounding-Box regression的目的是对此预测窗口进行微调,使其接近真实值。 具体逻辑 在图像检测里面,子窗口一般使用四维向量(x,y,w,h)表示,代表着子窗口中心所对应的母图像坐标与自身宽高,目标是在前一步预测窗口对于真实窗口偏差过大的情况下,使得预测窗口经过某种变换得到更接近与真实值的窗口。 在实际使用之中,变换的输入输出按照具体算法给出的已经经过变换的结果和最终适合的结果的变换,可以理解为一个损失函数的线性回归。
NMS(非极大值抑制)
顾名思义,非极大值抑制就是抑制不是极大值的元素。在目标检测领域里面,可以使用该方法快速去掉重合度很高且标定相对不准确的预测框,但是这种方法对于重合的目标检测不友好。
Soft-NMS
对于优化重合目标检测的一种改进方法。核心在于在进行NMS的时候不直接删除被抑制的对象,而是降低其置信度。处理之后在最后统一一个置信度进行统一删除。
PRelu
在MTCNN中,卷积网络采用的激活函数是PRelu,带有参数的带有参数的Relu,相对于Relu滤除负值的做法,PRule对负值进行了添加参数而不是直接滤除,这种做法会给算法带来更多的计算量和更多的过拟合的可能性,但是由于保留了更多的信息,也可能是训练结果拟合性能更好。
总结流程
总结下MTCNN的流程:图片经过Pnet,会得到feature map,通过分类、NMS筛选掉大部分假的候选;然后剩余候选去原图crop图片输入Rnet,再对Rnet的输出筛选掉False、NMS去掉众多的候选;剩余候选再去原图crop出图片再输入到Onet,这个时候就能够输出准确的bbox、landmark坐标了。