【技术文档】RetinaFace

RetinaFace模型原本是作者用于检测人脸,但我将其用于行人检测。我只是简单的将其anchor的宽高比例由原来的1:1,变为现在1:2.25,只有这一种比例,只是在FPN每个位置会有两个不同大小的,该比例的anchor。模型的特点就是比较小(4M)。
config

cfg_mnet = {
    'name': 'mobilenet0.25',
    # anchor大小的设置必须用绝对值,用相对值会带来不必要的麻烦
    'min_sizes': [[16, 32], [64, 128], [256, 512]],
    # step是用来计算每层featuremap的大小
    'steps': [8, 16, 32],
    'variance': [0.1, 0.2],
    'clip': False,
    'loc_weight': 2.0,
    'gpu_train': True,
    'batch_size': 32,
    'ngpu': 1,
    'epoch': 1000,
    'decay1': 190,
    'decay2': 220,
    'image_size': 640,
    'pretrain': False,
    'return_layers': {'stage1': 1, 'stage2': 2, 'stage3': 3},
    'in_channel': 32,
    'out_channel': 64
}

数据

读取格式

数据标注的保存形式是txt文档。第一行是图片名称,下面几行就是每个目标的标注,包括bbox(x1,y1,x2,y2)和landmarks。通过代码修改也可以只训练bbox。
在这里插入图片描述
若不需要预测skeleton,那就把skeleton的标注设置为0。

图片处理

训练时图片预处理
  1. 填充成正方形
def _pad_to_square(image, rgb_mean):
    height, width, _ = image.shape
    long_side = max(width, height)
    image_t = np.empty((long_side, long_side, 3), dtype=image.dtype)
    image_t[:, :] = rgb_mean
    image_t[0:0 + height, 0:0 + width] = image
    return image_t
  1. resize 640,减均值,h w c => c h w
def _resize_subtract_mean(image, insize, rgb_mean):
    interp_methods = [cv2.INTER_LINEAR, cv2.INTER_CUBIC, cv2.INTER_AREA, cv2.INTER_NEAREST, cv2.INTER_LANCZOS4]
    interp_method = interp_methods[random.randrange(5)]
    image = cv2.resize(image, (insize, insize), interpolation=interp_method)
    image = image.astype(np.float32)
    image -= rgb_mean
    return image.transpose(2, 0, 1)  #(h,w,c) -> (c,h,w)
  1. 标注归一化

此时的width和height是padding之后的宽和高,而不是原始图片的宽和高。

boxes_t[:, 0::2] /= width
boxes_t[:, 1::2] /= height
landm_t[:, 0::2] /= width
landm_t[:, 1::2] /= height
labels_t = np.expand_dims(labels_t, 1)
# 输出的targets需要调整成二维数的,每一行代表的是一张图片中一个目标的bbox坐标landmark坐标和label属性
# 有多少个目标就有多少行
targets_t = np.hstack((boxes_t, landm_t, labels_t))
检测时图片预处理

在检测时,可以直接检测原图,但考虑到图片中目标大小和模型感受野的原因,一般都不直接检测原图。而是将最小边固定大小为640,另一条边等比例缩放。

数据集的使用和影响

coco

我先是用coco2014(行人图片约4万张)训练带bbox和landmarks的模型。遇到了两个问题,第一个问题是预测时候会出现大框,原因是因为在coco数据集的annotation中,如果iscrowd=1,那么代表该边界框中包含不止一个目标。我当时没把这些样本删掉,所以导致在密集行人检测是出现了很多大框。如下图所示:

coco数据集训练出的大框
后来注意到这个问题之后就把iscrowd=1的标注删掉了,预测时大框也就消失了。但会有很多人被漏检,这是数据集的原因。
在这里插入图片描述
在用COCO2014训练出现的另一个问题就是coco用于行人检测的skeleton是全的,17个skeleton只有照片上人肉眼能看到的部分才有标注,肉眼看不到的点就用两个零表示(分别代表横纵坐标)。这个问题就需要在损失函数上进行修改,那就是只计算哪些有标注点的损失,如果有的点没有给出标注,那么无论该点的预测值是多少,该点的损失都算为零。最后skeleton的预测效果是,大体的轮廓预测的差不多,但对不齐,原因可能是由于模型太小,拟合不了太复杂的任务。
在这里插入图片描述
在训练的过程中,关于skeleton的那部分损失看起来使基本不下降的。我打印出模型上关于预测skeleton的参数,发现其基本不变。所以我推测是由于关于预测skeleton的参数没有变,所以导致关于skeleton的损失没有下降。于是我将BackBone、Classfication和Regression的网络参数冻结(此时他们已经收敛了),让后将学习率提高进行训练,此时 关于skeleton的参数变了,但是迭代多次之后,skeleton损失依然没有收敛。所以现在我只能把原因归因于模型太小,拟合不了太复杂的任务。

crowdhuman

该数据集的特点就是目标密集,训练集一共15000张图片,平均每张图20多个行人。所以很适合用于密集人群检测,其效果如下:
在这里插入图片描述
整体上没有漏检的,但在细节上不太好,有一些多余的框。之后我就换了一个模型(Retinet,80M)去预测,效果在细节上就提升了很多
在这里插入图片描述
基本上做到了一个目标一个框。效果提升的主要原因我认为是模型更深了,拟合能力更强了,很多问题就自然而然解决了。对于深度学习来说很多问题(比如遮挡)不需要专门的解决方案,只需要足够多的数据和差不多的模型,大多数问题就基本上可以解决。

模型

在这里插入图片描述

backbone

模型的主干网络是MobileNet,所以这是导致模型比较小的主要原因。
其卷积模块的结构是

def conv_dw(inp, oup, stride, leaky=0.1):
    return nn.Sequential(
        nn.Conv2d(inp, inp, kernel_size=3, stride, padding=1, groups=inp, bias=False),
        nn.BatchNorm2d(inp),
        nn.LeakyReLU(negative_slope= leaky,inplace=True),

        nn.Conv2d(inp, oup, 1, 1, 0, bias=False),
        nn.BatchNorm2d(oup),
        nn.LeakyReLU(negative_slope= leaky,inplace=True),
    )

其主要部分是

nn.Conv2d(inp, inp, kernel_size=3, stride, padding=1, groups=inp, bias=False)

当stride=2的时候其作用就相当于pooling layer,现在大多数代码的池化层都这样写,当stride=2的时候,输入featuremap的大小无论是10还是9,输出的大小都是5。相当于maxpooling计算出来的小数结果向上取整。等价于下面这行代码

maxpool = nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True)

无论如何写,都是为了向上取整,其最终目的是为了每层产生特征图的大小和产生的anchor对齐。

FPN

fpn一共三层,每一层的输入通道数不同,但输出通道数都是相同的。FPN不对每层featuremap输入的大小进行改变,它只负责改变通道数和特征融合。在将不同特征层的特征进行融合是,需要将尺度比较小的特征进行上采样,其方式具体为

up3 = F.interpolate(output3, size=[output2.size(2), output2.size(3)], mode="nearest")
output2 = output2 + up3
output2 = self.merge(output2)

context module

该模块是RetinaFace的特色部分,和FPN的作用差不多,主要用来将不同的感受野融合到一个模块里。

classification head

class ClassHead(nn.Module):
    def __init__(self,inchannels=512, num_anchors=num_anchor):
        super(ClassHead,self).__init__()
        self.num_anchors = num_anchors
        self.conv1x1 = nn.Conv2d(inchannels, self.num_anchors*2, kernel_size=(1, 1), stride=1, padding=0)
    def forward(self,x):
        out = self.conv1x1(x)
        out = out.permute(0, 2, 3, 1).contiguous()
        return out.view(out.shape[0], -1, 2)

anchor

每一层featuremap大小的计算结果都要向上取整,与模型的中的卷积模块对应。

class PriorBox(object):
    def __init__(self, cfg, image_size=None, phase='train'):
        super(PriorBox, self).__init__()
        self.min_sizes = cfg['min_sizes']
        self.steps = cfg['steps']
        self.image_size = image_size
        self.feature_maps = [[ceil(self.image_size[0]/step), ceil(self.image_size[1]/step)] for step in self.steps]

    def forward(self):
        anchors = []
        for k, f in enumerate(self.feature_maps):
            min_sizes = self.min_sizes[k]
            for i, j in product(range(f[0]), range(f[1])):
            # 每个位置放大小不同的两个anchor
                for min_size in min_sizes:
                    # s_kx_1 = (min_size / self.image_size[1])
                    # s_ky_1 = (min_size / self.image_size[0])
                    s_kx_2 = (min_size / self.image_size[1]) / 1.5
                    s_ky_2 = (min_size / self.image_size[0]) * 1.5
                    dense_cx = [x * self.steps[k] / self.image_size[1] for x in [j + 0.5]]
                    dense_cy = [y * self.steps[k] / self.image_size[0] for y in [i + 0.5]]
                    for cy, cx in product(dense_cy, dense_cx):
                        # anchors += [cx, cy, s_kx_1, s_ky_1]
                        anchors += [cx, cy, s_kx_2, s_ky_2]

        # back to torch land
        output = torch.Tensor(anchors).view(-1, 4)
        if self.clip:
            output.clamp_(max=1, min=0)
        return output

NMS

先选择出类别输出中,行人置信度那一列的预测结果,conf是模型classification head的输出结果

scores = conf.squeeze(0).data.cpu().numpy()[:, 1]

然后将scores与对应的bbox的预测结果进行拼接

dets = np.hstack((boxes, scores[:, np.newaxis])).astype(np.float32, copy=False)

通过nms

def py_cpu_nms(dets, thresh):
    """Pure Python NMS baseline."""
    x1 = dets[:, 0]
    y1 = dets[:, 1]
    x2 = dets[:, 2]
    y2 = dets[:, 3]
    scores = dets[:, 4]

    areas = (x2 - x1 + 1) * (y2 - y1 + 1)
    order = scores.argsort()[::-1]  # score由大道小的索引

    keep = []
    while order.size > 0:
        i = order[0]
        keep.append(i)
        xx1 = np.maximum(x1[i], x1[order[1:]])
        yy1 = np.maximum(y1[i], y1[order[1:]])
        xx2 = np.minimum(x2[i], x2[order[1:]])
        yy2 = np.minimum(y2[i], y2[order[1:]])

        w = np.maximum(0.0, xx2 - xx1 + 1)
        h = np.maximum(0.0, yy2 - yy1 + 1)
        inter = w * h
        ovr = inter / (areas[i] + areas[order[1:]] - inter)

        inds = np.where(ovr <= thresh)[0]
        order = order[inds + 1]

    return keep

损失函数

  1. 根据IOU给每个anchor匹配GroundTruth
  2. encoding
def encode(matched, priors, variances):
    """Encode the variances from the priorbox layers into the ground truth boxes
    we have matched (based on jaccard overlap) with the prior boxes.
    Args:
        matched: (tensor) Coords of ground truth for each prior in point-form
            Shape: [num_priors, 4].
        priors: (tensor) Prior boxes in center-offset form
            Shape: [num_priors,4].
        variances: (list[float]) Variances of priorboxes
    Return:
        encoded boxes (tensor), Shape: [num_priors, 4]
    """

    # dist b/t match center and prior's center
    g_cxcy = (matched[:, :2] + matched[:, 2:])/2 - priors[:, :2]
    # encode variance
    g_cxcy /= (variances[0] * priors[:, 2:])
    # match wh / prior wh
    g_wh = (matched[:, 2:] - matched[:, :2]) / priors[:, 2:]
    g_wh = torch.log(g_wh) / variances[1]
    # return target for smooth_l1_loss
    return torch.cat([g_cxcy, g_wh], 1)  # [num_priors,4]
  • 计算正样本的回归损失,和有标注的正样本的skeleton损失。类别损失是使用难例挖掘。

思考

对一个项目来说,研究到什么程度才算差不多了?

  • 每行代码都了解其作用,并清晰它们之间的联系,出现问题能快速解决
  • 达到满意的测试效果
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值