本文主要结合官方的示例代码解释MTCNN的Inference细节
###建立并加载MTCNN模型
模型结构如下:
建立模型:
with tf.Graph().as_default():
sess = tf.Session()
with sess.as_default():
pnet, rnet, onet = align.detect_face.create_mtcnn(sess, None)
其中,create_mtcnn加载了预训练的模型数据并返回pnet,rnet,onet的运行入口
def create_mtcnn(sess, model_path):
if not model_path:
model_path,_ = os.path.split(os.path.realpath(__file__))
with tf.variable_scope('pnet'):
data = tf.placeholder(tf.float32, (None,None,None,3), 'input')
pnet = PNet({'data':data})
pnet.load(os.path.join(model_path, 'det1.npy'), sess)
with tf.variable_scope('rnet'):
data = tf.placeholder(tf.float32, (None,24,24,3), 'input')
rnet = RNet({'data':data})
rnet.load(os.path.join(model_path, 'det2.npy'), sess)
with tf.variable_scope('onet'):
data = tf.placeholder(tf.float32, (None,48,48,3), 'input')
onet = ONet({'data':data})
onet.load(os.path.join(model_path, 'det3.npy'), sess)
pnet_fun = lambda img : sess.run(('pnet/conv4-2/BiasAdd:0', 'pnet/prob1:0'), feed_dict={'pnet/input:0':img})
rnet_fun = lambda img : sess.run(('rnet/conv5-2/conv5-2:0', 'rnet/prob1:0'), feed_dict={'rnet/input:0':img})
onet_fun = lambda img : sess.run(('onet/conv6-2/conv6-2:0', 'onet/conv6-3/conv6-3:0', 'onet/prob1:0'), feed_dict={'onet/input:0':img})
return pnet_fun, rnet_fun, onet_fun
MTCNN检测
模型检测:
bounding_boxes, _ = align.detect_face.detect_face(img, minsize, pnet, rnet, onet, threshold, factor)
检测过程分为如下图的四个部分:建立金字塔,pnet过程,rnet过程,onet过程
建立金字塔
factor_count=0
total_boxes=np.empty((0,9))
points=np.empty(0)
h=img.shape[0]
w=img.shape[1]
minl=np.amin([h, w])
m=12.0/minsize
minl=minl*m
# create scale pyramid
scales=[]
while minl>=12:
scales += [m*np.power(factor, factor_count)]
minl = minl*factor
factor_count += 1
下面解释一下建立金字塔的过程:
我们认为图像中人脸的最小尺寸为minsize,而图像的尺寸以图像的最短边picsize来表示
假设图像中的人脸大小为minl,则有:minsize<minl<picsize
由于训练MTCNN时采用的图片大小为12*12所以希望minl与这个大小相仿
当minl=minsize时,是缩放尺度最大的时候,应该缩放的尺度是12/minsize
当minl=picsize时,是缩放尺度最小的时候,应该缩放的尺度是12/minl
在[12/picsize, 12/minsize]的这个区间内,可以增加更多的尺度来适配不同facesize的情况。尺度的细粒度可以根据factor来调节,factor越接近1,金字塔的层数越多,考虑的facesize的情况越细致。一般情况下,factor取0.709,大概意思是每次将图像面积缩小一半
P-NET
P-NET操作最为复杂,类似于一个RPN网络
-
经过金字塔操作之后的图片输入pnet
pnet是全卷积结构,不限制输入图像的大小,输出一个人脸概率构成的heatmap,和对应的bbox regression
-
根据heatmap的概率值筛选出目标框,并还原到原图大小,得到bbox:
# imap即heatmap,t为预设的threshold
y, x = np.where(imap >= t)
if y.shape[0]==1:
dx1 = np.flipud(dx1)
dy1 = np.flipud(dy1)
dx2 = np.flipud(dx2)
dy2 = np.flipud(dy2)
score = imap[(y,x)]
reg = np.transpose(np.vstack([ dx1[(y,x)], dy1[(y,x)], dx2[(y,x)], dy2[(y,x)] ]))
if reg.size==0:
reg = np.empty((0,3))
bb = np.transpose(np.vstack([y,x]))
q1 = np.fix((stride*bb+1)/scale)
q2 = np.fix((stride*bb+cellsize-1+1)/scale)
boundingbox = np.hstack([q1, q2, np.expand_dims(score,1), reg])
-
得到的bbox经过nms作初步筛选后,和金字塔其他层所生成的bbox汇总。汇总后,再作一次nms
-
对bbox这些作bbox regression进行位置微调
# bbox regression regw = total_boxes[:,2]-total_boxes[:,0] regh = total_boxes[:,3]-total_boxes[:,1] qq1 = total_boxes[:,0]+total_boxes[:,5]*regw qq2 = total_boxes[:,1]+total_boxes[:,6]*regh qq3 = total_boxes[:,2]+total_boxes[:,7]*regw qq4 = total_boxes[:,3]+total_boxes[:,8]*regh total_boxes = np.transpose(np.vstack([qq1, qq2, qq3, qq4, total_boxes[:,4]])) total_boxes = rerec(total_boxes.copy()) total_boxes[:,0:4] = np.fix(total_boxes[:,0:4]).astype(np.int32) dy, edy, dx, edx, y, ey, x, ex, tmpw, tmph = pad(total_boxes.copy(), w, h)
-
rerec:将regression后的坐标转化为矩形
-
pad:后面两个网络的输入需要将bbox对应的图像(称作src)从原图中抠出来(称作target),pad可以获取了bbox对应的src坐标(x,ex,y,ey)和target坐标 (dx,edx,dy,edy)
R-Net
R-Net的输入是P-net得到的roi,类似传统two-stage网络的head
numbox = total_boxes.shape[0]
if numbox>0:
# second stage
tempimg = np.zeros((24,24,3,numbox))
for k in range(0,numbox):
tmp = np.zeros((int(tmph[k]),int(tmpw[k]),3))
tmp[dy[k]-1:edy[k],dx[k]-1:edx[k],:] = img[y[k]-1:ey[k],x[k]-1:ex[k],:]
if tmp.shape[0]>0 and tmp.shape[1]>0 or tmp.shape[0]==0 and tmp.shape[1]==0:
tempimg[:,:,:,k] = imresample(tmp, (24, 24))
else:
return np.empty()
tempimg = (tempimg-127.5)*0.0078125
tempimg1 = np.transpose(tempimg, (3,1,0,2))
out = rnet(tempimg1)
out0 = np.transpose(out[0])
out1 = np.transpose(out[1])
score = out1[1,:]
ipass = np.where(score>threshold[1])
total_boxes = np.hstack([total_boxes[ipass[0],0:4].copy(), np.expand_dims(score[ipass].copy(),1)])
mv = out0[:,ipass[0]]
if total_boxes.shape[0]>0:
pick = nms(total_boxes, 0.7, 'Union')
total_boxes = total_boxes[pick,:]
total_boxes = bbreg(total_boxes.copy(), np.transpose(mv[:,pick]))
total_boxes = rerec(total_boxes.copy())
-
将P-net得到的bbox从原图中抠出来,并作了归一化处理,输入到R-net中
-
根据score作初步筛选得到的结果,再通过nms进一步筛选
-
作bbox regreesion,修正位置,并通过rerec转化为矩形
O-Net
O-Net与R-net操作基本一致,最终输出结果是bbox和特征点
由于MTCNN的输出要进一步交给后续的CNN网络进行识别,O-Net输出之后又有一些后处理,可以看到这里给bbox加了边框,裁剪出的图像经过了白化处理
det = np.squeeze(bounding_boxes[0,0:4])
bb = np.zeros(4, dtype=np.int32)
bb[0] = np.maximum(det[0]-margin/2, 0)
bb[1] = np.maximum(det[1]-margin/2, 0)
bb[2] = np.minimum(det[2]+margin/2, img_size[1])
bb[3] = np.minimum(det[3]+margin/2, img_size[0])
cropped = img[bb[1]:bb[3],bb[0]:bb[2],:]
aligned = misc.imresize(cropped, (image_size, image_size), interp='bilinear')
prewhitened = facenet_util.prewhiten(aligned)
官方的代码中并没有进行人脸对齐操作