转自:https://blog.youkuaiyun.com/warrentdrew/article/details/98742948
介绍
Insight Face在2019年提出的最新人脸检测模型,原模型使用了deformable convolution和dense regression loss, 在 WiderFace 数据集上达到SOTA。截止2019年8月,原始模型尚未全部开源,目前开源的简化版是基于传统物体检测网络RetinaNet的改进版,添加了SSH网络的检测模块,提升检测精度,作者提供了三种基础网络,基于ResNet的ResNet50和ResNet152版本能提供更好的精度,以及基于mobilenet(0.25)的轻量版本mnet,检测速度更快。
简化版mnet结构
RetinaFace的mnet本质是基于RetinaNet的结构,采用了特征金字塔的技术,实现了多尺度信息的融合,对检测小物体有重要的作用,RetinaNet的结构如下
简化版的mnet与RetinaNet采用了相同的proposal策略,即保留了在feature pyramid net的3层特征图每一层检测框分别proposal,生成3个不同尺度上的检测框,每个尺度上又引入了不同尺寸的anchor大小,保证可以检测到不同大小的物体。
简化版mnet与RetinaNet的区别除了在于主干网络的选择上使用了mobilenet做到了模型的轻量化,最大的区别在于检测模块的设计。mnet使用了SSH检测网络的检测模块,SSH检测模块由SSH上下文模块组成
上下文模块的作用是扩张预检测区域的上下文信息。上下文模块和conv结合组成了一个检测模块
上图为SSH网络的检测模块,将一个上下文模块与conv叠加后生成分类头和回归头得到网络的输出。
mnet网络在使用SSH检测模块的同时实现了多任务学习,即在分类和回归的基础上加入了目标点的回归。官方的网络结构采用了5个目标点的学习,后续也可以修改为更多目标点,比如AFLW中的21个目标点以及常用的68或者106个目标点
上图是原版RetinaFace论文中的检测分支图,注意在开源简化版的不包含self-supervision部分,但是有5个关键点的extra-supervision部分
检测
RetinaFace的检测过程和所有的single-stage的检测器过程相似,在github原版的实现上主要在retinaface.py中的detect()中实现,实验中主要可调整的超参数包括threshold, nms_threshold,scale等。
- threshold : 分类概率的阈值,超过这个阈值的检测被判定为正例
- nms_threshold : 非极大值抑制中的IOU阈值,即在nms中与正例的IOU超过这个阈值的检测将被舍弃
- scale : 图像金字塔的缩放值,通过对原图进行由scale值指定的大小缩放得到网络图片的输入大小,注意在检测时网络的输入不必保持相同的大小
简化版的RetinaFace在特征金字塔上有3个检测分支,分别对应3个stride: 32, 16和8。在stride32上一个feature map对应的原图的32X32的感受野,可以用来检测较大的区域人脸,同理stride16和stride8可用于中等和较小人脸区域的检测。默认设置为每个stride对应一个ratio,每个ratio对应两个scale,即每个stride对应的feature map的每个位置会在原图上生成两个anchor box,anchor box默认设置代码如下:
_ratio = (1.,)
self._feat_stride_fpn = [32, 16, 8]
self.anchor_cfg = {
'32': {'SCALES': (32,16), 'BASE_SIZE': 16, 'RATIOS': _ratio, 'ALLOWED_BORDER': 9999},
'16': {'SCALES': (8,4), 'BASE_SIZE': 16, 'RATIOS': _ratio, 'ALLOWED_BORDER': 9999},
'8': {'SCALES': (2,1), 'BASE_SIZE': 16, 'RATIOS': _ratio, 'ALLOWED_BORDER': 9999},
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
这样在stride32上对应的feature map的每个像素点对应的原图位置上生成两个大小分别为512 × 512和256 × 256的anchor, 假设采取训练的默认输入大小640 × 640, stride32 对应的feature map大小为20 × 20 (640 / 32),那么在stride32对应的feature map上一共可以得到 20 × 20 × 2 = 800个anchor, 同理在stride16对应的feature map可以生成 大小128 × 128和64 × 64的anchor,共有40 × 40 × 2 = 3200个, 在stride8对应的feature map可以生成大小为32 × 32 和 16 × 16的feature map,共有80 × 80 × 2 = 12800个,3个scale总共可以生成800 + 3200 + 12800 = 16800个anchor, 在每个feature map上生成anchor时可以调用rcnn/cython/anchors_cython(),代码如下:
def anchors_cython(int height, int width, int stride, np.ndarray[DTYPE_t, ndim=2] base_anchors):
"""
Parameters
----------
height: height of plane
width: width of plane
stride: stride ot the original image
anchors_base: (A, 4) a base set of anchors
Returns
-------
all_anchors: (height, width, A, 4) ndarray of anchors spreading over the plane
"""
cdef unsigned int A = base_anchors.shape[0]
cdef np.ndarray[DTYPE_t, ndim=4] all_anchors = np.zeros((height, width, A, 4), dtype=DTYPE)
cdef unsigned int iw, ih
cdef unsigned int k
cdef unsigned int sh
cdef unsigned int sw
for iw in range(width):
sw = iw * stride
for ih in range(height):
sh = ih * stride
for k in range(A):
all_anchors[ih, iw, k, 0] = base_anchors[k, 0] + sw
all_anchors[ih, iw, k, 1] = base_anchors[k, 1] + sh
all_anchors[ih, iw, k, 2] = base_anchors[k, 2] + sw
all_anchors[ih, iw, k, 3] = base_anchors[k, 3] + sh
return all_anchors
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
图像金字塔即将图像resize的各种不同的大小输入到检测网络得到个尺度的检测结果,是检测中常用的提取多尺度的方式,使模型能有更有效的检测数不同尺度的人脸。简化版的retinaface在测试时也使用的这个方式来提升精度。由于检测时输入图片的大小可以各不相同,在检测时定义了target_size和max_size两个参数。target_size定义了模型输入数据短边的长度,max_size定义的输入数据长边的最大范围。
在单尺度测试时(即不采用图像金字塔),优先考虑target_size,当图像短边达到target_size而长边没有超出max_size时,即将图像缩放为短边target_size的大小,否则缩放为长边是max_size的大小。而在多尺度测试时,这个target_size被定义为各个不同的大小,如在源代码测试widerface数据集时,target_size被定义为[500, 800, 1100, 1400, 1700]
,由此也得到了不同的图像缩放系数im_scale,对缩放各个不同尺度的图片做检测,得到关于这幅图片的所有检测框,具体实现即上文提到的***修改retinaface.py中的detect()方法里的scale参数***(即scale = im_scale)。
在测试中运用图像金字塔的做法也被广泛应用在各种人脸检测的模型中,有时也被成为multi-scale testing,在S3FD和SRN以及最新的SOTA模型AFD_HP等模型中都有应用。
训练
训练过程中如果要做到多张图片一起训练需要保持每张图片的大小一致,且与网络的输入层尺寸一致,即训练过程中所有图片的大小均为640×640×3。开源版的github采用了crop的方式实现了图片尺寸的一致性,主要的实现代码在io/image.py中的get_crop_image1(roidb)中:
def get_crop_image1(roidb):
"""
preprocess image and return processed roidb
:param roidb: a list of roidb
:return: list of img as in mxnet format
roidb add new item['im_info']
0 --- x (width, second dim of im)
|
y (height, first dim of im)
"""
#roidb and each roi_rec can not be changed as it will be reused in next epoch
num_images = len(roidb)
processed_ims = []
processed_roidb = []
for i in range(num_images):
roi_rec = roidb[i]
if 'stream' in roi_rec:
im = cv2.imdecode(roi_rec['stream'], cv2.IMREAD_COLOR)
else:
assert os.path.exists(roi_rec['image']), '{} does not exist'.format(roi_rec['image'])
im = cv2.imread(roi_rec['image'])
if roidb[i]['flipped']:
im = im[:, ::-1, :]
if 'boxes_mask' in roi_rec:
#im = im.astype(np.float32)
boxes_mask = roi_rec['boxes_mask'].copy()
boxes_mask = boxes_mask.astype(np.int)
for j in range(boxes_mask.shape[0]):
m = boxes_mask[j]
im[m[1]:m[3],m[0]:m[2],:] = 127
#print('find mask', m, file=sys.stderr)
SIZE = config.SCALES[0][0] ###640
PRE_SCALES = [0.3, 0.45, 0.6, 0.8, 1.0]
#PRE_SCALES = [0.3, 0.45, 0.6, 0.8, 1.0, 0.8, 1.0, 0.8, 1.0]
_scale = random.choice(PRE_SCALES)
#_scale = np.random.uniform(PRE_SCALES[0], PRE_SCALES[-1])
size = int(np.min(im.shape[0:2])*_scale)
#size = int(np.round(_scale*np.min(im.shape[0:2])))
im_scale = float(SIZE)/size
im = cv2.resize(im, None, None, fx=im_scale, fy=im_scale, interpolation=cv2.INTER_LINEAR)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
训练数据集的准备引入了数据增强的策略,对于图片做不同尺度的缩放,图片的基准尺寸用的是网络的输入大小640,首先将输入图片较短的维度缩放成基础尺寸640, 在此基础上根据PRE_SCALES = [0.3, 0.45, 0.6, 0.8, 1.0]
再进行缩放,每张图片都会随机匹配一个PRE_SCALE,将图像短边缩放成640 / PRE_SCALE, 即图像的短边尺寸的取值包括[640, 800, 1067, 1422, 2133]
(在代码中稍有误差)。在得到了调整尺寸的原图后,要根据图片的尺寸调整标注位置框和特征点的尺寸,如下程序段所示
assert im.shape[0]>=SIZE and im.shape[1]>=SIZE
#print('image size', origin_shape, _scale, SIZE, size, im_scale)
new_rec = roi_rec.copy()
new_rec['boxes'] = roi_rec['boxes'].copy() * im_scale
if config.FACE_LANDMARK:
new_rec['landmarks'] = roi_rec['landmarks'].copy()
new_rec['landmarks'][:,:,0:2] *= im_scale
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
调整位置框bbox和特征点landmark的尺寸调整非常容易,只要将原来的坐标位置×缩放尺度im_scale,就能得到尺度调整后的bbox和landmark坐标。得到了调整完的图片和bbox,landmark坐标后,下一步就可以对图片进行裁剪以适应网络的输入大小640 × 640,如下程序段所示
retry = 0
LIMIT = 25
size = SIZE
while retry<LIMIT:
up, left = (np.random.randint(0, im.shape[0]-size+1), np.random.randint(0, im.shape[1]-size+1))
boxes_new = new_rec['boxes'].copy()
im_new = im[up:(up+size), left:(left+size), :]
#print('crop', up, left, size, im_scale)
boxes_new[:,0] -= left
boxes_new[:,2] -= left
boxes_new[:,1] -= up
boxes_new[:,3] -= up
if config.FACE_LANDMARK:
landmarks_new = new_rec['landmarks'].copy()
landmarks_new[:,:,0] -= left
landmarks_new[:,:,1] -= up
valid_landmarks = []
valid = []
valid_boxes = []
for i in range(boxes_new.shape[0]):
box = boxes_new[i]
#center = np.array(([box[0], box[1]]+[box[2], box[3]]))/2
centerx = (box[0]+box[2])/2
centery = (box[1]+box[3])/2
box_size = max(box[2]-box[0], box[3]-box[1])
if centerx<0 or centery<0 or centerx>=im_new.shape[1] or centery>=im_new.shape[0]:
continue
if box_size<config.TRAIN.MIN_BOX_SIZE:
continue
#filter by landmarks? TODO
valid.append(i)
valid_boxes.append(box)
if config.FACE_LANDMARK:
valid_landmarks.append(landmarks_new[i])
if len(valid)>0 or retry==LIMIT-1:
im = im_new
new_rec['boxes'] = np.array(valid_boxes)
new_rec['gt_classes'] = new_rec['gt_classes'][valid]
if config.FACE_LANDMARK:
new_rec['landmarks'] = np.array(valid_landmarks)
if config.HEAD_BOX:
face_box = new_rec['boxes']
head_box = expand_bboxes(face_box, image_width=im.shape[1], image_height=im.shape[0])
new_rec['boxes_head'] = np.array(head_box)
break
retry+=1
if config.COLOR_MODE>0 and config.COLOR_JITTERING>0.0:
im = im.astype(np.float32)
im = color_aug(im, config.COLOR_JITTERING)
im_tensor = transform(im, config.PIXEL_MEANS, config.PIXEL_STDS, config.PIXEL_SCALE)
processed_ims.append(im_tensor)
#print('boxes', new_rec['boxes'], file=sys.stderr)
im_info = [im_tensor.shape[2], im_tensor.shape[3], im_scale]
new_rec['im_info'] = np.array(im_info, dtype=np.float32)
processed_roidb.append(new_rec)
return processed_ims, processed_roidb
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
这里设置LIMIT=25对每张图片做25次随机裁剪,每次裁剪的大小即为640 × 640 × 3,同时同样需要对bbox和landmark的标注进行调整,即减去随机裁剪图片的在原图中的左上角位置left和up,得到标注坐标在crop中的位置。验证新的bbox是否有效,即中心点位置是否在裁剪后的图中,大小是否小于预定义的人脸最小大小,筛选符合要求的人脸。之后再对裁剪后的图片做常规的数据增强和特征归一化等操作,得到了用于网络输入的图片processed_ims,以及尺度和位置调整过的bbox和landmark坐标processed_roidb。