BakoAI / utils /court_geometry.py
Okidi Norbert
Deployment fix: clean backend only
c6abe34
import numpy as np
import cv2
# ==================================================
# CANONICAL 18-POINT COURT KEYPOINT SCHEMA
# ==================================================
COURT_KEYPOINTS = {
0: "left_outer_top",
1: "left_baseline_upper_inner",
2: "left_paint_outer_top",
3: "left_paint_outer_bottom",
4: "left_baseline_lower_inner",
5: "left_outer_bottom",
6: "left_paint_inner_top",
7: "left_paint_inner_bottom",
8: "midline_top",
9: "midline_bottom",
10: "right_outer_top",
11: "right_baseline_upper_inner",
12: "right_paint_outer_top",
13: "right_paint_outer_bottom",
14: "right_baseline_lower_inner",
15: "right_outer_bottom",
16: "right_paint_inner_top",
17: "right_paint_inner_bottom",
}
# Tactical-map coordinates using a 94 x 50 foot style court.
# These match the semantic locations in the schema.
TACTICAL_POINTS = {
0: (0, 0),
1: (0, 8),
2: (0, 18),
3: (0, 32),
4: (0, 42),
5: (0, 50),
6: (18, 18),
7: (18, 32),
8: (47, 0),
9: (47, 50),
10: (94, 0),
11: (94, 8),
12: (94, 18),
13: (94, 32),
14: (94, 42),
15: (94, 50),
16: (76, 18),
17: (76, 32),
}
# Adjacency logic for drawing the court
COURT_EDGES = [
(0, 8), (8, 10), # top boundary
(5, 9), (9, 15), # bottom boundary
(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), # left side chain
(10, 11), (11, 12), (12, 13), (13, 14), (14, 15), # right side chain
(8, 9), # center line
(2, 6), (6, 7), (7, 3), # left paint
(12, 16), (16, 17), (17, 13), # right paint
]
def validate_court_keypoints(points_dict, conf_threshold=0.3):
"""
Validate the geometric structure of detected court keypoints.
points_dict: dict[int, dict] where each dict has 'x', 'y', 'conf'
"""
errors = []
# 1. Basic Confidence Filter
valid_points = {k: (v['x'], v['y']) for k, v in points_dict.items() if v.get('conf', 1.0) >= conf_threshold}
# 2. Minimum Points Check
if len(valid_points) < 4:
errors.append("Insufficient points for homography (minimum 4 required)")
# 3. Horizontal Order Validation (Left -> Midline -> Right)
if all(k in valid_points for k in [0, 8, 10]):
if not (valid_points[0][0] < valid_points[8][0] < valid_points[10][0]):
errors.append("Top boundary x-order mismatch (Left < Mid < Right)")
if all(k in valid_points for k in [5, 9, 15]):
if not (valid_points[5][0] < valid_points[9][0] < valid_points[15][0]):
errors.append("Bottom boundary x-order mismatch (Left < Mid < Right)")
# 4. Vertical Order Validation
if all(k in valid_points for k in [8, 9]):
if not (valid_points[8][1] < valid_points[9][1]):
errors.append("Midline vertical order mismatch (Top should be above Bottom)")
# 5. Paint Structural Validation
if all(k in valid_points for k in [2, 6]):
if not (valid_points[2][0] < valid_points[6][0]):
errors.append("Left paint width invalid (Outer should be left of Inner)")
if all(k in valid_points for k in [12, 16]):
if not (valid_points[16][0] < valid_points[12][0]):
errors.append("Right paint width invalid (Inner should be left of Outer)")
return errors, valid_points
def get_homography_points(points_dict, conf_threshold=0.3):
"""
Collect valid source and destination points for homography calculation.
Returns (src_pts, dst_pts) as numpy arrays.
"""
_, valid_points = validate_court_keypoints(points_dict, conf_threshold)
src_list = []
dst_list = []
for idx, (x, y) in valid_points.items():
if idx in TACTICAL_POINTS:
src_list.append([x, y])
dst_list.append(TACTICAL_POINTS[idx])
if len(src_list) < 4:
return None, None
return np.array(src_list, dtype=np.float32), np.array(dst_list, dtype=np.float32)