天天看點

深度學習——SSD目标檢測網絡源碼學習之圖像預處理1.讀取訓練資料2.資料預處理

抽空把這個網絡細究一下,希望大佬指正~~

大緻了解: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網絡中的圖像預處理部分就結束了。

繼續閱讀