赞
踩
主要参考博客:
OpenPCDet环境搭建参考:【3D目标检测】环境搭建(OpenPCDet、MMdetection3d)https://blog.csdn.net/qq_44703886/article/details/131732662?spm=1001.2014.3001.5502
源码地址:OpenPCDet:https://github.com/open-mmlab/OpenPCDet
https://dgithub.xyz 或 https://bgithub.xyz
首先,看一下需要的数据集格式,按照KITTI数据结构规划自定义数据集如下:
- custom
- ├── testing
- │ ├── velodyne # 点云数据
- ├── training
- │ ├── label_2 # 标签文件
- │ ├── velodyne
其中点云数据的格式是bin文件。如果采集到的数据是rosbag包,则先进行bag2pcd,然后pcd2bin。得到bin格式的点云数据。代码如下:
- import rospy
- import rosbag
- from sensor_msgs.msg import PointCloud2
- from sensor_msgs import point_cloud2
- import struct
-
- def pointcloud2_to_pcd(point_cloud2_msg, filename):
- # 更新头部信息以包含intensity
- header = f"""# .PCD v0.7 - Point Cloud Data file format
- VERSION 0.7
- FIELDS x y z intensity
- SIZE 4 4 4 4
- TYPE F F F F
- COUNT 1 1 1 1
- WIDTH {point_cloud2_msg.width}
- HEIGHT {point_cloud2_msg.height}
- VIEWPOINT 0 0 0 1 0 0 0
- POINTS {point_cloud2_msg.width * point_cloud2_msg.height}
- DATA ascii
- """
-
- # 将点云数据(包括intensity)转换为ASCII格式并保存到PCD文件
- with open(filename, 'w') as f:
- f.write(header)
- for p in point_cloud2.read_points(point_cloud2_msg, field_names=("x", "y", "z", "intensity"), skip_nans=True):
- f.write(f"{' '.join(str(value) for value in p)}\n")
-
- def main():
- bag_file = '/home/dxy/SUSTechPOINTS/备份/rosbag/train.bag'
- topic = '/velodyne_points'
- output_directory = '/home/dxy/SUSTechPOINTS/备份/lidar/'
- frame_count = 0
-
- with rosbag.Bag(bag_file, 'r') as bag:
- for topic, msg, t in bag.read_messages(topics=[topic]):
- if 1:
- filename = f"{output_directory}{t.to_nsec()}.pcd"
- pointcloud2_to_pcd(msg, filename)
- frame_count += 1
- print(f"Processed frame {frame_count}: Saved {filename}")
- else:
- print(f"Message is not of type PointCloud2: {type(msg)}")
-
- print(f"Total frames processed: {frame_count}")
-
- if __name__ == "__main__":
- main()
- import os
- import numpy as np
-
- def read_pcd(filepath):
- lidar = []
- header_passed = False
- with open(filepath, 'r') as f:
- for line in f:
- line = line.strip()
- if line.startswith('DATA'):
- header_passed = True
- continue
- if header_passed:
- linestr = line.split()
- if len(linestr) == 3:
- linestr_convert = list(map(float, linestr)) + [1.0]
- linestr_convert[2] += 0
- print("!!!!!!!!!!!!!!!!!!!!!!ERROR")
- lidar.append(linestr_convert)
- elif len(linestr) == 4:
- linestr_convert = list(map(float, linestr))
- linestr_convert[2] += 0
- lidar.append(linestr_convert)
- return np.array(lidar)
-
- def pcd2bin(pcdfolder, binfolder, start_idx, end_idx):
- ori_path = pcdfolder
- des_path = binfolder
- if not os.path.exists(des_path):
- os.makedirs(des_path)
-
- for idx in range(start_idx, end_idx + 1):
- filename = f"{idx:06d}" # 格式化文件名,确保是六位数字,例如000001
- velodyne_file = os.path.join(ori_path, filename + '.pcd')
- if os.path.exists(velodyne_file): # 确保文件存在
- pl = read_pcd(velodyne_file)
- pl = pl.reshape(-1, 4).astype(np.float32)
- velodyne_file_new = os.path.join(des_path, filename + '.bin')
- pl.tofile(velodyne_file_new)
- else:
- print(f"File not found: {velodyne_file}")
-
- if __name__ == "__main__":
- pcdfolder = "/home/dxy/SUSTechPOINTS/data/备份/lidar_copy"
- binfolder = "/home/dxy/SUSTechPOINTS/data/备份/lidar_bin"
-
- # 可以在这里设置开始和结束的帧
- start_frame = 1
- end_frame = 35
-
- pcd2bin(pcdfolder, binfolder, start_idx=start_frame, end_idx=end_frame)
'运行
经过上面的步骤,就可以得到velodyne文件夹下所需的bin格式点云数据。
标注工具:sustechpoints
SUSTechPOINTS三维点云标注工具使用-CSDN博客
附上转换文件代码,sus2kitti.py
- import os
- import json
- import math
- import numpy as np
- import sys
-
- def trans_detection_label(src_label_path, tgt_label_path, start_idx=None, end_idx=None):
- files = os.listdir(src_label_path)
- files.sort() # 确保文件按名称排序
-
-
-
- # 初始化最大ID为0
- max_id = 0
-
- # 如果指定了 start_idx 和 end_idx,过滤出范围内的文件
- if start_idx is not None and end_idx is not None:
- files = [f for f in files if f.split('.')[0].isdigit() and start_idx <= int(f.split('.')[0]) <= end_idx]
-
- for fname in files:
- frame, _ = os.path.splitext(fname)
- print(frame)
-
- kitti_lines = []
- with open(os.path.join(src_label_path, fname), encoding='utf-8') as f:
- labels = json.load(f, strict=False)
- for label in labels:
- obj_type = label["obj_type"]
-
- # if label.get('obj_attr') == 'static':
- # continue # 跳过当前对象
-
- # 根据条件修改 obj_type
- if obj_type == 'Scooter':
- obj_type = 'Bicycle'
- elif obj_type == 'Bus':
- obj_type = 'Truck'
-
- if obj_type == 'Bicycle':
- obj_type = 'Cyclist'
-
- box_id = int(label["obj_id"])
- box_id += 282
-
-
- # 更新最大ID
- if int(box_id) > max_id:
- max_id = int(box_id)
-
-
- box_position_x = label['psr']['position']['x']
- box_position_y = label['psr']['position']['y']
- box_position_z = label['psr']['position']['z']
- box_scale_x = label['psr']['scale']['x']
- box_scale_y = label['psr']['scale']['y']
- box_scale_z = label['psr']['scale']['z']
- box_position_z_kitti = float(box_position_z) + 0 - float(box_scale_z / 2)
- rotation_yaw = -float(label['psr']['rotation']['z']) - math.pi / 2
-
- kitti_lines.append(f'{obj_type} 1.0 0 0.0 -1 -1 -1 -1 {box_scale_z:.4f} {box_scale_y:.4f} {box_scale_x:.4f} '
- f'{box_position_x:.4f} {box_position_y:.4f} {box_position_z_kitti:.4f} {rotation_yaw:.4f}\n')
-
- with open(os.path.join(tgt_label_path, frame + ".txt"), 'w') as outfile:
- outfile.writelines(kitti_lines)
- # 在处理完所有文件后打印最大ID
- print(f"The maximum ID in the sequence is: {max_id}")
-
- if __name__ == "__main__":
- src_label = "/home/dxy/SUSTechPOINTS/data/备份/label_copy" # 替换成自己的路径
- tgt_label = "/home/dxy/SUSTechPOINTS/data/备份/label_kitti/"
- # 这里你可以指定开始和结束的索引,例如处理000001到001000范围内的文件
- start_idx = 1
- end_idx = 100
- trans_detection_label(src_label, tgt_label, start_idx, end_idx)
运行分割数据集代码:
- """
- 2024.03.21
- author:alian
- 数据预处理操作
- 1.数据集分割
- """
- import os
- import random
- import shutil
- import numpy as np
-
-
- def get_train_val_txt_kitti(src_path):
- """
- 数据格式:KITTI
- # For KITTI Dataset
- └── KITTI_DATASET_ROOT
- ├── training <-- 7481 train data
- | ├── image_2 <-- for visualization
- | ├── calib
- | ├── label_2
- | └── velodyne
- └── testing <-- 7580 test data
- ├── image_2 <-- for visualization
- ├── calib
- └── velodyne
-
- src_path: KITTI_DATASET_ROOT kitti文件夹
- """
- # 1.自动生成数据集划分文件夹ImageSets
- set_path = "%s/ImageSets/"%src_path
- if os.path.exists(set_path): # 如果文件存在
- shutil.rmtree(set_path) # 清空原始数据
- os.makedirs(set_path) # 重新创建
- else:
- os.makedirs(set_path) # 自动新建文件夹
-
- # 2.训练样本分割 生成train.txt val.txt trainval.txt
- train_list = os.listdir(os.path.join(src_path,'training','velodyne'))
- random.shuffle(train_list) # 打乱顺序,随机采样
- # 设置训练和验证的比例
- train_p = 0.8
-
- # 开始写入分割文件
- f_train = open(os.path.join(set_path, "train.txt"), 'w')
- f_val = open(os.path.join(set_path, "val.txt"), 'w')
- f_trainval = open(os.path.join(set_path, "trainval.txt"), 'w')
-
- for i,src in enumerate(train_list):
- if i<int(len(train_list)*train_p): # 训练集的数量
- f_train.write(src[:-4] + '\n')
- f_trainval.write(src[:-4] + '\n')
- else:
- f_val.write(src[:-4] + '\n')
- f_trainval.write(src[:-4] + '\n')
-
- # 3.测试样本分割 生成test.txt
- test_list = os.listdir(os.path.join(src_path,'testing','velodyne'))
- f_test = open(os.path.join(set_path, "test.txt"), 'w')
- for i,src in enumerate(test_list):
- f_test.write(src[:-4] + '\n')
-
-
- if __name__=='__main__':
- """
- src_path: 数据目录
- """
- src_path = '/home/dxy/Openpcdet-Test-master/data/custom'
- get_train_val_txt_kitti(src_path)
分割数据集后,如图:
1.3 生成标准数据格式
建议复制kitti_dataset.py、kitti_dataset.yaml,重命名为custom_dataset.py、kitti_custom_dataset.yaml,修改文件路径如下:
Openpcdet-Test-master/pcdet/datasets/custom/custom_dataset.py
Openpcdet-Test-master/tools/cfgs/dataset_configs/kitti_custom_dataset.yaml
- DATASET: 'CustomDataset'
- DATA_PATH: '/home/dxy/Openpcdet-Test-master/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: ['Car:5'], # 2.修改类别
- # filter_by_difficulty: [-1], # 注释掉,防止训练报错
- }
-
- SAMPLE_GROUPS: ['Car: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
- }
- 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.custom_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=['Car'], # 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
没有报错的话,文件夹中会生成一些文件,如图:
笔者选用模型为potinpillar,其他模型以此类推
修改文件如下:
Openpcdet-Test-master/tools/cfgs/kitti_models/pointpillar.yaml
- # CLASS_NAMES: ['Car', 'Pedestrian', 'Cyclist'] # 修改类别
- CLASS_NAMES: ['Car']
- DATA_CONFIG:
- _BASE_CONFIG_: /home/dxy/Openpcdet-Test-master/tools/cfgs/dataset_configs/kitti_custom_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: False
- DB_INFO_PATH:
- - custom_dbinfos_train.pkl
- PREPARE: {
- filter_by_min_points: ['Car:5'],
- filter_by_difficulty: [-1],
- }
-
- SAMPLE_GROUPS: ['Car: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': 'Car',
- 'anchor_sizes': [[0.8, 0.6, 1.0]],
- 'anchor_rotations': [0, 1.57],
- 'anchor_bottom_heights': [-0.4],
- '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=100
python tools/demo.py --cfg_file /home/dxy/Openpcdet-Test-master/tools/cfgs/kitti_models/pointpillar.yaml --data_path /home/dxy/Openpcdet-Test-master/data/custom/testing/velodyne/ --ckpt /home/dxy/Openpcdet-Test-master/output/cfgs/kitti_models/pointpillar/default/ckpt/checkpoint_epoch_100.pth
解决办法:
pip install kornia==0.6.8
安装open3d报错:“libc++.so.1: cannot open shared object file: No such file or directory”
解决办法:
sudo apt install libc++-dev
数据训练所需:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。