1、mtcnn
MTCNN,英文全称是Multi-task convolutional neural network,中文全称是多任务卷积神经网络,该神经网络将人脸区域检测与人脸关键点检测放在了一起。总体可分为P-Net、R-Net、和O-Net三层网络结构。
2、实现流程
1、构建图像金字塔
为了检测到不同尺寸大小的人脸,在进入P-Net之前,我们要首先对图像进行金字塔操作。根据设定的min_face_size尺寸,将图像按照一定的尺寸缩小,每次将图像缩小到前级图像面积的一半,形成scales列表,直至边长小于min_face_size,此时可以得到一组不同尺寸的输入图像。
实现代码如下,当一个图片输入的时候,会缩放为不同大小的图片,但是缩小后的长宽最小不可以小于12:
def calculateScales(img):
copy_img = img.copy()
pr_scale = 1.0
h,w,_ = copy_img.shape
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 = []
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
1、Pnet输入图像金子塔
2、输入12×12固定。 将原图随机剪裁(不是缩放)成不同尺度,random(12,min(height,width)/2),在resize到12×12,也就是固定为12×12,但是输入图像是随机大小。
- 个人感觉,如果针对迁移学习,则可用1,直接输入图像金字塔
- 如果从头训练自己的网络,则根据方式2,PNet的训练数据主要由4部分组成,包括正label数据(IOU>0.65,面部轮廓特值为0),负label数据(IOU<0.4,面部轮廓特值为0,回归框值为0),中间数据(0.4<IOU<0.65,面部轮廓特值为0),面部轮廓数据(回归框值为0)。
疑问:为什么要做图像金字塔?
由于原始图像中,存在大小不同的脸,为了在统一尺度下检测人脸,进入网络训练之前,就要将原始图片缩放至不同尺度。以增强网络对不同尺寸大小人脸的鲁棒性。
2、Pnet(Proposal Network)
Pnet的全称为Proposal Network,其基本的构造是一个全卷积网络。对上一步构建完成的图像金字塔,通过一个FCN进行初步特征提取与标定边框.
由于PNet的输入大小是12×12,因此单纯的PNet只能检测12×12大小的图像中的人脸。这显然是不实际的。为了让PNet能够检测多尺度范围的人脸,有必要对原图像进行缩放。这就是引入图像金字塔的原因。
Pnet的网络比较简单,实现代码如下
def create_Pnet(weight_path):
input = Input(shape=[None, None, 3])
x = Conv2D(10, (3, 3), strides=1, padding='valid', name='conv1')(input)
x = PReLU(shared_axes=[1,2],name='PReLU1')(x)
x = MaxPool2D(pool_size=2)(x)
x = Conv2D(16, (3, 3), strides=1, padding='valid', name='conv2')(x)
x = PReLU(shared_axes=[1,2],name='PReLU2')(x)
x = Conv2D(32, (3, 3), strides=1, padding='valid', name='conv3')(x)
x = PReLU(shared_axes=[1,2],name='PReLU3')(x)
classifier = Conv2D(2, (1, 1), activation='softmax', name='conv4-1')(x)
# 无激活函数,线性。
bbox_regress = Conv2D(4, (1, 1), name='conv4-2')(x)
model = Model([input], [classifier, bbox_regress])
model.load_weights(weight_path, by_name=True)
return model
将图像缩放完毕后,就可以进行计算了。对于 x * y 的输入,将产生大小为[(x-10)/2]*[(y-10)/2]的输出.
在mtcnn算法中的Pnet是为了得到一批人脸框。过程如下
针对金字塔中每张图,网络forward计算后都得到了人脸得分以及人脸框回归的结果。人脸分类得分是两个通道的三维矩阵m* m* 2,其实对应在网络输入图片上m* m个12* 12的滑框,结合当前图片在金字塔图片中的缩放scale,可以推算出每个滑框在原始图像中的具体坐标。
首先要根据得分进行筛选,得分低于阈值的滑框,排除。
当金字塔中所有图片处理完后,再利用nms对汇总的滑框进行合并,然后利用最后剩余的滑框对应的Bbox结果转换成原始图像中像素坐标,也就是得到了人脸框的坐标。
Pnet最终能够得到了一批人脸框
输入是一个1212大小的图片,所以训练前需要把生成的训练数据(通过生成bounding box,然后把该bounding box 剪切成1212大小的图片),转换成12123的结构。
- 通过10个333的卷积核,22的Max Pooling(stride=2)操作,生成10个55的特征图
- 通过16个3310的卷积核,生成16个3*3的特征图
- 通过32个3316的卷积核,生成32个1*1的特征图。
- 针对32个11的特征图,可以通过2个1132的卷积核,生成2个11的
- 征图用于分类;4个1132的卷积核,生成4个1*1的特征图用于回归框判断。
Pnet有两个输出,classifier用于判断这个网格点上的框的可信度,bbox_regress表示框的位置。
bbox_regress并不代表这个框在图片上的真实位置,如果需要将bbox_regress映射到真实图像上,还需要进行一次解码过程。
注意:
没有将网络输出做全连接层展开,而是直接将卷积层结果作为输出。原因是在应用端,每一个输入图像都是尺度不一的。如果增加全连接层,当尺度不一的图像输入pnet时,全连接层的特征向量也会不等长。而利用卷积层能将不同size的输入图像最终输出为同一尺寸的特征图(1x1x?)
代码中,激活函数为PReLU(Parametric Rectified Linear Unit),其为ReLU的改进版,即带了参数的ReLU,该参数a会随着数据变化,而当a为定值时,则变身为Leaky ReLU。
解码过程利用detect_face_12net函数实现,其实现步骤如下(需要配合代码理解):
1、判断哪些网格点的置信度较高,即该网格点内存在人脸。
2、记录该网格点的x,y轴。
3、利用函数计算bb1和bb2,分别代表图中框的左上角基点和右下角基点,二者之间差了11个像素,堆叠得到boundingbox 。
4、利用bbox_regress计算解码结果,解码公式为boundingbox = boundingbox + offset12.0scale。
虽然网络定义的时候input的size是12 * 12* 3,由于Pnet只有卷积层,我们可以直接将resize后的图像给网络进行前传,只是得到的结果不是1* 1* 2和1* 1* 4,而是m* m* 2和m* m* 4。这样就不用先从resize的图上滑动截取各种12* 12* 3的图进入网络,而是一次性送入通过卷积,在根据结果回推每个结果对应的12* 12的图在输入图的什么位置。利用的就是卷积来代替原来的滑动窗口。
然后利用nms非极大值抑制,对剩下的滑框进行合并
简单理解就是Pnet的输出就是将整个网格分割成若干个网格点,;然后每个网格点初始状态下是一个11x11的框,这个由第三步得到;之后bbox_regress代表 每个网格点确定的框 的 左上角基点和右下角基点 的偏移情况。
#-------------------------------------#
# 对pnet处理后的结果进行处理
#-------------------------------------#
def detect_face_12net(cls_prob,roi,out_side,scale,width,height,threshold):
cls_prob = np.swapaxes(cls_prob, 0, 1)
roi = np.swapaxes(roi, 0, 2)
stride = 0
# stride略等于2,由于池化
if out_side != 1:
stride = float(2*out_side-1)