所以,我们需要结合 anchor 原图中的绝对位置 以及 bounding box 回归参数,计算出 预测 bounding box 在原图中的绝对位置。 求 predicted bounding box 的绝对坐标位置,是为了和 ground truth bounding box 的坐标位置 做对比,计算出 loss。
raw anchor:使用超参数 s i z e = ( 32 , 64 , 128 , 256 , 512 ) size=(32, 64, 128, 256, 512) size=(32,64,128,256,512) 和 a s p e c t _ r a t i o s = ( 0.5 , 1.0 , 2.0 ) aspect\_ratios=(0.5, 1.0, 2.0) aspect_ratios=(0.5,1.0,2.0) 创建anchors,可创建 5 ∗ 3 = 15 5*3=15 5∗3=15 个 anchors。( feature map 上的每一个像素点都会产生这15个anchors)
映射到原图像上的 anchors :feature map 上的每个像素都会产生15个anchors,将 feature map 上的每个像素点都映射回原图像中,原图像中的这些像素点就是 anchors 的中心点。然后,将 anchors 也映射回原图中,就得到了原图像中的 anchors 的绝对坐标位置。
每张图像 anchors总量 = 15 ∗ h e i g h t ∗ w i d t h 15 * height * width 15∗height∗width ,其中,height、width 为feature map 的高和宽。
预测 bounding box 回归参数:由 RPN Head 网络得出,shape=(batch_size, 60, height, width) ,我们将它形状转换为 shape= ( b a t c h _ s i z e ∗ 15 ∗ h e i g h t ∗ w i d t h , 4 ) (batch\_size*15*height*width, 4) (batch_size∗15∗height∗width,4) 用于之后的计算
目的:计算出 predicted boxes 在原图上的 坐标位置。
方法:在原图上 anchor 坐标的基础上,加上 predicted box 的偏移量。 predicted box 的偏移量 是由 预测bounding box 回归参数 计算得到。
1)将anchor 的坐标形式 从(xmin, ymin, xmax, ymax) 转换为 (ctr_x, ctr_y, widths, heights)
anchor_widths = anchors[:, 2] - anchors[:, 0] # anchors 宽度
anchor_heights = anchors[:, 3] - anchors[:, 1] # anchors 高度
ctr_x = anchors[:, 0] + 0.5 * anchor_widths # anchors 中心x坐标
ctr_y = anchors[:, 1] + 0.5 * anchor_heights # anchors 中心y坐标
\quad
2)预测回归参数 pred_box_regression,shape 为 ( b a t c h _ s i z e ∗ 15 ∗ h e i g h t ∗ w i d t h , 4 ) (batch\_size*15*height*width, 4) (batch_size∗15∗height∗width,4),其中 b a t c h _ s i z e ∗ 15 ∗ h e i g h t ∗ w i d t h batch\_size*15*height*width batch_size∗15∗height∗width 是 anchor 的数量, 4 就是每个anchor 对应的坐标回归参数,将这4个值解析出来,分别记为:dx, dy, dw, dh。
限制 dw, dh 的最大值为 1000./16=4.135,预防 后面计算的时候,传递过大的值到 torch.exp()中
# self.bbox_xform_clip = math.log(1000. / 16)
dw = torch.clamp(dw, max=self.bbox_xform_clip)
dh = torch.clamp(dh, max=self.bbox_xform_clip)
\quad
3)计算出 预测box 相对于anchor 中心的偏移量
将偏移量加到 anchor 中心坐标,就得到 预测box 的中心坐标位置
预测 box 的高度 和宽度
pred_ctr_x = dx * anchor_widths[:, None] + ctr_x[:, None]
pred_ctr_y = dy * anchor_heights[:, None] + ctr_y[:, None]
pred_w = torch.exp(dw) * anchor_widths[:, None]
pred_h = torch.exp(dh) * anchor_heights[:, None]
\quad
4)最后,将 预测 box的坐标形式 从 (ctr_x, ctr_y, width, height) 转换为 (xmin, ymin, xmax, ymax)
pred_boxes_xmin = pred_ctr_x - torch.tensor(0.5, dtype=pred_ctr_x.dtype, device=pred_w.device) * pred_w
pred_boxes_ymin = pred_ctr_y - torch.tensor(0.5, dtype=pred_ctr_y.dtype, device=pred_h.device) * pred_h
pred_boxes_xmax = pred_ctr_x + torch.tensor(0.5, dtype=pred_ctr_x.dtype, device=pred_w.device) * pred_w
pred_boxes_ymax = pred_ctr_y + torch.tensor(0.5, dtype=pred_ctr_y.dtype, device=pred_h.device) * pred_h
\quad
到此,我们就计算出了 proposals 在原图中的坐标了。
\quad
当前获取到的 候选框 (proposals) 数量太大了,一般都是 十万级的数量。
我们接下来我们要筛选出 1000+ 个proposals.(不超过2000个)
筛选出 预测概率 排前 2000 的proposals,也就是最有可能包含 objects 的 proposals
还记得我们之前从 RPN Head 网络(如下图)中得到了 所有 proposals 的概率嘛,我们从中筛选出 top 2000
筛选出的 proposals,我们按照概率的降序的顺序 存储,包括 概率 和 对应的proposals的坐标。
'''
objectness :预测概率值
proposals :proposals的坐标
train阶段, pre_nms_top_n = 2000
'''
# 预测概率值为 top 2000 的 proposals 的索引
_, top_n_idx = objectness.topk(pre_nms_top_n, dim=1)
# top 2000 的 proposals 的 预测概率值 和 坐标
batch_idx = torch.arange(num_images, device=device).reshape(-1, 1)
objectness = objectness[batch_idx, top_n_idx]
proposals = proposals[batch_idx, top_n_idx]
bjectness_prob = torch.sigmoid(objectness)
将 预测概率 做 sigmoid 运算,转成真正的概率值 : 数值 映射到 0~1
objectness_prob = torch.sigmoid(objectness)
截断 proposals 超出原图像的部分,并将坐标调整到图片边界上。
这里 width 和 height 分别为 图像 resize 之后,padding 之前的高和宽。 细节在这里
boxes = clip_boxes_to_image(boxes, img_shape)
def clip_boxes_to_image(boxes, size):
boxes_x = boxes[:, 0::2] # x1, x2
boxes_y = boxes[:, 1::2] # y1, y2
height, width = size
boxes_x = boxes_x.clamp(min=0, max=width) # 限制x坐标范围在[0,width]之间
boxes_y = boxes_y.clamp(min=0, max=height) # 限制y坐标范围在[0,height]之间
clipped_boxes = torch.stack((boxes_x, boxes_y), dim=2)
return clipped_boxes.reshape(boxes.shape) # (top_k_num, 4)
这里的 min_size=1 , 是超参数
keep = remove_small_boxes(boxes, self.min_size)
boxes, scores = boxes[keep], scores[keep]
def remove_small_boxes(boxes, min_size):
ws = boxes[:, 2] - boxes[:, 0] # boxes的宽
hs = boxes[:, 3] - boxes[:, 1] # boxes的高
keep = torch.logical_and(torch.ge(ws, min_size), torch.ge(hs, min_size))
keep = torch.where(keep)[0]
return keep
这里 阈值设置的是 0.0
keep = torch.where(torch.ge(scores, self.score_thresh))[0]
boxes, scores = boxes[keep], scores[keep]
使用的 shreshold=0.7
keep = batched_nms(boxes, scores, self.nms_thresh)
def batched_nms(boxes, scores, iou_threshold):
if boxes.numel() == 0:
return torch.empty((0,), dtype=torch.int64, device=boxes.device)
keep = torch.ops.torchvision.nms(boxes, scores, iou_threshold)
return keep
我们第一步处理 的时候,是先筛选出了 top 2000 的proposals (pre_nms_top_n=20000)
最后一步,我们会再设置超参数 post_nms_top_n 筛选出 一定数量的 proposals,只是这里我们设置的 post_nms_top_n = pre_nms_top_n = 2000
def decode(self, pred_box_regression, anchors):
# concatgate batch_size 张图片的所有 anchors, 形为 (batch_size * Anchor * Height * Width, 1)
anchors = torch.cat(anchors, dim=0)
# 将预测的bbox回归参数应用到对应anchors上得到预测bbox的坐标
anchors = anchors.to(pred_box_regression.dtype)
# anchor 的坐标形式 从(xmin, ymin, xmax, ymax) 转换为 (ctr_x, ctr_y, anchor_widths, anchor_heights)
anchor_widths = anchors[:, 2] - anchors[:, 0] # anchor/proposal宽度
anchor_heights = anchors[:, 3] - anchors[:, 1] # anchor/proposal高度
ctr_x = anchors[:, 0] + 0.5 * anchor_widths # anchor/proposal中心x坐标
ctr_y = anchors[:, 1] + 0.5 * anchor_heights # anchor/proposal中心y坐标
# 解析出 predicted box 的中心坐标回归参数
dx, dy, dw, dh = pred_box_regression.split([1, 1, 1, 1], dim=1)
# limit max value, prevent sending too large values into torch.exp()
# self.bbox_xform_clip = math.log(1000. / 16) = 4.135
dw = torch.clamp(dw, max=self.bbox_xform_clip)
dh = torch.clamp(dh, max=self.bbox_xform_clip)
# 在anchor的基础上计算 predicted box 的坐标: pred_ctr_x, pred_ctr_y, pred_w, pred_h
pred_ctr_x = dx * anchor_widths[:, None] + ctr_x[:, None]
pred_ctr_y = dy * anchor_heights[:, None] + ctr_y[:, None]
pred_w = torch.exp(dw) * anchor_widths[:, None]
pred_h = torch.exp(dh) * anchor_heights[:, None]
# 将 predicted box 坐标形式 由(pred_ctr_x, pred_ctr_y, pred_w, pred_h) 转换为 (xmin、ymin、xmax、ymax)
pred_boxes_xmin = pred_ctr_x - torch.tensor(0.5, dtype=pred_ctr_x.dtype, device=pred_w.device) * pred_w
pred_boxes_ymin = pred_ctr_y - torch.tensor(0.5, dtype=pred_ctr_y.dtype, device=pred_h.device) * pred_h
pred_boxes_xmax = pred_ctr_x + torch.tensor(0.5, dtype=pred_ctr_x.dtype, device=pred_w.device) * pred_w
pred_boxes_ymax = pred_ctr_y + torch.tensor(0.5, dtype=pred_ctr_y.dtype, device=pred_h.device) * pred_h
pred_boxes = torch.stack((pred_boxes_xmin, pred_boxes_ymin, pred_boxes_xmax, pred_boxes_ymax), dim=2).flatten(1)
return pred_boxes
def filter_proposals(self, proposals, objectness, image_shapes):
# # type: (Tensor, Tensor, List[Tuple[int, int]], List[int]) -> Tuple[List[Tensor], List[Tensor]]
num_images = proposals.shape[0]
device = proposals.device
# do not backprop throught objectness
objectness = objectness.detach()
objectness = objectness.reshape(num_images, -1)
# select top_n boxes before applying nms
if self.training:
pre_nms_top_n = self.pre_nms_top_n['training'] # pre_nms_top_n = 2000
else:
pre_nms_top_n = self.pre_nms_top_n['testing'] # pre_nms_top_n = 1000
# 预测概率值为 top 2000 的 proposals 的索引
_, top_n_idx = objectness.topk(pre_nms_top_n, dim=1)
# top 2000 的 预测概率值 & proposals坐标
batch_idx = torch.arange(num_images, device=device).reshape(-1, 1)
objectness = objectness[batch_idx, top_n_idx]
proposals = proposals[batch_idx, top_n_idx]
objectness_prob = torch.sigmoid(objectness)
final_boxes = []
final_scores = []
# 遍历每张图像的相关预测信息
for boxes, scores, img_shape in zip(proposals, objectness_prob, image_shapes):
# 调整 proposals 的坐标,将越界的坐标调整到图片边界上
boxes = clip_boxes_to_image(boxes, img_shape)
# 返回boxes满足宽,高都大于min_size的索引
keep = remove_small_boxes(boxes, self.min_size)
boxes, scores = boxes[keep], scores[keep]
# 移除小概率boxes,参考 https://github.com/pytorch/vision/pull/3205
keep = torch.where(torch.ge(scores, self.score_thresh))[0] # ge: >=
boxes, scores = boxes[keep], scores[keep]
# NMS (non-maximum suppression), independently done per level
keep = batched_nms(boxes, scores, self.nms_thresh)
# keep only topk scoring predictions # self.pre_nms_top_n={'training': 2000, 'testing': 1000}
if self.training:
keep = keep[: self.post_nms_top_n['training']]
else:
keep = keep[: self.post_nms_top_n['testing']]
boxes, scores = boxes[keep], scores[keep]
final_boxes.append(boxes)
final_scores.append(scores)
return final_boxes, final_scores
def clip_boxes_to_image(boxes, size):
boxes_x = boxes[:, 0::2] # x1, x2
boxes_y = boxes[:, 1::2] # y1, y2
height, width = size
boxes_x = boxes_x.clamp(min=0, max=width) # 限制x坐标范围在[0,width]之间
boxes_y = boxes_y.clamp(min=0, max=height) # 限制y坐标范围在[0,height]之间
clipped_boxes = torch.stack((boxes_x, boxes_y), dim=2)
return clipped_boxes.reshape(boxes.shape)
def remove_small_boxes(boxes, min_size):
ws = boxes[:, 2] - boxes[:, 0] # boxes的宽
hs = boxes[:, 3] - boxes[:, 1] # boxes的高
keep = torch.logical_and(torch.ge(ws, min_size), torch.ge(hs, min_size))
keep = torch.where(keep)[0]
return keep
def batched_nms(boxes, scores, iou_threshold):
if boxes.numel() == 0:
return torch.empty((0,), dtype=torch.int64, device=boxes.device)
keep = torch.ops.torchvision.nms(boxes, scores, iou_threshold)
return keep