Nuscenes数据集生成MotionNet训练数据 (一)

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或者1
  • voxel_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训练数据 (二)

你可能感兴趣的:(工程复现,python,开发语言,计算机视觉,目标检测,自动驾驶)