目标检测 Anchor Free:CenterNet

前言

之前写了几篇关于目标检测的博客,目标检测综述目标检测Yolo系列目标检测YOLO算法代码实现。里面都详细介绍了Yolo系列的算法以及代码实现。在《目标检测综述》博客中简单提到了一些anchor free的算法。在一些技术讨论当中,我也会被问到一些anchor free的算法,因此最近实现centernet去训练一下看一下效果,并写下这篇博客作为记录。

本篇博客先从论文入手,然后分析一些技术点,包括网络结构,heatmap生成,损失函数等,最后根据网络架构图入手用代码实现。

CenterNet

CenterNet将检测对象看作是一个点,即目标边界框的中心点,然后使用关键点估计来寻找中心点,并回归到其他对象属性,如大小、三维位置、方向甚至姿态。将目标检测看作是关键点检测,抛弃了由anchor生成的大量需要被抑制的样本,因此不需要NMS做后处理以及提高检测的速度。CenterNet与其他目标检测算法做对比如下图所示。

Centernet简单把一个输入图像输入到一个全卷积神经网络中生成一个热力图。该热力图的峰值对应的就是目标的中心。图像特征在每个峰值处预测目标的边界框的高和宽。模型训练使用标准的密集监督学习方法。推理过程就是一个网络的前向推理,不需要非极大值抑制作为后处理。
在这里插入图片描述

相关工作

与其他的关键点估计的目标检测网络相比,如CornetNet(检测bounding box的左上角和右下角作为关键点)和ExtremeNet,他们都需要在关键点检测后进行一个组合分组阶段,这个过程会降低算法的速度。然而CenterNet只需要简单地提取每个对象的单个中心点,而不需要分组或后处理。
在这里插入图片描述

技术点

关键点

I ∈ R W × H × 3 I\in R^{W\times H\times 3} IRW×H×3为宽 W W W和高 H H H的输入图像,CenterNet的目的是产生一个heatmap Y ^ ∈ [ 0 , 1 ] W R × H R × C \hat{Y}\in [0, 1]^{\frac{W}{R}\times \frac{H}{R}\times C} Y^[0,1]RW×RH×C,其中 R R R表示输入步长, C C C表示关键点类型的数量。文献中使用默认输出步长为 R = 4 R=4 R=4,输出步幅将输出预测降低采样一个因子 R R R。对于 Y ^ x , y , c = 1 \hat{Y}_{x,y,c}=1 Y^x,y,c=1表示检测到的关键点,反正为背景。stacked hourglass network、resnet和deep layer aggregation(DLA)等全连接encoder-decoder结构网络作为预测。

关键点数据标签

对于类别 c c c中每一个真实关键点分布 p ∈ R 2 p\in R^2 pR2,计算出来用于训练,中心点的计算方式为 p = ( x 1 + x 2 2 , y 1 + y 2 2 ) p=(\frac{x_1+x_2}{2},\frac{y_1+y_2}{2}) p=(2x1+x2,2y1+y2),对于下采样后的坐标,设置式 p ~ = ⌊ P R ⌋ \widetilde{p}=\lfloor\frac{P}{R}\rfloor p =RP R R R就是上文提到的下采样因子。通过这式子最终计算出来的中心点对应的是低分辨率的中心点。然后利用 Y ^ ∈ [ 0 , 1 ] W R × W R × C \hat{Y}\in [0, 1]^{\frac{W}{R}\times \frac{W}{R}\times C} Y^[0,1]RW×RW×C来对图像进行标记,在下采样 [ 128 , 128 ] [128, 128] [128,128]图像中用一个高斯核 Y x y c = e x p ( − ( x − p ~ x ) 2 + ( y − p ~ y ) 2 2 σ p 2 ) Y_{xyc}=exp(-\frac{(x-\widetilde{p}_x)^2+(y-\widetilde{p}_y)^2}{2\sigma^2_p}) Yxyc=exp(2σp2(xp x)2+(yp y)2)来将关键点分布到特征图上,其中 σ p \sigma_p σp是一个与目标大小相关的标准差。如果某一个类的两个高斯分布发生了重叠,直接取元素间最大的就可以。

损失函数

这一部分我的能力不足,主要还是摘抄自https://zhuanlan.zhihu.com/p/66048276。

中心点预测

损失函数: L k = − 1 N ∑ x y c { ( 1 − Y ^ x y c ) β ( Y ^ x y c ) α log ⁡ ( 1 − Y ^ x y c ) o t h e r w i s e ( 1 − Y ^ x y c ) a log ⁡ ( Y ^ x y c ) i f Y ^ x y c = 1 L_k=-\frac{1}{N}\sum_{xyc}\{^{ (1-\hat{Y}_{xyc})^a\log(\hat{Y}_{xyc}) \quad if \quad \hat{Y}_{xyc}=1 } _{ (1-\hat{Y}_{xyc})^\beta(\hat{Y}_{xyc})^\alpha\log(1-\hat{Y}_{xyc}) \quad otherwise } Lk=N1xyc{ (1Y^xyc)β(Y^xyc)αlog(1Y^xyc)otherwise(1Y^xyc)alog(Y^xyc)ifY^xyc=1其中 α \alpha α β \beta βfocal loss的超参数, N N N是图像 I I I的关键点数量,用于将所有positive focal loss标准化为1。在这个loss当中,对于easy example的中心点,适当减少其训练比重也就是loss值,当 Y x y c = 1 Y_{xyc}=1 Yxyc=1的时候, ( 1 − Y ^ x y c ) α (1-\hat{Y}_{xyc})^\alpha (1Y^xyc)α就充当了校正的作用。假如 Y ^ x y c \hat{Y}_{xyc} Y^xyc接近1的话,说明这是个比较容易检测的点,那么 ( 1 − Y ^ x y c ) α (1-\hat{Y}_{xyc})^\alpha (1Y^xyc)α就相应比较低了。而当 Y ^ x y c \hat{Y}_{xyc} Y^xyc接近0,说明这个中心点没有学习到,需要增加训练比重,因此 ( 1 − Y ^ x y c ) α (1-\hat{Y}_{xyc})^\alpha (1Y^xyc)α就会很大。
对于otherwise的情况

目标中心的偏置损失

在这里插入图片描述
在这里插入图片描述

目标大小的损失

假设 ( x 1 ( k ) , y 1 ( k ) , x 2 ( k ) , y 2 ( k ) ) (x_1^{(k)}, y_1^{(k)}, x_2^{(k)}, y_2^{(k)}) (x1(k),y1(k),x2(k),y2(k))为目标 k k k,所属类别为 c k c_k ck,它的中心点为 p k = ( x 1 ( k ) + x 2 ( k ) 2 , y 1 ( k ) + y 2 ( k ) 2 ) p_k=(\frac{x_1^{(k)}+x_2^{(k)}}{2}, \frac{y_1^{(k)}+y_2^{(k)}}{2}) pk=(2x1(k)+x2(k),2y1(k)+y2(k))。当使用关键点 Y ^ \hat{Y} Y^预测所有中心点,然后对每个目标 k k k的size进行回归,最终回归到 s k = ( x 2 ( k ) − x 1 ( k ) , y 2 ( k ) − y 1 ( k ) ) s_k=(x_2^{(k)}-x_1^{(k)},y_2^{(k)}-y_1^{(k)} ) sk=(x2(k)x1(k),y2(k)y1(k)),这个值是在训练前提前算出来的,是进行下采样后的长宽值。为了减少回归难度,这里使用 S ^ ∈ R W R × W R × 2 \hat{S}\in R^{\frac{W}{R}\times \frac{W}{R}\times 2} S^RRW×RW×2作为预测值,使用 L 1 L1 L1损失函数,与之前的 L o f f s e t L_{offset} Loffset的损失函数一致: L s i z e = 1 N ∑ k = 1 N ∣ S p k ^ − s k ∣ L_{size}=\frac{1}{N}\sum^N_{k=1}|\hat{S_{p_k}}-s_k| Lsize=N1k=1NSpk^sk综上所述,整体的损失函数为物体的损失,大小的损失和位置偏置的损失: L d e t = L k + λ s i z e L s i z e + λ o f f s e t L o f f s e t L_{det}=L_k+\lambda_{size}L_{size}+\lambda_{offset}L_{offset} Ldet=Lk+λsizeLsize+λoffsetLoffset

复原阶段

复原阶段主要是从一个中心点如何复原到bbox。
在这里插入图片描述

在推理阶段,对于每个类别都单独提取热力图的峰值,检测当前热点的值是否比周围八领域都打,然后取100个这样的点,采用方式是 3 × 3 3\times 3 3×3的maxpool。
在这里插入图片描述
在这里插入图片描述

代码实现

代码部分可以参考:CenterNet-Tensorflow2,喜欢的可以给个star。

数据加载

centernet在训练的时候对数据进行增强处理:

def generate(self):
        i = 0
        while True:
            batch_images    = np.zeros((self.batch_size, self.input_shape[0], self.input_shape[1], 3), dtype=np.float32)
            batch_hms       = np.zeros((self.batch_size, self.output_shape[0], self.output_shape[1], self.num_classes), dtype=np.float32)
            batch_whs       = np.zeros((self.batch_size, self.max_objects, 2), dtype=np.float32)
            batch_regs      = np.zeros((self.batch_size, self.max_objects, 2), dtype=np.float32)
            batch_reg_masks = np.zeros((self.batch_size, self.max_objects), dtype=np.float32)
            batch_indices   = np.zeros((self.batch_size, self.max_objects), dtype=np.float32)
                
            for b in range(self.batch_size):
                image, box  = self.get_random_data(self.annotation_lines[i], self.input_shape, random = self.train)
                if len(box) != 0:
                    boxes = np.array(box[:, :4],dtype=np.float32)
                    boxes[:, [0, 2]] = np.clip(boxes[:, [0, 2]] / self.input_shape[1] * self.output_shape[1], 0, self.output_shape[1] - 1)
                    boxes[:, [1, 3]] = np.clip(boxes[:, [1, 3]] / self.input_shape[0] * self.output_shape[0], 0, self.output_shape[0] - 1)

                for i in range(len(box)):
                    bbox    = boxes[i].copy()
                    cls_id  = int(box[i, -1])
                    
                    h, w = bbox[3] - bbox[1], bbox[2] - bbox[0]
                    if h > 0 and w > 0:
                        radius  = gaussian_radius((math.ceil(h), math.ceil(w)))
                        radius  = max(0, int(radius))
                        #   计算真实框所属的特征点
                        ct      = np.array([(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2], dtype=np.float32)
                        ct_int  = ct.astype(np.int32)
                        batch_hms[b, :, :, cls_id] = draw_gaussian(batch_hms[b, :, :, cls_id], ct_int, radius)
						# 计算宽高真实值
                        batch_whs[b, i]         = 1. * w, 1. * h
                        #   计算中心偏移量
                        batch_regs[b, i]        = ct - ct_int
                        #   将对应的mask设置为1,用于排除多余的0
                        batch_reg_masks[b, i]   = 1
                        #   表示第ct_int[1]行的第ct_int[0]个。
                        batch_indices[b, i]     = ct_int[1] * self.output_shape[0] + ct_int[0]

                batch_images[b] = preprocess_input(image)
            yield batch_images, batch_hms, batch_whs, batch_regs, batch_reg_masks, batch_indice

Backbone

以RestNet为例。
ResNet50

def ResNet50(inputs):
    # 512x512x3
    x = ZeroPadding2D((3, 3))(inputs)
    # 256,256,64
    x = Conv2D(64, (7, 7), strides=(2, 2), kernel_initializer=RandomNormal(stddev=0.02), name='conv1', use_bias=False)(x)
    x = BatchNormalization(name='bn_conv1')(x)
    x = Activation('relu')(x)

    # 256,256,64 -> 128,128,64
    x = MaxPooling2D((3, 3), strides=(2, 2), padding="same")(x)

    # 128,128,64 -> 128,128,256
    x = conv_block(x, 3, [64, 64, 256], stage=2, block='a', strides=(1, 1))
    x = identity_block(x, 3, [64, 64, 256], stage=2, block='b')
    x = identity_block(x, 3, [64, 64, 256], stage=2, block='c')

    # 128,128,256 -> 64,64,512
    x = conv_block(x, 3, [128, 128, 512], stage=3, block='a')
    x = identity_block(x, 3, [128, 128, 512], stage=3, block='b')
    x = identity_block(x, 3, [128, 128, 512], stage=3, block='c')
    x = identity_block(x, 3, [128, 128, 512], stage=3, block='d')

    # 64,64,512 -> 32,32,1024
    x = conv_block(x, 3, [256, 256, 1024], stage=4, block='a')
    x = identity_block(x, 3, [256, 256, 1024], stage=4, block='b')
    x = identity_block(x, 3, [256, 256, 1024], stage=4, block='c')
    x = identity_block(x, 3, [256, 256, 1024], stage=4, block='d')
    x = identity_block(x, 3, [256, 256, 1024], stage=4, block='e')
    x = identity_block(x, 3, [256, 256, 1024], stage=4, block='f')

    # 32,32,1024 -> 16,16,2048
    x = conv_block(x, 3, [512, 512, 2048], stage=5, block='a')
    x = identity_block(x, 3, [512, 512, 2048], stage=5, block='b')
    x = identity_block(x, 3, [512, 512, 2048], stage=5, block='c')

    return x

提取完成特征后,对feature map进行上采样,即解码器:

def centernet_head(x,num_classes):
    x = Dropout(rate=0.5)(x)
    #-------------------------------#
    #   解码器
    #-------------------------------#
    num_filters = 256
    # 16, 16, 2048  ->  32, 32, 256 -> 64, 64, 128 -> 128, 128, 64
    for i in range(3):
        # 进行上采样
        x = Conv2DTranspose(num_filters // pow(2, i), (4, 4), strides=2, use_bias=False, padding='same',
                            kernel_initializer='he_normal',
                            kernel_regularizer=l2(5e-4))(x)
        x = BatchNormalization()(x)
        x = Activation('relu')(x)
    # 最终获得128,128,64的特征层
    # hm header
    y1 = Conv2D(64, 3, padding='same', use_bias=False, kernel_initializer=RandomNormal(stddev=0.02), kernel_regularizer=l2(5e-4))(x)
    y1 = BatchNormalization()(y1)
    y1 = Activation('relu')(y1)
    y1 = Conv2D(num_classes, 1, kernel_initializer=RandomNormal(stddev=0.02), kernel_regularizer=l2(5e-4), activation='sigmoid')(y1)

    # wh header
    y2 = Conv2D(64, 3, padding='same', use_bias=False, kernel_initializer=RandomNormal(stddev=0.02), kernel_regularizer=l2(5e-4))(x)
    y2 = BatchNormalization()(y2)
    y2 = Activation('relu')(y2)
    y2 = Conv2D(2, 1, kernel_initializer=RandomNormal(stddev=0.02), kernel_regularizer=l2(5e-4))(y2)

    # reg header
    y3 = Conv2D(64, 3, padding='same', use_bias=False, kernel_initializer=RandomNormal(stddev=0.02), kernel_regularizer=l2(5e-4))(x)
    y3 = BatchNormalization()(y3)
    y3 = Activation('relu')(y3)
    y3 = Conv2D(2, 1, kernel_initializer=RandomNormal(stddev=0.02), kernel_regularizer=l2(5e-4))(y3)
    return y1, y2, y3

Loss Function

def loss(args):
    #   hm_pred:热力图的预测值       (batch_size, 128, 128, num_classes)
    #   wh_pred:宽高的预测值         (batch_size, 128, 128, 2)
    #   reg_pred:中心坐标偏移预测值  (batch_size, 128, 128, 2)
    #   hm_true:热力图的真实值       (batch_size, 128, 128, num_classes)
    #   wh_true:宽高的真实值         (batch_size, max_objects, 2)
    #   reg_true:中心坐标偏移真实值  (batch_size, max_objects, 2)
    #   reg_mask:真实值的mask        (batch_size, max_objects)
    #   indices:真实值对应的坐标     (batch_size, max_objects)
    hm_pred, wh_pred, reg_pred, hm_true, wh_true, reg_true, reg_mask, indices = args
    hm_loss = focal_loss(hm_pred, hm_true)
    wh_loss = 0.1 * reg_l1_loss(wh_pred, wh_true, indices, reg_mask)
    reg_loss = reg_l1_loss(reg_pred, reg_true, indices, reg_mask)
    total_loss = hm_loss + wh_loss + reg_loss
    # total_loss = tf.Print(total_loss,[hm_loss,wh_loss,reg_loss])
    return total_loss

focal loss

def focal_loss(hm_pred, hm_true):
    #   找到每张图片的正样本和负样本
    #   一个真实框对应一个正样本
    #   除去正样本的特征点,其余为负样本
    pos_mask = tf.cast(tf.equal(hm_true, 1), tf.float32)
    #   正样本特征点附近的负样本的权值更小一些
    neg_mask = tf.cast(tf.less(hm_true, 1), tf.float32)
    neg_weights = tf.pow(1 - hm_true, 4)
    pos_loss = -tf.math.log(tf.clip_by_value(hm_pred, 1e-6, 1.)) * tf.pow(1 - hm_pred, 2) * pos_mask
    neg_loss = -tf.math.log(tf.clip_by_value(1 - hm_pred, 1e-6, 1.)) * tf.pow(hm_pred, 2) * neg_weights * neg_mask

    num_pos = tf.reduce_sum(pos_mask)
    pos_loss = tf.reduce_sum(pos_loss)
    neg_loss = tf.reduce_sum(neg_loss)
    cls_loss = tf.cond(tf.greater(num_pos, 0), lambda: (pos_loss + neg_loss) / num_pos, lambda: neg_loss)
    return cls_loss

L1 loss

def reg_l1_loss(y_pred, y_true, indices, mask):
    #-------------------------------------------------------------------------#
    #   获得batch_size和num_classes
    #-------------------------------------------------------------------------#
    b, c = tf.shape(y_pred)[0], tf.shape(y_pred)[-1]
    k = tf.shape(indices)[1]

    y_pred = tf.reshape(y_pred, (b, -1, c))
    length = tf.shape(y_pred)[1]
    indices = tf.cast(indices, tf.int32)

    #-------------------------------------------------------------------------#
    #   利用序号取出预测结果中,和真实框相同的特征点的部分
    #-------------------------------------------------------------------------#
    batch_idx = tf.expand_dims(tf.range(0, b), 1)
    batch_idx = tf.tile(batch_idx, (1, k))
    full_indices = (tf.reshape(batch_idx, [-1]) * tf.cast(length, tf.int32) +
                    tf.reshape(indices, [-1]))

    y_pred = tf.gather(tf.reshape(y_pred, [-1,c]),full_indices)
    y_pred = tf.reshape(y_pred, [b, -1, c])

    mask = tf.tile(tf.expand_dims(mask, axis=-1), (1, 1, 2))
    #-------------------------------------------------------------------------#
    #   求取l1损失值
    #-------------------------------------------------------------------------#
    total_loss = tf.reduce_sum(tf.abs(y_true * mask - y_pred * mask))
    reg_loss = total_loss / (tf.reduce_sum(mask) + 1e-4)
    return reg_loss

总结

训练工作中的数据集,数据规模大概有4000多张,测试了几张照片,对比yolov4的效果,Centernet的表现不是特别地好。可能由于数据样本少或者是类别比例不平衡导致,但是在yolov4中训练能够得到不错的效果。

参考

  1. 目标检测网络CenterNet详解(四)
  2. 扔掉anchor!真正的CenterNet
  3. centernet论文与代码剖析

猜你喜欢

转载自blog.csdn.net/u012655441/article/details/121395058