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>
This commit is contained in:
Ruslan Bakiev
2026-03-06 13:16:12 +07:00
parent d61d6b3636
commit 4c9b48e057
13 changed files with 1930 additions and 0 deletions

143
src/web/app.py Normal file
View File

@@ -0,0 +1,143 @@
"""
Flask web application for Pickle Vision referee system.
Three tabs: Detection, Court, Trajectory.
"""
import os
import cv2
import time
import json
import base64
import threading
import numpy as np
from pathlib import Path
from flask import Flask, Response, render_template, jsonify, request
app = Flask(__name__, template_folder=os.path.join(os.path.dirname(__file__), 'templates'))
# Global state — set by main.py before starting
state = {
'cameras': {}, # sensor_id -> {frame: bytes, lock, fps, detections}
'trajectory': None, # TrajectoryModel instance
'event_detector': None,
'calibrators': {}, # sensor_id -> CameraCalibrator
'calibration_dir': '', # path to save calibration files
'events': [], # recent VAR events
'last_var': None, # last VAR event with snapshot
'calibrate_fn': None, # callback for calibration trigger
}
@app.route('/')
def index():
return render_template('index.html')
@app.route('/frame/<int:sensor_id>')
def frame(sensor_id):
cam = state['cameras'].get(sensor_id)
if not cam:
return "Camera not found", 404
with cam['lock']:
jpg = cam.get('frame')
if jpg is None:
return "No frame yet", 503
return Response(jpg, mimetype='image/jpeg',
headers={'Cache-Control': 'no-cache, no-store'})
@app.route('/api/stats')
def api_stats():
result = {}
for k, v in state['cameras'].items():
result[str(k)] = {
'fps': v.get('fps', 0),
'detections': v.get('detections', 0),
}
return jsonify(result)
@app.route('/api/trajectory')
def api_trajectory():
traj = state.get('trajectory')
if traj is None:
return jsonify({'points': [], 'speed': None, 'landing': None})
points = traj.get_recent(120)
speed = traj.get_speed()
landing = traj.predict_landing()
return jsonify({
'points': points,
'speed': speed,
'landing': {'x': landing[0], 'y': landing[1], 't': landing[2]} if landing else None,
})
@app.route('/api/events')
def api_events():
return jsonify(state.get('events', [])[-20:])
@app.route('/api/var/last')
def api_var_last():
"""Get last VAR event with snapshot image."""
last = state.get('last_var')
if not last:
return jsonify(None)
result = {
'event': last['event'],
'ago_seconds': time.time() - last['event']['timestamp'],
'snapshot_b64': last.get('snapshot_b64'),
}
return jsonify(result)
@app.route('/api/calibration/status')
def api_calibration_status():
cals = {}
for sid, cal in state.get('calibrators', {}).items():
cals[str(sid)] = cal.calibrated
return jsonify(cals)
@app.route('/api/calibration/trigger', methods=['POST'])
def api_calibration_trigger():
"""Trigger one-click court calibration from current camera frames."""
fn = state.get('calibrate_fn')
if fn is None:
return jsonify({'ok': False, 'error': 'Calibration not available'}), 500
try:
result = fn()
return jsonify({'ok': True, 'result': result})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)}), 500
@app.route('/api/calibration/data')
def api_calibration_data():
"""Return calibration data for 3D scene reconstruction."""
cals = state.get('calibrators', {})
result = {}
for sid, cal in cals.items():
if not cal.calibrated:
continue
# Camera position in world coordinates
cam_pos = (-cal.rotation_matrix.T @ cal.translation_vec).flatten()
# Camera look direction (Z axis of camera in world coords)
look_dir = cal.rotation_matrix.T @ np.array([0, 0, 1.0])
result[str(sid)] = {
'position': cam_pos.tolist(),
'look_direction': look_dir.tolist(),
'focal_length': float(cal.camera_matrix[0, 0]),
'image_size': [int(cal.camera_matrix[0, 2] * 2),
int(cal.camera_matrix[1, 2] * 2)],
}
return jsonify(result)
def run(host='0.0.0.0', port=8080):
app.run(host=host, port=port, threaded=True)