从零实现一个3D目标检测算法(2):点云数据预处理

在上一篇文章《从零实现一个3D目标检测算法(1):3D目标检测概述》对3D目标检测研究现状PointPillars模型进行了介绍,在本文中我们开始写代码一步步实现PointPillars,这里我们先实现如何对点云数据进行预处理。

在图像目标检测中,一般不需要对图像进行预处理操作,直接输入原始图像即可得到最终的检测结果。

但是在点云3D目标检测中,往往需要对点云进行一定的预处理,本文将介绍在PointPillars模型中如何对点云进行预处理。这里的点云数据预处理操作同样也适用其它的基于Voxels的3D检测模型中


文章目录

    • 1. 模型配置文件config.py
      • 1.1 将模型参数保存在日志文件
      • 1.2 加载模型配置文件
      • 1.3 解析终端命令修改模型配置参数
    • 2. 点云数据预处理
      • 2.1 DatasetTemplate类
      • 2.2 KittiDataset类
      • 2.3 KITTI数据加载器

1. 模型配置文件config.py

在这里我们将首先编写在整个工程中最重要的config.py文件,该文件主要包括三个函数。

作用分别是:加载模型配置文件pointpillar.yaml、将模型参数保存在日志文件中、以及解析终端命令修改模型配置参数。

关于上述三个函数,只需要会使用即可。首先导入需要的Python库:

from easydict import EasyDict
from pathlib import Path
import yaml

1.1 将模型参数保存在日志文件

这一部分是将整个网络模型的全部参数保存到日志文件中,在模型训练过程中每一个模块的代码往往会修改很多次。有了日志文件,我们就能很方便地查看每次所修改的地方,如果有疑问的话,可以借助日志文件快速定位问题,代码如下:

def log_config_to_file(cfg, pre='cfg', logger=None):for key, val in cfg.items():if isinstance(cfg[key], EasyDict):logger.info('\n%s.%s = edict()' % (pre, key))log_config_to_file(cfg[key], pre=pre+ '.' + key, logger=logger)continuelogger.info('%s.%s: %s' % (pre, key, val))

1.2 加载模型配置文件

下面一个函数是从配置文件pointpillar.yaml中加载网络模型参数。

在Python中我们使用字典这种数据类型来存储网络的各种参数,只需要命名好参数名称即可,如测试集,训练集名称,网络各子模块名称,损失函数名称等,在修改时也只需要修改对应参数的变量值即可,这是一个很方便的调参方式。代码如下:

def cfg_from_yaml_file(cfg_file, config):with open(cfg_file, 'r') as f:try:new_config = yaml.load(f, Loader=yaml.FullLoader)except:new_config = yaml.load(f)config.update(EasyDict(new_config))return config

1.3 解析终端命令修改模型配置参数

除了对模型配置文件.yaml进行修改外,也可以在执行时通过终端来修改模型的参数。

这时就要求程序能够获取终端信息,包括参数名称以及参数值,通常是成对出现,代码如下:

def cfg_from_list(cfg_list, config):"""Set config keys via list (e.g., from command line)."""from ast import literal_evalassert len(cfg_list) % 2 == 0for k, v in zip(cfg_list[0::2], cfg_list[1::2]):key_list = k.split('.')d = configfor subkey in key_list[:-1]:assert subkey in d, 'NotFoundKey: %s' % subkeyd = d[subkey]subkey = key_list[-1]assert subkey in d, 'NotFoundKey: %s' % subkeytry: value = literal_eval(v)except:value = v if type(value) != type(d[subkey]) and isinstance(d[subkey], EasyDict):key_val_list = value.split('.')for src in key_val_list:cur_key, cur_val = src.split(':')val_type = type(d[subkey][cur_key])cur_val = val_type(cur_val)d[subkey][cur_key] = cur_valelif type(value) != type(d[subkey]) and isinstance(d[subkey], list):val_list = value.split('.')for k, x in enumerate(val_list):val_list[k] = type(d[subkey][0])(x)d[subkey] = val_listelse:assert type(value) == type(d[subkey]), \'type {} dose not match original type {}'.format(type(value), type(d[subkey]))d[subkey] = value

下面我们来定义模型参数配置变量cfg,其本身是一个字典,现在我们先定义它的根路径。

至此配置文件代码编写完毕,不妨可以调用cfg_from_yaml_file函数加载yaml文件看看模型参数加载是否正确。

cfg = EasyDict()
cfg.ROOT_DIR = (Path(__file__).resolve().parent / '../').resolve()
cfg.LOCAL_RANK = 0if __name__=='__main__':pass

2. 点云数据预处理

现在我们对KITTI数据集进行预处理,最终将其加载到PyTorchDataLoader中。

2.1 DatasetTemplate类

首先是dataset.py文件,我们使用Python中的Class来对点云数据进行预处理,数据的预处理操作都定义为Class的成员函数。

先首先定义一个DatasetTemplate类,当做点云数据的一个基本类,后面处理其它点云数据集时可以在此基础上进行不同的操作,导入必要的Python库:

import numpy as np
from collections import defaultdict
import torch.utils.data as torch_data
import sys 
sys.path.append('../')
sys.path.append('../../')
from utils import common_utils
from config import cfgclass DatasetTemplate(torch_data.Dataset):def __init__(self):super().__init__()

DatasetTemplate中我们定义两个成员函数,一个是数据准备函数prepare_data

输入的是点云数据帧编号(idx)(idx)idx原始点云数据(N,3+C1)(N,3+C1)N3+C1,以字典形式传输,输出为:

  • Voxels
  • Voxels坐标
  • 每个Voxels中点的个数
  • Voxels中心坐标(全局坐标)
  • 原始点云数据

输出同样以字典形式输出。

def prepare_data(self, input_dict):""":param input_dict:sample_idx: stringpoints: (N, 3 + C1):return:voxels: (N, max_points_of_each_voxel, 3 + C2), floatnum_points: (N), intcoordinates: (N, 3), [idx_z, idx_y, idx_x]voxel_centers: (N, 3)points: (M, 3 + C)"""sample_idx = input_dict['sample_idx']points = input_dict['points']points = points[:, :cfg.DATA_CONFIG.NUM_POINT_FEATURES['use']]     # voxels, coordinates, num_pointsvoxels, coordinates, num_points = self.voxel_generator.generate(points, \max_voxels=cfg.DATA_CONFIG[self.mode].MAX_NUMBER_OF_VOXELS)    # voxel_centersvoxel_centers = (coordinates[:, ::-1] + 0.5) * self.voxel_generator.voxel_size \+ self.voxel_generator.point_cloud_range[0:3]print('voxel_centers.shape is: ', voxel_centers.shape)       # (11719, 3)if cfg.DATA_CONFIG.MASK_POINTS_BY_RANGE:points = common_utils.mask_points_by_range(points, cfg.DATA_CONFIG.POINT_CLOUD_RANGE)example = {}example.update({'voxels': voxels,'num_points': num_points,'coordinates': coordinates,'voxel_centers': voxel_centers,'points': points})return example

另一个函数是collate_batch,作用是在加载数据集时如何选取数据。

@staticmethod
def collate_batch(batch_list, _unused=False):example_merged = defaultdict(list)for example in batch_list:for k, v in example.items():example_merged[k].append(v)ret = {}for key, elems in example_merged.items():if key in ['voxels', 'num_points', 'voxel_centers', 'seg_labels', 'part_labels', 'bbox_reg_labels']:ret[key] = np.concatenate(elems, axis=0)elif key in ['coordinates', 'points']:coors = []for i, coor in enumerate(elems):coor_pad = np.pad(coor, ((0, 0), (1, 0)), mode='constant', constant_values=i)coors.append(coor_pad)ret[key] = np.concatenate(coors, axis=0)elif key in ['gt_boxes']:max_gt = 0batch_size = elems.__len__()for k in range(batch_size):max_gt = max(max_gt, elems[k].__len__())batch_gt_boxes3d = np.zeros((batch_size, max_gt, elems[0].shape[-1]), dtype=np.float32)for k in range(batch_size):batch_gt_boxes3d[k, :elems[k].__len__(), :] = elems[k]ret[key] = batch_gt_boxes3delse:ret[key] = np.stack(elems, axis=0)ret['batch_size'] = batch_list.__len__()return ret

2.2 KittiDataset类

现在我们编写kitti_dataset.py,主要目的是创造KittiDataset类,首先是导入所需库:

import os
import sys
import pickle 
import copy
import numpy as np
from pathlib import Path 
import torch 
import sys 
sys.path.append('../')
sys.path.append('../../')
from config import cfg 
from spconv.utils import VoxelGenerator
from ..dataset import DatasetTemplate

在这里我们首先定义一个BaseKittiDataset类,这里初始化只有一个参数,就是点云数据的存储路径root_path

class BaseKittiDataset(DatasetTemplate):def __init__(self, root_path):super().__init__()self.root_path = root_path

现在我们编写获取点云数据的get_lidar函数,KITTI中点云数据是以二进制格式保存的,每个点有4个信息:(x,y,z,r)(x,y,z,r)(x,y,z,r),数据类型为float32,代码如下:

def get_lidar(self, idx):lidar_file = os.path.join(self.root_path, 'velodyne', '%06d.bin' % idx)assert os.path.exists(lidar_file)return np.fromfile(lidar_file, dtype=np.float32).reshape([-1, 4])      

此外我们也可以编写函数get_infos来获取点云信息,具体为:

def get_infos(self, idx):import concurrent.futures as futuresinfo = {}pc_info = {'num_features':4, 'lidar_idx': idx}info['point_cloud'] = pc_inforeturn info

这里有一个生成最终预测结果的函数,因为模型计算时使用的是GPU,而要保存时需要转化为CPU可访问的数据。

预测信息有box尺寸box3d_lidar,分值scores,目标类型标签label_preds,以及点云编号sample_idx

@staticmethod
def generate_prediction_dict(input_dict, index, record_dict):# finally generate predictions.sample_idx = input_dict['sample_idx'][index] if 'sample_idx' in input_dict else -1boxes3d_lidar_preds = record_dict['boxes'].cpu().numpy()if boxes3d_lidar_preds.shape[0] == 0:return {'sample_idx': sample_idx}predictions_dict ={'box3d_lidar': boxes3d_lidar_preds,'scores': record_dict['scores'].cpu.numpy(),'label_preds': record_dict['labels'].cpu().numpy(),'sample_idx': sample_idx}return predictions_dict

现在我们就可以创建KittiDataset类了,同样初始化时需要设置数据路径,这里我们需要将模式设置为TEST

class KittiDataset(BaseKittiDataset):def __init__(self, root_path, logger=None):super().__init__(root_path=root_path)self.logger = loggerself.mode = 'TEST'self.kitti_infos = []self.include_kitti_data(self.mode, logger)self.dataset_init(logger)

在初始化时,有一个dataset_init函数,这个函数是用来生成voxel_generator的,使用的库为Spconv,在上面的prepare_data函数中会使用这个voxel_generator,代码如下:

def dataset_init(self, logger):voxel_generator_cfg = cfg.DATA_CONFIG.VOXEL_GENERATORself.voxel_generator = VoxelGenerator(voxel_size=voxel_generator_cfg.VOXEL_SIZE,point_cloud_range=cfg.DATA_CONFIG.POINT_CLOUD_RANGE,max_num_points=voxel_generator_cfg.MAX_POINTS_PER_VOXEL)

include_kitti_data函数是用来加载pkl文件的,我们会将待处理的点云信息存储在pkl文件中,这样测试模型时只需使用这一个文件就可以访问全部点云数据了:

def include_kitti_data(self, mode, logger):if cfg.LOCAL_RANK == 0 and logger is not None:logger.info('Loading KITTI dataset')kitti_infos = []for info_path in cfg.DATA_CONFIG[mode].INFO_PATH:        info_path = cfg.ROOT_DIR / info_pathwith open(info_path, 'rb') as f:infos = pickle.load(f)kitti_infos.append(infos)self.kitti_infos.extend(kitti_infos)if cfg.LOCAL_RANK == 0 and logger is not None:logger.info('Total samples for KITTI dataset: %d' % (len(kitti_infos)))

此外我们也可以对点云进行筛选,下面的代码为选取xxx[0,70.4][0, 70.4][0,70.4]yyy[−40,40][-40, 40][40,40]zzz[−3,1][-3, 1][3,1]范围的点,这个一般要根据具体应用场景来设置。

@staticmethod
def get_valid_flag(pts_lidar):'''Valid points should be in the PC_AREA_SCOPE'''val_flag_x = np.logical_and(pts_lidar[:, 0]>=0, pts_lidar[:, 0]<=70.4)val_flag_y = np.logical_and(pts_lidar[:, 1]>=-40, pts_lidar[:, 1]<=40)val_flag_z = np.logical_and(pts_lidar[:, 2]>=-3, pts_lidar[:, 2]<=1)val_flag_merge = np.logical_and(val_flag_x, val_flag_y, val_flag_z)pts_valid_flag = val_flag_mergereturn pts_valid_flag

然后,就是编写__getitem__函数

def __len__(self):return len(self.kitti_infos)def __getitem__(self, index):info = copy.deepcopy(self.kitti_infos[index])sample_idx = info['point_cloud']['lidar_idx']points = self.get_lidar(sample_idx)pts_valid_flag = self.get_valid_flag(points[:, 0:3])points = points[pts_valid_flag]input_dict = {'points': points, 'sample_idx': sample_idx}example = self.prepare_data(input_dict=input_dict)example['sample_idx'] = sample_idxreturn example

下面是create_kitti_infos函数:

def create_kitti_infos(data_path, save_path):dataset = BaseKittiDataset(root_path=data_path)val_filename = save_path / ('kitti_infos_val.pkl')print('val_filename is: ', val_filename)print('---------------Start to generate data infos---------------')kitti_infos_val = dataset.get_infos(idx)print(kitti_infos_val)with open(val_filename, 'wb') as f:pickle.dump(kitti_infos_val, f)print('Kitti info val file is saved to %s' % val_filename)

最后编写main函数,函数主要作用是获取终端信息,生成kitti_infos

if __name__=='__main__':if sys.argv.__len__() > 1 and sys.argv[1] == 'create_kitti_infos':create_kitti_infos(data_path=cfg.ROOT_DIR / 'data',save_path=cfg.ROOT_DIR / 'data')

生成后的kitti_infos如下:

{'point_cloud': {'num_features': 4, 'lidar_idx': '000010'}}

2.3 KITTI数据加载器

现在编写__init__.py,这里的作用是通过DataLoader加载点云数据,这在PyTorch是十分常见的,代码如下:

import os
from pathlib import Path
import torch
from torch.utils.data import DataLoader
from .kitti.kitti_dataset import KittiDataset, BaseKittiDataset
from config import cfg__all__ = {'BaseKittiDataset': BaseKittiDataset,'KittiDataset': KittiDataset}def build_dataloader(data_dir, batch_size, logger=None):data_dir = Path(data_dir) if os.path.isabs(data_dir) else cfg.ROOT_DIR / data_dirdataset = __all__[cfg.DATA_CONFIG.DATASET](root_path=data_dir, logger=logger)dataloader = DataLoader(dataset, batch_size=batch_size, pin_memory=True, shuffle=False, collate_fn=dataset.collate_batch, drop_last=False)return dataset, dataloader

至此,点云数据预处理部分我们就已经完成了,预处理后的点云数据将变成如下形式:

  • 原始points
  • voxels及其坐标
  • voxels中心位置
  • 点的数量
  • 点云帧编号
  • batch_size

下一篇文章中我们将开始实现PointPillars的网络部分。

input_dict`:{`voxels`, `num_points`, `coordinates`, `voxel_centers` ,  `points`, `sample_idx`,  `batch_size`}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/439700.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【CodeForces - 129C】Statues(思维,bfs)

题干&#xff1a; In this task Anna and Maria play a game with a very unpleasant rival. Anna and Maria are in the opposite squares of a chessboard (8  8): Anna is in the upper right corner, and Maria is in the lower left one. Apart from them, the board h…

一步步编写操作系统 47 48 二进制程序运行方式

操作系统并不是在功能上给予用户的支持&#xff0c;这种支持是体现在机制上。也就是说&#xff0c;单纯的操作系统&#xff0c;用户拿它什么都做不了&#xff0c;用户需要的是某种功能。而操作系统仅仅是个提供支持的平台。 虽然我们是模仿linux来写一个黑屏白字的系统&#x…

百度顶会论文复现(1):课程概述

最近百度推出了一款重磅课程《全球顶会论文作者&#xff0c;28天免费手把手带你复现顶会论文》。这个课程真的是很硬核的课程&#xff0c;这里简单记录下自己的学习过程。 文章目录1. 课程设计思路和安排2. 课程大纲1. 课程设计思路和安排 课程设计思路如下&#xff0c;共分为…

【Codeforces - 127D】Password(思维,二分+字符串Hash)

题干&#xff1a; Asterix, Obelix and their temporary buddies Suffix and Prefix has finally found the Harmony temple. However, its doors were firmly locked and even Obelix had no luck opening them. A little later they found a string s, carved on a rock be…

百度顶会论文复现(2):GAN综述

本节课主要是对GAN的发展进行了介绍&#xff0c;包括基本原理&#xff0c;训练方法&#xff0c;存在问题&#xff0c;改进以及应用场景等。实践作业则为手写数字生成。课程地址为&#xff1a;https://aistudio.baidu.com/aistudio/education/preview/493290。 文章目录1.什么是…

一步步编写操作系统 48 二进制程序的加载方式

接上节&#xff0c;程序头可以自定义&#xff0c;只要我们按照自己定义的格式去解析就行。也许我光这么一说&#xff0c;很多同学还是不能彻底明白如何自定义文件头&#xff0c;因为大多数同学都是用高级语言来写程序&#xff0c;即使用了偏底层的c语言&#xff0c;不同平台的c…

【Codeforces - 864D】Make a Permutation!(贪心,字典序)

题干&#xff1a; Ivan has an array consisting of n elements. Each of the elements is an integer from 1 to n. Recently Ivan learned about permutations and their lexicographical order. Now he wants to change (replace) minimum number of elements in his arra…

百度顶会论文复现(3):视频分类综述

本节课主要是对视频分类的发展进行了介绍&#xff0c;包括任务与背景&#xff0c;分类方法&#xff0c;前沿进展等。课程地址为&#xff1a;https://aistudio.baidu.com/aistudio/course/introduce/1340?directly1&shared1。 文章目录1. 任务与背景2. 视频分类方法2.1 双流…

一步步编写操作系统 46 linux的elf可执行文件格式1

ELF文件格式依然是分为文件头和文件体两部分&#xff0c;只是该文件头相对稍显复杂&#xff0c;类似层次化结构&#xff0c;先用个ELF header从“全局上”给出程序文件的组织结构&#xff0c;概要出程序中其它头表的位置大小等信息&#xff0c;如程序头表的大小及位置、节头表的…

百度顶会论文复现(4):飞桨API详解

本节课主要是对飞桨常用API进行了介绍&#xff0c;课程地址为&#xff1a;https://aistudio.baidu.com/aistudio/education/group/info/1340。 文章目录1.飞桨API官网2. API使用介绍3. 飞桨模型操作1.飞桨API官网 官网地址为&#xff1a;https://www.paddlepaddle.org.cn/docu…

【Codeforces - 977D】Divide by three, multiply by two(思维构造)

题干&#xff1a; Polycarp likes to play with numbers. He takes some integer number xx, writes it down on the board, and then performs with it n−1n−1 operations of the two kinds: divide the number xx by 33 (xx must be divisible by 33);multiply the numbe…

一步步编写操作系统 45 linux的elf可执行文件中的段和节

接上文&#xff0c;为了描述清楚文件格式的本质&#xff0c;咱们先从最基本的“段”说起。 程序中最重要的部分就是段&#xff08;segment&#xff09;和节&#xff08;section&#xff09;&#xff0c;它们是真正的程序体&#xff0c;是真真切切的程序资源&#xff0c;所以下…

视觉SLAM十四讲(3):三维空间刚体运动

本章需要掌握的知识点有&#xff1a;旋转矩阵&#xff0c;变换矩阵&#xff0c;四元数&#xff0c;欧拉角定义和数学表达&#xff1b;同时也要掌握Eigen库关于矩阵、几何模块的使用方法。 文章目录3.1 旋转矩阵3.1.1 点&#xff0c;向量和矩阵的关系3.1.2 坐标系间的欧式变换3.…

【CodeForces - 483C】Diverse Permutation(思维构造)

题干&#xff1a; Permutation p is an ordered set of integers p1,   p2,   ...,   pn, consisting of ndistinct positive integers not larger than n. Well denote as n the length of permutation p1,   p2,   ...,   pn. Your task is to find such…

一步步编写操作系统 47 elf格式文件分析实验

在上一节中&#xff0c;我们讲述了elf格式的部分理论知识&#xff0c;为什么是部分呢&#xff1f;因为我们本着“够用”的原则&#xff0c;只把我们需要了解的部分说完啦。不过&#xff0c;我相信大部分同学仅仅凭上一节中的理论知识还是领悟不到elf本质&#xff0c;咱们在本节…

百度飞桨顶会论文复现(5):视频分类论文之《Representation Flow for Action Recognition》篇

这次老师在课上总共领读了4篇分类论文&#xff0c;我这里分享其中的一篇论文&#xff0c;是关于使用神经网络对光流进行学习。 课程地址是&#xff1a;https://aistudio.baidu.com/aistudio/education/group/info/1340。 论文地址是&#xff1a;https://arxiv.org/abs/1810.014…

智能算法(GA、DBO等)求解零等待流水车间调度问题(NWFSP)

先做一个声明&#xff1a;文章是由我的个人公众号中的推送直接复制粘贴而来&#xff0c;因此对智能优化算法感兴趣的朋友&#xff0c;可关注我的个人公众号&#xff1a;启发式算法讨论。我会不定期在公众号里分享不同的智能优化算法&#xff0c;经典的&#xff0c;或者是近几年…

【蓝桥官网试题 - 算法提高】change(思维)

题干&#xff1a; 问题描述 数组A中共有n个元素&#xff0c;初始全为0。你可以对数组进行两种操作&#xff1a;1、将数组中的一个元素加1&#xff1b;2、将数组中所有元素乘2。求将数组A从初始状态变为目标状态B所需要的最少操作数。 输入格式 第一行一个正整数n表示数组中元…

一步步编写操作系统 50 加载内核3

接上节&#xff0c;在这里&#xff0c;我们把参数放到了栈中保存&#xff0c;大家注意到了&#xff0c;参数入栈的顺序是先从最右边的开始&#xff0c;最后压入的参数最左边的&#xff0c;其实这是某种约定&#xff0c;要不&#xff0c;为什么不先把中间的参数src入栈呢。既然主…

【POJ - 2965】The Pilots Brothers' refrigerator(暴力枚举,思维)

题干&#xff1a; The game “The Pilots Brothers: following the stripy elephant” has a quest where a player needs to open a refrigerator. There are 16 handles on the refrigerator door. Every handle can be in one of two states: open or closed. The refrige…