GoldenBear23's picture
Deploy snapshot for HF Space
71efe81
Raw
History Blame Contribute Delete
5.61 kB
#!/usr/bin/env python3
"""
Generic polygon/ring pixel-sampling primitives shared by the per-shape
validators (diamond.py, circle.py, hexagon.py). No shape-specific thresholds
or dispatch logic lives here — just parameterized geometry sampling.
"""
import cv2
import numpy as np
def polygon_vertex_points(
cx: int, cy: int, r: int, n_sides: int, angle_offset_deg: float = 0.0
) -> list[tuple[float, float]]:
"""
Vertex positions of a regular n-gon centred at (cx, cy) with circumradius r.
angle_offset_deg=-90 places the first vertex directly above the centre
(image y-down convention), matching a diamond's top vertex.
"""
pts = []
for k in range(n_sides):
theta = np.radians(angle_offset_deg + k * 360.0 / n_sides)
pts.append((cx + r * np.cos(theta), cy + r * np.sin(theta)))
return pts
def count_visible_sides(
binary: np.ndarray,
cx: int, cy: int, r: int,
n_sides: int,
angle_offset_deg: float = 0.0,
n_samples: int = 8,
hit_frac: float = 0.40,
) -> int:
"""
Count how many of an n-gon's sides have visible (white) pixels along
their interior, sampled at n_samples evenly-spaced points per side
(avoiding the vertices themselves, t in [0.15, 0.85]).
A side is 'present' if >= hit_frac of its samples hit a white pixel in a
small 5x5 patch. Generalises the diamond-only visible-sides check to any
regular polygon: diamond uses n_sides=4, angle_offset_deg=-90 (vertices at
bbox midpoints — top/right/bottom/left); hexagon uses n_sides=6.
"""
H, W = binary.shape
pts = polygon_vertex_points(cx, cy, r, n_sides, angle_offset_deg)
count = 0
for i in range(n_sides):
x1, y1 = pts[i]
x2, y2 = pts[(i + 1) % n_sides]
hits = 0
for t in np.linspace(0.15, 0.85, n_samples):
px = int(x1 + (x2 - x1) * t)
py = int(y1 + (y2 - y1) * t)
if 0 <= py < H and 0 <= px < W:
patch = binary[max(0, py - 2):py + 3, max(0, px - 2):px + 3]
if patch.any():
hits += 1
if hits >= n_samples * hit_frac:
count += 1
return count
def count_hits_at_angles(
binary: np.ndarray,
cx: int, cy: int, r: int,
angles_deg,
dr_window: int = 6,
n_probe: int = 5,
) -> int:
"""
Count how many of the given angles (degrees, image y-down convention)
have a white pixel near radius r.
At each angle, probes n_probe points spaced across [r - dr_window,
r + dr_window] and counts a hit if any of them lands on a white pixel
in a small 3x3 patch. Generalises the diamond-only inter-vertex check
(angles_deg=(45,135,225,315)) to arbitrary angle sets.
"""
H, W = binary.shape
count = 0
for deg in angles_deg:
rad = np.radians(deg)
for dr in np.linspace(max(1, r - dr_window), r + dr_window, n_probe):
py = int(cy + dr * np.sin(rad))
px = int(cx + dr * np.cos(rad))
if 0 <= py < H and 0 <= px < W:
patch = binary[max(0, py - 3):py + 3, max(0, px - 3):px + 3]
if patch.any():
count += 1
break
return count
def ring_coverage_fraction(
binary: np.ndarray,
cx: int, cy: int, r: int,
n_samples: int = 24,
) -> float:
"""
Fraction of n_samples evenly-spaced angles around the full circle that
have a white pixel near radius r. A real circle's boundary is continuous,
so this should be high; a diamond's discrete 4 sides leave it low.
"""
angles = np.linspace(0, 360, n_samples, endpoint=False)
hits = count_hits_at_angles(binary, cx, cy, r, angles)
return hits / n_samples if n_samples else 0.0
def outer_ring_density(
binary: np.ndarray,
cx: int, cy: int, r: int,
width: int = 8,
) -> float:
"""
Fraction of white pixels in the annular band just outside the estimated
shape boundary. Real symbols float in open blueprint space (low density);
table labels and grid-surrounded shapes have a high density.
"""
H, W = binary.shape
mask = np.zeros((H, W), np.uint8)
cv2.circle(mask, (cx, cy), r + width, 255, thickness=-1)
cv2.circle(mask, (cx, cy), max(1, r - 2), 0, thickness=-1)
ring_pixels = int(cv2.countNonZero(mask))
if ring_pixels == 0:
return 0.0
return float(cv2.countNonZero(cv2.bitwise_and(binary, binary, mask=mask))) / ring_pixels
def is_diamond_vertex_layout(
approx: np.ndarray, x: int, y: int, w: int, h: int, vertex_tol: float = 0.22
) -> bool:
"""
A diamond's 4 vertices sit near the MIDPOINTS of the bounding-box sides.
An axis-aligned rectangle's vertices sit at the CORNERS.
Each vertex must be within vertex_tol * min(w, h) of some expected midpoint.
Used both by classify_shape() (to distinguish a diamond from a square
among 4-vertex exemplars) and by diamond.passes_geometry().
"""
pts = approx.reshape(-1, 2).astype(float)
cx, cy = x + w / 2.0, y + h / 2.0
# Canonical ordering by polar angle (independent of contour winding direction)
order = np.argsort(np.arctan2(pts[:, 1] - cy, pts[:, 0] - cx))
pts = pts[order]
midpoints = np.array([
[x + w, cy ], # right (≈ 0°)
[cx, y + h], # bottom (≈ 90°)
[x, cy ], # left (≈180°)
[cx, y ], # top (≈270°)
])
tol = min(w, h) * vertex_tol
for pt in pts:
if np.min(np.linalg.norm(midpoints - pt, axis=1)) > tol:
return False
return True