diff --git a/jetson/main.py b/jetson/main.py index 09ceb41..4cb4989 100644 --- a/jetson/main.py +++ b/jetson/main.py @@ -133,6 +133,7 @@ def auto_calibrate(): 'debug_image': base64.b64encode(jpeg.tobytes()).decode('ascii'), 'lines_detected': {'across': n_across, 'along': n_along}, 'points_matched': len(match['points_2d']), + 'matched_lines_3d': match.get('matched_lines_3d', []), } print(f"[CAM {sensor_id}] Calibrated! Camera at " f"({cam_pos[0]:.2f}, {cam_pos[1]:.2f}, {cam_pos[2]:.2f}), " @@ -359,13 +360,51 @@ def _match_court_lines(grouped, side, frame_w, frame_h): points_2d.append(pt) points_3d.append([across_3d_x[1], center_service_y, 0]) + # Build list of matched 3D court lines for visualization + matched_lines_3d = [] + # Baseline + matched_lines_3d.append({ + 'name': 'baseline', + 'from': [across_3d_x[0], along_3d_y[0], 0], + 'to': [across_3d_x[0], along_3d_y[1], 0], + }) + # Kitchen + matched_lines_3d.append({ + 'name': 'kitchen', + 'from': [across_3d_x[1], along_3d_y[0], 0], + 'to': [across_3d_x[1], along_3d_y[1], 0], + }) + # Left sideline + matched_lines_3d.append({ + 'name': 'sideline_near', + 'from': [across_3d_x[0], along_3d_y[0], 0], + 'to': [across_3d_x[1], along_3d_y[0], 0], + }) + # Right sideline + matched_lines_3d.append({ + 'name': 'sideline_far', + 'from': [across_3d_x[0], along_3d_y[1], 0], + 'to': [across_3d_x[1], along_3d_y[1], 0], + }) + # Center service (if detected) + if len(along_sorted) >= 3: + matched_lines_3d.append({ + 'name': 'center_service', + 'from': [across_3d_x[0], center_service_y, 0], + 'to': [across_3d_x[1], center_service_y, 0], + }) + if len(points_2d) < 4: return { 'points_2d': None, 'points_3d': None, + 'matched_lines_3d': matched_lines_3d, 'error': f'Only {len(points_2d)} intersection points found (need >= 4)', } - return {'points_2d': points_2d, 'points_3d': points_3d, 'error': None} + return { + 'points_2d': points_2d, 'points_3d': points_3d, + 'matched_lines_3d': matched_lines_3d, 'error': None, + } diff --git a/src/web/templates/index.html b/src/web/templates/index.html index 0be2b37..871d6f4 100644 --- a/src/web/templates/index.html +++ b/src/web/templates/index.html @@ -335,6 +335,7 @@ border-radius: 4px; border: 1px solid #333; display: block; + cursor: pointer; } .cal-debug-card .cal-live { opacity: 0.5; @@ -356,6 +357,23 @@ z-index: 1; } + /* Fullscreen image overlay */ + .fullscreen-overlay { + display: none; + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + z-index: 1000; + background: rgba(0,0,0,0.95); + cursor: pointer; + align-items: center; + justify-content: center; + } + .fullscreen-overlay.show { display: flex; } + .fullscreen-overlay img { + max-width: 95vw; + max-height: 95vh; + object-fit: contain; + } @@ -374,6 +392,9 @@
VAR: Close Call
+
+ +
@@ -533,6 +554,9 @@ function doCalibrate() { fetch('/api/calibration/data') .then(function(r) { return r.json(); }) .then(function(camData) { addCamerasToScene(camData); }); + + // Highlight detected court lines on 3D scene + if (data.result) highlightDetectedLines(data.result); } else { btn.textContent = 'Calibrate'; // Show errors @@ -555,6 +579,15 @@ function doCalibrate() { }); } +// ===================== Fullscreen image viewer ===================== +document.querySelectorAll('.cal-debug-card img').forEach(function(img) { + img.addEventListener('click', function() { + if (!this.src || this.src === location.href) return; + document.getElementById('fullscreenImg').src = this.src; + document.getElementById('fullscreenOverlay').classList.add('show'); + }); +}); + var systemReady = false; function updateCalibrationStatus() { @@ -621,6 +654,38 @@ function addCamerasToScene(camData) { } } +// ===================== Highlight detected court lines on 3D ===================== +var detectedLineMeshes = []; + +function highlightDetectedLines(calibResult) { + if (!courtSceneInitialized) return; + + // Remove old highlights + detectedLineMeshes.forEach(function(m) { courtScene.remove(m); }); + detectedLineMeshes = []; + + var camColors = { '0': 0x44aaff, '1': 0xff44aa }; + + for (var sid in calibResult) { + var r = calibResult[sid]; + var lines3d = r.matched_lines_3d || []; + var color = camColors[sid] || 0xffffff; + + for (var i = 0; i < lines3d.length; i++) { + var l = lines3d[i]; + var z = 0.08; // slightly above court surface + var geo = new THREE.BufferGeometry().setFromPoints([ + new THREE.Vector3(l.from[0], l.from[1], z), + new THREE.Vector3(l.to[0], l.to[1], z) + ]); + var mat = new THREE.LineBasicMaterial({ color: color, linewidth: 3 }); + var mesh = new THREE.Line(geo, mat); + courtScene.add(mesh); + detectedLineMeshes.push(mesh); + } + } +} + // ===================== Camera frame polling ===================== var camPrefixes = { 'camera': 'cam', 'calibration': 'cal', 'trajectory': 'traj' };