动手学深度学习——目标检测 SSD R-CNN Fast R-CNN Faster R-CNN Mask R-CNN

来源:13.4. 锚框 — 动手学深度学习 2.0.0-beta1 documentation

目标检测:锚框算法原理与实现、SSD、R-CNN_神洛华的博客

目录

目标检测简介

目标检测模型

​编辑​编辑目标检测数据集

数据集示例

目标检测评价指标

目标检测研究方向

基本概念

边界框bounding box

转换函数

画出边界框

锚框anchor box

以每个像素为中心点生成锚框

画出锚框

交并比(IoU)

训练:使用训练数据标记锚框

为锚框确定对应的真实边界框

确定锚框相对于真实边界框的偏移量

为锚框标记类别和偏移量

举例测试

预测:为图片生成预测边界框

使用锚框和偏移量生成预测边界框

使用非极大值抑制处理预测边界框

举例测试

算法过程总结

目标检测算法:SDD(单发多框检测)

特点

多尺度目标检测

SSD模型搭建

类别预测层

边界框预测层 

连结多尺度的预测

前向传播

完整的模型

训练模型

读取数据集和初始化

定义损失函数和评价函数 

训练

预测目标

区域卷积神经网络(R-CNN)

R-CNN

Fast R-CNN

RoI 池化层

Faster R-CNN

Mask R-CNN

小结

需要注意的函数

对Series对象使用list

torch.set_printoptions

torch.meshgrid

reshape(-1)

repeat和repeat_interleave

扩维后相减操作

torch.stack()

切片时使用None

unique

列表和常数相乘


目标检测简介

很多时候图像里有多个我们感兴趣的目标,我们不仅想知道它们的类别,还想得到它们在图像中的具体位置。 在计算机视觉里,我们将这类任务称为目标检测(object detection)或目标识别(object recognition)。

目标检测模型

深度学习目标检测方法分为分为Anchor-Based(锚框法)Anchor-Free(无锚框)两大类,根据有无区域提案阶段划分为双阶段模型单阶段检测模型。

  • 双阶段模型:区域检测模型将目标检测任务分为区域提案生成、特征提取和分类预测三个阶段。
    • 在区域提案生成阶段,检测模型利用搜索算法如选择性搜索(SelectiveSearch,SS)、EdgeBoxes、区域提案网络(Region Proposal Network,RPN) 等在图像中搜寻可能包含物体的区域。
    • 在特征提取阶段,模型利用深度卷积网络提取区域提案中的目标特征。
    • 在分类预测阶段,模型从预定义的类别标签对区域提案进行分类和边框信息预测。
  • 单阶段模型:单阶段检测模型联合区域提案和分类预测,输入整张图像到卷积神经网络中提取特征,最后直接输出目标类别和边框位置信息。这类代表性的方法有:YOLO、SSD和CenterNet等。



目标检测评价指标

当前用于评估检测模型的性能指标主要有帧率每秒(Frames Per Second,FPS)、准确率(accuracy)、精确率(precision)、召回率(recall)、平均精度(Average Precision,AP)、平均 精度均值(mean Average Precision,mAP)等。

  • FPS即每秒识别图像的数量,用于评估目标检测模型的检测速度;
  • P-R曲线:以Recall、Precision为横纵坐标的曲线

    如下图所示,当检测框和标注框的IoU>设定阈值(比如0.3)时,可以认为这个检测框正确检测出物体。IoU>=阈值的检测框的数量就是TP。

  • AP(Average Precision):对不同召回率点上的精确率进行平均,在PR曲线图上表现为某一类别的 PR 曲线下的面积;
  • mAP(mean Average Precision):所有类别AP的均值

目标检测研究方向

目标检测方法可分为检测部件、数据增强、优化方法和学习策略四个方面 。其中检测部件包含基准模型和基准网络;数据增强包含几何变换、光学变换等;优化方法包含特征图、上下文模型、边框优化、区域提案方法、类别不平衡和训练策略六个方面,学习策略涵盖监督学习、弱监督学习和无监督学习。

目标检测数据集

目前主流的通用目标检测数据集有PASCAL VOC、ImageNet、MS COCO(80类物体,330K图片,所有图片共标注1.5M物体)、Open Images和Objects365。

目标检测数据集的常见表示:每一行表示一个物体,对于每一个物体而言,用“图片文件名,物体类别,边缘框”表示,由于边缘框用4个数值表示,因此对于每一行的那一个物体而言,需要用6个数值表示

数据集示例

收集并标记了一个小型数据集。 首先,拍摄了一组香蕉的照片,并生成了1000张不同角度和大小的香蕉图像。 然后,我们在一些背景图片的随机位置上放一张香蕉的图像。 最后,在图片上为这些香蕉标记了边界框。

 label.csv格式如下:

读出来csv_data数据集格式如下:

定义三个辅助函数:读取数据,创建dataset和创建dataloader。

%matplotlib inline
import os
import pandas as pd
import torch
import torchvision
from PIL import Image
from d2l import torch as d2l
import torchvision.transforms as transforms

torch.set_printoptions(2)  # 精简输出精度

d2l.DATA_HUB['banana-detection'] = (
    d2l.DATA_URL + 'banana-detection.zip',
    '5de26c8fce5ccdea9f91267273464dc968d20d72')


def read_data_bananas(is_train=True):
    """读取香蕉检测数据集中的图像和标签"""
    # Image.open()读出来的图片是PIL格式,要转换为tensor格式
    totensor = transforms.ToTensor()  
    data_dir = d2l.download_extract('banana-detection')
    csv_fname = os.path.join(data_dir, 'bananas_train' if is_train
                             else 'bananas_val', 'label.csv')
    csv_data = pd.read_csv(csv_fname)
    csv_data = csv_data.set_index('img_name')
    images, targets = [], []
    #将所有图片读到内存(数据集比较小)
    for img_name, target in csv_data.iterrows():
        images.append(totensor(Image.open(
            os.path.join(data_dir, 'bananas_train' if is_train else
                         'bananas_val', 'images', f'{img_name}'))))
        # 这里的target包含(类别,左上角x,左上角y,右下角x,右下角y),
        # 其中所有图像都具有相同的香蕉类(索引为0)
        targets.append(list(target))  # target格式是Series,用list函数变成列表格式

    #返回图片和标签tensor。/256是除以高宽(图片是256*256),得到一个0-1的数
    return images, torch.tensor(targets).unsqueeze(1) / 256


class BananasDataset(torch.utils.data.Dataset):
    """一个用于加载香蕉检测数据集的自定义数据集"""
    def __init__(self, is_train):
        self.features, self.labels = read_data_bananas(is_train)
        print('read ' + str(len(self.features)) + (f' training examples' if
              is_train else f' validation examples'))

    def __getitem__(self, idx):
        return (self.features[idx].float(), self.labels[idx])

    def __len__(self):
        return len(self.features)


def load_data_bananas(batch_size):
    """加载香蕉检测数据集"""
    train_iter = torch.utils.data.DataLoader(BananasDataset(is_train=True), 
                                             batch_size, shuffle=True)
    val_iter = torch.utils.data.DataLoader(BananasDataset(is_train=False),                                        
                                           batch_size)
    return train_iter, val_iter

读取数据的时候遇到问题No such operator image::read_file,解决方法参考:No such operator image::read_file问题解决_iwill323的博客-CSDN博客

读取一个小批量,并打印其中的图像和标签的形状。

batch_size, edge_size = 32, 256
train_iter, _ = load_data_bananas(batch_size)
batch = next(iter(train_iter))
batch[0].shape, batch[1].shape

结果:(torch.Size([32, 3, 256, 256]), torch.Size([32, 1, 5]))

图像的小批量batch[0]的形状为(批量大小、通道数、高度、宽度)。 标签的小批量batch[1]的形状为(批量大小,m,5),其中m是数据集的任何图像中边界框可能出现的最大数量。

  • 通常来说,图像可能拥有不同数量的主体,则有不同数据的边界框,这样会造成每个批量标签不一样。所以限制每张图片主体最多有m个。
  • 对于不到m个主体的图像将被非法边界框填充。这样,每个边界框的标签将被长度为5的数组表示, 即[𝑙𝑎𝑏𝑒𝑙,𝑥𝑚𝑖𝑛,𝑦𝑚𝑖𝑛,𝑥𝑚𝑎𝑥,𝑦𝑚𝑎𝑥] (坐标值域在0到1之间)。这样每个批次物体数量一样。 对于香蕉数据集而言,由于每张图像上只有一个边界框,因此 𝑚=1 。

展示10幅带有真实边界框的图像

imgs = (batch[0][0:10].permute(0, 2, 3, 1)) # 不能除以255
axes = d2l.show_images(imgs, 2, 5, scale=2)
for ax, label in zip(axes, batch[1][0:10]):
    d2l.show_bboxes(ax, [label[0][1:5] * edge_size], colors=['w'])

基本概念

边界框bounding box

在目标检测中,我们通常使用边界框(bounding box)来描述对象的空间位置。 边界框是矩形的,两种常用的边界框表示:(中心,宽度,高度)和(左上,右下)。坐标的原点是图像的左上角,向右的方向为𝑥轴的正方向,向下的方向为𝑦轴的正方向。

转换函数

定义在这两种表示法之间进行转换的函数:box_corner_to_center从两角表示法转换为中心宽度表示法,而box_center_to_corner反之。 输入参数boxes可以是长度为4的张量,也可以是形状为(𝑛,4)的二维张量,其中𝑛是边界框的数量。

def box_corner_to_center(boxes):
    """从(左上,右下)转换到(中间,宽度,高度)"""
    x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
    cx = (x1 + x2) / 2
    cy = (y1 + y2) / 2
    w = x2 - x1
    h = y2 - y1
    boxes = torch.stack((cx, cy, w, h), axis=-1)  # 其他维度不变,-1维叠在一起
    return boxes


def box_center_to_corner(boxes):
    """从(中间,宽度,高度)转换到(左上,右下)"""
    cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
    x1 = cx - 0.5 * w
    y1 = cy - 0.5 * h
    x2 = cx + 0.5 * w
    y2 = cy + 0.5 * h
    boxes = torch.stack((x1, y1, x2, y2), axis=-1)
    return boxes

画出边界框

定义一个辅助函数bbox_to_rect,将边界框表示成matplotlib的边界框格式。

def bbox_to_rect(bbox, color):
    # 将边界框(左上x,左上y,右下x,右下y)格式转换成matplotlib格式:
    # ((左上x,左上y),宽,高)
    return d2l.plt.Rectangle(
        xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1],
        fill=False, edgecolor=color, linewidth=2)


fig = d2l.plt.imshow(img)
fig.axes.add_patch(bbox_to_rect(dog_bbox, 'blue'))
fig.axes.add_patch(bbox_to_rect(cat_bbox, 'red'));

锚框anchor box

目标检测算法通常会在输入图像中采样大量的区域,然后判断这些区域中是否包含我们感兴趣的目标。 不同的模型使用的区域采样方法可能不同, 这里介绍其中的一种方法:以每个像素为中心,生成多个缩放比和宽高比(aspect ratio)不同的边界框。 这些边界框被称为锚框(anchor box)。

  • 读取图片和已经标记好的边缘框
  • 生成大量锚框,每个锚框是一个训练样本
  • 预测每个锚框是否包含目标物体
  • 如果是,则预测从锚框到实际边界框的偏移量

以每个像素为中心点生成锚框

假设输入图像的高度为ℎ,宽度为𝑤,缩放比(scale)为𝑠∈(0,1](即锚框占图片大小的比例),宽高比(aspect ratio)为𝑟>0。 那么锚框的宽度和高度分别是ws√r和hs/√r(二者相除保证高宽比是r)。设置缩放比取值𝑠1,…,𝑠𝑛和宽高比取值𝑟1,…,𝑟𝑚。当使用这些比例和长宽比的所有组合以每个像素为中心时,输入图像将总共有 whnm个锚框,计算复杂性很容易过高。于是只考虑包含𝑠1或𝑟1的组合:(𝑠1,𝑟1), (𝑠1,𝑟2), …, (𝑠1,𝑟𝑚), (𝑠2,𝑟1), (𝑠3,𝑟1), …, (𝑠𝑛,𝑟1)。也就是说,以同一像素为中心的锚框的数量是𝑛+𝑚−1。 对于整个输入图像,我们将共生成𝑤ℎ(𝑛+𝑚−1)个锚框。

s1​、r1​是最合适的缩放比和高宽比。比如选取锚框的sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5]。 s1​=0.75、r1​=1是最合适的取值,是一定要选的。

multibox_prior:指定输入图像、尺寸列表和宽高比列表,然后返回所有的锚框。函数对data的使用其实就是取出图像的高和宽(in_height和in_width),生成锚框中心点的时候,将高和宽分别分为in_height和in_width份,所以是在每个像素点上生成锚框。

def multibox_prior(data, sizes, ratios):
    """生成以每个像素为中心具有不同形状的锚框"""
    in_height, in_width = data.shape[-2:]
    device, num_sizes, num_ratios = data.device, len(sizes), len(ratios)
    boxes_per_pixel = (num_sizes + num_ratios - 1)
    size_tensor = torch.tensor(sizes, device=device)
    ratio_tensor = torch.tensor(ratios, device=device)

    # 为了将锚点移动到像素的中心,需要设置偏移量。因为一个像素的高为1且宽为1,我们选择偏移我们的中心0.5
    offset_h, offset_w = 0.5, 0.5
    steps_h = 1.0 / in_height  # 在y轴上缩放步长
    steps_w = 1.0 / in_width  # 在x轴上缩放步长

    # 生成锚框的所有中心点 
    center_h = (torch.arange(in_height, device=device) + offset_h) * steps_h
    center_w = (torch.arange(in_width, device=device) + offset_w) * steps_w
    shift_y, shift_x = torch.meshgrid(center_h, center_w, indexing='ij')
    shift_y, shift_x = shift_y.reshape(-1), shift_x.reshape(-1) 
    # shift_y和shift_x对应元素组合就是中心点。reshape(-1)变成一维,shape:[center_h*center_w]

    # 生成“boxes_per_pixel”个高和宽,之后用于创建锚框的四角坐标(xmin,xmax,ymin,ymax)
    w = torch.cat((size_tensor * torch.sqrt(ratio_tensor[0]),
                   sizes[0] * torch.sqrt(ratio_tensor[1:])))\
                   * in_height / in_width  # 只包含 r1 或 s1 的组合  shape:[boxes_per_pixel]   
    h = torch.cat((size_tensor / torch.sqrt(ratio_tensor[0]),
                   sizes[0] / torch.sqrt(ratio_tensor[1:])))
    # 除以2来获得半高和半宽
    # torch.stack:扩展0维,并做拼接,转置后的形状:[boxes_per_pixel,4],每一行是一个anchor
    # 上面是对于一个像素点的。像素点总共有in_height*in_width个,于是行复制in_height*in_width倍,列数不变
    # repeat复制的时候是将原矩阵整体复制,在不同维度上按规定的数量堆积
    # 每boxes_per_pixel行就对应boxes_per_pixel种偏移方式
    anchor_manipulations = torch.stack((-w, -h, w, h)).T.repeat(
                                        in_height * in_width, 1) / 2

    # 每个中心点都将有“boxes_per_pixel”个锚框,所以重复“boxes_per_pixel”次来生成含所有锚框中心的网格    
    # repeat_interleave复制的时候是按照规定的维度(行),先将第一行复制boxes_per_pixel次,再将第二行复制boxes_per_pixel次
    # 于是每boxes_per_pixel行就对应同一个像素点的中心位置
    # out_grid 行:in_height*in_width*boxes_per_pixel  列:4
    out_grid = torch.stack([shift_x, shift_y, shift_x, shift_y],
                dim=1).repeat_interleave(boxes_per_pixel, dim=0) 
    
    # 中心位置加四个偏移量
    output = out_grid + anchor_manipulations
    
    return output.unsqueeze(0)

应用举例

img = d2l.plt.imread('../img/catdog.jpg')
h, w = img.shape[:2]

print(h, w)  # 561 728
X = torch.rand(size=(1, 3, h, w))
Y = multibox_prior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5])
Y.shape
torch.Size([1, 2042040, 4])

将锚框变量Y的形状更改为(图像高度,图像宽度,以同一像素为中心的锚框的数量,4)后,可以获得以指定像素的位置为中心的所有锚框。 在接下来的内容中,访问以(250,250)为中心的第一个锚框。 它有四个元素:锚框左上角的(𝑥,𝑦)轴坐标和右下角的(𝑥,𝑦)轴坐标,他们都分别除以了图像的宽度和高度,所得的值介于0和1之间。

boxes = Y.reshape(h, w, 5, 4)

boxes[250, 250, 0, :]
tensor([0.06, 0.07, 0.63, 0.82])

画出锚框

显示以图像中以某个像素为中心的所有锚框

def show_bboxes(axes, bboxes, labels=None, colors=None):
    """显示所有边界框"""
    def _make_list(obj, default_values=None):
        if obj is None:
            obj = default_values
        elif not isinstance(obj, (list, tuple)):
            obj = [obj]
        return obj

    labels = _make_list(labels)
    colors = _make_list(colors, ['b', 'g', 'r', 'm', 'c'])
    for i, bbox in enumerate(bboxes):
        color = colors[i % len(colors)]
        rect = d2l.bbox_to_rect(bbox.detach().numpy(), color)
        axes.add_patch(rect)
        if labels and len(la
  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值