目录
目标检测
边缘框
目标检测数据集
总结
代码实现
定义在两种表示之间进行转换的函数
定义图像中狗和猫的边界框
将边框在图中画出
锚框 Anchor Box
IoU——交并比
赋予锚框标号
使用非极大值抑制(NMS)输出
总结
代码实现
锚框
IoU——交并比
标记类别和偏移量
目标检测
也算是计算机视觉里面最广泛的应用!其实我们常见的图片分类,在应用里面没有那么常见。
我们认为在一个图片里面,存在一个主体。而目标检测的目的就是——找出这个主体(我们感兴趣的东西)
我们发现目标检测其实做的比图片分类要多,不仅要进行多个物体的识别,还需要标注出位置
边缘框
用来表示物体的位置,一个边缘框可以通过4个数字定义:
- (左上x,左上y,右下x,右下y)
- (左上x,左上y,宽,高)
注意这里x轴和y轴的定义和我们平时的不太一样,y轴的0点在上方!
目标检测数据集
一般目标检测的数据集通常要比分类任务的小很多,因为标注的成本很高
通常结构是这样:
- 每一行表示一个物体
- 图片文件名、物体类别、边缘框
如果一个图片文件里面有多个物体的话,这个文件名可能会出现多次
e.g.一个图片文件里面有五个物体,这个文件可能就会出现五次
所以目标检测数据集每一行都有六个值(一个文件名字,一个物品类别,四个边缘框数字)
总结
- 物体检测识别图片里的多个物体的类别和位置
- 位置通常用边缘框表示
代码实现
去原来的文件地方取一张猫和狗的照片
%matplotlib inline
import torch
from d2l import torch as d2l
d2l.set_figsize()
img = d2l.plt.imread('../img/catdog.jpg')
d2l.plt.imshow(img);
定义在两种表示之间进行转换的函数
box_corner_to_center
从两角表示法转换为中心宽度表示法,而box_center_to_corner
反之亦然。 输入参数boxes
可以是长度为4的张量,也可以是形状为(n,4)的二维张量,其中n是边界框的数量。
def box_corner_to_center(boxes):
"""从(左上,右下)转换到(中间,宽度,高度)"""
x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
cx = (x1 + x2) / 2 # 中间就是左上右下的x轴取平均
cy = (y1 + y2) / 2
w = x2 - x1 # 一减就可以
h = y2 - y1
boxes = torch.stack((cx, cy, w, h), axis=-1)
return boxes
def box_center_to_corner(boxes):
"""从(中间,宽度,高度)转换到(左上,右下)"""
cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
x1 = cx - 0.5 * w
y1 = cy - 0.5 * h
x2 = cx + 0.5 * w
y2 = cy + 0.5 * h
boxes = torch.stack((x1, y1, x2, y2), axis=-1)
return boxes
定义图像中狗和猫的边界框
图像中坐标的原点是图像的左上角,向右的方向为x轴的正方向,向下的方向为y轴的正方向。
# bbox是边界框的英文缩写
dog_bbox, cat_bbox = [60.0, 45.0, 378.0, 516.0], [400.0, 112.0, 655.0, 493.0]
# 通过转换两次来验证边界框转换函数的正确性
boxes = torch.tensor((dog_bbox, cat_bbox))
box_center_to_corner(box_corner_to_center(boxes)) == boxes
# 输出
tensor([[True, True, True, True],
[True, True, True, True]])
将边框在图中画出
我们可以将边界框在图中画出,以检查其是否准确。 画之前,我们定义一个辅助函数bbox_to_rect
。 它将边界框表示成matplotlib
的边界框格式
def bbox_to_rect(bbox, color):
# 将边界框(左上x,左上y,右下x,右下y)格式转换成matplotlib格式:
# ((左上x,左上y),宽,高)
return d2l.plt.Rectangle(
xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1],
fill=False, edgecolor=color, linewidth=2)
fig = d2l.plt.imshow(img)
fig.axes.add_patch(bbox_to_rect(dog_bbox, 'blue'))
fig.axes.add_patch(bbox_to_rect(cat_bbox, 'red'));
锚框 Anchor Box
目标检测算法通常会在输入图像中采样大量的区域,然后判断这些区域中是否包含我们感兴趣的目标,并调整区域边界从而更准确地预测目标的真实边界框(ground-truth bounding box)。
不同的模型使用的区域采样方法可能不同。 这里我们介绍其中的一种方法:以每个像素为中心,生成多个缩放比和宽高比(aspect ratio)不同的边界框。 这些边界框被称为锚框(anchor box)。步骤如下:
- 提出多个被称为锚框的区域(边缘框)
- 预测每个锚框里是否含有关注的物体
- 如果是,预测从这个锚框到真实边缘框的偏移
IoU——交并比
用来比较两个框之间的相似度
- 0表示无重叠,1表示重合
我们刚刚提到某个锚框“较好地”覆盖了图像中的狗。 如果已知目标的真实边界框,那么这里的“好”该如何如何量化呢? 直观地说,可以衡量锚框和真实边界框之间的相似性。 杰卡德系数(Jaccard)可以衡量两组之间的相似性。 给定集合A和B,他们的杰卡德系数是他们交集的大小除以他们并集的大小:
事实上,我们可以将任何边界框的像素区域视为一组像素。通过这种方式,我们可以通过其像素集的杰卡德系数来测量两个边界框的相似性。 对于两个边界框,它们的杰卡德系数通常称为交并比(intersection over union,IoU),即两个边界框相交面积与相并面积之比,如下图所示。 交并比的取值范围在0和1之间:0表示两个边界框无重合像素,1表示两个边界框完全重合。
赋予锚框标号
每次图片读进来都要做一次的操作,所以目标检测可能不是一个batch一个batch读,而是一张一张照片读的
这个方法只是赋予标号的其中一个算法!还有很多种不一样的方法。
在训练集中,我们将每个锚框视为一个训练样本。 为了训练目标检测模型,我们需要每个锚框的类别(class)和偏移量(offset)标签,其中前者是与锚框相关的对象的类别,后者是真实边界框相对于锚框的偏移量。 在预测时,我们为每个图像生成多个锚框,预测所有锚框的类别和偏移量,根据预测的偏移量调整它们的位置以获得预测的边界框,最后只输出符合特定条件的预测边界框。
目标检测训练集带有真实边界框的位置及其包围物体类别的标签。 要标记任何生成的锚框,我们可以参考分配到的最接近此锚框的真实边界框的位置和类别标签。
- 每个锚框,要么标注成背景,要么关联上一个真实边缘框
- 可能会生成大量的锚框,但我们数据集标号也不多,会导致绝大部分锚框都是背景。这样会导致大量的负类样本
下面用一个具体的例子来说明上述算法。
- 如 下图(左)所示,假设矩阵X中的最大值为x23,我们将真实边界框B3分配给锚框A2。
- 然后,我们丢弃矩阵第2行和第3列中的所有元素,在剩余元素(阴影区域)中找到最大的x71,然后将真实边界框B1分配给锚框A7。
- 接下来,如 下图(中)所示,丢弃矩阵第7行和第1列中的所有元素,在剩余元素(阴影区域)中找到最大的x54,然后将真实边界框B4分配给锚框A5。
- 最后,如 下图(右)所示,丢弃矩阵第5行和第4列中的所有元素,在剩余元素(阴影区域)中找到最大的x92,然后将真实边界框B2分配给锚框A9。
- 之后,我们只需要遍历剩余的锚框A1,A3,A4,A6,A8,然后根据阈值确定是否为它们分配真实边界框。
这里的每个值都是IoU值
使用非极大值抑制(NMS)输出
一个 去除很多相似锚框 的办法
NMA可以合并相似的预测,通过以下步骤:
- 选中非背景类的最大预测值
- 去掉所有其它和它IoU值大于 的预测
- 重复上述过程直到所有预测要么被选中,要么被去掉
重复上述步骤,可以得到一个比较干净的输出。
总结
-
我们以图像的每个像素为中心生成不同形状的锚框。
-
交并比(IoU)也被称为杰卡德系数,用于衡量两个边界框的相似性。它是相交面积与相并面积的比率。
-
在训练集中,我们需要给每个锚框两种类型的标签。一个是与锚框中目标检测的类别,另一个是锚框真实相对于边界框的偏移量。
-
预测期间可以使用非极大值抑制(NMS)来移除类似的预测边界框,从而简化输出。
代码实现
锚框
%matplotlib inline
import torch
from d2l import torch as d2l
torch.set_printoptions(2) # 精简输出精度
假设输入图像的高度为h,宽度为w。 我们以图像的每个像素为中心生成不同形状的锚框:缩放比为s∈(0,1],宽高比为r>0。 那么锚框的宽度和高度分别是hsr和hs/r。 请注意,当中心位置给定时,已知宽和高的锚框是确定的。
要生成多个不同形状的锚框,让我们设置许多缩放比(scale)取值s1,…,sn和许多宽高比(aspect ratio)取值r1,…,rm。 当使用这些比例和长宽比的所有组合以每个像素为中心时,输入图像将总共有whnm个锚框。 尽管这些锚框可能会覆盖所有真实边界框,但计算复杂性很容易过高。 在实践中,我们只考虑包含s1或r1的组合:
(13.4.1)(s1,r1),(s1,r2),…,(s1,rm),(s2,r1),(s3,r1),…,(sn,r1).
也就是说,以同一像素为中心的锚框的数量是n+m−1。 对于整个输入图像,将共生成wh(n+m−1)个锚框。
上述生成锚框的方法在下面的multibox_prior
函数中实现。 我们指定输入图像、尺寸列表和宽高比列表,然后此函数将返回所有的锚框。
def multibox_prior(data, sizes, ratios):
"""生成以每个像素为中心具有不同形状的锚框
size:缩放比 ratios:高宽比"""
in_height, in_width = data.shape[-2:]
device, num_sizes, num_ratios = data.device, len(sizes), len(ratios)
boxes_per_pixel = (num_sizes + num_ratios - 1)
size_tensor = torch.tensor(sizes, device=device)
ratio_tensor = torch.tensor(ratios, device=device)
# 为了将锚点移动到像素的中心,需要设置偏移量。
# 因为一个像素的高为1且宽为1,我们选择偏移我们的中心0.5
offset_h, offset_w = 0.5, 0.5
steps_h = 1.0 / in_height # 在y轴上缩放步长
steps_w = 1.0 / in_width # 在x轴上缩放步长
# 生成锚框的所有中心点
center_h = (torch.arange(in_height, device=device) + offset_h) * steps_h
center_w = (torch.arange(in_width, device=device) + offset_w) * steps_w
shift_y, shift_x = torch.meshgrid(center_h, center_w, indexing='ij')
shift_y, shift_x = shift_y.reshape(-1), shift_x.reshape(-1)
# 生成“boxes_per_pixel”个高和宽,
# 之后用于创建锚框的四角坐标(xmin,xmax,ymin,ymax)
w = torch.cat((size_tensor * torch.sqrt(ratio_tensor[0]),
sizes[0] * torch.sqrt(ratio_tensor[1:])))\
* in_height / in_width # 处理矩形输入
h = torch.cat((size_tensor / torch.sqrt(ratio_tensor[0]),
sizes[0] / torch.sqrt(ratio_tensor[1:])))
# 除以2来获得半高和半宽
anchor_manipulations = torch.stack((-w, -h, w, h)).T.repeat(
in_height * in_width, 1) / 2
# 每个中心点都将有“boxes_per_pixel”个锚框,
# 所以生成含所有锚框中心的网格,重复了“boxes_per_pixel”次
out_grid = torch.stack([shift_x, shift_y, shift_x, shift_y],
dim=1).repeat_interleave(boxes_per_pixel, dim=0)
output = out_grid + anchor_manipulations
return output.unsqueeze(0)
返回的锚框变量Y的形状是(批量大小,锚框的数量,4)
img = d2l.plt.imread('../img/catdog.jpg')
h, w = img.shape[:2]
print(h, w)
X = torch.rand(size=(1, 3, h, w))
Y = multibox_prior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5])
Y.shape
# 输出
561 728
torch.Size([1, 2042040, 4])
将锚框变量Y
的形状更改为(图像高度,图像宽度,以同一像素为中心的锚框的数量,4)后,我们可以获得以指定像素的位置为中心的所有锚框。 在接下来的内容中,我们访问以(250,250)为中心的第一个锚框。 它有四个元素:锚框左上角的(x,y)轴坐标和右下角的(x,y)轴坐标。 输出中两个轴的坐标各分别除以了图像的宽度和高度。
boxes = Y.reshape(h, w, 5, 4)
boxes[250, 250, 0, :]
# 输出
tensor([0.06, 0.07, 0.63, 0.82])
IoU——交并比
给定两个锚框或边界框的列表,以下box_iou
函数将在这两个列表中计算它们成对的交并比
def box_iou(boxes1, boxes2):
"""计算两个锚框或边界框列表中成对的交并比"""
box_area = lambda boxes: ((boxes[:, 2] - boxes[:, 0]) *
(boxes[:, 3] - boxes[:, 1]))
# boxes1,boxes2,areas1,areas2的形状:
# boxes1:(boxes1的数量,4),
# boxes2:(boxes2的数量,4),
# areas1:(boxes1的数量,),
# areas2:(boxes2的数量,)
areas1 = box_area(boxes1)
areas2 = box_area(boxes2)
# inter_upperlefts,inter_lowerrights,inters的形状:
# (boxes1的数量,boxes2的数量,2)
inter_upperlefts = torch.max(boxes1[:, None, :2], boxes2[:, :2])
inter_lowerrights = torch.min(boxes1[:, None, 2:], boxes2[:, 2:])
inters = (inter_lowerrights - inter_upperlefts).clamp(min=0)
# inter_areasandunion_areas的形状:(boxes1的数量,boxes2的数量)
inter_areas = inters[:, :, 0] * inters[:, :, 1]
union_areas = areas1[:, None] + areas2 - inter_areas
return inter_areas / union_areas
标记类别和偏移量
现在我们可以为每个锚框标记类别和偏移量了。 假设一个锚框A被分配了一个真实边界框B。
一方面,锚框A的类别将被标记为与B相同。
另一方面,锚框A的偏移量将根据B和A中心坐标的相对位置以及这两个框的相对大小进行标记。
鉴于数据集内不同的框的位置和大小不同,我们可以对那些相对位置和大小应用变换,使其获得分布更均匀且易于拟合的偏移量。 这里介绍一种常见的变换。
def offset_boxes(anchors, assigned_bb, eps=1e-6):
"""对锚框偏移量的转换"""
c_anc = d2l.box_corner_to_center(anchors)
c_assigned_bb = d2l.box_corner_to_center(assigned_bb)
offset_xy = 10 * (c_assigned_bb[:, :2] - c_anc[:, :2]) / c_anc[:, 2:]
offset_wh = 5 * torch.log(eps + c_assigned_bb[:, 2:] / c_anc[:, 2:])
offset = torch.cat([offset_xy, offset_wh], axis=1)
return offset
def multibox_target(anchors, labels):
"""使用真实边界框标记锚框"""
batch_size, anchors = labels.shape[0], anchors.squeeze(0)
batch_offset, batch_mask, batch_class_labels = [], [], []
device, num_anchors = anchors.device, anchors.shape[0]
for i in range(batch_size):
label = labels[i, :, :]
anchors_bbox_map = assign_anchor_to_bbox(
label[:, 1:], anchors, device)
bbox_mask = ((anchors_bbox_map >= 0).float().unsqueeze(-1)).repeat(
1, 4)
# 将类标签和分配的边界框坐标初始化为零
class_labels = torch.zeros(num_anchors, dtype=torch.long,
device=device)
assigned_bb = torch.zeros((num_anchors, 4), dtype=torch.float32,
device=device)
# 使用真实边界框来标记锚框的类别。
# 如果一个锚框没有被分配,标记其为背景(值为零)
indices_true = torch.nonzero(anchors_bbox_map >= 0)
bb_idx = anchors_bbox_map[indices_true]
class_labels[indices_true] = label[bb_idx, 0].long() + 1
assigned_bb[indices_true] = label[bb_idx, 1:]
# 偏移量转换
offset = offset_boxes(anchors, assigned_bb) * bbox_mask
batch_offset.append(offset.reshape(-1))
batch_mask.append(bbox_mask.reshape(-1))
batch_class_labels.append(class_labels)
bbox_offset = torch.stack(batch_offset)
bbox_mask = torch.stack(batch_mask)
class_labels = torch.stack(batch_class_labels)
return (bbox_offset, bbox_mask, class_labels)