Files
pickle_vision/src/physics/event_detector.py
Ruslan Bakiev 4c9b48e057 Add referee system architecture: 3-tab web UI, physics, VAR detection
- src/calibration: 3D camera calibration (solvePnP, homography)
- src/physics: trajectory model with gravity/drag, close call event detector
- src/streaming: camera reader + ring buffer for VAR clips
- src/web: Flask app with 3-tab UI (Detection, Court, Trajectory) + Three.js
- jetson/main.py: unified entry point wiring all components

Court tab: one-click calibration button, 3D scene with camera positions
Trajectory tab: accumulated ball path, VAR panel with snapshot + timer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:16:12 +07:00

114 lines
4.1 KiB
Python

"""
Close call / event detector for referee system.
Watches the trajectory model and triggers VAR when ball lands near a line.
"""
import time
from typing import Optional, Tuple, List, Dict
from .trajectory import TrajectoryModel
# Court line definitions in meters (X=along court, Y=across court)
# Full court: 13.4 x 6.1
COURT_LENGTH = 13.4
COURT_WIDTH = 6.1
HALF = COURT_LENGTH / 2 # 6.7 (net / center line)
NVZ = 2.13 # non-volley zone (kitchen) depth from net
COURT_LINES = {
'baseline_left': {'type': 'horizontal', 'x': 0, 'y_start': 0, 'y_end': COURT_WIDTH},
'baseline_right': {'type': 'horizontal', 'x': COURT_LENGTH, 'y_start': 0, 'y_end': COURT_WIDTH},
'sideline_near': {'type': 'vertical', 'y': 0, 'x_start': 0, 'x_end': COURT_LENGTH},
'sideline_far': {'type': 'vertical', 'y': COURT_WIDTH, 'x_start': 0, 'x_end': COURT_LENGTH},
'centerline': {'type': 'horizontal', 'x': HALF, 'y_start': 0, 'y_end': COURT_WIDTH},
'kitchen_left': {'type': 'horizontal', 'x': HALF - NVZ, 'y_start': 0, 'y_end': COURT_WIDTH},
'kitchen_right': {'type': 'horizontal', 'x': HALF + NVZ, 'y_start': 0, 'y_end': COURT_WIDTH},
'center_service_left': {'type': 'vertical', 'y': COURT_WIDTH / 2,
'x_start': 0, 'x_end': HALF - NVZ},
'center_service_right': {'type': 'vertical', 'y': COURT_WIDTH / 2,
'x_start': HALF + NVZ, 'x_end': COURT_LENGTH},
}
class EventDetector:
"""Detects close calls by monitoring ball trajectory near court lines."""
def __init__(self, trigger_distance_m=0.3, cooldown_seconds=5.0):
self.trigger_distance = trigger_distance_m
self.cooldown = cooldown_seconds
self.last_trigger_time = 0.0
self.events: List[Dict] = []
def check(self, trajectory: TrajectoryModel) -> Optional[Dict]:
"""Check if current trajectory warrants a VAR trigger.
Conditions:
1. Ball is descending (vz < 0) or near ground (z < 0.3m)
2. Predicted landing point is within trigger_distance of a line
3. Cooldown has passed since last trigger
"""
now = time.time()
if now - self.last_trigger_time < self.cooldown:
return None
if not trajectory.points or trajectory.velocity is None:
return None
last = trajectory.points[-1]
vz = trajectory.velocity[2]
# Ball must be descending or near ground
if vz > 0 and last.z > 0.5:
return None
# Get predicted landing
landing = trajectory.predict_landing()
if landing is None:
return None
land_x, land_y, time_to_land = landing
# Check distance to each line
closest_line = None
closest_dist = float('inf')
for name, line in COURT_LINES.items():
dist = self._distance_to_line(land_x, land_y, line)
if dist < closest_dist:
closest_dist = dist
closest_line = name
if closest_dist <= self.trigger_distance:
self.last_trigger_time = now
event = {
'type': 'close_call',
'timestamp': now,
'line': closest_line,
'distance_m': closest_dist,
'landing_x': land_x,
'landing_y': land_y,
'time_to_land': time_to_land,
'ball_z': last.z,
'ball_speed': trajectory.get_speed(),
}
self.events.append(event)
return event
return None
def _distance_to_line(self, x: float, y: float, line: Dict) -> float:
if line['type'] == 'horizontal':
lx = line['x']
y_start, y_end = line['y_start'], line['y_end']
if y_start <= y <= y_end:
return abs(x - lx)
return float('inf')
else: # vertical
ly = line['y']
x_start, x_end = line['x_start'], line['x_end']
if x_start <= x <= x_end:
return abs(y - ly)
return float('inf')
def is_in_bounds(self, x: float, y: float) -> bool:
return 0 <= x <= COURT_LENGTH and 0 <= y <= COURT_WIDTH