detectron2(目标检测框架)无死角玩转-08:源码详解(4)-数据预处理,数据增强

以下链接是个人关于detectron2(目标检测框架),所有见解,如有错误欢迎大家指出,我会第一时间纠正。有兴趣的朋友可以加微信:a944284742相互讨论技术。若是帮助到了你什么,一定要记得点赞!因为这是对我最大的鼓励。
detectron2(目标检测框架)无死角玩转-00:目录

前言

该篇博客,主要讲解的是数据预处理,其还包含了数据增强,如中心剪切,多尺度训练等等

batch_size统一(讲解)

不知道大家有没有这样的困惑,就是进行多尺训练的时候,每张图片的尺寸都不一样,他们怎么组成一个batch_size。其实处理的过程还是还是挺简单的,首先来看看本人编写的源码(前面的博客有给出源码),在 tools/train_my.py 的def setup(args):可以看到如下几个配置:

    cfg.INPUT.CROP.ENABLED = True # 开启中心点随机剪裁数据增强
    cfg.INPUT.MAX_SIZE_TRAIN = 732# 训练图片输入的最大尺寸
    cfg.INPUT.MIN_SIZE_TRAIN = (384, 576) # 训练图片输入的最小尺寸,可以指定为多尺度训练
    cfg.INPUT.MAX_SIZE_TEST = 640 # 测试数据输入的最大尺寸
    cfg.INPUT.MIN_SIZE_TEST = 640

    cfg.INPUT.MIN_SIZE_TRAIN_SAMPLING = 'range'

首先给他们说说cfg.INPUT.MIN_SIZE_TRAIN_SAMPLING,其存在两种配置,分别为 choice 与 range :

choice : 把输入图像转化为指定的,有限的几种图片大小进行训练
range: 把输入图像转化为指定范围内,随机尺寸进行训练

就来本人编写的代码来说,配置为range模式,首先我们来看看:

# 表示图片进行缩放的时候,图片最大的边长为缩放732(包括了长和框)
cfg.INPUT.MAX_SIZE_TRAIN = 732  

# 因为配置为 range,所以图片小的边长为 384 到 576 之间的任意一个数值
cfg.INPUT.MIN_SIZE_TRAIN = (384, 576)

或许你很奇怪,为什么要这样指定?有什么意义。 \color{red}{重点如下:}
1.无论图片的尺寸怎么改变,我们一定要保证图片中的内容不会变形,比如一个人,他原本多瘦就是多瘦,多胖就多胖。
2.为满足上面的要求,也就是说,图像在缩放的时候,必须保存长宽的比例不被改变。
3.本人训练的数据集,高宽分别为492,658。 其改变大小可能为下:

torch.Size([3, 457, 613])
torch.Size([3, 501, 679])
torch.Size([3, 451, 579])
torch.Size([3, 414, 547]) 
torch.Size([3, 503, 712]) 

可以看到,其上数据的的高都在(384, 576)之间,宽是随着高的变化而变化的,但是不会超过732,同时他们都保持一个规律,那就是高和框的比都接近492/658 = 0.74。如果去计算过的朋友,发现这个比例不是完全接近于0.74.为什么呢?当然是为了数据增强,小幅度的波动能够增加网络的鲁棒性。

通过上面,我们知道在保持特征的情况下对图片进行缩放,但是依旧不明白,如何把多张尺寸不同的图片统一成一个batch_size。其实,就是把改变过大小的图片,通过填充黑色像素的办法,统一成最大图片的大小。如本人实验中,所有图片被填充成torch.Size([3, 576, 736])的张量。

预处理源码分析

上面仅仅是讲解过程,并没有证据,所以接下来,我们去分析源码,把上面的介绍一一证实。依旧从本人编写的源码 tools\train_my.py 讲起,可以看到如下:

# 注册数据集和元数据
def plain_register_dataset():

    DatasetCatalog.register("coco_my_train", lambda: load_coco_json(TRAIN_JSON, TRAIN_PATH))
    MetadataCatalog.get("coco_my_train").set(thing_classes=CLASS_NAMES,  # 可以选择开启,但是不能显示中文,所以本人关闭
                                                    evaluator_type='coco', # 指定评估方式
                                                    json_file=TRAIN_JSON,
                                                    image_root=TRAIN_PATH)

    #DatasetCatalog.register("coco_my_val", lambda: load_coco_json(VAL_JSON, VAL_PATH, "coco_2017_val"))
    DatasetCatalog.register("coco_my_val", lambda: load_coco_json(VAL_JSON, VAL_PATH))
    MetadataCatalog.get("coco_my_val").set(thing_classes=CLASS_NAMES, # 可以选择开启,但是不能显示中文,所以本人关闭
                                                evaluator_type='coco', # 指定评估方式
                                                json_file=VAL_JSON,
                                                image_root=VAL_PATH)

其上是对训练以及测试数据集的注册,那么他是被怎么获取的呢?一路追踪class Trainer(DefaultTrainer): 可以在找到 detectron2/data/build.py 中的 def build_detection_train_loader(cfg, mapper=None):函数,该函数前面博客有过简单的注释了,所以这里就不讲解了,注意的是,其中调用了一个函数:

    # 根据配置创建数据迭代器
    dataset_dicts = get_detection_dataset_dicts(
        cfg.DATASETS.TRAIN, # 指定为训练模式
        filter_empty=cfg.DATALOADER.FILTER_EMPTY_ANNOTATIONS, # 是否过滤掉注释为空的图像
        min_keypoints=cfg.MODEL.ROI_KEYPOINT_HEAD.MIN_KEYPOINTS_PER_IMAGE
        if cfg.MODEL.KEYPOINT_ON # 是否开启关键点
        else 0,
        proposal_files=cfg.DATASETS.PROPOSAL_FILES_TRAIN if cfg.MODEL.LOAD_PROPOSALS else None, # 是否预定义了训练文件
    )

实现过程如下:

def get_detection_dataset_dicts(
    dataset_names, filter_empty=True, min_keypoints=0, proposal_files=None
):

    # 根据dataset_names获取对应的数据集
    assert len(dataset_names)
    dataset_dicts = [DatasetCatalog.get(dataset_name) for dataset_name in dataset_names]
    for dataset_name, dicts in zip(dataset_names, dataset_dicts):
        assert len(dicts), "Dataset '{}' is empty!".format(dataset_name)

    # 主要是是用于把不符合的proposal_files指标的注释,更改为合适,或者删除。比如为负数的坐标,超出边界的坐标等
    # 本人没有使用所以暂时不做详细讲解
    if proposal_files is not None:
        assert len(dataset_names) == len(proposal_files)
        # load precomputed proposals from proposal files
        dataset_dicts = [
            load_proposals_into_dataset(dataset_i_dicts, proposal_file)
            for dataset_i_dicts, proposal_file in zip(dataset_dicts, proposal_files)
        ]

    dataset_dicts = list(itertools.chain.from_iterable(dataset_dicts))

    has_instances = "annotations" in dataset_dicts[0]
    
    # Keep images without instance-level GT if the dataset has semantic labels.
    # 如果存在语义分割的注释,则保留语义分割的标签
    if filter_empty and has_instances and "sem_seg_file_name" not in dataset_dicts[0]:
        dataset_dicts = filter_images_with_only_crowd_annotations(dataset_dicts)

    # 如果存在关键点的注释,则保存关键点的注释
    if min_keypoints > 0 and has_instances:
        dataset_dicts = filter_images_with_few_keypoints(dataset_dicts, min_keypoints)

    # 如果存在annotations,即box字段
    if has_instances:
        # 尝试是否有可用的类名,可以自行定义,也可以从数据集中通过"thing_classes"获得
        try:
            class_names = MetadataCatalog.get(dataset_names[0]).thing_classes
            check_metadata_consistency("thing_classes", dataset_names)
            print_instances_class_histogram(dataset_dicts, class_names)
        except AttributeError:  # class names are not available for this dataset
            pass
    return dataset_dicts

其实也没有什么特别要注意的地方,就是获得数据集的注释字典而已,我们继续查看def get_detection_dataset_dicts(…),可以看到如下代码:

    # 把数据映射成模型模型训练需要的格式
    if mapper is None:
        mapper = DatasetMapper(cfg, True)
    dataset = MapDataset(dataset, mapper)

这里就是一个重点了,进入DatasetMapper查看:

class DatasetMapper:
    """
    A callable which takes a dataset dict in Detectron2 Dataset format,
    and map it into a format used by the model.

    This is the default callable to be used to map your dataset dict into training data.
    You may need to follow it to implement your own one for customized logic.

    The callable currently does the following:

    1. Read the image from "file_name"
    2. Applies cropping/geometric transforms to the image and annotations
    3. Prepare data and annotations to Tensor and :class:`Instances`
    """

    def __init__(self, cfg, is_train=True):
        # 根据配置,决定是否采用中心点随机剪切数据增强
        if cfg.INPUT.CROP.ENABLED and is_train:
            self.crop_gen = T.RandomCrop(cfg.INPUT.CROP.TYPE, cfg.INPUT.CROP.SIZE)
            logging.getLogger(__name__).info("CropGen used in training: " + str(self.crop_gen))
        else:
            self.crop_gen = None

        # 获得图片缩放的边缘大小,以及水平翻转的配置
        self.tfm_gens = utils.build_transform_gen(cfg, is_train)

        # fmt: off
        self.img_format     = cfg.INPUT.FORMAT
        self.mask_on        = cfg.MODEL.MASK_ON
        self.mask_format    = cfg.INPUT.MASK_FORMAT
        self.keypoint_on    = cfg.MODEL.KEYPOINT_ON
        self.load_proposals = cfg.MODEL.LOAD_PROPOSALS

        # fmt: on,关键点水平翻转
        if self.keypoint_on and is_train:
            # Flip only makes sense in training
            self.keypoint_hflip_indices = utils.create_keypoint_hflip_indices(cfg.DATASETS.TRAIN)
        else:
            self.keypoint_hflip_indices = None

        # 如果使用了load_proposals,则对数据进行一些筛选以及纠正
        if self.load_proposals:
            self.min_box_side_len = cfg.MODEL.PROPOSAL_GENERATOR.MIN_SIZE
            self.proposal_topk = (
                cfg.DATASETS.PRECOMPUTED_PROPOSAL_TOPK_TRAIN
                if is_train
                else cfg.DATASETS.PRECOMPUTED_PROPOSAL_TOPK_TEST
            )
        self.is_train = is_train

    def __call__(self, dataset_dict):
        """
        # 把数据转化为训练模型需要的格式
        Args:
            dataset_dict (dict): Metadata of one image, in Detectron2 Dataset format.

        Returns:
            dict: a format that builtin models in detectron2 accept
        """
        dataset_dict = copy.deepcopy(dataset_dict)  # it will be modified by code below

        # USER: Write your own image loading if it's not from a file,读取图片,并且进行检测
        image = utils.read_image(dataset_dict["file_name"], format=self.img_format)
        utils.check_image_size(dataset_dict, image)

        # 如果没有注释,也就是说明该图像没有检测的对象,则随机进行剪切
        # 同时进行多尺度的图像变换
        if "annotations" not in dataset_dict:
            image, transforms = T.apply_transform_gens(
                ([self.crop_gen] if self.crop_gen else []) + self.tfm_gens, image
            )
        else:
            # 如果存在注释,也就是有实例对象,则围绕实例对象的中心进行剪切,# 同时进行多尺度的图像变换
            # Crop around an instance if there are instances in the image.
            # USER: Remove if you don't use cropping
            if self.crop_gen:
                crop_tfm = utils.gen_crop_transform_with_instance(
                    self.crop_gen.get_crop_size(image.shape[:2]),
                    image.shape[:2],
                    np.random.choice(dataset_dict["annotations"]),
                )
                image = crop_tfm.apply_image(image)
            image, transforms = T.apply_transform_gens(self.tfm_gens, image)
            if self.crop_gen:
                transforms = crop_tfm + transforms

        # 获得图片的宽高
        image_shape = image.shape[:2]  # h, w

        # Pytorch's dataloader is efficient on torch.Tensor due to shared-memory,
        # but not efficient on large generic data structures due to the use of pickle & mp.Queue.
        # Therefore it's important to use torch.Tensor.
        dataset_dict["image"] = torch.as_tensor(
            image.transpose(2, 0, 1).astype("float32")
        ).contiguous()
        # Can use uint8 if it turns out to be slow some day

        # USER: Remove if you don't use pre-computed proposals.
        if self.load_proposals:
            utils.transform_proposals(
                dataset_dict, image_shape, transforms, self.min_box_side_len, self.proposal_topk
            )

        if not self.is_train:
            dataset_dict.pop("annotations", None)
            dataset_dict.pop("sem_seg_file_name", None)
            return dataset_dict

        if "annotations" in dataset_dict:
            # USER: Modify this if you want to keep them for some reason.
            for anno in dataset_dict["annotations"]:
                if not self.mask_on:
                    anno.pop("segmentation", None)
                if not self.keypoint_on:
                    anno.pop("keypoints", None)

            # USER: Implement additional transformations if you have other types of data
            # 实例分割的相关操作
            annos = [
                utils.transform_instance_annotations(
                    obj, transforms, image_shape, keypoint_hflip_indices=self.keypoint_hflip_indices
                )
                for obj in dataset_dict.pop("annotations")
                if obj.get("iscrowd", 0) == 0
            ]
            instances = utils.annotations_to_instances(
                annos, image_shape, mask_format=self.mask_format
            )
            # 为masks创建一个大小刚刚好的box
            # Create a tight bounding box from masks, useful when image is cropped
            if self.crop_gen and instances.has("gt_masks"):
                instances.gt_boxes = instances.gt_masks.get_bounding_boxes()
            dataset_dict["instances"] = utils.filter_empty_instances(instances)

        # 语义分割的相关操作
        # USER: Remove if you don't do semantic/panoptic segmentation.
        if "sem_seg_file_name" in dataset_dict:
            with PathManager.open(dataset_dict.pop("sem_seg_file_name"), "rb") as f:
                sem_seg_gt = Image.open(f)
                sem_seg_gt = np.asarray(sem_seg_gt, dtype="uint8")
            sem_seg_gt = transforms.apply_segmentation(sem_seg_gt)
            sem_seg_gt = torch.as_tensor(sem_seg_gt.astype("long"))
            dataset_dict["sem_seg"] = sem_seg_gt
        return dataset_dict

个人感觉英文注释,比较重要,所以就没有删减了,总得来说DatasetMapper的作用就是进行数据映射,读取图片,调整注释。把初始数据转化为模型训练可以直接使用的数据,其中包含了数据增强,如中心点随机剪切等。其中多尺寸的实现,为其中的:

        # 获得图片缩放的边缘大小,以及水平翻转的配置
        self.tfm_gens = utils.build_transform_gen(cfg, is_train)

函数的

 tfm_gens.append(T.ResizeShortestEdge(min_size, max_size, sample_style))

中的ResizeShortestEdge可以找到其实现,过程很简单就不讲解了

batch_size统一(源码)

下面我们就来看看,多尺度的训练是如何统一成一个batch_size的。其代码的关键的部分位于 detectron2/modeling/meta_arch/retinanet.py,即RetinaNet中的:

    def preprocess_image(self, batched_inputs):
        """
        Normalize, pad and batch the input images.
        """
        images = [x["image"].to(self.device) for x in batched_inputs]
        images = [self.normalizer(x) for x in images]
        images = ImageList.from_tensors(images, self.backbone.size_divisibility)
        return images

主要工作是完成了正则化,零值填充。过程比较简单,就不讲解了。

发布了234 篇原创文章 · 获赞 1708 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/weixin_43013761/article/details/104106792