一、学习目标
初学BEV,目前0基础,项目需要创建自定义数据集。因此,根据create_data_bevdet.py文件了解数据集 .pkl 与配置文件 .json 的创建流程。
二、BEVDet部署参考
BEVDet部署:BEVDet代码复现实践_bevdet复现过程-CSDN博客
三、代码部分
3.1主函数运行流程
if __name__ == '__main__':
dataset = 'nuscenes' # 数据集名称
version = 'v1.0-trainval' # 数据集版本
root_path = './data/nuscenes' # 数据集路径
extra_tag = 'bevdetv3-nuscenes' # 数据集额外标签
# 调用函数准备nuScenes数据集的数据
nuscenes_data_prep(
root_path=root_path,
info_prefix=extra_tag,
version=version,
max_sweeps=10) # 输入连续帧的数量
# 为数据集添加额外的标注信息
add_ann_adj_info(extra_tag)
# 创建地面实况数据库,该函数在tools/data_converter/create_gt_database.py中
create_groundtruth_database('NuScenesDataset',
root_path,
extra_tag,
f'{root_path}/{extra_tag}_infos_train.pkl')
3.2根据Nuscenes数据集生成自定义类别
定义字典 map_name_from_general_to_detection
,用于将NuScenes数据集中的一般类别名称映射到特定的检测类别。其中,“ignore”类别的对象在检测任务中直接忽略。
map_name_from_general_to_detection = {
# 成人、儿童、警察、建筑工人均被视为pedestrian。
'human.pedestrian.adult': 'pedestrian',
'human.pedestrian.child': 'pedestrian',
'human.pedestrian.police_officer': 'pedestrian',
'human.pedestrian.construction_worker': 'pedestrian',
# 轮椅、婴儿车、个人移动设备、动物被标记为ignore,意味着在检测中不考虑这些类别。
'human.pedestrian.wheelchair': 'ignore',
'human.pedestrian.stroller': 'ignore',
'human.pedestrian.personal_mobility': 'ignore',
'animal': 'ignore',
# 汽车、摩托车、自行车、各种类型的卡车、公交车、拖车、施工车辆分别对应各自的类别。
'vehicle.car': 'car',
'vehicle.motorcycle': 'motorcycle',
'vehicle.bicycle': 'bicycle',
'vehicle.bus.bendy': 'bus',
'vehicle.bus.rigid': 'bus',
'vehicle.truck': 'truck',
'vehicle.construction': 'construction_vehicle',
'movable_object.barrier': 'barrier',
'movable_object.trafficcone': 'traffic_cone',
'vehicle.trailer': 'trailer',
# 救护车、警车也被标记为ignore,可能是因为在某些场景下它们不是主要的检测目标。
'vehicle.emergency.ambulance': 'ignore',
'vehicle.emergency.police': 'ignore',
# 可推拉物品、碎片、自行车架都被标记为ignore。
'movable_object.pushable_pullable': 'ignore',
'movable_object.debris': 'ignore',
'static_object.bicycle_rack': 'ignore',
}
classes
列表则包含了模型在检测任务中需要识别的具体类别。它列出了模型在执行检测任务时需要识别的具体对象类别,这些类别是基于 map_name_from_general_to_detection 字典中定义的规则筛选出来的。在模型训练或评估阶段,只有classes列表中的类别会被考虑,而被标记为ignore的类别将被排除在外。
classes = [
'car', 'truck', 'construction_vehicle', 'bus', 'trailer', 'barrier',
'motorcycle', 'bicycle', 'pedestrian', 'traffic_cone'
]
3.3根据Nuscenes和上一节的字典生成GT
从给定的信息字典中提取并转换地面实况(Ground Truth,GT)标签,特别是针对3D目标检测任务。此函数主要用于处理来自NuScenes数据集的数据,将原始的标注信息转换为模型训练所需的格式。
def get_gt(info):
"""
从给定的信息中生成地面真实标签(GT labels)。
输入参数:
info (dict): 包含生成GT标签所需信息的字典,如摄像头参数和标注信息。
返回:
list: GT边界框(bboxes),在全局坐标系中的位置。
list: GT类别标签。
"""
# 从信息中提取前摄像头到全局坐标的旋转矩阵
ego2global_rotation = info['cams']['CAM_FRONT']['ego2global_rotation']
# 提取前摄像头到全局坐标的平移向量
ego2global_translation = info['cams']['CAM_FRONT']['ego2global_translation']
# info: 这是输入的字典。
# info['cams']: 在字典info中,有一个键(key)为'cams'的条目。
# 这个键指向的是一个子字典,里面包含了所有摄像头的信息。
# info['cams']['CAM_FRONT']: 继续深入,
# 'cams'键下的子字典中有一个为'CAM_FRONT'的条目,这代表了前向摄像头的具体信息。
# 'CAM_FRONT'可能包含摄像头的位置、角度、分辨率、帧率等信息,但在这个特定的上下文中,我们关注的是与坐标变换相关的信息。
# info['cams']['CAM_FRONT']['ego2global_rotation']: 最后,'CAM_FRONT'键下的字典中还有一个键为'ego2global_rotation'的条目。
# 这个条目存储了一个四元数,表示从自车(ego vehicle)坐标系到全局坐标系的旋转变换四元数。
# 计算反向平移向量,将平移向量转换成NumPy数组。
trans = -np.array(ego2global_translation)
# 创建逆旋转矩阵Quaternion对象,赋值给rot
rot = Quaternion(ego2global_rotation).inverse
# 初始化GT边界框列表
gt_boxes = []
# 初始化GT标签列表
gt_labels = []
# 遍历所有标注信息
# 遍历 info['ann_infos'] 列表中的每一个标注信息字典。
# 对于列表中的每一个字典,用 ann_info 来访问和处理单个标注的详细信息。
for ann_info in info['ann_infos']:
# 在车体坐标系下
# 当前标注必须在class内 且 有lidar、radar的点云数据
if (map_name_from_general_to_detection[ann_info['category_name']] not in classes
or ann_info['num_lidar_pts'] + ann_info['num_radar_pts'] <= 0):
continue
# 创建3D边界框,Box对象
box = Box(
ann_info['translation'], # 位置
ann_info['size'], # 尺寸
Quaternion(ann_info['rotation']), # 旋转姿态
velocity=ann_info['velocity'], # 速度
)
# 将边界盒从车辆坐标转换至全局坐标
box.translate(trans)
box.rotate(rot)
# 获取边界盒中心坐标,为Numpy数组
box_xyz = np.array(box.center)
# 获取边界盒尺寸(深度、宽度、高度)
# wlh 属性代表宽度(Width)、长度(Length)和高度(Height)
# 列表 [1, 0, 2] 指定了新的顺序
box_dxdydz = np.array(box.wlh)[[1, 0, 2]]
# 获取边界框偏航角
box_yaw = np.array([box.orientation.yaw_pitch_roll[0]])
# 获取边界框速度(前两维)
box_velo = np.array(box.velocity[:2])
# 合并边界框数据:位置、尺寸、航向角、速度
gt_box = np.concatenate([box_xyz, box_dxdydz, box_yaw, box_velo])
# 将GT边界盒添加到列表
gt_boxes.append(gt_box)
# 将GT标签添加到列表
# classes.index(...): index 方法用于查找列表中某个元素的索引位置。
# classes.index(...) 返回map_name_from_general_to_detection[ann_info['category_name']] 所表示的类别在 classes 列表中的索引。
gt_labels.append(classes.index(map_name_from_general_to_detection[ann_info['category_name']]))
# 返回GT边界框和GT标签
return gt_boxes, gt_labels
3.4 预处理NuScenes数据
调用./tools/data_converter/nuscene_converter.py中的nuscenes_converter函数对数据进行预处理
def nuscenes_data_prep(root_path, info_prefix, version, max_sweeps=10):
"""
准备与nuScenes数据集相关的数据。
相关数据包括记录基本信息、2D标注和地面实况数据库的'.pkl'文件。
参数:
root_path (str): 数据集的路径。
info_prefix (str): 信息文件名的前缀。
version (str): 数据集版本。
max_sweeps (int, 可选): 输入连续帧的数量,默认为10。
"""
nuscenes_converter.create_nuscenes_infos(
root_path, info_prefix, version=version, max_sweeps=max_sweeps)
3.5向数据集加入额外的标注信息
该函数用于给NuScenes数据集的训练和验证集添加注释和相邻帧信息。具体步骤如下:
- 定义NuScenes数据集的版本和数据路径。
- 初始化NuScenes数据集。
- 遍历训练集和验证集。
- 加载数据集的元信息。
- 对于每个样本,获取其注释信息。
- 计算注释的速度,并处理NaN值。
- 将速度信息添加到注释信息中。
- 使用get_gt函数处理注释信息。
- 添加场景token到样本信息中。
- 根据场景名称和样本token生成路径。
- 将更新后的数据集保存到文件中。
def add_ann_adj_info(extra_tag):
# 指定NuScenes数据集的版本和根目录
nuscenes_version = 'v1.0-trainval'
dataroot = './data/nuscenes/'
# 初始化NuScenes对象
# NuScenes: 这是一个类,它封装了对NuScenes数据集的访问,提供了多种方法来查询和操作数据集中的信息,例如获取样本、注释、传感器数据等。
# 执行 nuscenes = NuScenes(nuscenes_version, dataroot) 这行代码时,以下事情会发生:
# 实例化:创建了一个 NuScenes 类的新实例,并将其赋值给变量 nuscenes。
# 初始化:调用了 NuScenes 类的构造函数,该构造函数接受数据集版本和数据根目录作为参数。
# 加载元数据:构造函数会加载数据集的元数据,包括样本信息、注释、类别定义、传感器校准数据等。这些元数据存储在数据集的JSON文件中。
# 建立索引:为了加快后续的查询速度,构造函数可能还会建立各种内部索引或缓存。
nuscenes = NuScenes(nuscenes_version, dataroot)
# 遍历训练集和验证集
for set in ['train', 'val']:
# 加载数据集dataset
# ./data/nuscenes/%s_infos_%s.pkl: 这是一个文件路径模板,其中%s是占位符,会被后面的变量替换。这里%s会被extra_tag和set变量的值替换,生成实际的文件路径
# (extra_tag, set): 这是一个元组,其中extra_tag和set是变量名,它们的值将被用来生成具体的文件名。
# 'rb': 打开文件的模式,r表示读取,b表示以二进制模式读取
# pickle.load()函数被调用,它从打开的文件中读取字节流,并将其反序列化为Python对象。
# 反序列化后的对象被赋值给dataset变量
dataset = pickle.load(open(f'./data/nuscenes/{extra_tag}_infos_{set}.pkl', 'rb'))
# 遍历dataset中的info
for id in range(len(dataset['infos'])):
# 打印进度
if id % 10 == 0:
print(f'{id}/{len(dataset["infos"])}')
# 根据id获取 info
info = dataset['infos'][id]
# nuscenes.get('sample', info['token']) 是调用 NuScenes 类的一个方法 get() 来获取数据集中的一个样本
# 'sample' 指定了要获取的元数据类型,在这里是样本数据。
# info['token'] 唯一标识符,用于定位数据集中的特定样本。这个 token 通常是在 info 字典中找到的,它是NuScenes数据集中用于唯一标识每个样本的字段
sample = nuscenes.get('sample', info['token'])
# 初始化标注信息列表
ann_infos = list()
# 遍历样本的所有标注
for ann in sample['anns']:
# 获取标注详细信息
ann_info = nuscenes.get('sample_annotation', ann)
# 获得速度
velocity = nuscenes.box_velocity(ann_info['token'])
# 处理速度中可能存在的NaN值
if np.any(np.isnan(velocity)):
velocity = np.zeros(3)
# 将速度添加到标注信息中
ann_info['velocity'] = velocity
# 将处理后的标注信息添加到列表
ann_infos.append(ann_info)
# 更新数据集中的标注信息
dataset['infos'][id]['ann_infos'] = ann_infos
# 获取地面标签GT 并更新数据集
dataset['infos'][id]['ann_infos'] = get_gt(dataset['infos'][id])
# 添加场景令牌,scene_token用于唯一标识场景的令牌。每个场景包含多个连续的样本,而 scene_token 用于将样本关联到正确的场景
# 将 sample 字典中找到的 scene_token 值复制到 dataset['infos'][id] 字典的 scene_token 键中
dataset['infos'][id]['scene_token'] = sample['scene_token']
# 获取场景信息
scene = nuscenes.get('scene', sample['scene_token'])
# 设置占用栅格路径
dataset['infos'][id]['occ_path'] = f'./data/nuscenes/gts/{scene["name"]}/{info["token"]}'
# 保存更新后的数据集
# open() 函数用于打开或创建一个文件,这里使用了字符串格式化来构建文件路径。
# 'wb' 参数表示以二进制写入模式打开文件。b 表示二进制,这是因为pickle序列化后的数据通常不是纯文本格式,而是字节流
# with 语句用于安全地管理文件的打开和关闭。即使在写入数据时发生异常,with 语句也会确保文件被正确关闭,避免资源泄漏
# fid 是一个文件对象,它是在 with 语句中打开的文件
with open('./data/nuscenes/%s_infos_%s.pkl' % (extra_tag, set),
'wb') as fid:
pickle.dump(dataset, fid)
# pickle.dump() 函数用于将Python对象序列化并写入到一个文件中。
# fid 是前面打开的文件对象,pickle.dump() 将把 dataset 对象转换成字节流并写入到这个文件中。
四、小结
从3.5节NuScenes类中可得,具体数据标注在Nuscenes的配置文件Json中,本着按需求学习的原则,先看.json配置文件结合NuScenes官网、OpenMM的教程,学习如何创建自定义数据集。所以先忽略其他两个文件函数。