nuScenes3D目标检测数据集解析(完整版附python代码)

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 数据集结构msinfo
在介绍如何使⽤⼯具包遍历数据集之前,先对nuScenes数据集结构做⼀个简单介绍。nuScenes数据集采⽤关系数据库来管理数据,数据库⼀共包含13张表,以json⽂件格式存储在./v1.0-version⽬录下。
1self.table_names = ['category', 'attribute', 'visibility', 'instance', 'sensor', 'calibrated_sensor',
2                    'ego_pose', '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_dataset_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            else:
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_dataset_info2(nusc):
28    sample_num = len(nusc.sample)
29    ann_num = 0
30
31    scene_tokens = set()
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_dataset_info3(nusc):
44    ann_num = len(nusc.sample_annotation)
45
46    scene_tokens = set()
47    sample_tokens = set()
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                    verbose=True)
65    get_dataset_info1(nusc)
66    get_dataset_info2(nusc)
67    get_dataset_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_sensor', lidar_data['calibrated_sensor_token'])
4ego_data = ('ego_pose', lidar_data['ego_pose_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']).inverse
10center -= np.array(ego_data['translation'])
11center = np.ation_matrix, center)
12orientation = quaternion * orientation
13# 从ego vehicle frame转换到sensor frame
14quaternion = Quaternion(calib_data['rotation']).inverse
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_sensor', camera_data['calibrated_sensor_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_sensor', lidar_file['calibrated_sensor_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_pose', lidar_file['ego_pose_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_pose', camera_data['ego_pose_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_sensor', camera_data['calibrated_sensor_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 可视化效果

本文发布于:2024-09-23 17:17:05,感谢您对本站的认可!

本文链接:https://www.17tex.com/tex/3/380575.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:数据   坐标系   激光   标注   包含   场景
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2024 Comsenz Inc.Powered by © 易纺专利技术学习网 豫ICP备2022007602号 豫公网安备41160202000603 站长QQ:729038198 关于我们 投诉建议