博客参考:论文阅读:VoxelNet(3D-detection)+代码复现_手写代码3ddetection_Little_sky_jty的博客-CSDN博客
Voxelnet 模型构建
1,init方法初始化模型,包括多级体素特征编码模块,3D卷积特征提取模块,RPN检测头回归分类模块
2,forward方法训练模型,传入参数:
sparse_features(k,t,7)体素特征,来源于数据集构建模块,k个体素,每个体素t个点,每个点7个特征信息(x,y,z,r,x',y,'z')
coords(k,3):所有体素位置信息
1,通过SVFE对于体素特征进行编码,编码过程聚合各个单独点与所在体素局部信息,最后进行最大池化,代表每个体素特征信息(k,128)
2,稀疏特征浓密化,也就是对于每个位置的体素,赋予其特征信息,构成(b,c,d,h,w)张量
3,使用3D卷积对于深度方向进行压缩,并减少通道数(b,64,2,400,352)
4,合并通道和深度方向,此时可看做2维图像进行2D卷积处理(b,(64x2),400,352),
处理过程常用的特征金字塔构建形式,拓展通道数,减少尺寸,最后上采样聚合多层特征图得到全局特征,
使用全局特征进行分类预测(b,2,400,352)这里1x1卷积核图像尺寸不变
使用全局信息进行回归判断(b,2*7,400,452)也是1x1卷积图像尺寸不变
网络的输入是 batch×N×4 ,batchxNx4batch×N×4的数据格式,即是batch(代码中设置为1,即每次值输入一个Bin文件),一个bin文件中的点云数量N、每个点云的维度(x,y,z,r)。
我们知道该层的处理是首先分区,输入点云经过裁剪后的D×W×H=4×80×70.4,实际上的范围可以在config.py文件中看到:
# 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)
D×W×H=4×80×70.4每个bin文件分成了
其中xrange(0-70.4)
yrange(-40-40)
zrange(-3 - 1)
cfg.W = 70.2/0.2=352 x轴方向352个格子
cfg.H = 80/0.2=400 y方向400个格子
cfg.D = 4/0.2=20 z方向20个格子
每个格子中最大包含点的个数为35
处理后为K个空体素则处理后表示为 (K,35,4)
融合局部信息局部voxel的点云的均值,再通过残差的方式加入到每个点的信息中 (K,35,7)
X轴:(0,70.4);Y轴:(−40,40);然后经过partion,代码中把每一个voxel的最大的点云量设置为T=35;所以我们假设一共有k个非空的体素,经过这样处理后我们将点云表示为K×T×4。
接下来就是对全局信息和局部信息的综合。通过求每一个局部voxel的点云的均值,再通过残差的方式加入到每个点的信息中,即会增加三个局部信息的维度:K × T × 7 。
一个简单的FCN层
普通FCN是(12,30) -> (12,30) * (30,128) == (12,128)改变通道的作用,
也可以拉伸二维成1位 (12,5) - (1,12*5,) * (60,80) ==(1,80)映射到分类个数
这里应用在VoxelNet中,SVFE中的VFE模块,提取体素特征,改变通道数
处理[b,k,t,7]体素特征到[b,k,t,128]
思路流程就是,分清楚那个是通道,是需要变化的,那些是不需要变化的
对于不需要变化的给他乘成kt的形式,(kt , 7) * (7,128)==(kt , 128)
再进行一个还原 (kt , 128) .view(k,t,128)即可。
简单而言,体素特征编码就是将(k,t,7)->(k,t,32)但是转化过程中不仅仅使用FCN改变通道数,这样直接改变获取的还是各个点的单独点特征信息,没有与局部特征信息融合。
这里提出的融合策略是,首先将(k,t,7)->(k,t,16)通过fcn得到每个点16个特征,之后获取每个体素中所有点16个特征中每一位最大值,一个16位,比如第一个特征,在35个点中取最大的,第二个特征在35个点取最大的。维度变化叫(k,t,16) -> (k,16)。获得的这16个特征就相当于35个点中16个位置上最明显的16个特征,这个点能够代表这35个点,把这个点重复35次,之后与之前每个点16个单独特征cat起来,相当于对于35个点自己存在的16个单独特征的基础上添加局部特征,(k,16)->(k,1,16)->(k,t,16)。up(k,t,16) cat down(k,t,16) = (k,t,32)。
使用多次VFE操作对于当前点云集合进行特征提取以及融合单个点和局部体素特征信息。同时在每次融合的过程中会使用mask对于那些点特征没0大进行忽略。(k,t,7)->(k,t,32)->(k,t,128)->(k,t,128)->(k,128)。最后使用了一个max来获取这个体素的全局信息,以一个包含128特征的点代表这个体素。
class FCN(nn.Module):
def __init__(self,cin,cout):
super(FCN, self).__init__()
self.cout = cout
self.cin = cin
self.linear = nn.Linear(cin,cout)
self.bn = nn.BatchNorm1d(cout)
def forward(self,x):
k,t,_ = x.shape
x = self.linear(x.view(k*t,-1))#(3,4,7) ->(12,7) * (7,128) ->(12,128)
x = self.bn(x)
x = F.relu(x)
return x.view(k,t,-1) #(12,128) ->(3,4,128)
#(k,16)->(k,1,16)->(k,t,16)。up(k,t,16) cat down(k,t,16) = (k,t,32)
class VFE(nn.Module):
def __init__(self,cin,cout):
super(VFE, self).__init__()
#这里是对2求余确保能够整除
assert cout % 2 == 0
self.unit = cout//2
#这里我们的线性层已经进行了处理,(3,4,7)->(3,4,32)改变通道数了直接
self.linear = FCN(cin,self.unit)
self.relu = nn.ReLU()
self.bn = nn.BatchNorm1d(self.unit)
def forward(self,x,mask):
k,t,d = x.size()
#up(k,t,16)所以这里直接放原始数据就好
up = self.linear(x)
#(k,t,16) -> (k,16)->(k,1,16)->(k,t,16)获取t个点的每一位特征中最大值共16位,重复t次
down = torch.max(up,1)[0].unsqueeze(1).repeat(1,cfg.T,1)
#up(k,t,16) cat down(k,t,16) = (k,t,32)融合t个点一半单独特征一半局部特征
pwcf = torch.concat((up,down),dim=2)
# mask = [k,t] [3,35]->[3,35,cout] mask选择特征值大于0的点
mask = mask.unsqueeze(2).repeat(1,1,self.unit*2)
pwcf = pwcf * mask.float()
#mask = mask.unsqueeze(2).repeat(1,1,self.unit*2)
#pwcf = pwcf*mask.float()
return pwcf
# 第一步1 堆叠体素特征编码吗 Stacked Voxel Feature Encoding
#网络输入形式 batch×N×4
#D×W×H=4×80×70.4每个bin文件分成了 其中xrange(0-70.4) yrange(-40-40) zrange(-3 - 1)
#每个体素网格尺寸 vd = 0.4 vh = 0.2,vw = 0.2 70.2/0.2=352 80/0.2=400 4/0.2=20
class SVFE(nn.Module):
def __init__(self):
super(SVFE, self).__init__()
#[B, K, T, 7];在生成大小为[B, K, T, 128]经过两个VFE层
self.vfe_1 = VFE(7,32)
self.vfe_2 = VFE(32,128)
self.fcn = FCN(128,128)
def forward(self, x):
#torch.ne 判断元素不相等[b,k,t,7]就是判断该体素包含点云的点不为空嘛
mask = torch.ne(torch.max(x,2)[0], 0)
x = self.vfe_1(x, mask)
x = self.vfe_2(x, mask)
x = self.fcn(x)
# element-wise max pooling
x = torch.max(x,1)[0]
return x
VEF的具体实现如图所示:其思想就是从体素特征中获得各个点的特征,之后进行一个特征聚合,如同残差操作将聚合后的局部信息与原始点特征信息结合起来。
def voxel_indexing(self, sparse_features, coords):
'''
#vwfs:体素特征编码(k,128),k个体素,每个包含128特征 voxel_coords:所有点在空间中对应的体素位置(k,4),k个体素位置和其归属bt
:param sparse_features: 体素特征编码(k,128)
:param coords: 在空间中对应的体素位置(k,4) (n,(bt_id,x,y,z))-->(1,2,200,150)这里一共四个参数,每个体素归属的batch和x,y,z坐标位置
:return:带有位置信息以及特征信息的(两个batch)样本的体素特征信息(b,C,Z,Y,X)
'''
# 128
dim = sparse_features.shape[-1]
#D×W×H=4×80×70.4每个bin文件分成了
#(特征维度, batchsize, 深度, 高度, 宽度)
#(128, 2, 10, 400, 352) 2代表一个batch有两个样本,要区分不同样本,sparse_features是一个batch所有体素特征
#初始化浓密特征,目的将每个体素信息和特征信息组合起来(128,bt归属,z,w,h,w)
dense_feature = Variable(torch.zeros(dim, cfg.N, cfg.D, cfg.H, cfg.W).cuda())
#dense_feature[:, coords[:,0], coords[:,1], coords[:,2], coords[:,3]]= sparse_features
#dense_feature[128 , (k个)b , (k个)z, (k个)y, (k个)x] = [128 ,k]给以每个具体包含位置信息的体素添加其特征信息
#比如我k=1时,代表第一个体素
# dense_feature(128,coords[1,0] -- 第1个体素bt归属,coords[1,1]-- 第1个体素z,coords[1,2],coords[1,3])
#给予每个对应位置的体素,对应的128维度的特征信息
dense_feature[:, coords[:, 0], coords[:, 1], coords[:, 2], coords[:, 3]] = sparse_features.transpose(0,1)
#[128,2,10,400,352]->[2,128,10,400,352] (C,B,D,W,H) −> (B,C,D,H,W)
return dense_feature.transpose(0, 1)
总结一下这一层,这里做的操作就是对每一个voxel的特征进行提取,但是为了不操作空的呢,就只对非空的进行提取;同时VFE层采用了局部和全局坐标结合的方式特取特征;同时又为了解决有的voxel的点不足T个,所有最后采用最大池化表示了一个voxel的特征。
# Convolutional Middle Layer
#(B,C,D,H,W) : [1,128,10,400,352]
#(B,D,H,W,C)−>(B,C,D,H,W)
#[2,128,10,400,352]
class CML(nn.Module):
def __init__(self):
super(CML, self).__init__()
#[1,128,10,400,352]->[1,64,5,400,352]
self.conv3d_1 = Conv3d(128, 64, 3, s=(2, 1, 1), p=(1, 1, 1))
#[1,64,10,400,352]->[1,64,3,400,352]
self.conv3d_2 = Conv3d(64, 64, 3, s=(1, 1, 1), p=(0, 1, 1))
#[1,64,4,400,352]->[1,64,2,400,352]
self.conv3d_3 = Conv3d(64, 64, 3, s=(2, 1, 1), p=(1, 1, 1))
def forward(self, x):
x = self.conv3d_1(x)
x = self.conv3d_2(x)
x = self.conv3d_3(x)
return x
# Region Proposal Network
#(2,128,400,352)
class RPN(nn.Module):
def __init__(self):
super(RPN, self).__init__()
#第一次下采样,通道数不变,卷积核3,步长为2,宽高减小一半 h/2 w/2
self.block_1 = [Conv2d(128, 128, 3, 2, 1)]
#再来三次,通道不变,宽高不变
self.block_1 += [Conv2d(128, 128, 3, 1, 1) for _ in range(3)]
self.block_1 = nn.Sequential(*self.block_1)
#第二次下采样,通道不变,积核3,步长为2,宽高减小一半h/2 w/2
#再来五次,通道不变,宽高不变
self.block_2 = [Conv2d(128, 128, 3, 2, 1)]
self.block_2 += [Conv2d(128, 128, 3, 1, 1) for _ in range(5)]
self.block_2 = nn.Sequential(*self.block_2)
#第三次下采样,通道128->256,卷积核3,步长2,尺寸减半
self.block_3 = [Conv2d(128, 256, 3, 2, 1)]
#重复5次,通道不变,尺寸不变
self.block_3 += [nn.Conv2d(256, 256, 3, 1, 1) for _ in range(5)]
self.block_3 = nn.Sequential(*self.block_3)
#利用进行上采样为聚合做准备,通道数统一到256,上采样4倍,2倍,1倍,尺寸统一到w/2
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))
#检测头,一边是分类,一边是回归 分类到2类,回归到2*7
self.score_head = Conv2d(768, cfg.anchors_per_position, 1, 1, 0, activation=False, batch_norm=False)
self.reg_head = Conv2d(768, 7 * cfg.anchors_per_position, 1, 1, 0, activation=False, batch_norm=False)
def forward(self,x):
x = self.block_1(x)
x_skip_1 = x
x = self.block_2(x)
x_skip_2 = x
x = self.block_3(x)
x_0 = self.deconv_1(x)
x_1 = self.deconv_2(x_skip_2)
x_2 = self.deconv_3(x_skip_1)
x = torch.cat((x_0,x_1,x_2),1)
return self.score_head(x),self.reg_head(x)
import torch.nn as nn
import torch.nn.functional as F
import torch
from torch.autograd import Variable
from config import config as cfg
import torch.utils.data as data
# conv2d + bn + relu
from data.kitti import KittiDataset, detection_collate
class Conv2d(nn.Module):
def __init__(self,in_channels,out_channels,k,s,p, activation=True, batch_norm=True):
super(Conv2d, 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 Conv3d(nn.Module):
def __init__(self, in_channels, out_channels, k, s, p, batch_norm=True):
super(Conv3d, 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
#1 SVFE调用VFE模块,处理[b,k,t,7]体素特征到[b,k,t,128]
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):
# point-wise feauture
#使用全连接层获取每个点的特征,此时通道数为16
pwf = self.fcn(x)
#locally aggregated feature
#进行聚合获取区域局部特征,聚合16个通道的最大值,最为局部信息,重复16次
laf = torch.max(pwf,1)[0].unsqueeze(1).repeat(1,cfg.T,1)
# point-wise concat feature
#点特征与局部特征堆叠 16+16=32组合成输出通道要求
pwcf = torch.cat((pwf,laf),dim=2)
# apply mask mask = [k,t] [3,35]->[3,35,cout]
mask = mask.unsqueeze(2).repeat(1, 1, self.units * 2)
pwcf = pwcf * mask.float()
return pwcf
# 第一步1 堆叠体素特征编码吗 Stacked Voxel Feature Encoding
#D×W×H=4×80×70.4每个bin文件分成了 其中xrange(0-70.4) yrange(-40-40) zrange(-3 - 1)
#每个体素网格尺寸 vd = 0.4 vh = 0.2,vw = 0.2 70.2/0.2=352 80/0.2=400 4/0.2=20
class SVFE(nn.Module):
def __init__(self):
super(SVFE, self).__init__()
#[ K, T, 7];在生成大小为[K, T, 128]经过两个VFE层
#完成每个单个体素特征编码的工作,一般点单独特征加上一半体素局部特征
self.vfe_1 = VFE(7,32)
self.vfe_2 = VFE(32,128)
self.fcn = FCN(128,128)
def forward(self, x):
#torch.ne 判断元素不相等[k,t,7]就是判断该体素包含的特征不为0.
# 也就是将一个体素中那些点最大特征值特征没0大的点进行忽略[3,35]
#这里7个特征是(x,y,z,r,x',y',z') '代表的是各个点的相对位置
#忽略最大值小于0的点(k,t)
mask = torch.ne(torch.max(x,2)[0], 0)
#[k,t,7]->[k,t,32]
x = self.vfe_1(x, mask)
#[k,t,32]->[k,t,128]
x = self.vfe_2(x, mask)
x = self.fcn(x)
# element-wise max pooling
# 对于一个体素中所有点进行maxpollig选一个点代表这个体素
# [k,128]这个点包含了这个体素所有点的单独信息和完整体素的局部信息,相当于CNN中升通道降尺寸的操作
x = torch.max(x,1)[0]
return x
# Convolutional Middle Layer
#(B,C,D,H,W) : [1,128,10,400,352]
#(B,D,H,W,C)−>(B,C,D,H,W)
#[2,128,10,400,352]
class CML(nn.Module):
def __init__(self):
super(CML, self).__init__()
#[1,128,10,400,352]->[1,64,5,400,352]
self.conv3d_1 = Conv3d(128, 64, 3, s=(2, 1, 1), p=(1, 1, 1))
#[1,64,10,400,352]->[1,64,3,400,352]
self.conv3d_2 = Conv3d(64, 64, 3, s=(1, 1, 1), p=(0, 1, 1))
#[1,64,4,400,352]->[1,64,2,400,352]
self.conv3d_3 = Conv3d(64, 64, 3, s=(2, 1, 1), p=(1, 1, 1))
def forward(self, x):
x = self.conv3d_1(x)
x = self.conv3d_2(x)
x = self.conv3d_3(x)
return x
# Region Proposal Network
#(2,128,400,352)
class RPN(nn.Module):
def __init__(self):
super(RPN, self).__init__()
#第一次下采样,通道数不变,卷积核3,步长为2,宽高减小一半 h/2 w/2
self.block_1 = [Conv2d(128, 128, 3, 2, 1)]
#再来三次,通道不变,宽高不变
self.block_1 += [Conv2d(128, 128, 3, 1, 1) for _ in range(3)]
self.block_1 = nn.Sequential(*self.block_1)
#第二次下采样,通道不变,积核3,步长为2,宽高减小一半h/2 w/2
#再来五次,通道不变,宽高不变
self.block_2 = [Conv2d(128, 128, 3, 2, 1)]
self.block_2 += [Conv2d(128, 128, 3, 1, 1) for _ in range(5)]
self.block_2 = nn.Sequential(*self.block_2)
#第三次下采样,通道128->256,卷积核3,步长2,尺寸减半
self.block_3 = [Conv2d(128, 256, 3, 2, 1)]
#重复5次,通道不变,尺寸不变
self.block_3 += [nn.Conv2d(256, 256, 3, 1, 1) for _ in range(5)]
self.block_3 = nn.Sequential(*self.block_3)
#利用进行上采样为聚合做准备,通道数统一到256,上采样4倍,2倍,1倍,尺寸统一到w/2
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))
#检测头,一边是分类,一边是回归 分类到2类,回归到2*7
self.score_head = Conv2d(768, cfg.anchors_per_position, 1, 1, 0, activation=False, batch_norm=False)
self.reg_head = Conv2d(768, 7 * cfg.anchors_per_position, 1, 1, 0, activation=False, batch_norm=False)
def forward(self,x):
x = self.block_1(x)
x_skip_1 = x
x = self.block_2(x)
x_skip_2 = x
x = self.block_3(x)
x_0 = self.deconv_1(x)
x_1 = self.deconv_2(x_skip_2)
x_2 = self.deconv_3(x_skip_1)
x = torch.cat((x_0,x_1,x_2),1)
return self.score_head(x),self.reg_head(x)
'''
Voxelnet 模型构建
1,init方法初始化模型,包括多级体素特征编码模块,3D卷积特征提取模块,RPN检测头回归分类模块
2,forward方法训练模型,传入参数:
sparse_features(k,t,7)体素特征,来源于数据集构建模块,k个体素,每个体素t个点,每个点7个特征信息(x,y,z,r,x',y,'z')
coords(k,3):所有体素位置信息
1,通过SVFE对于体素特征进行编码,编码过程聚合各个单独点与所在体素局部信息,最后进行最大池化,代表每个体素特征信息(k,128)
2,稀疏特征浓密化,也就是对于每个位置的体素,赋予其特征信息,构成(b,c,d,h,w)张量
3,使用3D卷积对于深度方向进行压缩,并减少通道数(b,64,2,400,352)
4,合并通道和深度方向,此时可看做2维图像进行2D卷积处理(b,(64x2),400,352),
处理过程常用的特征金字塔构建形式,拓展通道数,减少尺寸,最后上采样聚合多层特征图得到全局特征,
使用全局特征进行分类预测(b,2,400,352)这里1x1卷积核图像尺寸不变
使用全局信息进行回归判断(b,2*7,400,452)也是1x1卷积图像尺寸不变
'''
class VoxelNet(nn.Module):
def __init__(self):
super(VoxelNet, self).__init__()
#堆叠体素特征
self.svfe = SVFE()
#3D卷积特征提取
self.cml = CML()
#检测头
self.rpn = RPN()
#作用就是组合所有体素的特征和位置信息
def voxel_indexing(self, sparse_features, coords):
'''
#vwfs:体素特征编码(k,128),k个体素,每个包含128特征 voxel_coords:所有点在空间中对应的体素位置(k,4),k个体素位置和其归属bt
:param sparse_features: 体素特征编码(k,128)
:param coords: 在空间中对应的体素位置(k,4) (n,(bt_id,x,y,z))-->(1,2,200,150)这里一共四个参数,每个体素归属的batch和x,y,z坐标位置
:return:带有位置信息以及特征信息的(两个batch)样本的体素特征信息(b,C,Z,Y,X)
'''
# 128
dim = sparse_features.shape[-1]
#D×W×H=4×80×70.4每个bin文件分成了
#(特征维度, batchsize, 深度, 高度, 宽度)
#(128, 2, 10, 400, 352) 2代表一个batch有两个样本,要区分不同样本,sparse_features是一个batch所有体素特征
#初始化浓密特征,目的将每个体素信息和特征信息组合起来(128,bt归属,z,w,h,w)
dense_feature = Variable(torch.zeros(dim, cfg.N, cfg.D, cfg.H, cfg.W).cuda())
#dense_feature[:, coords[:,0], coords[:,1], coords[:,2], coords[:,3]]= sparse_features
#dense_feature[128 , (k个)b , (k个)z, (k个)y, (k个)x] = [128 ,k]给以每个具体包含位置信息的体素添加其特征信息
#比如我k=1时,代表第一个体素
# dense_feature(128,coords[1,0] -- 第1个体素bt归属,coords[1,1]-- 第1个体素z,coords[1,2],coords[1,3])
#给予每个对应位置的体素,对应的128维度的特征信息
dense_feature[:, coords[:, 0], coords[:, 1], coords[:, 2], coords[:, 3]] = sparse_features.transpose(0,1)
#[128,2,10,400,352]->[2,128,10,400,352] (C,B,D,W,H) −> (B,C,D,H,W)
return dense_feature.transpose(0, 1)
def forward(self, voxel_features, voxel_coords):
#这里是在数据集中getitem得到数据,也就是一个样本的点云数据,没有batch,直接是一个场景样本包含的数据
#(3186,35,7) 一共3186个体素,每个体素35以下的点,每个点7个特征(x,y,z,r,x',y',z')
# feature learning network
#(k,128)
vwfs = self.svfe(voxel_features)
#vwfs:体素特征编码(k,128) voxel_coords:所有点对应的体素坐标(n,3)
vwfs = self.voxel_indexing(vwfs,voxel_coords)
# convolutional middle network
#[1,64,2,400,352]
cml_out = self.cml(vwfs)
# region proposal network
#将深度维度和特征维度融合,相当于拉到一个平面,输出分数和边界回归结果
# merge the depth and feature dim into one, output probability score map and regression map
# cml_out.view(cfg.N,-1,cfg.H, cfg.W) = (2,128(2x64),400,352)
psm,rm = self.rpn(cml_out.view(cfg.N,-1,cfg.H,cfg.W))
#probability score map , regression map
return psm, rm
def main():
#[b,k,t,7]这里t要是35才行一个体素35个点,一个点32个特征
root_path = "D:/python/data/mkitti"
dataset = KittiDataset(cfg=cfg, root=root_path, set='train')
data_loader = data.DataLoader(dataset, batch_size=cfg.N, num_workers=0, collate_fn=detection_collate, shuffle=True, \
pin_memory=False)
batch_iterator = None
epoch_size = len(dataset) // cfg.N
net = VoxelNet()
net.cuda()
for iteration in range(5):
if (not batch_iterator) or (iteration % epoch_size == 0):
# create batch iterator
batch_iterator = iter(data_loader)
voxel_features, voxel_coords, pos_equal_one, neg_equal_one, targets, images, calibs, ids = next(batch_iterator)
voxel_features = Variable(torch.cuda.FloatTensor(voxel_features))#(8874,35,7)
pos_equal_one = Variable(torch.cuda.FloatTensor(pos_equal_one))#(2,200,176,2)
neg_equal_one = Variable(torch.cuda.FloatTensor(neg_equal_one))#(2,200,176,2)
targets = Variable(torch.cuda.FloatTensor(targets))#(2,200,176,14)
psm, rm = net(voxel_features, voxel_coords)
print(psm.size())
print(rm.size())
# x = torch.randn(3,cfg.T,7)
# x_coords = torch.arange(12).reshape(3,4)
# vef = VFE(7,32)
# y = vef(x)
# fcn = FCN(7,32)
# y = fcn(x)
#print(y.size())
#torch.Size([3, 35, 32])
if __name__ == '__main__':
main()
'''
数据集 __getitem__(self, i)方法:
1,取得该图像对应的各个数据路径地址
2,加载映射文件,calib字典,包括内参P,转置矩阵R,外参Tr
3,加载标签文件得到所有目标边界框坐标(n,8,3) 点云数据Lidar(n,4)
4,读入图像,数据增强加过滤
5,处理点云得到(k,t,7)k个体素,每个t个点云,每个点云(x,y,z,t,x',y',z') , (k,3),k个点云的所在位置
6,构建标签
'''
重点去看体素化和体素特征构建。
from __future__ import division
import os
import os.path
import torch.utils.data as data
import utils
from utils import box3d_corner_to_center_batch, anchors_center_to_corner, corner_to_standup_box2d_batch
from data_aug import aug_data
from box_overlaps import bbox_overlaps
import numpy as np
import cv2
from config import config as cfg
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE"
#点云数据一共16列
#第1列 目标类比别(type),共有8种类别
#第2列 截断程度(truncated),表示处于边缘目标的截断程度,取值范围为0~1
#第3列 遮挡程度(occlude),取值为(0,1,2,3)。0表示完全可见
#第4列 观测角度(alpha),取值范围为(-pi, pi)
#第5-8列 二维检测框(bbox),目标二维矩形框坐标,分别对应left、top、right、bottom,即左上(xy)和右下的坐标(xy)。
#第9-11列 三维物体的尺寸(dimensions),分别对应高度、宽度、长度,以米为单位。
#第12-14列 中心坐标(location),三维物体中心在相机坐标系下的位置坐标(x,y,z),单位为米。
#第15列 旋转角(rotation_y),取值范围为(-pi, pi)。
#第16列 置信度分数(score),仅在测试评估的时候才需要用到。置信度越高,表示目标越存在的概率越大。
#./KITTI
#D:\python\data\mkitti
#D:/python/data/mkitti
#dataset=KittiDataset(cfg=cfg,root='./data/KITTI',set='train')
'''
数据集 __getitem__(self, i)方法:
1,取得该图像对应的各个数据路径地址
2,加载映射文件,calib字典,包括内参P,转置矩阵R,外参Tr
3,加载标签文件得到所有目标边界框坐标(n,8,3) 点云数据Lidar(n,4)
4,读入图像,数据增强加过滤
5,处理点云得到(k,t,7)k个体素,每个t个点云,每个点云(x,y,z,t,x',y',z') , (k,3),k个点云的所在位置
6,构建标签
'''
class KittiDataset(data.Dataset):
def __init__(self, cfg, root='D:/python/data/mkitti',set='train',type='velodyne_train'):
'''
:param cfg:配置文件 cinfig.py
:param root:数据集根目录
:param set:训练还是测试
:param type:数据形式,雷达点云
'''
self.type = type
self.root = root
#训练集目录D:/python/data/mkitti/training
self.data_path = os.path.join(root, 'training')
#预处理后雷达数据路径
self.lidar_path = os.path.join(self.data_path, "crop/")
#图像数据路径
self.image_path = os.path.join(self.data_path, "image_2/")
#雷达映射图像文件路径
self.calib_path = os.path.join(self.data_path, "calib/")
#标签文件路径
self.label_path = os.path.join(self.data_path, "label_2/")
#每个文件夹名
with open(os.path.join(self.data_path, '%s.txt' % set)) as f:
self.file_list = f.read().splitlines()
self.T = cfg.T
self.M = cfg.M
# voxel size 0.4 0.2 0.2
self.vd = cfg.vd
self.vh = cfg.vh
self.vw = cfg.vw
# points cloud range
self.xrange = cfg.xrange
self.yrange = cfg.yrange
self.zrange = cfg.zrange
#锚框xyzwlhr的形式中心长宽高(7044,7)
self.anchors = cfg.anchors.reshape(-1,7)
#特征图(200,176)
self.feature_map_shape = (int(cfg.H / 2), int(cfg.W / 2))
#anchors_per_position 2
self.anchors_per_position = cfg.anchors_per_position
self.pos_threshold = cfg.pos_threshold
self.neg_threshold = cfg.neg_threshold
def cal_target(self, gt_box3d):
# Input:
# labels: (N,)
# feature_map_shape: (w, l)特征图尺寸
# anchors: (w, l, 2, 7)
# Output:
# pos_equal_one (w, l, 2) 正样本
# neg_equal_one (w, l, 2) 负样本
# targets (w, l, 14) 偏移量 xyz取相对偏移 whl取log偏移
# attention: cal IoU on birdview在鸟瞰图上计算iou
#feature_map_shape(200,176)
anchors_d = np.sqrt(self.anchors[:, 4] ** 2 + self.anchors[:, 5] ** 2)
#正样本(200,176,2)
pos_equal_one = np.zeros((*self.feature_map_shape, 2))
#负样本(200,176,2)
neg_equal_one = np.zeros((*self.feature_map_shape, 2))
targets = np.zeros((*self.feature_map_shape, 14))
#标签坐标变化,<边角坐标表示>-----------<中心长宽高表示>成xyzhqlr gt_box3d(-1,8,3) ,
# (N, 8, 3) -> (N, 7)相当于二维边界框 顶点坐标 转 中心长宽 表示
gt_xyzhwlr = box3d_corner_to_center_batch(gt_box3d)
#对于每个锚框,将锚框 <中心长宽高> -------- <边角坐标> 表示(n,4,2)
anchors_corner = anchors_center_to_corner(self.anchors)
#锚框<边角坐标表示>转化<二维鸟瞰图>表示(n,4)
anchors_standup_2d = corner_to_standup_box2d_batch(anchors_corner)
# 标签<边角坐标表示> ------- <二维鸟瞰图表示>(4,4)
gt_standup_2d = corner_to_standup_box2d_batch(gt_box3d)
#计算鸟瞰图锚框 与 标签鸟瞰图边角框 iou(n,4)与(4,4)->(n,4)
iou = bbox_overlaps(
np.ascontiguousarray(anchors_standup_2d).astype(np.float32),
np.ascontiguousarray(gt_standup_2d).astype(np.float32),
)
#选择iou最大的锚框作为后候选锚框去预测该目标,得到索引id_highest(4) id_highest_gt(4)
id_highest = np.argmax(iou.T, axis=1) # the maximum anchor's ID
id_highest_gt = np.arange(iou.T.shape[0])
#选择最大iou的锚框和包含目标的
mask = iou.T[id_highest_gt, id_highest] > 0
id_highest, id_highest_gt = id_highest[mask], id_highest_gt[mask]
# find anchor iou > cfg.XXX_POS_IOU
#选取较大iou锚框中阈值大于正样本阈值的 0.6作为正样本
id_pos, id_pos_gt = np.where(iou > self.pos_threshold)
# find anchor iou < cfg.XXX_NEG_IOU 小于负样本阈值的作为负样本0.45
id_neg = np.where(np.sum(iou < self.neg_threshold,
axis=1) == iou.shape[1])[0]
#正样本索引
id_pos = np.concatenate([id_pos, id_highest])
id_pos_gt = np.concatenate([id_pos_gt, id_highest_gt])
# TODO: uniquify the array in a more scientific way
id_pos, index = np.unique(id_pos, return_index=True)
id_pos_gt = id_pos_gt[index]
id_neg.sort()
# cal the target and set the equal one
index_x, index_y, index_z = np.unravel_index(
id_pos, (*self.feature_map_shape, self.anchors_per_position))
#正样本点
pos_equal_one[index_x, index_y, index_z] = 1
# ATTENTION: index_z should be np.array
# targets (w, l, 14) self.anchors中心长宽高
# xyz: (gt-an)/d = 相对偏移 wlh:求的log(gt_w / an_w)
targets[index_x, index_y, np.array(index_z) * 7] = \
(gt_xyzhwlr[id_pos_gt, 0] - self.anchors[id_pos, 0]) / anchors_d[id_pos]
targets[index_x, index_y, np.array(index_z) * 7 + 1] = \
(gt_xyzhwlr[id_pos_gt, 1] - self.anchors[id_pos, 1]) / anchors_d[id_pos]
targets[index_x, index_y, np.array(index_z) * 7 + 2] = \
(gt_xyzhwlr[id_pos_gt, 2] - self.anchors[id_pos, 2]) / self.anchors[id_pos, 3]
targets[index_x, index_y, np.array(index_z) * 7 + 3] = np.log(
gt_xyzhwlr[id_pos_gt, 3] / self.anchors[id_pos, 3])
targets[index_x, index_y, np.array(index_z) * 7 + 4] = np.log(
gt_xyzhwlr[id_pos_gt, 4] / self.anchors[id_pos, 4])
targets[index_x, index_y, np.array(index_z) * 7 + 5] = np.log(
gt_xyzhwlr[id_pos_gt, 5] / self.anchors[id_pos, 5])
targets[index_x, index_y, np.array(index_z) * 7 + 6] = (
gt_xyzhwlr[id_pos_gt, 6] - self.anchors[id_pos, 6])
index_x, index_y, index_z = np.unravel_index(
id_neg, (*self.feature_map_shape, self.anchors_per_position))
neg_equal_one[index_x, index_y, index_z] = 1
# to avoid a box be pos/neg in the same time
# 避免一次预测框同时是正样本和负样本
index_x, index_y, index_z = np.unravel_index(
id_highest, (*self.feature_map_shape, self.anchors_per_position))
neg_equal_one[index_x, index_y, index_z] = 0
return pos_equal_one, neg_equal_one, targets
#点云数据体素化
'''
1.随机打乱点云数据顺序
2.得到候选体素块,使用所有的点云点减去原点坐标得到长度,除以单位距离(x-minx / vw)得到各个点具体所在的体素块
3.通过unique得到voxel_coords(3186,3)体素和位置,inv_ind(18102)每个点对应的体素索引,voxel_counts(3186)体素数量,且该体素包含的点的个数
'''
def preprocess(self, lidar):
# shuffling the points随机打乱顺序
np.random.shuffle(lidar)
#体素化, x-minx / vw 归到某体素上
#每个点对应到空间中某个体素上,比如10,400,352个体素
#这个点属于 5,200,120 空间位置的体素
#(n,3)n个点,每个点对应体素坐标(18102,3)->(3186,3)共构成3186个体素
#这里的候选体素已经排除了没有点的体素,各个点对应的体素位置,但是有的体素只包含一个点(稀疏体素)
voxel_coords = ((lidar[:, :3] - np.array([self.xrange[0], self.yrange[0], self.zrange[0]])) / (
self.vw, self.vh, self.vd)).astype(np.int32)
""""
这里可以对稀疏体素进行一个处理,判断体素包含点的数目,小于阈值则忽略该体素
"""
# convert to (D, H, W) (x,y,z) -> (z,y,x)
voxel_coords = voxel_coords[:,[2,1,0]]
# 沿着0轴进行unique,也就是对于所有点进行unique(1,2,3) (1,2,3)这样在排除一个
#voxel_coords(3186,3)体素和位置,
# inv_ind(18102)每个点对应的体素索引,
# voxel_counts(3186)体素数量,且该体素包含的点的个数
voxel_coords, inv_ind, voxel_counts = np.unique(voxel_coords, axis=0, \
return_inverse=True, return_counts=True)
voxel_features = []
#voxel_coords_keep = []
#对于每一个体素
for i in range(len(voxel_coords)):
# maxiumum number of points per voxel
#T = 35
voxel = np.zeros((self.T, 7), dtype=np.float32)
pts = lidar[inv_ind == i]#在第i个体素中的点
#如果当前体素包含的点数目大于35
if voxel_counts[i] > self.T:
#体素去取前35个点
pts = pts[:self.T, :]
#设置该体素包含点数为35
voxel_counts[i] = self.T
#如果当前体素包含的点太少属于稀疏体素,忽略该体素
'''自己改的东西,在所有体素中删除该体素,并且在体素特征中不记录该体素'''
voxel[:pts.shape[0], :] = np.concatenate((pts, pts[:, :3] - np.mean(pts[:, :3], 0)), axis=1)
# 得到体素特征(3186,35,7) 一共3186个体素,每个体素35以下的点,每个点7个特征(x,y,z,r,x',y',z')
voxel_features.append(voxel)
# augment the points点数据增强组成7维数据,(x,y,z,r,x',y',z')该点原始坐标加上该点对于所在体素所有点均值中心偏移
return np.array(voxel_features), voxel_coords
def preprocess_densevoxel(self, lidar):
# shuffling the points随机打乱顺序
np.random.shuffle(lidar)
#体素化, x-minx / vw 归到某体素上
#每个点对应到空间中某个体素上,比如10,400,352个体素
#这个点属于 5,200,120 空间位置的体素
#(n,3)n个点,每个点对应体素坐标(18102,3)->(3186,3)共构成3186个体素
#这里的候选体素已经排除了没有点的体素,各个点对应的体素位置,但是有的体素只包含一个点(稀疏体素)
voxel_coords = ((lidar[:, :3] - np.array([self.xrange[0], self.yrange[0], self.zrange[0]])) / (
self.vw, self.vh, self.vd)).astype(np.int32)
""""
这里可以对稀疏体素进行一个处理,判断体素包含点的数目,小于阈值则忽略该体素
"""
# convert to (D, H, W) (x,y,z) -> (z,y,x)
voxel_coords = voxel_coords[:,[2,1,0]]
# 沿着0轴进行unique,也就是对于所有点进行unique(1,2,3) (1,2,3)这样在排除一个
#voxel_coords(3186,3)体素和位置,
# inv_ind(18102)每个点对应的体素索引,
# voxel_counts(3186)体素数量,且该体素包含的点的个数
voxel_coords, inv_ind, voxel_counts = np.unique(voxel_coords, axis=0, \
return_inverse=True, return_counts=True)
voxel_features = []
voxel_coords_keep = []
#对于每一个体素
for i in range(len(voxel_coords)):
# maxiumum number of points per voxel
#T = 35
voxel = np.zeros((self.T, 7), dtype=np.float32)
pts = lidar[inv_ind == i]#在第i个体素中的点
#如果当前体素包含的点数目大于35
if voxel_counts[i] > self.T:
#体素去取前35个点
pts = pts[:self.T, :]
#设置该体素包含点数为35
voxel_counts[i] = self.T
#如果当前体素包含的点太少属于稀疏体素,忽略该体素
'''自己改的东西,在所有体素中删除该体素,并且在体素特征中不记录该体素'''
if voxel_counts[i]>self.M:
voxel[:pts.shape[0], :] = np.concatenate((pts, pts[:, :3] - np.mean(pts[:, :3], 0)), axis=1)
# 得到体素特征(3186,35,7) 一共3186个体素,每个体素35以下的点,每个点7个特征(x,y,z,r,x',y',z')
voxel_features.append(voxel)
voxel_coords_keep.append(voxel_coords[i])
# augment the points点数据增强组成7维数据,(x,y,z,r,x',y',z')该点原始坐标加上该点对于所在体素所有点均值中心偏移
return np.array(voxel_features), np.array(voxel_coords_keep)
def __getitem__(self, i):
'''
:param i: 样本索引
:return:
voxel_features, 点云voxelize体素化(8874,35,7)
voxel_coords, 体素候选点(8874,4)
pos_equal_one (w, l, 2) 正样本#(2,200,176,2)
neg_equal_one (w, l, 2) 负样本(2,200,176,2)
targets (w, l, 14) 偏移量 xyz取相对偏移 whl取log偏移(2,200,176,14)
image, 样本对于图片
calib, 样本点云映射图像文件
self.file_list[i]:样本名称000000
'''
#取得该图像对应的各个数据路径地址
lidar_file = self.lidar_path + '/' + self.file_list[i] + '.bin'
calib_file = self.calib_path + '/' + self.file_list[i] + '.txt'
label_file = self.label_path + '/' + self.file_list[i] + '.txt'
image_file = self.image_path + '/' + self.file_list[i] + '.png'
#加载映射文件,calib字典,包括内参P,转置矩阵R,外参Tr
calib = utils.load_kitti_calib(calib_file)
Tr = calib['Tr_velo2cam']
#加载标签得到所有目标边界框坐标(n,8,3) n个目标的8个边界点的3个世界坐标 Lidar(n,4)
gt_box3d = utils.load_kitti_label(label_file, Tr)
lidar = np.fromfile(lidar_file, dtype=np.float32).reshape(-1, 4)
#对于训练集数据
if self.type == 'velodyne_train':
#读入该样本图像
image = cv2.imread(image_file)
# data augmentation进行数据增强
lidar, gt_box3d = aug_data(lidar, gt_box3d)
# specify a range选择在规定范围内的点
lidar, gt_box3d = utils.get_filtered_lidar(lidar, gt_box3d)
# voxelize体素化voxel_features(4005,35,7) voxel_coords(4005,3)
voxel_features, voxel_coords = self.preprocess_densevoxel(lidar)#(739,3)
#voxel_features, voxel_coords = self.preprocess(lidar)#(3372,3)
# bounding-box encoding边界框编码
# pos_equal_one (w, l, 2) 正样本
# neg_equal_one (w, l, 2) 负样本
# targets (w, l, 14) 偏移量 xyz取相对偏移 whl取log偏移
pos_equal_one, neg_equal_one, targets = self.cal_target(gt_box3d)
return voxel_features, voxel_coords, pos_equal_one, neg_equal_one, targets, image, calib, self.file_list[i]
elif self.type == 'velodyne_test':
NotImplemented
else:
raise ValueError('the type invalid')
def __len__(self):
return len(self.file_list)
def detection_collate(batch):
voxel_features = []
voxel_coords = []
pos_equal_one = []
neg_equal_one = []
targets = []
images = []
calibs = []
ids = []
#链接一个batch中所有样本target信息,包括
for i, sample in enumerate(batch):
#体素特征(n,35,7)
voxel_features.append(sample[0])
#所有体素(n,3),进行了一下填充
#填充内容使得各个体素添加了一个batch归属信息
#如 (3,4,7)的一个体素->(1,3,4,7)属于第一个batch
voxel_coords.append(
np.pad(sample[1], ((0, 0), (1, 0)),
mode='constant', constant_values=i))
#正负样本(w,l,2)
pos_equal_one.append(sample[2])
neg_equal_one.append(sample[3])
#标签(w,l,14)
targets.append(sample[4])
#图像,calibs,样本名称000009,这3是list
images.append(sample[5])
calibs.append(sample[6])
ids.append(sample[7])
return np.concatenate(voxel_features), \
np.concatenate(voxel_coords), \
np.array(pos_equal_one),\
np.array(neg_equal_one),\
np.array(targets),\
images, calibs, ids
if __name__ == '__main__':
import torch
root_path = "D:/python/data/mkitti"
# IMG_ROOT = root_path + "training/image_2/"
# PC_ROOT = root_path + "training/velodyne/"
# CALIB_ROOT = root_path + "training/calib/"
# PC_CROP_ROOT = root_path + "training/crop/"
# lidar_path = os.path.join(root_path, "crop/")
# image_path = os.path.join(root_path, "image_2/")
# calib_path = os.path.join(root_path, "calib/")
# label_path = os.path.join(root_path, "label_2/")
#D:\python\data\modelnet40_normal_resampled
# path = r"D:\python\data\modelnet40_normal_resampled"
dataset = KittiDataset(cfg=cfg, root=root_path, set='train')
data_loader = data.DataLoader(dataset, batch_size=cfg.N, num_workers=0, collate_fn=detection_collate, shuffle=True, \
pin_memory=False)
i=0
# return np.concatenate(voxel_features), \
# np.concatenate(voxel_coords), \
# np.array(pos_equal_one),\
# np.array(neg_equal_one),\
# np.array(targets),\
# images, calibs, ids
for voxel_features, voxel_coords, pos_equal_one,neg_equal_one,targets,images,calibs,ids in data_loader:
print(voxel_features.shape)
print(voxel_coords.shape)
print(pos_equal_one.shape)
print(neg_equal_one.shape)
print(targets.shape)
print(images[0].shape)
print(len(calibs))
print(len(ids))
# (8581, 35, 7)
# (8581, 3)
# (2, 200, 176, 2)
# (2, 200, 176, 2)
# (2, 200, 176, 14)
# (375, 1242, 3)
# 2
# 2
# 0 - ----------------------------
print(i,"-----------------------------")
# print(len(images))
# print(len(calibs))
# print(len(ids))
#torch.Size([12, 1024, 3])
#torch.Size([12])
i +=1
if i==3:
break