李沐動手學習深度學習 錨框部分程式碼解析

蘑菇王国大聪明發表於2024-04-21

這裡只是對程式碼的解析,我在寫這個解析的時候並沒有看後面的內容,只能大概猜一下可能是要幹嘛的

首先是import相關工具,這裡使用pytorch

%matplotlib inline
import torch
from d2l import torch as d2l

torch.set_printoptions(2)  # 精簡輸出精度

1.生成錨框

接下來是第一個難點,這個程式碼生啃確實得整理一下,不然很多細節都不知道。
大家可以參考
https://zh-v2.d2l.ai/chapter_computer-vision/anchor.html#subsec-predicting-bounding-boxes-nms
的計算公式,其實沐神的公式沒啥問題,歸一化之後的結果就是下面:
image
很多人不明白為什麼w歸一化之後要乘以一個in_height/in_width,假如沒有這個的話
最後的錨框寬為w * in_width, 高為 h * in_height, 這裡面發現只有高能對上
如果w = w * in_height/in_width, author_w = (w*in_height/in_width) * in_width = w * in_height
這樣是不是發現最後的錨框寬高是不是就滿足歸一化的成比例關係了,此時r就是錨框的寬高比
不過這個地方,具體問題具體分析吧,我覺得沒有 * in_height/in_width也無所謂,畢竟這樣也改變了面積。
歸根結底是在準確的錨框都是訓練出來的,最後都是會把物體框住

#@save
def multibox_prior(data, sizes, 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軸上縮放步長

    # 生成錨框的所有中心點
    # shift_y, shift_x都是笛卡爾座標系下的值
    # 例如有四個點(1, 0), (2, 0), (1, 1), (2, 1)
    # 輸出為 [0, 0], [1, 1] 和  [1, 1], [2, 2] / reshape(-1)後也是中心點的總個數
    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  # 處理矩形輸入,主要是保證w/h是歸一化用的係數
    h = torch.cat((size_tensor / torch.sqrt(ratio_tensor[0]),
                   sizes[0] / torch.sqrt(ratio_tensor[1:])))
    
    # 除以2來獲得半高和半寬
    # 因為要和中心點相加,所以這裡除以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)

img = d2l.plt.imread('./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

2.顯示邊框

這裡沒什麼好說的

def show_bboxes(axes, bboxes, labels=None, colors=None):
    """顯示所有邊界框"""
    def _make_list(obj, default_values=None):
        if obj is None:
            obj = default_values
        elif not isinstance(obj, (list, tuple)):
            obj = [obj]
        return obj

    labels = _make_list(labels)
    colors = _make_list(colors, ['b', 'g', 'r', 'm', 'c'])
    for i, bbox in enumerate(bboxes):
        color = colors[i % len(colors)]
        rect = d2l.bbox_to_rect(bbox.detach().numpy(), color)
        axes.add_patch(rect)
        if labels and len(labels) > i:
            text_color = 'k' if color == 'w' else 'w'
            axes.text(rect.xy[0], rect.xy[1], labels[i],
                      va='center', ha='center', fontsize=9, color=text_color,
                      bbox=dict(facecolor=color, lw=0))
d2l.set_figsize()
bbox_scale = torch.tensor((w, h, w, h))
fig = d2l.plt.imshow(img)
show_bboxes(fig.axes, boxes[250, 250, :, :] * bbox_scale,
            ['s=0.75, r=1', 's=0.5, r=1', 's=0.25, r=1', 's=0.75, r=2',
             's=0.75, r=0.5'])

3.iou

下面的程式碼我舉了例子,其實還好理解,唯一我覺得對不上的地方,這個座標不是左下角和右上角嗎,和文章裡說的不一樣,不過意思也是一個意思

如果左上和右下:

inter_upperlefts = torch.max(boxes1[:, None, :2], boxes2[:, :2])
inter_lowerrights = torch.min(boxes1[:, None, 2:], boxes2[:, 2:])

這兩行的max和min應該換一下

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)
    # print(boxes1[:, None, :2].shape, boxes2[:, :2].shape)
    # print(boxes1[:, None, :2])
    # print(boxes2[:, :2])
    inter_upperlefts = torch.max(boxes1[:, None, :2], boxes2[:, :2])
    # print(inter_upperlefts)
    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

boxes1 = torch.tensor([
    [0.00, 0.10, 0.20, 0.30],
    [0.15, 0.20, 0.40, 0.40],
    [0.63, 0.05, 0.88, 0.98],
    [0.66, 0.45, 0.80, 0.80],
    [0.57, 0.30, 0.92, 0.90]
])

boxes2 = torch.tensor([
    [0.10, 0.08, 0.52, 0.92],
    [0.55, 0.20, 0.90, 0.88]
])

print(boxes1.shape, boxes2.shape)
box_iou(boxes1, boxes2)

4.分配接近的錨框

#@save
def assign_anchor_to_bbox(ground_truth, anchors, device, iou_threshold=0.5):
    """將最接近的真實邊界框分配給錨框"""
    num_anchors, num_gt_boxes = anchors.shape[0], ground_truth.shape[0]
    # 位於第i行和第j列的元素x_ij是錨框i和真實邊界框j的IoU
    jaccard = box_iou(anchors, ground_truth)
    # print(jaccard)
    # 對於每個錨框,分配的真實邊界框的張量
    anchors_bbox_map = torch.full((num_anchors,), -1, dtype=torch.long,
                                  device=device)
    # 根據閾值,決定是否分配真實邊界框
    max_ious, indices = torch.max(jaccard, dim=1)
    # print(max_ious, indices)
    anc_i = torch.nonzero(max_ious >= iou_threshold).reshape(-1)
    # print(anc_i)
    box_j = indices[max_ious >= iou_threshold]
    anchors_bbox_map[anc_i] = box_j
    # print(anchors_bbox_map)
    col_discard = torch.full((num_anchors,), -1)
    row_discard = torch.full((num_gt_boxes,), -1)
    for _ in range(num_gt_boxes):
        max_idx = torch.argmax(jaccard)
        box_idx = (max_idx % num_gt_boxes).long()
        anc_idx = (max_idx / num_gt_boxes).long()
        anchors_bbox_map[anc_idx] = box_idx
        jaccard[:, box_idx] = col_discard
        jaccard[anc_idx, :] = row_discard
    return anchors_bbox_map

anchors = torch.tensor([
    [0.00, 0.10, 0.20, 0.30],
    [0.15, 0.20, 0.40, 0.40],
    [0.63, 0.05, 0.88, 0.98],
    [0.66, 0.45, 0.80, 0.80],
    [0.57, 0.30, 0.92, 0.90]
])

ground_truth = torch.tensor([
    [0.10, 0.08, 0.52, 0.92],
    [0.55, 0.20, 0.90, 0.88]
])

assign_anchor_to_bbox(ground_truth, anchors, "cpu")

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
#@save
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對應真實labels的索引
        # 例如[-1,  0,  1, -1,  1],表示錨框1和label0最相近,錨框2和label1最相近
        # label[:, 0]代表類
        anchors_bbox_map = assign_anchor_to_bbox(
            label[:, 1:], anchors, device)
        # 這裡面把預測到的對應label的變成一個mask矩陣
        # 例如[-1,  0,  1, -1,  1] 
        # [0., 0., 0., 0.],
        # [1., 1., 1., 1.],
        # [1., 1., 1., 1.],
        # [0., 0., 0., 0.],
        # [1., 1., 1., 1.]
        bbox_mask = ((anchors_bbox_map >= 0).float().unsqueeze(-1)).repeat(
            1, 4)
        print(bbox_mask)
        # 將類標籤和分配的邊界框座標初始化為零
        class_labels = torch.zeros(num_anchors, dtype=torch.long,
                                   device=device)
        assigned_bb = torch.zeros((num_anchors, 4), dtype=torch.float32,
                                  device=device)
        # 使用真實邊界框來標記錨框的類別。
        # 如果一個錨框沒有被分配,標記其為背景(值為零)
        print(anchors_bbox_map)
        # 獲取anchors_bbox_map中的哪些錨框是預測出來類了
        indices_true = torch.nonzero(anchors_bbox_map >= 0)
        print(indices_true)
        # 根據上面獲得的錨框idx,再次獲得這些錨框預測的哪一個類
        bb_idx = anchors_bbox_map[indices_true]
        print(bb_idx)
        # 分別填上預測的類和錨框座標
        # label中寫死了,第一個索引list裡面的idx值就是種類值,+1是因為,這裡認為背景是0
        class_labels[indices_true] = label[bb_idx, 0].long() + 1
        assigned_bb[indices_true] = label[bb_idx, 1:]
        # 偏移量轉換, 傳入生成的anchors和label錨框
        # 注意這裡只比較預測對的,不預測的都按0算, 所以×了一個bbox_mask
        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)

ground_truth = torch.tensor([[0, 0.1, 0.08, 0.52, 0.92],
                         [1, 0.55, 0.2, 0.9, 0.88]])
anchors = torch.tensor([[0, 0.1, 0.2, 0.3], [0.15, 0.2, 0.4, 0.4],
                    [0.63, 0.05, 0.88, 0.98], [0.66, 0.45, 0.8, 0.8],
                    [0.57, 0.3, 0.92, 0.9]])
print(ground_truth.shape, anchors.shape)
labels = multibox_target(anchors.unsqueeze(dim=0),
                         ground_truth.unsqueeze(dim=0))

5.nms

#@save
def offset_inverse(anchors, offset_preds):
    """根據帶有預測偏移量的錨框來預測邊界框"""
    anc = d2l.box_corner_to_center(anchors)
    pred_bbox_xy = (offset_preds[:, :2] * anc[:, 2:] / 10) + anc[:, :2]
    pred_bbox_wh = torch.exp(offset_preds[:, 2:] / 5) * anc[:, 2:]
    pred_bbox = torch.cat((pred_bbox_xy, pred_bbox_wh), axis=1)
    predicted_bbox = d2l.box_center_to_corner(pred_bbox)
    return predicted_bbox

#@save
def nms(boxes, scores, iou_threshold):
    """對預測邊界框的置信度進行排序"""
    B = torch.argsort(scores, dim=-1, descending=True)
    keep = []  # 保留預測邊界框的指標
    while B.numel() > 0:
        i = B[0]
        keep.append(i)
        if B.numel() == 1: break
        iou = box_iou(boxes[i, :].reshape(-1, 4),
                      boxes[B[1:], :].reshape(-1, 4)).reshape(-1)
        inds = torch.nonzero(iou <= iou_threshold).reshape(-1)

        B = B[inds + 1]
    return torch.tensor(keep, device=boxes.device)

#@save
def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5,
                       pos_threshold=0.009999999):
    """使用非極大值抑制來預測邊界框"""
    # print(cls_probs.shape, offset_preds.shape, anchors.shape)
    device, batch_size = cls_probs.device, cls_probs.shape[0]
    anchors = anchors.squeeze(0)
    # print(anchors)
    num_classes, num_anchors = cls_probs.shape[1], cls_probs.shape[2]
    out = []
    for i in range(batch_size):
        cls_prob, offset_pred = cls_probs[i], offset_preds[i].reshape(-1, 4)
        # print(offset_pred)
        # class_id是類別預測的tesnor, 例如
        # [
        # [0, 0, 0, 0],
        # [0.9, 0.8, 0.7, 0.1],
        # [0.1, 0.2, 0.3, 0.9],
        # ]
        # 代表第0類(背景)所有錨框都預測不出來,第一類四個錨框預測為第一類的機率為[0.9, 0.8, 0.7, 0.1]
        # 下面的操作會把所有錨框裡面,不管是什麼種類,只要是預測最大,都給展現出來,還有這個最大的機率是哪個種類
        # class_id = [0, 0, 0, 1]
        conf, class_id = torch.max(cls_prob[1:], 0)
        # print(cls_prob, conf, class_id)
        predicted_bb = offset_inverse(anchors, offset_pred)
        # keep 返回置信度最高, 但是互相不相關的索引(錨框id, 小於num_anchors)
        # eg. keep = [0, 3], 第0個和第一個錨框置信度比較高,但是他們不相關,是兩個class
        keep = nms(predicted_bb, conf, nms_threshold)
        # print(keep)

        # 找到所有的non_keep索引,並將類設定為背景
        # 例如num_anchors = 4, all_idx=[0, 1, 2, 3]
        all_idx = torch.arange(num_anchors, dtype=torch.long, device=device)
        # eg.上面的例子中 keep = [0, 3], combined = [0, 3, 0, 1, 2, 3], 明顯可以看到置信度最高的錨框個數會多一個
        combined = torch.cat((keep, all_idx))
        # print(combined)
        # 因為一共四個錨框,uniques=[0, 1, 2, 3], counts=[2, 1, 1, 2]
        uniques, counts = combined.unique(return_counts=True)
        # print(uniques, counts)
        # counts == 1 等價於[False, True, True, False]
        # 此時non_keep = [1, 2]
        non_keep = uniques[counts == 1]
        # print(non_keep)
        # 這裡面就把置信度最高的錨框放在最前面,[0, 3, 1, 2], 不重要的在後面了
        all_id_sorted = torch.cat((keep, non_keep))
        # print(class_id, all_id_sorted)
        # class_id是四個錨框的預測最大機率的種類,non_keep是不要的錨框
        # 因為non_keep = [1, 2],表示第一個和第二個錨框已經丟棄了,換成背景就可以了
        class_id[non_keep] = -1
        # print(class_id)
        # 換完之後 -1 的錨框就都丟到後面了,all_id_sorted就是索引
        # 此時class_id = [0, 1, -1, -1]
        class_id = class_id[all_id_sorted]
        # print(class_id)
        # 同理conf,和predicted_bb也要同樣移動位置
        # conf:[0.9, 0.8, 0.7, 0.9] --> [0.9, 0.9, 0.8, 0.7]
        # predicted_bb: [[aucher0的座標], [aucher1的座標], [aucher2的座標], [aucher3的座標]]
        #            -->[[aucher0的座標], [aucher3的座標], [aucher1的座標], [aucher2的座標]]
        conf, predicted_bb = conf[all_id_sorted], predicted_bb[all_id_sorted]
        # pos_threshold是一個用於非背景預測的閾值
        # 小於pos_threshold。 不管前面怎麼計算,就徹底是背景了
        # 這是因為計算nms的時候,可能一個類,有多個預測還可以的錨框。
        # 比如人的上半身是一個auchor,下半身是一個,nms有可能把這兩個都篩選出來
        below_min_idx = (conf < pos_threshold)
        # 標記為背景
        class_id[below_min_idx] = -1
        # 既然是背景,目標類的機率 + 背景類的機率 = 1, 所以背景類的機率為 1 - 目標類的機率
        conf[below_min_idx] = 1 - conf[below_min_idx]
        # 拼接起來,分別是該錨框是哪個類,預測機率和錨框座標 [0.00,  0.90,  0.10,  0.08,  0.52,  0.92]
        pred_info = torch.cat((class_id.unsqueeze(1),
                               conf.unsqueeze(1),
                               predicted_bb), dim=1)
        out.append(pred_info)
    return torch.stack(out)

anchors = torch.tensor([[0.1, 0.08, 0.52, 0.92], [0.08, 0.2, 0.56, 0.95],
                      [0.15, 0.3, 0.62, 0.91], [0.55, 0.2, 0.9, 0.88]])
offset_preds = torch.tensor([0] * anchors.numel())
cls_probs = torch.tensor([[0] * 4,  # 背景的預測機率
                      [0.9, 0.8, 0.7, 0.1],  # 狗的預測機率
                      [0.1, 0.2, 0.3, 0.9]])  # 貓的預測機率

output = multibox_detection(cls_probs.unsqueeze(dim=0),
                            offset_preds.unsqueeze(dim=0),
                            anchors.unsqueeze(dim=0),
                            nms_threshold=0.5)

print(output)

fig = d2l.plt.imshow(img)
for i in output[0].detach().numpy():
    if i[0] == -1:
        continue
    label = ('dog=', 'cat=')[int(i[0])] + str(i[1])
    show_bboxes(fig.axes, [torch.tensor(i[2:]) * bbox_scale], label)

相關文章