|
|
| import math
|
| import numpy as np
|
| from enum import IntEnum, unique
|
| from typing import List, Tuple, Union
|
| import torch
|
| from torch import device
|
|
|
| _RawBoxType = Union[List[float], Tuple[float, ...], torch.Tensor, np.ndarray]
|
|
|
|
|
| @unique
|
| class BoxMode(IntEnum):
|
| """
|
| Enum of different ways to represent a box.
|
| """
|
|
|
| XYXY_ABS = 0
|
| """
|
| (x0, y0, x1, y1) in absolute floating points coordinates.
|
| The coordinates in range [0, width or height].
|
| """
|
| XYWH_ABS = 1
|
| """
|
| (x0, y0, w, h) in absolute floating points coordinates.
|
| """
|
| XYXY_REL = 2
|
| """
|
| Not yet supported!
|
| (x0, y0, x1, y1) in range [0, 1]. They are relative to the size of the image.
|
| """
|
| XYWH_REL = 3
|
| """
|
| Not yet supported!
|
| (x0, y0, w, h) in range [0, 1]. They are relative to the size of the image.
|
| """
|
| XYWHA_ABS = 4
|
| """
|
| (xc, yc, w, h, a) in absolute floating points coordinates.
|
| (xc, yc) is the center of the rotated box, and the angle a is in degrees ccw.
|
| """
|
|
|
| @staticmethod
|
| def convert(box: _RawBoxType, from_mode: "BoxMode", to_mode: "BoxMode") -> _RawBoxType:
|
| """
|
| Args:
|
| box: can be a k-tuple, k-list or an Nxk array/tensor, where k = 4 or 5
|
| from_mode, to_mode (BoxMode)
|
|
|
| Returns:
|
| The converted box of the same type.
|
| """
|
| if from_mode == to_mode:
|
| return box
|
|
|
| original_type = type(box)
|
| is_numpy = isinstance(box, np.ndarray)
|
| single_box = isinstance(box, (list, tuple))
|
| if single_box:
|
| assert len(box) == 4 or len(box) == 5, (
|
| "BoxMode.convert takes either a k-tuple/list or an Nxk array/tensor,"
|
| " where k == 4 or 5"
|
| )
|
| arr = torch.tensor(box)[None, :]
|
| else:
|
|
|
| if is_numpy:
|
| arr = torch.from_numpy(np.asarray(box)).clone()
|
| else:
|
| arr = box.clone()
|
|
|
| assert to_mode not in [BoxMode.XYXY_REL, BoxMode.XYWH_REL] and from_mode not in [
|
| BoxMode.XYXY_REL,
|
| BoxMode.XYWH_REL,
|
| ], "Relative mode not yet supported!"
|
|
|
| if from_mode == BoxMode.XYWHA_ABS and to_mode == BoxMode.XYXY_ABS:
|
| assert (
|
| arr.shape[-1] == 5
|
| ), "The last dimension of input shape must be 5 for XYWHA format"
|
| original_dtype = arr.dtype
|
| arr = arr.double()
|
|
|
| w = arr[:, 2]
|
| h = arr[:, 3]
|
| a = arr[:, 4]
|
| c = torch.abs(torch.cos(a * math.pi / 180.0))
|
| s = torch.abs(torch.sin(a * math.pi / 180.0))
|
|
|
| new_w = c * w + s * h
|
| new_h = c * h + s * w
|
|
|
|
|
| arr[:, 0] -= new_w / 2.0
|
| arr[:, 1] -= new_h / 2.0
|
|
|
| arr[:, 2] = arr[:, 0] + new_w
|
| arr[:, 3] = arr[:, 1] + new_h
|
|
|
| arr = arr[:, :4].to(dtype=original_dtype)
|
| elif from_mode == BoxMode.XYWH_ABS and to_mode == BoxMode.XYWHA_ABS:
|
| original_dtype = arr.dtype
|
| arr = arr.double()
|
| arr[:, 0] += arr[:, 2] / 2.0
|
| arr[:, 1] += arr[:, 3] / 2.0
|
| angles = torch.zeros((arr.shape[0], 1), dtype=arr.dtype)
|
| arr = torch.cat((arr, angles), axis=1).to(dtype=original_dtype)
|
| else:
|
| if to_mode == BoxMode.XYXY_ABS and from_mode == BoxMode.XYWH_ABS:
|
| arr[:, 2] += arr[:, 0]
|
| arr[:, 3] += arr[:, 1]
|
| elif from_mode == BoxMode.XYXY_ABS and to_mode == BoxMode.XYWH_ABS:
|
| arr[:, 2] -= arr[:, 0]
|
| arr[:, 3] -= arr[:, 1]
|
| else:
|
| raise NotImplementedError(
|
| "Conversion from BoxMode {} to {} is not supported yet".format(
|
| from_mode, to_mode
|
| )
|
| )
|
|
|
| if single_box:
|
| return original_type(arr.flatten().tolist())
|
| if is_numpy:
|
| return arr.numpy()
|
| else:
|
| return arr
|
|
|
|
|
| class Boxes:
|
| """
|
| This structure stores a list of boxes as a Nx4 torch.Tensor.
|
| It supports some common methods about boxes
|
| (`area`, `clip`, `nonempty`, etc),
|
| and also behaves like a Tensor
|
| (support indexing, `to(device)`, `.device`, and iteration over all boxes)
|
|
|
| Attributes:
|
| tensor (torch.Tensor): float matrix of Nx4. Each row is (x1, y1, x2, y2).
|
| """
|
|
|
| def __init__(self, tensor: torch.Tensor):
|
| """
|
| Args:
|
| tensor (Tensor[float]): a Nx4 matrix. Each row is (x1, y1, x2, y2).
|
| """
|
| if not isinstance(tensor, torch.Tensor):
|
| tensor = torch.as_tensor(tensor, dtype=torch.float32, device=torch.device("cpu"))
|
| else:
|
| tensor = tensor.to(torch.float32)
|
| if tensor.numel() == 0:
|
|
|
|
|
| tensor = tensor.reshape((-1, 4)).to(dtype=torch.float32)
|
| assert tensor.dim() == 2 and tensor.size(-1) == 4, tensor.size()
|
|
|
| self.tensor = tensor
|
|
|
| def clone(self) -> "Boxes":
|
| """
|
| Clone the Boxes.
|
|
|
| Returns:
|
| Boxes
|
| """
|
| return Boxes(self.tensor.clone())
|
|
|
| def to(self, device: torch.device):
|
|
|
| return Boxes(self.tensor.to(device=device))
|
|
|
| def area(self) -> torch.Tensor:
|
| """
|
| Computes the area of all the boxes.
|
|
|
| Returns:
|
| torch.Tensor: a vector with areas of each box.
|
| """
|
| box = self.tensor
|
| area = (box[:, 2] - box[:, 0]) * (box[:, 3] - box[:, 1])
|
| return area
|
|
|
| def clip(self, box_size: Tuple[int, int]) -> None:
|
| """
|
| Clip (in place) the boxes by limiting x coordinates to the range [0, width]
|
| and y coordinates to the range [0, height].
|
|
|
| Args:
|
| box_size (height, width): The clipping box's size.
|
| """
|
| assert torch.isfinite(self.tensor).all(), "Box tensor contains infinite or NaN!"
|
| h, w = box_size
|
| x1 = self.tensor[:, 0].clamp(min=0, max=w)
|
| y1 = self.tensor[:, 1].clamp(min=0, max=h)
|
| x2 = self.tensor[:, 2].clamp(min=0, max=w)
|
| y2 = self.tensor[:, 3].clamp(min=0, max=h)
|
| self.tensor = torch.stack((x1, y1, x2, y2), dim=-1)
|
|
|
| def nonempty(self, threshold: float = 0.0) -> torch.Tensor:
|
| """
|
| Find boxes that are non-empty.
|
| A box is considered empty, if either of its side is no larger than threshold.
|
|
|
| Returns:
|
| Tensor:
|
| a binary vector which represents whether each box is empty
|
| (False) or non-empty (True).
|
| """
|
| box = self.tensor
|
| widths = box[:, 2] - box[:, 0]
|
| heights = box[:, 3] - box[:, 1]
|
| keep = (widths > threshold) & (heights > threshold)
|
| return keep
|
|
|
| def __getitem__(self, item) -> "Boxes":
|
| """
|
| Args:
|
| item: int, slice, or a BoolTensor
|
|
|
| Returns:
|
| Boxes: Create a new :class:`Boxes` by indexing.
|
|
|
| The following usage are allowed:
|
|
|
| 1. `new_boxes = boxes[3]`: return a `Boxes` which contains only one box.
|
| 2. `new_boxes = boxes[2:10]`: return a slice of boxes.
|
| 3. `new_boxes = boxes[vector]`, where vector is a torch.BoolTensor
|
| with `length = len(boxes)`. Nonzero elements in the vector will be selected.
|
|
|
| Note that the returned Boxes might share storage with this Boxes,
|
| subject to Pytorch's indexing semantics.
|
| """
|
| if isinstance(item, int):
|
| return Boxes(self.tensor[item].view(1, -1))
|
| b = self.tensor[item]
|
| assert b.dim() == 2, "Indexing on Boxes with {} failed to return a matrix!".format(item)
|
| return Boxes(b)
|
|
|
| def __len__(self) -> int:
|
| return self.tensor.shape[0]
|
|
|
| def __repr__(self) -> str:
|
| return "Boxes(" + str(self.tensor) + ")"
|
|
|
| def inside_box(self, box_size: Tuple[int, int], boundary_threshold: int = 0) -> torch.Tensor:
|
| """
|
| Args:
|
| box_size (height, width): Size of the reference box.
|
| boundary_threshold (int): Boxes that extend beyond the reference box
|
| boundary by more than boundary_threshold are considered "outside".
|
|
|
| Returns:
|
| a binary vector, indicating whether each box is inside the reference box.
|
| """
|
| height, width = box_size
|
| inds_inside = (
|
| (self.tensor[..., 0] >= -boundary_threshold)
|
| & (self.tensor[..., 1] >= -boundary_threshold)
|
| & (self.tensor[..., 2] < width + boundary_threshold)
|
| & (self.tensor[..., 3] < height + boundary_threshold)
|
| )
|
| return inds_inside
|
|
|
| def get_centers(self) -> torch.Tensor:
|
| """
|
| Returns:
|
| The box centers in a Nx2 array of (x, y).
|
| """
|
| return (self.tensor[:, :2] + self.tensor[:, 2:]) / 2
|
|
|
| def scale(self, scale_x: float, scale_y: float) -> None:
|
| """
|
| Scale the box with horizontal and vertical scaling factors
|
| """
|
| self.tensor[:, 0::2] *= scale_x
|
| self.tensor[:, 1::2] *= scale_y
|
|
|
| @classmethod
|
| def cat(cls, boxes_list: List["Boxes"]) -> "Boxes":
|
| """
|
| Concatenates a list of Boxes into a single Boxes
|
|
|
| Arguments:
|
| boxes_list (list[Boxes])
|
|
|
| Returns:
|
| Boxes: the concatenated Boxes
|
| """
|
| assert isinstance(boxes_list, (list, tuple))
|
| if len(boxes_list) == 0:
|
| return cls(torch.empty(0))
|
| assert all([isinstance(box, Boxes) for box in boxes_list])
|
|
|
|
|
| cat_boxes = cls(torch.cat([b.tensor for b in boxes_list], dim=0))
|
| return cat_boxes
|
|
|
| @property
|
| def device(self) -> device:
|
| return self.tensor.device
|
|
|
|
|
|
|
| @torch.jit.unused
|
| def __iter__(self):
|
| """
|
| Yield a box as a Tensor of shape (4,) at a time.
|
| """
|
| yield from self.tensor
|
|
|
|
|
| def pairwise_intersection(boxes1: Boxes, boxes2: Boxes) -> torch.Tensor:
|
| """
|
| Given two lists of boxes of size N and M,
|
| compute the intersection area between __all__ N x M pairs of boxes.
|
| The box order must be (xmin, ymin, xmax, ymax)
|
|
|
| Args:
|
| boxes1,boxes2 (Boxes): two `Boxes`. Contains N & M boxes, respectively.
|
|
|
| Returns:
|
| Tensor: intersection, sized [N,M].
|
| """
|
| boxes1, boxes2 = boxes1.tensor, boxes2.tensor
|
| width_height = torch.min(boxes1[:, None, 2:], boxes2[:, 2:]) - torch.max(
|
| boxes1[:, None, :2], boxes2[:, :2]
|
| )
|
|
|
| width_height.clamp_(min=0)
|
| intersection = width_height.prod(dim=2)
|
| return intersection
|
|
|
|
|
|
|
|
|
| def pairwise_iou(boxes1: Boxes, boxes2: Boxes) -> torch.Tensor:
|
| """
|
| Given two lists of boxes of size N and M, compute the IoU
|
| (intersection over union) between **all** N x M pairs of boxes.
|
| The box order must be (xmin, ymin, xmax, ymax).
|
|
|
| Args:
|
| boxes1,boxes2 (Boxes): two `Boxes`. Contains N & M boxes, respectively.
|
|
|
| Returns:
|
| Tensor: IoU, sized [N,M].
|
| """
|
| area1 = boxes1.area()
|
| area2 = boxes2.area()
|
| inter = pairwise_intersection(boxes1, boxes2)
|
|
|
|
|
| iou = torch.where(
|
| inter > 0,
|
| inter / (area1[:, None] + area2 - inter),
|
| torch.zeros(1, dtype=inter.dtype, device=inter.device),
|
| )
|
| return iou
|
|
|
|
|
| def pairwise_ioa(boxes1: Boxes, boxes2: Boxes) -> torch.Tensor:
|
| """
|
| Similar to :func:`pariwise_iou` but compute the IoA (intersection over boxes2 area).
|
|
|
| Args:
|
| boxes1,boxes2 (Boxes): two `Boxes`. Contains N & M boxes, respectively.
|
|
|
| Returns:
|
| Tensor: IoA, sized [N,M].
|
| """
|
| area2 = boxes2.area()
|
| inter = pairwise_intersection(boxes1, boxes2)
|
|
|
|
|
| ioa = torch.where(
|
| inter > 0, inter / area2, torch.zeros(1, dtype=inter.dtype, device=inter.device)
|
| )
|
| return ioa
|
|
|
|
|
| def pairwise_point_box_distance(points: torch.Tensor, boxes: Boxes):
|
| """
|
| Pairwise distance between N points and M boxes. The distance between a
|
| point and a box is represented by the distance from the point to 4 edges
|
| of the box. Distances are all positive when the point is inside the box.
|
|
|
| Args:
|
| points: Nx2 coordinates. Each row is (x, y)
|
| boxes: M boxes
|
|
|
| Returns:
|
| Tensor: distances of size (N, M, 4). The 4 values are distances from
|
| the point to the left, top, right, bottom of the box.
|
| """
|
| x, y = points.unsqueeze(dim=2).unbind(dim=1)
|
| x0, y0, x1, y1 = boxes.tensor.unsqueeze(dim=0).unbind(dim=2)
|
| return torch.stack([x - x0, y - y0, x1 - x, y1 - y], dim=2)
|
|
|
|
|
| def matched_pairwise_iou(boxes1: Boxes, boxes2: Boxes) -> torch.Tensor:
|
| """
|
| Compute pairwise intersection over union (IOU) of two sets of matched
|
| boxes that have the same number of boxes.
|
| Similar to :func:`pairwise_iou`, but computes only diagonal elements of the matrix.
|
|
|
| Args:
|
| boxes1 (Boxes): bounding boxes, sized [N,4].
|
| boxes2 (Boxes): same length as boxes1
|
| Returns:
|
| Tensor: iou, sized [N].
|
| """
|
| assert len(boxes1) == len(
|
| boxes2
|
| ), "boxlists should have the same" "number of entries, got {}, {}".format(
|
| len(boxes1), len(boxes2)
|
| )
|
| area1 = boxes1.area()
|
| area2 = boxes2.area()
|
| box1, box2 = boxes1.tensor, boxes2.tensor
|
| lt = torch.max(box1[:, :2], box2[:, :2])
|
| rb = torch.min(box1[:, 2:], box2[:, 2:])
|
| wh = (rb - lt).clamp(min=0)
|
| inter = wh[:, 0] * wh[:, 1]
|
| iou = inter / (area1 + area2 - inter)
|
| return iou
|
|
|