Callibration Board 0

This commit is contained in:
chk
2026-06-10 13:58:21 +02:00
parent 03b7883105
commit 67257d9754
10 changed files with 4286 additions and 46 deletions

View File

@@ -260,7 +260,14 @@ async function capturePhotos(sessionName) {
const savedFiles = [];
for (const camId of cameraIds) {
const response = await new WebcamClient(WEBCAM_URL).getSnapshot(camId, true);
let response;
// Bei 503 (Kamera kurz busy nach Hires-Grab) einmal nach 2 s neu versuchen
for (let attempt = 1; attempt <= 2; attempt++) {
response = await new WebcamClient(WEBCAM_URL).getSnapshot(camId, true);
if (response.status !== 503) break;
if (attempt < 2) await new Promise(r => setTimeout(r, 2000));
}
if (!response.ok) throw new Error(`getSnapshot(${camId}): HTTP ${response.status}`);
const buffer = Buffer.from(await response.arrayBuffer());
const filename = `${camId}_${setNr}.jpg`;
await fsPromises.writeFile(path.join(sessionDir, filename), buffer);
@@ -371,44 +378,15 @@ app.post('/api/calibration/compute', async (req, res) => {
send({ type: 'log', text: `▶ Script: ${calibScriptPath}` });
send({ type: 'log', text: '' });
// -u = unbuffered (Python gibt jede Zeile sofort aus)
const proc = spawn(PYTHON_BIN, [
'-u',
const exitCode = await runScript([
calibScriptPath,
'--camera', camera,
'--input-dir', sessionDir,
'--output-dir', sessionDir,
]);
], send);
let stdoutBuf = '';
proc.stdout.on('data', (chunk) => {
stdoutBuf += chunk.toString();
const lines = stdoutBuf.split('\n');
stdoutBuf = lines.pop();
for (const line of lines) send({ type: 'log', text: line });
});
let stderrBuf = '';
proc.stderr.on('data', (chunk) => {
stderrBuf += chunk.toString();
const lines = stderrBuf.split('\n');
stderrBuf = lines.pop();
for (const line of lines) send({ type: 'log', text: `[stderr] ${line}` });
});
proc.on('error', (err) => {
console.error('calibration/compute spawn error:', err);
send({ type: 'log', text: `Fehler beim Starten: ${err.message}` });
send({ type: 'done', exitCode: -1 });
if (!res.writableEnded) res.end();
});
proc.on('close', (code) => {
if (stdoutBuf) send({ type: 'log', text: stdoutBuf });
if (stderrBuf) send({ type: 'log', text: `[stderr] ${stderrBuf}` });
send({ type: 'done', exitCode: code ?? -1 });
if (!res.writableEnded) res.end();
});
send({ type: 'done', exitCode });
if (!res.writableEnded) res.end();
} catch (err) {
// Fehler VOR flushHeaders → normaler JSON-Fehler
@@ -426,6 +404,174 @@ app.post('/api/calibration/compute', async (req, res) => {
}
});
// ── Board-Erkennung ───────────────────────────────────────────────────────────
const boardDataDir = path.join(__dirname, '..', 'data', 'board');
const ROBOT_JSON = process.env.ROBOT_JSON
|| path.join(__dirname, '..', 'scripts', 'robot_1781069752019.json');
const SCRIPT_1 = path.join(__dirname, '..', 'scripts', '1_detect_aruco_observations.py');
const SCRIPT_2 = path.join(__dirname, '..', 'scripts', '2_estimate_camera_from_observations.py');
/**
* Führt ein Python-Script aus und leitet stdout/stderr zeilenweise an `send` weiter.
* Gibt den Exit-Code zurück (Promise<number>).
*/
function runScript(args, send) {
return new Promise((resolve) => {
const proc = spawn(PYTHON_BIN, ['-u', ...args]);
let outBuf = '';
proc.stdout.on('data', chunk => {
outBuf += chunk.toString();
const lines = outBuf.split('\n');
outBuf = lines.pop();
for (const line of lines) send({ type: 'log', text: line });
});
let errBuf = '';
proc.stderr.on('data', chunk => {
errBuf += chunk.toString();
const lines = errBuf.split('\n');
errBuf = lines.pop();
for (const line of lines) send({ type: 'log', text: `[stderr] ${line}` });
});
proc.on('error', err => {
send({ type: 'log', text: `Fehler beim Starten: ${err.message}` });
resolve(-1);
});
proc.on('close', code => {
if (outBuf) send({ type: 'log', text: outBuf });
if (errBuf) send({ type: 'log', text: `[stderr] ${errBuf}` });
resolve(code ?? -1);
});
});
}
/**
* POST /api/board/run
* 1. Erstellt data/board/{timestamp}/
* 2. Holt Snapshot jeder Kamera
* 3. Für jede Kamera: Script 1 (ArUco-Erkennung) → Script 2 (Kamera-Pose)
* SSE-Stream während der Ausführung.
*/
app.post('/api/board/run', async (req, res) => {
try {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
const send = (obj) => {
if (!res.writableEnded) res.write(`data: ${JSON.stringify(obj)}\n\n`);
};
// 1. Temp-Verzeichnis
const ts = makeTimestamp();
const runDir = path.join(boardDataDir, ts);
await fsPromises.mkdir(runDir, { recursive: true });
send({ type: 'log', text: `▶ Board-Run: ${ts}` });
send({ type: 'log', text: `▶ Ordner: ${runDir}` });
send({ type: 'log', text: `▶ Robot-JSON: ${ROBOT_JSON}` });
send({ type: 'log', text: '' });
// 2. Kameras ermitteln
if (!WEBCAM_URL) throw new Error('WEBCAM_URL nicht konfiguriert');
const camData = await new WebcamClient(WEBCAM_URL).getCameras();
const cameraIds = (camData.cameras ?? []).map(c => c.id);
send({ type: 'log', text: `▶ Kameras: ${cameraIds.join(', ')}` });
// 3. Aktuelle Kalibrierungs-Session für NPZ-Dateien
const calibSession = await findLatestCalibSession();
if (!calibSession) throw new Error('Keine Kalibrierungs-Session. Bitte zuerst Camera NPZ kalibrieren.');
send({ type: 'log', text: `▶ NPZ-Session: ${calibSession}` });
send({ type: 'log', text: '' });
// 4. Pro Kamera: Foto → Script 1 → Script 2
for (const camId of cameraIds) {
send({ type: 'log', text: `─── ${camId} ${'─'.repeat(40 - camId.length)}` });
// Snapshot
send({ type: 'log', text: 'Foto aufnehmen …' });
let snapResp;
for (let attempt = 1; attempt <= 2; attempt++) {
snapResp = await new WebcamClient(WEBCAM_URL).getSnapshot(camId, true);
if (snapResp.status !== 503) break;
if (attempt < 2) await new Promise(r => setTimeout(r, 2000));
}
if (!snapResp.ok) {
send({ type: 'log', text: `⚠ HTTP ${snapResp.status} Kamera übersprungen` });
continue;
}
const imgPath = path.join(runDir, `${camId}.jpg`);
await fsPromises.writeFile(imgPath, Buffer.from(await snapResp.arrayBuffer()));
send({ type: 'log', text: `✅ Foto: ${camId}.jpg` });
// NPZ prüfen
const npzPath = path.join(calibDataDir, calibSession, `${camId}_calibration.npz`);
try { await fsPromises.access(npzPath); }
catch {
send({ type: 'log', text: `⚠ Keine NPZ (${camId}_calibration.npz) übersprungen` });
continue;
}
// Script 1 ArUco-Erkennung
send({ type: 'log', text: '\n▷ 1_detect_aruco_observations' });
const exit1 = await runScript([
SCRIPT_1,
'-i', imgPath,
'-npz', npzPath,
'-robot', ROBOT_JSON,
'-cameraId', camId,
'-outDir', runDir,
'--saveDebugImage',
], send);
if (exit1 !== 0) {
send({ type: 'log', text: `❌ Script 1 Exit ${exit1}` });
continue;
}
// Script 2 Kamera-Pose schätzen
const detJson = path.join(runDir, `${camId}_aruco_detection.json`);
try { await fsPromises.access(detJson); }
catch {
send({ type: 'log', text: '⚠ Detection-JSON fehlt Script 2 übersprungen' });
continue;
}
send({ type: 'log', text: '\n▷ 2_estimate_camera_from_observations' });
const exit2 = await runScript([
SCRIPT_2,
'-i', detJson,
'-robot', ROBOT_JSON,
'-outDir', runDir,
], send);
if (exit2 !== 0) {
send({ type: 'log', text: `❌ Script 2 Exit ${exit2}` });
}
send({ type: 'log', text: '' });
}
send({ type: 'log', text: `✅ Board-Run abgeschlossen: ${ts}` });
send({ type: 'done', exitCode: 0, runDir: ts });
if (!res.writableEnded) res.end();
} catch (err) {
console.error('board/run error:', err);
if (!res.headersSent) {
res.status(500).json({ error: String(err) });
} else {
try {
res.write(`data: ${JSON.stringify({ type: 'log', text: `${err.message}` })}\n\n`);
res.write(`data: ${JSON.stringify({ type: 'done', exitCode: -1 })}\n\n`);
res.end();
} catch {}
}
}
});
/**
* POST /api/calibration/upload-npz
* Liest {camera}_calibration.npz aus der aktuellen Session und