| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | """Utilities for boxes. |
| | |
| | Axis-aligned utils implemented based on: |
| | https://github.com/facebookresearch/detr/blob/master/util/box_ops.py. |
| | |
| | Rotated box utils implemented based on: |
| | https://github.com/lilanxiao/Rotated_IoU. |
| | """ |
| | from typing import Any, Union |
| |
|
| | import jax.numpy as jnp |
| | import numpy as np |
| |
|
| | PyModule = Any |
| | Array = Union[jnp.ndarray, np.ndarray] |
| |
|
| |
|
| | def box_cxcywh_to_xyxy(x: Array, np_backbone: PyModule = jnp) -> Array: |
| | """Converts boxes from [cx, cy, w, h] format into [x, y, x', y'] format.""" |
| | x_c, y_c, w, h = np_backbone.split(x, 4, axis=-1) |
| | b = [(x_c - 0.5 * w), (y_c - 0.5 * h), (x_c + 0.5 * w), (y_c + 0.5 * h)] |
| | return np_backbone.concatenate(b, axis=-1) |
| |
|
| |
|
| | def box_cxcywh_to_yxyx(x: Array, np_backbone: PyModule = jnp) -> Array: |
| | """Converts boxes from [cx, cy, w, h] format into [y, x, y', x'] format.""" |
| | x_c, y_c, w, h = np_backbone.split(x, 4, axis=-1) |
| | b = [(y_c - 0.5 * h), (x_c - 0.5 * w), (y_c + 0.5 * h), (x_c + 0.5 * w)] |
| | return np_backbone.concatenate(b, axis=-1) |
| |
|
| |
|
| | def box_xyxy_to_cxcywh(x: Array, np_backbone: PyModule = jnp) -> Array: |
| | """Converts boxes from [x, y, x', y'] format into [cx, cy, w, h] format.""" |
| | x0, y0, x1, y1 = np_backbone.split(x, 4, axis=-1) |
| | b = [(x0 + x1) / 2, (y0 + y1) / 2, (x1 - x0), (y1 - y0)] |
| | return np_backbone.concatenate(b, axis=-1) |
| |
|
| |
|
| | def box_yxyx_to_cxcywh(x: Array, np_backbone: PyModule = jnp) -> Array: |
| | """Converts boxes from [y, x, y', x'] format into [cx, cy, w, h] format.""" |
| | y0, x0, y1, x1 = np_backbone.split(x, 4, axis=-1) |
| | b = [(x0 + x1) / 2, (y0 + y1) / 2, (x1 - x0), (y1 - y0)] |
| | return np_backbone.concatenate(b, axis=-1) |
| |
|
| |
|
| | def box_iou(boxes1: Array, |
| | boxes2: Array, |
| | np_backbone: PyModule = jnp, |
| | all_pairs: bool = True, |
| | eps: float = 1e-6) -> Array: |
| | """Computes IoU between two sets of boxes. |
| | |
| | Boxes are in [x, y, x', y'] format [x, y] is top-left, [x', y'] is bottom |
| | right. |
| | |
| | Args: |
| | boxes1: Predicted bounding-boxes in shape [bs, n, 4]. |
| | boxes2: Target bounding-boxes in shape [bs, m, 4]. Can have a different |
| | number of boxes if all_pairs is True. |
| | np_backbone: numpy module: Either the regular numpy package or jax.numpy. |
| | all_pairs: Whether to compute IoU between all pairs of boxes or not. |
| | eps: Epsilon for numerical stability. |
| | |
| | Returns: |
| | If all_pairs == True, returns the pairwise IoU cost matrix of shape |
| | [bs, n, m]. If all_pairs == False, returns the IoU between corresponding |
| | boxes. The shape of the return value is then [bs, n]. |
| | """ |
| |
|
| | |
| | wh1 = boxes1[..., 2:] - boxes1[..., :2] |
| | area1 = wh1[..., 0] * wh1[..., 1] |
| |
|
| | wh2 = boxes2[..., 2:] - boxes2[..., :2] |
| | area2 = wh2[..., 0] * wh2[..., 1] |
| |
|
| | if all_pairs: |
| | |
| | |
| | lt = np_backbone.maximum(boxes1[..., :, None, :2], |
| | boxes2[..., None, :, :2]) |
| | rb = np_backbone.minimum(boxes1[..., :, None, 2:], |
| | boxes2[..., None, :, 2:]) |
| |
|
| | |
| | wh = (rb - lt).clip(0.0) |
| | intersection = wh[..., 0] * wh[..., 1] |
| |
|
| | |
| | union = area1[..., :, None] + area2[..., None, :] - intersection |
| |
|
| | iou = intersection / (union + eps) |
| |
|
| | else: |
| | |
| | |
| | assert boxes1.shape[1] == boxes2.shape[1], ( |
| | 'Different number of boxes when all_pairs is False') |
| | lt = np_backbone.maximum(boxes1[..., :, :2], |
| | boxes2[..., :, :2]) |
| | rb = np_backbone.minimum(boxes1[..., :, 2:], boxes2[..., :, |
| | 2:]) |
| |
|
| | |
| | wh = (rb - lt).clip(0.0) |
| | intersection = wh[..., :, 0] * wh[..., :, 1] |
| |
|
| | |
| | union = area1 + area2 - intersection |
| |
|
| | |
| | iou = intersection / (union + eps) |
| |
|
| | return iou, union |
| |
|
| |
|
| | def generalized_box_iou(boxes1: Array, |
| | boxes2: Array, |
| | np_backbone: PyModule = jnp, |
| | all_pairs: bool = True, |
| | eps: float = 1e-6) -> Array: |
| | """Generalized IoU from https://giou.stanford.edu/. |
| | |
| | The boxes should be in [x, y, x', y'] format specifying top-left and |
| | bottom-right corners. |
| | |
| | Args: |
| | boxes1: Predicted bounding-boxes in shape [..., n, 4]. |
| | boxes2: Target bounding-boxes in shape [..., m, 4]. |
| | np_backbone: Numpy module: Either the regular numpy package or jax.numpy. |
| | all_pairs: Whether to compute generalized IoU from between all-pairs of |
| | boxes or not. Note that if all_pairs == False, we must have m==n. |
| | eps: Epsilon for numerical stability. |
| | |
| | Returns: |
| | If all_pairs == True, returns a [bs, n, m] pairwise matrix, of generalized |
| | ious. If all_pairs == False, returns a [bs, n] matrix of generalized ious. |
| | """ |
| | |
| | |
| | |
| | |
| | iou, union = box_iou( |
| | boxes1, boxes2, np_backbone=np_backbone, all_pairs=all_pairs, eps=eps) |
| |
|
| | |
| | |
| | |
| | if all_pairs: |
| | lt = np_backbone.minimum(boxes1[..., :, None, :2], |
| | boxes2[..., None, :, :2]) |
| | rb = np_backbone.maximum(boxes1[..., :, None, 2:], |
| | boxes2[..., None, :, 2:]) |
| |
|
| | else: |
| | lt = np_backbone.minimum(boxes1[..., :, :2], |
| | boxes2[..., :, :2]) |
| | rb = np_backbone.maximum(boxes1[..., :, 2:], boxes2[..., :, |
| | 2:]) |
| |
|
| | |
| | wh = (rb - lt).clip(0.0) |
| | area = wh[..., 0] * wh[..., 1] |
| |
|
| | |
| | |
| | return iou - (area - union) / (area + eps) |
| |
|
| |
|
| | |
| |
|
| |
|
| | def cxcywha_to_corners(cxcywha: Array, np_backbone: PyModule = jnp) -> Array: |
| | """Convert [cx, cy, w, h, a] to four corners of [x, y]. |
| | |
| | Args: |
| | cxcywha: [..., 5]-ndarray of [center-x, center-y, width, height, angle] |
| | representation of rotated boxes. Angle is in radians and center of rotation |
| | is defined by [center-x, center-y] point. |
| | np_backbone: Numpy module: Either the regular numpy package or jax.numpy. |
| | |
| | Returns: |
| | [..., 4, 2]-ndarray of four corners of the rotated box as [x, y] points. |
| | """ |
| | assert cxcywha.shape[-1] == 5, 'Expected [..., [cx, cy, w, h, a] input.' |
| | bs = cxcywha.shape[:-1] |
| | cx, cy, w, h, a = np_backbone.split(cxcywha, indices_or_sections=5, axis=-1) |
| | xs = np_backbone.array([.5, .5, -.5, -.5]) * w |
| | ys = np_backbone.array([-.5, .5, .5, -.5]) * h |
| | pts = np_backbone.stack([xs, ys], axis=-1) |
| | sin = np_backbone.sin(a) |
| | cos = np_backbone.cos(a) |
| | rot = np_backbone.concatenate([cos, -sin, sin, cos], axis=-1).reshape( |
| | (*bs, 2, 2)) |
| | offset = np_backbone.concatenate([cx, cy], -1).reshape((*bs, 1, 2)) |
| | corners = pts @ rot + offset |
| | return corners |
| |
|
| |
|
| | def corners_to_cxcywha(corners: jnp.ndarray, |
| | np_backbone: PyModule = jnp) -> jnp.ndarray: |
| | """Convert four corners of [x, y] to [cx, cy, w, h, a]. |
| | |
| | Although the conversion is only guaranteed to produce an exact rbox when given |
| | vertices that form an rbox, there is some graceful handling of nearly rbox |
| | vertices by choosing the rbox with corners minimizing the square distance to |
| | the rbox vertices. This solution is equivalent to taking the average of the |
| | top and bottom edges (wcorners*) as well as the left and right edges |
| | (hcornersy). |
| | |
| | Args: |
| | corners: [..., 4, 2]-ndarray of four corners of the rotated box as [x, y] |
| | points. |
| | np_backbone: Numpy module: Either the regular numpy package or jax.numpy. |
| | |
| | Returns: |
| | [..., 5]-ndarray of [center-x, center-y, width, height, angle] |
| | representation of rotated boxes. Angle is in radians and center of rotation |
| | is defined by [center-x, center-y] point. |
| | """ |
| | assert corners.shape[-2] == 4 and corners.shape[-1] == 2, ( |
| | 'Expected four corners [..., 4, 2] input.') |
| |
|
| | cornersx, cornersy = corners[..., 0], corners[..., 1] |
| | cx = np_backbone.mean(cornersx, axis=-1) |
| | cy = np_backbone.mean(cornersy, axis=-1) |
| | wcornersx = ( |
| | cornersx[..., 0] + cornersx[..., 1] - cornersx[..., 2] - cornersx[..., 3]) |
| | wcornersy = ( |
| | cornersy[..., 0] + cornersy[..., 1] - cornersy[..., 2] - cornersy[..., 3]) |
| | hcornersy = (-cornersy[..., 0,] + cornersy[..., 1] + cornersy[..., 2] - |
| | cornersy[..., 3]) |
| | a = -np_backbone.arctan2(wcornersy, wcornersx) |
| | cos = np_backbone.cos(a) |
| | w = wcornersx / (2 * cos) |
| | h = hcornersy / (2 * cos) |
| | cxcywha = np_backbone.stack([cx, cy, w, h, a], axis=-1) |
| |
|
| | return cxcywha |
| |
|
| |
|
| | def intersect_line_segments( |
| | lines1: jnp.ndarray, lines2: jnp.ndarray, eps: float = 1e-8 |
| | ) -> jnp.ndarray: |
| | """Intersect two line segments. |
| | |
| | Given two 2D line segments, where a line segment is defined as two 2D points. |
| | Finds the point of intersection or returns [nan, nan] if no point exists. |
| | |
| | See https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection (Given two |
| | points on each line segment). |
| | |
| | Performance Note: At the calling point, we expect user to appropriately vmap |
| | function to work on batches of lines. |
| | Args: |
| | lines1: [..., 2, 2]-ndarray, [[x1, y1], [x2, y2]] for lines. |
| | lines2: [..., 2, 2]-ndarray, [[x3, y3], [x4, y4]] for other lines. |
| | eps: Epsilon for numerical stability. |
| | |
| | Returns: |
| | Intersection points [..., 2]-ndarray or [..., [nan, nan]] if no point |
| | exists. Since we are intersecting line segments in 2D, this happens if |
| | lines are parallel or the intersection of the infinite line would occur |
| | outside of both segments. |
| | """ |
| | assert lines1.shape[-2:] == (2, 2) and lines2.shape[-2:] == (2, 2) |
| | x1, y1 = jnp.split(lines1[..., 0, :], 2, -1) |
| | x2, y2 = jnp.split(lines1[..., 1, :], 2, -1) |
| | x3, y3 = jnp.split(lines2[..., 0, :], 2, -1) |
| | x4, y4 = jnp.split(lines2[..., 1, :], 2, -1) |
| | den = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4) |
| | num_t = (x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4) |
| | num_u = (x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3) |
| | |
| | |
| | t = num_t / (den + eps) |
| | u = -num_u / (den + eps) |
| |
|
| | intersection_pt = jnp.concatenate([x1 + t * (x2 - x1), y1 + t * (y2 - y1)], |
| | -1) |
| | are_parallel = jnp.abs(den) < eps |
| | not_on_line1 = jnp.logical_or(u < 0, u > 1) |
| | not_on_line2 = jnp.logical_or(t < 0, t > 1) |
| |
|
| | not_possible = jnp.any( |
| | jnp.concatenate([are_parallel, not_on_line1, not_on_line2], -1), -1) |
| | nan_pt = jnp.ones_like(intersection_pt) * jnp.nan |
| | return jnp.where(not_possible[..., None], nan_pt, intersection_pt) |
| |
|
| |
|
| | def intersect_rbox_edges(corners1: jnp.ndarray, |
| | corners2: jnp.ndarray) -> jnp.ndarray: |
| | """Find intersection points between all four edges of both rotated boxes. |
| | |
| | Note that you are expected to explicitly use vmap to control batching. |
| | |
| | Args: |
| | corners1: (4, 2)-ndarray of corners for rbox1. |
| | corners2: (4, 2)-ndarray of corners for rbox2. |
| | |
| | Returns: |
| | intersections: (4, 4, 2)-ndarray (i, j, :) means intersection of i-th |
| | edge of rbox1 with j-th of rbox2. |
| | """ |
| | intersections = [] |
| | |
| | |
| | |
| | for i in range(4): |
| | line1 = jnp.stack([corners1[i, :], corners1[(i + 1) % 4, :]], axis=0) |
| | for j in range(4): |
| | line2 = jnp.stack([corners2[j, :], corners2[(j + 1) % 4, :]], axis=0) |
| | intersections.append(intersect_line_segments(line1, line2)) |
| | intersections = jnp.reshape(jnp.stack(intersections), (4, 4, 2)) |
| | return intersections |
| |
|