PyTorch-现代计算机视觉-四-

105 阅读1小时+

PyTorch 现代计算机视觉(四)

七、目标检测基础

到目前为止,在前面的章节中,我们学习了执行图像分类。想象一个场景,我们正在利用计算机视觉来实现自动驾驶汽车。不仅需要检测道路的图像是否包含车辆、人行道和行人的图像,而且识别这些物体所在的位置也很重要。在这种情况下,我们将在本章和下一章学习的各种物体探测技术将会派上用场。

在这一章和下一章,我们将学习一些执行目标检测的技术。我们将从学习基础知识开始——使用名为ybat的工具标记边界框对象的基本事实,使用selectivesearch方法提取区域建议,并通过使用联合交集 ( IoU )度量和平均精度度量来定义边界框预测的准确性。之后,我们将了解两个基于区域提议的网络——R-CNN 和 Fast R-CNN,首先了解它们的工作细节,然后在包含卡车和公共汽车图像的数据集上实现它们。

本章将涵盖以下主题:

  • 目标检测简介
  • 为训练创建边界框地面真实
  • 了解区域提案
  • 了解 IoU、非最大抑制和平均精度
  • 训练基于 R-CNN 的自定义目标检测器
  • 训练快速的基于 R-CNN 的自定义目标检测器

目标检测简介

随着自动驾驶汽车、面部检测、智能视频监控和人数统计解决方案的兴起,快速准确的目标检测系统需求很大。这些系统不仅包括来自图像的对象分类,还包括通过在每个对象周围绘制适当的边界框来定位每个对象。这(绘制边界框和分类)使得目标检测比其传统的计算机视觉前身图像分类更难。

为了了解目标检测的输出是什么样的,我们来看一下下图:

在上图中,我们可以看到,虽然典型的对象分类仅提及图像中存在的对象类别,但对象定位会在图像中存在的对象周围绘制一个边界框。另一方面,目标检测将涉及在图像中的单个对象周围绘制边界框,以及在跨越图像中存在的多个对象的边界框内识别对象的类别。

在我们了解目标检测的广泛使用案例之前,让我们了解它是如何添加到我们在上一章中介绍的对象分类任务中的。

想象一个场景,在一个图像中有多个对象。我要求你预测图像中出现的物体类别。举个例子,假设图像中同时包含了猫和狗。你如何对这些图片进行分类?在这种情况下,目标检测非常方便,它不仅可以预测其中存在的对象(边界框)的位置,还可以预测各个边界框中存在的对象的类别。

利用目标检测的各种使用案例包括:

  • 安全:这有助于识别入侵者。
  • 自主 汽车:这有助于识别道路图像上出现的各种物体。
  • **图像搜索:**这有助于识别包含感兴趣的物体(或人)的图像。
  • 汽车:这有助于识别汽车图像中的车牌。

在上述所有情况下,利用目标检测来围绕图像中存在的各种对象绘制边界框。

在这一章中,我们将学习预测物体的类别,并在图像中的物体周围有一个紧密的边界框,这是定位任务。我们还将学习检测图片中多个对象对应的类,以及每个对象周围的边界框,这是目标检测任务。

训练典型的目标检测模型包括以下步骤:

  1. 创建包含与图像中存在的各种对象相对应的边界框和类的标签的基础事实数据。
  2. 提出扫描图像以识别可能包含对象的区域(区域提议)的机制。在本章中,我们将了解如何利用名为选择性搜索的方法生成的区域建议。在下一章,我们将学习如何利用定位框来识别包含对象的区域。在结合计算机视觉和自然语言处理技术的一章中(第十五章),我们将学习如何利用转换器中的位置嵌入来帮助识别包含物体的区域。
  3. 使用 IoU 度量创建目标类变量。
  4. 创建目标边界框偏移变量,以对第二步中出现的区域提议的位置进行校正。
  5. 建立模型,该模型可以预测对象的类别以及对应于区域提议的目标包围盒偏移。
  6. 使用平均精度 ( )测量目标检测的精度。

现在,我们对训练目标检测模型要做的事情有了一个高层次的概述,我们将在下一节中学习如何为边界框创建数据集(这是构建目标检测模型的第一步)。

为训练创建边界框地面真实

我们已经知道,目标检测给出的输出是图像中感兴趣的对象周围的边界框。为了构建检测图像中对象周围的边界框的算法,我们必须创建输入-输出组合,其中输入是图像,输出是给定图像中对象周围的边界框,以及对应于对象的类。

注意,当我们检测边界框时,我们检测的是包围图像的边界框的四个角的像素位置。

为了训练提供边界框的模型,我们需要图像,以及图像中所有对象的相应边界框坐标。在本节中,我们将了解一种创建训练数据集的方法,其中图像是输入,相应的边界框和对象类存储在 XML 文件中作为输出。我们将使用ybat工具来注释边界框和相应的类。

让我们了解一下如何安装和使用ybat来创建(注释)图像中物体周围的边界框。此外,我们还将在下一节中检查包含带注释的类和边界框信息的 XML 文件。

安装图像注释工具

让我们从下面的 GitHub 链接开始下载ybat-master.zipgithub.com/drainingsun/ybat,然后解压。解压后,存储在你选择的文件夹中。使用您选择的浏览器打开ybat.html,您将看到一个空白页面。下面的截图展示了文件夹的样子以及如何打开ybat.html文件:

在我们开始创建对应于一个图像的地面实况之前,让我们指定我们想要跨图像标记并存储在classes.txt文件中的所有可能的类,如下所示:

现在,让我们准备一个图像对应的地面真相。这包括在对象(下图中的人)周围绘制一个边界框,并在以下步骤中为图像中出现的对象分配标签/类别:

  1. 上传所有要添加注释的图像(下图中的步骤 1)。
  2. 上传classes.txt文件(下图中的步骤 2)。
  3. 为每个图像添加标签,首先选择文件名,然后在每个要添加标签的对象周围画一个十字准线(下图中的步骤 3)。在绘制十字准线之前,请确保在“类别”区域(下图中第二个椭圆形下方的“类别”窗格)中选择了正确的类别。
  4. 以所需格式保存数据转储(下图中的步骤 4)。每种格式都是由不同的研究团队独立开发的,所有格式都同样有效。基于它们的受欢迎程度和便利性,每个实现都喜欢不同的格式。

使用下图可以更好地表示所有这些步骤:

例如,当我们下载 PascalVOC 格式时,它会下载一个 XML 文件的 zip 文件。绘制矩形边界框后,XML 文件的快照如下:

从前面的截图中,注意到bndbox字段包含与图像中感兴趣的对象相对应的 xy 坐标的最小值和最大值的坐标。我们还应该能够使用name字段提取对应于图像中对象的类。

既然我们已经了解了如何创建图像中存在的对象(类标签和边界框)的基本事实,在下面的部分中,我们将深入到识别图像中的对象的构建块中。首先,我们将讨论有助于突出显示图像中最有可能包含对象的部分的区域提议。

了解区域提案

想象一个假设场景,其中感兴趣的图像在背景中包含一个人和天空。此外,对于这种情况,让我们假设背景(天空)的像素强度几乎没有变化,而前景(人)的像素强度有相当大的变化。

仅仅从前面的描述本身,我们可以得出结论,这里有两个主要区域——一个是人的,另一个是天空的。此外,在人的图像的区域内,对应于头发的像素将具有与对应于脸的像素不同的强度,从而确定在一个区域内可以有多个子区域。

区域提议是一种帮助识别像素彼此相似的区域岛的技术。

生成区域建议对于我们必须识别图像中存在的对象的位置的目标检测来说很方便。此外,给定一个区域提议生成该区域的提议,这有助于对象定位,其中任务是识别正好适合图像中对象周围的边界框。我们将在后面的培训基于 R-CNN 的自定义目标检测器中了解区域提议如何帮助对象定位和检测,但让我们首先了解如何从图像中生成区域提议。

利用选择性研究生成区域建议书

SelectiveSearch 是一种用于对象定位的区域建议算法,它根据像素强度生成可能组合在一起的区域建议。SelectiveSearch 根据相似像素的层次分组对像素进行分组,这反过来又利用了图像中内容的颜色、纹理、大小和形状兼容性。

最初,SelectiveSearch 通过基于前面的属性对像素进行分组来过度分割图像。接下来,它遍历这些过度分割的组,并根据相似性对它们进行分组。在每次迭代中,它将较小的区域组合成一个较大的区域。

我们通过下面的例子来了解一下selectivesearch的流程:

以下代码在本书的 GitHub 知识库的Chapter07文件夹中以Understanding_selectivesearch.ipynb的形式提供-【tinyurl.com/mcvp-packt】… GitHub 中的笔记本上复制 URL,以避免在复制结果时出现任何问题

  1. 安装所需的软件包:
!pip install selectivesearch
!pip install torch_snippets
from torch_snippets import *
import selectivesearch
from skimage.segmentation import felzenszwalb
  1. 获取并加载所需的图像:
!wget https://www.dropbox.com/s/l98leemr7r5stnm/Hemanvi.jpeg
img = read('Hemanvi.jpeg', 1)
  1. 从图像中提取felzenszwalb片段(根据图像中内容的颜色、纹理、大小和形状兼容性获得):
segments_fz = felzenszwalb(img, scale=200)

注意,在felzenszwalb方法中,scale表示可以在图像的片段内形成的簇的数量。scale的值越高,保留的原始图像细节越多。

  1. 绘制原始图像和分段图像:
subplots([img, segments_fz], \
         titles=['Original Image',\
                 'Image post\nfelzenszwalb segmentation'],\
         sz=10, nc=2)

上述代码会产生以下输出:

从前面的输出可以看出,属于同一组的像素具有相似的像素值。

具有相似值的像素形成一个区域提议。这现在有助于目标检测,因为我们现在将每个区域提议传递给网络,并要求它预测该区域提议是背景还是对象。此外,如果它是一个对象,这将有助于我们确定获取与该对象相对应的紧边界框以及与区域建议内的内容相对应的类的偏移量。

现在我们已经了解了 SelectiveSearch 的工作,让我们实现selectivesearch函数来获取给定图像的区域建议。

实现选择性研究以生成区域建议

在本节中,我们将使用selectivesearch定义extract_candidates函数,以便在后续关于训练基于 R-CNN 和快速 R-CNN 的自定义目标检测器的部分中利用它:

  1. 定义从图像中提取区域建议的extract_candidates函数:
  • 定义将图像作为输入参数的函数:
def extract_candidates(img):
  • 使用selectivesearch包中可用的selective_search方法提取图像中的候选区域:
    img_lbl, regions = selectivesearch.selective_search(img, \
                                     scale=200, min_size=100)
  • 计算图像面积并初始化一个列表(候选项),我们将使用该列表存储通过定义阈值的候选项:
    img_area = np.prod(img.shape[:2])
    candidates = []
  • 仅提取那些超过总图像面积 5%且小于或等于图像面积 100%的候选(区域)并返回它们:
    for r in regions:
        if r['rect'] in candidates: continue
        if r['size'] < (0.05*img_area): continue
        if r['size'] > (1*img_area): continue
        x, y, w, h = r['rect']
        candidates.append(list(r['rect']))
    return candidates
  1. 导入相关包并获取图像:
!pip install selectivesearch
!pip install torch_snippets
from torch_snippets import *
import selectivesearch
!wget https://www.dropbox.com/s/l98leemr7r5stnm/Hemanvi.jpeg
img = read('Hemanvi.jpeg', 1)
  1. 提取候选对象并将其绘制在图像上:
candidates = extract_candidates(img)
show(img, bbs=candidates)

上述代码生成以下输出:

上图中的网格代表来自selective_search方法的候选区域(区域建议)。

现在我们已经了解了区域建议书的生成,还有一个问题没有回答。我们如何利用区域提议进行目标检测和定位?

与感兴趣的图像中的对象的位置(地面实况)具有高交集的区域提议被标记为包含该对象的区域提议,而具有低交集的区域提议被标记为背景。

在下一节中,我们将在理解构成构建目标检测模型的主干的各种技术的过程中,了解如何计算候选区域提议与基本事实边界框的交集。

理解欠条

想象一个场景,我们想出了一个物体的边界框的预测。我们如何衡量我们预测的准确性?在这种情况下,交于 ( 借据)的概念就派上了用场。

交集在术语交集在并集内测量预测和实际边界框的重叠程度,而并集测量可能重叠的整体空间。IoU 是两个边界框之间的重叠区域与两个边界框的组合区域的比率。

这可以用下图表示:

在前面的两个边界框(矩形)的图中,让我们将左边的边界框视为地面真实,将右边的边界框视为对象的预测位置。作为度量的 IoU 是两个边界框之间的重叠区域与组合区域的比率。

在下图中,您可以观察到 IoU 度量随着边界框之间重叠的变化而变化:

从上图中我们可以看出,随着重叠减少,IoU 也会减少,在最后一张图中,当没有重叠时,IoU 度量为 0。

既然我们有了测量 IoU 的直觉,让我们用代码实现它,并创建一个函数来计算 IoU,因为我们将在“训练 R-CNN”和“训练快速 R-CNN”部分利用它。

下面的代码可以在本书的 GitHub 库【tinyurl.com/mcvp-packt[…](tinyurl.com/mcvp-packt)

让我们定义一个函数,它将两个边界框作为输入,并将 IoU 作为输出返回:

  1. 指定将boxAboxB作为输入的get_iou函数,其中boxAboxB是两个不同的包围盒(可以将boxA视为地面真实包围盒,boxB视为区域提议):
def get_iou(boxA, boxB, epsilon=1e-5):

我们定义了epsilon参数来处理两个盒子之间的并集为 0 的罕见情况,这导致了被零除的错误。注意,在每个边界框中,将有四个值对应于边界框的四个角。

  1. 计算相交框的坐标:
    x1 = max(boxA[0], boxB[0])
    y1 = max(boxA[1], boxB[1])
    x2 = min(boxA[2], boxB[2])
    y2 = min(boxA[3], boxB[3])

注意,x1正在存储两个边界框之间最左边的 x 值的最大值。类似地,y1存储最上面的 y 值,x2y2分别存储最右边的 x 值和最下面的 y 值,对应于相交部分。

  1. 计算相交区域(重叠区域)对应的widthheight:
    width = (x2 - x1)

    height = (y2 - y1)
  1. 计算重叠面积(area_overlap):
    if (width<0) or (height <0):
        return 0.0
    area_overlap = width * height

请注意,在前面的代码中,我们指定如果对应于重叠区域的宽度或高度小于 0,则相交面积为 0。否则,我们计算重叠(相交)的面积,类似于矩形面积的计算方式——宽度乘以高度。

  1. 计算对应于两个边界框的组合面积:
    area_a = (boxA[2] - boxA[0]) * (boxA[3] - boxA[1])
    area_b = (boxB[2] - boxB[0]) * (boxB[3] - boxB[1])
    area_combined = area_a + area_b - area_overlap

在前面的代码中,我们已经计算了两个边界框的组合面积—area_aarea_b,然后在计算area_combined时减去重叠面积,因为area_overlap被计算了两次,一次是在计算area_a时,另一次是在计算area_b时。

  1. 计算欠条并归还:
    iou = area_overlap / (area_combined+epsilon)
    return iou

在前面的代码中,我们将iou计算为重叠区域(area_overlap)与组合区域(area_combined)的面积之比,并将其返回。

到目前为止,我们已经学习了创建地面实况和计算 IoU,这有助于准备训练数据。接下来,目标检测模型将在检测图像中的对象时派上用场。最后,我们将计算模型性能并在新图像上进行推断。

我们将推迟构建模型,直到接下来的部分,因为训练模型更复杂,我们还必须在训练它之前学习更多的组件。在下一节中,我们将了解非最大值抑制,这有助于在新图像上使用训练模型进行推断时,从对象周围不同的可能预测边界框中进行筛选。

非最大抑制

设想一个场景,其中生成了多个区域提议,并且这些提议彼此明显重叠。本质上,所有预测的边界框坐标(对区域提议的偏移)彼此明显重叠。例如,让我们考虑下面的图像,其中为图像中的人生成了多个区域建议:

在上图中,我要求您在众多区域建议中找出我们认为包含对象的框,以及我们将丢弃的框。在这种情况下,非最大抑制很方便。让我们解开术语“非最大抑制”

非最大值是指不包含最大概率包含物体的盒子,抑制是指我们丢弃那些不包含最大概率包含物体的盒子。在非最大值抑制中,我们识别具有最高概率的边界框,并且丢弃 IoU 大于某个阈值的所有其他边界框,其中该框包含包含对象的最高概率。

在 PyTorch 中,使用torchvision.ops模块中的nms功能执行非最大抑制。nms函数采用边界框坐标、边界框中对象的置信度以及边界框间 IoU 的阈值来识别要保留的边界框。在分别位于步骤 19步骤 16训练基于 R-CNN 的定制目标检测器训练快速基于 R-CNN 的定制目标检测器部分中,当预测新图像中的对象类别和对象边界框时,您将利用nms功能。

平均精度

到目前为止,我们已经了解了如何获得包含图像中每个对象周围的边界框和对应于边界框中对象的类的输出。现在下一个问题来了:我们如何量化来自我们模型的预测的准确性?

在这种情况下,地图可以提供帮助。在我们尝试理解 mAP 之前,让我们先理解精度,然后是平均精度,最后是 mAP:

  • 精度:通常,我们计算精度为:

真阳性指的是预测正确的对象类别并且具有大于某个阈值的地面真值的 IoU 的边界框。假阳性是指边界框错误地预测了类,或者与地面真实值的重叠小于定义的阈值。此外,如果有多个边界框被识别为同一个基本事实边界框,则只有一个边界框会变成真阳性,而其他所有边界框都会变成假阳性。

  • **平均精度:**平均精度是在各种 IoU 阈值下计算的精度值的平均值。
  • mAP: mAP 是在数据集中所有对象类别的各种 IoU 阈值下计算的精度值的平均值。

到目前为止,我们已经了解了如何为模型准备训练数据集,对模型的预测执行非最大值抑制,以及计算其准确性。在下面的部分中,我们将学习训练模型(基于 R-CNN 和基于快速 R-CNN)来检测新图像中的对象。

训练基于 R-CNN 的自定义目标检测器

R-CNN 代表基于区域的卷积神经网络R-CNN 内部的代表地区提案。区域提议用于识别图像内的对象。注意,R-CNN 有助于识别图像中存在的对象和图像中对象的位置。

在接下来的部分中,我们将学习 R-CNN 的工作细节,然后在我们的自定义数据集上训练它。

R-CNN 的工作细节

让我们使用下图在高层次上了解一下基于 R-CNN 的目标检测:

图片来源:arxiv.org/pdf/1311.25…

当利用 R-CNN 技术进行目标检测时,我们执行以下步骤:

  1. 从图像中提取区域建议:
  • 确保我们提取大量的建议,不遗漏图像中的任何潜在对象。
  1. 调整(扭曲)所有提取的区域,以获得相同大小的图像。
  2. 通过网络传递调整大小的区域提议:
  • 通常,我们通过预先训练的模型(如 VGG16 或 ResNet50)传递调整大小的区域建议,并在完全连接的层中提取特征。
  1. 创建用于模型训练的数据,其中输入是通过将区域提议传递通过预训练的模型而提取的特征,输出是对应于每个区域提议的类以及区域提议与对应于图像的地面真实的偏移:
  • 如果区域提议具有大于某个阈值的与对象的 IoU,我们以这样的方式准备训练数据,即该区域负责预测与其重叠的对象的类别,以及区域提议与包含感兴趣对象的地面真实边界框的偏移。

为区域方案创建边界框偏移和基础真值类的示例如下:

在上图中,o(红色)表示区域建议的中心(虚线边界框),x 表示与 cat 类对应的地面真实边界框(实线边界框)的中心。我们将区域提议边界框和地面真实边界框之间的偏移计算为两个边界框的中心坐标之间的差(dx,dy)和边界框的高度和宽度之间的差(dw,dh)。

  1. 将两个输出头,一个对应于图像的类别,另一个对应于区域提议的偏移,与地面真实边界框连接,以提取物体上的精细边界框:
  • 这个练习将类似于我们根据上一章中人的面部图像预测性别(分类变量,类似于本案例研究中的对象类别)和年龄(连续变量,类似于要在区域建议之上完成的偏移)的用例。
  1. 训练模型柱,编写自定义损失函数,使对象分类误差和边界框偏移误差最小化。

请注意,我们将最小化的损失函数不同于原始论文中优化的损失函数。我们这样做是为了降低从头构建 R-CNN 和快速 R-CNN 的复杂性。一旦读者熟悉了模型是如何工作的,并且能够使用下面的代码构建一个模型,我们强烈鼓励他们从头开始实现原始的论文。

在下一节中,我们将学习获取数据集和创建用于训练的数据。在接下来的部分中,我们将学习在预测新图像中存在的对象类别及其边界框之前设计和训练模型。

在自定义数据集上实现用于目标检测的 R-CNN

到目前为止,我们对 R-CNN 的工作原理有了理论上的了解。在本节中,我们将了解如何为培训创建数据。该过程包括以下步骤:

  1. 下载数据集

  2. 准备数据集

  3. 定义区域建议提取和 IoU 计算功能

  4. 创建培训数据

  • 为模型创建输入数据
  • 调整区域大小的建议
  • 将它们通过预训练的模型以获取完全连接的层值
  • 为模型创建输出数据
  • 用类别或背景标签标记每个区域提议
  • 如果所述区域提议对应于对象而不是背景,则定义所述区域提议相对于地面实况的偏移
  1. 定义和训练模型
  2. 对新图像进行预测

让我们从下面几节开始编码。

下载数据集

对于目标检测的场景,我们将从 Google Open Images v6 数据集下载数据(可从storage . Google APIs . com/Open img/V5/test-annotations-bbox . CSV获得)。然而,在代码中,我们将只处理那些公共汽车或卡车的图像,以确保我们可以训练图像(您将很快注意到与使用selectivesearch相关的内存问题)。我们将扩大我们将在第十章、目标检测和分割应用中培训的类别数量(除了公共汽车和卡车之外的更多类别)。

以下代码在本书的 GitHub 知识库的Chapter07文件夹中以Training_RCNN.ipynb的形式提供-【tinyurl.com/mcvp-packt 代码包含下载数据的 URL,长度适中。我们强烈建议您在 GitHub 中执行笔记本以再现结果,同时您理解执行的步骤和对文本中各种代码组件的解释。

  1. 导入相关包以下载包含图像及其基本事实的文件:
!pip install -q --upgrade selectivesearch torch_snippets
from torch_snippets import *
import selectivesearch
from google.colab import files
files.upload() # upload kaggle.json file 
!mkdir -p ~/.kaggle
!mv kaggle.json ~/.kaggle/
!ls ~/.kaggle
!chmod 600 /root/.kaggle/kaggle.json
!kaggle datasets download -d sixhky/open-images-bus-trucks/
!unzip -qq open-images-bus-trucks.zip
from torchvision import transforms, models, datasets
from torch_snippets import Report
from torchvision.ops import nms
device = 'cuda' if torch.cuda.is_available() else 'cpu'

一旦我们执行了前面的代码,我们就会将图像及其相应的基本事实存储在一个可用的 CSV 文件中。

准备数据集

现在我们已经下载了数据集,我们将准备数据集。这包括以下步骤:

  1. 获取每个图像及其对应的类和边界框值
  2. 获取每个图像内的区域提议、它们对应的 IoU 以及区域提议相对于地面实况要被校正的增量
  3. 为每个类别分配数字标签(其中我们有一个额外的背景类别(除了公共汽车和卡车类别之外),其中带有真实边界框的 IoU 低于阈值)
  4. 将每个区域提议调整到共同的大小,以便将它们传递到网络

在本练习结束时,我们将调整区域方案的裁剪大小,为每个区域方案分配地面实况类,并计算区域方案相对于地面实况边界框的偏移。我们将从上一节停止的地方继续编码:

  1. 指定图像的位置,并阅读我们下载的 CSV 文件中的基本事实:
IMAGE_ROOT = 'img/images'
DF_RAW = pd.read_csv('df.csv')
print(DF_RAW.head())

前面数据帧的示例如下:

注意,XMinXMaxYMinYMax对应于图像的边界框的地面真实度。再者,LabelName提供了形象的类。

  1. 定义一个类,该类返回图像及其对应的类和基本事实以及图像的文件路径:
  • 将数据框(df)和包含图像的文件夹路径(image_folder)作为输入传递给__init__方法,并获取数据框(self.unique_images)中的唯一值ImageID。我们这样做是因为一幅图像可以包含多个对象,因此多行可以对应同一个ImageID值:
class OpenImages(Dataset):
    def __init__(self, df, image_folder=IMAGE_ROOT):
        self.root = image_folder
        self.df = df
        self.unique_images = df['ImageID'].unique()
    def __len__(self): return len(self.unique_images)
  • 定义__getitem__方法,其中我们获取对应于索引(ix)的图像(image_id),获取其边界框坐标(boxes)、classes,并返回图像、边界框、类和图像路径:
    def __getitem__(self, ix):
        image_id = self.unique_images[ix]
        image_path = f'{self.root}/{image_id}.jpg'
        # Convert BGR to RGB
        image = cv2.imread(image_path, 1)[...,::-1] 
        h, w, _ = image.shape
        df = self.df.copy()
        df = df[df['ImageID'] == image_id]
        boxes = df['XMin,YMin,XMax,YMax'.split(',')].values
        boxes = (boxes*np.array([w,h,w,h])).astype(np.uint16)\
                                           .tolist()
        classes = df['LabelName'].values.tolist()
        return image, boxes, classes, image_path
  1. 检查样本图像及其相应的类和边界框基础事实:
ds = OpenImages(df=DF_RAW)
im, bbs, clss, _ = ds[9]
show(im, bbs=bbs, texts=clss, sz=10)

上述代码会产生以下结果:

  1. 定义extract_iouextract_candidates功能:
def extract_candidates(img):
    img_lbl,regions = selectivesearch.selective_search(img, \
                                    scale=200, min_size=100)
    img_area = np.prod(img.shape[:2])
    candidates = []
    for r in regions:
        if r['rect'] in candidates: continue
        if r['size'] < (0.05*img_area): continue
        if r['size'] > (1*img_area): continue
        x, y, w, h = r['rect']
        candidates.append(list(r['rect']))
    return candidates
def extract_iou(boxA, boxB, epsilon=1e-5):
    x1 = max(boxA[0], boxB[0])
    y1 = max(boxA[1], boxB[1])
    x2 = min(boxA[2], boxB[2])
    y2 = min(boxA[3], boxB[3])
    width = (x2 - x1)
    height = (y2 - y1)
    if (width<0) or (height <0):
        return 0.0
    area_overlap = width * height
    area_a = (boxA[2] - boxA[0]) * (boxA[3] - boxA[1])
    area_b = (boxB[2] - boxB[0]) * (boxB[3] - boxB[1])
    area_combined = area_a + area_b - area_overlap
    iou = area_overlap / (area_combined+epsilon)
    return iou

到目前为止,我们已经定义了准备数据和初始化数据加载器所需的所有函数。在下一节中,我们将获取区域建议(模型的输入区域)和边界框偏移的基本事实以及对象的类别(预期输出)。

提取区域建议和偏移的基本原理

在本节中,我们将学习如何创建与我们的模型相对应的输入和输出值。输入构成使用selectivesearch方法提取的候选项,输出构成对应于候选项的类别和候选项相对于边界框的偏移,如果候选项包含对象,则该偏移与边界框重叠最多。我们将从上一节结束的地方继续编码:

  1. 初始化空列表以存储文件路径(FPATHS)、基础事实边界框(GTBBS)、对象的类别(CLSS)、具有区域提议的边界框的增量偏移(DELTAS)、区域提议位置(ROIS)以及具有基础事实的区域提议的 IoU(IOUS):
FPATHS, GTBBS, CLSS, DELTAS, ROIS, IOUS = [],[],[],[],[],[]
  1. 遍历数据集并填充上面初始化的列表:
  • 在这个练习中,我们可以使用所有的数据点进行训练,或者只使用前 500 个数据点进行说明。您可以在这两者之间进行选择,这决定了训练时间和训练精度(数据点越多,训练时间和精度就越大):
N = 500
for ix, (im, bbs, labels, fpath) in enumerate(ds):
    if(ix==N):
        break

在前面的代码中,我们指定我们将处理 500 个图像。

  • 使用extract_candidates功能从每幅图像(im)中提取绝对像素值的候选对象(注意XMinXmaxYMinYMax可作为下载数据帧中图像形状的比例),并将提取的区域坐标从(x,y,w,h)系统转换为(x,y,x+w,y+h)系统:
    H, W, _ = im.shape
    candidates = extract_candidates(im)
    candidates = np.array([(x,y,x+w,y+h) \
                           for x,y,w,h in candidates])
  • iousroisdeltasclss初始化为存储每个候选的iou、区域提议位置、边界框偏移和对应于每个图像的每个候选的类的列表。我们将仔细检查 SelectiveSearch 中的所有提案,并将 IOU 高的提案存储为公共汽车/卡车提案(以标签中的类别为准),其余的作为背景提案:
    ious, rois, clss, deltas = [], [], [], []
  • 存储关于图像的所有基本事实的所有候选的 IoU,其中bbs是图像中存在的不同对象的基本事实边界框,并且candidates是在先前步骤中获得的区域提议候选:
    ious = np.array([[extract_iou(candidate, _bb_) for \
                candidate in candidates] for _bb_ in bbs]).T
  • 遍历每个候选项,并存储候选项的 XMin ( cx)、YMin ( cy)、XMax ( cX)和 YMax ( cY)值:
    for jx, candidate in enumerate(candidates):
        cx,cy,cX,cY = candidate
  • 提取在获取ious的列表列表时已经计算出的所有基本事实包围盒的候选对应的 IoU
        candidate_ious = ious[jx]
  • 找到具有最高 IoU 的候选(best_iou_at)的索引和相应的基础事实(best_bb):
        best_iou_at = np.argmax(candidate_ious)
        best_iou = candidate_ious[best_iou_at]
        best_bb = _x,_y,_X,_Y = bbs[best_iou_at]
  • 如果 IoU ( best_iou)大于阈值(0.3),我们分配对应于候选的类的标签,否则分配背景:
        if best_iou > 0.3: clss.append(labels[best_iou_at])
        else : clss.append('background')
  • 获取所需的偏移量(delta)以将当前提议转换为最佳区域提议的候选项(即基本事实边界框)–best_bb,换句话说,当前提议的左、右、上、下边界应调整多少,以使其与基本事实的best_bb完全对齐:
        delta = np.array([_x-cx, _y-cy, _X-cX, _Y-cY]) /\
                    np.array([W,H,W,H])
        deltas.append(delta)
        rois.append(candidate / np.array([W,H,W,H]))
  • 追加文件路径、IoU、roi、等级增量和基本事实边界框:
    FPATHS.append(fpath)
    IOUS.append(ious)
    ROIS.append(rois)
    CLSS.append(clss)
    DELTAS.append(deltas)
    GTBBS.append(bbs)
  • 获取图像路径名,并将获得的所有信息FPATHSIOUSROISCLSSDELTASGTBBS存储在列表列表中:
FPATHS = [f'{IMAGE_ROOT}/{stem(f)}.jpg' for f in FPATHS] 
FPATHS, GTBBS, CLSS, DELTAS, ROIS = [item for item in \
                                     [FPATHS, GTBBS, \
                                      CLSS, DELTAS, ROIS]]

注意,到目前为止,类都可以作为类名使用。现在,我们将它们转换成相应的索引,这样,背景类的索引为 0,公共汽车类的索引为 1,卡车类的索引为 2。

  1. 为每个类别分配索引:
targets = pd.DataFrame(flatten(CLSS), columns=['label'])
label2target = {l:t for t,l in \
                enumerate(targets['label'].unique())}
target2label = {t:l for l,t in label2target.items()}
background_class = label2target['background']

到目前为止,我们已经为每个区域提议分配了一个类,并且还创建了边界框偏移的另一个基本事实。在下一节中,我们将获取与所获得的信息相对应的数据集和数据加载器(FPATHSIOUSROISCLSSDELTASGTBBS)。

创建培训数据

到目前为止,我们已经获取了数据、跨所有图像的区域提议、准备了每个区域提议中存在的对象类别的基本事实、以及对应于与对应图像中的对象具有高重叠(IoU)的每个区域提议的偏移。

在本节中,我们将根据在第步第 8 结束时获得的区域建议的基本事实准备一个数据集类,并从中创建数据加载器。接下来,我们将通过将每个区域调整到相同的形状并缩放来规范化它们。我们将从上一节停止的地方继续编码:

  1. 定义归一化图像的函数:
normalize= transforms.Normalize(mean=[0.485, 0.456, 0.406], \
                                 std=[0.229, 0.224, 0.225])
  1. 定义一个函数(preprocess_image)来预处理图像(img),其中我们切换通道、标准化图像并将其注册到设备:
def preprocess_image(img):
    img = torch.tensor(img).permute(2,0,1)
    img = normalize(img)
    return img.to(device).float()
  • 定义函数给类decode预测:
def decode(_y):
    _, preds = _y.max(-1)
    return preds
  1. 使用预处理的区域建议以及在先前步骤中获得的基础事实来定义数据集(步骤 7 ):
class RCNNDataset(Dataset):
    def __init__(self, fpaths, rois, labels, deltas, gtbbs):
        self.fpaths = fpaths
        self.gtbbs = gtbbs
        self.rois = rois
        self.labels = labels
        self.deltas = deltas
    def __len__(self): return len(self.fpaths)
  • 根据区域建议获取作物,以及与类和边界框偏移相关的其他地面实况:
    def __getitem__(self, ix):
        fpath = str(self.fpaths[ix])
        image = cv2.imread(fpath, 1)[...,::-1]
        H, W, _ = image.shape
        sh = np.array([W,H,W,H])
        gtbbs = self.gtbbs[ix]
        rois = self.rois[ix]
        bbs = (np.array(rois)*sh).astype(np.uint16)
        labels = self.labels[ix]
        deltas = self.deltas[ix]
        crops = [image[y:Y,x:X] for (x,y,X,Y) in bbs]
        return image,crops,bbs,labels,deltas,gtbbs,fpath
  • 定义collate_fn,执行裁剪图像的尺寸调整和规格化(preprocess_image):
    def collate_fn(self, batch):
        input, rois, rixs, labels, deltas =[],[],[],[],[]
        for ix in range(len(batch)):
            image, crops, image_bbs, image_labels, \
                image_deltas, image_gt_bbs, \
                image_fpath = batch[ix]
            crops = [cv2.resize(crop, (224,224)) \
                     for crop in crops]
            crops = [preprocess_image(crop/255.)[None] \
                     for crop in crops]
            input.extend(crops)
            labels.extend([label2target[c] \
                           for c in image_labels])
            deltas.extend(image_deltas)
        input = torch.cat(input).to(device)
        labels = torch.Tensor(labels).long().to(device)
        deltas = torch.Tensor(deltas).float().to(device)
        return input, labels, deltas
  1. 创建训练和验证数据集以及数据加载器:
n_train = 9*len(FPATHS)//10
train_ds = RCNNDataset(FPATHS[:n_train], ROIS[:n_train], \
                       CLSS[:n_train], DELTAS[:n_train], \
                       GTBBS[:n_train])
test_ds = RCNNDataset(FPATHS[n_train:], ROIS[n_train:], \
                      CLSS[n_train:], DELTAS[n_train:], \
                      GTBBS[n_train:])

from torch.utils.data import TensorDataset, DataLoader
train_loader = DataLoader(train_ds, batch_size=2, \
                          collate_fn=train_ds.collate_fn, \
                          drop_last=True)
test_loader = DataLoader(test_ds, batch_size=2, \
                         collate_fn=test_ds.collate_fn, \
                         drop_last=True)

到目前为止,我们已经学习了准备数据。接下来,我们将了解如何定义和训练模型,该模型预测要对区域提议进行的分类和偏移,以适合图像中对象周围的紧密边界框。

R-CNN 网络架构

现在我们已经准备好了数据,在本节中,我们将学习如何构建一个模型,该模型可以预测区域建议的类别及其对应的偏移量,以便在图像中的对象周围绘制一个紧密的边界框。我们采取的策略如下:

  1. 定义一个 VGG 主干。

  2. 通过预训练模型传递归一化裁剪后获取要素。

  3. 将具有 sigmoid 激活的线性层连接到 VGG 主干,以预测与区域提议相对应的类别。

  4. 附加一个额外的线性图层来预测四个边界框偏移。

  5. 为两个输出中的每一个定义损失计算(一个用于预测类,另一个用于预测四个边界框偏移)。

  6. 训练预测区域建议类别和四个边界框偏移量的模型。

执行下面的代码。我们将从上一节结束的地方继续编码:

  1. 定义 VGG 主干:
vgg_backbone = models.vgg16(pretrained=True)
vgg_backbone.classifier = nn.Sequential()
for param in vgg_backbone.parameters():
    param.requires_grad = False
vgg_backbone.eval().to(device)
  1. 定义RCNN网络模块:
  • 定义类别:
class RCNN(nn.Module):
    def __init__(self):
        super().__init__()
  • 定义主干(self.backbone)以及我们如何计算类分数(self.cls_score)和边界框偏移值(self.bbox):
        feature_dim = 25088
        self.backbone = vgg_backbone
        self.cls_score = nn.Linear(feature_dim, \
                                    len(label2target))
        self.bbox = nn.Sequential(
                          nn.Linear(feature_dim, 512),
                          nn.ReLU(),
                          nn.Linear(512, 4),
                          nn.Tanh(),
                        )
  • 定义对应于类别预测(self.cel)和边界框偏移回归(self.sl1)的损失函数:
        self.cel = nn.CrossEntropyLoss()
        self.sl1 = nn.L1Loss()
  • 定义前馈方法,其中我们通过 VGG 主干(self.backbone)传递图像以获取特征(feat),这些特征进一步通过对应于分类和边界框回归的方法以获取跨类的概率(cls_score)和边界框偏移(bbox):
    def forward(self, input):
        feat = self.backbone(input)
        cls_score = self.cls_score(feat)
        bbox = self.bbox(feat)
        return cls_score, bbox
  • 定义计算损失的函数(calc_loss)。注意,如果实际类别是背景,我们不计算对应于偏移的回归损失:
    def calc_loss(self, probs, _deltas, labels, deltas):
        detection_loss = self.cel(probs, labels)
        ixs, = torch.where(labels != 0)
        _deltas = _deltas[ixs]
        deltas = deltas[ixs]
        self.lmb = 10.0
        if len(ixs) > 0:
            regression_loss = self.sl1(_deltas, deltas)
            return detection_loss + self.lmb *\
                regression_loss, detection_loss.detach(), \
                regression_loss.detach()
        else:
            regression_loss = 0
            return detection_loss + self.lmb *\
                regression_loss, detection_loss.detach(), \
                regression_loss

有了模型类,我们现在定义函数来训练一批数据并预测验证数据。

  1. 定义train_batch功能:
def train_batch(inputs, model, optimizer, criterion):
    input, clss, deltas = inputs
    model.train()
    optimizer.zero_grad()
    _clss, _deltas = model(input)
    loss, loc_loss, regr_loss = criterion(_clss, _deltas, \
                                            clss, deltas)
    accs = clss == decode(_clss)
    loss.backward()
    optimizer.step()
    return loss.detach(), loc_loss, regr_loss, \
        accs.cpu().numpy()
  1. 定义validate_batch功能:
@torch.no_grad()
def validate_batch(inputs, model, criterion):
    input, clss, deltas = inputs
    with torch.no_grad():
        model.eval()
        _clss,_deltas = model(input)
        loss,loc_loss,regr_loss = criterion(_clss, _deltas, \
                                               clss, deltas)
        _, _clss = _clss.max(-1)
        accs = clss == _clss
    return _clss,_deltas,loss.detach(),loc_loss, regr_loss, \
         accs.cpu().numpy()
  1. 现在,让我们创建一个模型对象,获取损失标准,然后定义优化器和时期数:
rcnn = RCNN().to(device)
criterion = rcnn.calc_loss
optimizer = optim.SGD(rcnn.parameters(), lr=1e-3)
n_epochs = 5
log = Report(n_epochs)
  1. 我们现在在增加的时期上训练模型:
for epoch in range(n_epochs):

    _n = len(train_loader)
    for ix, inputs in enumerate(train_loader):
        loss, loc_loss,regr_loss,accs = train_batch(inputs, \
                                  rcnn, optimizer, criterion)
        pos = (epoch + (ix+1)/_n)
        log.record(pos, trn_loss=loss.item(), \
                   trn_loc_loss=loc_loss, \
                   trn_regr_loss=regr_loss, \
                   trn_acc=accs.mean(), end='\r')

    _n = len(test_loader)
    for ix,inputs in enumerate(test_loader):
        _clss, _deltas, loss, \
        loc_loss, regr_loss, \
        accs = validate_batch(inputs, rcnn, criterion)
        pos = (epoch + (ix+1)/_n)
        log.record(pos, val_loss=loss.item(), \
                val_loc_loss=loc_loss, \
                val_regr_loss=regr_loss, \
                val_acc=accs.mean(), end='\r')

# Plotting training and validation metrics
log.plot_epochs('trn_loss,val_loss'.split(','))

跨训练和验证数据的总体损失图如下:

现在我们已经训练了一个模型,我们将在下一节中使用它来预测新图像。

预测新的图像

在本节中,我们将利用迄今为止训练的模型来预测和绘制新图像上的对象周围的边界框以及预测的边界框内的相应对象类别。我们采取的策略如下:

  1. 从新图像中提取区域建议。

  2. 调整大小和正常化每个作物。

  3. 前馈已处理的作物以预测类别和偏移量。

  4. 执行非最大抑制,仅提取那些包含对象的可信度最高的框。

我们通过一个将图像作为输入的函数和一个地面真实边界框(仅在我们比较地面真实和预测边界框时使用)来执行前面的策略。我们将从上一节停止的地方继续编码:

  1. 定义test_predictions函数对新图像进行预测:
  • 该函数将filename作为输入:
def test_predictions(filename, show_output=True):
  • 阅读图像并提取候选人:
    img = np.array(cv2.imread(filename, 1)[...,::-1])
    candidates = extract_candidates(img)
    candidates = [(x,y,x+w,y+h) for x,y,w,h in candidates]
  • 遍历候选图像以调整图像大小并对图像进行预处理:
    input = []
    for candidate in candidates:
        x,y,X,Y = candidate
        crop = cv2.resize(img[y:Y,x:X], (224,224))
        input.append(preprocess_image(crop/255.)[None])
    input = torch.cat(input).to(device)
  • 预测类别和偏移量:
    with torch.no_grad():
        rcnn.eval()
        probs, deltas = rcnn(input)
        probs = torch.nn.functional.softmax(probs, -1)
        confs, clss = torch.max(probs, -1)
  • 提取不属于背景类的候选项,并将候选项与预测的边界框偏移值相加:
    candidates = np.array(candidates)
    confs,clss,probs,deltas =[tensor.detach().cpu().numpy() \
                                  for tensor in [confs, \
                                        clss, probs, deltas]]

    ixs = clss!=background_class
    confs, clss,probs,deltas,candidates = [tensor[ixs] for \
           tensor in [confs,clss, probs, deltas,candidates]]
    bbs = (candidates + deltas).astype(np.uint16)
  • 使用非最大抑制nms消除近似重复的边界框(在这种情况下,IoU 大于 0.05 的框对被视为重复)。在重复的盒子中,我们挑选最有把握的那个盒子,并丢弃其余的:
    ixs = nms(torch.tensor(bbs.astype(np.float32)), \
                torch.tensor(confs), 0.05)
    confs,clss,probs,deltas,candidates,bbs = [tensor[ixs] \
                                            for tensor in \
                            [confs, clss, probs, deltas, \
                            candidates, bbs]]
    if len(ixs) == 1:
        confs, clss, probs, deltas, candidates, bbs = \
                [tensor[None] for tensor in [confs, clss,
                            probs, deltas, candidates, bbs]]
  • 获取具有最高可信度的边界框:
    if len(confs) == 0 and not show_output:
        return (0,0,224,224), 'background', 0
    if len(confs) > 0:
        best_pred = np.argmax(confs)
        best_conf = np.max(confs)
        best_bb = bbs[best_pred]
        x,y,X,Y = best_bb
  • 沿着预测的边界框绘制图像:
    _, ax = plt.subplots(1, 2, figsize=(20,10))
    show(img, ax=ax[0])
    ax[0].grid(False)
    ax[0].set_title('Original image')
    if len(confs) == 0:
        ax[1].imshow(img)
        ax[1].set_title('No objects')
        plt.show()
        return
    ax[1].set_title(target2label[clss[best_pred]])
    show(img, bbs=bbs.tolist(), 
        texts=[target2label[c] for c in clss.tolist()], 
        ax=ax[1], title='predicted bounding box and class')
    plt.show()
    return (x,y,X,Y),target2label[clss[best_pred]],best_conf
  1. 对新图像执行上述功能:
image, crops, bbs, labels, deltas, gtbbs, fpath = test_ds[7]
test_predictions(fpath)

上述代码生成了以下图像:

从前面的图中,我们可以看到对图像类别的预测是准确的,并且包围盒预测也很不错。请注意,为前面的图像生成预测大约需要 1.5 秒。

所有这些时间都消耗在生成区域建议、调整每个区域建议的大小、通过 VGG 主干传递它们以及使用定义的模型生成预测上。然而,大部分时间都花在通过 VGG 骨干网传递每一个提案上。在下一节中,我们将学习如何通过使用基于快速 R-CNN 架构的模型来解决“将每个提议传递给 VGG”的问题。

训练快速的基于 R-CNN 的自定义目标检测器

R-CNN 的一个主要缺点是生成预测需要相当长的时间,因为为每个图像生成区域建议、调整区域裁剪的大小以及提取对应于每个裁剪的特征(区域建议)构成了瓶颈。

快速 R-CNN 通过将整个图像通过预训练的模型来提取特征,然后获取与原始图像的区域提议(从selectivesearch获得)相对应的特征区域,从而解决了这个问题。在接下来的部分中,我们将学习快速 R-CNN 的工作细节,然后在我们的自定义数据集上训练它。

快速 R-CNN 的工作细节

我们通过下图来了解一下 Fast R-CNN:

让我们通过以下步骤来理解上图:

  1. 使图像通过预训练的模型,以在展平层之前提取特征;让我们称输出为特征地图。
  2. 提取对应于该图像的区域提议。
  3. 提取对应于区域提议的特征映射区域(注意,当图像通过 VGG16 架构时,由于执行了 5 次汇集操作,图像在输出端被缩小 32 倍。因此,如果在原始图像中存在具有(40,32,200,240)的边界框的区域,则对应于(5,4,25,30)的边界框的特征图将对应于完全相同的区域)。
  4. 通过 RoI(感兴趣区域)汇集层一次传递一个对应于区域提议的特征图,使得区域提议的所有特征图具有相似的形状。这是对在 R-CNN 技术中执行的扭曲的替换。
  5. 通过完全连接的层传递 RoI 池层输出值。
  6. 训练模型以预测对应于每个区域建议的类别和偏移量。

请注意,R-CNN 和快速 R-CNN 之间的巨大差异在于,在 R-CNN 中,我们通过预训练模型一次一个地传递裁剪(调整大小的区域提议),而在快速 R-CNN 中,我们裁剪与每个区域提议相对应的特征图(通过将整个图像通过预训练模型获得),从而避免需要通过预训练模型传递每个调整大小的区域提议。

现在已经了解了 R-CNN 的工作速度,在下一节中,我们将使用在 R-CNN 一节中使用的相同数据集来构建模型。

在自定义数据集上实现用于目标检测的快速 R-CNN

在本节中,我们将使用快速 R-CNN 来训练我们的自定义目标检测器。此外,为了保持简洁,我们在本节中仅提供了额外的或更改的代码(您应该运行所有代码,直到 R-CNN 的前一节中的创建训练数据小节中的步骤 2 ):

为了保持简洁,我们只提供了训练快速 R-CNN 的附加代码。完整的代码可以在本书的 GitHub 库的Chapter07文件夹中的*Training_Fast_R_CNN.ipynb*中找到。

  1. 创建一个FRCNNDataset类,该类返回图像、标签、基本事实、区域建议以及对应于每个区域建议的增量:
class FRCNNDataset(Dataset):
    def __init__(self, fpaths, rois, labels, deltas, gtbbs):
        self.fpaths = fpaths
        self.gtbbs = gtbbs
        self.rois = rois
        self.labels = labels
        self.deltas = deltas
    def __len__(self): return len(self.fpaths)
    def __getitem__(self, ix):
        fpath = str(self.fpaths[ix])
        image = cv2.imread(fpath, 1)[...,::-1]
        gtbbs = self.gtbbs[ix]
        rois = self.rois[ix]
        labels = self.labels[ix]
        deltas = self.deltas[ix]
        assert len(rois) == len(labels) == len(deltas), \
            f'{len(rois)}, {len(labels)}, {len(deltas)}'
        return image, rois, labels, deltas, gtbbs, fpath

    def collate_fn(self, batch):
        input, rois, rixs, labels, deltas = [],[],[],[],[]
        for ix in range(len(batch)):
            image, image_rois, image_labels, image_deltas, \
                image_gt_bbs, image_fpath = batch[ix]
            image = cv2.resize(image, (224,224))
            input.append(preprocess_image(img/255.)[None])
            rois.extend(image_rois)
            rixs.extend([ix]*len(image_rois))
            labels.extend([label2target[c] for c in \
                                image_labels])
            deltas.extend(image_deltas)
        input = torch.cat(input).to(device)
        rois = torch.Tensor(rois).float().to(device)
        rixs = torch.Tensor(rixs).float().to(device)
        labels = torch.Tensor(labels).long().to(device)
        deltas = torch.Tensor(deltas).float().to(device)
        return input, rois, rixs, labels, deltas

请注意,前面的代码非常类似于我们在 R-CNN 部分学到的内容,唯一的变化是我们返回了更多的信息(roisrixs)。

rois矩阵保存了关于哪个 RoI 属于批次中哪个图像的信息。注意,input包含多个图像,而rois是一个单一的盒子列表。我们不知道有多少 ROI 属于第一幅图像,有多少属于第二幅图像,等等。这就是ridx出现的原因。这是一个索引列表。列表中的每个整数将相应的边界框与适当的图像相关联;例如,如果ridx[0,0,0,1,1,2,3,3,3],那么我们知道前三个边界框属于该批中的第一个图像,接下来的两个属于该批中的第二个图像。

  1. 创建训练和测试数据集:
n_train = 9*len(FPATHS)//10
train_ds = FRCNNDataset(FPATHS[:n_train], ROIS[:n_train], \
                        CLSS[:n_train], DELTAS[:n_train], \
                        GTBBS[:n_train])
test_ds = FRCNNDataset(FPATHS[n_train:], ROIS[n_train:], \
                       CLSS[n_train:], DELTAS[n_train:], \
                       GTBBS[n_train:])

from torch.utils.data import TensorDataset, DataLoader
train_loader = DataLoader(train_ds, batch_size=2, \
                          collate_fn=train_ds.collate_fn, \
                          drop_last=True)
test_loader = DataLoader(test_ds, batch_size=2, \
                         collate_fn=test_ds.collate_fn, \
                         drop_last=True)
  1. 定义要在数据集上训练的模型:
  • 首先,导入torchvision.ops类中的RoIPool方法:
from torchvision.ops import RoIPool
  • 定义FRCNN网络模块:
class FRCNN(nn.Module):
    def __init__(self):
        super().__init__()
  • 加载预训练模型并冻结参数:
        rawnet = torchvision.models.vgg16_bn(pretrained=True)
        for param in rawnet.features.parameters():
            param.requires_grad = True
  • 提取要素直到最后一个图层:
        self.seq = nn.Sequential(*list(\
                            rawnet.features.children())[:-1])
  • 指定RoIPool提取一个 7×7 的输出。这里,spatial_scale是建议(来自原始图像)需要缩小的因子,以便每个输出在通过展平层之前具有相同的形状。图像的大小为 224 x 224,而特征图的大小为 14 x 14:
        self.roipool = RoIPool(7, spatial_scale=14/224)
  • 定义输出头-cls_scorebbox:
        feature_dim = 512*7*7
        self.cls_score = nn.Linear(feature_dim, \
                                   len(label2target))
        self.bbox = nn.Sequential(
                          nn.Linear(feature_dim, 512),
                          nn.ReLU(),
                          nn.Linear(512, 4),
                          nn.Tanh(),
                        )
  • 定义损失函数:
        self.cel = nn.CrossEntropyLoss()
        self.sl1 = nn.L1Loss()
  • 定义forward方法,该方法将图像、区域建议和区域建议索引作为之前定义的网络的输入:
    def forward(self, input, rois, ridx):
  • input图像通过预训练的模型:
        res = input
        res = self.seq(res)
  • 创建一个矩阵rois作为self.roipool的输入,首先将ridx作为第一列,接下来的四列是区域提议边界框的绝对值:
        rois = torch.cat([ridx.unsqueeze(-1), rois*224], \
                            dim=-1)
        res = self.roipool(res, rois)
        feat = res.view(len(res), -1)
        cls_score = self.cls_score(feat)
        bbox=self.bbox(feat)#.view(-1,len(label2target),4)
        return cls_score, bbox
  • 定义损失值计算(calc_loss),就像我们在 R-CNN 部分所做的那样:
    def calc_loss(self, probs, _deltas, labels, deltas):
        detection_loss = self.cel(probs, labels)
        ixs, = torch.where(labels != background_class)
        _deltas = _deltas[ixs]
        deltas = deltas[ixs]
        self.lmb = 10.0
        if len(ixs) > 0:
            regression_loss = self.sl1(_deltas, deltas)
            return detection_loss +\
                self.lmb * regression_loss, \
                detection_loss.detach(), \
                regression_loss.detach()
        else:
            regression_loss = 0
            return detection_loss + \
                self.lmb * regression_loss, \
                detection_loss.detach(), \
                regression_loss
  1. 像我们在 R-CNN 部分所做的那样,定义函数来训练和验证一个批处理:
def train_batch(inputs, model, optimizer, criterion):
    input, rois, rixs, clss, deltas = inputs
    model.train()
    optimizer.zero_grad()
    _clss, _deltas = model(input, rois, rixs)
    loss, loc_loss, regr_loss = criterion(_clss, _deltas, \
                                           clss, deltas)
    accs = clss == decode(_clss)
    loss.backward()
    optimizer.step()
    return loss.detach(), loc_loss, regr_loss, \
        accs.cpu().numpy()
def validate_batch(inputs, model, criterion):
    input, rois, rixs, clss, deltas = inputs
    with torch.no_grad():
        model.eval()
        _clss,_deltas = model(input, rois, rixs)
        loss, loc_loss,regr_loss = criterion(_clss, _deltas, \
                                                clss, deltas)
        _clss = decode(_clss)
        accs = clss == _clss
    return _clss, _deltas,loss.detach(), loc_loss,regr_loss, \
        accs.cpu().numpy()
  1. 在不断增加的时期内定义和训练模型:
frcnn = FRCNN().to(device)
criterion = frcnn.calc_loss
optimizer = optim.SGD(frcnn.parameters(), lr=1e-3)

n_epochs = 5
log = Report(n_epochs)
for epoch in range(n_epochs):

    _n = len(train_loader)
    for ix, inputs in enumerate(train_loader):
        loss, loc_loss,regr_loss, accs = train_batch(inputs, \
                                 frcnn, optimizer, criterion)
        pos = (epoch + (ix+1)/_n)
        log.record(pos, trn_loss=loss.item(), \
                   trn_loc_loss=loc_loss, \
                   trn_regr_loss=regr_loss, \
                   trn_acc=accs.mean(), end='\r')

    _n = len(test_loader)
    for ix,inputs in enumerate(test_loader):
        _clss, _deltas, loss, \
        loc_loss, regr_loss, accs = validate_batch(inputs, \
                                          frcnn, criterion)
        pos = (epoch + (ix+1)/_n)
        log.record(pos, val_loss=loss.item(), \
                val_loc_loss=loc_loss, \
                val_regr_loss=regr_loss, \
                val_acc=accs.mean(), end='\r')

# Plotting training and validation metrics
log.plot_epochs('trn_loss,val_loss'.split(','))

总损失的变化如下:

  1. 定义一个函数来预测测试图像:
  • 定义一个函数,该函数将文件名作为输入,然后读取文件并将其大小调整为 224 x 224:
import matplotlib.pyplot as plt
%matplotlib inline
import matplotlib.patches as mpatches
from torchvision.ops import nms
from PIL import Image
def test_predictions(filename):
    img = cv2.resize(np.array(Image.open(filename)), \
                               (224,224))
  • 获取区域建议并将其转换为(x1,y1,x2,y2)格式(左上像素和右下像素坐标),然后将这些值转换为它们所呈现的宽度和高度的比率,与图像成比例:
    candidates = extract_candidates(img)
    candidates = [(x,y,x+w,y+h) for x,y,w,h in candidates]
  • 预处理图像并缩放感兴趣区域(rois):
    input = preprocess_image(img/255.)[None]
    rois = [[x/224,y/224,X/224,Y/224] for x,y,X,Y in \
                candidates]
  • 由于所有建议都属于同一个图像,rixs将是一个零列表(与建议的数量一样多):
    rixs = np.array([0]*len(rois))
  • 通过训练的模型向前传播输入和rois,并获得每个提议的置信度和类别分数;
    rois,rixs = [torch.Tensor(item).to(device) for item in \
                    [rois, rixs]]
    with torch.no_grad():
        frcnn.eval()
        probs, deltas = frcnn(input, rois, rixs)
        confs, clss = torch.max(probs, -1)
  • 过滤掉背景类:
    candidates = np.array(candidates)
    confs,clss,probs,deltas=[tensor.detach().cpu().numpy() \
                                  for tensor in [confs, \
                                       clss, probs, deltas]]

    ixs = clss!=background_class
    confs, clss, probs, deltas,candidates = [tensor[ixs] for \
           tensor in [confs, clss, probs, deltas,candidates]]
    bbs = candidates + deltas
  • 移除带有nms的近似重复的边界框,并获得其中高度可信的模型是对象的那些提议的索引:
    ixs = nms(torch.tensor(bbs.astype(np.float32)), \
                torch.tensor(confs), 0.05)
    confs, clss, probs,deltas,candidates,bbs = [tensor[ixs] \
                            for tensor in [confs,clss,probs, \
                            deltas, candidates, bbs]]
    if len(ixs) == 1:
        confs, clss, probs, deltas, candidates, bbs = \
                    [tensor[None] for tensor in [confs,clss, \
                     probs, deltas, candidates, bbs]]

    bbs = bbs.astype(np.uint16)
  • 绘制获得的边界框:
    _, ax = plt.subplots(1, 2, figsize=(20,10))
    show(img, ax=ax[0])
    ax[0].grid(False)
    ax[0].set_title(filename.split('/')[-1])
    if len(confs) == 0:
        ax[1].imshow(img)
        ax[1].set_title('No objects')
        plt.show()
        return
    else:
        show(img,bbs=bbs.tolist(),texts=[target2label[c] for \
                                c in clss.tolist()],ax=ax[1])
        plt.show()
  1. 在测试图像上预测:
test_predictions(test_ds[29][-1])

上述代码会产生以下结果:

前面的代码在 0.5 秒内执行,这明显优于 R-CNN。不过实时使用起来还是很慢。这主要是因为我们仍在使用两种不同的模型,一种用于生成区域建议,另一种用于预测等级和修正。在下一章中,我们将学习如何用一个单一的模型来做预测,这样在实时场景中就可以快速地做出推断。

摘要

在这一章中,我们开始学习为目标定位和检测过程创建训练数据集。接下来,我们学习了 SelectiveSearch,这是一种基于邻近像素相似性推荐区域的区域推荐技术。接下来,我们学习了如何计算 IoU 度量,以了解图像中存在的对象周围的预测边界框的优劣。接下来,我们学习了如何执行非最大值抑制,以便在从头开始构建 R-CNN 和快速 R-CNN 模型之前,为图像中的每个对象获取一个边界框。此外,我们还了解了 R-CNN 速度慢的原因,以及 R-CNN 利用 RoI 池和从特征地图获取区域建议的速度有多快,从而加快了推理速度。最后,我们了解到,来自单独模型的区域建议会导致在新图像上进行预测需要更长的时间。

在下一章中,我们将学习一些现代目标检测技术,这些技术被用来在更实时的基础上进行推断。

问题

  1. 区域提议技术如何生成提议?
  2. 如果一个图像中有多个对象,如何计算 IoU?
  3. 为什么 R-CNN 生成预测需要很长时间?
  4. 为什么快速 R-CNN 比 R-CNN 快?
  5. 投资回报池是如何工作的?
  6. 预测边界框校正时,如果没有多个图层来发布所获得的要素地图,会有什么影响?
  7. 为什么在计算总体损失时,我们必须为回归损失分配更高的权重?
  8. 非最大抑制是如何工作的?

八、高级目标检测

在前一章中,我们学习了 R-CNN 和快速 R-CNN 技术,它们利用区域建议来预测图像中对象的位置以及图像中对象对应的类别。此外,我们了解了推理速度的瓶颈,这是因为有两个不同的模型——一个用于区域提议生成,另一个用于目标检测。在这一章中,我们将了解不同的现代技术,如更快的 R-CNN、YOLO 和单镜头检测器 ( SSD ),它们通过采用单个模型在单镜头中预测对象的类别和边界框来克服缓慢的推理时间。我们将从学习锚盒开始,然后继续学习每种技术如何工作,以及如何实现它们来检测图像中的对象。

我们将在本章中讨论以下主题:

  • 现代目标检测算法的组成部分
  • 在自定义数据集上训练更快的 R-CNN
  • YOLO 的工作细节
  • 在自定义数据集上训练 YOLO
  • 固态硬盘的工作细节
  • 在自定义数据集上训练 SSD

现代目标检测算法的组成部分

R-CNN 和快速 R-CNN 技术的缺点是它们有两个脱节的网络——一个用于识别可能包含对象的区域,另一个用于对识别对象的边界框进行校正。此外,两个模型都需要与区域建议一样多的前向传播。现代目标检测算法主要集中在训练单个神经网络,并且具有在一次正向传递中检测所有对象的能力。在随后的章节中,我们将了解典型现代目标检测算法的各种组件:

  • 锚箱
  • 地区提案网 ( RPN )
  • 利息集中区域

锚箱

到目前为止,我们已经收到了来自selectivesearch方法的地区提案。锚定框是选择性搜索的便捷替代品——我们将在本节中了解它们如何取代基于selectivesearch的区域提议。

通常,大多数对象具有相似的形状,例如,在大多数情况下,与人的图像相对应的边界框的高度大于宽度,与卡车的图像相对应的边界框的宽度大于高度。因此,即使在训练模型之前(通过检查与各种类别的对象相对应的边界框的基本事实),我们也将对图像中存在的对象的高度和宽度有相当好的了解。

此外,在某些图像中,感兴趣的对象可能会被缩放,导致比平均值小得多或大得多的高度和宽度,同时仍然保持纵横比(即)。

一旦我们对图像中存在的对象的长宽比以及高度和宽度(可以从数据集中的地面真实值获得)有了一个合适的概念,我们就可以用代表我们的数据集中大多数对象的边界框的高度和宽度来定义锚框。

典型地,这是通过在图像中存在的对象的真实边界框的顶部采用 K-means 聚类来获得的。

现在我们已经了解了锚盒的高度和宽度是如何获得的,我们将了解如何在此过程中利用它们:

  1. 将每个锚定框从左上向右下滑动到图像上。
  2. 与对象在并集 ( IoU )上有高交集的锚定框将有一个标签,表明它包含一个对象,其他的将被标记为 0:
  • 我们可以这样修改 IoU 的阈值:如果 IoU 大于某个阈值,则对象类为 1;如果小于另一个阈值,则对象类为 0,否则未知。

一旦我们获得了这里定义的地面真相,我们就可以建立一个模型,该模型可以预测对象的位置以及对应于锚框的偏移,以将其与地面真相相匹配。现在让我们了解下图中锚定框是如何表示的:

在上图中,我们有两个定位框,一个高度大于宽度,另一个宽度大于高度,以对应图像中的对象(类)——一个人和一辆车。

我们在图像上滑动两个锚框,并注意锚框的 IoU 与地面实况最高的位置,并表示该特定位置包含对象,而其余位置不包含对象。

除了前面的两个锚定框,我们还将创建具有不同比例的锚定框,以便我们适应对象在图像中呈现的不同比例。以下是不同比例的锚盒外观示例:

请注意,所有锚定框都具有相同的中心,但具有不同的纵横比或比例。

现在我们已经了解了锚盒,在下一节中,我们将学习 RPN,它利用锚盒来预测可能包含对象的区域。

区域提案网络

想象一个场景,我们有一个 224 x 224 x 3 的图像。此外,在本例中,假设锚盒的形状为 8 x 8。如果我们有一个 8 像素的跨距,我们为每一行获取图像的 224/8 = 28 个裁剪,实质上是从一个图像获取 28*28 = 576 个裁剪。然后,我们获取这些作物中的每一个,并通过一个区域提议网络模型(RPN)来指示该作物是否包含图像。从本质上来说, RPN 暗示作物包含物体的可能性。

让我们比较一下selectivesearch的输出和一个 RPN 的输出。

selectivesearch基于像素值之上的一组计算,给出候选区域。然而,RPN 基于锚框和锚框在图像上滑动的步幅来生成区域候选。一旦我们使用这两种方法中的任何一种获得候选区域,我们就识别出最有可能包含对象的候选区域。

虽然基于selectivesearch的区域提议生成是在神经网络之外完成的,但是我们可以构建作为目标检测网络一部分的 RPN。使用 RPN,我们现在不必执行不必要的计算来计算网络外部的区域提议。这样,我们有一个单一的模型来识别区域,识别图像中的对象类别,并识别它们相应的边界框位置。

接下来,我们将学习 RPN 如何识别候选区域(滑动锚框后获得的裁剪)是否包含对象。在我们的训练数据中,我们会将地面实况与对象相对应。我们现在取每个候选区域,并与图像中对象的基本真实边界框进行比较,以识别候选区域和基本真实边界框之间的 IoU 是否大于某个阈值。如果 IoU 大于某个阈值(比如 0.5),则候选区域包含对象,如果 IoU 小于阈值(比如 0.1),则候选区域不包含对象,并且在训练时忽略 IoU 在两个阈值(0.1 - 0.5)之间的所有候选区域。

一旦我们训练一个模型来预测候选区域是否包含一个对象,我们就执行非最大值抑制,因为多个重叠区域可以包含一个对象。

总之,RPN 通过执行以下步骤来训练模型,以使其能够识别具有包含对象的高可能性的区域提议:

  1. 在图像上滑动不同纵横比和大小的锚定框,以获取图像的裁剪。
  2. 计算图像中对象的地面真实边界框和上一步获得的裁剪之间的 IoU。
  3. 准备训练数据集,使 IoU 大于阈值的作物包含对象,IoU 小于阈值的作物不包含对象。
  4. 训练模型以识别包含对象的区域。
  5. 执行非最大抑制以识别包含对象的概率最高的候选区域,并消除与其高度重叠的其他候选区域。

分类和回归

到目前为止,我们已经了解了识别对象和执行边界框偏移的以下步骤:

  1. 识别包含对象的区域。
  2. 使用感兴趣区域 ( 感兴趣区域)池化(我们在前一章中了解过),确保所有区域的特征图完全相同,而不管区域的形状如何。

这些步骤的两个问题如下:

  1. 区域建议在对象上并不紧密对应(IoU>0.5 是我们在 RPN 中的阈值)。
  2. 我们识别该区域是否包含对象,但是不识别位于该区域中的对象的类别。

我们将在本节中解决这两个问题,我们采用之前获得的统一形状的特征地图,并通过网络传递它。我们期望网络预测包含在该区域内的对象的类别以及对应于该区域的偏移,以确保边界框尽可能紧密地围绕图像中的对象。

让我们通过下图来理解这一点:

在上图中,我们将 RoI 池的输出作为输入(7 x 7 x 512 形状),将其展平,并连接到密集层,然后预测两个不同的方面:

  1. 区域中对象的类别
  2. 要在区域的预测边界框上完成的偏移量,以使 IoU 与地面真实值最大化

因此,如果数据中有 20 个类,则神经网络的输出总共包含 25 个输出–21 个类(包括背景类)和应用于边界框的高度、宽度和两个中心坐标的 4 个偏移量。

现在我们已经了解了目标检测管道的不同组件,让我们用下图总结一下:

有了更快的 R-CNN 的每个组件的工作细节,在下一节中,我们将使用更快的 R-CNN 算法编写目标检测代码。

在自定义数据集上训练更快的 R-CNN

在下面的代码中,我们将训练更快的 R-CNN 算法来检测图像中对象周围的边界框。为此,我们将进行与上一章相同的卡车和公交车检测练习:

The following code is available as Training_Faster_RCNN.ipynb in the Chapter08 folder of this book's GitHub repository - tinyurl.com/mcvp-packt.

  1. 下载数据集:
import os
if not os.path.exists('images'):
    !pip install -qU torch_snippets
    from google.colab import files
    files.upload() # upload kaggle.json
    !mkdir -p ~/.kaggle
    !mv kaggle.json ~/.kaggle/
    !ls ~/.kaggle
    !chmod 600 /root/.kaggle/kaggle.json
    !kaggle datasets download \
        -d sixhky/open-images-bus-trucks/
    !unzip -qq open-images-bus-trucks.zip
    !rm open-images-bus-trucks.zip
  1. 读取包含关于图像及其边界框和类的信息的元数据的数据帧:
from torch_snippets import *
from PIL import Image
IMAGE_ROOT = 'img/images'
DF_RAW = df = pd.read_csv('df.csv')
  1. 定义与标签和目标相对应的索引:
label2target = {l:t+1 for t,l in \
                enumerate(DF_RAW['LabelName'].unique())}
label2target['background'] = 0
target2label = {t:l for l,t in label2target.items()}
background_class = label2target['background']
num_classes = len(label2target)
  1. 定义预处理图像的功能-preprocess_image:
def preprocess_image(img):
    img = torch.tensor(img).permute(2,0,1)
    return img.to(device).float()
  1. 定义数据集类-OpenDataset:
  • 定义一个__init__方法,将包含图像的文件夹和包含图像元数据的数据帧作为输入:
class OpenDataset(torch.utils.data.Dataset):
    w, h = 224, 224
    def __init__(self, df, image_dir=IMAGE_ROOT):
        self.image_dir = image_dir
        self.files = glob.glob(self.image_dir+'/*')
        self.df = df
        self.image_infos = df.ImageID.unique()
  • 定义__getitem__方法,其中我们返回预处理的图像和目标值:
    def __getitem__(self, ix):
        # load images and masks
        image_id = self.image_infos[ix]
        img_path = find(image_id, self.files)
        img = Image.open(img_path).convert("RGB")
        img = np.array(img.resize((self.w, self.h), \
                              resample=Image.BILINEAR))/255.
        data = df[df['ImageID'] == image_id]
        labels = data['LabelName'].values.tolist()
        data = data[['XMin','YMin','XMax','YMax']].values
        # Convert to absolute coordinates
        data[:,[0,2]] *= self.w
        data[:,[1,3]] *= self.h 
        boxes = data.astype(np.uint32).tolist() 
        # torch FRCNN expects ground truths as 
        # a dictionary of tensors
        target = {}
        target["boxes"] = torch.Tensor(boxes).float()
        target["labels"] = torch.Tensor([label2target[i] \
                                for i in labels]).long()
        img = preprocess_image(img)
        return img, target

注意,我们第一次将输出作为张量的字典返回,而不是作为张量的列表。这是因为FRCNN类的官方 PyTorch 实现期望目标包含边界框的绝对坐标和标签信息。

  • 定义collate_fn方法(默认情况下,collate_fn只使用张量作为输入,但是在这里,我们处理的是字典列表)和__len__方法:
    def collate_fn(self, batch):
        return tuple(zip(*batch)) 

    def __len__(self):
        return len(self.image_infos)
  1. 创建培训和验证数据加载器和数据集:
from sklearn.model_selection import train_test_split
trn_ids, val_ids = train_test_split(df.ImageID.unique(), \
                    test_size=0.1, random_state=99)
trn_df, val_df = df[df['ImageID'].isin(trn_ids)], \
                    df[df['ImageID'].isin(val_ids)]

train_ds = OpenDataset(trn_df)
test_ds = OpenDataset(val_df)

train_loader = DataLoader(train_ds, batch_size=4, \
            collate_fn=train_ds.collate_fn, drop_last=True)
test_loader = DataLoader(test_ds, batch_size=4, \
            collate_fn=test_ds.collate_fn, drop_last=True)
  1. 定义模型:
import torchvision
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor

device = 'cuda' if torch.cuda.is_available() else 'cpu'

def get_model():
    model = torchvision.models.detection\
                .fasterrcnn_resnet50_fpn(pretrained=True)
    in_features = model.roi_heads.box_predictor\
                       .cls_score.in_features
    model.roi_heads.box_predictor = FastRCNNPredictor(\

                                in_features, num_classes)
    return model

该模型包含以下关键子模块:

我们注意到以下情况:

  • GeneralizedRCNNTransform是一个简单的 resize,后面跟着一个 normalize 变换:

  • BackboneWithFPN是将输入转换成特征图的神经网络。
  • RegionProposalNetwork为前面的特征图生成锚框,并为分类和回归任务预测各个特征图:

  • RoIHeads获取前面的图,使用 RoI 池对齐它们,处理它们,并返回每个建议的分类概率和相应的偏移:

  1. 定义函数以对批量数据进行训练,并计算验证数据的损失值:
# Defining training and validation functions 
def train_batch(inputs, model, optimizer):
    model.train()
    input, targets = inputs
    input = list(image.to(device) for image in input)
    targets = [{k: v.to(device) for k, v \
                in t.items()} for t in targets]
    optimizer.zero_grad()
    losses = model(input, targets)
    loss = sum(loss for loss in losses.values())
    loss.backward()
    optimizer.step()
    return loss, losses

@torch.no_grad() 
def validate_batch(inputs, model):
    model.train() 
#to obtain losses, model needs to be in train mode only
#Note that here we arn't defining the model's forward method 
#hence need to work per the way the model class is defined
    input, targets = inputs
    input = list(image.to(device) for image in input)
    targets = [{k: v.to(device) for k, v \
                in t.items()} for t in targets]

    optimizer.zero_grad()
    losses = model(input, targets)
    loss = sum(loss for loss in losses.values())
    return loss, losses
  1. 在不断增加的时期内训练模型:
  • 定义模型:
model = get_model().to(device)
optimizer = torch.optim.SGD(model.parameters(), lr=0.005, \
                            momentum=0.9,weight_decay=0.0005)
n_epochs = 5
log = Report(n_epochs)
  • 训练模型并计算训练和测试数据集的损失值:
for epoch in range(n_epochs):
    _n = len(train_loader)
    for ix, inputs in enumerate(train_loader):
        loss, losses = train_batch(inputs, model, optimizer)
        loc_loss, regr_loss, loss_objectness, \
            loss_rpn_box_reg = \
                [losses[k] for k in ['loss_classifier', \
                'loss_box_reg', 'loss_objectness', \
                'loss_rpn_box_reg']]
        pos = (epoch + (ix+1)/_n)
        log.record(pos, trn_loss=loss.item(), \
                 trn_loc_loss=loc_loss.item(), \
                 trn_regr_loss=regr_loss.item(), \
                 trn_objectness_loss=loss_objectness.item(), \
               trn_rpn_box_reg_loss=loss_rpn_box_reg.item(), \
                 end='\r')

    _n = len(test_loader)
    for ix,inputs in enumerate(test_loader):
        loss, losses = validate_batch(inputs, model)
        loc_loss, regr_loss, loss_objectness, \
            loss_rpn_box_reg = \
                [losses[k] for k in ['loss_classifier', \
                'loss_box_reg', 'loss_objectness', \
                'loss_rpn_box_reg']]
        pos = (epoch + (ix+1)/_n)
        log.record(pos, val_loss=loss.item(), \
                 val_loc_loss=loc_loss.item(), \
                 val_regr_loss=regr_loss.item(), \
                val_objectness_loss=loss_objectness.item(), \
               val_rpn_box_reg_loss=loss_rpn_box_reg.item(), \
                 end='\r')
    if (epoch+1)%(n_epochs//5)==0: log.report_avgs(epoch+1)
  1. 绘制各种损失值在增加的时期内的变化:
log.plot_epochs(['trn_loss','val_loss'])

这会产生以下输出:

  1. 对新图像进行预测:
  • 经过训练的模型的输出包含对应于类别的框、标签和分数。在下面的代码中,我们定义了一个decode_output函数,它接受模型的输出,并在非最大值抑制后提供框、分数和类的列表:
from torchvision.ops import nms
def decode_output(output):
    'convert tensors to numpy arrays'
    bbs = \
    output['boxes'].cpu().detach().numpy().astype(np.uint16)
    labels = np.array([target2label[i] for i in \
                output['labels'].cpu().detach().numpy()])
    confs = output['scores'].cpu().detach().numpy()
    ixs = nms(torch.tensor(bbs.astype(np.float32)), 
                            torch.tensor(confs), 0.05)
    bbs, confs, labels = [tensor[ixs] for tensor in [bbs, \
                                            confs, labels]]

    if len(ixs) == 1:
        bbs,confs,labels = [np.array([tensor]) for tensor \
                                in [bbs, confs, labels]]
    return bbs.tolist(), confs.tolist(), labels.tolist()
  • 获取测试图像上的盒子和类的预测:
model.eval()
for ix, (images, targets) in enumerate(test_loader):
    if ix==3: break
    images = [im for im in images]
    outputs = model(images)
    for ix, output in enumerate(outputs):
        bbs, confs, labels = decode_output(output)
        info = [f'{l}@{c:.2f}' for l,c in zip(labels, confs)]
        show(images[ix].cpu().permute(1,2,0), bbs=bbs, \
                texts=labels, sz=5)

上述代码提供了以下输出:

在本节中,我们使用 PyTorch models包中提供的fasterrcnn_resnet50_fpn模型类训练了一个更快的 R-CNN 模型。在下一节中,我们将了解 YOLO,这是一种现代的目标检测算法,它可以在单次拍摄中执行对象类别检测和区域校正,而无需单独的 RPN。

YOLO 的工作细节

你只看一次 ( YOLO )及其变体是突出的目标检测算法之一。在这一节中,我们将从较高的层面了解 YOLO 是如何工作的,以及 YOLO 所克服的基于 R-CNN 的目标检测框架的潜在局限性。

首先,让我们了解一下基于 R-CNN 的检测算法可能存在的局限性。在更快的 R-CNN 中,我们使用锚框在图像上滑动,并识别可能包含对象的区域,然后我们进行边界框校正。然而,在完全连接的层中,其中只有检测到的区域的 RoI 汇集输出作为输入被传递,在区域没有完全包围对象的情况下(其中对象在区域提议的边界框的边界之外),网络必须猜测对象的真实边界,因为它没有看到完整的图像(但是只看到了区域提议)。

在这种情况下,YOLO 派上了用场,因为它在预测图像对应的边界框时会查看整个图像。

此外,fast R-CNN 仍然很慢,因为我们有两个网络:RPN 和预测类和对象周围的边界框的最终网络。

在这里,我们将了解 YOLO 如何克服更快的 R-CNN 的限制,既通过一次查看整个图像,又通过单一网络进行预测。我们将通过以下示例了解如何为 YOLO 准备数据:

  1. 为给定图像创建地面实况以训练模型:
  • 让我们考虑一个给定的红色边界框的图像:

  • 将图像分成 N x N 个网格单元——现在,假设 N =3:

  • 识别那些包含至少一个基本事实边界框中心的格网单元。在我们的例子中,它们是 3×3 网格图像的单元格 b1b3
  • 地面真实边界框的中点所在的单元负责预测对象的边界框。让我们创建对应于每个单元格的地面真相。
  • 对应于每个单元的输出接地真值如下:

这里, pc (对象性得分)是单元格包含对象的概率。

先来了解一下如何通过、 bwbh 计算 bx

首先,我们将网格单元(让我们将 b1 网格单元)视为我们的宇宙,并将其归一化为 0 到 1 之间的范围,如下所示:

bxby 是地面真实边界框相对于(网格单元的)图像的中点位置,如前所述。在我们的例子中, bx = 0.5,因为地面真实的中点距离原点 0.5 个单位。同理, = 0.5:

到目前为止,我们已经计算了从网格单元中心到对应于图像中对象的地面真实中心的偏移。现在,我们来了解一下 bwbh 是如何计算的。

bw 是边界框的宽度相对于网格单元宽度的比值。

bh 是边界框的高度相对于网格单元高度的比值。

接下来,我们将预测网格单元对应的类。如果我们有三个类(C1-卡车,C2-汽车,C3-公共汽车),我们将预测单元包含三个类中任何一个对象的概率。注意,这里我们不需要背景类,因为 pc 对应于网格单元是否包含对象。

现在我们已经了解了如何表示每个单元的输出层,让我们了解如何构造 3 x 3 网格单元的输出。

  • 让我们考虑网格单元 a3 的输出:

单元格 a3 的输出如前面的截图所示。由于网格单元不包含对象,第一个输出(PC–objectness score)是 0,并且剩余的值无关紧要,因为单元不包含对象的任何地面真实边界框的中心。

  • 让我们考虑对应于网格单元 b1 的输出:

前面的输出之所以是这样,是因为网格单元格包含一个对象,该对象的 bxbybwbh 值是以与我们之前经历的相同的方式获得的(在上一个项目符号中),最后类为car,导致 c2 为 1,而 c1 和 c3 为 0。

注意,对于每个单元,我们能够获取 8 个输出。因此,对于 3×3 网格的单元,我们获取 3×3×8 输出。

  1. 定义一个模型,其中输入是一个图像,输出是 3 x 3 x 8,地面实况是上一步中定义的:

  1. 通过考虑锚盒来定义地面实况。

到目前为止,我们已经构建了一个场景,期望在一个网格单元中只有一个对象。然而,在现实中,可能会出现在同一个网格单元中有多个对象的情况。这将导致创造不正确的真相。让我们通过下面的示例图像来理解这一现象:

在前面的示例中,汽车和人的基本事实边界框的中点都落在同一个单元格中——单元格 b1

避免这种情况的一种方法是使用包含更多行和列的网格,例如 19 x 19 的网格。但是,仍有可能出现增加网格单元数量于事无补的情况。锚盒在这种情况下就派上了用场。假设我们有两个锚定框——一个高度大于宽度(对应于人),另一个宽度大于高度(对应于车):

通常,定位框会将网格单元中心作为它们的中心。在我们有两个锚盒的场景中,每个单元的输出被表示为两个锚盒的预期输出的串联:

这里, bxbybwbh 表示从锚定框的偏移(这是图像中看到的这个场景中的宇宙,而不是网格单元)。

从前面的截图中,我们看到我们有一个 3 x 3 x 16 的输出,因为我们有两个锚点。预期输出的形式为NxNx(num_classes+1)x(num_anchor_boxes),其中 N x N 是网格中单元的数量,num_classes是数据集中类的数量,num_anchor_boxes是锚框的数量。

  1. 现在我们定义损失函数来训练模型。

当计算与模型相关联的损失时,我们需要确保当对象性分数小于某个阈值(这对应于不包含对象的单元)时,我们不计算回归损失和分类损失。

接下来,如果单元格包含一个对象,我们需要确保不同类之间的分类尽可能准确。

最后,如果单元格包含一个对象,边界框偏移量应该尽可能接近预期值。然而,由于宽度和高度的偏移量与中心的偏移量相比可能高得多(因为中心的偏移量在 0 和 1 之间,而宽度和高度的偏移量不需要),所以我们通过取平方根值来给宽度和高度的偏移量一个较低的权重。

计算本地化和分类的损失如下:

在这里,我们观察到以下情况:

  • 是与回归损失相关的权重。
  • 表示单元格中是否包含对象。
  • 对应于预测的类别概率,表示客观分数。

总损失是分类和回归损失值的总和。

有了这些,我们现在可以训练一个模型来预测物体周围的边界框。然而,为了更好地理解 YOLO 和它的变体,我们鼓励你浏览原始论文。现在我们已经了解了 YOLO 是如何在一次拍摄中预测边界框和对象类别的,我们将在下一节中编写代码。

在自定义数据集上训练 YOLO

建立在他人工作的基础上对于成为深度学习的成功实践者非常重要。对于这个实现,我们将使用官方的 YOLO-v4 实现来识别图像中公共汽车和卡车的位置。我们将克隆作者自己的 YOLO 实现的存储库,并在下面的代码中根据我们的需要定制它。

以下代码在本书的 GitHub 资源库【tinyurl.com/mcvp-packt[…](tinyurl.com/mcvp-packt)

安装暗网

首先,从 GitHub 中拉出darknet库,并在环境中编译它。该模型是用一种叫做 Darknet 的独立语言编写的,它不同于 PyTorch。我们将使用以下代码来实现这一点:

  1. 拉动 Git 回购:
!git clone https://github.com/AlexeyAB/darknet
%cd darknet
  1. 重新配置Makefile文件:
!sed -i 's/OPENCV=0/OPENCV=1/' Makefile
# In case you dont have a GPU, make sure to comment out the
# below 3 lines
!sed -i 's/GPU=0/GPU=1/' Makefile
!sed -i 's/CUDNN=0/CUDNN=1/' Makefile
!sed -i 's/CUDNN_HALF=0/CUDNN_HALF=1/' Makefile

Makefile是在环境中安装darknet所需的配置文件(将此过程想象成类似于在 Windows 上安装软件时所做的选择)。我们正在强制darknet安装以下标志:OPENCVGPUCUDNNCUDNN_HALF。这些都是让训练更快的重要优化。 此外,在前面的代码中,有一个奇怪的函数叫做sed,它代表流编辑器。这是一个强大的 Linux 命令,可以直接从命令提示符下修改文本文件中的信息。具体来说,这里我们使用它的搜索替换功能将OPENCV=0替换为OPENCV=1,以此类推。这里要理解的语法是sed 's/<search-string>/<replace-with>/' path/to/text/file

  1. 编译darknet源代码:
!make
  1. 安装torch_snippets包:
!pip install -q torch_snippets
  1. 下载并提取数据集,并删除 ZIP 文件以节省空间:
!wget --quiet \
 https://www.dropbox.com/s/agmzwk95v96ihic/open-images-bus-trucks.tar.xz
!tar -xf open-images-bus-trucks.tar.xz
!rm open-images-bus-trucks.tar.xz
  1. 获取预先训练的权重以进行样本预测:
!wget --quiet\ https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.weights
  1. 通过运行以下命令测试安装是否成功:
!./darknet detector test cfg/coco.data cfg/yolov4.cfg\ yolov4.weights
 data/person.jpg

这将使用从cfg/yolov4.cfg和预先训练的权重yolov4.weights构建的网络对data/person.jpg进行预测。此外,它从cfg/coco.data中获取类,这是预训练权重被训练的内容。

前面的代码导致对样本图像(data/person.jpg)的预测如下:

现在我们已经了解了如何安装darknet,在下一节中,我们将了解如何为我们的自定义数据集创建基本事实来利用darknet

设置数据集格式

YOLO 使用固定的训练模式。一旦我们以所需的格式存储了图像和标签,我们就可以用一个命令在数据集上进行训练。因此,让我们了解一下 YOLO 培训所需的文件和文件夹结构。

有三个重要步骤:

  1. 通过运行下面一行,在data/obj.names创建一个包含类名的文本文件,每行一个类(%%writefile是一个神奇的命令,它在data/obj.names创建一个包含笔记本单元格中存在的任何内容的文本文件):
%%writefile data/obj.names
bus
truck
  1. data/obj.data创建一个文本文件,描述数据集中的参数、包含训练和测试图像路径的文本文件的位置、包含对象名称的文件的位置以及保存训练模型的文件夹:
%%writefile data/obj.data
classes = 2
train = data/train.txt
valid = data/val.txt
names = data/obj.names
backup = backup/

The extensions for the preceding text files are not .txt. Yolo uses hardcoded names and folders to identify where data is. Also, the magic %%writefile Jupyter function creates a file with the content mentioned in a cell, as shown previously. Treat each %%writefile ... as a separate cell in Jupyter.

  1. 将所有图像和地面实况文本文件移动到data/obj文件夹。我们将把图像从bus-trucks数据集连同标签一起复制到该文件夹:
!mkdir -p data/obj
!cp -r open-images-bus-trucks/img/* data/obj/
!cp -r open-images-bus-trucks/yolo_labels/all/\
{train,val}.txt data/
!cp -r open-images-bus-trucks/yolo_labels/all/\
labels/*.txt data/obj/

请注意,所有训练和验证图像都在同一个data/obj文件夹中。我们还将一些文本文件移动到同一个文件夹中。每个包含图像基本事实的文件都与该图像同名。例如,文件夹可能包含1001.jpg1001.txt,这意味着文本文件包含该图像的标签和边界框。如果data/train.txt包含1001.jpg作为它的一条线,那么它就是一个训练图像。如果它出现在val.txt中,那么它是一个验证图像。

文本文件本身应该包含这样的信息:cls, xc, yc, w, h,,其中cls是位于(xc, yc)的边界框中对象的类索引,它代表宽度w和高度h的矩形的质心。xcycwh中的每一个都是图像宽度和高度的一部分。将每个对象存储在单独的行上。

例如,如果宽度为 800、高度为 600 的图像包含一辆卡车和一辆公共汽车,分别位于中心(500,300)和(100,400),并且宽度和高度分别为(200,100)和(300,50),则文本文件将如下所示:

1 0.62 0.50 0.25 0.12
0 0.12 0.67 0.38 0.08

现在我们已经创建了数据,让我们在下一部分配置网络架构。

配置架构

YOLO 有一长串的建筑。一些是大的,一些是小的,以在大或小的数据集上进行训练。配置可以有不同的主干。标准数据集有预先训练好的配置。每个配置都是一个.cfg文件,位于我们克隆的同一个 GitHub repo 的cfgs文件夹中。每一个都包含了文本文件形式的网络架构(与我们用nn.Module类构建的方式相反)以及一些超参数,比如批量大小和学习速率。我们将采用最小的可用架构,并针对我们的数据集进行配置:

# create a copy of existing configuration and modify it in place
!cp cfg/yolov4-tiny-custom.cfg cfg/\
yolov4-tiny-bus-trucks.cfg
# max_batches to 4000 (since the dataset is small enough)
!sed -i 's/max_batches = 500200/max_batches=4000/' \
cfg/yolov4-tiny-bus-trucks.cfg
# number of sub-batches per batch
!sed -i 's/subdivisions=1/subdivisions=16/' \
cfg/yolov4-tiny-bus-trucks.cfg
# number of batches after which learning rate is decayed
!sed -i 's/steps=400000,450000/steps=3200,3600/' \
cfg/yolov4-tiny-bus-trucks.cfg
# number of classes is 2 as opposed to 80 
# (which is the number of COCO classes)
!sed -i 's/classes=80/classes=2/g' \
cfg/yolov4-tiny-bus-trucks.cfg
# in the classification and regression heads, 
# change number of output convolution filters
# from 255 -> 21 and 57 -> 33, since we have fewer classes 
# we don't need as many filters
!sed -i 's/filters=255/filters=21/g' \
cfg/yolov4-tiny-bus-trucks.cfg
!sed -i 's/filters=57/filters=33/g' \
cfg/yolov4-tiny-bus-trucks.cfg

这样,我们重新调整了yolov4-tiny的用途,使其可以在我们的数据集上训练。剩下的唯一步骤是加载预训练的权重并训练模型,这将在下一节中进行。

训练和测试模型

我们将从下面的 GitHub 位置获取权重,并将它们存储在build/darknet/x64中:

!wget --quiet \ https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v4_pre/yolov4-tiny.conv.29
!cp yolov4-tiny.conv.29 build/darknet/x64/

最后,我们将使用以下代码来训练模型:

!./darknet detector train data/obj.data \
cfg/yolov4-tiny-bus-trucks.cfg yolov4-tiny.conv.29 \
-dont_show -mapLastAt

-dont_show标志跳过显示中间预测图像,并且-mapLastAt将定期打印验证数据的平均精度。整个培训可能需要 1 到 2 个小时。权重定期存储在备份文件夹中,并可在训练后用于预测,如以下代码,该代码对新图像进行预测:

!pip install torch_snippets
from torch_snippets import Glob, stem, show, read
# upload your own images to a folder
image_paths = Glob('images-of-trucks-and-busses')
for f in image_paths:
    !./darknet detector test \
    data/obj.data cfg/yolov4-tiny-bus-trucks.cfg\
    backup/yolov4-tiny-bus-trucks_4000.weights {f}
    !mv predictions.jpg {stem(f)}_pred.jpg
for i in Glob('*_pred.jpg'):
    show(read(i, 1), sz=20)

上述代码会导致以下结果:

现在,我们已经了解了如何利用 YOLO 对我们的自定义数据集执行目标检测,在下一部分,我们将了解如何利用 SSD 执行目标检测。

固态硬盘的工作细节

到目前为止,我们已经看到了这样一个场景,我们在逐渐卷积和汇集来自前一层的输出后进行预测。然而,我们知道不同的层对原始图像有不同的感受域。例如,与具有较大感受野的最终层相比,初始层具有较小的感受野。在这里,我们将了解 SSD 如何利用这一现象来预测图像的边界框。

SSD 如何帮助克服检测不同比例对象的问题,其工作原理如下:

  • 我们利用预训练的 VGG 网络,并用几个附加层来扩展它,直到我们获得 1×1 块。
  • 我们将利用所有最后几层来进行类和边界框预测,而不是只利用最后一层来进行边界框和类预测。
  • 在锚定框的位置,我们将提出具有特定比例和纵横比的默认框。
  • 每个默认框都应该预测对象和边界框的偏移量,就像锚框在 YOLO 预测类和偏移量一样。

现在我们已经了解了 SSD 与 YOLO 的主要不同之处(即 SSD 中的默认盒取代了 YOLO 的锚盒,多个层连接到 SSD 中的最终层,而不是 YOLO 的渐进卷积池),让我们了解以下内容:

  • 固态硬盘的网络架构
  • 如何利用不同的层进行边界框和类预测
  • 如何为不同图层中的默认框分配比例和纵横比

固态硬盘的网络架构如下:

正如您在前面的图表中看到的,我们正在获取一个大小为 300 x 300 x 3 的图像,并通过预先训练的 VGG-16 网络来获取conv5_3层的输出。此外,我们通过向conv5_3输出添加一些卷积来扩展网络。

接下来,我们获得每个单元格和每个默认框的边界框偏移量和类预测(在下一节中有更多关于默认框的内容;现在,我们假设这类似于一个锚盒)。来自conv5_3输出的预测总数是 38 x 38 x 4,其中 38 x 38 是conv5_3层的输出形状,4 是在conv5_3层上运行的默认盒子的数量。类似地,网络中的参数总数如下:

| | 参数数量 | | conv5_3 | 38 X 38 X 4 = 5,776 | | FC6 | 19 X 19 X 6 = 2,166 | | conv8_2 | 10 X 10 X 6 = 600 | | conv9_2 | 5 X 5 X 6 = 150 | | conv10_2 | 3 X 3 X 4 = 36 | | conv11_2 | 1 X 1 X 4 = 4 | | 总参数 | 8732 |

请注意,与原始论文中描述的架构中的其他层相比,某些层有更多的盒子(6 个而不是 4 个)。

现在,让我们了解一下默认框的不同比例和长宽比。我们将从比例开始,然后讨论长宽比。

让我们设想一个场景,一个物体的最小尺度是一个图像高度的 20%和宽度的 20%,物体的最大尺度是高度的 90%和宽度的 90%。在这种情况下,我们逐渐增加跨层的比例(随着我们向后面的层前进,图像大小会显著缩小),如下所示:

实现图像逐步缩放的公式如下:

现在我们已经了解了如何跨层计算比例,我们现在将学习如何制作不同纵横比的盒子。

可能的纵横比如下:

不同层的方框中心如下:

这里 ij 一起代表 l 层中的一个单元格。

对应于不同纵横比的宽度和高度计算如下:

请注意,我们考虑的是在某些层中有四个盒子,在另一层中有六个盒子。现在,如果我们想要有四个盒子,我们移除{3,1/3}纵横比,否则我们考虑所有六个可能的盒子(五个具有相同比例的盒子和一个具有不同比例的盒子)。那么,让我们学习如何获得第六个盒子:

现在我们已经有了所有可能的框,让我们了解如何准备训练数据集。

IoU 大于阈值(比如 0.5)的默认框被认为是正匹配,其余的是负匹配。

在 SSD 的输出中,我们预测盒子属于一个类的概率(其中第 0 个^(到第)个类代表背景)以及地面真实相对于默认盒子的偏移。

最后,我们通过优化以下损失值来训练模型:

  • 分类损失:使用以下等式表示:

在前面的等式中,pos表示与地面实况高度重叠的几个默认框,而neg表示预测了类但实际上不包含对象的误分类框。最后,我们确保pos : neg的比率最多为 1:3,就好像我们不执行这个采样,我们将拥有背景类盒的优势。

  • **定位损失:**对于定位,我们仅在客观分数大于某个阈值时才考虑损失值。本地化损失计算如下:

这里 t 是预测偏移, d 是实际偏移。

现在我们已经了解了如何训练 SSD,让我们在下一节中使用它来进行公交车与卡车目标检测练习。

GitHub repo:github.com/sizhky/ssd-utils/中提供了该部分的核心实用函数。在开始训练过程之前,让我们一个一个地了解它们。

SSD 代码中的组件

GitHub repo 中有三个文件。先稍微挖掘一下,了解一下再训练。请注意,本节不是培训过程的一部分,而是为了理解培训过程中使用的导入。

我们正在从 GitHub 资源库的model.py文件中导入SSD300MultiBoxLoss类。让我们来了解一下他们两个。

固态硬盘 300

当您查看SSD300函数定义时,很明显该模型包含三个子模块:

class SSD300(nn.Module):
    ...
    def __init__(self, n_classes, device):
        ...
 self.base = VGGBase()
 self.aux_convs = AuxiliaryConvolutions()
 self.pred_convs = PredictionConvolutions(n_classes) ...

我们首先将输入发送给VGGBase,它返回维度为(N, 512, 38, 38)(N, 1024, 19, 19)的两个特征向量。第二个输出将是AuxiliaryConvolutions的输入,它返回更多维度的特征图(N, 512, 10, 10)(N, 256, 5, 5)(N, 256, 3, 3)(N, 256, 1, 1)。最后,来自VGGBase的第一个输出和这四个特征图被发送到PredictionConvolutions,它返回 8732 个锚盒,正如我们之前讨论的。

SSD300类的另一个关键方面是create_prior_boxes方法。对于每个特征地图,都有三个与之相关的项目:网格的大小、缩小网格单元的比例(这是该特征地图的基础锚点框),以及单元中所有锚点的纵横比。使用这三种配置,代码使用三重for循环并为所有 8732 个锚盒创建一个(cx, cy, w, h)列表。

最后,detect_objects方法获取分类和回归值的张量(预测锚框的),并将它们转换成实际的边界框坐标。

多盒损耗

作为人类,我们只担心少数边界框。但是对于 SSD 的工作方式,我们需要比较来自几个特征地图的 8732 个边界框,并预测锚框是否包含有价值的信息。我们将这个损失计算任务分配给MultiBoxLoss

正向方法的输入是来自模型和地面真实边界框的锚框预测。

首先,我们通过将模型中的每个锚点与边界框进行比较,将基础真实框转换成 8,732 个锚点框的列表。如果 IoU 足够高,则该特定锚定框将具有非零回归坐标,并且关联一个对象作为分类的基础事实。自然地,大多数被计算的定位框将有它们的相关类作为background,因为它们与实际边界框的 IoU 将很小,或者在相当多的情况下为零。

一旦地面事实被转换成这 8,732 个锚盒回归和分类张量,就很容易将它们与模型的预测进行比较,因为形状现在是相同的。

我们对回归张量执行MSE-Loss,对定位张量执行CrossEntropy-Loss,并将它们相加,作为最终损失返回。

在自定义数据集上训练 SSD

在下面的代码中,我们将训练 SSD 算法来检测图像中存在的对象周围的边界框。我们将使用我们一直在进行的卡车与公共汽车目标检测任务:

The following code is available as Training_SSD.ipynb in the Chapter08 folder of this book's GitHub repository - tinyurl.com/mcvp-packt The code contains URLs to download data from and is moderately lengthy. We strongly recommend you to execute the notebook in GitHub to reproduce results while you understand the steps to perform and explanation of various code components from text.

  1. 下载图像数据集并克隆 Git 存储库,该存储库托管模型的代码和用于处理数据的其他实用程序:
import os
if not os.path.exists('open-images-bus-trucks'):
    !pip install -q torch_snippets
    !wget --quiet https://www.dropbox.com/s/agmzwk95v96ihic/\
    open-images-bus-trucks.tar.xz
    !tar -xf open-images-bus-trucks.tar.xz
    !rm open-images-bus-trucks.tar.xz
    !git clone https://github.com/sizhky/ssd-utils/
%cd ssd-utils
  1. 预处理数据,就像我们在培训 更快的 R-CNN 定制数据集部分所做的那样:
from torch_snippets import *
DATA_ROOT = '../open-images-bus-trucks/'
IMAGE_ROOT = f'{DATA_ROOT}/images'
DF_RAW = pd.read_csv(f'{DATA_ROOT}/df.csv')
df = DF_RAW.copy()

df = df[df['ImageID'].isin(df['ImageID'].unique().tolist())]

label2target = {l:t+1 for t,l in enumerate(DF_RAW['LabelName'].unique())}
label2target['background'] = 0
target2label = {t:l for l,t in label2target.items()}
background_class = label2target['background']
num_classes = len(label2target)

device = 'cuda' if torch.cuda.is_available() else 'cpu'
  1. 准备一个数据集类,就像我们在在自定义数据集上训练更快的 R-CNN部分所做的那样:
import collections, os, torch
from PIL import Image
from torchvision import transforms
normalize = transforms.Normalize(
                mean=[0.485, 0.456, 0.406],
                std=[0.229, 0.224, 0.225]
            )
denormalize = transforms.Normalize(
                mean=[-0.485/0.229,-0.456/0.224,-0.406/0.255],
                std=[1/0.229, 1/0.224, 1/0.255]
            )

def preprocess_image(img):
    img = torch.tensor(img).permute(2,0,1)
    img = normalize(img)
    return img.to(device).float()

class OpenDataset(torch.utils.data.Dataset):
    w, h = 300, 300
    def __init__(self, df, image_dir=IMAGE_ROOT):
        self.image_dir = image_dir
        self.files = glob.glob(self.image_dir+'/*')
        self.df = df
        self.image_infos = df.ImageID.unique()
        logger.info(f'{len(self)} items loaded')

    def __getitem__(self, ix):
        # load images and masks
        image_id = self.image_infos[ix]
        img_path = find(image_id, self.files)
        img = Image.open(img_path).convert("RGB")
        img = np.array(img.resize((self.w, self.h), \
                       resample=Image.BILINEAR))/255.
        data = df[df['ImageID'] == image_id]
        labels = data['LabelName'].values.tolist()
        data = data[['XMin','YMin','XMax','YMax']].values
        data[:,[0,2]] *= self.w
        data[:,[1,3]] *= self.h
        boxes = data.astype(np.uint32).tolist() # convert to 
        # absolute coordinates
        return img, boxes, labels

    def collate_fn(self, batch):
        images, boxes, labels = [], [], []
        for item in batch:
            img, image_boxes, image_labels = item
            img = preprocess_image(img)[None]
            images.append(img)
            boxes.append(torch.tensor( \
                        image_boxes).float().to(device)/300.)
            labels.append(torch.tensor([label2target[c] \
                    for c in image_labels]).long().to(device))
        images = torch.cat(images).to(device)
        return images, boxes, labels
    def __len__(self):
        return len(self.image_infos)
  1. 准备训练和测试数据集以及数据加载器:
from sklearn.model_selection import train_test_split
trn_ids, val_ids = train_test_split(df.ImageID.unique(), \
                             test_size=0.1, random_state=99)
trn_df, val_df = df[df['ImageID'].isin(trn_ids)], \
                df[df['ImageID'].isin(val_ids)]

train_ds = OpenDataset(trn_df)
test_ds = OpenDataset(val_df)

train_loader = DataLoader(train_ds, batch_size=4, \
                          collate_fn=train_ds.collate_fn, \
                          drop_last=True)
test_loader = DataLoader(test_ds, batch_size=4, \
                         collate_fn=test_ds.collate_fn, \
                         drop_last=True)
  1. 定义函数以对一批数据进行训练,并计算验证数据的准确度和损失值:
def train_batch(inputs, model, criterion, optimizer):
    model.train()
    N = len(train_loader)
    images, boxes, labels = inputs
    _regr, _clss = model(images)
    loss = criterion(_regr, _clss, boxes, labels)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    return loss

@torch.no_grad()
def validate_batch(inputs, model, criterion):
    model.eval()
    images, boxes, labels = inputs
    _regr, _clss = model(images)
    loss = criterion(_regr, _clss, boxes, labels)
    return loss
  1. 导入模型:
from model import SSD300, MultiBoxLoss
from detect import *
  1. 初始化模型、优化器和损失函数:
n_epochs = 5

model = SSD300(num_classes, device)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, \
                              weight_decay=1e-5)
criterion = MultiBoxLoss(priors_cxcy=model.priors_cxcy, \
                         device=device)

log = Report(n_epochs=n_epochs)
logs_to_print = 5
  1. 在不断增加的时期内训练模型:
for epoch in range(n_epochs):
    _n = len(train_loader)
    for ix, inputs in enumerate(train_loader):
        loss = train_batch(inputs, model, criterion, \
                            optimizer)
        pos = (epoch + (ix+1)/_n)
        log.record(pos, trn_loss=loss.item(), end='\r')

    _n = len(test_loader)
    for ix,inputs in enumerate(test_loader):
        loss = validate_batch(inputs, model, criterion)
        pos = (epoch + (ix+1)/_n)
        log.record(pos, val_loss=loss.item(), end='\r')

各时期的训练和测试损失值的变化如下:

  1. 获取对新图像的预测:
  • 获取随机图像:
image_paths = Glob(f'{DATA_ROOT}/img/*')
image_id = choose(test_ds.image_infos)
img_path = find(image_id, test_ds.files)
original_image = Image.open(img_path, mode='r')
original_image = original_image.convert('RGB')
  • 获取与图像中存在的对象相对应的边界框、标签和分数:
bbs, labels, scores = detect(original_image, model, \
                             min_score=0.9, max_overlap=0.5,\
                             top_k=200, device=device)
  • 将获得的输出叠加到图像上:
labels = [target2label[c.item()] for c in labels]
label_with_conf = [f'{l} @ {s:.2f}' \
                   for l,s in zip(labels,scores)]
print(bbs, label_with_conf)
show(original_image, bbs=bbs, \
     texts=label_with_conf, text_sz=10)

前面的代码获取如下输出示例(每次迭代执行一个图像):

由此可见,我们可以相当准确地检测图像中的对象。

摘要

在这一章中,我们已经了解了现代目标检测算法的工作细节:更快的 R-CNN、YOLO 和 SSD。我们了解了他们如何克服拥有两个独立模型的限制——一个用于获取区域提议,另一个用于获取区域提议上的类和边界框偏移量。此外,我们使用 PyTorch 实现了更快的 R-CNN,使用darknet实现了 YOLO,并从头开始实现 SSD。

在下一章,我们将学习图像分割,它通过识别对应于物体的像素,比物体定位更进了一步。

此外,在第十五章、结合计算机视觉和 NLP 技术中,我们将了解 DETR,一种基于转换器的目标检测算法,在第十章、目标检测和分割的应用中,我们将了解 Detectron2 框架,该框架不仅有助于检测对象,而且有助于在单次拍摄中分割它们。

测试你的理解能力

  1. 为什么快速 R-CNN 比快速 R-CNN 更快?
  2. 与更快的 R-CNN 相比,YOLO 和 SSD 的速度如何?
  3. 是什么让 YOLO 和 SSD 单拍算法?
  4. 客观分和类分有什么区别?
  5. 锚定框和默认框有什么区别?