SSD模型解读(四)数据集加载部分SSDDataset类代码解读- SSDData类解读

书接上文

上一部分我们走完一遍数据加载器的加载机理,接下来我们来看,怎么按照DataLoader的索引往外蹦图片给它的Dataset类,也就是SSDData。

SSDData类传入参数解读

模型训练需要:超调参数、模型、数据集
对应代码里的 optimizer、model_train、train_dataset、val_dataset

optimizer = {
    
    
'adam'  : optim.Adam(model.parameters(), Init_lr_fit, betas = (momentum, 0.999), weight_decay = weight_decay),
'sgd'   : optim.SGD(model.parameters(), Init_lr_fit, momentum = momentum, nesterov=True, weight_decay = weight_decay)
        }[optimizer_type]
        
model_train     = model.train()

train_dataset   = SSDDataset(train_lines, input_shape, anchors, batch_size, num_classes, train = True)
val_dataset     = SSDDataset(val_lines, input_shape, anchors, batch_size, num_classes, train = False)

这里要研究数据集是如何加载到模型里的,首先先来看train_dataset这个实例向SSDDatase类传入了些什么参数:

train_dataset   = SSDDataset(train_lines, input_shape, anchors, batch_size, num_classes, train = True)
val_dataset     = SSDDataset(val_lines, input_shape, anchors, batch_size, num_classes, train = False)
        
  • train_lines 是训练集数据对应的txt文件内容,格式为 [图片绝对路径图片所有真实框]。如图所示
    在这里插入图片描述
  • input_shape 为设定输入模型的固定尺寸,该例设为 [300,300]
  • anchor 是老朋友了,根据input_shape、anchor所设尺寸以及模型自动生成,与输入的具体图片无关,格式为[8743,4]。

在之前写过的这篇笔记中,提到过anchor,如果忘记了ahchor是什么,具体看这篇笔记的坐标表达格式部分

  • batch_size 将数据集以batch_size为一批分成若干组,这里batch_size取16,数据集有4058张,可分4058/16=253组
  • numclasses 数据集类别总数
  • train 判断是否是训练模式,训练模式数据集加载中的处理与验证模式存在差异,具体下文分析。

根据索引开始处理单张图片-getitem()

DataLoader通过fetch()去调用SSDDataset()中的__getitem__()
在这里插入图片描述
通过getitem()根据fetch下发的索引(inx)往外一张一张蹦图。
在这里插入图片描述
下发的index,先判断是否超出总长度

index = index % self.length

然后通过self.get_random_data()将索引的图片地址跟真实框信息读取进来进行处理。
在这里插入图片描述

image, box  = self.get_random_data(self.annotation_lines[index], self.input_shape, random = self.train)

1.数据初步处理-get_random_data()

传进该函数的annotation_line如图所示,包含了图片的地址跟真实框信息:
在这里插入图片描述
将图片地址加载进来获取图片,然后将图片转RGB格式,如果原来就是RGB就返回,不是的话就转RGB。然后将获取图片真实宽和高,再获取真实框。

    def get_random_data(self, annotation_line, input_shape, jitter=.3, hue=.1, sat=0.7, val=0.4, random=True):
        line = annotation_line.split()
        #------------------------------#
        #   读取图像并转换成RGB图像
        #------------------------------#
        image   = Image.open(line[0])
        image   = cvtColor(image)
        #------------------------------#
        #   获得图像的高宽与目标高宽
        #------------------------------#
        iw, ih  = image.size
        h, w    = input_shape
        #------------------------------#
        #   获得真实框
        #------------------------------#
        box     = np.array([np.array(list(map(int,box.split(',')))) for box in line[1:]])

判断是不是训练模式,random=True时,是训练模式,下图是非训练模式,我们留一会再说,先看训练模式下,怎么处理数据。

在这里插入图片描述

- 图片预处理

获取完图片后,判断是训练模式还是验证模式,如果是训练模式就对图像先进行预处理
图片缩放

        new_ar = iw/ih * self.rand(1-jitter,1+jitter) / self.rand(1-jitter,1+jitter)
        scale = self.rand(.25, 2)
        if new_ar < 1:
            nh = int(scale*h)
            nw = int(nh*new_ar)
        else:
            nw = int(scale*w)
            nh = int(nw/new_ar)
        image = image.resize((nw,nh), Image.BICUBIC)

将图片多余部分设置成灰条

        dx = int(self.rand(0, w-nw))
        dy = int(self.rand(0, h-nh))
        new_image = Image.new('RGB', (w,h), (128,128,128))
        new_image.paste(image, (dx, dy))
        image = new_image

翻转图片

        flip = self.rand()<.5
        if flip: image = image.transpose(Image.FLIP_LEFT_RIGHT)

        image_data      = np.array(image, np.uint8)

色域变换 这里用的是HSV变换

        r               = np.random.uniform(-1, 1, 3) * [hue, sat, val] + 1
        hue, sat, val   = cv2.split(cv2.cvtColor(image_data, cv2.COLOR_RGB2HSV))
        dtype           = image_data.dtype
                x       = np.arange(0, 256, dtype=r.dtype)
        lut_hue = ((x * r[0]) % 180).astype(dtype)
        lut_sat = np.clip(x * r[1], 0, 255).astype(dtype)
        lut_val = np.clip(x * r[2], 0, 255).astype(dtype)

        image_data = cv2.merge((cv2.LUT(hue, lut_hue), cv2.LUT(sat, lut_sat), cv2.LUT(val, lut_val)))
        image_data = cv2.cvtColor(image_data, cv2.COLOR_HSV2RGB)

以上就完成了对图像的预处理,非训练的验证模式没有图片预处理部分,它只做真实框调整。

- 真实框按比例调整

真实框格式如下图所示,真实框的内容是左上角右下角坐标,将真实框送入模型进行预测,需要将真实框转换为相对锚框的偏移量,然后预测处理的预测框也是相对锚框偏移量的格式来表示。转换成[x,y,w,h]格式,中心坐标为(x,y),w为框的宽度,h为高度。

在这里插入图片描述

  • 因为将图片从原来本身的尺寸压缩到要输入图片的尺寸时,按比例压缩,当真实图片的尺寸跟输入模型图片的尺寸不等比例时,就产生灰边。下列代码将获取图片按输入模型尺寸缩放后的尺寸。
        if len(box)>0:
            np.random.shuffle(box)
            box[:, [0,2]] = box[:, [0,2]]*nw/iw + dx
            box[:, [1,3]] = box[:, [1,3]]*nh/ih + dy
            if flip: box[:, [0,2]] = w - box[:, [2,0]]
            box[:, 0:2][box[:, 0:2]<0] = 0
            box[:, 2][box[:, 2]>w] = w
            box[:, 3][box[:, 3]>h] = h
            box_w = box[:, 2] - box[:, 0]
            box_h = box[:, 3] - box[:, 1]
            box = box[np.logical_and(box_w>1, box_h>1)] 
        
        return image_data, box

调整完后,将图片跟真实框返给image, box 。
在这里插入图片描述
如果是验证模式,就不进行图像预处理,直接调整真实框。

  • 调整图片格式
    这一部分不是真实框处理的内容,但是按程序走的流程来讲述这个代码,我也就放在这里了
    图片用的from PIL import Image 来读取的,读取进来的格式如图:
    在这里插入图片描述
    送入模型预测需要[3,300,300]的格式,所以这里通过下列函数进行调整:
image_data  = np.transpose(preprocess_input(np.array(image, dtype = np.float32)), (2, 0, 1))
  • 归一化
    对真实框内容数值进行归一化
            boxes               = np.array(box[:,:4] , dtype=np.float32)
            # 进行归一化,调整到0-1之间
            boxes[:, [0, 2]]    = boxes[:,[0, 2]] / self.input_shape[1]
            boxes[:, [1, 3]]    = boxes[:,[1, 3]] / self.input_shape[0]

对真实框的种类进行one hot处理

            one_hot_label   = np.eye(self.num_classes - 1)[np.array(box[:,4], np.int32)]

处理过后的one_hot_label,20个类别,该真实框是哪个类别,就在20类别的对应位置标志为1:
在这里插入图片描述
再将真实框的坐标跟类别合到一起,[:4]为坐标,[4:]为类别:

            box             = np.concatenate([boxes, one_hot_label], axis=-1)

在这里插入图片描述

2.将真实框转换为相对锚框的偏移量

终于来讲怎么把真实框转为相对锚框的偏移量了,这个过程是通过box = self.assign_boxes(box)来实现:
在这里插入图片描述
转换后的格式如下图所示:
在这里插入图片描述
接下来我们来看如果转换。

- 按锚框格式设置载体

先做一个跟锚框格式一样的载体,然后第5个位置设置为,一会根据锚框做偏移量的调整,调整完送入这个载体,这个载体就是真实框处理后的表达形式,其格式为[8732,26],预测框的格式是[8732,25],真实框的第5位判断背景,最后1位判断目标,为互补关系,在损失函数中,取真实框的[4:-1]跟预测框的[4:]计算,也就是真实框最后1位判断目标的不计入loss计算:

    def assign_boxes(self, boxes):
    		#[8732,26]:8732个真实框,第4位是背景,中间20个是类别,最后1位是目标,有目标的话为1,没有目标的话为0,与背景互补。
        assignment          = np.zeros((self.num_anchors, 4 + self.num_classes + 1))
        #第5位意味着这个真实框是不是背景,如果是背景就为1,是目标就为0.
        assignment[:, 4]    = 1.0

assignment的第5位意味着这个真实框是不是背景,如果是背景就为1,是目标就为0.最后一位判断是否有目标。

- 对坐标进行解码

np.apply_along_axis 的作用是在数组的每一个轴上执行函数,下列代码的意思就是,对boxes的每一行都进行self.encode_box计算,也就是对每一个真实框都进行一次解码计算(encode),注意每次传入encode_box的只有一个真实框。
在这里插入图片描述再来展开这个解码函数,看看咋样对boxes坐标进行解码

  • 首先将这个真实坐标框boxes与8732个锚框匹配,计算当前真实框跟锚框的重合情况,计算结果例子:
    在这里插入图片描述
    iou计算代码:
    def iou(self, box):
        #---------------------------------------------#
        #   计算出每个真实框与所有的锚框的iou
        #   判断真实框与先验框的重合情况
        #---------------------------------------------#
        inter_upleft    = np.maximum(self.anchors[:, :2], box[:2])
        inter_botright  = np.minimum(self.anchors[:, 2:4], box[2:])

        inter_wh    = inter_botright - inter_upleft
        inter_wh    = np.maximum(inter_wh, 0)
        inter       = inter_wh[:, 0] * inter_wh[:, 1]
        #---------------------------------------------# 
        #   真实框的面积
        #---------------------------------------------#
        area_true = (box[2] - box[0]) * (box[3] - box[1])
        #---------------------------------------------#
        #   先验框的面积
        #---------------------------------------------#
        area_gt = (self.anchors[:, 2] - self.anchors[:, 0])*(self.anchors[:, 3] - self.anchors[:, 1])
        #---------------------------------------------#
        #   计算iou
        #---------------------------------------------#
        union = area_true + area_gt - inter

        iou = inter / union
        return iou
  • 找到找到iou大于所设的overlap_threshold阈值,如果这个真实框box坐标与锚框的iou计算结果没有大于所设阈值,就让最大的那个iou对应的边界框来匹配这个真实框。如果这个边框所设的iou大于所设阈值就先记录下来,下图为有若干个计算结果的iou大于所设阈值,经过enconde_box返回到上一层的结果,如下图所示,其中[:4]是iou的值,前4个是坐标:
    在这里插入图片描述
    在这里插入图片描述
    具体代码如下:
    def encode_box(self, box, return_iou=True, variances = [0.1, 0.1, 0.2, 0.2]):
        #---------------------------------------------#
        #   计算当前真实框和锚框的重合情况
        #   iou [self.num_anchors]
        #   encoded_box [self.num_anchors, 5]
        #---------------------------------------------#
        iou = self.iou(box)
        encoded_box = np.zeros((self.num_anchors, 4 + return_iou))
        
        #---------------------------------------------#
        #   找到每一个真实框,重合程度较高的锚框
        #   真实框可以由这个锚框来负责预测
        #---------------------------------------------#
        assign_mask = iou > self.overlap_threshold

        #---------------------------------------------#
        #   如果没有一个锚框重合度大于self.overlap_threshold
        #   则选择重合度最大的为正样本
        #---------------------------------------------#
        if not assign_mask.any():
            #argmax()就是找iou中最大的值的索引,如果这里iou最大的值比设置的overlap_threshold还小,
            #就让最大的那个iou对应的边界框来匹配这个真实框
            assign_mask[iou.argmax()] = True
        
        #---------------------------------------------#
        #   利用iou进行赋值 
        #---------------------------------------------#
        if return_iou:
            encoded_box[:, -1][assign_mask] = iou[assign_mask]
        
        #---------------------------------------------#
        #   找到对应的锚框
        #---------------------------------------------#
        assigned_anchors = self.anchors[assign_mask]

        #---------------------------------------------#
        #   逆向编码,将真实框转化为ssd预测结果的格式
        #   先计算真实框的中心与长宽
        #---------------------------------------------#
        box_center  = 0.5 * (box[:2] + box[2:])
        box_wh      = box[2:] - box[:2]
        #---------------------------------------------#
        #   再计算重合度较高的锚框的中心与长宽
        #---------------------------------------------#
        assigned_anchors_center = (assigned_anchors[:, 0:2] + assigned_anchors[:, 2:4]) * 0.5
        assigned_anchors_wh     = (assigned_anchors[:, 2:4] - assigned_anchors[:, 0:2])
        
        #------------------------------------------------#
        #   逆向求取ssd应该有的预测结果
        #   先求取中心的预测结果,再求取宽高的预测结果
        #   存在改变数量级的参数,默认为[0.1,0.1,0.2,0.2]
        #------------------------------------------------#
        encoded_box[:, :2][assign_mask] = box_center - assigned_anchors_center
        encoded_box[:, :2][assign_mask] /= assigned_anchors_wh
        encoded_box[:, :2][assign_mask] /= np.array(variances)[:2]

        encoded_box[:, 2:4][assign_mask] = np.log(box_wh / assigned_anchors_wh)
        encoded_box[:, 2:4][assign_mask] /= np.array(variances)[2:4]
        return encoded_box.ravel()

所有真实框与锚框计算iou后得到的best_iou如图所示:
在这里插入图片描述
看一下格式[5,8732,5]第一个5意味着有五个真实框。这与锚框[8732,26]维度都不一样,所以我们要把这五个真实框装在一个[8732,5]中表现出来:
具体过程:
先取出encode_boxes的[:,:,4],也就是每个真实框与锚框计算的iou值,得到[5,8732]的数据。
在这里插入图片描述
再进行5个真实框与锚框的匹配,axis=0,[5,8732]的0轴是真实框的数量,8732对应着8732个锚框,在ssd算法中,一个锚框只能对应一个真实框,一个真实框可以对应多个锚框。在同一个锚框下,5个真实框中谁的iou大,就保留谁的值,最后得到的结果如图:
在这里插入图片描述
这里只知道哪个锚框要启用,还不知道这个锚框要给5个真实框中的哪个真实框用:

        best_iou_idx    = encoded_boxes[:, :, -1].argmax(axis=0)

通过这个代码得到如图,best_iou_idx的意思就是8732个锚框,每个锚框与哪个真实框匹配,如图中,best_iou_idx中最大值等4,就说明8732个锚框中至少有一个锚框是跟第五个真实框匹配的。
在这里插入图片描述
上述解释了一个锚框只能匹配一个真实框,我们来看看一个真实框可能匹配多个锚框:

下列表中是单个真实框与锚框计算得出的iou返回情况(8732个,因为锚框有8732个),这里只举8个

真实框编号\锚框编号 0 1 2 3 4 5 6 7 8 单个真实框与锚框计算得出的iou>0.5的数量
0 0, 0.62, 0.63 0.63, 0.86 0, 0, 0, 0 4
1 0.89 0.75 0, 0, 0, 0, 0, 0, 0 2
2 0, 0, 0, 0.85 0.65, 0, 0.54, 0, 0 3
3 0, 0, 0, 0, 0, 0, 0, 0.74 0.88 2
4 0, 0, 0, 0, 0.76, 0.67 0.80 0.67, 0.96 4

以这8个锚框为例,编号为0的锚框匹配的是编号为1的真实框,编号为1的锚框匹配的是编号为1的真实框,以此类推,可以看到每个锚框只匹配一个真实框。但是编号为0的真实匹配却匹配了编号为2,4的锚框,所以一个真实框可以了若干个锚框。能匹配的前提都是,这个真实框跟8732个锚框计算得到的iou>预设值,也就是重合度高才能匹配。

取出这些匹配好的锚框的真实框:

        best_iou_mask   = best_iou > 0
        best_iou_idx    = best_iou_idx[best_iou_mask]
        assign_num      = len(best_iou_idx)

如图所示,有81个锚框被用来跟这5个真实框匹配了,assign_num=81。
在这里插入图片描述
再根据索引将这些真实框编码后的的坐标取出,将坐标赋值给要输出的载体assignmentbest_iou_mask是个bool列表,可以对assignment进行筛选。

	assignment[:, :4][best_iou_mask] = encoded_boxes[best_iou_idx, np.arange(assign_num), :4]

在这里插入图片描述
在这里插入图片描述
判断锚框是不是有匹配的真实框,如果有的话说明这里有目标,没有的话说明这里没有目标没有目标就是背景。
第5为是背景的判断,所以该位置匹配的话,就设置为0,说明该位置有目标,不是背景。

	assignment[:, 4][best_iou_mask]     = 0

最后1位是目标,如果该位置匹配的话,就设置为1,说明有目标

  	assignment[:, -1][best_iou_mask]    = 1

再根据81个已经匹配完真实框的锚框,其中每一个锚框选的是5个真实框中的哪个的索引,取检索boxes中的类别,将其赋值给81个锚框的类别属性。

        assignment[:, 5:-1][best_iou_mask]  = boxes[best_iou_idx, 4:]

在这里插入图片描述
在这里插入图片描述
到此就将assignment的26位数据全部赋值完毕,将其返回,完成解码

- 数据返回

这样子就处理完这种图片的box信息。
在这里插入图片描述
将这张图片的Image与box信息返给DataLoade的fetch函数后,DataLoader开始提供一个新的index送入getietm,进行下一张的处理。
在这里插入图片描述
这一部分在加载机制中解读过。

等一个batch_size的数据全部处理完后,送入模型,进行下一步的训练。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_55224780/article/details/129865788
今日推荐