• 【3D目标检测】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
    • 2
    • 3
    • 4

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    其中数据集划分文件生成代码如下:

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    1.3 生成标准数据格式

    建议复制kitti_dataset.py、kitti_dataset.yaml,重命名为custom_dataset.py、kitti_custom_dataset.yaml,修改文件路径如下:

    OpenPCDet/pcdet/datasets/kitti/custom_dataset.py
    OpenPCDet/tools/cfgs/dataset_configs/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
          }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72

    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'
            )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307
    • 308
    • 309
    • 310
    • 311
    • 312
    • 313
    • 314
    • 315
    • 316
    • 317
    • 318
    • 319
    • 320
    • 321
    • 322
    • 323
    • 324
    • 325
    • 326
    • 327
    • 328
    • 329
    • 330
    • 331
    • 332
    • 333
    • 334
    • 335
    • 336
    • 337
    • 338
    • 339
    • 340
    • 341
    • 342
    • 343
    • 344
    • 345
    • 346
    • 347
    • 348
    • 349
    • 350
    • 351
    • 352
    • 353
    • 354
    • 355
    • 356
    • 357
    • 358
    • 359
    • 360
    • 361
    • 362
    • 363
    • 364
    • 365
    • 366
    • 367
    • 368
    • 369
    • 370
    • 371
    • 372
    • 373
    • 374
    • 375
    • 376
    • 377
    • 378
    • 379
    • 380
    • 381
    • 382
    • 383
    • 384
    • 385
    • 386
    • 387
    • 388
    • 389
    • 390
    • 391
    • 392
    • 393
    • 394
    • 395
    • 396
    • 397
    • 398
    • 399
    • 400
    • 401
    • 402
    • 403
    • 404
    • 405
    • 406
    • 407
    • 408
    • 409
    • 410
    • 411
    • 412
    • 413
    • 414
    • 415
    • 416
    • 417
    • 418
    • 419
    • 420
    • 421
    • 422
    • 423
    • 424
    • 425
    • 426
    • 427
    • 428
    • 429
    • 430
    • 431
    • 432
    • 433
    • 434
    • 435

    注:源码中已存在custom的相关文件,因为数据标注格式以kitti为标准,所以笔者是基于kitti文件的格式进行修改
    生成标注数据指令

    python -m pcdet.datasets.kitti.custom_dataset create_custom_infos tools/cfgs/dataset_configs/kitti_custom_dataset.yaml
    
    • 1

    2 模型训练

    笔者选用模型为potinpillar,其他模型以此类推
    修改文件如下:

    OpenPCDet/tools/cfgs/kitti_models/pointpillar.yaml

    # 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164

    训练指令:

    python tools/train.py --cfg_file tools/cfgs/kitti_models/pointpillar.yaml --batch_size=2 --epochs=300
    
    • 1

    成功训练如下:
    在这里插入图片描述

    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

    在这里插入图片描述
    在这里插入图片描述
    参考链接:
    1.OpenPCDet 训练自己的数据集详细教程!
    2.基于OpenPCDet实现自定义数据集的训练
    3.OpenPCDet安装、使用方式及自定义数据集训练

  • 相关阅读:
    VR航天科普体验馆VR航空馆规划遨游太空感受其中乐趣
    “控制情绪,理性交流”刍议
    Java基础之《netty(5)—NIO之Selector》
    [附源码]java毕业设计学生考试成绩分析系统
    法制博览杂志法制博览杂志社法制博览编辑部2022年第24期目录
    C# Linq中的Select和SelectMany
    Java自定义注解
    CountDownLatch的使用
    (一)TinyWebServer的环境配置与运行
    【web-攻击用户】(9.6.3)其他客户端注入攻击:客户端SQL注入、HTTP参数污染
  • 原文地址:https://blog.csdn.net/qq_44703886/article/details/136608008