Nuscenes数据集生成MotionNet训练数据
需求:结合Nuscenes-devkit工具,利用Nuscenes数据集生成MotionNet训练数据。因为最终目的要在自己的数据集上训练,故需要先搞清源码中数据生成流程与数据结构、重点函数。
1. 数据生成流程解析
1)样本数据(关键帧)遍历,获取当前关键帧以及前后帧的多帧点云、时间差list、齐次变换矩阵,构建具有部分属性的save_data_dict字典。
2)利用关键帧数据,获取标注实例boxes和对应类别category,每个实例含有多时间段。将多帧实例boxes存放到box_data_dict,最后添加到save_box_dict_list中。
3)更新计数器为偶数时,保存临近两帧数据。重新组织实例tokens,共有tokens放在前面,其余tokens放在后面,并将token改为数字表示。达到完善save_data_dict_list的目的。
4)利用save_data_dict_list前后两个clips数据,生成稀疏bev数据,并将其保存一对npy格式文件,用于训练。
注意:
tokens代表唯一标示符。
save_box_dict_list维度是2,包含两个前后两个clips数据,时间差为50ms。
每个片段(clips)通常有5帧点云数据,帧间相差200ms
2. 重要数据结构
1)save_data_dict 字典最终形式,主要含有以下三个部分:
'pc_0'= {ndarray:(4,22995)} # 点云数据
'pc_1'= {ndarray:(4,23027)}
...
'pc_49'= {ndarray:(4,22976)}
'times'= {ndarray:(50,0)} # 时间戳之差
'num_sweeps' = {int} 50 # 帧数
'trans_matrices' = {ndarray:(30,4,4)} # 齐次变换矩阵(只包含30个过去帧)
'num_instance' = {int} 46 # 关键帧中实例个数
'instance_boxes_0' = {ndarray:(50,10)} # 实例0, 含有前后50帧,每个实例box含有10个维度:中心、长宽高、四元数
'category_0' = {int} 1 # 实例0 的所属类别
'instance_boxes_1' = {ndarray:(50,10)}
'category_1' = {int} 2
...
'instance_boxes_45' = {ndarray:(50,10)}
'category_45' = {int} 1
2)save_box_dict 字典,用于存放save_data_dict属性的过度字典:
'category_19804c352c0a4767b61b8d0709d1db99' = {int} 1 # 实例1 的所属类别
'instance_boxes_19804c352c0a4767b61b8d0709d1db99' = {ndarray:(50,10)} # 实例1, 含有前后50帧,每个实例box含有10个维度:中心、长宽高、四元数
'category_b61b72a5b5ad482e9bc94c203f75b8d4' = {int} 2
'instance_boxes_b61b72a5b5ad482e9bc94c203f75b8d4' = {ndarray:(50,10)}
...
'category_ff07eea720434dc0ba0ccdef45cb318c' = {int} 1
'instance_boxes_ff07eea720434dc0ba0ccdef45cb318c' = {ndarray:(50,10)}
3)dense_bev_data 元组,主要包含以下参数:
00 = {list: 5} # voxel_indices_list, 占据点云的体素索引列表,内含过去5帧,间隔200ms
01 = {ndarray:(5,256,256,13)} # padded_voxel_points, 点云占据体素,通过T/F判断是否被占据,只用作完整性检查
02 = {ndarray:(4475,2)} # pixel_indices , 格网索引值
03 = {ndarray:(256,256)} # pixel_instance_map, 格网实例图
04 = {ndarray:(20,256,256,2)} # all_disp_field_gt, 每一个格网场景流真值向量
05 = {ndarray:(20,256,256)} # all_valid_pixel_maps,过去帧有效的二进制掩膜图
06 = {ndarray:(256,256)} # non_empty_map, 非空图,即有点占据的像素
07 = {ndarray:(256,256,5)} # pixel_cat_map, 类别图,深度是类别,0是背景 4是其他
08 = {int} 5 # num_past_frames_for_bev_seq ,过去连续BEV图 帧数
09 = {int} 20 # num_future_pcs, 未来点云帧数
10 = {ndarray:(30,4,4)} # trans_matrices, 过去帧的旋转矩阵
4)sparse_bev_data 字典,主要包含以下参数:
'voxel_indices_0' = {ndarray:(6294,3)} # dense_bev_data['voxel_indices_list'], 与dense_bev_data中00体素索引列表相同
...
'voxel_indices_4' = {ndarray:(6294,3)}
'disp_field' = {ndarray:(20,4475,2)} # 场景流,包含未来20帧
'valid_pixel_map' = {ndarray:(20,4475)} # 有效像素图
'pixel_cat_map' = {ndarray:(4475,5)} # 像素类别图
'num_past_pcs' = {int} 5 # 过去采样后点云帧数
'num_future_pcs' = {int} 20 # 未来点云帧数
'trans_matrices' = {ndarray:(30,4,4)} # 过去所有帧的齐次旋转矩阵
'3d_dimension' = {tuple:3}(256,256,13) # 三维各轴向尺度
'pixel_indices' = {ndarray:(4475,2)} # 当前帧像素索引
'pixel_instance_ids' = {ndarray:(4475,)} # 当前帧像素实例id
3. 核心代码解析
3.1 将原始点云转换为稠密BEV地图
1. 函数原型:convert_to_dense_bev(data_dict)
data_dict
: 数据字典,完整数据内容参考 2.1)save_data_dict
2. 代码流程:
1)设置相关变量:帧数、时间差数组、齐次变换矩阵,过去帧数,未来帧数,加载点云
num_sweeps = data_dict['num_sweeps'] # 帧数
times = data_dict['times'] # 时间差数组
trans_matrices = data_dict['trans_matrices'] # 齐次变换矩阵,只存储了过去帧的
num_past_sweeps = len(np.where(times >= 0)[0]) # 过去帧数
num_future_sweeps = len(np.where(times < 0)[0]) # 未来帧数
assert num_past_sweeps + num_future_sweeps == num_sweeps, "The number of sweeps is incorrect!"
# 加载点云存放到pc_list
pc_list = []
for i in range(num_sweeps):
pc = data_dict['pc_' + str(i)]
pc_list.append(pc.T)
2)点云帧排序,当前点云存储顺序为[cur,cur-1,cur-2, …,cur-m, cur+1, cur+2,…, cur+n], 变化为[cur-p,… cur-2, cur, cur+1, cur+2,…,cur+n ]
tmp_pc_list_1 = pc_list[0:num_past_sweeps:(past_frame_skip + 1)] # 按间隔筛选
tmp_pc_list_1 = tmp_pc_list_1[::-1] # 倒叙
tmp_pc_list_2 = pc_list[(num_past_sweeps + future_frame_skip)::(future_frame_skip + 1)]
pc_list = tmp_pc_list_1 + tmp_pc_list_2 # now the order is: [past frames -> current frame -> future frames]
num_past_pcs = len(tmp_pc_list_1)
num_future_pcs = len(tmp_pc_list_2)
3)体素化输入点云,计算体素占据
voxel_indices_list = list() # 体素索引列表
padded_voxel_points_list = list() # 体素占据数组的列表
past_pcs_idx = list(range(num_past_pcs)) # 临近8帧
past_pcs_idx = past_pcs_idx[-num_past_frames_for_bev_seq:] # 通常使用5帧(包含当前帧)
for i in past_pcs_idx:
# 计算体素占据
res, voxel_indices = voxelize_occupy(pc_list[i], voxel_size=voxel_size, extents=area_extents, return_indices=True)
voxel_indices_list.append(voxel_indices)
padded_voxel_points_list.append(res)
padded_voxel_points = np.stack(padded_voxel_points_list, axis=0).astype(np.bool)
4)计算流向量真值,详细参考 3.3
all_disp_field_gt, all_valid_pixel_maps, non_empty_map, pixel_cat_map, pixel_indices, pixel_instance_map \
= gen_2d_grid_gt(data_dict, grid_size=voxel_size[0:2], extents=area_extents,
frame_skip=future_frame_skip, return_instance_map=True)
3.2 点云体素化,计算体素占据,仅使用二进制表征
1. 函数原型:voxelize_occupy(pts, voxel_size, extents=None, return_indices=False)
参数:
pts
: 点云N * [x,y,z,i]voxel_size
: 体素尺寸extents
: 可选项,裁剪范围,是否进行roi过滤return_indices
: 是否返回非空体素索引,默认是false
返回值:
leaf_layout
: 体素占据数组,ndarray:(256,256,13),内部只含有0或者1voxel_indices
: 体素索引数组,形如[[-117 96 -3], [-116 58 -4],…]
2. 代码流程:
1)范围过滤,选取感兴趣区域中点云
if extents is not None:
if extents.shape != (3, 2):
raise ValueError("Extents are the wrong shape {}".format(extents.shape))
filter_idx = np.where((extents[0, 0] < pts[:, 0]) & (pts[:, 0] < extents[0, 1]) &
(extents[1, 0] < pts[:, 1]) & (pts[:, 1] < extents[1, 1]) &
(extents[2, 0] < pts[:, 2]) & (pts[:, 2] < extents[2, 1]))[0]
pts = pts[filter_idx] # 裁剪后的索引值,对应的点云
2)体素划分,索引排序,去掉重复索引
discrete_pts = np.floor(pts[:, :3] / voxel_size).astype(np.int32)
# Use Lex Sort, sort by x, then y, then z
x_col = discrete_pts[:, 0]
y_col = discrete_pts[:, 1]
z_col = discrete_pts[:, 2]
sorted_order = np.lexsort((z_col, y_col, x_col))
discrete_pts = discrete_pts[sorted_order]
# 创建c连续内存数组,加速,去重复索引
contiguous_array = np.ascontiguousarray(discrete_pts).view(
np.dtype((np.void, discrete_pts.dtype.itemsize * discrete_pts.shape[1])))
_, unique_indices = np.unique(contiguous_array, return_index=True)
unique_indices.sort()
# 去重后体素坐标,形如[[-117 96 -3], [-116 58 -4],..]
voxel_coords = discrete_pts[unique_indices]
3)建立体素坐标,创建体素极值[256, 256, 13]。确立体素占据:点云占据位置赋1,其余赋值0。
if extents is not None:
min_voxel_coord = np.floor(extents.T[0] / voxel_size)
max_voxel_coord = np.ceil(extents.T[1] / voxel_size) - 1
else:
min_voxel_coord = np.amin(voxel_coords, axis=0)
max_voxel_coord = np.amax(voxel_coords, axis=0)
# 获取体素三个维度上值的大小[256,256,13]
num_divisions = ((max_voxel_coord - min_voxel_coord) + 1).astype(np.int32)
# 最小体素归到原点位置
voxel_indices = (voxel_coords - min_voxel_coord).astype(int)
# 建立体素占据
leaf_layout = VOXEL_EMPTY * np.ones(num_divisions.astype(int), dtype=np.float32)
# 填充有点云的体素为1,其余仍为0
leaf_layout[voxel_indices[:, 0],
voxel_indices[:, 1],
voxel_indices[:, 2]] = VOXEL_FILLED
…
未完待续,接下一篇Nuscenes数据集生成MotionNet训练数据 (二)