diff --git a/public/calibration.html b/public/calibration.html index 85f231d..319491a 100644 --- a/public/calibration.html +++ b/public/calibration.html @@ -155,6 +155,31 @@ .status-badge.done { color: #34d399; background: #064e3b; } .status-badge.wip { color: #60a5fa; } + /* ===== INFO GRID ===== */ + + .info-grid { + display: grid; + grid-template-columns: 160px 1fr; + gap: 6px 12px; + margin-top: 14px; + font-size: 13px; + } + + .info-label { + color: var(--muted); + } + + .info-value { + color: var(--text); + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + } + + /* Buttons: aktiv vs. deaktiviert visuell unterscheiden */ + .controls button:disabled { + opacity: 0.35; + cursor: not-allowed; + } + @@ -183,22 +208,33 @@
+
-

Camera NPZ offen

-
- Ziel: Intrinsische Kameraparameter (Kameramatrix, Verzerrungskoeffizienten) für jede - Kamera ermitteln und als .npz-Datei speichern.

- Geplante Aktionen: Fotos aufnehmen (verschiedene Posen) · Kalibrierung berechnen · - Reprojektionsfehler anzeigen · Datei speichern.

- Aktionen werden ergänzt sobald das Konzept feststeht. -
-
- - - +

Aktuelle Kalibrierung

+
+ Timestamp + + + Erstellt am + + + Bilder / Kameras +
+ +
+

Aktionen

+
+ + + + +
+
+ +

Ausgabe / Log

@@ -304,7 +340,7 @@
diff --git a/server/server.js b/server/server.js index 30c82ed..e6f8878 100755 --- a/server/server.js +++ b/server/server.js @@ -194,6 +194,144 @@ app.post('/api/estimate', async (req, res) => { } }); +// ── Kalibrierung ───────────────────────────────────────────────────────────── + +const calibDataDir = path.join(__dirname, '..', 'data', 'calibration'); + +/** Timestamp-String im Format YYYYMMDD_HHmmss */ +function makeTimestamp() { + const now = new Date(); + const p = (n, l = 2) => String(n).padStart(l, '0'); + return `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}_${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`; +} + +/** Neueste Kalibrierungs-Session (Verzeichnisname) oder null */ +async function findLatestCalibSession() { + try { + await fsPromises.access(calibDataDir); + const entries = await fsPromises.readdir(calibDataDir, { withFileTypes: true }); + const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort().reverse(); + return dirs[0] ?? null; + } catch { + return null; + } +} + +/** Liest meta.json einer Session */ +async function readCalibMeta(sessionName) { + try { + const raw = await fsPromises.readFile(path.join(calibDataDir, sessionName, 'meta.json'), 'utf8'); + return JSON.parse(raw); + } catch { + return null; + } +} + +/** Schreibt meta.json einer Session */ +async function writeCalibMeta(sessionName, meta) { + await fsPromises.writeFile( + path.join(calibDataDir, sessionName, 'meta.json'), + JSON.stringify(meta, null, 2), + 'utf8' + ); +} + +/** + * Holt Snapshots aller verfügbaren Kameras und speichert sie im Session-Verzeichnis. + * Dateiname: {cameraId}_{setNr}.jpg (setNr = 001, 002, …) + */ +async function capturePhotos(sessionName) { + if (!WEBCAM_URL) throw new Error('WEBCAM_URL ist nicht konfiguriert – keine Kameras erreichbar'); + + const wc = new WebcamClient(WEBCAM_URL); + const data = await wc.getCameras(); + const cameraIds = (data.cameras ?? []).map(c => c.id); + if (cameraIds.length === 0) throw new Error('Keine Kameras vom WebCam-Service gemeldet'); + + // Nächste Set-Nummer bestimmen (höchste vorhandene + 1) + const sessionDir = path.join(calibDataDir, sessionName); + const existing = await fsPromises.readdir(sessionDir); + const maxSet = existing.reduce((max, f) => { + const m = f.match(/_(\d+)\.jpg$/); + return m ? Math.max(max, parseInt(m[1], 10)) : max; + }, 0); + const setNr = String(maxSet + 1).padStart(3, '0'); + + const savedFiles = []; + for (const camId of cameraIds) { + const response = await new WebcamClient(WEBCAM_URL).getSnapshot(camId, true); + const buffer = Buffer.from(await response.arrayBuffer()); + const filename = `${camId}_${setNr}.jpg`; + await fsPromises.writeFile(path.join(sessionDir, filename), buffer); + savedFiles.push(filename); + } + + return { cameraIds, savedFiles, setNr: parseInt(setNr, 10) }; +} + +/** GET /api/calibration/current — aktuelle Session-Info */ +app.get('/api/calibration/current', async (req, res) => { + try { + const session = await findLatestCalibSession(); + if (!session) return res.json({ session: null, meta: null }); + const meta = await readCalibMeta(session); + return res.json({ session, meta }); + } catch (err) { + return res.status(500).json({ error: String(err) }); + } +}); + +/** POST /api/calibration/new — neue Session anlegen + erste Fotos */ +app.post('/api/calibration/new', async (req, res) => { + try { + const ts = makeTimestamp(); + const sessionDir = path.join(calibDataDir, ts); + await fsPromises.mkdir(sessionDir, { recursive: true }); + + const meta = { timestamp: ts, createdAt: new Date().toISOString(), cameras: [], imageSets: 0, imageCount: 0 }; + await writeCalibMeta(ts, meta); + + try { + const capture = await capturePhotos(ts); + meta.cameras = capture.cameraIds; + meta.imageSets = capture.setNr; + meta.imageCount = capture.setNr * capture.cameraIds.length; + await writeCalibMeta(ts, meta); + return res.json({ session: ts, meta, savedFiles: capture.savedFiles }); + } catch (captureErr) { + // Session angelegt, aber Fotos nicht verfügbar → trotzdem OK zurück + return res.json({ session: ts, meta, warning: String(captureErr) }); + } + } catch (err) { + return res.status(500).json({ error: String(err) }); + } +}); + +/** POST /api/calibration/foto — weitere Fotos für aktuelle Session */ +app.post('/api/calibration/foto', async (req, res) => { + try { + const session = await findLatestCalibSession(); + if (!session) { + return res.status(400).json({ error: 'Keine Session vorhanden. Bitte zuerst "Neue Kalibrierung anlegen".' }); + } + + const capture = await capturePhotos(session); + + const meta = await readCalibMeta(session) ?? { + timestamp: session, createdAt: new Date().toISOString(), + cameras: [], imageSets: 0, imageCount: 0 + }; + meta.cameras = capture.cameraIds; + meta.imageSets = capture.setNr; + meta.imageCount = capture.setNr * capture.cameraIds.length; + await writeCalibMeta(session, meta); + + return res.json({ session, meta, savedFiles: capture.savedFiles }); + } catch (err) { + return res.status(500).json({ error: String(err) }); + } +}); + async function checkServiceReachability(name, url) { try { const controller = new AbortController();