Click-to-fullscreen debug images + highlight detected lines on 3D scene
- Debug images clickable — opens fullscreen overlay (click to close) - After calibration, detected court lines highlighted on 3D scene (blue for CAM0, pink for CAM1) - Return matched_lines_3d from calibration (baseline, kitchen, sidelines, service) - Show line names in calibration results Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -133,6 +133,7 @@ def auto_calibrate():
|
|||||||
'debug_image': base64.b64encode(jpeg.tobytes()).decode('ascii'),
|
'debug_image': base64.b64encode(jpeg.tobytes()).decode('ascii'),
|
||||||
'lines_detected': {'across': n_across, 'along': n_along},
|
'lines_detected': {'across': n_across, 'along': n_along},
|
||||||
'points_matched': len(match['points_2d']),
|
'points_matched': len(match['points_2d']),
|
||||||
|
'matched_lines_3d': match.get('matched_lines_3d', []),
|
||||||
}
|
}
|
||||||
print(f"[CAM {sensor_id}] Calibrated! Camera at "
|
print(f"[CAM {sensor_id}] Calibrated! Camera at "
|
||||||
f"({cam_pos[0]:.2f}, {cam_pos[1]:.2f}, {cam_pos[2]:.2f}), "
|
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_2d.append(pt)
|
||||||
points_3d.append([across_3d_x[1], center_service_y, 0])
|
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:
|
if len(points_2d) < 4:
|
||||||
return {
|
return {
|
||||||
'points_2d': None, 'points_3d': None,
|
'points_2d': None, 'points_3d': None,
|
||||||
|
'matched_lines_3d': matched_lines_3d,
|
||||||
'error': f'Only {len(points_2d)} intersection points found (need >= 4)',
|
'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,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -335,6 +335,7 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
display: block;
|
display: block;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.cal-debug-card .cal-live {
|
.cal-debug-card .cal-live {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
@@ -356,6 +357,23 @@
|
|||||||
z-index: 1;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -374,6 +392,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="event-banner" id="eventBanner">VAR: Close Call</div>
|
<div class="event-banner" id="eventBanner">VAR: Close Call</div>
|
||||||
|
<div class="fullscreen-overlay" id="fullscreenOverlay" onclick="this.classList.remove('show')">
|
||||||
|
<img id="fullscreenImg">
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Tab 1: Camera -->
|
<!-- Tab 1: Camera -->
|
||||||
<div class="tab-content active" id="tab-camera">
|
<div class="tab-content active" id="tab-camera">
|
||||||
@@ -533,6 +554,9 @@ function doCalibrate() {
|
|||||||
fetch('/api/calibration/data')
|
fetch('/api/calibration/data')
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(camData) { addCamerasToScene(camData); });
|
.then(function(camData) { addCamerasToScene(camData); });
|
||||||
|
|
||||||
|
// Highlight detected court lines on 3D scene
|
||||||
|
if (data.result) highlightDetectedLines(data.result);
|
||||||
} else {
|
} else {
|
||||||
btn.textContent = 'Calibrate';
|
btn.textContent = 'Calibrate';
|
||||||
// Show errors
|
// 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;
|
var systemReady = false;
|
||||||
|
|
||||||
function updateCalibrationStatus() {
|
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 =====================
|
// ===================== Camera frame polling =====================
|
||||||
var camPrefixes = { 'camera': 'cam', 'calibration': 'cal', 'trajectory': 'traj' };
|
var camPrefixes = { 'camera': 'cam', 'calibration': 'cal', 'trajectory': 'traj' };
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user