nuScenes3D⽬标检测数据集解析(完整版附python代码)最近在⽤nuScenes3D⽬标检测数据集,可能由于官⽅已经提供了解析⼯具包nuscenes-devkit,绝⼤多数博客只介绍了如何使⽤⼯具包进⾏数据解析和可视化,对于数据解析的内部逻辑就不是很关注了。本⽂根据官⽹说明、论⽂以及⼯具包源码整理归纳得到,对其他博客未提及的数据集结构、坐标系转换、传感器同步等问题进⾏了详细分析。
1. 数据集概述
1.1 传感器配置
nuScenes的数据采集车辆为Renault Zoe迷你电动车,配备6个周视相机,5个毫⽶波雷达,具备360°的视野感知能⼒。具体传感器信息即分布见下⾯的图表。
传感器类型 详细信息
相机 6台彩⾊相机,1600×900的分辨率,采⽤JPEG格式压缩,采样频率为12Hz
激光雷达 1台32线旋转式激光雷达,20Hz采样频率,360°⽔平FOV,-30°-10°的垂直FOV,探测距离70m,探测精度2cm,每秒140万点云
毫⽶波雷达 5个77GHz的毫⽶波雷达,FMCW调频,13Hz采样频率,探测距离250m,速度精度±0.1km/h
GPS和IMU 20mm的RTK定位精度,1000Hz采样频率
1.2 数据采集
nuScenes数据集使⽤了两辆传感器配置相同的雷诺电动车进⾏采集,采集地点为波⼠顿和新加坡,这两个城市以交通密集和驾驶场景复杂闻名。
整个数据集包含了由⼈⼯挑选的84段log,时长约15⼩时,距离约242km,平均车速16km/h。数据场景覆盖了城市、住宅区、郊区、⼯业区各个场景,也涵盖了⽩天、⿊夜、晴天、⾬天、多云等不同时段不同天⽓状况。最终数据集分为1000个⽚段,每个⽚段约20s。
1.3 传感器同步和数据标注
nuScenes和KITTI⼀样,也是采⽤激光来控制相机的曝光时刻,不过nuScenes有6个覆盖360°视野的相机。
在nuScenes中,图像的时间戳表⽰相机开始曝光的时刻,激光的时间戳表⽰激光扫描⼀圈结束的时刻。当车顶激光的线束扫描到相机FOV 中⼼区域时,便会给对应相机⼀个曝光信号,激光扫描⼀圈会触发6次相机曝光。
这⾥我对训练集进⾏了简单的统计,850个场景共34149帧数据。统计结果及含义如下所⽰:
1Total Scene Num: 850
2Total Sample Num: 34149
3[ expo_time_bet_adj_cams] Avg: 8.52ms 2STD: 8.53ms
4[max_delta_time_bet_cams] Avg: 42.61ms 2STD: 42.67ms
5[ cam_refresh_time] Avg: 498.93ms 2STD: 503.95ms
6[ lidar_refresh_time] Avg: 498.93ms 2STD: 503.97ms
7[ delta_lidar_cam_time] Avg: 0.99ms 2STD: 2.37ms
统计结果 含义
expo_time_bet_adj_cams 相邻两个相机曝光时间差,平均8.5ms,6个相机正好50ms,符合激光的扫描频率,侧⾯证明了每⼀圈激光会触发6次相机曝光
max_delta_time_bet_cams 6个周视相机依次曝光时刻的最⼤时间间隔,平均42ms,这意味着在相对40km/h相对速度下,第⼀个开始曝光的左前⽅相机和最后⼀个曝光的左后⽅相机,对同⼀个物体的
观测距离会相差接近半⽶
cam_refresh_time 相机采样间隔,平均500ms,对应2Hz的关键帧采样频率
lidar_refresh_time 激光采样间隔,平均500ms,对应2Hz的关键帧采样频率
delta_lidar_cam_time 激光时间戳和左后⽅相机曝光时刻的差值,均值仅1ms,说明激光扫描是从左后⽅相机附近位置开始的
1.4 建图定位
nuScenes的定位数据的⽣成分为两个阶段,⾸先采⽤离线的⽅式使⽤激光点云⽣成⾼清地图,然后在线采集数据阶段,结合⾥程计和激光数据,使⽤蒙特卡洛定位算法进⾏车辆定位,最终定位误差可以达到10cm以内。
2. 数据集下载
在nuScenes官⽹注册⼀个账号下载数据集,完整数据集包含3个部分:
Mini:从训练/验证集抽取10个场景组成,包含完整的原始数据和标注信息,主要⽤于数据集的熟悉;京东自营有假货吗
TrainVal:训练/验证集,包含850个场景,其中700个训练场景,150个验证场景
Test:测试集,包含150个场景,不包含标注数据。
下载好的数据集包含4个⽂件夹:
maps:地图数据,四张地图对应着4个数据采集地点
samples:带有标注信息的关键帧数据,训练主要⽤这部分数据
sweeps:完整时序数据,不带有标注信息,⼀般⽤于跟踪任务
v1.0-version:存有数据依赖关系、标注信息、标定参数的各种json⽂件
3. 数据集解析
nuScenes官⽅提供了⼀个数据集开发⼯具nuscenes-devkit,封装了数据读取、索引、可视化等常⽤操作,可以直接使⽤pip安装:
pip install nuscenes-devkit
这个⼯具包⽤起来还⽐较⽅便,具体怎么使⽤这⾥就不做过多介绍了,官⽹有详细的教程。有⼀点要注意的是,nuScenes解析库要求
v1.0-version等4个⽂件夹在同⼀级⽬录,否则会⽆法解析。
3.1 数据集结构
在介绍如何使⽤⼯具包遍历数据集之前,先对nuScenes数据集结构做⼀个简单介绍。nuScenes数据集采⽤关系数据库来管理数据,数据库⼀共包含13张表,以json⽂件格式存储在./v1.0-version⽬录下。
1lf.table_names = ['category', 'attribute', 'visibility', 'instance', 'nsor', 'calibrated_nsor',
2 'ego_po', 'log', 'scene', 'sample', 'sample_data', 'sample_annotation', 'map']如何瘦腰
在使⽤⼯具包获得数据集的句柄nusc后,可以通过(table_name, token)函数来快速获取任意表中的任意数据,⼗分⽅便快捷。表和表之间的依赖关系图可以去nuScenes官⽹的Data format标签页中查看,这⾥我对官⽹的关系图进⾏了归纳和精简。
总的来说,nuScenes数据集分为mini、trainval、test三个部分,每个部分的数据结构完全相同,可以分成scene、sample、
sample_data三个层级,数据访问通过token(可以理解为指针)来实现:
scene:是⼀段约20s的视频⽚段,由于关键帧采样频率为2Hz,所以每个scene⼤约包含40个关键帧,可以通过scene中的pre和next 来访问上下相邻的sample
sample:对应着⼀个关键帧数据,存储了相机、激光雷达、毫⽶波雷达的token信息,mini和trainval数据集中的sample还存储了标注信息的token
sample_data:sample中存储的token指向的数据,即我们最终真正关⼼的信息,⽐如图⽚路径、位姿数据、传感器标定结果、标注⽬标的3d信息等。获取到这些信息就可以开始训练模型了。
3.2 数据集遍历
nuScenes以关系数据库的⽅式管理数据,scene、sample、sample_data之间可以通过token⾮常⽅便的互相访问。这⾥分别从scene、sample和annotation为起点对整个数据集进⾏访问,统计数据集的场景、关键帧、标注⽬标的数⽬。
1from nuscenes.nuscenes import NuScenes
2
3
4def get_datat_info1(nusc):
5 scene_num = len(nusc.scene)
6 sample_num = 0
7 ann_num = 0
8
9 for scene in nusc.scene:
10 sample = None
11 while True:
12 if sample is None:
13 sample = ('sample', scene['first_sample_token'])
14
15 sample_num += 1
15 sample_num += 1
16 ann_num += len(sample['anns'])
17
18 if sample['next'] != '':
19 sample = ('sample', sample['next'])
20 el:
21 break
22
23 print('====== Start from scene')
薄荷奶茶
24 print('Scene Num: %d\nSample Num: %d\nAnnotation Num: %d' % (scene_num, sample_num, ann_num)) 25
26
27def get_datat_info2(nusc):
28 sample_num = len(nusc.sample)
29 ann_num = 0
30
31 scene_tokens = t()
32 for sample in nusc.sample:
33 ann_num += len(sample['anns'])
34
35 scene = ('scene', sample['scene_token'])
36 scene_tokens.add(scene['token'])
37 scene_num = len(scene_tokens)
38
39 print('====== Start from sample')
40 print('Scene Num: %d\nSample Num: %d\nAnnotation Num: %d' % (scene_num, sample_num, ann_num)) 41
42
43def get_datat_info3(nusc):
44 ann_num = len(nusc.sample_annotation)
45
46 scene_tokens = t()
47 sample_tokens = t()
48 for ann in nusc.sample_annotation:
49 sample = ('sample', ann['sample_token'])
50 sample_tokens.add(sample['token'])
51
52 scene = ('scene', sample['scene_token'])
53 scene_tokens.add(scene['token'])
54 scene_num = len(scene_tokens)
55 sample_num = len(sample_tokens)
56
57 print('====== Start from annotation')
58 print('Scene Num: %d\nSample Num: %d\nAnnotation Num: %d' % (scene_num, sample_num, ann_num)) 59
60
61if __name__ == '__main__':
62 nusc = NuScenes(version='v1.0-mini',
63 dataroot=data_root,
64 verbo=True)
65 get_datat_info1(nusc)
66 get_datat_info2(nusc)
67 get_datat_info3(nusc)
运⾏结果如下,可以看到3种遍历⽅式的结果完全⼀致。
1====== Start from scene
2Scene Num: 10
3Sample Num: 404
4Annotation Num: 18538
5====== Start from sample
6Scene Num: 10
7Sample Num: 404
8Annotation Num: 18538
9====== Start from annotation
10Scene Num: 10
11Sample Num: 404
12Annotation Num: 18538
3.3 标注格式及坐标系转换
宝宝出牙顺序
⽬标的标注信息包含如下⼏个字段:
字段名称 含义
visibility ⽬标可见程度,分为0~40%, 40%~60%, 60%~80%, 80%~100%四类,⽤1-4表⽰
category_name 类别名称,包含10个检测类别
translation 3D框的中⼼位置(x,y,z),单位m,是全局坐标系下的坐标
rotation 3D框的旋转量,⽤四元数(w,x,y,z)表⽰
size 3D框的尺⼨(w,l,h),单位⽶
表⽰rotation的四元数可以利⽤Python包pyquaternion转换成(pitch,yaw,roll)的形式,⽽且可以计算对应的旋转矩阵和逆矩阵,⾮常好⽤。
这⾥主要说⼀下nuScenes中的坐标系转换。nuScenes存在四个坐标系:全局坐标系、车⾝坐标系、相机坐标系和激光坐标系。后⾯三个⽐较好理解,都是相对坐标系,⽬标的位置随本车的运动⽽变化;⽽全局坐标系是绝对坐标系,是⽬标在地图中的绝对坐标,不随本车的运动⽽变化。标注真值的
坐标是全局坐标系下的坐标。各坐标系的转换关系如下图所⽰,所有转换都必须先转到车⾝坐标系(ego vehicle frame),然后再转换到⽬标坐标系。需要注意的是,由于每张图像的时间戳、激光的时间戳都两两不相同,它们有各⾃的位姿补偿(ego data),进⾏坐标系转换的时候需要注意⼀下。
标注真值(global frame)转换到激光坐标系(lidar frame):使⽤位姿补偿转换到车⾝坐标系,然后再根据激光雷达外参转换到激光坐标系。
1# 标注真值到激光坐标系
2ann = ('sample_annotation', token)
3calib_data = ('calibrated_nsor', lidar_data['calibrated_nsor_token'])
4ego_data = ('ego_po', lidar_data['ego_po_token'])
5# global frame
6center = np.array(ann['translation'])
7orientation = np.array(ann['rotation'])
8# 从global frame转换到ego vehicle frame新民事诉讼法
9quaternion = Quaternion(ego_data['rotation']).inver
10center -= np.array(ego_data['translation'])
11center = np.ation_matrix, center)
12orientation = quaternion * orientation
13# 从ego vehicle frame转换到nsor frame
14quaternion = Quaternion(calib_data['rotation']).inver
状组词15center -= np.array(calib_data['translation'])
16center = np.ation_matrix, center)
幼儿园数学加减法
17orientation = quaternion * orientation
标注真值(global frame)投影到图像(pixel coord):使⽤位姿补偿转换到车⾝坐标系,然后再根据相机
外参转换到相机坐标系,最后使⽤相机内参得到像素坐标系下的坐标。标注真值到车⾝坐标系的过程和上⾯类似,不过calib_data和ego_data需要从camera_data中获取,得到标注3D框在相机坐标系下的⾓点坐标points后,然后再使⽤相机内参投影⾄图像。
# 相机坐标系到像素坐标系
1calib_data = ('calibrated_nsor', camera_data['calibrated_nsor_token'])
2intrinsic = calib_data['camera_intrinsic']
3trans_mat = np.eye(4)
4trans_mat[:3, :3] = np.array(intrinsic)
5points = obj['box']
6points = np.concatenate((points, np.ones((1, points.shape[1]))), axis=0)
7points = np.dot(trans_mat, points)[:3, :]
8points /= points[2, :]
激光真值(lidar frame)投影⾄图像(pixel coord)就相对⿇烦⼀点,因为图像和激光时间戳不⼀致,需要多进⾏⼀步时间戳的变换。
1# step1: lidar frame -> ego frame
2calib_data = ('calibrated_nsor', lidar_file['calibrated_nsor_token'])
3rot_matrix = Quaternion(calib_data['rotation']).rotation_matrix
4points[:3, :] = np.dot(rot_matrix, points[:3, :])
5for i in range(3):
6 points[i, :] += calib_data['translation'][i]
7
应季水果时间表
8# step2: ego frame -> global frame
9ego_data = ('ego_po', lidar_file['ego_po_token'])
10rot_matrix = Quaternion(ego_data['rotation']).rotation_matrix
11points[:3, :] = np.dot(rot_matrix, points[:3, :])
12for i in range(3):
13 points[i, :] += ego_data['translation'][i]
14
15# step3: global frame -> ego frame
16ego_data = ('ego_po', camera_data['ego_po_token'])
17for i in range(3):
18 points[i, :] -= ego_data['translation'][i]
19rot_matrix = Quaternion(ego_data['rotation']).rotation_matrix.T
20points[:3, :] = np.dot(rot_matrix, points[:3, :])
21
22# step4: ego frame -> cam frame
23calib_data = ('calibrated_nsor', camera_data['calibrated_nsor_token'])
24for i in range(3):
25 points[i, :] -= calib_data['translation'][i]
26rot_matrix = Quaternion(calib_data['rotation']).rotation_matrix.T
27points[:3, :] = np.dot(rot_matrix, points[:3, :])
28
29# step5: cam frame -> uv pixel
30visible = points[2, :] > 0.1
31colors = get_rgb_by_distance(points[2, :], min_val=0, max_val=50)
32intrinsic = calib_data['camera_intrinsic']
33trans_mat = np.eye(4)
34trans_mat[:3, :3] = np.array(intrinsic)
35points = np.concatenate((points[:3, :], np.ones((1, points.shape[1]))), axis=0)
36points = np.dot(trans_mat, points)[:3, :]
37points /= points[2, :]
38points = points[:2, :]
3.4 可视化效果