简介:
人脸检测不同于别的目标检测算法,其实它就是一个二分类问题。如果仅仅从工程角度来讲,用官方或者其他人训练的结果即可。因此在这里只介绍其推理过程,而不再介绍训练过程,训练步骤和数据集的制作可以参考该文章。根据工程目标可以调整一下内部参数来提高其中的检测速度或者最小检测范围。在人脸检测算法中,MTCNN是已知的开源人脸检测算法中相对优秀的算法。该算法主要由3个stage对人脸进行从初略到精确的方式定位:Proposal Network(P-Net) Refine Network(R-Net)和Output Network(O-Net)。可以把他们当成三个子网络对待,最后两个网络(R-Net和O-Net)其结构形式几乎相同,区别就是参数的设置。
网络结构:
首先,对整个网络做一个大体的梳理。
首先对输入的一张原图进行resize,即缩放。该缩放规则是金字塔型缩放,即每次在上次缩放的基础上在进行0.709倍数的缩放,每一次缩放后把图片进行存储,直到图像的最小尺寸为20(可以灵活设置)。当然该尺寸可以自定义。由此也可以看出,原图越大,缩放的次数也越大,存储的图片也越多。.论文中给出的三个子网络图为:
P-Net,图中的input size表述的是训练过程的固定输入尺寸,在推理过程其输入为原图以及原图进行缩放后的所有图片,最小的图片才为20。因此图中的表示是缩放的最小的图片的流程图。其经过三次卷积(其padding形式为VALDI)和一次stride为2的池化后为1*1*2,1*1*4,1*1*10是最小图片的结果,其他的为n*n*2,n*n*4以及n*n*10。n的值根据不同缩放后不同尺寸的图片取值是不同的。另外需要注意一点的是n*n*10的结果在推理过程是不需要的,同样在R-Net中也是不需要的。图像对齐的功能只在最后一个子网络O-Net作为输出用到。因为输入图片的尺寸不同,无法用全连接层,由此也可以看出P-Net是全卷积网络。最后输出的n*n*2的2表示的是该位置是脸还是非脸的概率。n*n*4为该位置的坐标点偏移量。
R-Net,这里的input-size为24*24是确定值,网络的最后一层还是全连接层。那么24*24是如何得来的呢?这是根据P-Net中的n*n*4中的4表示的坐标,从原图上抠下来的图。扣下来的图不一定是这个尺寸,因此也进行了resize,并统一resize为24*24的尺寸。在输出的facial landmark lacalization在推理过程中同样不需要。
O-Net和R-Net很相似,也是根据R-Net上表示坐标的输出点从原图进行抠图,统一resize成48*48的图片后送入网络。最后得出相应位置为人脸的概率、坐标以及双眼,鼻子,左右嘴角的点的坐标偏移量,从而实现人脸检测和人脸对齐。
TensorFlow的实现过程:
下面结合TensorFlow程序来分别介绍三个模块的具体实现过程。从上面的流程图也可以看出,经过卷积、池化以及全连接层得到每个子流程图的结果,其结果代表的意义也介绍完毕。但是这里需要补充的是三个stage的后处理情况。有很多源代码给出的是基于numpy的后处理情况。但是在集成pb文件后,numpy框架内的方式是集成不到pb文件内的。首先看P-Net的具体实现过程:
def stage_one(images, min_size, factor, thresold, scope):
img_shape = tf.shape(images)
width, height = tf.to_float(img_shape[2]), tf.to_float(img_shape[1])
min_side = tf.to_float(tf.minimum(width, height))
with tf.device('/cpu:0'):
prob_arr = tf.TensorArray(
tf.float32, size=0, clear_after_read=True,
dynamic_size=True, element_shape=[None], infer_shape=False)
reg_arr = tf.TensorArray(
tf.float32, size=0, clear_after_read=True,
dynamic_size=True, element_shape=[None, 4], infer_shape=False)
box_arr = tf.TensorArray(
tf.float32, size=0, clear_after_read=True,
dynamic_size=True, element_shape=[None, 4], infer_shape=False)
stride = 2
cell_size = 12
def body(i, scale, prob_arr, reg_arr, box_arr):
width_scaled = tf.to_int32(width * scale)
height_scaled = tf.to_int32(height * scale)
img = tf.image.resize_bilinear(images, [height_scaled, width_scaled])
prob, reg = det1(img, 'NHWC')
with tf.device('/cpu:0'):
prob, reg = prob[0], reg[0]
scope.reuse_variables()
mask = prob[:, :, 1] > thresold
indexes = tf.where(mask)
bbox = [
tf.to_float(indexes*stride + 1)/scale,
tf.to_float(indexes*stride + cell_size)/scale]
bbox = tf.concat(bbox, axis=1)
prob = tf.boolean_mask(prob[:, :, 1], mask)
reg = tf.boolean_mask(reg, mask)
idx = tf.image.non_max_suppression(bbox, prob, 1000, 0.5)
bbox = tf.gather(bbox, idx)
prob = tf.gather(prob, idx)
reg = tf.gather(reg, idx)
prob_arr = prob_arr.write(i, prob)
reg_arr = reg_arr.write(i, reg)
box_arr = box_arr.write(i, bbox)
return i+1, scale * factor, prob_arr, reg_arr, box_arr
_, _, prob_arr, reg_arr, box_arr = tf.while_loop(
lambda i, scale, prob_arr, reg_arr, box_arr: min_side * scale > 12.,
body,
[0, 12. / min_size, prob_arr, reg_arr, box_arr],
back_prop=False)
prob, reg, bbox = prob_arr.concat(), reg_arr.concat(), box_arr.concat()
idx = tf.image.non_max_suppression(bbox, prob, 1000, 0.7)
bbox, prob, reg = tf.gather(bbox, idx), tf.gather(prob, idx), tf.gather(reg, idx)
bbox = regress_box(bbox, reg)
bbox = square_box(bbox)
return bbox, prob
首先是一些参数的设置,比如说最小尺寸以及每次缩小的倍数等,经过det1()这个函数后就是P-Net子网络的输出结果。定义了三个TensorFlow中的可变数组用来存储prob_arr[](筛选后的备选框是人脸的概率值),reg_arr[](经过赛选过后的备选框偏移量),box_arr[](经过赛选过后的建议框的位置,这里类似yolo或者faster-rcnn的rpn的建议框,faster在每个anchor点上有九个建议框。建议框加上偏移量才是备选框的具体位置)。通过body函数进行筛选,这里用到了非极大值抑制,nms(此刻的nms筛选是每一张经过缩放的图片)。这相对于大部分利用numpy有个好处就是可以留下的框的数目,这对于比较大的原图进行人脸检测的速度提升有很大的帮助。最后所有图片(原图和缩放后的图像)留下来的框再进行一次NMS的筛选。应当注意的是,此时建议框的位置都是相对于原图的,因为程序中有 bbox = [tf.to_float(indexes*stride + 1)/scale, tf.to_float(indexes*stride + cell_size)/scale]这个语句对每次缩放图片后对原图的映射。
最后将建议框和框的偏移量相加行程备选框,并对框进行方块话,即将每个矩形框变成正方形框。
R-Net:
def stage_two(images, bbox, prob, threshold, scope):
img_shape = tf.shape(images)
width, height = tf.to_float(img_shape[2]), tf.to_float(img_shape[1])
bbox_norm = bbox / [height, width, height, width]
img_batch = tf.image.crop_and_resize(images, bbox_norm, tf.tile([0], tf.shape(prob)), [24, 24])
prob, reg = det2(img_batch)
with tf.device('/cpu:0'):
mask = prob[:, 1] > threshold
prob, reg, bbox = (
tf.boolean_mask(prob[:, 1], mask),
tf.boolean_mask(reg, mask),
tf.boolean_mask(bbox, mask))
idx = tf.image.non_max_suppression(bbox, prob, 1000, 0.7)
bbox, prob, reg = tf.gather(bbox, idx), tf.gather(prob, idx), tf.gather(reg, idx)
bbox = regress_box(bbox, reg)
bbox = square_box(bbox)
return bbox, prob
按照由P-Net输出的备选框坐标对原图进行裁剪,并将裁剪后的图片resize成24*24的正方形图。经过R-Net的子网络det2()并通过nms筛选后,经过线性回归得到正方形的备选框。
O-Net:
def stage_three(images, bbox, prob, threshold, scope):
img_shape = tf.shape(images)
width, height = tf.to_float(img_shape[2]), tf.to_float(img_shape[1])
bbox_norm = bbox / [height, width, height, width]
img_batch = tf.image.crop_and_resize(images, bbox_norm, tf.tile([0], tf.shape(prob)), [48, 48])
prob, reg, landmarks = det3(img_batch)
with tf.device('/cpu:0'):
mask = prob[:, 1] > threshold
prob, reg, bbox, landmarks = (
tf.boolean_mask(prob[:, 1], mask),
tf.boolean_mask(reg, mask),
tf.boolean_mask(bbox, mask),
tf.boolean_mask(landmarks, mask))
hw = bbox[:, 2:] - bbox[:, :2]
hw = tf.reshape(tf.tile(tf.expand_dims(hw, 2), [1, 1, 5]), [-1, 10])
top_left = tf.reshape(tf.tile(tf.expand_dims(bbox[:, :2], 2), [1, 1, 5]), [-1, 10])
landmarks = top_left + hw * landmarks
bbox = regress_box(bbox, reg)
idx = tf.image.non_max_suppression(bbox, prob, 1000, 0.6)
bbox, prob, reg, landmarks = (
tf.gather(bbox, idx), tf.gather(prob, idx),
tf.gather(reg, idx), tf.gather(landmarks, idx))
return bbox, prob, landmarks
与R-Net的stage处理相识,这里只是多出了选出筛选后的landmarks的点坐标。
回归和进行正方形框的处理程序如下:
def regress_box(bbox, reg):
hw = bbox[:, 2:] - bbox[:, :2]
hw = tf.concat([hw, hw], axis=1)
bbox = bbox + hw * reg
return bbox
def square_box(bbox):
hw = bbox[:, 2:] - bbox[:, :2] + 1
max_side = tf.reduce_max(hw, axis=1, keepdims=True)
delta = tf.concat([(hw - max_side) * 0.5, (hw - max_side) * -0.5], axis=1)
bbox = bbox + delta
return bbox
这里全程由TensorFlow内部的api实现,对整个网络进行固化形成pb文件。然后调用pb文件即可直接得出人脸检测结果。
class MTCNN:
def __init__(self, model_path, min_size=40, factor=0.709, thresholds=[0.6, 0.7, 0.7]):
self.min_size = min_size
self.factor = factor
self.thresholds = thresholds
graph = tf.Graph()
with graph.as_default():
with open(model_path, 'rb') as f:
graph_def = tf.GraphDef.FromString(f.read())
tf.import_graph_def(graph_def, name='')
self.graph = graph
config = tf.ConfigProto(
allow_soft_placement=True,
intra_op_parallelism_threads=4,
inter_op_parallelism_threads=4)
config.gpu_options.allow_growth = True
self.sess = tf.Session(graph=graph, config=config)
def detect(self, img):
feeds = {
self.graph.get_operation_by_name('input').outputs[0]: img,
self.graph.get_operation_by_name('min_size').outputs[0]: self.min_size,
self.graph.get_operation_by_name('thresholds').outputs[0]: self.thresholds,
self.graph.get_operation_by_name('factor').outputs[0]: self.factor
}
fetches = [self.graph.get_operation_by_name('prob').outputs[0],
self.graph.get_operation_by_name('landmarks').outputs[0],
self.graph.get_operation_by_name('box').outputs[0]]
prob, landmarks, box = self.sess.run(fetches, feeds)
return box, prob, landmarks
def test_demo(imgpath):
mtcnn = MTCNN('./mtcnn.pb')
img = cv2.imread(imgpath)
bbox, scores, landmarks = mtcnn.detect(img)
nrof_faces = bbox.shape[0] # 人脸数目
print('找到人脸数目为:{}'.format(nrof_faces))