OpenPCDet自定义数据集训练
引言
OpenPCDet环境搭建参考:【3D目标检测】环境搭建(OpenPCDet、MMdetection3d)
源码地址:OpenPCDet:https://github.com/open-mmlab/OpenPCDet
1 自定义数据集准备
1.1 标注工具labelCloud
源码地址:https://github.com/ch-sa/labelCloud
建议在Windows上安装,Ubuntu对Qt的版本限制比较麻烦
1.1.1 安装labelCloud
git clone https://github.com/ch-sa/labelCloud.git # 1. Clone repository
pip install -r requirements.txt # 2. Install requirements
# 3. Copy point clouds into `pointclouds` folder.
python labelCloud.py # 4. Start labelCloud
1.1.2 自定义标签
按照如下格式,修改"./labelCloud/labels/_classes.json"
文件,自定义标签
{
"classes": [
{
"name": "pedestrian",
"id": 0,
"color": "#ff0000"
},
{
"name": "stone",
"id": 1,
"color": "#7fc4ff"
},
{
"name": "paperbox",
"id": 2,
"color": "#00ffc4"
},
{
"name": "pedestrian_seated",
"id": 3,
"color": "#00e546"
},
{
"name": "pedestrian_carrying",
"id": 3,
"color": "#0046e5"
}
],
"default": 0,
"type": "object_detection",
"format": "kitti_untransformed",
"created_with": {
"name": "labelCloud",
"version": "1.1.0"
}
}
1.1.3 labelCloud配置
有效的配置,可以是数据标注工作事半功倍!
配置参数定义参考链接:https://ch-sa.github.io/labelCloud/configuration/
接着就开始标注数据吧
笔者这里按kitti格式生成label数据:
1.2 数据集准备和预处理
数据结构如下
custom
├── ImageSets
│ ├── test.txt # 数据集划分文件
│ ├── train.txt
├── testing
│ ├── velodyne # 点云数据
├── training
│ ├── label_2 # 标签文件
│ ├── velodyne
其中数据集划分文件生成代码如下:
def get_train_val_txt():
object = '/media/ll/L/llr/a2023_my_3d/OpenPCDet/data/custom' # 根据自定义的数据集名称
# 1.自动生成数据集划分文件夹ImageSets
if os.path.exists("%s/ImageSets/"%object): # 如果文件存在
shutil.rmtree("%s/ImageSets/"%object) # 清空原始数据
os.makedirs('%s/ImageSets/'%object) # 重新创建
else:
os.makedirs('%s/ImageSets/'%object) # 自动新建文件夹
# 设置训练和验证的比例
train_percent = 0.8
val_percent = 0.2
xmlfilepath = '%s/label_2'%object
total_xml = os.listdir(xmlfilepath) # 遍历标签文件
num = len(total_xml) # 统计标签文件的数量
list = list(range(num)) # 标签文件的索引
num_test = int(num * val_percent) # 验证集的数量
num_train = num-num_test # 训练集的数量
# 挑选训练集
train_list = random.sample(list, num_train) # 随机挑选标签文件的索引(要达到训练集的数量)
# train_list = np.array(list).reshape(int(num/5),5)[:,:4].flatten() # 前提是5的倍数
for i in list:
list.remove(i) # 将已挑选为训练集的索引删除,剩下的便是验证集的索引(训练集与验证集不可存在交集)
val_list = list
ftrain = open('%s/ImageSets/train.txt'%object, 'w') # 写入文件
fval = open('%s/ImageSets/test.txt'%object, 'w')
# 进行标签文件路径的写入,用于后续数据集的转换
for i in range(num):
name = total_xml[i][:-4] + '\n' # 无后缀名
if i in train_list:
ftrain.write(name)
else:
fval.write(name)
ftrain.close()
fval.close()
1.3 生成标准数据格式
建议复制kitti_dataset.py、kitti_dataset.yaml,重命名为custom_dataset.py、kitti_custom_dataset.yaml,修改文件路径如下:
OpenPCDet/pcdet/datasets/kitti/custom_dataset.py
DATASET: 'CustomDataset'
DATA_PATH: '/media/ll/L/llr/a2023_my_3d/OpenPCDet/data/custom' # 1.绝对路径
# If this config file is modified then pcdet/models/detectors/detector3d_template.py:
# Detector3DTemplate::build_networks:model_info_dict needs to be modified.
POINT_CLOUD_RANGE: [-70.4, -40, -3, 70.4, 40, 1] # x=[-70.4, 70.4], y=[-40,40], z=[-3,1] 根据自己的标注框进行调整
DATA_SPLIT: {
'train': train,
'test': val
}
INFO_PATH: {
'train': [custom_infos_train.pkl],
'test': [custom_infos_val.pkl],
}
GET_ITEM_LIST: ["points"]
FOV_POINTS_ONLY: True
POINT_FEATURE_ENCODING: {
encoding_type: absolute_coordinates_encoding,
used_feature_list: ['x', 'y', 'z', 'intensity'],
src_feature_list: ['x', 'y', 'z', 'intensity'],
}
# Same to pv_rcnn[DATA_AUGMENTOR]
DATA_AUGMENTOR:
DISABLE_AUG_LIST: ['placeholder']
AUG_CONFIG_LIST:
- NAME: gt_sampling
# Notice that 'USE_ROAD_PLANE'
USE_ROAD_PLANE: False
DB_INFO_PATH:
- custom_dbinfos_train.pkl # pcdet/datasets/augmentor/database_ampler.py:line 26
PREPARE: {
filter_by_min_points: ['pedestrian:5', 'stone:5'], # 2.修改类别
# filter_by_difficulty: [-1], # 注释掉,防止训练报错
}
SAMPLE_GROUPS: ['pedestrian:15', 'stone:15'] # 3. 修改类别
NUM_POINT_FEATURES: 4
DATABASE_WITH_FAKELIDAR: False
REMOVE_EXTRA_WIDTH: [0.0, 0.0, 0.0]
LIMIT_WHOLE_SCENE: True
- NAME: random_world_flip
ALONG_AXIS_LIST: ['x']
- NAME: random_world_rotation
WORLD_ROT_ANGLE: [-0.78539816, 0.78539816]
- NAME: random_world_scaling
WORLD_SCALE_RANGE: [0.95, 1.05]
DATA_PROCESSOR:
- NAME: mask_points_and_boxes_outside_range
REMOVE_OUTSIDE_BOXES: True
- NAME: shuffle_points
SHUFFLE_ENABLED: {
'train': True,
'test': False
}
- NAME: transform_points_to_voxels
VOXEL_SIZE: [0.05, 0.05, 0.1]
MAX_POINTS_PER_VOXEL: 5
MAX_NUMBER_OF_VOXELS: {
'train': 16000,
'test': 40000
}
OpenPCDet/tools/cfgs/dataset_configs/kitti_custom_dataset.yaml
import copy
import pickle
import os
import numpy as np
from skimage import io
from ...ops.roiaware_pool3d import roiaware_pool3d_utils
from ...utils import box_utils, common_utils, object3d_custom
from ..dataset import DatasetTemplate
# 定义属于自己的数据集,集成数据集模板
class CustomDataset(DatasetTemplate):
def __init__(self, dataset_cfg, class_names, training=True, root_path=None, logger=None, ext='.bin'):
"""
Args:
root_path:
dataset_cfg:
class_names:
training:
logger:
"""
super().__init__(
dataset_cfg=dataset_cfg, class_names=class_names, training=training, root_path=root_path, logger=logger
)
self.split = self.dataset_cfg.DATA_SPLIT[self.mode]
self.root_split_path = os.path.join(self.root_path, ('training' if self.split != 'test' else 'testing'))
split_dir = os.path.join(self.root_path, 'ImageSets',(self.split + '.txt'))
self.sample_id_list = [x.strip() for x in open(split_dir).readlines()] if os.path.exists(split_dir) else None
self.custom_infos = []
self.include_custom_data(self.mode)
self.ext = ext
# 用于导入自定义数据
def include_custom_data(self, mode):
if self.logger is not None:
self.logger.info('Loading Custom dataset.')
custom_infos = []
for info_path in self.dataset_cfg.INFO_PATH[mode]:
info_path = self.root_path / info_path
if not info_path.exists():
continue
with open(info_path, 'rb') as f:
infos = pickle.load(f)
custom_infos.extend(infos)
self.custom_infos.extend(custom_infos)
if self.logger is not None:
self.logger.info('Total samples for CUSTOM dataset: %d' % (len(custom_infos)))
# 用于获取标签的标注信息
def get_infos(self, num_workers=4, has_label=True, count_inside_pts=True, sample_id_list=None):
import concurrent.futures as futures
# 线程函数,主要是为了多线程读取数据,加快处理速度
# 处理一帧
def process_single_scene(sample_idx):
print('%s sample_idx: %s' % (self.split, sample_idx))
# 创建一个用于存储一帧信息的空字典
info = {}
# 定义该帧点云信息,pointcloud_info
pc_info = {'num_features': 4, 'lidar_idx': sample_idx}
# 将pc_info这个字典作为info字典里的一个键值对的值,其键名为‘point_cloud’添加到info里去
info['point_cloud'] = pc_info
'''
# image信息和calib信息都暂时不需要
# image_info = {'image_idx': sample_idx, 'image_shape': self.get_image_shape(sample_idx)}
# info['image'] = image_info
# calib = self.get_calib(sample_idx)
# P2 = np.concatenate([calib.P2, np.array([[0., 0., 0., 1.]])], axis=0)
# R0_4x4 = np.zeros([4, 4], dtype=calib.R0.dtype)
# R0_4x4[3, 3] = 1.
# R0_4x4[:3, :3] = calib.R0
# V2C_4x4 = np.concatenate([calib.V2C, np.array([[0., 0., 0., 1.]])], axis=0)
# calib_info = {'P2': P2, 'R0_rect': R0_4x4, 'Tr_velo_to_cam': V2C_4x4}
# info['calib'] = calib_info
'''
if has_label:
# 通过get_label函数,读取出该帧的标签标注信息
obj_list = self.get_label(sample_idx)
# 创建用于存储该帧标注信息的空字典
annotations = {}
# 下方根据标注文件里的属性将对应的信息加入到annotations的键值对,可以根据自己的需求取舍
annotations['name'] = np.array([obj.cls_type for obj in obj_list])
# annotations['truncated'] = np.array([obj.truncation for obj in obj_list])
# annotations['occluded'] = np.array([obj.occlusion for obj in obj_list])
# annotations['alpha'] = np.array([obj.alpha for obj in obj_list])
# annotations['bbox'] = np.concatenate([obj.box2d.reshape(1, 4) for obj in obj_list], axis=0)
annotations['dimensions'] = np.array([[obj.l, obj.h, obj.w] for obj in obj_list]) # lhw(camera) format
annotations['location'] = np.concatenate([obj.loc.reshape(1, 3) for obj in obj_list], axis=0)
annotations['rotation_y'] = np.array([obj.ry for obj in obj_list])
annotations['score'] = np.array([obj.score for obj in obj_list])
# annotations['difficulty'] = np.array([obj.level for obj in obj_list], np.int32)
# 统计有效物体的个数,即去掉类别名称为“Dontcare”以外的
num_objects = len([obj.cls_type for obj in obj_list if obj.cls_type != 'DontCare'])
# 统计物体的总个数,包括了Dontcare
num_gt = len(annotations['name'])
# 获得当前的index信息
index = list(range(num_objects)) + [-1] * (num_gt - num_objects)
annotations['index'] = np.array(index, dtype=np.int32)
# 从annotations里提取出从标注信息里获取的location、dims、rots等信息,赋值给对应的变量
loc = annotations['location'][:num_objects]
dims = annotations['dimensions'][:num_objects]
rots = annotations['rotation_y'][:num_objects]
# 由于我们的数据集本来就是基于雷达坐标系标注,所以无需坐标转换
#loc_lidar = calib.rect_to_lidar(loc)
loc_lidar = self.get_calib(loc)
# 原来的dims排序是高宽长hwl,现在转到pcdet的统一坐标系下,按lhw排布
l, h, w = dims[:, 0:1], dims[:, 1:2], dims[:, 2:3]
# 由于我们基于雷达坐标系标注,所以获取的中心点本来就是空间中心,所以无需从底面中心转到空间中心
# bottom center -> object center: no need for loc_lidar[:, 2] += h[:, 0] / 2
# print("sample_idx: ", sample_idx, "loc: ", loc, "loc_lidar: " , sample_idx, loc_lidar)
# get gt_boxes_lidar see https://zhuanlan.zhihu.com/p/152120636
# loc_lidar[:, 2] += h[:, 0] / 2
gt_boxes_lidar = np.concatenate([loc_lidar, l, w, h, -(np.pi / 2 + rots[..., np.newaxis])], axis=1)
# 将雷达坐标系下的真值框信息存入annotations中
annotations['gt_boxes_lidar'] = gt_boxes_lidar
# 将annotations这整个字典作为info字典里的一个键值对的值
info['annos'] = annotations
return info
# 后续的由于没有calib信息和image信息,所以可以直接注释
'''
# if count_inside_pts:
# points = self.get_lidar(sample_idx)
# calib = self.get_calib(sample_idx)
# pts_rect = calib.lidar_to_rect(points[:, 0:3])
# fov_flag = self.get_fov_flag(pts_rect, info['image']['image_shape'], calib)
# pts_fov = points[fov_flag]
# corners_lidar = box_utils.boxes_to_corners_3d(gt_boxes_lidar)
# num_points_in_gt = -np.ones(num_gt, dtype=np.int32)
# for k in range(num_objects):
# flag = box_utils.in_hull(pts_fov[:, 0:3], corners_lidar[k])
# num_points_in_gt[k] = flag.sum()
# annotations['num_points_in_gt'] = num_points_in_gt
# return info
'''
sample_id_list = sample_id_list if sample_id_list is not None else self.sample_id_list
with futures.ThreadPoolExecutor(num_workers) as executor:
infos = executor.map(process_single_scene, sample_id_list)
return list(infos)
# 此时返回值infos是列表,列表元素为字典类型
# 用于获取标定信息
def get_calib(self, loc):
# calib_file = self.root_split_path / 'calib' / ('%s.txt' % idx)
# assert calib_file.exists()
# return calibration_kitti.Calibration(calib_file)
# loc_lidar = np.concatenate([np.array((float(loc_obj[2]),float(-loc_obj[0]),float(loc_obj[1]-2.3)),dtype=np.float32).reshape(1,3) for loc_obj in loc])
# return loc_lidar
# 这里做了一个由相机坐标系到雷达坐标系翻转(都遵从右手坐标系),但是 -2.3这个数值具体如何得来需要再看下
# 我们的label中的xyz就是在雷达坐标系下,不用转变,直接赋值
loc_lidar = np.concatenate([np.array((float(loc_obj[0]),float(loc_obj[1]),float(loc_obj[2])),dtype=np.float32).reshape(1,3) for loc_obj in loc])
return loc_lidar
# 用于获取标签
def get_label(self, idx):
# 从指定路径中提取txt内容
label_file = self.root_split_path / 'label_2' / ('%s.txt' % idx)
assert label_file.exists()
# 主要就是从这个函数里获取具体的信息
return object3d_custom.get_objects_from_label(label_file)
# 用于获取雷达点云信息
def get_lidar(self, idx, getitem):
"""
Loads point clouds for a sample
Args:
index (int): Index of the point cloud file to get.
Returns:
np.array(N, 4): point cloud.
"""
# get lidar statistics
if getitem == True:
lidar_file = self.root_split_path + '/velodyne/' + ('%s.bin' % idx)
else:
lidar_file = self.root_split_path / 'velodyne' / ('%s.bin' % idx)
return np.fromfile(str(lidar_file), dtype=np.float32).reshape(-1, 4)
# 用于数据集划分
def set_split(self, split):
super().__init__(
dataset_cfg=self.dataset_cfg, class_names=self.class_names, training=self.training, root_path=self.root_path, logger=self.logger
)
self.split = split
self.root_split_path = self.root_path / ('training' if self.split != 'test' else 'testing')
split_dir = self.root_path / 'ImageSets' / (self.split + '.txt')
self.sample_id_list = [x.strip() for x in open(split_dir).readlines()] if split_dir.exists() else None
# 创建真值数据库
# Create gt database for data augmentation
def create_groundtruth_database(self, info_path=None, used_classes=None, split='train'):
import torch
database_save_path = Path(self.root_path) / ('gt_database' if split == 'train' else ('gt_database_%s' % split))
db_info_save_path = Path(self.root_path) / ('custom_dbinfos_%s.pkl' % split)
database_save_path.mkdir(parents=True, exist_ok=True)
all_db_infos = {}
with open(info_path, 'rb') as f:
infos = pickle.load(f)
for k in range(len(infos)):
print('gt_database sample: %d/%d' % (k + 1, len(infos)))
info = infos[k]
sample_idx = info['point_cloud']['lidar_idx']
points = self.get_lidar(sample_idx,False)
annos = info['annos']
names = annos['name']
# difficulty = annos['difficulty']
# bbox = annos['bbox']
gt_boxes = annos['gt_boxes_lidar']
num_obj = gt_boxes.shape[0]
point_indices = roiaware_pool3d_utils.points_in_boxes_cpu(
torch.from_numpy(points[:, 0:3]), torch.from_numpy(gt_boxes)
).numpy() # (nboxes, npoints)
for i in range(num_obj):
filename = '%s_%s_%d.bin' % (sample_idx, names[i], i)
filepath = database_save_path / filename
gt_points = points[point_indices[i] > 0]
gt_points[:, :3] -= gt_boxes[i, :3]
with open(filepath, 'w') as f:
gt_points.tofile(f)
if (used_classes is None) or names[i] in used_classes:
db_path = str(filepath.relative_to(self.root_path)) # gt_database/xxxxx.bin
# db_info = {'name': names[i], 'path': db_path, 'image_idx': sample_idx, 'gt_idx': i,
# 'box3d_lidar': gt_boxes[i], 'num_points_in_gt': gt_points.shape[0],
# 'difficulty': difficulty[i], 'bbox': bbox[i], 'score': annos['score'][i]}
db_info = {'name': names[i], 'path': db_path, 'gt_idx': i,
'box3d_lidar': gt_boxes[i], 'num_points_in_gt': gt_points.shape[0], 'score': annos['score'][i]}
if names[i] in all_db_infos:
all_db_infos[names[i]].append(db_info)
else:
all_db_infos[names[i]] = [db_info]
for k, v in all_db_infos.items():
print('Database %s: %d' % (k, len(v)))
with open(db_info_save_path, 'wb') as f:
pickle.dump(all_db_infos, f)
# 生成预测字典信息
@staticmethod
def generate_prediction_dicts(batch_dict, pred_dicts, class_names, output_path=None):
"""
Args:
batch_dict:
frame_id:
pred_dicts: list of pred_dicts
pred_boxes: (N,7), Tensor
pred_scores: (N), Tensor
pred_lables: (N), Tensor
class_names:
output_path:
Returns:
"""
def get_template_prediction(num_smaples):
ret_dict = {
'name': np.zeros(num_smaples), 'alpha' : np.zeros(num_smaples),
'dimensions': np.zeros([num_smaples, 3]), 'location': np.zeros([num_smaples, 3]),
'rotation_y': np.zeros(num_smaples), 'score': np.zeros(num_smaples),
'boxes_lidar': np.zeros([num_smaples, 7])
}
return ret_dict
def generate_single_sample_dict(batch_index, box_dict):
pred_scores = box_dict['pred_scores'].cpu().numpy()
pred_boxes = box_dict['pred_boxes'].cpu().numpy()
pred_labels = box_dict['pred_labels'].cpu().numpy()
# Define an empty template dict to store the prediction information, 'pred_scores.shape[0]' means 'num_samples'
pred_dict = get_template_prediction(pred_scores.shape[0])
# If num_samples equals zero then return the empty dict
if pred_scores.shape[0] == 0:
return pred_dict
# No calibration files
# pred_boxes_camera = box_utils.boxes3d_lidar_to_kitti_camera(pred_boxes,None)
pred_dict['name'] = np.array(class_names)[pred_labels - 1]
# pred_dict['alpha'] = -np.arctan2(-pred_boxes[:, 1], pred_boxes[:, 0]) + pred_boxes_camera[:, 6]
# pred_dict['dimensions'] = pred_boxes_camera[:, 3:6]
# pred_dict['location'] = pred_boxes_camera[:, 0:3]
# pred_dict['rotation_y'] = pred_boxes_camera[:, 6]
pred_dict['score'] = pred_scores
pred_dict['boxes_lidar'] = pred_boxes
return pred_dict
annos = []
for index, box_dict in enumerate(pred_dicts):
frame_id = batch_dict['frame_id'][index]
single_pred_dict = generate_single_sample_dict(index, box_dict)
single_pred_dict['frame_id'] = frame_id
annos.append(single_pred_dict)
# Output pred results to Output-path in .txt file
if output_path is not None:
cur_det_file = output_path / ('%s.txt' % frame_id)
with open(cur_det_file, 'w') as f:
bbox = single_pred_dict['bbox']
loc = single_pred_dict['location']
dims = single_pred_dict['dimensions'] # lhw -> hwl: lidar -> camera
for idx in range(len(bbox)):
print('%s -1 -1 %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f'
% (single_pred_dict['name'][idx], single_pred_dict['alpha'][idx],
bbox[idx][0], bbox[idx][1], bbox[idx][2], bbox[idx][3],
dims[idx][1], dims[idx][2], dims[idx][0], loc[idx][0],
loc[idx][1], loc[idx][2], single_pred_dict['rotation_y'][idx],
single_pred_dict['score'][idx]), file=f)
return annos
def evaluation(self, det_annos, class_names, **kwargs):
if 'annos' not in self.custom_infos[0].keys():
return None, {}
from .kitti_object_eval_python import eval as kitti_eval
eval_det_annos = copy.deepcopy(det_annos)
eval_gt_annos = [copy.deepcopy(info['annos']) for info in self.kitti_infos]
ap_result_str, ap_dict = kitti_eval.get_official_eval_result(eval_gt_annos, eval_det_annos, class_names)
return ap_result_str, ap_dict
# 用于返回训练帧的总个数
def __len__(self):
if self._merge_all_iters_to_one_epoch:
return len(self.sample_id_list) * self.total_epochs
return len(self.custom_infos)
# 用于将点云与3D标注框均转至前述统一坐标定义下,送入数据基类提供的self.prepare_data()
def __getitem__(self, index): ## 修改如下
if self._merge_all_iters_to_one_epoch:
index = index % len(self.custom_infos)
info = copy.deepcopy(self.custom_infos[index])
sample_idx = info['point_cloud']['lidar_idx']
points = self.get_lidar(sample_idx, True)
input_dict = {
'frame_id': self.sample_id_list[index],
'points': points
}
if 'annos' in info:
annos = info['annos']
annos = common_utils.drop_info_with_name(annos, name='DontCare')
gt_names = annos['name']
gt_boxes_lidar = annos['gt_boxes_lidar']
input_dict.update({
'gt_names': gt_names,
'gt_boxes': gt_boxes_lidar
})
data_dict = self.prepare_data(data_dict=input_dict)
return data_dict
# 用于创建自定义数据集的信息
def create_custom_infos(dataset_cfg, class_names, data_path, save_path, workers=4):
dataset = CustomDataset(dataset_cfg=dataset_cfg, class_names=class_names, root_path=data_path, training=False)
train_split, val_split = 'train', 'val'
# 定义文件的路径和名称
train_filename = save_path / ('custom_infos_%s.pkl' % train_split)
val_filename = save_path / ('custom_infos_%s.pkl' % val_split)
trainval_filename = save_path / 'custom_infos_trainval.pkl'
test_filename = save_path / 'custom_infos_test.pkl'
print('---------------Start to generate data infos---------------')
dataset.set_split(train_split)
# 执行完上一步,得到train相关的保存文件,以及sample_id_list的值为train.txt文件下的数字
# 下面是得到train.txt中序列相关的所有点云数据的信息,并且进行保存
custom_infos_train = dataset.get_infos(num_workers=workers, has_label=True, count_inside_pts=True)
with open(train_filename, 'wb') as f:
pickle.dump(custom_infos_train, f)
print('Custom info train file is saved to %s' % train_filename)
dataset.set_split(val_split)
# 对验证集的数据进行信息统计并保存
custom_infos_val = dataset.get_infos(num_workers=workers, has_label=True, count_inside_pts=True)
with open(val_filename, 'wb') as f:
pickle.dump(custom_infos_val, f)
print('Custom info val file is saved to %s' % val_filename)
with open(trainval_filename, 'wb') as f:
pickle.dump(custom_infos_train + custom_infos_val, f)
print('Custom info trainval file is saved to %s' % trainval_filename)
dataset.set_split('test')
# kitti_infos_test = dataset.get_infos(num_workers=workers, has_label=False, count_inside_pts=False)
custom_infos_test = dataset.get_infos(num_workers=workers, has_label=False, count_inside_pts=False)
with open(test_filename, 'wb') as f:
pickle.dump(custom_infos_test, f)
print('Custom info test file is saved to %s' % test_filename)
print('---------------Start create groundtruth database for data augmentation---------------')
# 用trainfile产生groundtruth_database
# 只保存训练数据中的gt_box及其包围点的信息,用于数据增强
dataset.set_split(train_split)
dataset.create_groundtruth_database(info_path=train_filename, split=train_split)
print('---------------Data preparation Done---------------')
if __name__=='__main__':
import sys
if sys.argv.__len__() > 1 and sys.argv[1] == 'create_custom_infos':
import yaml
from pathlib import Path
from easydict import EasyDict
dataset_cfg = EasyDict(yaml.safe_load(open(sys.argv[2])))
ROOT_DIR = (Path(__file__).resolve().parent / '../../../').resolve()
create_custom_infos(
dataset_cfg=dataset_cfg,
class_names=['pedestrian', 'stone'], # 1.修改类别
data_path=ROOT_DIR / 'data' / 'custom',
save_path=ROOT_DIR / 'data' / 'custom'
)
注:源码中已存在custom的相关文件,因为数据标注格式以kitti为标准,所以笔者是基于kitti文件的格式进行修改
生成标注数据指令
python -m pcdet.datasets.kitti.custom_dataset create_custom_infos tools/cfgs/dataset_configs/kitti_custom_dataset.yaml
2 模型训练
笔者选用模型为potinpillar,其他模型以此类推
修改文件如下:
# CLASS_NAMES: ['Car', 'Pedestrian', 'Cyclist'] # 修改类别
CLASS_NAMES: ['pedestrian', 'stone']
DATA_CONFIG:
_BASE_CONFIG_: /media/ll/L/llr/a2023_my_3d/OpenPCDet/tools/cfgs/dataset_configs/alian_dataset.yaml
POINT_CLOUD_RANGE: [0, -39.68, -3, 69.12, 39.68, 1]
DATA_PROCESSOR:
- NAME: mask_points_and_boxes_outside_range
REMOVE_OUTSIDE_BOXES: True
- NAME: shuffle_points
SHUFFLE_ENABLED: {
'train': True,
'test': False
}
- NAME: transform_points_to_voxels
VOXEL_SIZE: [0.16, 0.16, 4]
MAX_POINTS_PER_VOXEL: 32
MAX_NUMBER_OF_VOXELS: {
'train': 16000,
'test': 40000
}
DATA_AUGMENTOR:
DISABLE_AUG_LIST: ['placeholder']
AUG_CONFIG_LIST:
- NAME: gt_sampling
USE_ROAD_PLANE: True
DB_INFO_PATH:
- kitti_dbinfos_train.pkl
PREPARE: {
filter_by_min_points: ['pedestrian:5', 'stone:5'],
filter_by_difficulty: [-1],
}
SAMPLE_GROUPS: ['pedestrian:5', 'stone:5']
NUM_POINT_FEATURES: 4
DATABASE_WITH_FAKELIDAR: False
REMOVE_EXTRA_WIDTH: [0.0, 0.0, 0.0]
LIMIT_WHOLE_SCENE: False
- NAME: random_world_flip
ALONG_AXIS_LIST: ['x']
- NAME: random_world_rotation
WORLD_ROT_ANGLE: [-0.78539816, 0.78539816]
- NAME: random_world_scaling
WORLD_SCALE_RANGE: [0.95, 1.05]
MODEL:
NAME: PointPillar
VFE:
NAME: PillarVFE
WITH_DISTANCE: False
USE_ABSLOTE_XYZ: True
USE_NORM: True
NUM_FILTERS: [64]
MAP_TO_BEV:
NAME: PointPillarScatter
NUM_BEV_FEATURES: 64
BACKBONE_2D:
NAME: BaseBEVBackbone
LAYER_NUMS: [3, 5, 5]
LAYER_STRIDES: [2, 2, 2]
NUM_FILTERS: [64, 128, 256]
UPSAMPLE_STRIDES: [1, 2, 4]
NUM_UPSAMPLE_FILTERS: [128, 128, 128]
DENSE_HEAD:
NAME: AnchorHeadSingle
CLASS_AGNOSTIC: False
USE_DIRECTION_CLASSIFIER: True
DIR_OFFSET: 0.78539
DIR_LIMIT_OFFSET: 0.0
NUM_DIR_BINS: 2
# anchor配置,需要适配自己的数据集
ANCHOR_GENERATOR_CONFIG: [
# {
# 'class_name': 'Car',
# 'anchor_sizes': [[3.9, 1.6, 1.56]],
# 'anchor_rotations': [0, 1.57],
# 'anchor_bottom_heights': [-1.78],
# 'align_center': False,
# 'feature_map_stride': 2,
# 'matched_threshold': 0.6,
# 'unmatched_threshold': 0.45
# },
{
'class_name': 'pedestrian',
'anchor_sizes': [[0.8, 0.6, 1.9]],
'anchor_rotations': [0, 1.57],
'anchor_bottom_heights': [-0.6],
'align_center': False,
'feature_map_stride': 2,
'matched_threshold': 0.5,
'unmatched_threshold': 0.35
},
{
'class_name': 'stone',
'anchor_sizes': [[1.0, 1.0, 0.73]],
'anchor_rotations': [0, 1.57],
'anchor_bottom_heights': [-0.6],
'align_center': False,
'feature_map_stride': 2,
'matched_threshold': 0.5,
'unmatched_threshold': 0.35
}
]
TARGET_ASSIGNER_CONFIG:
NAME: AxisAlignedTargetAssigner
POS_FRACTION: -1.0
SAMPLE_SIZE: 512
NORM_BY_NUM_EXAMPLES: False
MATCH_HEIGHT: False
BOX_CODER: ResidualCoder
LOSS_CONFIG:
LOSS_WEIGHTS: {
'cls_weight': 1.0,
'loc_weight': 2.0,
'dir_weight': 0.2,
'code_weights': [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
}
POST_PROCESSING:
RECALL_THRESH_LIST: [0.3, 0.5, 0.7]
SCORE_THRESH: 0.1
OUTPUT_RAW_SCORE: False
EVAL_METRIC: kitti
NMS_CONFIG:
MULTI_CLASSES_NMS: False
NMS_TYPE: nms_gpu
NMS_THRESH: 0.01
NMS_PRE_MAXSIZE: 4096
NMS_POST_MAXSIZE: 500
OPTIMIZATION:
BATCH_SIZE_PER_GPU: 4
NUM_EPOCHS: 80
OPTIMIZER: adam_onecycle
LR: 0.003
WEIGHT_DECAY: 0.01
MOMENTUM: 0.9
MOMS: [0.95, 0.85]
PCT_START: 0.4
DIV_FACTOR: 10
DECAY_STEP_LIST: [35, 45]
LR_DECAY: 0.1
LR_CLIP: 0.0000001
LR_WARMUP: False
WARMUP_EPOCH: 1
GRAD_NORM_CLIP: 10
训练指令:
python tools/train.py --cfg_file tools/cfgs/kitti_models/pointpillar.yaml --batch_size=2 --epochs=300
成功训练如下:
3 自定义模型测试
测试demo代码:
python tools/demo.py --cfg_file /media/ll/L/llr/a2023_my_3d/OpenPCDet/tools/cfgs/kitti_models/pointpillar.yaml --data_path /media/ll/L/llr/a2023_my_3d/OpenPCDet/data/custom/testing/velodyne/ --ckpt /media/ll/L/llr/a2023_my_3d/OpenPCDet/output/cfgs/kitti_models/pointpillar/default/ckpt/checkpoint_epoch_300.pth
参考链接:
1.OpenPCDet 训练自己的数据集详细教程!
2.基于OpenPCDet实现自定义数据集的训练
3.OpenPCDet安装、使用方式及自定义数据集训练