十一、对象检测
目标检测是计算机视觉最重要的应用之一。 对象检测是同时定位和识别图像中存在的对象的任务。 为了使自动驾驶汽车安全地在街道上行驶,该算法必须检测到行人,道路,车辆,交通信号灯,标志和意外障碍物的存在。 在安全方面,入侵者的存在可以用来触发警报或通知适当的当局。
尽管很重要,但是对象检测一直是计算机视觉中的一个长期存在的问题。 已经提出了许多算法,但是通常很慢,并且精度和召回率很低。 与 AlexNet [1]在 ImageNet 大规模图像分类问题中所取得的成就类似,深度学习显着提高了对象检测领域。 最新的对象检测方法现在可以实时运行,并且具有更高的精度和召回率。
在本章中,我们重点介绍实时对象检测。 特别是,我们讨论了tf.keras
中单发检测(SSD)[2]的概念和实现。 与其他深度学习检测算法相比,SSD 可在现代 GPU 上实现实时检测速度,而表现不会显着下降。 SSD 还易于端到端训练。
总之,本章的目的是介绍:
- 对象检测的概念
- 多尺度目标检测的概念
- SSD 作为多尺度目标检测算法
tf.keras
中 SSD 的实现
我们将从介绍对象检测的概念开始。
1. 对象检测
在对象检测中,目标是在图像中定位和识别物体。“图 11.1.1”显示了目标汽水罐的目标物检测。 本地化意味着必须估计对象的边界框。 使用左上角像素坐标和右下角像素坐标是用于描述边界框的通用约定。 在“图 11.1.1”中,左上角像素具有坐标(x_min, y_min)
,而右下角像素的坐标为(x_max, y_max)
。像素坐标系的原点(0, 0)
位于整个图像的左上角像素。
在执行定位时,检测还必须识别对象。 识别是计算机视觉中的经典识别或分类任务。 至少,对象检测必须确定边界框是属于已知对象还是背景。 可以训练对象检测网络以仅检测一个特定对象,例如“图 11.1.1”中的汽水罐。 其他所有内容均视为背景,因此无需显示其边界框。 同一对象的多个实例,例如两个或多个汽水罐,也可以通过同一网络检测到,如图“图 11.1.2”所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c0QWKYMw-1681704403407)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_01.png)]
图 11.1.1 说明了对象检测是在图像中定位和识别对象的过程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yr25EewR-1681704403408)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_02.png)]
图 11.1.2 被训练为检测一个对象实例的同一网络可以检测到同一对象的多个实例。
如果场景中存在多个对象,例如在“图 11.1.3”中,则对象检测方法只能识别在其上训练的一个对象。 其他两个对象将被分类为背景,并且不会分配边界框。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LFN8USt6-1681704403408)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_03.png)]
图 11.1.3 如果仅在检测汽水罐方面训练了对象检测,它将忽略图像中的其他两个对象。
但是,如果重新训练了网络以检测三个对象:1)汽水罐,2)果汁罐和 3)水瓶会同时定位和识别,如图“图 11.1.4”所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0RMai6Ir-1681704403409)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_04.png)]
图 11.1.4 即使背景杂乱或照明发生变化,也可以重新训练对象检测网络以检测所有三个对象。
一个好的对象检测器必须在现实环境中具有鲁棒性。“图 11.1.4”显示了一个好的对象检测网络,即使背景杂乱甚至在弱光条件下,也可以定位和识别已知对象。 对象检测器必须具有鲁棒性的其他因素是物体变换(旋转和/或平移),表面反射,纹理变化和噪声。
总之,对象检测的目标是针对图像中每个可识别的对象同时预测以下内容:
y_cls
或单热向量形式的类别或类y_box = ((x_min, y_min), (x_max, y_max))
或像素坐标形式的边界框坐标
通过解释了对象检测的基本概念,我们可以开始讨论对象检测的某些特定机制。 我们将从介绍锚框开始。
2. 锚框
从上一节的讨论中,我们了解到,对象检测必须预测边界框区域以及其中的对象类别。 假设与此同时,我们的重点是边界框坐标估计。
网络如何预测坐标(x_min, y_min)
和(x_max, y_max)
? 网络可以做出与图像的左上角像素坐标和右下角像素坐标相对应的初始猜测,例如(0, 0)
和(w, h)
。w
是图像宽度,而h
是图像高度。 然后,网络通过对地面真实边界框坐标执行回归来迭代地校正估计。
由于可能的像素值存在较大差异,因此使用原始像素估计边界框坐标不是最佳方法。 SSD 代替原始像素,将地面真值边界框和预测边界框坐标之间的像素误差值最小化。 对于此示例,像素的误差值为(x_min, y_min)
和(x_max - w, y_max - h)
。 这些值称为offsets
。
为了帮助网络找出正确的边界框坐标,将图像划分为多个区域。 每个区域称为定位框。 然后,网络估计每个锚框的偏移。 这样得出的预测更接近于基本事实。
例如,如图“图 11.2.1”所示,将普通图像尺寸640 x 480
分为2 x 1
个区域,从而产生两个锚框。 与2 x 2
的大小不同,2 x 1
的划分创建了近似方形的锚框。 在第一个锚点框中,新的偏移量是(x_min, y_min)
和{x_max - w/2, y_max - h}
,它们比没有锚框的像素误差值更小。 第二个锚框的偏移量也较小。
在“图 11.2.2”中,图像被进一步分割。 这次,锚框为3 x 2
。第二个锚框偏移为{x_min - w/3, y_min}
和{x_max - 2w/3, y_max - h/2}
,这是迄今为止最小的。 但是,如果将图像进一步分为5 x 4
,则偏移量开始再次增加。 主要思想是,在创建各种尺寸的区域的过程中,将出现最接近地面真值边界框的最佳锚框大小。 使用多尺度锚框有效地检测不同大小的对象将巩固多尺度对象检测算法的概念。
找到一个最佳的锚框并不是零成本。 尤其是,有些外部锚框的偏移量比使用整个图像还要差。 在这种情况下,SSD 建议这些锚定框不应对整个优化过程有所帮助,而应予以抑制。 在以下各节中,将更详细地讨论排除非最佳锚框的算法。
到目前为止,我们已经有三套锚框。
第一个创建一个2 x 1
的锚框网格,每个锚框的尺寸为(w/2, h)
。
第二个创建一个3 x 2
的锚框网格,每个锚框的尺寸为(w/3, h/2)
。
第三个创建一个5 x 4
的锚框网格,每个锚框的尺寸为(w/5, h/4)
。
我们还需要多少套锚盒? 它取决于图像的尺寸和对象最小边框的尺寸。 对于此示例中使用的640 x 480
图像,其他锚点框为:
10 x 8
格的锚框,每个框的尺寸为(w/10, h/8)
20 x 15
格的锚框,每个锚框的尺寸为(w/20, h/15)
40 x 30
格的锚框,每个框的尺寸为(w/40, h/30)
对于具有40 x 30
网格的锚框的640 x 480
图像,最小的锚框覆盖输入图像的16 x 16
像素斑块,也称为接收域。 到目前为止,包围盒的总数为 1608。对于所有尺寸,最小的缩放因子可以总结为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oaJBrmYO-1681704403409)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_010.png)] (Equation 11.2.1)
锚框如何进一步改进? 如果我们允许锚框具有不同的纵横比,则可以减少偏移量。 每个调整大小的锚点框的质心与原始锚点框相同。 除宽高比 1 外,SSD [2]包括其他宽高比:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iYrgozfl-1681704403409)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_011.png)] (Equation 11.2.2)
对于每个纵横比a[i]
,对应的锚框尺寸为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9YigF3Dn-1681704403409)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_013.png)] (Equation 11.2.3)
(s[xj], s[yj])
是“公式 11.2.1”中的第j
个比例因子。
使用每个锚框五个不同的长宽比,锚框的总数将增加到1,608 x 5 = 8,040
。“图 11.2.3”显示了(s[x4], s[y4]) = (1/3, 1/2)
和a[i ∈ {0, 1, 3}] = 1, 2, 1/2
情况下的锚框。
请注意,为了达到一定的纵横比,我们不会使锚框变形。 而是调整锚框的宽度和高度。
对于a[0] = 1
,SSD 建议使用其他尺寸的锚框:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CrAJxyOz-1681704403410)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_018.png)] (Equation 11.2.4)
现在每个区域有六个锚定框。 有五个是由于五个纵横比,另外还有一个纵横比为 1。新的锚框总数增加到 9,648。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y6np5yWp-1681704403410)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_05.png)]
图 11.2.1 将图像划分为多个区域(也称为锚框),使网络可以进行更接近地面真实情况的预测。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yHAv3ROI-1681704403410)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_06.png)]
图 11.2.2 使用较小的锚框可以进一步减少偏移。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XNYjynKc-1681704403410)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_07.png)]
图 11.2.3 具有比例因子(s[x4], s[y4]) = (1/3, 1/2)
和纵横比a[i ∈ {0, 1, 3}] = 1, 2, 1/2
的一个区域的锚框。
下面的“列表 11.2.1”显示了锚框生成函数anchor_boxes()
。 给定输入的图像形状(image_shape
),纵横比(aspect_ratios
)和缩放因子(sizes
),将计算不同的锚框大小并将其存储在名为width_height
的列表中。 从给定的特征映射形状(feature_shape
或(h_fmap, w_fmap)
和width_height
, 生成具有尺寸(h_fmap, w_fmap, n_boxes, 4)
。n_boxes
或每个特征映射点的锚点框数是基于纵横比和等于 1 的纵横比的一个附加大小计算的。
“列表 11.2.1”:锚框生成函数的layer_utils.py
函数:
def anchor_boxes(feature_shape, image_shape, index=0, n_layers=4, aspect_ratios=(1, 2, 0.5)): """ Compute the anchor boxes for a given feature map. Anchor boxes are in minmax format
Arguments: feature_shape (list): Feature map shape image_shape (list): Image size shape index (int): Indicates which of ssd head layers are we referring to n_layers (int): Number of ssd head layers
Returns: boxes (tensor): Anchor boxes per feature map """
# anchor box sizes given an index of layer in ssd head sizes = anchor_sizes(n_layers)[index] # number of anchor boxes per feature map pt n_boxes = len(aspect_ratios) + 1 # ignore number of channels (last) image_height, image_width, _ = image_shape # ignore number of feature maps (last) feature_height, feature_width, _ = feature_shape
# normalized width and height # sizes[0] is scale size, sizes[1] is sqrt(scale*(scale+1)) norm_height = image_height * sizes[0] norm_width = image_width * sizes[0]
# list of anchor boxes (width, height) width_height = [] # anchor box by aspect ratio on resized image dims # Equation 11.2.3 for ar in aspect_ratios: box_width = norm_width * np.sqrt(ar) box_height = norm_height / np.sqrt(ar) width_height.append((box_width, box_height)) # multiply anchor box dim by size[1] for aspect_ratio = 1 # Equation 11.2.4 box_width = image_width * sizes[1] box_height = image_height * sizes[1] width_height.append((box_width, box_height))
# now an array of (width, height) width_height = np.array(width_height)
# dimensions of each receptive field in pixels grid_width = image_width / feature_width grid_height = image_height / feature_height
# compute center of receptive field per feature pt # (cx, cy) format # starting at midpoint of 1st receptive field start = grid_width * 0.5 # ending at midpoint of last receptive field end = (feature_width - 0.5) * grid_width cx = np.linspace(start, end, feature_width)
start = grid_height * 0.5 end = (feature_height - 0.5) * grid_height cy = np.linspace(start, end, feature_height)
# grid of box centers cx_grid, cy_grid = np.meshgrid(cx, cy)
# for np.tile() cx_grid = np.expand_dims(cx_grid, -1) cy_grid = np.expand_dims(cy_grid, -1)
# tensor = (feature_map_height, feature_map_width, n_boxes, 4) # aligned with image tensor (height, width, channels) # last dimension = (cx, cy, w, h) boxes = np.zeros((feature_height, feature_width, n_boxes, 4))
# (cx, cy) boxes[..., 0] = np.tile(cx_grid, (1, 1, n_boxes)) boxes[..., 1] = np.tile(cy_grid, (1, 1, n_boxes))
# (w, h) boxes[..., 2] = width_height[:, 0] boxes[..., 3] = width_height[:, 1]
# convert (cx, cy, w, h) to (xmin, xmax, ymin, ymax) # prepend one dimension to boxes # to account for the batch size = 1 boxes = centroid2minmax(boxes) boxes = np.expand_dims(boxes, axis=0) return boxes
def centroid2minmax(boxes): """Centroid to minmax format (cx, cy, w, h) to (xmin, xmax, ymin, ymax)
Arguments: boxes (tensor): Batch of boxes in centroid format
Returns: minmax (tensor): Batch of boxes in minmax format """ minmax= np.copy(boxes).astype(np.float) minmax[..., 0] = boxes[..., 0] - (0.5 * boxes[..., 2]) minmax[..., 1] = boxes[..., 0] + (0.5 * boxes[..., 2]) minmax[..., 2] = boxes[..., 1] - (0.5 * boxes[..., 3]) minmax[..., 3] = boxes[..., 1] + (0.5 * boxes[..., 3]) return minmax
我们已经介绍了锚框如何协助对象检测以及如何生成它们。 在下一节中,我们将介绍一种特殊的锚点框:真实情况锚点框。 给定图像中的对象,必须将其分配给多个锚点框之一。 这就是,称为真实情况锚定框。
3. 真实情况锚框
从“图 11.2.3”看来,给定一个对象边界框,有许多可以分配给对象的真实情况锚定框。 实际上,仅出于“图 11.2.3”中的说明,已经有 3 个锚定框。 如果考虑每个区域的所有锚框,则仅针对(s[x4], s[y4]) = (1/3, 1/2)
就有6 x 6 = 36
个地面真实框。 使用所有 9,648 个锚点框显然过多。 所有锚定框中只有一个应与地面真值边界框相关联。 所有其他锚点框都是背景锚点框。 选择哪个对象应被视为图像中对象的真实情况锚定框的标准是什么?
选择锚框的基础称为交并比(IoU)。 IoU 也称为 Jaccard 指数。 在“图 11.3.1”中说明了 IoU。 给定 2 个区域,对象边界框B[0]
和锚定框A[1]
,IoU 等于重叠除以合并区域的面积:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VZIjlJRy-1681704403411)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_022.png)] (Equation 11.3.1)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ClluQ9bE-1681704403411)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_08.png)]
图 11.3.1 IoU 等于(左)候选锚点框A[1]
与(右)对象边界框B[0]
之间的相交面积除以并集面积。
我们删除了该等式的下标。 对于给定的对象边界框B[i]
,对于所有锚点框A[j]
,地面真值锚点框A[j(gt)]
是具有最大 IoU 的一个:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iRYHLbjy-1681704403411)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_023.png)] (Equation 11.3.2)
请注意,对于每个对象,只有一个基于“公式 11.3.2”的地面真值锚定框。 此外,必须在所有比例因子和尺寸(长宽比和附加尺寸)中对所有锚框进行最大化。 在“图 11.3.1”中,在 9,648 个锚框中仅显示了一个比例因子大小。
为了说明“公式 11.3.2”,假设考虑了“图 11.3.1”中纵横比为 1 的锚框。 对于每个锚框,估计的 IoU 均显示在“表 11.3.1”中。 由于边界框B[0]
的最大 IoU 为 0.32,因此带有锚框A[1]
,A[1]
被分配为地面真值边界框B[0]
。A[1]
也被称为正锚框。
正锚定框的类别和偏移量是相对于其地面真值边界框确定的。 正锚定框的类别与其地面真值边界框相同。 同时,可以将正锚框偏移量计算为等于地面真实边界框坐标减去其自身的边界框坐标。
其余锚框发生了什么,A[0]
,A[2]
,A[3]
,A[4]
,和A[5]
? 我们可以通过找到他们的 IoU 大于某个阈值的边界框来给他们第二次机会。
例如,如果阈值为 0.5,则没有可分配给它们的地面真理边界框。 如果阈值降低到 0.25,则A[4]
也分配有地面真值边界框B[0]
,因为其 IoU 为 0.30 。 将A[4]
添加到肯定锚框列表中。 在这本书中,A[4]
被称为额外的正面锚盒。 没有地面边界框的其余锚框称为负锚框。
在以下有关损失函数的部分中,负锚框不构成偏移损失函数。
B[0] |
|
A[0] |
0 |
A[1] |
0.32 |
A[2] |
0 |
A[3] |
0 |
A[4] |
0.30 |
A[5] |
0 |
“表 11.3.1”每个锚框A[j ∈ 0 .. 5]
的 IoU,带有对象边界框B[0]
,如“图 11.3.1”所示。
如果加载了另一个带有 2 个要检测的对象的图像,我们将寻找 2 个正 IoU,最大 IoU,并带有边界框B[0]
和B[1]
。 然后,我们使用边界框B[0]
和B[1]
寻找满足最小 IoU 准则的额外正锚框。
为了简化讨论,我们只考虑每个区域一个锚框。 实际上,应该考虑代表不同缩放比例,大小和纵横比的所有锚框。 在下一节中,我们讨论如何制定损失函数,这些损失函数将通过 SSD 网络进行优化。
“列表 11.3.1”显示了get_gt_data()
的实现,该实现计算锚定框的真实情况标签。
“列表 11.3.1”:layer_utils.py
def get_gt_data(iou, n_classes=4, anchors=None, labels=None, normalize=False, threshold=0.6): """Retrieve ground truth class, bbox offset, and mask Arguments: iou (tensor): IoU of each bounding box wrt each anchor box n_classes (int): Number of object classes anchors (tensor): Anchor boxes per feature layer labels (list): Ground truth labels normalize (bool): If normalization should be applied threshold (float): If less than 1.0, anchor boxes>threshold are also part of positive anchor boxes
Returns: gt_class, gt_offset, gt_mask (tensor): Ground truth classes, offsets, and masks """ # each maxiou_per_get is index of anchor w/ max iou # for the given ground truth bounding box maxiou_per_gt = np.argmax(iou, axis=0)
# get extra anchor boxes based on IoU if threshold < 1.0: iou_gt_thresh = np.argwhere(iou>threshold) if iou_gt_thresh.size > 0: extra_anchors = iou_gt_thresh[:,0] extra_classes = iou_gt_thresh[:,1] extra_labels = labels[extra_classes] indexes = [maxiou_per_gt, extra_anchors] maxiou_per_gt = np.concatenate(indexes, axis=0) labels = np.concatenate([labels, extra_labels], axis=0)
# mask generation gt_mask = np.zeros((iou.shape[0], 4)) # only indexes maxiou_per_gt are valid bounding boxes gt_mask[maxiou_per_gt] = 1.0
# class generation gt_class = np.zeros((iou.shape[0], n_classes)) # by default all are background (index 0) gt_class[:, 0] = 1 # but those that belong to maxiou_per_gt are not gt_class[maxiou_per_gt, 0] = 0 # we have to find those column indexes (classes) maxiou_col = np.reshape(maxiou_per_gt, (maxiou_per_gt.shape[0], 1)) label_col = np.reshape(labels[:,4], (labels.shape[0], 1)).astype(int) row_col = np.append(maxiou_col, label_col, axis=1) # the label of object in maxio_per_gt gt_class[row_col[:,0], row_col[:,1]] = 1.0
# offsets generation gt_offset = np.zeros((iou.shape[0], 4))
#(cx, cy, w, h) format if normalize: anchors = minmax2centroid(anchors) labels = minmax2centroid(labels) # bbox = bounding box # ((bbox xcenter - anchor box xcenter)/anchor box width)/.1 # ((bbox ycenter - anchor box ycenter)/anchor box height)/.1 # Equation 11.4.8 Chapter 11 offsets1 = labels[:, 0:2] - anchors[maxiou_per_gt, 0:2] offsets1 /= anchors[maxiou_per_gt, 2:4] offsets1 /= 0.1
# log(bbox width / anchor box width) / 0.2 # log(bbox height / anchor box height) / 0.2 # Equation 11.4.8 Chapter 11 offsets2 = np.log(labels[:, 2:4]/anchors[maxiou_per_gt, 2:4]) offsets2 /= 0.2
offsets = np.concatenate([offsets1, offsets2], axis=-1)
# (xmin, xmax, ymin, ymax) format else: offsets = labels[:, 0:4] - anchors[maxiou_per_gt]
gt_offset[maxiou_per_gt] = offsets
return gt_class, gt_offset, gt_mask
def minmax2centroid(boxes): """Minmax to centroid format (xmin, xmax, ymin, ymax) to (cx, cy, w, h)
Arguments: boxes (tensor): Batch of boxes in minmax format
Returns: centroid (tensor): Batch of boxes in centroid format """ centroid = np.copy(boxes).astype(np.float) centroid[..., 0] = 0.5 * (boxes[..., 1] - boxes[..., 0]) centroid[..., 0] += boxes[..., 0] centroid[..., 1] = 0.5 * (boxes[..., 3] - boxes[..., 2]) centroid[..., 1] += boxes[..., 2] centroid[..., 2] = boxes[..., 1] - boxes[..., 0] centroid[..., 3] = boxes[..., 3] - boxes[..., 2] return centroid
maxiou_per_gt = np.argmax(iou, axis=0)
实现了“公式 11.3.2”。 额外的阳性锚框是基于由iou_gt_thresh = np.argwhere(iou>threshold)
实现的用户定义的阈值确定的。
仅当阈值小于 1.0 时,才会查找额外的正锚框。 所有带有地面真值边界框的锚框(即组合的正锚框和额外的正锚框)的索引成为真实情况掩码的基础:
gt_mask[maxiou_per_gt] = 1.0
。
所有其他锚定框(负锚定框)的掩码为 0.0,并且不影响偏移损失函数的优化。
每个锚定框的类别gt_class
被分配为其地面实况边界框的类别。 最初,为所有锚框分配背景类:
# class generation gt_class = np.zeros((iou.shape[0], n_classes)) # by default all are background (index 0) gt_class[:, 0] = 1
然后,将每个正面锚点框的类分配给其非背景对象类:
# but those that belong to maxiou_per_gt are not gt_class[maxiou_per_gt, 0] = 0 # we have to find those column indexes (classes) maxiou_col = np.reshape(maxiou_per_gt, (maxiou_per_gt.shape[0], 1)) label_col = np.reshape(labels[:,4], (labels.shape[0], 1)).astype(int) row_col = np.append(maxiou_col, label_col, axis=1) # the label of object in maxio_per_gt gt_class[row_col[:,0], row_col[:,1]] = 1.0
row_col[:,0]
是正锚框的索引,而row_col[:,1]
是它们的非背景对象类的索引。 请注意,gt_class
是单热点向量的数组。 这些值都为零,除了锚点框对象的索引处。 索引 0 是背景,索引 1 是第一个非背景对象,依此类推。 最后一个非背景对象的索引等于n_classes-1
。
例如,如果锚点框 0 是负锚点框,并且有 4 个对象类别(包括背景),则:
gt_class[0] = [1.0, 0.0, 0.0, 0.0]
如果锚定框 1 是正锚定框,并且其地面真值边界框包含带有标签 2 的汽水罐,则:
gt_class[1] = [0.0, 0.0, 1.0, 0.0]
最后,偏移量只是地面真实边界框坐标减去锚框坐标:
# (xmin, xmax, ymin, ymax) format else: offsets = labels[:, 0:4] - anchors[maxiou_per_gt]
注意,我们仅计算正锚框的偏移量。
如果选择了该选项,则可以将偏移量标准化。 下一部分将讨论偏移量归一化。 我们将看到:
#(cx, cy, w, h) format if normalize:
anchors = minmax2centroid(anchors) labels = minmax2centroid(labels) # bbox = bounding box # ((bbox xcenter - anchor box xcenter)/anchor box width)/.1 # ((bbox ycenter - anchor box ycenter)/anchor box height)/.1 # Equation 11.4.8 offsets1 = labels[:, 0:2] - anchors[maxiou_per_gt, 0:2] offsets1 /= anchors[maxiou_per_gt, 2:4] offsets1 /= 0.1
# log(bbox width / anchor box width) / 0.2 # log(bbox height / anchor box height) / 0.2 # Equation 11.4.8 offsets2 = np.log(labels[:, 2:4]/anchors[maxiou_per_gt, 2:4]) offsets2 /= 0.2
offsets = np.concatenate([offsets1, offsets2], axis=-1)
只是“公式 11.4.8”的实现,下一节将进行讨论,为方便起见,在此处显示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KeaT0TTn-1681704403411)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_025.png)] (Equation 11.4.8)
现在我们已经了解了地面真锚框的作用,我们将继续研究对象检测中的另一个关键组件:损失函数。
4. 损失函数
在 SSD 中,有数千个锚定框。 如本章前面所述,对象检测的目的是预测每个锚框的类别和偏移量。 我们可以对每个预测使用以下损失函数:
L_cls
-y_cls
的分类交叉熵损失L_off
- L1 或 L2,用于y_cls
。 请注意,只有正锚框有助于L_off
L1,也称为平均绝对误差(MAE)损失,而 L2 也称为均方误差(MSE)损失。
总的损失函数为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G8C3QxEy-1681704403412)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_081.png)] (Equation 11.4.1)
对于每个定位框,网络都会预测以下内容:
y_cls
或单热向量形式的类别或类y_off = ((x_omin, y_omin), (x_omax, y_omax))
或相对于锚框的像素坐标形式的偏移。
为了方便计算,可以将偏移量更好地表示为以下形式:
y_off = ((x_omin, y_omin), (x_omax, y_omax))
(Equation 11.4.2)
SSD 是一种监督对象检测算法。 可以使用以下基本真值:
y_label
或要检测的每个对象的类标签y_gt = (x_gmin, x_gmax, y_gmin, y_gmax)
或地面真实偏差,其计算公式如下:
y_gt = (x_bmin – x_amin, x_bmax – x_amax, y_bmin – y_amin, y_bmax – y_amax)
(Equation 11.4.3)
换句话说,将地面真实偏移量计算为对象包围盒相对于锚定框的地面真实偏移量。 为了清楚起见,y_box
下标中的细微调整。 如上一节所述,基本真值是通过get_gt_data()
函数计算的。
但是,SSD 不建议直接从预测原始像素误差值y_off
。 而是使用归一化的偏移值。 地面真值边界框和锚点框坐标首先以质心尺寸格式表示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IzEvl1fo-1681704403412)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_026.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ITSI8PBT-1681704403412)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_027.png)]
(Equation 11.4.4)
哪里:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q45JPu0F-1681704403412)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_028.png)] (Equation 11.4.5)
是边界框中心的坐标,并且:
(w[b], h[b]) = (x_max – x_min, y_max - y_min)
(Equation 11.4.6)
分别对应于宽度和高度。 锚框遵循相同的约定。 归一化的真实情况偏移量表示为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kSiOvctw-1681704403413)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_029.png)] (Equation 11.4.7)
通常,y_gt
的元素值很小,||y_gt|| << 1.0
。 较小的梯度会使网络训练更加难以收敛。
为了缓解该问题,将每个元素除以其估计的标准差。 由此产生的基本事实抵消了:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6nI54Gmr-1681704403413)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_025.png)] (Equation 11.4.8)
推荐值为:σ[x] = σ[y] = 0.1
和σ[w] = σ[h] = 0.2
。 换句话说,沿着x
和y
轴的像素误差的预期范围是± 10%
,而对于宽度和高度,则是`± 20%。 这些值纯粹是任意的。
“列表 11.4.1”:loss.py
L1 和平滑 L1 损失函数
from tensorflow.keras.losses import Huber def mask_offset(y_true, y_pred): """Pre-process ground truth and prediction data""" # 1st 4 are offsets offset = y_true[..., 0:4] # last 4 are mask mask = y_true[..., 4:8] # pred is actually duplicated for alignment # either we get the 1st or last 4 offset pred # and apply the mask pred = y_pred[..., 0:4] offset *= mask pred *= mask return offset, pred def l1_loss(y_true, y_pred): """MAE or L1 loss """ offset, pred = mask_offset(y_true, y_pred) # we can use L1 return K.mean(K.abs(pred - offset), axis=-1) def smooth_l1_loss(y_true, y_pred): """Smooth L1 loss using tensorflow Huber loss """ offset, pred = mask_offset(y_true, y_pred) # Huber loss as approx of smooth L1 return Huber()(offset, pred)
此外,代替y_cls
的 L1 损失,SSD 受 Fast-RCNN [3]启发,使用平滑 L1:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6Wqb047p-1681704403413)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_035.png)] (Equation 11.4.9)
其中u
代表地面真实情况与预测之间的误差中的每个元素:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pzYHveII-1681704403413)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_037.png)] (Equation 11.4.10)
与 L1 相比,平滑 L1 更健壮,并且对异常值的敏感性较低。 在 SSD 中,σ = 1
。 作为σ -> ∞
,平滑 L1 接近 L1。 L1 和平滑 L1 损失函数都在“列表 11.4.1”中显示。 mask_offset()
方法可确保仅在具有地面真实边界框的预测上计算偏移量。 平滑的 L1 函数与σ = 1
[8]时的 Huber 损失相同。
作为对损失函数的进一步改进,RetinaNet [3]建议将 CEy_cls
的分类交叉熵函数替换为焦点损失 FL:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7P4PYkjh-1681704403413)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_041.png)] (Equation 11.4.11)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3G2QzOqo-1681704403414)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/adv-dl-tf2-keras/img/B14853_11_042.png)] (Equation 11.4.12)
区别在于额外因素α(1 - p[i])^γ
。 在 RetinaNet 中,当γ = 2
和α = 0.25
时,对象检测效果最好。 焦点损失在“列表 11.4.2”中实现。
“列表 11.4.2”:loss.py
焦点损失
def focal_loss_categorical(y_true, y_pred): """Categorical cross-entropy focal loss""" gamma = 2.0 alpha = 0.25
# scale to ensure sum of prob is 1.0 y_pred /= K.sum(y_pred, axis=-1, keepdims=True)
# clip the prediction value to prevent NaN and Inf epsilon = K.epsilon() y_pred = K.clip(y_pred, epsilon, 1\. - epsilon) # calculate cross entropy cross_entropy = -y_true * K.log(y_pred)
# calculate focal loss weight = alpha * K.pow(1 - y_pred, gamma) cross_entropy *= weight
return K.sum(cross_entropy, axis=-1)
聚焦损失的动机是,如果我们检查图像,则大多数锚框应分类为背景或负锚框。 只有很少的正锚框是代表目标对象的良好候选对象。 负熵损失是造成交叉熵损失的主要因素。 因此,负锚框的贡献使优化过程中正锚框的贡献无法实现。 这种现象也称为类不平衡,其中一个或几个类占主导地位。 有关其他详细信息,Lin 等。 文献[4]讨论了对象检测中的类不平衡问题。
有了焦点损失,我们在优化过程的早期就确信负锚框属于背景。 因此,由于p[i] -> 1.0
,项(1 - p[i])^γ
减少了负锚框的贡献。 对于正锚框,其贡献仍然很大,因为p[i]
远非 1.0。
既然我们已经讨论了锚定框,地面真值锚定框和损失函数的概念,我们现在准备介绍实现多尺度目标检测算法的 SSD 模型架构。
TensorFlow 2 和 Keras 高级深度学习:11~13(2)https://developer.aliyun.com/article/1426964