File size: 6,992 Bytes
930daa4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
import torch
import numpy as np
import math

def bbox_bep(box1, box2, xywh=True, eps=1e-7, bep1 = True):
    """
    Calculates bottom edge proximity between two boxes
    
    Input shapes are box1(1,4) to box2(n,4)
    
    Implementation of bep2 from 
        Are object detection assessment criteria ready for maritime computer vision?
    """

    # Get the coordinates of bounding boxes
    if xywh:  # transform from xywh to xyxy
        (x1, y1, w1, h1), (x2, y2, w2, h2) = box1.chunk(4, -1), box2.chunk(4, -1)
        w1_, h1_, w2_, h2_ = w1 / 2, h1 / 2, w2 / 2, h2 / 2
        b1_x1, b1_x2, b1_y1, b1_y2 = x1 - w1_, x1 + w1_, y1 - h1_, y1 + h1_
        b2_x1, b2_x2, b2_y1, b2_y2 = x2 - w2_, x2 + w2_, y2 - h2_, y2 + h2_
    else:  # x1, y1, x2, y2 = box1
        b1_x1, b1_y1, b1_x2, b1_y2 = box1.chunk(4, -1)
        b2_x1, b2_y1, b2_x2, b2_y2 = box2.chunk(4, -1)
        w1, h1 = b1_x2 - b1_x1, (b1_y2 - b1_y1).clamp(eps)
        w2, h2 = b2_x2 - b2_x1, (b2_y2 - b2_y1).clamp(eps)

    # Bottom edge distance (absolute value)
    # xb = torch.abs(b2_x2 - b1_x1)
    xb = torch.min(b2_x2-b1_x1, b1_x2-b2_x1)
    xa = w2 - xb
    xc = w1 - xb
    ybe = torch.abs(b2_y2 - b1_y2)

    X2 = xb/(xb+xa)
    Y2 = 1-ybe/h2

    X1 = xb/(xb+xa+xc+eps)
    Y1 = 1-ybe/(torch.max(h2,h1)+eps)

    bep = X1*Y1 if bep1 else X2*Y2

    return bep

def bbox_iou(box1, box2, xywh=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-7):
    """
    Calculates IoU, GIoU, DIoU, or CIoU between two boxes, supporting xywh/xyxy formats.

    Input shapes are box1(1,4) to box2(n,4).
    """

    # Get the coordinates of bounding boxes
    if xywh:  # transform from xywh to xyxy
        (x1, y1, w1, h1), (x2, y2, w2, h2) = box1.chunk(4, -1), box2.chunk(4, -1)
        w1_, h1_, w2_, h2_ = w1 / 2, h1 / 2, w2 / 2, h2 / 2
        b1_x1, b1_x2, b1_y1, b1_y2 = x1 - w1_, x1 + w1_, y1 - h1_, y1 + h1_
        b2_x1, b2_x2, b2_y1, b2_y2 = x2 - w2_, x2 + w2_, y2 - h2_, y2 + h2_
    else:  # x1, y1, x2, y2 = box1
        b1_x1, b1_y1, b1_x2, b1_y2 = box1.chunk(4, -1)
        b2_x1, b2_y1, b2_x2, b2_y2 = box2.chunk(4, -1)
        w1, h1 = b1_x2 - b1_x1, (b1_y2 - b1_y1).clamp(eps)
        w2, h2 = b2_x2 - b2_x1, (b2_y2 - b2_y1).clamp(eps)

    # Intersection area
    inter = (b1_x2.minimum(b2_x2) - b1_x1.maximum(b2_x1)).clamp(0) * (
        b1_y2.minimum(b2_y2) - b1_y1.maximum(b2_y1)
    ).clamp(0)

    # Union Area
    union = w1 * h1 + w2 * h2 - inter + eps

    # IoU
    iou = inter / union
    if CIoU or DIoU or GIoU:
        cw = b1_x2.maximum(b2_x2) - b1_x1.minimum(b2_x1)  # convex (smallest enclosing box) width
        ch = b1_y2.maximum(b2_y2) - b1_y1.minimum(b2_y1)  # convex height
        if CIoU or DIoU:  # Distance or Complete IoU https://arxiv.org/abs/1911.08287v1
            c2 = cw**2 + ch**2 + eps  # convex diagonal squared
            rho2 = ((b2_x1 + b2_x2 - b1_x1 - b1_x2) ** 2 + (b2_y1 + b2_y2 - b1_y1 - b1_y2) ** 2) / 4  # center dist ** 2
            if CIoU:  # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47
                v = (4 / math.pi**2) * (torch.atan(w2 / h2) - torch.atan(w1 / h1)).pow(2)
                with torch.no_grad():
                    alpha = v / (v - iou + (1 + eps))
                return iou - (rho2 / c2 + v * alpha)  # CIoU
            return iou - rho2 / c2  # DIoU
        c_area = cw * ch + eps  # convex area
        return iou - (c_area - union) / c_area  # GIoU https://arxiv.org/pdf/1902.09630.pdf
    return iou  # IoU

class BoxMetrics:
    # Updated version of https://github.com/kaanakan/object_detection_confusion_matrix
    def __init__(self):
        self.preds_tm = []
        self.target_tm = []
        self.bottom_x = []
        self.bottom_y = []
        self.widths = []
        self.heights = []
        self.ious = []
        self.beps = []

    def add_batch(self, preds, target):
        """
        Return intersection-over-union (Jaccard index) of boxes.
        Both sets of boxes are expected to be in (x1, y1, x2, y2) format.
        Arguments:
            detections torch(Array[N, 6]), x1, y1, x2, y2, conf, class 
            labels torch(Array[M, 5]), class, x1, y1, x2, y2
        Returns:
            None, updates confusion matrix accordingly
        """
        self.preds_tm.extend(preds)
        self.target_tm.extend(target)
    
    def compute(self):
        """
        Computes bbox iou, bep and location/size statistics
        """

        for i in range(len(self.target_tm)):
            target_batch_boxes = self.target_tm[i][:, 1:]
            pred_batch_boxes = self.preds_tm[i][:, :4]

            if pred_batch_boxes.shape[0] == 0:
                continue

            if target_batch_boxes.shape[0] == 0:
                continue

            for t_box in target_batch_boxes:
                    iou = bbox_iou(t_box.unsqueeze(0), pred_batch_boxes, xywh=False)
                    bep = bbox_bep(t_box.unsqueeze(0), pred_batch_boxes, xywh=False)

                    matches = pred_batch_boxes[iou.squeeze(1) > 0.1]

                    bep = bep[iou > 0]
                    iou = iou[iou > 0]
                    # if any iou value is 0 or less, raise error
                    if torch.any(iou <= 0):
                        raise ValueError("IoU values must be greater than 0.")
                    #same for bep
                    if torch.any(bep <= 0):
                        print(t_box.unsqueeze(0))
                        print(pred_batch_boxes)
                        print(bep)
                        print(iou)
                        raise ValueError("BEP values must be greater than 0.")
                    
                    self.ious.extend(iou.tolist())
                    self.beps.extend(bep.tolist())
                    
                    for match in matches:
                        t_xc = (match[0].item()+match[2].item())/2
                        p_xc = (t_box[0].item()+t_box[2].item())/2
                        t_w = t_box[2].item()-t_box[0].item()
                        p_w = match[2].item()-match[0].item()
                        t_h = t_box[3].item()-t_box[1].item()
                        p_h = match[3].item()-match[1].item()

                        self.bottom_x.append(p_xc - t_xc)
                        self.bottom_y.append(match[3].item()-t_box[3].item())
                        self.widths.append(p_w-t_w)
                        self.heights.append(p_h-t_h)

        return {"iou_mean": np.mean(self.ious),
                "bep_mean": np.mean(self.beps),
                "bottom_x_std": np.std(self.bottom_x),
                "bottom_y_std": np.std(self.bottom_y),
                "widths_std": np.std(self.widths),
                "heights_std": np.std(self.heights),
                "bottom_x_mean": np.mean(self.bottom_x),
                "bottom_y_mean": np.mean(self.bottom_y),
                "widths_mean": np.mean(self.widths),
                "heights_mean": np.mean(self.heights)}