🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎
📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝
📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
🖍foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟👋
文章目录
到目前为止,在前面的章节中,我们学习了执行图像分类。想象一下我们将计算机视觉用于自动驾驶汽车的场景。不仅需要检测道路图像是否包含车辆、人行道和行人的图像,识别这些物体的位置也很重要。我们将在本章和下一章学习的各种对象检测技术将在这种情况下派上用场。
在本章和下一章中,我们将学习一些用于执行对象检测的技术。我们将首先学习基础知识——使用名为 的工具标记边界框对象的基本事实,ybat使用该方法提取区域建议,并使用交并比( IoU ) 度量和均值selectivesearch定义边界框预测的准确性平均精度指标。在此之后,我们将了解两个基于区域提议的网络——R-CNN 和 Fast R-CNN,首先了解它们的工作细节,然后在包含卡车和公共汽车图像的数据集上实现它们。
本章将涵盖以下主题:
随着自动驾驶汽车、面部检测、智能视频监控和人数统计解决方案的兴起,对快速准确的物体检测系统的需求量很大。这些系统不仅包括来自图像的对象分类,还包括通过在对象周围绘制适当的边界框来定位每个对象。这(绘制边界框和分类)使对象检测比其传统的计算机视觉前身图像分类更难。
要了解物体检测的输出是什么样的,让我们看一下下图:
在上图中,我们可以看到,虽然典型的对象分类仅提及图像中存在的对象类别,但对象定位在图像中存在的对象周围绘制了一个边界框。另一方面,对象检测将涉及在图像中的单个对象周围绘制边界框,以及在图像中存在的多个对象的边界框中识别对象的类别。
在我们了解对象检测的广泛用例之前,让我们了解它如何添加到我们在上一章中介绍的对象分类任务中。
想象一个场景,您的图像中有多个对象。我要求您预测图像中存在的对象类别。例如,假设图像包含猫和狗。你会如何对这些图像进行分类?对象检测在这种情况下派上用场,它不仅可以预测其中存在的对象(边界框)的位置,还可以预测各个边界框中存在的对象类别。
利用对象检测的一些不同用例包括以下内容:
在上述所有情况下,都利用对象检测在图像中存在的各种对象周围绘制边界框。
在本章中,我们将学习预测对象的类别以及在图像中的对象周围有一个紧密的边界框,这就是定位任务。我们还将学习检测图片中多个对象对应的类别,以及每个对象周围的边界框,这就是对象检测任务。
训练一个典型的物体检测模型包括以下步骤:
现在我们已经对训练对象检测模型要做什么有了一个高层次的概述,我们将在下一节中学习为边界框创建数据集(这是构建对象检测模型的第一步) .
我们已经了解到,对象检测为我们提供了一个边界框围绕图像中感兴趣的对象的输出。为了构建一种算法来检测图像中对象周围的边界框,我们必须创建输入-输出组合,其中输入是图像,输出是给定图像中对象周围的边界框,以及对象对应的类。
要训练提供边界框的模型,我们需要图像,以及图像中所有对象的相应边界框坐标。在本节中,我们将了解一种创建训练数据集的方法,其中图像是输入,相应的边界框和对象类存储在 XML 文件中作为输出。我们将使用该ybat工具来注释边界框和相应的类。
让我们了解安装和使用ybat以在图像中的对象周围创建(注释)边界框。此外,我们还将在下一节中检查包含带注释的类和边界框信息的 XML 文件。
让我们ybat-master.zip从以下 GitHub 链接GitHub - drainingsun/ybat: Ybat - YOLO BBox Annotation Tool下载并解压缩它。解压后,将其存储在您选择的文件夹中。使用您选择的浏览器打开ybat.html,您将看到一个空白页面。以下屏幕截图显示了文件夹的外观以及如何打开ybat.html文件的示例:
在我们开始创建对应于图像的基本事实之前,让我们指定我们想要跨图像标记并存储在classes.txt文件中的所有可能类,如下所示:
现在,让我们准备对应于图像的基本事实。这涉及在对象(下图中的人)周围绘制一个边界框,并在以下步骤中为图像中存在的对象分配标签/类:
使用下图可以更好地表示所有这些步骤:
例如,当我们下载 PascalVOC 格式时,它会下载 XML 文件的 zip。绘制矩形边界框后的 XML 文件截图如下:
从前面的屏幕截图中,请注意,该字段包含与图像中感兴趣的对象对应的x和ybndbox坐标的最小值和最大值的坐标。我们还应该能够使用该字段提取与图像中的对象对应的类。name
现在我们了解了如何创建图像中存在的对象(类标签和边界框)的基本事实,在接下来的部分中,我们将深入研究识别图像中对象的构建块。首先,我们将讨论有助于突出图像中最有可能包含对象的部分的区域建议。
想象一个假设场景,其中感兴趣的图像在背景中包含人和天空。此外,对于这种情况,我们假设背景(天空)的像素强度变化不大,而前景(人)的像素强度变化很大。
仅从前面的描述本身,我们可以得出结论,这里有两个主要区域——一个是人的,另一个是天空的。此外,在人的图像区域内,对应于头发的像素与对应于面部的像素具有不同的强度,从而确定一个区域内可以存在多个子区域。
区域提议是一种有助于识别像素彼此相似的区域岛屿的技术。
生成区域提议对于对象检测非常方便,我们必须识别图像中存在的对象的位置。此外,给定区域建议会生成该区域的建议,它有助于对象定位,其中任务是识别与图像中的对象完全吻合的边界框。我们将在后面关于训练基于 R-CNN 的自定义对象检测器的部分中了解区域提议如何帮助对象定位和检测,但让我们首先了解如何从图像生成区域提议。
SelectiveSearch 是一种用于对象定位的区域建议算法,它生成可能根据像素强度组合在一起的区域建议。SelectiveSearch 根据相似像素的分层分组对像素进行分组,进而利用图像中内容的颜色、纹理、大小和形状兼容性。
最初, SelectiveSearch 通过根据前面的属性对像素进行分组来过度分割图像。接下来,它遍历这些过度分割的组,并根据相似性对它们进行分组。在每次迭代中,它将较小的区域组合成一个较大的区域。
让我们通过下面的例子来理解这个selectivesearch 过程:
1.安装所需的软件包:
- !pip install selectivesearch
- !pip install torch_snippets
- from torch_snippets import *
- import selectivesearch
- from skimage.segmentation import felzenszwalb
2.获取并加载所需的图像:
- !wget https://www.dropbox.com/s/l98leemr7r5stnm/Hemanvi.jpeg
- img = read('Hemanvi.jpeg', 1)
3.从图像中提取felzenszwalb片段(根据图像中内容的颜色、纹理、大小和形状兼容性获得):
segment_fz = felzenszwalb(img, scale=200)
请注意,在该felzenszwalb方法中,scale表示可以在图像片段内形成的簇数。的值越高scale,保留的原始图像的细节越多。
4.绘制原始图像和带有分割的图像:
- subplots([img, segments_fz], \
- titles=['Original Image',\
- 'Image post\nfelzenszwalb segmentation'],\
- sz=10, nc=2)
前面的代码产生以下输出:
从前面的输出中,请注意属于同一组的像素具有相似的像素值。
现在我们了解了 SelectiveSearch 的作用,让我们实现该selectivesearch函数来获取给定图像的区域建议。
在本节中,我们将定义extract_candidates函数 usingselectivesearch以便在后续训练基于 R-CNN 和 Fast R-CNN 的自定义对象检测器的部分中使用它:
1.定义extract_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
2.导入相关包并获取图像:
- !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)
3.提取候选对象并将它们绘制在图像上:
- candidates = extract_candidates(img)
- show(img, bbs=candidates)
上述代码生成以下输出:
上图中的网格表示来自该selective_search方法的候选区域(区域建议)。
现在我们了解了区域提案的生成,还有一个问题没有得到解答。我们如何利用区域建议进行对象检测和定位?
与感兴趣图像中对象的位置(ground truth)相交高的区域提议被标记为包含该对象的区域,相交低的区域提议被标记为背景。
在下一节中,我们将了解如何计算候选区域与地面实况边界框的交集,以了解构成构建对象检测模型的各种技术。
想象一个场景,我们提出了一个对象边界框的预测。我们如何衡量我们预测的准确性?Intersection over Union ( IoU )的概念在这种情况下会派上用场。
Intersection over Union术语中的Intersection测量预测的边界框和实际边界框的重叠程度,而Union测量可能重叠的整体空间。IoU 是两个边界框之间的重叠区域与两个边界框的组合区域的比率。
这可以在图表中表示如下:
在前面两个边界框(矩形)的图表中,让我们将左边界框视为地面实况,将右边界框视为对象的预测位置。作为度量的 IoU 是两个边界框之间的重叠区域与组合区域的比率。
在下图中,您可以观察到 IoU 指标的变化,因为边界框之间的重叠发生了变化:
从上图中,我们可以看到随着重叠减少,IoU 减少,在最后一个没有重叠的地方,IoU 度量为 0。
现在我们有了测量 IoU 的直觉,让我们在代码中实现它并创建一个计算 IoU 的函数,因为我们将在训练 R-CNN 和训练 Fast R-CNN 的部分中利用它。
让我们定义一个函数,它将两个边界框作为输入并返回 IoU 作为输出:
1.指定将和作为输入的get_iou函数,其中和是两个不同的边界框(您可以将其视为地面实况边界框和区域提议):boxAboxBboxAboxB boxAboxB
def get_iou(boxA, boxB, epsilon=1e-5):
我们定义epsilon参数以解决两个框之间的并集为 0 时的罕见情况,从而导致除以零错误。请注意,在每个边界框中,将有四个值对应于边界框的四个角。
2.计算相交框的坐标:
- 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值和x2存储y2最右边的x值和最底部的y值,分别对应于相交部分。
3.计算width和height对应的交叉区域(重叠区域):
- width = (x2 - x1)
- height = (y2 - y1)
4.计算重叠面积 ( area_overlap):
- if (width<0) or (height <0):
- return 0.0
- area_overlap = width * height
注意,在前面的代码中,我们指定如果重叠区域对应的宽度或高度小于0,则相交的面积为0。否则,我们计算重叠(相交)的面积类似于矩形的计算计算面积 - 宽度乘以高度。
5.计算两个bounding box对应的组合面积:
- 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_a和area_b,然后在计算时减去重叠区域,计算area_combined两次area_overlap,计算时一次,计算area_a时一次area_b。
6.计算 IoU 并返回:
- iou = area_overlap / (area_combined+epsilon)
- return iou
在前面的代码中,我们计算iou了重叠area_overlap面积 ( ) 与组合区域面积( ) 的比率area_combined并将其返回。
到目前为止,我们已经了解了如何创建 ground truth 和计算 IoU,这有助于准备训练数据。接下来,对象检测模型将在检测图像中的对象时派上用场。最后,我们将计算模型性能并推断新图像。
我们将推迟构建模型,直到接下来的部分,因为训练模型涉及更多内容,而且在训练之前我们还必须学习更多组件。在下一节中,我们将学习非最大抑制,它有助于在使用经过训练的模型在新图像上进行推断时,从对象周围不同的可能预测边界框中筛选出候选名单。
想象一个场景,其中生成了多个区域提案并且彼此显着重叠。本质上,所有预测的边界框坐标(区域建议的偏移量)彼此显着重叠。例如,让我们考虑下图,其中为图像中的人生成了多个区域建议:
在上图中,我要求您在我们将考虑为包含对象的许多区域建议中识别框以及我们将丢弃的框。在这种情况下,非最大抑制会派上用场。让我们解开术语“非最大抑制”。
非最大是指不包含最高概率包含对象的框,抑制是指我们丢弃那些不包含最高概率包含对象的框。在非最大抑制中,我们识别具有最高概率的边界框,并丢弃所有其他 IoU 大于某个阈值的边界框,该框包含包含对象的最高概率。
在 PyTorch 中,使用模块中的nms 函数执行非最大抑制torchvision.ops。该nms 函数通过边界框坐标、物体在边界框中的置信度和边界框之间的IoU阈值来识别要保留的边界框。您将分别在步骤 19和16中的基于训练 R-CNN 的自定义对象检测器和基于训练快速 R-CNN 的自定义对象检测器nms部分中预测新图像中的对象类别和对象边界框时利用该功能.
到目前为止,我们已经看到了一个输出,该输出包括图像中每个对象周围的边界框以及与边界框中的对象对应的类。现在来了下一个问题:我们如何量化来自我们模型的预测的准确性?
在这种情况下,mAP 就派上用场了。在我们尝试理解 mAP 之前,让我们先理解精度,然后是平均精度,最后是 mAP:
真正的肯定是指预测正确类别的对象并且具有大于特定阈值的地面实况的 IoU 的边界框。误报是指边界框对类别的预测不正确或与基本事实的重叠小于定义的阈值。此外,如果为同一个 ground truth 边界框识别出多个边界框,则只有一个框可以成为真阳性,而其他所有框都成为假阳性。
到目前为止,我们已经了解了如何为我们的模型准备训练数据集、对模型的预测执行非最大抑制以及计算其准确性。在以下部分中,我们将学习如何训练模型(基于 R-CNN 和基于 Fast R-CNN)来检测新图像中的对象。
R-CNN 代表基于区域的卷积神经网络。R-CNN 中基于区域的代表区域建议。区域提议用于识别图像中的对象。请注意,R-CNN 有助于识别图像中存在的对象以及图像中对象的位置。
在以下部分中,我们将了解 R-CNN 的工作细节,然后再在我们的自定义数据集上进行训练。
让我们使用下图从高层次上了解基于 R-CNN 的对象检测:
在利用 R-CNN 技术进行对象检测时,我们执行以下步骤:
1.从图像中提取区域建议:
2.调整(扭曲)所有提取区域的大小以获得相同大小的图像。
3.通过网络传递调整大小的区域提案:
4.为模型训练创建数据,其中输入是通过将区域提议通过预训练模型提取的特征,输出是与每个区域提议对应的类以及区域提议与图像对应的地面实况的偏移量:
为区域提议创建边界框偏移和地面实况类的结果示例如下:
在上图中,o(红色)表示区域提议的中心(虚线边界框),x 表示对应于猫类的 ground truth 边界框(实心边界框)的中心。我们将 region proposal 边界框和 ground truth 边界框之间的偏移量计算为两个边界框的中心坐标 (dx, dy) 与边界框的高度和宽度之间的差值 (dw, dh )。
5.将两个输出头连接起来,一个对应于图像的类别,另一个对应于区域提议的偏移量与ground truth边界框,以提取对象上的精细边界框:
6.训练模型帖子,编写自定义损失函数,以最小化对象分类误差和边界框偏移误差。
请注意,我们将最小化的损失函数与原始论文中优化的损失函数不同。我们这样做是为了降低与从头开始构建 R-CNN 和 Fast R-CNN 相关的复杂性。一旦读者熟悉了模型的工作原理并可以使用以下代码构建模型,我们强烈鼓励他们从头开始实现原始论文。
在下一节中,我们将学习获取数据集和创建训练数据。在之后的部分中,我们将学习如何设计模型并在预测新图像中存在的对象类别及其边界框之前对其进行训练。
到目前为止,我们对 R-CNN 的工作原理有了理论上的了解。在本节中,我们将学习如何为训练创建数据。此过程涉及以下步骤:
1.下载数据集
2.准备数据集
3.定义区域提议提取和 IoU 计算函数
4.创建训练数据
5.定义和训练模型
6.预测新图像
让我们开始在以下部分中进行编码。
对于目标检测场景,我们将从 Google Open Images v6 数据集(可在https://storage.googleapis.com/openimages/v5/test-annotations-bbox.csv获得)下载数据。但是,在代码中,我们将只处理公共汽车或卡车的图像,以确保我们可以训练图像(因为您很快就会注意到与使用相关的内存问题selectivesearch)。我们将扩大我们将在第 10 章“对象检测和分割的应用”中训练的课程数量(除了公共汽车和卡车之外的更多课程)。
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 = 'images/images'
- DF_RAW = pd.read_csv('df.csv')
- print(DF_RAW.head())
上述数据帧的示例如下:
请注意,XMin、XMax、YMin和YMax对应于图像边界框的基本事实。此外,LabelName提供图像的类别。
2.定义一个类,该类返回图像及其对应的类和基本事实以及图像的文件路径:
- 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)
- 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
3.检查样本图像及其对应的类和边界框基本事实:
- ds = OpenImages(df=DF_RAW)
- im, bbs, clss, _ = ds[9]
- show(im, bbs=bbs, texts=clss, sz=10)
前面的代码结果如下:
4.定义extract_iou和extract_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(真相():GTBBSCLSSDELTASROISIOUS
FPATHS, GTBBS, CLSS, DELTAS, ROIS, IOUS = [],[],[],[],[],[]
2.循环遍历数据集并填充上面初始化的列表:
- N = 500
- for ix, (im, bbs, labels, fpath) in enumerate(ds):
- if(ix==N):
- break
在前面的代码中,我们指定我们将处理 500 个图像。
- H, W, _ = im.shape Candidates
- = extract_candidates(im)
- Candidates = np.array([(x,y,x+w,y+h) \
- for x,y,w,h in Candidates])
ious, rois, clss, deltas = [], [], [], []
- ious = np.array([[extract_iou(candidate, _bb_) for \
- Candidate in Candidates] for _bb_ in bbs]).T
- for jx, candidate in enumerate(candidates):
- cx,cy,cX,cY = candidate
candidate_ious = ious[jx]
- best_iou_at = np.argmax(candidate_ious)
- best_iou = Candidate_ious[best_iou_at]
- best_bb = _x,_y,_X,_Y = bbs[best_iou_at]
- if best_iou > 0.3: clss.append(labels[best_iou_at])
- else : clss.append('background')
- 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]))
- FPATHS.append(fpath)
- IOUS.append (ious)
- ROIS.append(rois)
- CLSS.append(clss)
- DELTAS.append(deltas)
- GTBBS.append(bbs)
- 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。
3.为每个类分配索引:
- 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']
到目前为止,我们已经为每个区域提案分配了一个类,并且还创建了边界框偏移的另一个基本事实。在下一节中,我们将获取与获得的信息相对应的数据集和数据加载器(FPATHS、IOUS、ROIS、CLSS、DELTAS和GTBBS)。
到目前为止,我们已经获取了所有图像中的数据、区域提议,准备了每个区域提议中存在的对象类别的基本事实,以及与每个区域提议相对应的偏移量,这些区域提议与目标区域中的对象具有高度重叠 (IoU)。对应的图像。
在本节中,我们将根据在步骤 8结束时获得的区域建议的基本事实准备一个数据集类,并从中创建数据加载器。接下来,我们将通过将每个区域提案调整为相同的形状并缩放它们来规范化它们。我们将从上一节中停止的地方继续编码:
1.定义对图像进行归一化的函数:
- normalize= transforms.Normalize(mean=[0.485, 0.456, 0.406], \
- std=[0.229, 0.224, 0.225])
2.定义一个函数preprocess_image(img
- def preprocess_image(img):
- img = torch.tensor(img).permute(2,0,1)
- img = normalize(img)
- return img.to(device).float()
- def decode(_y):
- _, preds = _y.max(-1)
- return preds
3.使用预处理的区域建议以及在上一步(步骤 7RCNNDataset )中获得的基本事实定义数据集():
- 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
- 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
4.创建训练和验证数据集和数据加载器:
- 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)
到目前为止,我们已经了解了准备数据。接下来,我们将学习定义和训练模型,该模型预测要对区域提议进行的类别和偏移,以适应图像中对象周围的紧密边界框。
现在我们已经准备好数据,在本节中,我们将学习如何构建一个模型,该模型可以预测区域提议的类别和与其对应的偏移量,以便在图像中的对象周围绘制一个紧密的边界框。我们采用的策略如下:
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)
2.定义RCNN网络模块:
- class RCNN(nn.Module):
- def __init__(self):
- super().__init__()
- 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 = nn.CrossEntropyLoss()
- self.sl1 = nn.L1Loss()
- def forward(self, input):
- feat = self.backbone(input)
- cls_score = self.cls_score(feat)
- bbox = self.bbox(feat)
- return cls_score, bbox
- 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
有了模型类,我们现在定义函数来训练一批数据并预测验证数据。
3.定义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()
4.定义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()
5.现在,让我们创建一个模型对象,获取损失标准,然后定义优化器和 epoch 数:
- rcnn = RCNN().to(device)
- criterion = rcnn.calc_loss
- optimizer = optim.SGD(rcnn.parameters(), lr=1e-3)
- n_epochs = 5
- log = Report(n_epochs)
6.我们现在在越来越多的时期训练模型:
- 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')
-
- # 绘制训练和验证指标
- log.plot_epochs('trn_loss,val_loss'.split(','))
训练和验证数据的整体损失图如下:
现在我们已经训练了一个模型,我们将在下一节中使用它来预测新图像。
在本节中,我们将利用迄今为止训练的模型来预测和绘制对象周围的边界框以及新图像上预测的边界框内的相应对象类别。我们采用的策略如下:
1.从新图像中提取区域建议。
2.调整每个作物的大小并标准化。
3.前馈处理后的作物以预测类别和偏移量。
4.执行非最大抑制以仅获取那些对包含对象具有最高置信度的框。
我们通过一个将图像作为输入和一个地面实况边界框的函数执行上述策略(这仅用于比较地面实况和预测边界框)。我们将从上一节中停止的地方继续编码:
1.定义在新图像上预测的test_predictions 函数:
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)
- 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
2.在新图像上执行上述函数:
- image, crops, bbs, labels, deltas, gtbbs, fpath = test_ds[7]
- test_predictions(fpath)
上述代码生成以下图像:
从上图可以看出,图像类别的预测准确,边界框预测也不错。请注意,为前面的图像生成预测大约需要 1.5 秒。
所有这些时间都用于生成区域提议、调整每个区域提议的大小、将它们传递给 VGG 主干,以及使用定义的模型生成预测。然而,大部分时间都花在通过 VGG 主干传递每个提案。在下一节中,我们将学习如何使用基于 Fast R-CNN 架构的模型来解决“将每个提案传递给 VGG”的问题。
R-CNN 的主要缺点之一是生成预测需要相当长的时间,因为为每个图像生成区域建议、调整区域的裁剪以及提取与每个裁剪对应的特征(区域建议)构成了瓶颈。
Fast R-CNN 通过将整个图像selectivesearch传递给预训练模型以提取特征,然后获取与原始图像的区域提议(从 获得)相对应的特征区域,从而解决了这个问题。在以下部分中,我们将了解 Fast R-CNN 的工作细节,然后再在我们的自定义数据集上进行训练。
让我们通过下图来了解 Fast R-CNN :
让我们通过以下步骤来理解上图:
1.将图像通过预训练模型以在展平层之前提取特征;让我们将输出称为特征图。
2.提取与图像对应的区域建议。
3.提取与区域提议相对应的特征图区域(注意,当图像通过 VGG16 架构时,由于执行了 5 次池化操作,图像在输出处缩小了 32。因此,如果存在带有边界框的区域(40,32,200,240)在原始图像中,对应于(5,4,25,30)的边界框的特征图将对应于完全相同的区域)。
4.通过 RoI(Region of Interest)池化层一次传递一个与 region proposal 对应的特征图,使 region proposal 的所有特征图都具有相似的形状。这是在 R-CNN 技术中执行的变形的替代品。
5.通过全连接层传递 RoI 池化层输出值。
6.训练模型以预测每个区域提案对应的类别和偏移量。
现在了解了 Fast R-CNN 的工作原理,在下一节中,我们将使用我们在 R-CNN 部分中使用的相同数据集来构建模型。
在本节中,我们将努力使用 Fast R-CNN 训练我们的自定义对象检测器。此外,为了保持简洁,我们在本节中仅提供附加或更改的代码(您应该运行所有代码,直到R-CNN 上一节的创建训练数据子节中的第2 步):
1.创建一个FRCNNDataset类,该类返回图像、标签、ground truth、区域建议以及与每个区域建议相对应的增量:
- 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(image/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部分中学到的非常相似,唯一的变化是我们返回了更多信息(rois和rixs)。
该rois矩阵包含有关哪个 RoI 属于批次中的哪个图像的信息。请注意,它input包含多个图像,而rois是单个框列表。我们不知道有多少 rois 属于第一个图像,有多少属于第二个图像,依此类推。这就是ridx图片的来源。它是一个索引列表。列表中的每个整数都将相应的边界框与适当的图像相关联;例如,如果ridx是[0,0,0,1,1,2,3,3,3],那么我们知道前三个边界框属于批次中的第一个图像,接下来的两个属于批次中的第二个图像。
2.创建训练和测试数据集:
- 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)
3.定义要在数据集上训练的模型:
from torchvision.ops import RoIPool
- 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])
self.roipool = RoIPool(7, spatial_scale=14/224)
- 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()
def forward(self, input, rois, ridx):
- res = input
- res = self.seq(res)
- 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
- 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
4.就像我们在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()
5.在越来越多的时期定义和训练模型:
- 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(','))
整体损失的变化如下:
6.定义一个函数来预测测试图像:
- 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))
- candidates = extract_candidates(img)
- candidates = [(x,y,x+w,y+h) for x,y,w,h in candidates]
- input = preprocess_image(img/255.)[None]
- rois = [[x/224,y/224,X/224,Y/224] for x,y,X,Y in \
- Candidates]
rixs = np.array([0]*len(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
- 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()
7.在测试图像上预测:
test_predictions(test_ds[29][-1])
前面的代码结果如下:
上述代码执行时间为 0.5 秒,明显优于 R-CNN。但是,实时使用还是很慢的。这主要是因为我们仍在使用两种不同的模型,一种用于生成区域建议,另一种用于预测类别和修正。在下一章中,我们将学习使用单一模型进行预测,以便在实时场景中快速推理。
在本章中,我们从学习如何为对象定位和检测过程创建训练数据集开始。接下来,我们了解了 SelectiveSearch,这是一种基于邻近像素相似度推荐区域的区域提议技术。接下来,我们学习了计算 IoU 度量,以了解图像中存在的对象周围的预测边界框的优劣。接下来,我们学习了执行非最大抑制以在图像中为每个对象获取一个边界框,然后再学习从头开始构建 R-CNN 和 Fast R-CNN 模型。此外,我们了解了 R-CNN 速度慢的原因,以及 Fast R-CNN 如何利用 RoI 池化并从特征图中获取区域建议来加快推理速度。最后,
在下一章中,我们将了解一些用于在更实时的基础上进行推理的现代目标检测技术。