在PointRCNN训练过程中,需要用到KITTI中的四类数据。
calib (calibration, 校准文件)
是一个txt文件,每个文件有7行,用来描述相机的校准参数。七行的开头分别为:
P0,P1,P2,P3
:分别表示左侧灰度相机,右侧灰度相机,左侧彩色相机,右侧彩色相机。R0_rect
:校正旋转参数,可根据该参数将多个摄像机拍摄的照片位于同一个平面上。9个数字,表示3x3的矩阵。Tr_velo_to_cam
:3x4,顾名思义,可根据该矩阵的参数将lidar点云数据投影到未校正的相机照片上。Tr_imu_to_velo
:3x4,从imu坐标转换到velo的坐标。velodyne
激光雷达点云文件的bin格式,可见其可视化方法。
组织格式大概是(nx4),n为点云中点的个数 ,4中的前3个数是点的坐标,最后一个数代表激光反射强度。
lable_2
是对对应图片/点云的标注文件,共15列。
分别表示目标类别、截断率、遮挡程度、观察角度(-pi~pi)、左、上、右、下的2D边界框坐标、3维的高、宽、长(米)、照相机坐标下的物体位置3D坐标、相对y轴的旋转角度。
Van 0.00 1 1.60 801.70 162.11 837.52 202.62 2.52 2.10 5.64 13.98 1.89 47.96 1.88
Car 0.00 2 1.61 786.59 180.70 820.40 208.38 1.44 1.65 2.96 10.61 1.89 39.54 1.87
DontCare -1 -1 -10 829.35 159.40 867.93 195.90 -1 -1 -1 -1000 -1000 -1000 -10
image_2
由车载高分辨率彩色摄像机行驶中拍摄的照片,png格式。
在PointRCNN的stage 1中,采用了Pointnet++作为主干网络,它是Pointnet的改进版。
Pointnet的核心思想是点云中点的置换不变性
和旋转不变性
。
其网络结构大概是将3通道的点云向高维度映射,使用卷积进行3->64->128->1024编码,再将其分别送入分割网络和分类网络。这样直接将NxD的点云矩阵作为输入相比输入体素等方法可以获得很好的实时性。但由于直接提取全局特征,导致局部特征并没有被有效的提取,导致PointNet在分割任务中表现较差。
Pointnet++则改进了这个问题,类似传统特征提取的方法,Pointnet++先从每个小范围提取局部特征,再将每个小范围进行abstraction,通过逐层的提取局部特征、再将其合成全局特征,从而进行分类。
两篇Pointnet++很细致的博客,学习的榜样。
https://zhuanlan.zhihu.com/p/88238420
https://blog.csdn.net/weixin_39373480/article/details/88878629
从kitti中读取数据,为后续训练调用数据提供了丰富的接口。具体工作如下:
roipool3d_cuda.pts_in_boxes3d_cpu(pts_flag, pts, boxes3d)
,论文中指出可通过lable的边界框,将3D框之内的点都认为是前景点。./gt_database/train_gt_database_3level_Car.pkl
。关键代码如下
def generate_gt_database(self):
gt_database = []
for idx, sample_id in enumerate(self.image_idx_list):
sample_id = int(sample_id)
print('process gt sample (id=%06d)' % sample_id)
pts_lidar = self.get_lidar(sample_id)
calib = self.get_calib(sample_id)
pts_rect = calib.lidar_to_rect(pts_lidar[:, 0:3])
pts_intensity = pts_lidar[:, 3]
obj_list = self.filtrate_objects(self.get_label(sample_id))# 如:对于background和car的classes(22行),过滤掉行人。找出有效类
gt_boxes3d = np.zeros((obj_list.__len__(), 7), dtype=np.float32)
for k, obj in enumerate(obj_list):
gt_boxes3d[k, 0:3], gt_boxes3d[k, 3], gt_boxes3d[k, 4], gt_boxes3d[k, 5], gt_boxes3d[k, 6] \
= obj.pos, obj.h, obj.w, obj.l, obj.ry
if gt_boxes3d.__len__() == 0:
print('No gt object')
continue
boxes_pts_mask_list = roipool3d_utils.pts_in_boxes3d_cpu(torch.from_numpy(pts_rect), torch.from_numpy(gt_boxes3d))
for k in range(boxes_pts_mask_list.__len__()):
pt_mask_flag = (boxes_pts_mask_list[k].numpy() == 1)
cur_pts = pts_rect[pt_mask_flag].astype(np.float32)
cur_pts_intensity = pts_intensity[pt_mask_flag].astype(np.float32)
sample_dict = {'sample_id': sample_id,
'cls_type': obj_list[k].cls_type,
'gt_box3d': gt_boxes3d[k],
'points': cur_pts,
'intensity': cur_pts_intensity,
'obj': obj_list[k]}
gt_database.append(sample_dict)
save_file_name = os.path.join(args.save_dir, '%s_gt_database_3level_%s.pkl' % (args.split, self.classes[-1]))
with open(save_file_name, 'wb') as f:
pickle.dump(gt_database, f)
self.gt_database = gt_database
print('Save refine training sample info file to %s' % save_file_name)
类似于FastRCNN等二维特征提取方法,PointRCNN同样采用two-stage进行Object Detection。流程如下
backbone_xyz
和backbone_features
。Pointnet++的代码主要位于poingnet2_msg.py。rpn_cls
和rpn_reg
,主要代码如下。(rpn.py) def forward(self, input_data):
"""
:param input_data: dict (point_cloud)
:return:
"""
pts_input = input_data['pts_input']
backbone_xyz, backbone_features = self.backbone_net(pts_input) # (B, N, 3), (B, C, N) ## 首先由pointnet++进行处理
rpn_cls = self.rpn_cls_layer(backbone_features).transpose(1, 2).contiguous() # (B, N, 1)
rpn_reg = self.rpn_reg_layer(backbone_features).transpose(1, 2).contiguous() # (B, N, C)
ret_dict = {'rpn_cls': rpn_cls, 'rpn_reg': rpn_reg,
'backbone_xyz': backbone_xyz, 'backbone_features': backbone_features}
return ret_dict
rpn_cls
可以认为是前景点的概率,当该概率大于某阈值时设对应掩码值为1,由此获得seg_mask
。另外由于前景点的数量往往远小于背景点,使用focal损失来解决类不平衡的问题。rpn_reg
的网络相对复杂一些,在论文中提出的是一种基于bin的候选框回归方法,但是对应的代码部分(rpn.py:31-59)还没有看懂。最后每batch输出512个region of interest。stage 2主要用于微调stage 1生成的3D框位置。
input_data
)会被送入proposal_target_layer,输出一个target_dict
。两个dict的属性列表如下图 (rcnn_net.py:123)