Weipeng Studio.

Nuscenes 数据集

字数统计: 1.4k阅读时长: 6 min
2020/11/24 Share

Nuscenes 数据集

1.旋转角初理解

img

计算左边卡车的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)
1
>>-90.8701168926904

一开始不能理解为什么是90度,后来想明白了,这辆卡车虽然在图像上相对于ray(由相机射向卡车的射线)角度小于90°,但是其rot_y仍然是90°。正符合了3DB论文中的下图(世界线合并了!)

img

然后注意1图中的很多没有物体的框,其实在Lidar视角下都可以看间,但是投影(project)到图像上就看不见了,所以标注中的visibiality会设置为0,但其实不用担心,可以根据这个检索,也可以在训练的时候指定类别为车辆就行了,到时候会筛选掉很多看不见的目标。


1
2
3
from nuscenes.utils.geometry_utils import BoxVisibility, transform_matrix
image_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]这个轴上两个元素的反正切就是偏航。

img

给出偏航角的旋转矩阵公式,当三维物体绕z轴进行旋转(xy平面转动),旋转的角度就是α,根据旋转矩阵计算旋转过后的坐标。这个很好推,我在纸上推了一下就得到了。

那么如何算α就很好得知,使用第一列两个元素的反正切就可以算出来。

img

然后是重点:Nuscenes数据集保存的bbox框是全局坐标系下的,也就是说是用的点云保存的,点云中物体的高度投影到z轴上,而在单目图像当中高度用y进行表示,因此需要进行坐标变换,将z转到y

看上图,相机坐标系y是向地面的,方向相反因此坐标变换还需要乘-1,这也是为什么计算反正切需要乘-1.

img


一个月后更新

理解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
#in trainer.py
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: (B, 128, 8) [bin1_cls[0], bin1_cls[1], bin1_sin, bin1_cos,
# bin2_cls[0], bin2_cls[1], bin2_sin, bin2_cos]
# target_bin: (B, 128, 2) [bin1_cls, bin2_cls]
# target_res: (B, 128, 2) [bin1_res, bin2_res]
# mask: (B, 128, 1)
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

花费一整天看明白了数据集,还是蛮高兴的~

最后标记一篇好文章

还有一篇

CATALOG
  1. 1. Nuscenes 数据集
    1. 1.1. 1.旋转角初理解
    2. 1.2. 2.旋转角理解深入
    3. 1.3. 理解KITTI和Nuscenes旋转角不同
    4. 1.4. 角度训练代码