yolov5 篩選正樣本流程 程式碼多圖詳解

金色旭光發表於2024-07-10

yolov5正樣本篩選原理

正樣本全稱是anchor正樣本,正樣本所指的物件是anchor box,即先驗框。
先驗框:從YOLO v2開始吸收了Faster RCNN的優點,設定了一定數量的預選框,使得模型不需要直接預測物體尺度與座標,只需要預測先驗框到真實物體的偏移,降低了預測難度。

正樣本獲取規則

Yolov5演算法使用如下3種方式增加正樣本個數:

一、跨anchor預測

假設一個GT框落在了某個預測分支的某個網格內,該網格具有3種不同大小anchor,若GT可以和這3種anchor中的多種anchor匹配,則這些匹配的anchor都可以來預測該GT框,即一個GT框可以使用多種anchor來預測。
具體方法:
不同於IOU匹配,yolov5採用基於寬高比例的匹配策略,GT的寬高與anchors的寬高對應相除得到ratio1,anchors的寬高與GT的寬高對應相除得到ratio2,取ratio1和ratio2的最大值作為最後的寬高比,該寬高比和設定閾值(預設為4)比較,小於設定閾值的anchor則為匹配到的anchor。

anchor_boxes=torch.tensor([[1.25000, 1.62500],[2.00000, 3.75000],[4.12500, 2.87500]])
gt_box=torch.tensor([5,4])

ratio1=gt_box/anchor_boxes
ratio2=anchor_boxes/gt_box
ratio=torch.max(ratio1, ratio2).max(1)[0]
print(ratio)

anchor_t=4
res=ratio<anchor_t
print(res)
tensor([4.0000, 2.5000, 1.3913])
tensor([False,  True,  True])

與 GT 相匹配的的 anchor 為 **anchor2 **和 anchor3

二、跨grid預測

假設一個GT框落在了某個預測分支的某個網格內,則該網格有左、上、右、下4個鄰域網格,根據GT框的中心位置,將最近的2個鄰域網格也作為預測網格,也即一個GT框可以由3個網格來預測。
計算例子:

GT box中心點處於grid1中,grid1被選中。為了增加增樣本,grid1的上下左右grid為候選網格,因為GT中心點更靠近grid2和grid3,grid2和grid3也作為匹配到的網格。
根據上個步驟中的anchor匹配結果,GT與anchor2、anchor3相匹配,因此GT在當前層匹配到的正樣本有6個,分別為:

  • grid1_anchor2,grid1_anchor3
  • grid2_anchor2,grid2_anchor3
  • grid3_anchor2,grid3_anchor3

三、跨分支預測

假設一個GT框可以和2個甚至3個預測分支上的anchor匹配,則這2個或3個預測分支都可以預測該GT框。即一個GT框可以在3個預測分支上匹配正樣本,在每一個分支上重複anchor匹配和grid匹配的步驟,最終可以得到某個GT 匹配到的所有正樣本。
如下圖在Prediction的3個不同尺度的輸出中,gt都可以去匹配正樣本。

正樣本篩選

正樣本篩選主要做了四件事情:

  1. 透過寬高比獲得合適的anchor
  2. 透過anchor所在的網格獲得上下左右擴充套件網格
  3. 獲取標註框相對網格左上角的偏移量
  4. 返回獲得的anchor,網格序號,偏移量,類別等

yolov5中anchor值

anchors:
  - [10,13, 16,30, 33,23]  # P3/8
  - [30,61, 62,45, 59,119]  # P4/16
  - [116,90, 156,198, 373,326]  # P5/32

yolov5的網路有三個尺寸的輸出,不同大小的輸出對應不同尺寸:

  • 8倍下采樣: [10,13, 16,30, 33,23]
  • 16倍下采樣:[30,61, 62,45, 59,119]
  • 32倍下采樣:[116,90, 156,198, 373,326]

註釋程式碼

yolov5/utils/loss.py

    def build_targets(self, p, targets):
        # Build targets for compute_loss(), input targets(image,class,x,y,w,h)

        """
        p: 預測值
        targets:gt
        (Pdb) pp p[0].shape
        torch.Size([1, 3, 80, 80, 7])
        (Pdb) pp p[1].shape
        torch.Size([1, 3, 40, 40, 7])
        (Pdb) pp p[2].shape
        torch.Size([1, 3, 20, 20, 7])
        (Pdb) pp targets.shape
        torch.Size([23, 6])
        """
        na, nt = self.na, targets.shape[0]  # number of anchors, targets
        tcls, tbox, indices, anch = [], [], [], []
        
        """
        tcls    儲存類別id
        tbox    儲存的是gt中心相對於所在grid cell左上角偏移量。也會計算出gt中心相對擴充套件anchor的偏移量
        indices 儲存的內容是:image_id, anchor_id, grid x刻度  grid y刻度
        anch 儲存anchor的具體寬高
        """
        
        gain = torch.ones(7, device=self.device)  # normalized to gridspace gain
        ai = torch.arange(na, device=self.device).float().view(na, 1).repeat(1, nt)  # same as .repeat_interleave(nt)
        """
        (Pdb) ai
        tensor([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
                [1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
                [2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.]], device='cuda:0')
        (Pdb) ai.shape
        torch.Size([3, 23])
        """
        targets = torch.cat((targets.repeat(na, 1, 1), ai[..., None]), 2)  # append anchor indices

        g = 0.5  # bias
        off = torch.tensor(
            [
                [0, 0],
                [1, 0],
                [0, 1],
                [-1, 0],
                [0, -1],  # j,k,l,m
                # [1, 1], [1, -1], [-1, 1], [-1, -1],  # jk,jm,lk,lm
            ],
            device=self.device).float() * g  # offsets

        for i in range(self.nl):
            anchors, shape = self.anchors[i], p[i].shape
            """
            (Pdb) anchors
            tensor([[1.25000, 1.62500],
                    [2.00000, 3.75000],
                    [4.12500, 2.87500]], device='cuda:0')
            (Pdb) shape
            torch.Size([1, 3, 80, 80, 7])
            """
            gain[2:6] = torch.tensor(shape)[[3, 2, 3, 2]]  # xyxy gain
            """
            (Pdb) gain
            tensor([ 1.,  1., 80., 80., 80., 80.,  1.], device='cuda:0')
            """

            # Match targets to anchors
            t = targets * gain  # shape(3,n,7)  # 將grid cell還原到當前feature map上
            """
            (Pdb) t.shape
            torch.Size([3, 23, 7])
            """

            if nt:
                # Matches
                r = t[..., 4:6] / anchors[:, None]  # wh ratio
                j = torch.max(r, 1 / r).max(2)[0] < self.hyp['anchor_t']  # compare
                # j = wh_iou(anchors, t[:, 4:6]) > model.hyp['iou_t']  # iou(3,n)=wh_iou(anchors(3,2), gwh(n,2))
                t = t[j]  # filter
                """
                (Pdb) t.shape
                torch.Size([3, 23, 7]) -> torch.Size([62, 7])
                """

                # Offsets
                gxy = t[:, 2:4]  # grid xy
                gxi = gain[[2, 3]] - gxy  # inverse
                j, k = ((gxy % 1 < g) & (gxy > 1)).T
                """
                (Pdb) ((gxy % 1 < g) & (gxy > 1)).shape
                torch.Size([186, 2])
                (Pdb) ((gxy % 1 < g) & (gxy > 1)).T.shape
                torch.Size([2, 186])
                """
                l, m = ((gxi % 1 < g) & (gxi > 1)).T

                j = torch.stack((torch.ones_like(j), j, k, l, m))
                """
                torch.ones_like(j) 代表gt中心所在grid cell
                j, k, l, m 代表擴充套件的上下左右grid cell
                
                torch.Size([5, 51])
                """
                t = t.repeat((5, 1, 1))[j]
                """
                標籤也重複5次,和上面的擴充套件gird cell一起篩選出所有的,符合條件的grid cell
                (Pdb) pp t.shape
                torch.Size([153, 7])
                (Pdb) t.repeat((5, 1, 1)).shape
                torch.Size([5, 153, 7])
                (Pdb) pp t.shape
                torch.Size([232, 7])
                """
                offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j]

                """
                計算出所有grid cell的偏移量,作用在標籤上之後就能得到最終的grid cell
                (Pdb) pp offsets.shape
                torch.Size([529, 2])
                """
            else:
                t = targets[0]
                offsets = 0


            # Define
            bc, gxy, gwh, a = t.chunk(4, 1)  # (image, class), grid xy, grid wh, anchors
            a, (b, c) = a.long().view(-1), bc.long().T  # anchors, image, class
            gij = (gxy - offsets).long()
            """
            用gt中心點的座標減去偏移量,得到最終的grid cell的座標。其中中心點也在。
            gxy 是在當前feature map下的gt中心點,如80*80下的 (55.09, 36.23),減去偏移量,再取整就能得到一個grid cell的座標,如 (55,36)
            Pdb) pp gij.shape
            torch.Size([529, 2])
            (Pdb) pp gij
            tensor([[ 9, 22],
                [ 2, 23],
                [ 6, 23],
                ...,
                [ 5, 19],
                [ 5, 38],
                [15, 36]], device='cuda:0')
            """
            gi, gj = gij.T  # grid indices

            # Append
            # indices 儲存的內容是:image_id, anchor_id(0,1,2), grid x刻度  grid y刻度。這裡的刻度就是正樣本
            indices.append((b, a, gj.clamp_(0, shape[2] - 1), gi.clamp_(0, shape[3] - 1)))  # image, anchor, grid

            # tbox儲存的是gt中心相對於所在grid cell左上角偏移量。也會計算出gt中心相對擴充套件anchor的偏移量
            tbox.append(torch.cat((gxy - gij, gwh), 1))  # box
            """
            (Pdb) pp tbox[0].shape
                torch.Size([312, 4])
            (Pdb) pp tbox[0]
                tensor([[ 0.70904,  0.50893,  4.81701,  5.14418],
                        [ 0.28421,  0.45330,  3.58872,  4.42822],
                        [ 0.44398,  0.60475,  3.79576,  4.98174],
                        ...,
                        [ 0.59653, -0.37711,  3.97289,  4.44963],
                        [ 0.32074, -0.05419,  5.19988,  5.59987],
                        [ 0.28691, -0.38742,  5.79986,  6.66651]], device='cuda:0')
            (Pdb) gxy
                tensor([[ 9.19086, 22.46842],
                        [ 2.50407, 23.72271],
                        [ 6.35452, 23.75447],
                        ...,
                        [ 5.91273, 18.75906],
                        [ 5.16037, 37.97290],
                        [15.64346, 35.80629]], device='cuda:0')
                (Pdb) gij
                tensor([[ 9, 22],
                        [ 2, 23],
                        [ 6, 23],
                        ...,
                        [ 5, 19],
                        [ 5, 38],
                        [15, 36]], device='cuda:0')
                (Pdb) gxy.shape
                torch.Size([529, 2])
                (Pdb) gij.shape
                torch.Size([529, 2])
            """
            anch.append(anchors[a])  # anchors # 儲存anchor的具體寬高
            tcls.append(c)  # class 儲存類別id
            
            """
            (Pdb) pp anch[0].shape
                torch.Size([312, 2])
                (Pdb) pp tcls[0].shape
                torch.Size([312])
            """

        return tcls, tbox, indices, anch

程式碼基本思路

  1. 傳入預測值和標註資訊。預測值用於獲取當前操作的下采樣倍數
  2. 遍歷每一種feature map,分別獲取正樣本資料
  3. 獲取當前feature map的下采樣尺度,將歸一化的標註座標還原到當前feature map的大小上
  4. 計算gt和anchor的邊框長寬比,符合條件置為True,不符合條件置為False。過濾掉為False的anchor
  5. 計算gt中心的xy和左上邊框距離和右下邊框距離,篩選出符合條件的grid cell,並計算出所有符合條件的anchor相對當前gt所在anchor偏移量
  6. 透過上一步計算出來的偏移量和gt中心計算,得到所有anchor的座標資訊
  7. 用gt所在偏移量減去grid cell的座標資訊,得到gt相對於所屬anchor左上角的偏移量。包括gt中心anchor和擴充套件anchor
  8. 收集所有資訊,包括:
  • indices 儲存的內容是:image_id, anchor_id, grid x刻度 grid y刻度
  • tbox 儲存的是gt中心相對於所在grid cell左上角偏移量。也會計算出gt中心相對擴充套件anchor的偏移量
  • anchors 儲存anchor的具體寬高
  • class 儲存類別id

準備工作

在進入正樣本篩選之前,需要做一些準備工作,主要是獲取必要的引數。

def build_targets(self, p, targets):
    pass 

輸入的引數:
targets 是這一批圖片的標註資訊,每一行的內容分別是:image, class, x, y, w, h。

(Pdb) pp targets.shape
torch.Size([63, 6])

tensor([[0.00000, 1.00000, 0.22977, 0.56171, 0.08636, 0.09367],
        [0.00000, 0.00000, 0.06260, 0.59307, 0.07843, 0.08812],
        [0.00000, 0.00000, 0.15886, 0.59386, 0.06021, 0.06430],
        [0.00000, 0.00000, 0.31930, 0.58910, 0.06576, 0.09129],
        [0.00000, 0.00000, 0.80959, 0.70458, 0.23025, 0.26275],
        [1.00000, 1.00000, 0.85008, 0.07597, 0.09781, 0.11827],
        [1.00000, 0.00000, 0.22484, 0.09267, 0.14065, 0.18534]

p 模型預測資料。主要用於獲取每一層的尺度

(Pdb) pp p[0].shape
torch.Size([1, 3, 80, 80, 7])
(Pdb) pp p[1].shape
torch.Size([1, 3, 40, 40, 7])
(Pdb) pp p[2].shape
torch.Size([1, 3, 20, 20, 7])

獲取anchor的數量和標註的資料的個數。設定一批讀入的資料為6張圖片,產生了66個標註框。

na, nt = self.na, targets.shape[0]  # number of anchors, targets
tcls, tbox, indices, anch = [], [], [], []
pp na
3
(Pdb) pp nt
66
(Pd

targets儲存的標註資訊,首先將標註資訊複製成三份,同時給每一份標註資訊分配一個不同大小的anchor。相當於同一個標註框就擁有三個不同的anchor

在targets張量最後增加一個資料用於儲存anchor的index。後續的篩選都是以單個anchor為顆粒度。targets 每一行內容:image, class, x, y, w, h,anchor_id

targets = torch.cat((targets.repeat(na, 1, 1), ai[..., None]), 2)
>>>
(Pdb) pp targets.shape
torch.Size([3, 63, 7])

定義長寬比的比例g=0.5和擴充套件網格的選擇範圍off

g = 0.5  # bias
off = torch.tensor(
    [
        [0, 0],
        [1, 0],
        [0, 1],
        [-1, 0],
        [0, -1],  # j,k,l,m
        # [1, 1], [1, -1], [-1, 1], [-1, -1],  # jk,jm,lk,lm
    ],
    device=self.device).float() * g  # offsets

獲取正樣本anchor

遍歷三種尺度,在每一種尺度上獲取正樣本anchor和擴充套件網格
首先將標註框還原到當前尺度上。從傳入的預測資料中獲取尺度,如80 * 80,那麼就是將中心點和寬高還原到80*80的尺度上,還原之前的尺度都是0-1之間歸一化處理的,還原之後範圍就是在0-80。

anchors, shape = self.anchors[i], p[i].shape
"""
(Pdb) anchors
tensor([[1.25000, 1.62500],
        [2.00000, 3.75000],
        [4.12500, 2.87500]], device='cuda:0')
(Pdb) shape
torch.Size([1, 3, 80, 80, 7])
"""
gain[2:6] = torch.tensor(shape)[[3, 2, 3, 2]]  # xyxy gain
"""
(Pdb) gain
tensor([ 1.,  1., 80., 80., 80., 80.,  1.], device='cuda:0')
"""

# Match targets to anchors
t = targets * gain  # shape(3,n,7)  # 將grid cell還原到當前feature map上

targets此時一行資料分別是:image_id, clss_id, 當前尺度下的x,當前尺度下的y,當前尺度下的寬,當前尺度下的高,當前尺度下的anchor_id。

(Pdb) pp t.shape
torch.Size([3, 63, 7])
(Pdb) pp t[0,0]
tensor([ 0.00000,  1.00000, 18.38171, 44.93684,  6.90862,  7.49398,  0.00000], device='cuda:0')
(Pdb) pp t
tensor([[[ 0.00000,  1.00000, 18.38171,  ...,  6.90862,  7.49398,  0.00000],
         [ 0.00000,  0.00000,  5.00814,  ...,  6.27480,  7.04943,  0.00000],
         [ 0.00000,  0.00000, 12.70904,  ...,  4.81701,  5.14418,  0.00000],
         ...,
         [ 5.00000,  0.00000, 10.32074,  ...,  5.19988,  5.59987,  0.00000],
         [ 5.00000,  0.00000, 31.28691,  ...,  5.79986,  6.66651,  0.00000],
         [ 5.00000,  0.00000, 51.81977,  ...,  5.66653,  5.93320,  0.00000]],

        [[ 0.00000,  1.00000, 18.38171,  ...,  6.90862,  7.49398,  1.00000],
         [ 0.00000,  0.00000,  5.00814,  ...,  6.27480,  7.04943,  1.00000],
         [ 0.00000,  0.00000, 12.70904,  ...,  4.81701,  5.14418,  1.00000],
         ...,
         [ 5.00000,  0.00000, 10.32074,  ...,  5.19988,  5.59987,  1.00000],
         [ 5.00000,  0.00000, 31.28691,  ...,  5.79986,  6.66651,  1.00000],
         [ 5.00000,  0.00000, 51.81977,  ...,  5.66653,  5.93320,  1.00000]],

        [[ 0.00000,  1.00000, 18.38171,  ...,  6.90862,  7.49398,  2.00000],
         [ 0.00000,  0.00000,  5.00814,  ...,  6.27480,  7.04943,  2.00000],
         [ 0.00000,  0.00000, 12.70904,  ...,  4.81701,  5.14418,  2.00000],
         ...,
         [ 5.00000,  0.00000, 10.32074,  ...,  5.19988,  5.59987,  2.00000],
         [ 5.00000,  0.00000, 31.28691,  ...,  5.79986,  6.66651,  2.00000],
         [ 5.00000,  0.00000, 51.81977,  ...,  5.66653,  5.93320,  2.00000]]], device='cuda:0')

yolov5 正樣本選取規則
yolov5中正負樣本的計算規則是:比較標註框和anchor的寬高,比例在0.25-4以內就是正樣本。如下圖所示:
gt的原本面積為藍色,虛線標註了0.25倍和4倍。只要anchor在0.25-4之間,就是匹配成功。

如果存在標註框,則計算anchor和標註框的寬高比

if nt:
    # 獲取寬高比
    r = t[..., 4:6] / anchors[:, None]  

    # 獲取 寬高比或寬高比倒數 中最大的一個,和4比較。self.hyp['anchor_t'] = 4
    j = torch.max(r, 1 / r).max(2)[0] < self.hyp['anchor_t']  # compare

    # 將正樣本過濾出來
    t = t[j]  # filter

此時t儲存的就是所有符合條件的標註框,後續用於計算anchor和網格資訊。這一階段的結束之後,輸出的是所有符合條件的anchor。t儲存的是 image, class, x, y, w, h,anchor_id,同一個圖片會對應多個標註框,多個標註框可能會對應多個anchor。

跨anchor匹配
r計算的過程中包含了跨anchor匹配。在準備工作中已經介紹過將標註框複製了三份,每一份都分配了一個anchor,相當於一個標註框擁有三種不同大小的anchor。現在計算寬高比獲得的結果只要符合條件的都會認為是正樣本,3種anchor之間互不干擾,所以會出現一個標註框匹配多個anchor。

(Pdb) pp t.shape
torch.Size([3, 63, 7])
(Pdb) pp t
tensor([[[ 0.00000,  1.00000, 18.38171,  ...,  6.90862,  7.49398,  0.00000],
         [ 0.00000,  0.00000,  5.00814,  ...,  6.27480,  7.04943,  0.00000],
         [ 0.00000,  0.00000, 12.70904,  ...,  4.81701,  5.14418,  0.00000],
         ...,
         [ 5.00000,  0.00000, 10.32074,  ...,  5.19988,  5.59987,  0.00000],
         [ 5.00000,  0.00000, 31.28691,  ...,  5.79986,  6.66651,  0.00000],
         [ 5.00000,  0.00000, 51.81977,  ...,  5.66653,  5.93320,  0.00000]],

        [[ 0.00000,  1.00000, 18.38171,  ...,  6.90862,  7.49398,  1.00000],
         [ 0.00000,  0.00000,  5.00814,  ...,  6.27480,  7.04943,  1.00000],
         [ 0.00000,  0.00000, 12.70904,  ...,  4.81701,  5.14418,  1.00000],
         ...,
         [ 5.00000,  0.00000, 10.32074,  ...,  5.19988,  5.59987,  1.00000],
         [ 5.00000,  0.00000, 31.28691,  ...,  5.79986,  6.66651,  1.00000],
         [ 5.00000,  0.00000, 51.81977,  ...,  5.66653,  5.93320,  1.00000]],

        [[ 0.00000,  1.00000, 18.38171,  ...,  6.90862,  7.49398,  2.00000],
         [ 0.00000,  0.00000,  5.00814,  ...,  6.27480,  7.04943,  2.00000],
         [ 0.00000,  0.00000, 12.70904,  ...,  4.81701,  5.14418,  2.00000],
         ...,
         [ 5.00000,  0.00000, 10.32074,  ...,  5.19988,  5.59987,  2.00000],
         [ 5.00000,  0.00000, 31.28691,  ...,  5.79986,  6.66651,  2.00000],
         [ 5.00000,  0.00000, 51.81977,  ...,  5.66653,  5.93320,  2.00000]]], device='cuda:0')
(Pdb) pp t[0,0]
tensor([ 0.00000,  1.00000, 18.38171, 44.93684,  6.90862,  7.49398,  0.00000], device='cuda:0')

獲取擴充套件網格

在yolov5中除了將gt中心點所在網格的anchor匹配為正樣本之外,還會將網格相鄰的上下左右四個網格中的對應anchor作為正樣本。獲取擴充套件網格的規則就是根據中心點距離上下左右哪個更近來確定擴充套件的網格。如下圖中心點更靠近上和右,那麼上和右網格中對應的anchor就會成為正樣本。

獲取擴充套件網格主要分為幾步走:

  1. 獲取所有gt的中心點座標gxy
  2. 獲取中心點座標相對於右下邊界的距離
  3. 計算中心點距離上下左右哪兩個邊界更近
  4. 獲取所有anchor所在的網格,包括gt中心點所在網格和擴充套件網格
gxy = t[:, 2:4]  # grid xy
gxi = gain[[2, 3]] - gxy  # inverse
j, k = ((gxy % 1 < g) & (gxy > 1)).T
"""
(Pdb) ((gxy % 1 < g) & (gxy > 1)).shape
torch.Size([186, 2])
(Pdb) ((gxy % 1 < g) & (gxy > 1)).T.shape
torch.Size([2, 186])
"""
l, m = ((gxi % 1 < g) & (gxi > 1)).T

j = torch.stack((torch.ones_like(j), j, k, l, m))
"""
torch.ones_like(j) 代表gt中心所在grid cell
j, k, l, m 代表擴充套件的上下左右grid cell

torch.Size([5, 51])
"""
t = t.repeat((5, 1, 1))[j]
"""
標籤也重複5次,和上面的擴充套件gird cell一起篩選出所有的,符合條件的grid cell
(Pdb) pp t.shape
torch.Size([153, 7])
(Pdb) t.repeat((5, 1, 1)).shape
torch.Size([5, 153, 7])
(Pdb) pp t.shape
torch.Size([232, 7])
"""
offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j]
"""
計算出所有grid cell的偏移量,作用在標籤上之後就能得到最終的grid cell
(Pdb) pp offsets.shape
torch.Size([529, 2])
"""

gxy 是中心點的座標,中心點座標是相對於整個80*80網格的左上角(0,0)的距離,而gxi是80減去中心點座標,得到的結果相當於是中心點距離(80,80)的距離。將中心點取餘1之後相當於縮放到一個網格中,如上圖所示。

gxy = t[:, 2:4]  # grid xy
gxi = gain[[2, 3]] - gxy  # inverse
j, k = ((gxy % 1 < g) & (gxy > 1)).T

模擬以上操作,j,k得到的是一組布林值

>>> import torch
>>> 
>>> arr = torch.tensor([[1,2,3], [4,5,6]])
>>> one = arr % 2 < 2 
>>> two = arr > 3
>>> one
tensor([[True, True, True],
        [True, True, True]])
>>> two
tensor([[False, False, False],
        [ True,  True,  True]])
>>> one & two
tensor([[False, False, False],
        [ True,  True,  True]])

距離的計算過程:

j, k = ((gxy % 1 < g) & (gxy > 1)).T
"""
(Pdb) ((gxy % 1 < g) & (gxy > 1)).shape
torch.Size([186, 2])
(Pdb) ((gxy % 1 < g) & (gxy > 1)).T.shape
torch.Size([2, 186])
"""
l, m = ((gxi % 1 < g) & (gxi > 1)).T

gxy % 1 < g 代表x或y離左上角距離小於0.5,小於0.5也就意味著靠的更近
gxy > 1 代表x或y必須大於1,x必須大於1也就是說第一行的網格不能向上擴充套件;y必須大於1就是說第一列的網格不能向左擴充套件。

同理gxi是相對下邊和右邊的距離,得到布林張量。

l, m = ((gxi % 1 < g) & (gxi > 1)).T

獲取所有的正樣本網格結果

j = torch.stack((torch.ones_like(j), j, k, l, m))
t = t.repeat((5, 1, 1))[j]

j 儲存上面擴充套件網格和中心點網格的匹配結果,是bool陣列。torch.ones_like(j) 表示中心點匹配到的網格,jklm中儲存的上下左右匹配的網格。
t是將gt中心點的網格複製出來5份,用於計算所有網格。第一份是中心點匹配結果,剩餘四份是上下左右網格匹配結果。
用j來篩選t,最終留下所有選中的網格。

計算出從中心點網格出發到擴充套件網格的需要的偏移量。後續使用使用該偏移量即可獲取所有網格,包括中心點網格和擴充套件網格。計算的過程中涉及到了廣播機制。

offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j]

示例如下:

>>> off
tensor([[ 0,  0],
        [ 1,  0],
        [ 0,  1],
        [-1,  0],
        [ 0, -1]])
>>> arr = torch.tensor([10])
>>> 
>>> 
>>> arr + off
tensor([[10, 10],
        [11, 10],
        [10, 11],
        [ 9, 10],
        [10,  9]])

以下圖為例,視覺化正樣本anchor。
經過mosaic處理的圖片,藍色為標註框

三種尺度下的正樣本網格

三種尺度下的正樣本anchor

三種尺度下原圖的正樣本網格


三種尺度下原圖的anchor

儲存結果

從t中獲取相關資料,包括:

  • bc:image_id, class_id
  • gxy: gt中心點座標
  • gwh: gt寬高
  • a: anchor_id
bc, gxy, gwh, a = t.chunk(4, 1)  # (image, class), grid xy, grid wh, anchors
a, (b, c) = a.long().view(-1), bc.long().T  # anchors, image, class
gij = (gxy - offsets).long()

獲取所有正樣本網格:

gij = (gxy - offsets).long()
gi, gj = gij.T  # grid indices

gxy是gt中心點的座標,減去對應偏移量再取整, 得到所有正樣本所在網格。然後將xy拆分出來得到gi,gj。

(Pdb) pp gij
tensor([[74, 24],
        [37, 28],
        [72,  9],
        [75, 11],
        [67,  5],
        [73,  5],
        [70,  5],
        [75,  1],
        ...)

indices: 儲存圖片,anchor,網格等資訊

# indices 儲存的內容是:image_id, anchor_id(0,1,2), grid x刻度  grid y刻度。這裡的刻度就是正樣本
indices.append((b, a, gj.clamp_(0, shape[2] - 1), gi.clamp_(0, shape[3] - 1)))  # image, anchor, grid
(Pdb) pp a.shape
torch.Size([367])
(Pdb) pp gij.shape
torch.Size([367, 2])

儲存中心點偏移量

# tbox儲存的是gt中心相對於所在grid cell左上角偏移量。也會計算出gt中心相對擴充套件anchor的偏移量
tbox.append(torch.cat((gxy - gij, gwh), 1))  # box

gij是網格起始座標,gxy是gt中心點座標。gxy-gij就是獲取gt中心點相對於網格左上角座標的偏移量。

在後續的損失函式計算中,用這個偏移量和網路預測出來的偏移量計算損失函式。

儲存anchor具體的寬高和類別id

anch.append(anchors[a])  # anchors # 儲存anchor的具體寬高
tcls.append(c)  # class 儲存類別id

自此正樣本篩選的流程就結束了,最終返回了4個張量:

  1. indices 儲存的內容是:image_id, anchor_id, grid x刻度 grid y刻度
  2. tbox 儲存的是gt中心相對於所在grid cell左上角偏移量。也會計算出gt中心相對擴充套件anchor的偏移量
  3. anchors 儲存anchor的具體寬高
  4. class 儲存類別id

返回的正樣本anchor會在後續損失函式的計算中使用。用 indices 儲存的網格篩選出模型輸出的中對應的網格里的內容,用tbox中中心點相對網格的偏移模型輸出的預測中心點相對於網格左上角偏移量計算偏差,並不斷修正。

Q&A

一、正樣本指的是anchor,anchor匹配如何體現在過程?
targets 是這一批圖片的標註資訊,每一行的內容分別是:image, class, x, y, w, h。

(Pdb) pp targets.shape
torch.Size([63, 6])

tensor([[0.00000, 1.00000, 0.22977, 0.56171, 0.08636, 0.09367],
        [0.00000, 0.00000, 0.06260, 0.59307, 0.07843, 0.08812],
        [0.00000, 0.00000, 0.15886, 0.59386, 0.06021, 0.06430],
        [0.00000, 0.00000, 0.31930, 0.58910, 0.06576, 0.09129],
        [0.00000, 0.00000, 0.80959, 0.70458, 0.23025, 0.26275],
        [1.00000, 1.00000, 0.85008, 0.07597, 0.09781, 0.11827],
        [1.00000, 0.00000, 0.22484, 0.09267, 0.14065, 0.18534]

targets = torch.cat((targets.repeat(na, 1, 1), ai[..., None]), 2)
>>>
(Pdb) pp targets.shape
torch.Size([3, 63, 7])

targets儲存的標註資訊,首先將標註資訊複製成三份,因為每一個尺度每一個網格上有三個anchor,相當於給一份標註框分配了一個anchor
在後續的操作中,先透過先將標註框還原到對應的尺度上,透過寬高比篩選anchor,獲得符合正樣本的anchor。到這裡就獲得所有正樣本的anchor。
然後再透過中心點的座標獲得擴充套件網格。

j = torch.stack((torch.ones_like(j), j, k, l, m))
t = t.repeat((5, 1, 1))[j]

此時將t複製5份,每一份的每一行內容代表:image, class, x, y, w, h,anchor_id。
複製的過程中就攜帶了anchor_id的資訊,最終透過擴充套件獲取上下左右兩個網格,相當於獲得了兩個網格中的anchor。
最後將所有的anchor儲存起來,在計算損失函式時使用到anchor的兩個功能:

  1. 使用這些anchor的寬高作為基準,模型輸出的結果是anchor寬高的比例
  2. anchor所在的網格為定位引數提供範圍。網路輸出的xy是相對於網格左上角的偏移

二、 跨anchor匹配體現在哪裡?

targets儲存的標註資訊,首先將標註資訊複製成三份,因為每一個尺度每一個網格上有三個anchor,相當於給一份標註框分配了一個anchor

r = t[..., 4:6] / anchors[:, None]  

# 獲取 寬高比或寬高比倒數 中最大的一個,和0.5比較
j = torch.max(r, 1 / r).max(2)[0] < self.hyp['anchor_t']  # compare

# 將正樣本過濾出來
t = t[j]  # filter

r計算的過程中包含了跨anchor匹配。t是將原有的標註資訊複製了三份,而每一個網格也有三個anchor,也就是說一份標註資訊對應一個anchor。現在計算寬高比獲得的結果只要符合條件的都會認為是正樣本,3種anchor之間互不干擾。
那麼有可能存在的情況是三種anchor和gt的寬高比都符合條件,那麼這3個標註資料都會儲存下來,相應的anchor都會成為正樣本。

三、跨網格匹配體現在哪裡?

所謂跨網格匹配就是除了gt中心點所在網格,還會選擇擴充套件網格。

擴充套件網格的篩選過程就是跨網格匹配的過程

gxy = t[:, 2:4]  # grid xy
gxi = gain[[2, 3]] - gxy  # inverse
j, k = ((gxy % 1 < g) & (gxy > 1)).T
l, m = ((gxi % 1 < g) & (gxi > 1)).T
j = torch.stack((torch.ones_like(j), j, k, l, m))
t = t.repeat((5, 1, 1))[j]

四、跨尺度匹配體現在哪裡?

一個標註框可以在不同的預測分支上匹配上anchor。anchor的匹配在不同的尺度上分開單獨處理,三個尺度互相不干擾,所以一個標註框最多能在三個尺度上都匹配上anchor。

for i in range(self.nl):
    anchors, shape = self.anchors[i], p[i].shape
    ...
    indices.append((b, a, gj.clamp_(0, shape[2] - 1), gi.clamp_(0, shape[3] - 1)))  # image, anchor, grid

    # tbox儲存的是gt中心相對於所在grid cell左上角偏移量。也會計算出gt中心相對擴充套件anchor的偏移量
    tbox.append(torch.cat((gxy - gij, gwh), 1))  # box

可以看到以下三個不同尺度的anchor匹配中,右上角目標都匹配上了。

五、擴充套件的網格中用哪一個anchor?
透過寬高比篩選出來的正樣本才會被複制,也就是說一個網格中的anchor匹配上gt之後,然後才有可能被擴充套件網格選中。
在擴充套件網格之前,就已經篩選出正樣本,有一個確定大小的anchor。擴充套件網格的獲得過程是將正樣本複製5份。複製的過程就將中心點匹配的anchor_id攜帶過去。

j = torch.stack((torch.ones_like(j), j, k, l, m))
t = t.repeat((5, 1, 1))[j]

複製的是正樣本,那麼擴充套件網格最終獲得的也是中心點所在網格上匹配好的anchor
一個網格中有兩個anchor成為正樣本,那麼擴充套件網格中就有兩個anchor為正樣本。擴充套件網格的anchor_id 和中心點網格保持一致。

六、擴充套件網格中gt的偏移量如何計算?
計算gt中心點相對於網格左上角的偏移量中有幾個變數:

  1. gxy: 中心點的座標
  2. gij:網格的起始座標
gij = (gxy - offsets).long()

gij 是透過中心點減去偏移量再取整獲得的

# tbox儲存的是gt中心相對於所在grid cell左上角偏移量。也會計算出gt中心相對擴充套件anchor的偏移量
tbox.append(torch.cat((gxy - gij, gwh), 1))  # box

gxy - gij 的計算過程中,對於那些擴充套件的網格,也會同樣計算偏移量。所以擴充套件網格的偏移量就是網格的左上角到gt中心點的距離。

相關文章