Remove all fallbacks: show errors, draw debug lines on calibration

- Remove try-except in CameraCalibrator — errors propagate to UI
- Remove auto-load of saved calibrations — always start uncalibrated
- Remove hardcoded "Base Setup" card with fake values
- Remove addStereocameras/getCamParams dead code
- Draw all detected Hough lines + corners on debug frame during calibration
- Show debug images in calibration tab after attempt
- Show error messages in UI instead of swallowing them

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ruslan Bakiev
2026-03-22 14:27:53 +07:00
parent ee73aa80d8
commit e12edab19b
4 changed files with 178 additions and 229 deletions

View File

@@ -332,19 +332,15 @@
<div class="bottom-card"><img id="cal-cam1" alt="Camera 1"></div>
<div class="bottom-card"><img id="cal-cam0" alt="Camera 0"></div>
<div class="cam-card">
<div class="cc-title">Base Setup</div>
<div class="cc-item">Distance <b id="paramPosY">1.0</b>m</div>
<div class="cc-item">Height <b id="paramPosZ">1.0</b>m</div>
<div class="cc-item">Stereo <b id="paramStereo">6</b>cm</div>
<div class="cc-item">Rotation <b id="paramAngle">15</b>°</div>
<div class="cc-item">HFOV <b id="paramHfov">128</b>°</div>
<div class="cc-item">Sensor <b>IMX219</b></div>
<div class="cc-divider"></div>
<div class="cc-title">Calibration</div>
<button class="btn-calibrate" id="btnCalibrate" onclick="doCalibrate()">Calibrate</button>
<div class="calibrate-status" id="calStatus">
<span id="calStatusText">Not calibrated</span>
</div>
<div id="calError" style="color:#ff4444;font-size:8px;word-break:break-all;display:none"></div>
</div>
<div class="bottom-card" id="calDebug0" style="display:none"><img id="calDebugImg0" alt="CAM 0 debug"></div>
<div class="bottom-card" id="calDebug1" style="display:none"><img id="calDebugImg1" alt="CAM 1 debug"></div>
</div>
</div>
</div>
@@ -411,15 +407,34 @@ if (activeTab !== 'camera') switchTab(activeTab);
// ===================== Calibration =====================
function doCalibrate() {
var btn = document.getElementById('btnCalibrate');
var errEl = document.getElementById('calError');
btn.disabled = true;
btn.textContent = 'Calibrating...';
errEl.style.display = 'none';
fetch('/api/calibration/trigger', { method: 'POST' })
.then(function(r) { return r.json(); })
.then(function(data) {
btn.disabled = false;
// Show debug images from calibration
if (data.result) {
for (var sid in data.result) {
var r = data.result[sid];
if (r.debug_image) {
var dbgEl = document.getElementById('calDebug' + sid);
var imgEl = document.getElementById('calDebugImg' + sid);
if (dbgEl && imgEl) {
imgEl.src = 'data:image/jpeg;base64,' + r.debug_image;
dbgEl.style.display = 'block';
}
}
}
}
if (data.ok) {
btn.textContent = 'Re-calibrate';
errEl.style.display = 'none';
updateCalibrationStatus();
fetch('/api/calibration/data')
@@ -427,19 +442,23 @@ function doCalibrate() {
.then(function(camData) { addCamerasToScene(camData); });
} else {
btn.textContent = 'Calibrate';
// Show errors
var errors = [];
if (data.result) {
for (var sid in data.result) {
if (!data.result[sid].ok) errors.push('CAM ' + sid + ': ' + data.result[sid].error);
if (!data.result[sid].ok) errors.push(data.result[sid].error);
}
}
alert('Calibration failed:\n' + (errors.join('\n') || data.error || 'Unknown error'));
var msg = errors.join(' | ') || data.error || 'Unknown error';
errEl.textContent = msg;
errEl.style.display = 'block';
}
})
.catch(function(e) {
btn.disabled = false;
btn.textContent = 'Calibrate';
alert('Error: ' + e);
errEl.textContent = String(e);
errEl.style.display = 'block';
});
}
@@ -582,9 +601,6 @@ function initCourtScene() {
courtScene.add(new THREE.AmbientLight(0xffffff, 0.8));
// Physical camera marker: 1m from net, center, 1m height
addStereocameras(courtScene, getCamParams());
// Load existing calibration cameras
fetch('/api/calibration/data')
.then(function(r) { return r.json(); })
@@ -683,9 +699,6 @@ function initTrajectoryScene() {
trajScene.add(new THREE.AmbientLight(0xffffff, 0.8));
// Physical camera marker: 1m from net, center, 1m height
addStereocameras(trajScene, getCamParams());
function animateTraj() {
requestAnimationFrame(animateTraj);
controls.update();
@@ -702,115 +715,6 @@ function initTrajectoryScene() {
});
}
// ===================== Stereo camera rig with court coverage =====================
// Track camera overlay objects for live update
var camOverlayObjects = [];
function addStereocameras(scene, params) {
params = params || {};
var baseX = 6.7;
var baseY = -(params.posY || 1);
var baseZ = params.posZ || 1;
var stereoGap = (params.stereo || 6) / 100;
var camAngle = params.angle || 15;
var hfov = params.hfov || 128;
// Remove old overlay objects
camOverlayObjects.forEach(function(obj) { scene.remove(obj); });
camOverlayObjects = [];
function addObj(obj) {
scene.add(obj);
camOverlayObjects.push(obj);
}
var cam0x = baseX - stereoGap / 2;
var cam1x = baseX + stereoGap / 2;
// Small camera bodies
function drawCamBody(cx, color) {
var body = new THREE.Mesh(
new THREE.BoxGeometry(0.12, 0.08, 0.08),
new THREE.MeshBasicMaterial({ color: color })
);
body.position.set(cx, baseY, baseZ);
addObj(body);
}
drawCamBody(cam0x, 0x44aaff);
drawCamBody(cam1x, 0xff44aa);
// Pole
var poleGeo = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(baseX, baseY, 0),
new THREE.Vector3(baseX, baseY, baseZ)
]);
addObj(new THREE.Line(poleGeo, new THREE.LineBasicMaterial({ color: 0x666666 })));
// Court boundaries for clipping
var courtMinX = 0, courtMaxX = 13.4, courtMinY = 0, courtMaxY = 6.1;
var deg2rad = Math.PI / 180;
var halfFov = hfov / 2;
function drawCoverage(cx, angleDeg, color) {
var centerAngle = 90 + angleDeg;
var leftAngle = centerAngle + halfFov;
var rightAngle = centerAngle - halfFov;
var ox = cx, oy = baseY;
// Cast ray, return farthest point on court boundary
function castRay(aDeg) {
var rad = aDeg * deg2rad;
var dx = Math.cos(rad);
var dy = Math.sin(rad);
if (dy <= 0) return null;
// t where ray enters court (Y=0)
var tEnter = (courtMinY - oy) / dy;
// t where ray exits court
var tExit = (courtMaxY - oy) / dy;
// clip X bounds
if (dx > 1e-9) tExit = Math.min(tExit, (courtMaxX - ox) / dx);
else if (dx < -1e-9) tExit = Math.min(tExit, (courtMinX - ox) / dx);
if (tExit <= tEnter) return null;
var px = ox + dx * tExit;
var py = oy + dy * tExit;
px = Math.max(courtMinX, Math.min(courtMaxX, px));
py = Math.max(courtMinY, Math.min(courtMaxY, py));
return new THREE.Vector3(px, py, 0.015);
}
// Camera actual position (behind court at Y=-1)
var camOnCourt = new THREE.Vector3(cx, oy, 0.015);
// Far edge points
var farPoints = [];
var steps = 64;
for (var i = 0; i <= steps; i++) {
var a = rightAngle + (leftAngle - rightAngle) * (i / steps);
var pt = castRay(a);
if (pt) farPoints.push(pt);
}
if (farPoints.length < 2) return;
// Draw triangle fan from camera court position to far edge
var mat = new THREE.MeshBasicMaterial({
color: color, transparent: true, opacity: 0.35,
side: THREE.DoubleSide, depthWrite: false
});
for (var i = 0; i < farPoints.length - 1; i++) {
var triGeo = new THREE.BufferGeometry().setFromPoints([
camOnCourt, farPoints[i], farPoints[i + 1]
]);
addObj(new THREE.Mesh(triGeo, mat));
}
}
drawCoverage(cam0x, -camAngle, 0x4488ff); // blue
drawCoverage(cam1x, camAngle, 0xff44aa); // pink
// overlap blends to purple naturally
}
// ===================== Draw court lines =====================
function drawCourtLines(scene) {
@@ -983,26 +887,6 @@ setInterval(function() {
}).catch(function() {});
}, 1000);
// ===================== Camera params live update =====================
function getCamParams() {
return {
posY: parseFloat(document.getElementById('paramPosY').textContent) || 1,
posZ: parseFloat(document.getElementById('paramPosZ').textContent) || 1,
stereo: parseFloat(document.getElementById('paramStereo').textContent) || 6,
angle: parseFloat(document.getElementById('paramAngle').textContent) || 15,
hfov: parseFloat(document.getElementById('paramHfov').textContent) || 128
};
}
function setCamParams(p) {
document.getElementById('paramPosY').textContent = p.posY;
document.getElementById('paramPosZ').textContent = p.posZ;
document.getElementById('paramStereo').textContent = p.stereo;
document.getElementById('paramAngle').textContent = p.angle;
document.getElementById('paramHfov').textContent = p.hfov;
if (courtSceneInitialized) addStereocameras(courtScene, p);
if (trajSceneInitialized) addStereocameras(trajScene, p);
}
</script>
</body>
</html>