深度学习——SSD目标检测网络源码学习之图像预处理

本文详细解析了SSD网络中的图像预处理步骤,包括训练数据读取、图像增强如裁剪、翻转及颜色变化,以及目标框信息的更新方法。特别介绍了剪裁图像后目标框坐标调整和筛选过程。

抽空把这个网络细究一下,希望大佬指正~~

大致理解:SSD网络抽取不同的特征图,每个特征图可以看成是一个网格图,每个点即是一个锚点,以锚点为中心,可以生成不同大小和比例的anchor,这些anchor都是可能的目标。目标检测网络分为目标定位和分类两个部分,分类很简单,就是在每个特征图上的每个点的每个anchor都进行分类,SSD网络中把背景也单独分成了一类,至于定位,就涉及到了边框回归问题(bounding-box regression)。边框回归最早出现在R-CNN中,其意思就是,我们的网络可以在每个特征图每个点的每个anchor上预估一个边框回归的值,而真实的边框回归值也可以根据真实目标所在的位置计算出来,进而计算估计值和真实值之间的偏差。

1.读取训练数据

源码中的训练数据读取在datasets文件夹中。数据读取为以下几行代码

dataset_dir ='./datasets/train/'
dataset_split_name = 'train'
dataset_name = 'pascalvoc_2012'
dataset = dataset_factory.get_dataset(dataset_name, dataset_split_name, dataset_dir)

进入dataset_factory可以看到有cifar,imagenet和pascalvoc三种数据集。这里我选择的是voc2012数据集的格式来制作和读取训练数据,源码中voc生成tfrecord的过程非常简单,这里不多描述。

可以进入pascalvoc2012.py查看相应的配置。再看其中的get_split函数,主要是用到了slim.dataset.Dataset()函数来读取数据。具体可以参考tensorflow从磁盘读取数据

2.数据预处理

深度学习图像处理上的预处理主要是做一些图像增广,通常的操作是裁剪、随机亮度、随机对比度、白化等,SSD中,由于涉及到了目标框的标注,因此在剪裁图像之后需要对目标框的信息进行相应的改变。

源码中数据处理在preprocessing文件夹中,其中,preprocessing_factory.py用于选择使用何种预处理方式。

看源码时,发现python有直接返回函数的用法:

def get_preprocessing(name, is_training=False):
    
    preprocessing_fn_map = {
        'ssd_300_vgg': ssd_vgg_preprocessing,
        'ssd_512_vgg': ssd_vgg_preprocessing,
    }

    if name not in preprocessing_fn_map:
        raise ValueError('Preprocessing name [%s] was not recognized' % name)

    def preprocessing_fn(image, labels, bboxes,
                         out_shape, data_format='NHWC', **kwargs):
        return preprocessing_fn_map[name].preprocess_image(
            image, labels, bboxes, out_shape, data_format=data_format,
            is_training=is_training, **kwargs)
    return preprocessing_fn

百度了 一下这种用法,可以理解为把函数也看成了一个类,因此在外部调用get_preprocessing时,返回的是preprocessing_fn这一个类的实例化:

image_preprocessing_fn = preprocessing_factory.get_preprocessing(
        preprocessing_name, is_training=True)

个人觉得这种方式可以延迟函数的调用,便于在执行前检查参数,但是看源码时这层层调用确实让人懵逼,具体解释可以围观知乎:Python 里为什么函数可以返回一个函数内部定义的函数? 

再看具体的图像预处理方法,返回的也是一个函数preprocess_image,可以看到训练和验证时,预处理方法是不一样的。

def preprocess_image(image,
                     labels,
                     bboxes,
                     out_shape,
                     data_format,
                     is_training=False,
                     **kwargs):

    if is_training:
        return preprocess_for_train(image, labels, bboxes,
                                    out_shape=out_shape,
                                    data_format=data_format)
    else:
        return preprocess_for_eval(image, labels, bboxes,
                                   out_shape=out_shape,
                                   data_format=data_format,
                                   **kwargs)

训练时的预处理流程:1.剪裁图像;2.随机左右翻转;3.颜色改变;4.白化。这里麻烦一点的就是剪裁图像和翻转之后,bboxes都要进行相应的改变。

先看剪裁图像:

def distorted_bounding_box_crop(image,
                                labels,
                                bboxes,
                                min_object_covered=0.3,
                                aspect_ratio_range=(0.9, 1.1),
                                area_range=(0.1, 1.0),
                                max_attempts=200,
                                clip_bboxes=True,
                                scope=None):
    
    with tf.name_scope(scope, 'distorted_bounding_box_crop', [image, bboxes]):
        # Each bounding box has shape [1, num_boxes, box coords] and
        # the coordinates are ordered [ymin, xmin, ymax, xmax].
        # 生成用于剪裁图像的边界框,用作重新计算bbox的参考,bbox_begin是左上角点
        bbox_begin, bbox_size, distort_bbox = tf.image.sample_distorted_bounding_box(
                tf.shape(image),
                bounding_boxes=tf.expand_dims(bboxes, 0),
                min_object_covered=min_object_covered,
                aspect_ratio_range=aspect_ratio_range,
                area_range=area_range,
                max_attempts=max_attempts,
                use_image_if_no_bounding_boxes=True)
        # 上面返回的distort_bbox维度为[1,1,4],所以这里要重新取出
        distort_bbox = distort_bbox[0, 0]

        # Crop the image to the specified bounding box.
        cropped_image = tf.slice(image, bbox_begin, bbox_size)
        # Restore the shape since the dynamic slice loses 3rd dimension.
        cropped_image.set_shape([None, None, 3])

        # Update bounding boxes: resize and filter out.
        bboxes = tfe.bboxes_resize(distort_bbox, bboxes)
        labels, bboxes = tfe.bboxes_filter_overlap(labels, bboxes,
                                                   threshold=BBOX_CROP_OVERLAP,
                                                   assign_negative=False)
        return cropped_image, labels, bboxes, distort_bbox

bbox的更新在tf_extended中,首先是更新bbox的坐标点:

def bboxes_resize(bbox_ref, bboxes, name=None):
    
    # Bboxes is dictionary.
    if isinstance(bboxes, dict):
        with tf.name_scope(name, 'bboxes_resize_dict'):
            d_bboxes = {}
            for c in bboxes.keys():
                d_bboxes[c] = bboxes_resize(bbox_ref, bboxes[c])
            return d_bboxes

    # Tensors inputs.
    with tf.name_scope(name, 'bboxes_resize'):
        # Translate.
        # 相当于是把原点从[0,0]变换到了[bbox_ref[0], bbox_ref[1]]
        v = tf.stack([bbox_ref[0], bbox_ref[1], bbox_ref[0], bbox_ref[1]])
        bboxes = bboxes - v
        # Scale.
        # 重新计算归一化的尺度
        s = tf.stack([bbox_ref[2] - bbox_ref[0],
                      bbox_ref[3] - bbox_ref[1],
                      bbox_ref[2] - bbox_ref[0],
                      bbox_ref[3] - bbox_ref[1]])
        bboxes = bboxes / s
        return bboxes

然后判断有的目标是否被剪裁得太厉害,要不要保留:

def bboxes_filter_overlap(labels, bboxes,
                          threshold=0.5, assign_negative=False,
                          scope=None):

    with tf.name_scope(scope, 'bboxes_filter', [labels, bboxes]):
        # bbox被裁后,保留的部分与原来的面积比
        scores = bboxes_intersection(tf.constant([0, 0, 1, 1], bboxes.dtype),
                                     bboxes)
        mask = scores > threshold
        # 保留所有的label和框,重叠区不够的label置负
        if assign_negative:
            labels = tf.where(mask, labels, -labels)
            # bboxes = tf.where(mask, bboxes, bboxes)
        # 删除重叠区不够的label和框
        else:
            labels = tf.boolean_mask(labels, mask)
            bboxes = tf.boolean_mask(bboxes, mask)
        return labels, bboxes

def bboxes_intersection(bbox_ref, bboxes, name=None):
    with tf.name_scope(name, 'bboxes_intersection'):
        # Should be more efficient to first transpose.
        bboxes = tf.transpose(bboxes)
        bbox_ref = tf.transpose(bbox_ref)
        # Intersection bbox and volume.
        int_ymin = tf.maximum(bboxes[0], bbox_ref[0])
        int_xmin = tf.maximum(bboxes[1], bbox_ref[1])
        int_ymax = tf.minimum(bboxes[2], bbox_ref[2])
        int_xmax = tf.minimum(bboxes[3], bbox_ref[3])
        h = tf.maximum(int_ymax - int_ymin, 0.)
        w = tf.maximum(int_xmax - int_xmin, 0.)
        # Volumes.
        inter_vol = h * w
        bboxes_vol = (bboxes[2] - bboxes[0]) * (bboxes[3] - bboxes[1])
        scores = tfe_math.safe_divide(inter_vol, bboxes_vol, 'intersection')
        return scores

 再看水平翻转,其实也就是在x方向上,将x变换为1-x:

def random_flip_left_right(image, bboxes, seed=None):
    """Random flip left-right of an image and its bounding boxes.
    """
    def flip_bboxes(bboxes):
        """Flip bounding boxes coordinates.
        """
        bboxes = tf.stack([bboxes[:, 0], 1 - bboxes[:, 3],
                           bboxes[:, 2], 1 - bboxes[:, 1]], axis=-1)
        return bboxes

    # Random flip. Tensorflow implementation.
    with tf.name_scope('random_flip_left_right'):
        image = ops.convert_to_tensor(image, name='image')
        _Check3DImage(image, require_static=False)
        # 随机生成0-1之间的数,与0.5判断
        uniform_random = random_ops.random_uniform([], 0, 1.0, seed=seed)
        mirror_cond = math_ops.less(uniform_random, .5)
        # Flip image.
        # control_flow_ops.cond相当于if-else语句
        result = control_flow_ops.cond(mirror_cond,
                                       lambda: array_ops.reverse_v2(image, [1]),
                                       lambda: image)
        # Flip bboxes.
        bboxes = control_flow_ops.cond(mirror_cond,
                                       lambda: flip_bboxes(bboxes),
                                       lambda: bboxes)
        return fix_image_flip_shape(image, result), bboxes

另外的两种方法都比较简单,在此就不多做描述。验证时的预处理,主要是在没有目标时,加进去了一个原图大小的框,预处理采用的方式也是剪裁、白化等,在看验证代码时再进行补充。至此,ssd网络中的图像预处理部分就结束了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值