Nuscenes 数据集 1.旋转角初理解
计算左边卡车的rot_y,即全局坐标系下的旋转(相对于图像x轴)
1 2 3 v = np.dot(ve[0 ].rotation_matrix, np.array([1 , 0 , 0 ])) yaw = -np.arctan2(v[2 ], v[0 ]) print (yaw*180 /np.pi)
一开始不能理解为什么是90度,后来想明白了,这辆卡车虽然在图像上相对于ray(由相机射向卡车的射线)角度小于90°,但是其rot_y仍然是90°。正符合了3DB论文中的下图(世界线合并了!)
然后注意1图中的很多没有物体的框,其实在Lidar视角下都可以看间,但是投影(project)到图像上就看不见了,所以标注中的visibiality会设置为0,但其实不用担心,可以根据这个检索,也可以在训练的时候指定类别为车辆就行了,到时候会筛选掉很多看不见的目标。
1 2 3 from nuscenes.utils.geometry_utils import BoxVisibility, transform_matriximage_token = nusc.sample[2 ]['data' ]['CAM_FRONT' ] _, boxes, camera_intrinsic = nusc.get_sample_data(image_token, box_vis_level=BoxVisibility.ANY)
注意代码中get_sample_data会自动把bbox投影到选择的image_token的sensor上。
筛选boxes中的某一类框
1 ve = [i for i in boxes if i.name == 'vehicle.truck' ]
说明的是name属性是str,直接查数据集标注中用的字符串即可。
2.旋转角理解深入 1 2 3 v = np.dot(ve[0 ].rotation_matrix, np.array([1 , 0 , 0 ])) yaw = -np.arctan2(v[2 ], v[0 ]) print (yaw*180 /np.pi)
再回到第一个代码块,现在思考为什么旋转矩阵rotation_matrix在[1,0,0]这个轴上两个元素的反正切就是偏航。
给出偏航角的旋转矩阵公式,当三维物体绕z轴进行旋转(xy平面转动),旋转的角度就是α,根据旋转矩阵计算旋转过后的坐标。这个很好推,我在纸上推了一下就得到了。
那么如何算α就很好得知,使用第一列两个元素的反正切就可以算出来。
然后是重点:Nuscenes数据集保存的bbox框是全局坐标系下的,也就是说是用的点云保存的,点云中物体的高度投影到z轴上,而在单目图像当中高度用y进行表示,因此需要进行坐标变换,将z转到y
看上图,相机坐标系y是向地面的,方向相反因此坐标变换还需要乘-1,这也是为什么计算反正切需要乘-1.
一个月后更新
理解KITTI和Nuscenes旋转角不同 KITTI中alpha是局部旋转,rotation_y是全局旋转。使用神经网络回归时只能使用alpha作为回归目标。
Nuscenes中通过计算的偏航yaw实质就是rotation_y,我们需要使用rotation_y以及相机的标定计算得到相对旋转alpha。
CenterTrack中的Nuscenes转kitti转coco格式代码中很清晰的写了这一步骤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def _rot_y2alpha (rot_y, x, cx, fx ): """ Get rotation_y by alpha + theta - 180 alpha : Observation angle of object, ranging [-pi..pi] x : Object center x to the camera center (x-W/2), in pixels rotation_y : Rotation ry around Y-axis in camera coordinates [-pi..pi] """ alpha = rot_y - np.arctan2(x - cx, fx) if alpha > np.pi: alpha -= 2 * np.pi if alpha < -np.pi: alpha += 2 * np.pi return alpha bbox = KittiDB.project_kitti_box_to_image(copy.deepcopy(box), camera_intrinsic, imsize=(1600 , 900 )) alpha = _rot_y2alpha(yaw, (bbox[0 ] + bbox[2 ]) / 2 , camera_intrinsic[0 , 2 ], camera_intrinsic[0 , 0 ]) ann['alpha' ] = alpha
注意bbox此时已经被投影到了图像坐标系下。可以看出,使用相机模型可以计算出ray的角度进而和rot_y相减得到局部旋转alpha。
角度训练代码 使用alpha计算rotbin
根据3DB等文章的旋转角回归方法,分别计算角度的分类和偏移。
在KITTI Dataset的构建中,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 def _add_rot (self, ret, ann, k, gt_det ): if 'alpha' in ann: ret['rot_mask' ][k] = 1 alpha = ann['alpha' ] if alpha < np.pi / 6. or alpha > 5 * np.pi / 6. : ret['rotbin' ][k, 0 ] = 1 ret['rotres' ][k, 0 ] = alpha - (-0.5 * np.pi) if alpha > -np.pi / 6. or alpha < -5 * np.pi / 6. : ret['rotbin' ][k, 1 ] = 1 ret['rotres' ][k, 1 ] = alpha - (0.5 * np.pi) gt_det['rot' ].append(self._alpha_to_8(ann['alpha' ])) else : gt_det['rot' ].append(self._alpha_to_8(0 )) def _alpha_to_8 (self, alpha ): ret = [0 , 0 , 0 , 1 , 0 , 0 , 0 , 1 ] if alpha < np.pi / 6. or alpha > 5 * np.pi / 6. : r = alpha - (-0.5 * np.pi) ret[1 ] = 1 ret[2 ], ret[3 ] = np.sin(r), np.cos(r) if alpha > -np.pi / 6. or alpha < -5 * np.pi / 6. : r = alpha - (0.5 * np.pi) ret[5 ] = 1 ret[6 ], ret[7 ] = np.sin(r), np.cos(r) return ret
alpha_to_8 放在gt_det中,又被放在meta中,没有查清有什么用。Dataset最终返回的是ret中的,计算loss也是使用ret中的参数。
8长度向量分成前后各四个,推理时每个前两个数用来softmax计算得分,对比两个的得分,高的代表在这个bin内。
用$ arctan2(a_{j1},a_{j2})+m_j $计算最终得到的角度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 if 'rot' in output: losses['rot' ] += self.crit_rot( output['rot' ], batch['rot_mask' ], batch['ind' ], batch['rotbin' ], batch['rotres' ]) / opt.num_stacks def compute_rot_loss (output, target_bin, target_res, mask ): output = output.view(-1 , 8 ) target_bin = target_bin.view(-1 , 2 ) target_res = target_res.view(-1 , 2 ) mask = mask.view(-1 , 1 ) loss_bin1 = compute_bin_loss(output[:, 0 :2 ], target_bin[:, 0 ], mask) loss_bin2 = compute_bin_loss(output[:, 4 :6 ], target_bin[:, 1 ], mask) loss_res = torch.zeros_like(loss_bin1) if target_bin[:, 0 ].nonzero().shape[0 ] > 0 : idx1 = target_bin[:, 0 ].nonzero()[:, 0 ] valid_output1 = torch.index_select(output, 0 , idx1.long()) valid_target_res1 = torch.index_select(target_res, 0 , idx1.long()) loss_sin1 = compute_res_loss( valid_output1[:, 2 ], torch.sin(valid_target_res1[:, 0 ])) loss_cos1 = compute_res_loss( valid_output1[:, 3 ], torch.cos(valid_target_res1[:, 0 ])) loss_res += loss_sin1 + loss_cos1 if target_bin[:, 1 ].nonzero().shape[0 ] > 0 : idx2 = target_bin[:, 1 ].nonzero()[:, 0 ] valid_output2 = torch.index_select(output, 0 , idx2.long()) valid_target_res2 = torch.index_select(target_res, 0 , idx2.long()) loss_sin2 = compute_res_loss( valid_output2[:, 6 ], torch.sin(valid_target_res2[:, 1 ])) loss_cos2 = compute_res_loss( valid_output2[:, 7 ], torch.cos(valid_target_res2[:, 1 ])) loss_res += loss_sin2 + loss_cos2 return loss_bin1 + loss_bin2 + loss_res
花费一整天看明白了数据集,还是蛮高兴的~
最后标记一篇好文章 。
还有一篇 。