Download src/utils/geometry_utils.py from venki101/Venket: direct link, hf CLI and curl.
- Browser
- Download file 5.82 kB
-
https://huggingface.co/venki101/Venket/resolve/main/src/utils/geometry_utils.py
- Command line
-
hf download hf://venki101/Venket/src/utils/geometry_utils.py
-
curl -L -o geometry_utils.py https://huggingface.co/venki101/Venket/resolve/main/src/utils/geometry_utils.py
5.82 kB
| """Geometry helper functions built on Shapely and NumPy. | |
| Centralizes every polygon/box computation used by placement and analysis so | |
| that the same, well-tested math backs both "where should the signature go" | |
| and "how well did it land" — avoiding subtle mismatches between the two. | |
| """ | |
| from __future__ import annotations | |
| from typing import Sequence | |
| import numpy as np | |
| from shapely.geometry import Polygon, box as shapely_box | |
| from shapely.validation import make_valid | |
| Box = tuple[float, float, float, float] # x1, y1, x2, y2 | |
| def box_to_polygon(box_coords: Box) -> Polygon: | |
| """Convert an ``(x1, y1, x2, y2)`` axis-aligned box into a Shapely polygon.""" | |
| x1, y1, x2, y2 = box_coords | |
| return shapely_box(min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2)) | |
| def points_to_polygon(points: Sequence[Sequence[float]]) -> Polygon: | |
| """Build a valid Shapely polygon from an ordered list of ``(x, y)`` points. | |
| Degenerate inputs (fewer than 3 points, zero area) are expanded into a | |
| minimal valid polygon by buffering, so downstream geometry ops never | |
| raise on edge cases like a single-pixel detected signature. | |
| """ | |
| pts = np.asarray(points, dtype=float) | |
| if len(pts) < 3: | |
| if len(pts) == 0: | |
| return Polygon() | |
| center = pts.mean(axis=0) | |
| return shapely_box(center[0] - 0.5, center[1] - 0.5, center[0] + 0.5, center[1] + 0.5) | |
| poly = Polygon(pts) | |
| if not poly.is_valid: | |
| poly = make_valid(poly) | |
| if poly.geom_type != "Polygon": | |
| poly = poly.convex_hull | |
| if poly.area <= 0: | |
| poly = poly.buffer(0.5) | |
| return poly | |
| def safe_area(poly: Polygon) -> float: | |
| """Return polygon area, guarding against ``None``/empty geometries.""" | |
| if poly is None or poly.is_empty: | |
| return 0.0 | |
| return float(poly.area) | |
| def intersection_area(poly_a: Polygon, poly_b: Polygon) -> float: | |
| """Return the intersection area of two polygons, 0 if either is empty/invalid.""" | |
| if poly_a is None or poly_b is None or poly_a.is_empty or poly_b.is_empty: | |
| return 0.0 | |
| if not poly_a.is_valid: | |
| poly_a = make_valid(poly_a) | |
| if not poly_b.is_valid: | |
| poly_b = make_valid(poly_b) | |
| try: | |
| return float(poly_a.intersection(poly_b).area) | |
| except Exception: | |
| return 0.0 | |
| def iou(poly_a: Polygon, poly_b: Polygon) -> float: | |
| """Intersection-over-Union of two polygons.""" | |
| inter = intersection_area(poly_a, poly_b) | |
| union = safe_area(poly_a) + safe_area(poly_b) - inter | |
| if union <= 0: | |
| return 0.0 | |
| return float(np.clip(inter / union, 0.0, 1.0)) | |
| def coverage_pct(target_box_poly: Polygon, signature_poly: Polygon) -> float: | |
| """``100 * intersection(target_box, polygon) / area(target_box)``.""" | |
| target_area = safe_area(target_box_poly) | |
| if target_area <= 0: | |
| return 0.0 | |
| inter = intersection_area(target_box_poly, signature_poly) | |
| return float(np.clip(100.0 * inter / target_area, 0.0, 100.0)) | |
| def containment_pct(target_box_poly: Polygon, signature_poly: Polygon) -> float: | |
| """``100 * intersection(target_box, polygon) / area(polygon)``.""" | |
| sig_area = safe_area(signature_poly) | |
| if sig_area <= 0: | |
| return 0.0 | |
| inter = intersection_area(target_box_poly, signature_poly) | |
| return float(np.clip(100.0 * inter / sig_area, 0.0, 100.0)) | |
| def centroid_of(poly: Polygon) -> tuple[float, float]: | |
| """Return the ``(x, y)`` centroid of a polygon, ``(nan, nan)`` if empty.""" | |
| if poly is None or poly.is_empty: | |
| return (float("nan"), float("nan")) | |
| c = poly.centroid | |
| return (float(c.x), float(c.y)) | |
| def box_center(box_coords: Box) -> tuple[float, float]: | |
| """Return the center point of an axis-aligned box.""" | |
| x1, y1, x2, y2 = box_coords | |
| return ((x1 + x2) / 2.0, (y1 + y2) / 2.0) | |
| def box_size(box_coords: Box) -> tuple[float, float]: | |
| """Return ``(width, height)`` of an axis-aligned box.""" | |
| x1, y1, x2, y2 = box_coords | |
| return (abs(x2 - x1), abs(y2 - y1)) | |
| def distance_between(p1: tuple[float, float], p2: tuple[float, float]) -> float: | |
| """Euclidean distance between two points, ``nan``-safe.""" | |
| if any(np.isnan(v) for v in (*p1, *p2)): | |
| return float("nan") | |
| return float(np.hypot(p1[0] - p2[0], p1[1] - p2[1])) | |
| def margin_utilization(box_coords: Box, signature_poly: Polygon) -> float: | |
| """Fraction of the box's available margin consumed by the signature (0..1+). | |
| 0 means the signature sits exactly at the box center with no spatial | |
| extent; 1 means it exactly fills the box's half-diagonal. Values above 1 | |
| indicate the signature protrudes outside the box. | |
| """ | |
| if signature_poly is None or signature_poly.is_empty: | |
| return 0.0 | |
| bx, by = box_center(box_coords) | |
| bw, bh = box_size(box_coords) | |
| half_diag = float(np.hypot(bw, bh) / 2.0) or 1e-6 | |
| minx, miny, maxx, maxy = signature_poly.bounds | |
| corners = [(minx, miny), (minx, maxy), (maxx, miny), (maxx, maxy)] | |
| max_dist = max(distance_between((bx, by), c) for c in corners) | |
| return float(max_dist / half_diag) | |
| def clamp01(value: float) -> float: | |
| """Clamp a float into ``[0, 1]``, mapping NaN to 0.""" | |
| if value is None or np.isnan(value): | |
| return 0.0 | |
| return float(np.clip(value, 0.0, 1.0)) | |
| def rotated_rect_polygon(center: tuple[float, float], size: tuple[float, float], angle_deg: float) -> Polygon: | |
| """Build a Shapely polygon for a rectangle rotated about its center (OpenCV convention).""" | |
| cx, cy = center | |
| w, h = size | |
| theta = np.deg2rad(angle_deg) | |
| dx = w / 2.0 | |
| dy = h / 2.0 | |
| corners = np.array([(-dx, -dy), (dx, -dy), (dx, dy), (-dx, dy)]) | |
| rot = np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]]) | |
| rotated = corners @ rot.T + np.array([cx, cy]) | |
| return Polygon(rotated) | |