Rename tabs: Camera, Calibration, Trajectory — remove overlay

- Tab 1: Camera (raw feeds)
- Tab 2: Calibration (3D court + calibrate button, as before)
- Tab 3: Trajectory (disabled until calibrated)
- Remove unnecessary calibration overlay from Camera tab

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ruslan Bakiev
2026-03-22 14:13:28 +07:00
parent 1294747af2
commit ee73aa80d8
2 changed files with 34 additions and 124 deletions

View File

@@ -30,7 +30,7 @@ state = {
@app.route('/') @app.route('/')
@app.route('/<tab>') @app.route('/<tab>')
def index(tab='detection'): def index(tab='camera'):
return render_template('index.html', active_tab=tab) return render_template('index.html', active_tab=tab)

View File

@@ -293,59 +293,6 @@
.info-item .label { color: #666; } .info-item .label { color: #666; }
.info-item .value { color: #4ecca3; font-weight: 600; margin-left: 4px; } .info-item .value { color: #4ecca3; font-weight: 600; margin-left: 4px; }
/* Calibration overlay */
.calibration-overlay {
position: fixed;
top: 48px;
left: 0;
right: 0;
bottom: 0;
z-index: 5;
background: rgba(10, 10, 26, 0.85);
display: flex;
align-items: center;
justify-content: center;
}
.calibration-overlay.hidden { display: none; }
.cal-overlay-content {
text-align: center;
max-width: 500px;
padding: 40px;
}
.cal-overlay-title {
font-size: 28px;
font-weight: 700;
color: #4ecca3;
margin-bottom: 16px;
}
.cal-overlay-desc {
font-size: 15px;
color: #999;
line-height: 1.6;
margin-bottom: 20px;
}
.cal-overlay-params {
display: flex;
gap: 20px;
justify-content: center;
font-size: 13px;
color: #666;
margin-bottom: 24px;
}
.cal-overlay-params b { color: #4ecca3; }
.btn-calibrate-big {
padding: 14px 48px;
background: #4ecca3;
color: #000;
border: none;
border-radius: 8px;
font-size: 18px;
font-weight: 700;
cursor: pointer;
transition: background 0.2s;
}
.btn-calibrate-big:hover { background: #3dbb92; }
.btn-calibrate-big:disabled { background: #333; color: #666; cursor: not-allowed; }
</style> </style>
</head> </head>
<body> <body>
@@ -353,8 +300,8 @@
<div class="header"> <div class="header">
<div class="logo">Pickle Vision</div> <div class="logo">Pickle Vision</div>
<div class="tabs"> <div class="tabs">
<button class="tab active" data-tab="detection">Detection</button> <button class="tab active" data-tab="camera">Camera</button>
<button class="tab" data-tab="court" id="tabBtnCourt" disabled>Court</button> <button class="tab" data-tab="calibration">Calibration</button>
<button class="tab" data-tab="trajectory" id="tabBtnTrajectory" disabled>Trajectory</button> <button class="tab" data-tab="trajectory" id="tabBtnTrajectory" disabled>Trajectory</button>
</div> </div>
<div class="status-bar"> <div class="status-bar">
@@ -365,44 +312,25 @@
<div class="event-banner" id="eventBanner">VAR: Close Call</div> <div class="event-banner" id="eventBanner">VAR: Close Call</div>
<!-- Tab 1: Detection --> <!-- Tab 1: Camera -->
<div class="tab-content active" id="tab-detection"> <div class="tab-content active" id="tab-camera">
<!-- Calibration overlay — shown until system is calibrated -->
<div class="calibration-overlay" id="calibrationOverlay">
<div class="cal-overlay-content">
<div class="cal-overlay-title">Calibration Required</div>
<div class="cal-overlay-desc">
Position cameras so that court lines are clearly visible, then press Calibrate.
The system will auto-detect court geometry and determine camera positions.
</div>
<div class="cal-overlay-params">
<span>Sensor: <b>IMX219</b></span>
<span>HFOV: <b>128°</b></span>
<span>Stereo gap: <b>~6 cm</b></span>
</div>
<button class="btn-calibrate-big" id="btnCalibrateBig" onclick="doCalibrate()">Calibrate</button>
<div class="calibrate-status" id="calStatusOverlay" style="margin-top:8px">
<span id="calStatusTextOverlay">Waiting for calibration...</span>
</div>
</div>
</div>
<div class="cameras"> <div class="cameras">
<div class="cam-box"> <div class="cam-box">
<img id="det-cam1" alt="Camera 1"> <img id="cam-cam1" alt="Camera 1">
</div> </div>
<div class="cam-box"> <div class="cam-box">
<img id="det-cam0" alt="Camera 0"> <img id="cam-cam0" alt="Camera 0">
</div> </div>
</div> </div>
</div> </div>
<!-- Tab 2: Court — 3D main, cameras small bottom center --> <!-- Tab 2: Calibration — 3D main, cameras small bottom center -->
<div class="tab-content" id="tab-court"> <div class="tab-content" id="tab-calibration">
<div class="tab-full"> <div class="tab-full">
<div class="viewport-3d" id="court-3d"></div> <div class="viewport-3d" id="calibration-3d"></div>
<div class="bottom-bar"> <div class="bottom-bar">
<div class="bottom-card"><img id="court-cam1" alt="Camera 1"></div> <div class="bottom-card"><img id="cal-cam1" alt="Camera 1"></div>
<div class="bottom-card"><img id="court-cam0" alt="Camera 0"></div> <div class="bottom-card"><img id="cal-cam0" alt="Camera 0"></div>
<div class="cam-card"> <div class="cam-card">
<div class="cc-title">Base Setup</div> <div class="cc-title">Base Setup</div>
<div class="cc-item">Distance <b id="paramPosY">1.0</b>m</div> <div class="cc-item">Distance <b id="paramPosY">1.0</b>m</div>
@@ -450,7 +378,7 @@
<script> <script>
// ===================== Tab switching ===================== // ===================== Tab switching =====================
var activeTab = '{{ active_tab | default("detection") }}'; var activeTab = '{{ active_tab | default("camera") }}';
function switchTab(target) { function switchTab(target) {
activeTab = target; activeTab = target;
@@ -459,7 +387,7 @@ function switchTab(target) {
document.querySelector('.tab[data-tab="' + target + '"]').classList.add('active'); document.querySelector('.tab[data-tab="' + target + '"]').classList.add('active');
document.getElementById('tab-' + target).classList.add('active'); document.getElementById('tab-' + target).classList.add('active');
document.getElementById('infoPanel').style.display = (target === 'trajectory') ? 'flex' : 'none'; document.getElementById('infoPanel').style.display = (target === 'trajectory') ? 'flex' : 'none';
if (target === 'court' && !courtSceneInitialized) initCourtScene(); if (target === 'calibration' && !courtSceneInitialized) initCourtScene();
if (target === 'trajectory' && !trajSceneInitialized) initTrajectoryScene(); if (target === 'trajectory' && !trajSceneInitialized) initTrajectoryScene();
history.pushState(null, '', '/' + target); history.pushState(null, '', '/' + target);
} }
@@ -473,34 +401,32 @@ document.querySelectorAll('.tab').forEach(function(tab) {
}); });
window.addEventListener('popstate', function() { window.addEventListener('popstate', function() {
var path = location.pathname.replace('/', '') || 'detection'; var path = location.pathname.replace('/', '') || 'camera';
switchTab(path); switchTab(path);
}); });
// Init active tab from server // Init active tab from server
if (activeTab !== 'detection') switchTab(activeTab); if (activeTab !== 'camera') switchTab(activeTab);
// ===================== Calibration ===================== // ===================== Calibration =====================
function doCalibrate() { function doCalibrate() {
var btn = document.getElementById('btnCalibrate'); var btn = document.getElementById('btnCalibrate');
var btnBig = document.getElementById('btnCalibrateBig'); btn.disabled = true;
[btn, btnBig].forEach(function(b) { if (b) { b.disabled = true; b.textContent = 'Calibrating...'; } }); btn.textContent = 'Calibrating...';
fetch('/api/calibration/trigger', { method: 'POST' }) fetch('/api/calibration/trigger', { method: 'POST' })
.then(function(r) { return r.json(); }) .then(function(r) { return r.json(); })
.then(function(data) { .then(function(data) {
[btn, btnBig].forEach(function(b) { if (b) b.disabled = false; }); btn.disabled = false;
if (data.ok) { if (data.ok) {
[btn, btnBig].forEach(function(b) { if (b) b.textContent = 'Re-calibrate'; }); btn.textContent = 'Re-calibrate';
updateCalibrationStatus(); updateCalibrationStatus();
// Fetch camera positions and add to 3D scene
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); });
} else { } else {
[btn, btnBig].forEach(function(b) { if (b) b.textContent = 'Calibrate'; }); btn.textContent = 'Calibrate';
// Show per-camera errors
var errors = []; var errors = [];
if (data.result) { if (data.result) {
for (var sid in data.result) { for (var sid in data.result) {
@@ -511,7 +437,8 @@ function doCalibrate() {
} }
}) })
.catch(function(e) { .catch(function(e) {
[btn, btnBig].forEach(function(b) { if (b) { b.disabled = false; b.textContent = 'Calibrate'; } }); btn.disabled = false;
btn.textContent = 'Calibrate';
alert('Error: ' + e); alert('Error: ' + e);
}); });
} }
@@ -523,39 +450,22 @@ function updateCalibrationStatus() {
.then(function(r) { return r.json(); }) .then(function(r) { return r.json(); })
.then(function(data) { .then(function(data) {
var el = document.getElementById('calStatusText'); var el = document.getElementById('calStatusText');
var elOverlay = document.getElementById('calStatusTextOverlay');
var ok0 = data['0'], ok1 = data['1']; var ok0 = data['0'], ok1 = data['1'];
var statusText, statusClass;
if (ok0 && ok1) { if (ok0 && ok1) {
statusText = 'Calibrated'; el.textContent = 'Calibrated';
statusClass = 'ok'; el.className = 'ok';
} else if (ok0 || ok1) { } else if (ok0 || ok1) {
statusText = 'Partially calibrated'; el.textContent = 'Partially calibrated';
statusClass = 'ok'; el.className = 'ok';
} else { } else {
statusText = 'Not calibrated'; el.textContent = 'Not calibrated';
statusClass = ''; el.className = '';
} }
if (el) { el.textContent = statusText; el.className = statusClass; }
if (elOverlay) { elOverlay.textContent = statusText; elOverlay.className = statusClass; }
// Enable/disable system based on calibration
systemReady = data.system_ready || false; systemReady = data.system_ready || false;
var overlay = document.getElementById('calibrationOverlay');
var tabCourt = document.getElementById('tabBtnCourt');
var tabTraj = document.getElementById('tabBtnTrajectory'); var tabTraj = document.getElementById('tabBtnTrajectory');
if (tabTraj) tabTraj.disabled = !systemReady;
if (systemReady) {
if (overlay) overlay.classList.add('hidden');
if (tabCourt) tabCourt.disabled = false;
if (tabTraj) tabTraj.disabled = false;
} else {
if (overlay) overlay.classList.remove('hidden');
if (tabCourt) tabCourt.disabled = true;
if (tabTraj) tabTraj.disabled = true;
}
}); });
} }
// Check on load and periodically // Check on load and periodically
@@ -590,7 +500,7 @@ function addCamerasToScene(camData) {
} }
// ===================== Camera frame polling ===================== // ===================== Camera frame polling =====================
var camPrefixes = { 'detection': 'det', 'court': 'court', 'trajectory': 'traj' }; var camPrefixes = { 'camera': 'cam', 'calibration': 'cal', 'trajectory': 'traj' };
function refreshCam(tabName, camId) { function refreshCam(tabName, camId) {
var prefix = camPrefixes[tabName]; var prefix = camPrefixes[tabName];
@@ -608,7 +518,7 @@ function refreshCam(tabName, camId) {
newImg.src = '/frame/' + camId + '?' + Date.now(); newImg.src = '/frame/' + camId + '?' + Date.now();
} }
['detection', 'court', 'trajectory'].forEach(function(tab) { ['camera', 'calibration', 'trajectory'].forEach(function(tab) {
refreshCam(tab, 0); refreshCam(tab, 0);
refreshCam(tab, 1); refreshCam(tab, 1);
}); });
@@ -627,7 +537,7 @@ var courtScene, courtCamera, courtRenderer, courtBallMesh;
function initCourtScene() { function initCourtScene() {
courtSceneInitialized = true; courtSceneInitialized = true;
var container = document.getElementById('court-3d'); var container = document.getElementById('calibration-3d');
var w = container.clientWidth; var w = container.clientWidth;
var h = container.clientHeight; var h = container.clientHeight;
@@ -935,7 +845,7 @@ function drawCourtLines(scene) {
// ===================== Trajectory data polling ===================== // ===================== Trajectory data polling =====================
setInterval(function() { setInterval(function() {
if (!systemReady) return; if (!systemReady) return;
if (activeTab !== 'trajectory' && activeTab !== 'court') return; if (activeTab !== 'trajectory' && activeTab !== 'calibration') return;
fetch('/api/trajectory').then(function(r) { return r.json(); }).then(function(data) { fetch('/api/trajectory').then(function(r) { return r.json(); }).then(function(data) {
var points = data.points || []; var points = data.points || [];