Team Ai
Apppublic

Arulkumar03/Fox_Sheep_Detector_Computer_Vision_model

sourceHugging Facemitupdated 3y agoView on Hugging Face
0likes
rotated_boxes.py506 linesDownload Raw Back to structures
1# Copyright (c) Facebook, Inc. and its affiliates.2import math3from typing import List, Tuple4import torch5 6from detectron2.layers.rotated_boxes import pairwise_iou_rotated7 8from .boxes import Boxes9 10 11class RotatedBoxes(Boxes):12    """13    This structure stores a list of rotated boxes as a Nx5 torch.Tensor.14    It supports some common methods about boxes15    (`area`, `clip`, `nonempty`, etc),16    and also behaves like a Tensor17    (support indexing, `to(device)`, `.device`, and iteration over all boxes)18    """19 20    def __init__(self, tensor: torch.Tensor):21        """22        Args:23            tensor (Tensor[float]): a Nx5 matrix.  Each row is24                (x_center, y_center, width, height, angle),25                in which angle is represented in degrees.26                While there's no strict range restriction for it,27                the recommended principal range is between [-180, 180) degrees.28 29        Assume we have a horizontal box B = (x_center, y_center, width, height),30        where width is along the x-axis and height is along the y-axis.31        The rotated box B_rot (x_center, y_center, width, height, angle)32        can be seen as:33 34        1. When angle == 0:35           B_rot == B36        2. When angle > 0:37           B_rot is obtained by rotating B w.r.t its center by :math:`|angle|` degrees CCW;38        3. When angle < 0:39           B_rot is obtained by rotating B w.r.t its center by :math:`|angle|` degrees CW.40 41        Mathematically, since the right-handed coordinate system for image space42        is (y, x), where y is top->down and x is left->right, the 4 vertices of the43        rotated rectangle :math:`(yr_i, xr_i)` (i = 1, 2, 3, 4) can be obtained from44        the vertices of the horizontal rectangle :math:`(y_i, x_i)` (i = 1, 2, 3, 4)45        in the following way (:math:`\\theta = angle*\\pi/180` is the angle in radians,46        :math:`(y_c, x_c)` is the center of the rectangle):47 48        .. math::49 50            yr_i = \\cos(\\theta) (y_i - y_c) - \\sin(\\theta) (x_i - x_c) + y_c,51 52            xr_i = \\sin(\\theta) (y_i - y_c) + \\cos(\\theta) (x_i - x_c) + x_c,53 54        which is the standard rigid-body rotation transformation.55 56        Intuitively, the angle is57        (1) the rotation angle from y-axis in image space58        to the height vector (top->down in the box's local coordinate system)59        of the box in CCW, and60        (2) the rotation angle from x-axis in image space61        to the width vector (left->right in the box's local coordinate system)62        of the box in CCW.63 64        More intuitively, consider the following horizontal box ABCD represented65        in (x1, y1, x2, y2): (3, 2, 7, 4),66        covering the [3, 7] x [2, 4] region of the continuous coordinate system67        which looks like this:68 69        .. code:: none70 71            O--------> x72            |73            |  A---B74            |  |   |75            |  D---C76            |77            v y78 79        Note that each capital letter represents one 0-dimensional geometric point80        instead of a 'square pixel' here.81 82        In the example above, using (x, y) to represent a point we have:83 84        .. math::85 86            O = (0, 0), A = (3, 2), B = (7, 2), C = (7, 4), D = (3, 4)87 88        We name vector AB = vector DC as the width vector in box's local coordinate system, and89        vector AD = vector BC as the height vector in box's local coordinate system. Initially,90        when angle = 0 degree, they're aligned with the positive directions of x-axis and y-axis91        in the image space, respectively.92 93        For better illustration, we denote the center of the box as E,94 95        .. code:: none96 97            O--------> x98            |99            |  A---B100            |  | E |101            |  D---C102            |103            v y104 105        where the center E = ((3+7)/2, (2+4)/2) = (5, 3).106 107        Also,108 109        .. math::110 111            width = |AB| = |CD| = 7 - 3 = 4,112            height = |AD| = |BC| = 4 - 2 = 2.113 114        Therefore, the corresponding representation for the same shape in rotated box in115        (x_center, y_center, width, height, angle) format is:116 117        (5, 3, 4, 2, 0),118 119        Now, let's consider (5, 3, 4, 2, 90), which is rotated by 90 degrees120        CCW (counter-clockwise) by definition. It looks like this:121 122        .. code:: none123 124            O--------> x125            |   B-C126            |   | |127            |   |E|128            |   | |129            |   A-D130            v y131 132        The center E is still located at the same point (5, 3), while the vertices133        ABCD are rotated by 90 degrees CCW with regard to E:134        A = (4, 5), B = (4, 1), C = (6, 1), D = (6, 5)135 136        Here, 90 degrees can be seen as the CCW angle to rotate from y-axis to137        vector AD or vector BC (the top->down height vector in box's local coordinate system),138        or the CCW angle to rotate from x-axis to vector AB or vector DC (the left->right139        width vector in box's local coordinate system).140 141        .. math::142 143            width = |AB| = |CD| = 5 - 1 = 4,144            height = |AD| = |BC| = 6 - 4 = 2.145 146        Next, how about (5, 3, 4, 2, -90), which is rotated by 90 degrees CW (clockwise)147        by definition? It looks like this:148 149        .. code:: none150 151            O--------> x152            |   D-A153            |   | |154            |   |E|155            |   | |156            |   C-B157            v y158 159        The center E is still located at the same point (5, 3), while the vertices160        ABCD are rotated by 90 degrees CW with regard to E:161        A = (6, 1), B = (6, 5), C = (4, 5), D = (4, 1)162 163        .. math::164 165            width = |AB| = |CD| = 5 - 1 = 4,166            height = |AD| = |BC| = 6 - 4 = 2.167 168        This covers exactly the same region as (5, 3, 4, 2, 90) does, and their IoU169        will be 1. However, these two will generate different RoI Pooling results and170        should not be treated as an identical box.171 172        On the other hand, it's easy to see that (X, Y, W, H, A) is identical to173        (X, Y, W, H, A+360N), for any integer N. For example (5, 3, 4, 2, 270) would be174        identical to (5, 3, 4, 2, -90), because rotating the shape 270 degrees CCW is175        equivalent to rotating the same shape 90 degrees CW.176 177        We could rotate further to get (5, 3, 4, 2, 180), or (5, 3, 4, 2, -180):178 179        .. code:: none180 181            O--------> x182            |183            |  C---D184            |  | E |185            |  B---A186            |187            v y188 189        .. math::190 191            A = (7, 4), B = (3, 4), C = (3, 2), D = (7, 2),192 193            width = |AB| = |CD| = 7 - 3 = 4,194            height = |AD| = |BC| = 4 - 2 = 2.195 196        Finally, this is a very inaccurate (heavily quantized) illustration of197        how (5, 3, 4, 2, 60) looks like in case anyone wonders:198 199        .. code:: none200 201            O--------> x202            |     B\203            |    /  C204            |   /E /205            |  A  /206            |   `D207            v y208 209        It's still a rectangle with center of (5, 3), width of 4 and height of 2,210        but its angle (and thus orientation) is somewhere between211        (5, 3, 4, 2, 0) and (5, 3, 4, 2, 90).212        """213        device = tensor.device if isinstance(tensor, torch.Tensor) else torch.device("cpu")214        tensor = torch.as_tensor(tensor, dtype=torch.float32, device=device)215        if tensor.numel() == 0:216            # Use reshape, so we don't end up creating a new tensor that does not depend on217            # the inputs (and consequently confuses jit)218            tensor = tensor.reshape((0, 5)).to(dtype=torch.float32, device=device)219        assert tensor.dim() == 2 and tensor.size(-1) == 5, tensor.size()220 221        self.tensor = tensor222 223    def clone(self) -> "RotatedBoxes":224        """225        Clone the RotatedBoxes.226 227        Returns:228            RotatedBoxes229        """230        return RotatedBoxes(self.tensor.clone())231 232    def to(self, device: torch.device):233        # Boxes are assumed float32 and does not support to(dtype)234        return RotatedBoxes(self.tensor.to(device=device))235 236    def area(self) -> torch.Tensor:237        """238        Computes the area of all the boxes.239 240        Returns:241            torch.Tensor: a vector with areas of each box.242        """243        box = self.tensor244        area = box[:, 2] * box[:, 3]245        return area246 247    # Avoid in-place operations so that we can torchscript; NOTE: this creates a new tensor248    def normalize_angles(self) -> None:249        """250        Restrict angles to the range of [-180, 180) degrees251        """252        angle_tensor = (self.tensor[:, 4] + 180.0) % 360.0 - 180.0253        self.tensor = torch.cat((self.tensor[:, :4], angle_tensor[:, None]), dim=1)254 255    def clip(self, box_size: Tuple[int, int], clip_angle_threshold: float = 1.0) -> None:256        """257        Clip (in place) the boxes by limiting x coordinates to the range [0, width]258        and y coordinates to the range [0, height].259 260        For RRPN:261        Only clip boxes that are almost horizontal with a tolerance of262        clip_angle_threshold to maintain backward compatibility.263 264        Rotated boxes beyond this threshold are not clipped for two reasons:265 266        1. There are potentially multiple ways to clip a rotated box to make it267           fit within the image.268        2. It's tricky to make the entire rectangular box fit within the image269           and still be able to not leave out pixels of interest.270 271        Therefore we rely on ops like RoIAlignRotated to safely handle this.272 273        Args:274            box_size (height, width): The clipping box's size.275            clip_angle_threshold:276                Iff. abs(normalized(angle)) <= clip_angle_threshold (in degrees),277                we do the clipping as horizontal boxes.278        """279        h, w = box_size280 281        # normalize angles to be within (-180, 180] degrees282        self.normalize_angles()283 284        idx = torch.where(torch.abs(self.tensor[:, 4]) <= clip_angle_threshold)[0]285 286        # convert to (x1, y1, x2, y2)287        x1 = self.tensor[idx, 0] - self.tensor[idx, 2] / 2.0288        y1 = self.tensor[idx, 1] - self.tensor[idx, 3] / 2.0289        x2 = self.tensor[idx, 0] + self.tensor[idx, 2] / 2.0290        y2 = self.tensor[idx, 1] + self.tensor[idx, 3] / 2.0291 292        # clip293        x1.clamp_(min=0, max=w)294        y1.clamp_(min=0, max=h)295        x2.clamp_(min=0, max=w)296        y2.clamp_(min=0, max=h)297 298        # convert back to (xc, yc, w, h)299        self.tensor[idx, 0] = (x1 + x2) / 2.0300        self.tensor[idx, 1] = (y1 + y2) / 2.0301        # make sure widths and heights do not increase due to numerical errors302        self.tensor[idx, 2] = torch.min(self.tensor[idx, 2], x2 - x1)303        self.tensor[idx, 3] = torch.min(self.tensor[idx, 3], y2 - y1)304 305    def nonempty(self, threshold: float = 0.0) -> torch.Tensor:306        """307        Find boxes that are non-empty.308        A box is considered empty, if either of its side is no larger than threshold.309 310        Returns:311            Tensor: a binary vector which represents312            whether each box is empty (False) or non-empty (True).313        """314        box = self.tensor315        widths = box[:, 2]316        heights = box[:, 3]317        keep = (widths > threshold) & (heights > threshold)318        return keep319 320    def __getitem__(self, item) -> "RotatedBoxes":321        """322        Returns:323            RotatedBoxes: Create a new :class:`RotatedBoxes` by indexing.324 325        The following usage are allowed:326 327        1. `new_boxes = boxes[3]`: return a `RotatedBoxes` which contains only one box.328        2. `new_boxes = boxes[2:10]`: return a slice of boxes.329        3. `new_boxes = boxes[vector]`, where vector is a torch.ByteTensor330           with `length = len(boxes)`. Nonzero elements in the vector will be selected.331 332        Note that the returned RotatedBoxes might share storage with this RotatedBoxes,333        subject to Pytorch's indexing semantics.334        """335        if isinstance(item, int):336            return RotatedBoxes(self.tensor[item].view(1, -1))337        b = self.tensor[item]338        assert b.dim() == 2, "Indexing on RotatedBoxes with {} failed to return a matrix!".format(339            item340        )341        return RotatedBoxes(b)342 343    def __len__(self) -> int:344        return self.tensor.shape[0]345 346    def __repr__(self) -> str:347        return "RotatedBoxes(" + str(self.tensor) + ")"348 349    def inside_box(self, box_size: Tuple[int, int], boundary_threshold: int = 0) -> torch.Tensor:350        """351        Args:352            box_size (height, width): Size of the reference box covering353                [0, width] x [0, height]354            boundary_threshold (int): Boxes that extend beyond the reference box355                boundary by more than boundary_threshold are considered "outside".356 357        For RRPN, it might not be necessary to call this function since it's common358        for rotated box to extend to outside of the image boundaries359        (the clip function only clips the near-horizontal boxes)360 361        Returns:362            a binary vector, indicating whether each box is inside the reference box.363        """364        height, width = box_size365 366        cnt_x = self.tensor[..., 0]367        cnt_y = self.tensor[..., 1]368        half_w = self.tensor[..., 2] / 2.0369        half_h = self.tensor[..., 3] / 2.0370        a = self.tensor[..., 4]371        c = torch.abs(torch.cos(a * math.pi / 180.0))372        s = torch.abs(torch.sin(a * math.pi / 180.0))373        # This basically computes the horizontal bounding rectangle of the rotated box374        max_rect_dx = c * half_w + s * half_h375        max_rect_dy = c * half_h + s * half_w376 377        inds_inside = (378            (cnt_x - max_rect_dx >= -boundary_threshold)379            & (cnt_y - max_rect_dy >= -boundary_threshold)380            & (cnt_x + max_rect_dx < width + boundary_threshold)381            & (cnt_y + max_rect_dy < height + boundary_threshold)382        )383 384        return inds_inside385 386    def get_centers(self) -> torch.Tensor:387        """388        Returns:389            The box centers in a Nx2 array of (x, y).390        """391        return self.tensor[:, :2]392 393    def scale(self, scale_x: float, scale_y: float) -> None:394        """395        Scale the rotated box with horizontal and vertical scaling factors396        Note: when scale_factor_x != scale_factor_y,397        the rotated box does not preserve the rectangular shape when the angle398        is not a multiple of 90 degrees under resize transformation.399        Instead, the shape is a parallelogram (that has skew)400        Here we make an approximation by fitting a rotated rectangle to the parallelogram.401        """402        self.tensor[:, 0] *= scale_x403        self.tensor[:, 1] *= scale_y404        theta = self.tensor[:, 4] * math.pi / 180.0405        c = torch.cos(theta)406        s = torch.sin(theta)407 408        # In image space, y is top->down and x is left->right409        # Consider the local coordintate system for the rotated box,410        # where the box center is located at (0, 0), and the four vertices ABCD are411        # A(-w / 2, -h / 2), B(w / 2, -h / 2), C(w / 2, h / 2), D(-w / 2, h / 2)412        # the midpoint of the left edge AD of the rotated box E is:413        # E = (A+D)/2 = (-w / 2, 0)414        # the midpoint of the top edge AB of the rotated box F is:415        # F(0, -h / 2)416        # To get the old coordinates in the global system, apply the rotation transformation417        # (Note: the right-handed coordinate system for image space is yOx):418        # (old_x, old_y) = (s * y + c * x, c * y - s * x)419        # E(old) = (s * 0 + c * (-w/2), c * 0 - s * (-w/2)) = (-c * w / 2, s * w / 2)420        # F(old) = (s * (-h / 2) + c * 0, c * (-h / 2) - s * 0) = (-s * h / 2, -c * h / 2)421        # After applying the scaling factor (sfx, sfy):422        # E(new) = (-sfx * c * w / 2, sfy * s * w / 2)423        # F(new) = (-sfx * s * h / 2, -sfy * c * h / 2)424        # The new width after scaling tranformation becomes:425 426        # w(new) = |E(new) - O| * 2427        #        = sqrt[(sfx * c * w / 2)^2 + (sfy * s * w / 2)^2] * 2428        #        = sqrt[(sfx * c)^2 + (sfy * s)^2] * w429        # i.e., scale_factor_w = sqrt[(sfx * c)^2 + (sfy * s)^2]430        #431        # For example,432        # when angle = 0 or 180, |c| = 1, s = 0, scale_factor_w == scale_factor_x;433        # when |angle| = 90, c = 0, |s| = 1, scale_factor_w == scale_factor_y434        self.tensor[:, 2] *= torch.sqrt((scale_x * c) ** 2 + (scale_y * s) ** 2)435 436        # h(new) = |F(new) - O| * 2437        #        = sqrt[(sfx * s * h / 2)^2 + (sfy * c * h / 2)^2] * 2438        #        = sqrt[(sfx * s)^2 + (sfy * c)^2] * h439        # i.e., scale_factor_h = sqrt[(sfx * s)^2 + (sfy * c)^2]440        #441        # For example,442        # when angle = 0 or 180, |c| = 1, s = 0, scale_factor_h == scale_factor_y;443        # when |angle| = 90, c = 0, |s| = 1, scale_factor_h == scale_factor_x444        self.tensor[:, 3] *= torch.sqrt((scale_x * s) ** 2 + (scale_y * c) ** 2)445 446        # The angle is the rotation angle from y-axis in image space to the height447        # vector (top->down in the box's local coordinate system) of the box in CCW.448        #449        # angle(new) = angle_yOx(O - F(new))450        #            = angle_yOx( (sfx * s * h / 2, sfy * c * h / 2) )451        #            = atan2(sfx * s * h / 2, sfy * c * h / 2)452        #            = atan2(sfx * s, sfy * c)453        #454        # For example,455        # when sfx == sfy, angle(new) == atan2(s, c) == angle(old)456        self.tensor[:, 4] = torch.atan2(scale_x * s, scale_y * c) * 180 / math.pi457 458    @classmethod459    def cat(cls, boxes_list: List["RotatedBoxes"]) -> "RotatedBoxes":460        """461        Concatenates a list of RotatedBoxes into a single RotatedBoxes462 463        Arguments:464            boxes_list (list[RotatedBoxes])465 466        Returns:467            RotatedBoxes: the concatenated RotatedBoxes468        """469        assert isinstance(boxes_list, (list, tuple))470        if len(boxes_list) == 0:471            return cls(torch.empty(0))472        assert all([isinstance(box, RotatedBoxes) for box in boxes_list])473 474        # use torch.cat (v.s. layers.cat) so the returned boxes never share storage with input475        cat_boxes = cls(torch.cat([b.tensor for b in boxes_list], dim=0))476        return cat_boxes477 478    @property479    def device(self) -> torch.device:480        return self.tensor.device481 482    @torch.jit.unused483    def __iter__(self):484        """485        Yield a box as a Tensor of shape (5,) at a time.486        """487        yield from self.tensor488 489 490def pairwise_iou(boxes1: RotatedBoxes, boxes2: RotatedBoxes) -> None:491    """492    Given two lists of rotated boxes of size N and M,493    compute the IoU (intersection over union)494    between **all** N x M pairs of boxes.495    The box order must be (x_center, y_center, width, height, angle).496 497    Args:498        boxes1, boxes2 (RotatedBoxes):499            two `RotatedBoxes`. Contains N & M rotated boxes, respectively.500 501    Returns:502        Tensor: IoU, sized [N,M].503    """504 505    return pairwise_iou_rotated(boxes1.tensor, boxes2.tensor)506