| """ |
| Action / coordinate utility functions. |
| |
| All coordinates are in normalised [0, 1] space. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import re |
| from typing import Optional, Tuple |
|
|
|
|
| |
| |
| |
|
|
| _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() |
|
|
| |
| m = _POINT_RE.search(pred_str) |
| if m: |
| x, y = float(m.group(1)), float(m.group(2)) |
| return x, y |
|
|
| |
| 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 |
|
|
|
|
| |
| |
| |
|
|
| 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. |
| """ |
| |
| if point_in_bbox(pred_point, gt_bbox): |
| return True |
|
|
| |
| 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 |
|
|
|
|
| |
| |
| |
|
|
| 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})" |
|
|