File size: 3,563 Bytes
9f084b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""BBox operations — pure functions operating on bbox tuples.

All bboxes follow the canonical convention: (x, y, width, height).
x = left edge, y = top edge, width > 0, height > 0.
"""

from __future__ import annotations

# Type alias for the canonical bbox tuple
BBoxTuple = tuple[float, float, float, float]


def x2(bbox: BBoxTuple) -> float:
    """Right edge of the bbox."""
    return bbox[0] + bbox[2]


def y2(bbox: BBoxTuple) -> float:
    """Bottom edge of the bbox."""
    return bbox[1] + bbox[3]


def area(bbox: BBoxTuple) -> float:
    """Area of the bbox."""
    return bbox[2] * bbox[3]


def center(bbox: BBoxTuple) -> tuple[float, float]:
    """Center point (cx, cy) of the bbox."""
    return (bbox[0] + bbox[2] / 2, bbox[1] + bbox[3] / 2)


def contains(outer: BBoxTuple, inner: BBoxTuple, tolerance: float = 0.0) -> bool:
    """Check if *outer* fully contains *inner*, with optional pixel tolerance.

    A positive tolerance allows the inner bbox to exceed the outer bbox
    by up to that many pixels on each side without failing the check.
    """
    return (
        inner[0] >= outer[0] - tolerance
        and inner[1] >= outer[1] - tolerance
        and x2(inner) <= x2(outer) + tolerance
        and y2(inner) <= y2(outer) + tolerance
    )


def intersects(a: BBoxTuple, b: BBoxTuple) -> bool:
    """Check if two bboxes overlap (share any area)."""
    return not (
        x2(a) <= b[0]
        or x2(b) <= a[0]
        or y2(a) <= b[1]
        or y2(b) <= a[1]
    )


def intersection(a: BBoxTuple, b: BBoxTuple) -> BBoxTuple | None:
    """Compute the intersection bbox, or None if they don't overlap."""
    ix = max(a[0], b[0])
    iy = max(a[1], b[1])
    ix2 = min(x2(a), x2(b))
    iy2 = min(y2(a), y2(b))
    if ix2 <= ix or iy2 <= iy:
        return None
    return (ix, iy, ix2 - ix, iy2 - iy)


def union(a: BBoxTuple, b: BBoxTuple) -> BBoxTuple:
    """Compute the smallest bbox enclosing both a and b."""
    ux = min(a[0], b[0])
    uy = min(a[1], b[1])
    ux2 = max(x2(a), x2(b))
    uy2 = max(y2(a), y2(b))
    return (ux, uy, ux2 - ux, uy2 - uy)


def union_all(bboxes: list[BBoxTuple]) -> BBoxTuple:
    """Compute the smallest bbox enclosing all given bboxes.

    Raises ValueError if the list is empty.
    """
    if not bboxes:
        raise ValueError("Cannot compute union of an empty list of bboxes")
    result = bboxes[0]
    for b in bboxes[1:]:
        result = union(result, b)
    return result


def expand(bbox: BBoxTuple, margin: float) -> BBoxTuple:
    """Expand a bbox by *margin* pixels on each side.

    The result is clamped so that x and y don't go below 0.
    Width and height are always at least 1 pixel after expansion.
    """
    new_x = max(0.0, bbox[0] - margin)
    new_y = max(0.0, bbox[1] - margin)
    new_w = max(1.0, bbox[2] + 2 * margin)
    new_h = max(1.0, bbox[3] + 2 * margin)
    return (new_x, new_y, new_w, new_h)


def iou(a: BBoxTuple, b: BBoxTuple) -> float:
    """Intersection over Union of two bboxes."""
    inter = intersection(a, b)
    if inter is None:
        return 0.0
    inter_area = area(inter)
    union_area = area(a) + area(b) - inter_area
    if union_area <= 0:
        return 0.0
    return inter_area / union_area


def overlap_ratio(inner: BBoxTuple, outer: BBoxTuple) -> float:
    """Fraction of *inner*'s area that overlaps with *outer*."""
    inter = intersection(inner, outer)
    if inter is None:
        return 0.0
    inner_area = area(inner)
    if inner_area <= 0:
        return 0.0
    return area(inter) / inner_area