【3D目标检测】VoxelNet: End-to-End Learning for Point Cloud Based 3D Object Detection

概述

首先,本文是基于点云,并且将点云处理成体素的3D目标检测网络。
本文提出了一种基于体素的特征提取方法,将点云中点的信息与体素代表的局部信息融合,最终得到更具有表征能力的特征,该特征经过3D卷积用于RPN网络中得到目标检测的结果。
提出动机与贡献:

  1. 以往的基于体素的方法都是手工设计体素的特征,然后使用3D卷积处理,本文借鉴PointNet的方法,提出一种体素特征编码方法VFE,实现端到端。
  2. 采用一种简单的偏航角编码方法,也就是直接回归偏航角,不做复杂的偏航角编码。

细节

网络结构

主要包含三个部分,首先是特征学习网络,中间的卷积层以及最后的RPN网络用于目标检测结果的生成。
在这里插入图片描述

特征学习网络

首先将点云均匀地划分为3D体素,接着是grouping将点云分配到所属的体素中。由于点云本身的稀疏性,大多数的体素内都是没有点的,剩下部分体素内点的数量也是不定的。接下来是Sampling,对于点数不足 T T T的体素添加0,对于那些点数超过 T T T的体素,我们采样 T T T个点,一方面减小计算量,另一方面使得点云的密度分布均匀一点。(有点暴力啊)
注:上述操作完成之后,数据的shape就是 ( N , T , C ) (N,T,C) (N,T,C)了,其中N表示非空体素的个数,T是体素内的点数,C是每个点对应的特征数。

Voxel Feature Encoding(VFE):这个部分解决问题是的是怎样对体素进行特征提取。首先找到体素内的质心,坐标为体素内所有点坐标的平均值,然后给体素内的所有点添加三个额外的特征,他们是当前点距离质心的相对坐标差( Δ x , Δ y , Δ z \Delta x,\Delta y,\Delta z Δx,Δy,Δz)。因此,点的shape变为 ( N , T , C + 3 ) (N,T,C+3) (N,T,C+3)了。接下来就是通过全连接层提取点的特征得到 ( N , T , C 1 ) (N,T,C_1) (N,T,C1)了,再往后就是类似于残差的过程,对上一步得到的特征沿着点数方向做max pool得到全局特征,然后再将这个特征concat到所有上一步得到的特征中,得到 ( N , T , 2 ∗ C 1 ) (N,T,2*C_1) (N,T,2C1)了(类似于pointNet的分割头呀)
在这里插入图片描述
堆叠多次VFE模块得到更丰富的特征表示,之后经过一个全连接+一个max pool得到最终的全局点云特征(N,1,128),其中N是非空体素的数量。
稀疏张量表示:将N个点的特征映射回原来3D体素表示中,得到一个很稀疏的4D张量。

卷积层

使用3D卷积处理来自特征学习网络的稀疏张量,得到shape是 ( 64 , 2 , x , y ) (64,2,x,y) (64,2,x,y),因为KITTI等数据集的检测任务中,物体没有在3D空间中的高度方向进行堆叠,没有出现一个车在另一个车的上方这种情况,所以我们直接将他处理成 ( 64 ∗ 2 , x , y ) (64*2,x,y) (642,x,y)(变成鸟瞰图),这样的话RPN层的设计就会简单很多并且anchor的数量也会少很多,并且还能添加高度方向上的感受野。

RPN网络

这部分说是RPN,但感觉上好像就是单阶段目标检测算法的处理方式,根据特征图直接得到anchor的分类和回归结果(7个参数,6个坐标回归参数以及一个角度回归参数)。

注:在3D世界中,每个类别的物体大小相对固定,所以直接使用了基于KITTI数据集上每个类别的平均长宽高作为anchor大小。也就是每个类别一个anchor。
在这里插入图片描述

损失函数

这部分的损失函数其实和faster rcnn的损失函数一样,正负样本的损失以及边界框回归参数的损失相加就是总损失。这里的分类损失用得是交叉熵损失,回归损失用的是SmoothL1损失。
在这里插入图片描述

简单实现

主要参考:https://github.com/skyhehe123/VoxelNet-pytorch

注1:代码就是大佬的,我只是将网络结构和loss两部分拿了出来,单独测试了一下,并且加了点比较详细的备注。但是其中有一个部分代码我实在没看懂,也跑不通,就注释掉了,不影响数据的shape,但是不能真实使用,要是有大佬看懂了跑通了千万留言教教我 呜呜呜

注2:实现的分类貌似是2分类的,我也尝试改一下的,但是发现比较麻烦(主要是loss部分,还是我菜了),后面就改回来了

网络结构:

import torch.nn as nn
import torch.nn.functional as F
import torch
from torch.autograd import Variable
from config import config as cfg




# conv2d + bn + relu
class Conv2dBlock(nn.Module):

    def __init__(self, in_channels, out_channels, k, s, p, activation=True, batch_norm=True):
        super(Conv2dBlock, self).__init__()
        self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=k, stride=s, padding=p)
        if batch_norm:
            self.bn = nn.BatchNorm2d(out_channels)
        else:
            self.bn = None
        self.activation = activation

    def forward(self, x):
        x = self.conv(x)
        if self.bn is not None:
            x = self.bn(x)
        if self.activation:
            return F.relu(x, inplace=True)
        else:
            return x


# conv3d + bn + relu
class Conv3dBlock(nn.Module):

    def __init__(self, in_channels, out_channels, k, s, p, batch_norm=True):
        super(Conv3dBlock, self).__init__()
        self.conv = nn.Conv3d(in_channels, out_channels, kernel_size=k, stride=s, padding=p)
        if batch_norm:
            self.bn = nn.BatchNorm3d(out_channels)
        else:
            self.bn = None

    def forward(self, x):
        x = self.conv(x)
        if self.bn is not None:
            x = self.bn(x)

        return F.relu(x, inplace=True)


# Fully Connected Network
class FCN(nn.Module):

    def __init__(self, cin, cout):
        super(FCN, self).__init__()
        self.cout = cout
        self.linear = nn.Linear(cin, cout)
        self.bn = nn.BatchNorm1d(cout)

    def forward(self, x):
        # KK is the stacked k across batch
        kk, t, _ = x.shape
        x = self.linear(x.view(kk * t, -1))
        x = F.relu(self.bn(x))
        return x.view(kk, t, -1)


# Voxel Feature Encoding layer
class VFE(nn.Module):

    def __init__(self, cin, cout):
        super(VFE, self).__init__()
        assert cout % 2 == 0
        self.units = cout // 2
        self.fcn = FCN(cin, self.units)

    def forward(self, x, mask):
        # 点级的特征提取 (N, T, cin)->(N, T, cout/2)
        pwf = self.fcn(x)

        # 局部特征提取:将n个点聚合成一个点 (N, T, cout/2)->(N, cout/2)->(N, 1, cout/2)->(N, T, cout/2)
        # 其中max函数是在第1个维度上取最大值,所有第一个维度消失了
        # unsqueeze函数在不改变数据的情况下,在指定位置上添加一个维度上去
        # repeat(x,y,z)表示在第0个维度重复x次,依次类推;函数指定的维度和源数据维度不一致时会广播
        laf = torch.max(pwf, 1)[0].unsqueeze(1).repeat(1, cfg.T, 1)
        # 点级特征和局部特征concat
        # (N, T, cout / 2)->(N, T, cout)
        pwcf = torch.cat((pwf, laf), dim=2)
        # apply mask 对于没有特征的点,强制特征继续保持0
        # (N, T)->(N, T,1)->(N, T, cout)
        mask = mask.unsqueeze(2).repeat(1, 1, self.units * 2)
        # (N, T, cout)
        pwcf = pwcf * mask.float()

        return pwcf


# Stacked Voxel Feature Encoding
class SVFE(nn.Module):

    def __init__(self):
        super(SVFE, self).__init__()
        self.vfe_1 = VFE(7, 32)
        self.vfe_2 = VFE(32, 128)
        self.fcn = FCN(128, 128)

    def forward(self, x):
        # 这一步做的是拿到体素中所有点对应特征的最大值,看看是不是0(也就是获得哪些点是有特征的哪些点是没有特征的)
        # torch.max(x,2) 返回两个元组,分别是第二个维度上的最大值以及他们对应的索引
        # torch.ne(x,y) 用于比较x与y中的每一个元素(会广播),如果对应元素相同返回fasle,不同返回true
        # mask (N, T)
        mask = torch.ne(torch.max(x, 2)[0], 0)
        # (N, T, 7)->(N, T, 32)
        x = self.vfe_1(x, mask)
        # (N, T, 32)->(N, T, 128)
        x = self.vfe_2(x, mask)
        # (N, T, 128)->(N, T, 128)
        x = self.fcn(x)
        # 最终的max pool得到全局的点云信息 (N, T, 128)->(N, 128)
        x = torch.max(x, 1)[0]
        return x


# Convolutional Middle Layer
class CML(nn.Module):
    def __init__(self):
        super(CML, self).__init__()
        self.conv3d_1 = Conv3dBlock(128, 64, 3, s=(2, 1, 1), p=(1, 1, 1))
        self.conv3d_2 = Conv3dBlock(64, 64, 3, s=(1, 1, 1), p=(0, 1, 1))
        self.conv3d_3 = Conv3dBlock(64, 64, 3, s=(2, 1, 1), p=(1, 1, 1))

    def forward(self, x):
        # 3D卷积中的尺寸计算公式:(D_in+2padding-dilation(kernel_size-1)-1)/stride+1,默认dilation是1的
        # (N, 128, D, H, W)->(N, 64, D/2, H, W)
        x = self.conv3d_1(x)
        # (N, 64, D/2, H, W)->(N, 64, D/2-2, H, W)
        x = self.conv3d_2(x)
        # (N, 64, D/2-2, H, W)->(N, 64,(D/2-2)/2, H, W)
        x = self.conv3d_3(x)
        return x


# Region Proposal Network
class RPN(nn.Module):
    def __init__(self):
        super(RPN, self).__init__()
        self.block_1 = [Conv2dBlock(128, 128, 3, 2, 1)]
        self.block_1 += [Conv2dBlock(128, 128, 3, 1, 1) for _ in range(3)]
        self.block_1 = nn.Sequential(*self.block_1)

        self.block_2 = [Conv2dBlock(128, 128, 3, 2, 1)]
        self.block_2 += [Conv2dBlock(128, 128, 3, 1, 1) for _ in range(5)]
        self.block_2 = nn.Sequential(*self.block_2)

        self.block_3 = [Conv2dBlock(128, 256, 3, 2, 1)]
        self.block_3 += [Conv2dBlock(256, 256, 3, 1, 1) for _ in range(5)]
        self.block_3 = nn.Sequential(*self.block_3)

        self.deconv_1 = nn.Sequential(nn.ConvTranspose2d(256, 256, 4, 4, 0), nn.BatchNorm2d(256))
        self.deconv_2 = nn.Sequential(nn.ConvTranspose2d(128, 256, 2, 2, 0), nn.BatchNorm2d(256))
        self.deconv_3 = nn.Sequential(nn.ConvTranspose2d(128, 256, 1, 1, 0), nn.BatchNorm2d(256))

        self.score_head = Conv2dBlock(768, cfg.anchors_per_position, 1, 1, 0, activation=False, batch_norm=False)
        self.reg_head = Conv2dBlock(768, 7 * cfg.anchors_per_position, 1, 1, 0, activation=False, batch_norm=False)

    def forward(self, x):
        # (N, 128, H, W)->(N, 128, H/2, W/2)
        x = self.block_1(x)
        x_skip_1 = x
        # (N, 128, H/2, W/2)->(N, 128, H/4, W/4)
        x = self.block_2(x)
        x_skip_2 = x
        # (N, 128, H/4, W/4)->(N, 256, H/8, W/8)
        x = self.block_3(x)

        # 反卷积(转置卷积)计算公式:
        # 输出大小 = (输入大小 − 1) * Stride + Filter - 2 * Padding H/2-4+4
        # (N, 256, H/8, W/8)->(N, 256, H/2, W/2)
        x_0 = self.deconv_1(x)
        # (N, 128, H/4, W/4)->(N, 256, H/2, W/2)
        x_1 = self.deconv_2(x_skip_2)
        # (N, 128, H/2, W/2)->(N, 256, H/2, W/2)
        x_2 = self.deconv_3(x_skip_1)

        # (N, 3*256, H/2, W/2)
        x = torch.cat((x_0, x_1, x_2), 1)
        # (N, anchors_per_position, H/2, W/2),(N, 7*anchors_per_position, H/2, W/2)
        return self.score_head(x), self.reg_head(x)


class VoxelNet(nn.Module):

    def __init__(self):
        super(VoxelNet, self).__init__()
        self.svfe = SVFE()
        self.cml = CML()
        self.rpn = RPN()

    # sparse_features:(N, 128),coords:(NUM,3)
    def voxel_indexing(self, sparse_features, coords):
        dim = sparse_features.shape[-1]
        device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        # (128,2,10,400,352)
        dense_feature = Variable(torch.zeros(dim, cfg.N, cfg.D, cfg.H, cfg.W).to(device))
        # 这步是将我们通过堆叠VFE操作得到的稀疏特征映射到4D张量中,得到稀疏的4D张量 但是我着实看不懂,还疯狂报错
        # dense_feature[:, coords[:, 0], coords[:, 1], coords[:, 2], coords[:, 3]] = sparse_features

        return dense_feature.transpose(0, 1)

    # sparse_features是所有体素对应的点特征集合,shape是(N, T, 4+3),这里的N是体素个数,T是体素中的最大点数,4+3是每个点对应的4个原始特征+3个相对于体素点质心的坐标偏移
    # voxel_coords的shape是(NUM,3)其中3对应的是(D, H, W),这里的NUM是点云的总数
    def forward(self, voxel_features, voxel_coords):
        # 特征学习网络=堆叠多次VFE模块得到全局点云特征+将这个全局点云特征使用稀疏的4D张量表示,方便后续3D卷积特征提取
        # (N, T, 4+3)->(N, 128)
        vwfs = self.svfe(voxel_features)
        # (N, 128)->(batch_size, 128, D, H, W)
        vwfs = self.voxel_indexing(vwfs, voxel_coords)

        # 3D卷积网络
        # (batch_size, 128, D, H, W)->(batch_size, 64,(D/2-2)/2, H, W)
        cml_out = self.cml(vwfs)

        # RPN网络
        # 合并特征维度和深度维度得到鸟瞰图, 对鸟瞰图使用2D卷积得到bbox对应的分类得分和bbox回归参数
        # (batch_size, 64,(D/2-2)/2, H, W)->(N, 64*(D/2-2)/2, H, W)->(batch_size, anchors_per_position, H/2, W/2),(N, 7*anchors_per_position, H/2, W/2)
        psm, rm = self.rpn(cml_out.view(cfg.N, -1, cfg.H, cfg.W))

        return psm, rm

def main():
    N, T=2,35 # 体素个数以及每个体素中的最大点数
    voxel_features=torch.randn((N, T, 4+3))
    voxel_coords=torch.randint(0,5,(100,3))
    net=VoxelNet()
    psm, rm =net(voxel_features, voxel_coords)
    print(psm.shape, rm.shape)


if __name__ == '__main__':
    main()

配置文件:

import math
import numpy as np

class config:

    # classes
    class_list = ['Car', 'Van']

    # batch size
    N=2

    # maxiumum number of points per voxel
    T=35

    # voxel size
    vd = 0.4
    vh = 0.2
    vw = 0.2

    # points cloud range
    xrange = (0, 70.4)
    yrange = (-40, 40)
    zrange = (-3, 1)

    # voxel grid
    W = math.ceil((xrange[1] - xrange[0]) / vw)
    H = math.ceil((yrange[1] - yrange[0]) / vh)
    D = math.ceil((zrange[1] - zrange[0]) / vd)

    # iou threshold
    pos_threshold = 0.6
    neg_threshold = 0.45

    #   anchors: (200, 176, 2, 7) x y z h w l r
    x = np.linspace(xrange[0]+vw, xrange[1]-vw, W//2)
    y = np.linspace(yrange[0]+vh, yrange[1]-vh, H//2)
    cx, cy = np.meshgrid(x, y)
    # all is (w, l, 2)
    cx = np.tile(cx[..., np.newaxis], 2)
    cy = np.tile(cy[..., np.newaxis], 2)
    cz = np.ones_like(cx) * -1.0
    w = np.ones_like(cx) * 1.6
    l = np.ones_like(cx) * 3.9
    h = np.ones_like(cx) * 1.56
    r = np.ones_like(cx)
    r[..., 0] = 0
    r[..., 1] = np.pi/2
    anchors = np.stack([cx, cy, cz, h, w, l, r], axis=-1)

    anchors_per_position = 2

    # non-maximum suppression
    nms_threshold = 0.1
    score_threshold = 0.96

损失函数:

import torch
import torch.nn as nn
import torch.nn.functional as F

class VoxelLoss(nn.Module):
    def __init__(self, alpha, beta):
        super(VoxelLoss, self).__init__()
        self.smoothl1loss = nn.SmoothL1Loss(size_average=False)
        self.alpha = alpha
        self.beta = beta

    # psm是bbox对应的分类得分,(N, anchors_per_position, H/2, W/2)
    # rm是bbox回归参数,(N, 7*anchors_per_position, H/2, W/2)
    # pos_equal_one是正样本01图,(N,H/2,W/2,anchors_per_position)
    # neg_equal_one是负样本01图,(N,H/2,W/2,anchors_per_position)
    # targets是真实的回归参数,(N,H/2,W/2,7*anchors_per_position)

    def forward(self, rm, psm, pos_equal_one, neg_equal_one, targets):

        # (N, anchors_per_position, H/2, W/2)->(N, H/2, W/2,anchors_per_position)
        p_pos = F.sigmoid(psm.permute(0,2,3,1))
        # (N, 7*anchors_per_position, H/2, W/2)->(N, H/2, W/2,7*anchors_per_position)
        # contiguous方法使得rm存储的形状也进行变化,方便后面的view方法
        rm = rm.permute(0,2,3,1).contiguous()
        # (N, H / 2, W / 2, 7 * anchors_per_position)->(N, H/2, W/2,anchors_per_position,7)
        rm = rm.view(rm.size(0),rm.size(1),rm.size(2),-1,7)
        # (N,H/2,W/2,7*anchors_per_position)->(N,H/2,W/2,anchors_per_position,7)
        targets = targets.view(targets.size(0),targets.size(1),targets.size(2),-1,7)
        # (N,anchors_per_positionH/2,W/2,anchors_per_position)->(N,H/2,W/2,anchors_per_position,7)
        # pos_equal_one.dim()获取pos_equal_one的维度,这里得到的是4
        # expand函数用于维数的拓展,-1表示当前维度维数不变
        pos_equal_one_for_reg = pos_equal_one.unsqueeze(pos_equal_one.dim()).expand(-1,-1,-1,-1,7)
        
        # 只有正样本才有边界框回归损失
        rm_pos = rm * pos_equal_one_for_reg
        targets_pos = targets * pos_equal_one_for_reg
        reg_loss = self.smoothl1loss(rm_pos, targets_pos)
        reg_loss = reg_loss / (pos_equal_one.sum() + 1e-6)

        # 样本的分类损失-(1/n)*y*log(y^)
        cls_pos_loss = -pos_equal_one * torch.log(p_pos + 1e-6)
        cls_pos_loss = cls_pos_loss.sum() / (pos_equal_one.sum() + 1e-6)

        cls_neg_loss = -neg_equal_one * torch.log(1 - p_pos + 1e-6)
        cls_neg_loss = cls_neg_loss.sum() / (neg_equal_one.sum() + 1e-6)
        conf_loss = self.alpha * cls_pos_loss + self.beta * cls_neg_loss
        return conf_loss, reg_loss
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值