File size: 4,371 Bytes
c33a30c | 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 | """
Action / coordinate utility functions.
All coordinates are in normalised [0, 1] space.
"""
from __future__ import annotations
import re
from typing import Optional, Tuple
# ---------------------------------------------------------------------------
# Point parsing
# ---------------------------------------------------------------------------
_POINT_RE = re.compile(
r"[\(\[]\s*"
r"([+-]?\d+(?:\.\d+)?)"
r"\s*[,\s]\s*"
r"([+-]?\d+(?:\.\d+)?)"
r"\s*[\)\]]"
)
def parse_point(pred_str: str) -> Optional[Tuple[float, float]]:
"""
Parse a predicted (x, y) coordinate string.
Handles formats:
- "(0.5, 0.3)"
- "[0.5 0.3]"
- "(0.5,0.3)"
- "(512, 384)" (absolute pixels — treated as-is, caller should normalise)
Returns:
(x, y) float tuple, or None if parsing fails.
"""
if not pred_str:
return None
pred_str = pred_str.strip()
# Try regex first
m = _POINT_RE.search(pred_str)
if m:
x, y = float(m.group(1)), float(m.group(2))
return x, y
# Try comma/space separated without brackets
parts = re.split(r"[,\s]+", pred_str.strip("()[] "))
if len(parts) == 2:
try:
return float(parts[0]), float(parts[1])
except ValueError:
pass
return None
# ---------------------------------------------------------------------------
# Bounding-box helpers
# ---------------------------------------------------------------------------
def point_in_bbox(
point: Tuple[float, float],
bbox: Tuple[float, float, float, float],
) -> bool:
"""
Check whether a normalised point falls inside a normalised bounding box.
Args:
point: (x, y) in [0, 1].
bbox: (x1, y1, x2, y2) in [0, 1] with x2 > x1, y2 > y1.
Returns:
True if point is inside or on the boundary of the bbox.
"""
px, py = point
x1, y1, x2, y2 = bbox
return x1 <= px <= x2 and y1 <= py <= y2
def iou_click(
pred_point: Tuple[float, float],
gt_bbox: Tuple[float, float, float, float],
click_radius: float = 0.05,
) -> bool:
"""
Determine if a predicted click point "hits" the ground-truth bounding box.
A click is considered a hit if:
- It falls inside the gt_bbox, OR
- It is within `click_radius` of the bbox centre (in Euclidean distance).
Args:
pred_point: Predicted (x, y) in [0, 1].
gt_bbox: Ground-truth (x1, y1, x2, y2) in [0, 1].
click_radius: Maximum allowed distance from bbox centre.
Returns:
True if the click is considered a hit.
"""
# Inside the bbox
if point_in_bbox(pred_point, gt_bbox):
return True
# Within radius of bbox centre
x1, y1, x2, y2 = gt_bbox
cx, cy = (x1 + x2) / 2.0, (y1 + y2) / 2.0
px, py = pred_point
dist = ((px - cx) ** 2 + (py - cy) ** 2) ** 0.5
return dist <= click_radius
# ---------------------------------------------------------------------------
# Action matching
# ---------------------------------------------------------------------------
def action_match(
pred_str: str,
gt_str: str,
threshold: float = 0.05,
) -> bool:
"""
Check whether a predicted action string matches the ground-truth.
Both strings should be "(x, y)" coordinate strings. Returns True if
the Euclidean distance between the two parsed points is ≤ threshold.
Args:
pred_str: Predicted action string.
gt_str: Ground-truth action string.
threshold: Maximum Euclidean distance (in normalised coords).
Returns:
True if the actions match.
"""
pred_pt = parse_point(pred_str)
gt_pt = parse_point(gt_str)
if pred_pt is None or gt_pt is None:
return False
dx = pred_pt[0] - gt_pt[0]
dy = pred_pt[1] - gt_pt[1]
dist = (dx ** 2 + dy ** 2) ** 0.5
return dist <= threshold
def normalize_action_str(action_str: str) -> str:
"""
Normalise an action string to a canonical "(x,y)" representation.
Args:
action_str: Raw action string such as "(0.5, 0.3)" or "[0.5 0.3]".
Returns:
Canonical string like "(0.5000,0.3000)", or the original string if
parsing fails.
"""
pt = parse_point(action_str)
if pt is None:
return action_str.strip()
return f"({pt[0]:.4f},{pt[1]:.4f})"
|