Board Kamera-Position

This commit is contained in:
chk
2026-06-10 14:58:14 +02:00
parent 1032c53630
commit 5285ed468b
3 changed files with 163 additions and 115 deletions

View File

@@ -22,8 +22,6 @@
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
} }
/* ── Topbar ── */
#topbar { #topbar {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -34,12 +32,13 @@
flex-shrink: 0; flex-shrink: 0;
flex-wrap: wrap; flex-wrap: wrap;
} }
.legend { display: flex; gap: 12px; align-items: center; } .legend { display: flex; gap: 10px; align-items: center; }
.dot { .dot {
display: inline-block; width: 10px; height: 10px; display: inline-block; width: 10px; height: 10px;
border-radius: 2px; margin-right: 3px; vertical-align: middle; border-radius: 2px; margin-right: 3px; vertical-align: middle;
} }
#status { color: var(--muted); flex: 1; min-width: 120px; } .dot.circle { border-radius: 50%; }
#status { color: var(--muted); flex: 1; min-width: 100px; }
#stats { color: var(--accent); } #stats { color: var(--accent); }
.btn { .btn {
background: #1e293b; background: #1e293b;
@@ -52,26 +51,18 @@
font-size: 11px; font-size: 11px;
} }
.btn:hover { border-color: var(--accent); color: var(--accent); } .btn:hover { border-color: var(--accent); color: var(--accent); }
/* ── Canvas ── */
#canvas-wrap { flex: 1; position: relative; overflow: hidden; } #canvas-wrap { flex: 1; position: relative; overflow: hidden; }
canvas { display: block; width: 100%; height: 100%; } canvas { display: block; width: 100%; height: 100%; }
/* ── Hint overlay ── */
#hint { #hint {
position: absolute; position: absolute; bottom: 6px; right: 10px;
bottom: 6px; right: 10px; font-size: 9px; color: var(--muted); pointer-events: none;
font-size: 9px;
color: var(--muted);
pointer-events: none;
} }
</style> </style>
<script type="importmap"> <script type="importmap">
{ {
"imports": { "imports": {
"three": "https://unpkg.com/three@0.162.0/build/three.module.js", "three": "https://unpkg.com/three@0.162.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.162.0/examples/jsm/" "three/addons/": "https://unpkg.com/three@0.162.0/examples/jsm/"
} }
} }
</script> </script>
@@ -82,6 +73,7 @@
<div class="legend"> <div class="legend">
<span><span class="dot" style="background:#22c55e"></span>Erkannt</span> <span><span class="dot" style="background:#22c55e"></span>Erkannt</span>
<span><span class="dot" style="background:#ef4444"></span>Nicht erkannt</span> <span><span class="dot" style="background:#ef4444"></span>Nicht erkannt</span>
<span><span class="dot circle" style="background:#fbbf24"></span>Gemessen (3b)</span>
<span><span class="dot" style="background:#9b7bff"></span>Kamera</span> <span><span class="dot" style="background:#9b7bff"></span>Kamera</span>
</div> </div>
<span id="stats"></span> <span id="stats"></span>
@@ -98,13 +90,12 @@
import * as THREE from 'three'; import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const S = 1 / 1000; // mm → m const S = 1 / 1000; // mm → m
// robot (x=right, y=backward, z=up) → Three.js (x=right, y=up, z=toward viewer) // robot (x=right, y=backward, z=up) → Three.js (x=right, y=up, z=toward viewer)
function r2v(rx, ry, rz) { return new THREE.Vector3(rx * S, rz * S, -ry * S); } function r2v(rx, ry, rz) { return new THREE.Vector3(rx * S, rz * S, -ry * S); }
function r2vArr([rx, ry, rz]) { return r2v(rx, ry, rz); } function r2vArr([rx, ry, rz]) { return r2v(rx, ry, rz); }
// direction vector (no scale) function r2dir([dx, dy, dz]) { return new THREE.Vector3(dx, dz, -dy).normalize(); }
function r2dir([dx, dy, dz]) { return new THREE.Vector3(dx, dz, -dy).normalize(); }
// ── Renderer ────────────────────────────────────────────────────────────────── // ── Renderer ──────────────────────────────────────────────────────────────────
const canvas = document.getElementById('cv'); const canvas = document.getElementById('cv');
@@ -114,7 +105,6 @@ renderer.setPixelRatio(devicePixelRatio);
const scene = new THREE.Scene(); const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0d0f13); scene.background = new THREE.Color(0x0d0f13);
// Initial camera: top-angled view, board center ≈ (0.49, -0.03, 0.015)
const CAM_TARGET = new THREE.Vector3(0.49, -0.03, 0.015); const CAM_TARGET = new THREE.Vector3(0.49, -0.03, 0.015);
const cam = new THREE.PerspectiveCamera(45, 1, 0.001, 20); const cam = new THREE.PerspectiveCamera(45, 1, 0.001, 20);
cam.position.set(0.49, 1.5, 0.85); cam.position.set(0.49, 1.5, 0.85);
@@ -125,20 +115,19 @@ controls.target.copy(CAM_TARGET);
controls.enableDamping = true; controls.enableDamping = true;
controls.dampingFactor = 0.08; controls.dampingFactor = 0.08;
// Lighting scene.add(new THREE.AmbientLight(0xffffff, 0.8));
scene.add(new THREE.AmbientLight(0xffffff, 0.75));
const sun = new THREE.DirectionalLight(0xfff4e0, 0.6); const sun = new THREE.DirectionalLight(0xfff4e0, 0.6);
sun.position.set(-0.5, 1.2, 0.5); sun.position.set(-0.5, 1.2, 0.5);
scene.add(sun); scene.add(sun);
// World-origin axes
scene.add(new THREE.AxesHelper(0.08)); scene.add(new THREE.AxesHelper(0.08));
// ── Groups ──────────────────────────────────────────────────────────────────── // ── Groups ────────────────────────────────────────────────────────────────────
const gPaper = new THREE.Group(); // white A0 plane const gPaper = new THREE.Group(); // weißes A0-Papier
const gMarkers = new THREE.Group(); // ArUco squares const gMarkers = new THREE.Group(); // Modell-Rechtecke
const gCameras = new THREE.Group(); // camera frustums const gMeasured = new THREE.Group(); // gemessene Positionen (3b)
scene.add(gPaper, gMarkers, gCameras); const gCameras = new THREE.Group(); // Kamera-Frusta
scene.add(gPaper, gMarkers, gMeasured, gCameras);
function clearGroup(g) { function clearGroup(g) {
while (g.children.length) { while (g.children.length) {
@@ -151,17 +140,7 @@ function clearGroup(g) {
} }
// ── Geometry helpers ────────────────────────────────────────────────────────── // ── Geometry helpers ──────────────────────────────────────────────────────────
function makePlane(w, d, color, opacity = 1) { function makeMarkerSquare(pos, size, color) {
const geo = new THREE.PlaneGeometry(w, d);
geo.rotateX(-Math.PI / 2);
const mat = new THREE.MeshPhongMaterial({
color, side: THREE.DoubleSide,
transparent: opacity < 1, opacity,
});
return new THREE.Mesh(geo, mat);
}
function makeSquareMarker(pos, size, color) {
const geo = new THREE.PlaneGeometry(size, size); const geo = new THREE.PlaneGeometry(size, size);
geo.rotateX(-Math.PI / 2); geo.rotateX(-Math.PI / 2);
const mat = new THREE.MeshPhongMaterial({ color, side: THREE.DoubleSide }); const mat = new THREE.MeshPhongMaterial({ color, side: THREE.DoubleSide });
@@ -170,19 +149,20 @@ function makeSquareMarker(pos, size, color) {
return m; return m;
} }
function makeCameraFrustum(posThree, dirThree, size) { function makeEdgeBorder(pos, size, color) {
const geo = new THREE.ConeGeometry(size * 0.55, size, 4); const geo = new THREE.EdgesGeometry(new THREE.PlaneGeometry(size, size));
geo.translate(0, -size / 2, 0); // apex → local origin const mat = new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.75 });
geo.rotateY(Math.PI / 4); const m = new THREE.LineSegments(geo, mat);
const mat = new THREE.MeshPhongMaterial({ m.rotation.x = -Math.PI / 2;
color: 0x9b7bff, transparent: true, opacity: 0.55, side: THREE.DoubleSide, m.position.copy(pos);
}); return m;
const m = new THREE.Mesh(geo, mat); }
m.position.copy(posThree);
const d = dirThree.clone().normalize(); function makeSphere(pos, radius, color) {
if (d.lengthSq() > 1e-9) { const geo = new THREE.SphereGeometry(radius, 10, 8);
m.quaternion.setFromUnitVectors(new THREE.Vector3(0, -1, 0), d); const mat = new THREE.MeshPhongMaterial({ color, shininess: 80 });
} const m = new THREE.Mesh(geo, mat);
m.position.copy(pos);
return m; return m;
} }
@@ -192,105 +172,150 @@ function makeLine(p1, p2, color, opacity = 1) {
return new THREE.Line(geo, mat); return new THREE.Line(geo, mat);
} }
// ── Build scene from API data ───────────────────────────────────────────────── function makeCameraFrustum(posThree, dirThree, size) {
const geo = new THREE.ConeGeometry(size * 0.55, size, 4);
geo.translate(0, -size / 2, 0);
geo.rotateY(Math.PI / 4);
const mat = new THREE.MeshPhongMaterial({
color: 0x9b7bff, transparent: true, opacity: 0.55, side: THREE.DoubleSide,
});
const m = new THREE.Mesh(geo, mat);
m.position.copy(posThree);
const d = dirThree.clone().normalize();
if (d.lengthSq() > 1e-9) m.quaternion.setFromUnitVectors(new THREE.Vector3(0, -1, 0), d);
return m;
}
// ── Szene aus API-Daten aufbauen ──────────────────────────────────────────────
function buildScene(data) { function buildScene(data) {
clearGroup(gPaper); clearGroup(gPaper); clearGroup(gMarkers); clearGroup(gMeasured); clearGroup(gCameras);
clearGroup(gMarkers);
clearGroup(gCameras);
const { robot, detections, cameraPoses } = data; const { robot, detections, cameraPoses, measuredMarkers } = data;
// Alle erkannten Marker-IDs über alle Kameras sammeln // Alle erkannten Marker-IDs (über alle Kameras)
const detectedIds = new Set(); const detectedIds = new Set();
for (const det of (detections ?? [])) { for (const det of (detections ?? [])) {
for (const id of (det.detectedMarkerIds ?? [])) detectedIds.add(id); for (const id of (det.detectedMarkerIds ?? [])) detectedIds.add(id);
} }
const boardMarkers = robot?.links?.Board?.markers ?? []; const boardMarkers = robot?.links?.Board?.markers ?? [];
const markerSize = (robot?.vision_config?.MarkerSize ?? 0.025); // m const markerSize = robot?.vision_config?.MarkerSize ?? 0.025; // m
// ── A0-Papier (weißes Rechteck unter den Markern) ── // ── A0-Papier (weißes Rechteck) ──
if (boardMarkers.length > 0) { if (boardMarkers.length > 0) {
let minRx = Infinity, maxRx = -Infinity; let minRx = Infinity, maxRx = -Infinity;
let minRy = Infinity, maxRy = -Infinity; let minRy = Infinity, maxRy = -Infinity;
let markerRz = -27.3; let markerRz = -27.3;
for (const m of boardMarkers) { for (const m of boardMarkers) {
const [rx, ry, rz] = m.position; const [rx, ry, rz] = m.position;
minRx = Math.min(minRx, rx); maxRx = Math.max(maxRx, rx); if (rx < minRx) minRx = rx; if (rx > maxRx) maxRx = rx;
minRy = Math.min(minRy, ry); maxRy = Math.max(maxRy, ry); if (ry < minRy) minRy = ry; if (ry > maxRy) maxRy = ry;
markerRz = rz; markerRz = rz;
} }
const pad = 40; // mm Rand const pad = 40; // mm Rand
const planeW = (maxRx - minRx + 2 * pad) * S; const planeW = (maxRx - minRx + 2 * pad) * S;
const planeH = (maxRy - minRy + 2 * pad) * S; const planeH = (maxRy - minRy + 2 * pad) * S;
const planeCx = ((minRx + maxRx) / 2) * S; const cx = ((minRx + maxRx) / 2) * S;
const planeCz = -((minRy + maxRy) / 2) * S; // robot y → Three.js -z const cz = -((minRy + maxRy) / 2) * S;
const planeCy = markerRz * S - 0.001; // leicht unter den Markern const cy = markerRz * S - 0.001;
const paper = makePlane(planeW, planeH, 0xf0ebe0); // off-white const geo = new THREE.PlaneGeometry(planeW, planeH);
paper.position.set(planeCx, planeCy, planeCz); geo.rotateX(-Math.PI / 2);
gPaper.add(paper); const mat = new THREE.MeshPhongMaterial({ color: 0xf0ebe0, side: THREE.DoubleSide });
const plane = new THREE.Mesh(geo, mat);
plane.position.set(cx, cy, cz);
gPaper.add(plane);
} }
// ── ArUco-Marker ── // ── Modell-Marker (Rechtecke) ──
let nDetected = 0;
const visSize = markerSize * 0.9; const visSize = markerSize * 0.9;
let nDetected = 0;
for (const m of boardMarkers) { for (const m of boardMarkers) {
const id = m.id; const pos = r2vArr(m.position);
const pos = r2vArr(m.position); const detected = detectedIds.has(m.id);
const detected = detectedIds.has(id);
if (detected) nDetected++; if (detected) nDetected++;
const color = detected ? 0x22c55e : 0xef4444;
const color = detected ? 0x22c55e : 0xef4444; const sq = makeMarkerSquare(pos, visSize, color);
const sq = makeSquareMarker(pos, visSize, color); sq.position.y += 0.0005;
sq.position.y += 0.0005; // ganz knapp über der Papier-Ebene
gMarkers.add(sq); gMarkers.add(sq);
// dünner Rahmen const border = makeEdgeBorder(pos, visSize, detected ? 0x4ade80 : 0xfca5a5);
const borderGeo = new THREE.EdgesGeometry(new THREE.PlaneGeometry(visSize, visSize));
const borderMat = new THREE.LineBasicMaterial({
color: detected ? 0x4ade80 : 0xfca5a5,
transparent: true, opacity: 0.8,
});
const border = new THREE.LineSegments(borderGeo, borderMat);
border.rotation.x = -Math.PI / 2;
border.position.copy(pos);
border.position.y += 0.001; border.position.y += 0.001;
gMarkers.add(border); gMarkers.add(border);
} }
// ── Kamera-Frusta ── // ── Gemessene Positionen von 3b (gelbe Punkte) ──
const boardCenter = r2v(490, 0, -27.3); let nTriangulated = 0;
for (const cp of (cameraPoses ?? [])) { const measuredById = {};
if (!cp.position_mm) continue;
const camPos = r2vArr(cp.position_mm);
// Kamera-Blickrichtung: R_wc^T · [0,0,1] = dritte Zeile von R_wc if (measuredMarkers?.markers?.length > 0) {
let dirWorld = [0, 0, 1]; // Nur A0-Marker (Board-Link)
if (cp.rotation_matrix?.length >= 3) dirWorld = cp.rotation_matrix[2]; const a0markers = measuredMarkers.markers.filter(m =>
const dirThree = r2dir(dirWorld); m.link === 'Board' || boardMarkers.some(bm => bm.id === m.marker_id)
);
gCameras.add(makeCameraFrustum(camPos, dirThree, 0.07)); for (const m of a0markers) {
gCameras.add(makeLine(camPos, boardCenter, 0x9b7bff, 0.35)); nTriangulated++;
const mpos = r2vArr(m.position_mm);
mpos.y += 0.004; // leicht über der Papier-Ebene
measuredById[m.marker_id] = mpos;
// Kleiner Sphere an Kamera-Position // Gelber Punkt an gemessener Position
const sg = new THREE.SphereGeometry(0.012, 10, 8); const dot = makeSphere(mpos, 0.0055, 0xfbbf24);
const sm = new THREE.MeshPhongMaterial({ color: 0x9b7bff }); gMeasured.add(dot);
const sp = new THREE.Mesh(sg, sm);
sp.position.copy(camPos); // Verbindungslinie zum Modell-Mittelpunkt
gCameras.add(sp); const modelMarker = boardMarkers.find(bm => bm.id === m.marker_id);
if (modelMarker) {
const modelPos = r2vArr(modelMarker.position);
modelPos.y += 0.002;
gMeasured.add(makeLine(modelPos, mpos, 0x78716c, 0.5));
}
}
// Kamera-Frusta aus 3b (falls vorhanden, aktualisiert)
if (measuredMarkers.cameras?.length > 0) {
for (const c of measuredMarkers.cameras) {
const cpos = r2vArr(c.position_mm);
const cdir = r2dir(c.direction);
gCameras.add(makeCameraFrustum(cpos, cdir, 0.07));
const boardCenter = r2v(490, 0, -27.3);
gCameras.add(makeLine(cpos, boardCenter, 0x9b7bff, 0.35));
gCameras.add(makeSphere(cpos, 0.012, 0x9b7bff));
}
}
}
// Falls keine 3b-Daten: Kamera-Frusta aus camera_pose-Dateien
if (!measuredMarkers?.cameras?.length) {
const boardCenter = r2v(490, 0, -27.3);
for (const cp of (cameraPoses ?? [])) {
if (!cp.position_mm) continue;
const cpos = r2vArr(cp.position_mm);
let dirWorld = [0, 0, 1];
if (cp.rotation_matrix?.length >= 3) dirWorld = cp.rotation_matrix[2];
const cdir = r2dir(dirWorld);
gCameras.add(makeCameraFrustum(cpos, cdir, 0.07));
gCameras.add(makeLine(cpos, boardCenter, 0x9b7bff, 0.35));
gCameras.add(makeSphere(cpos, 0.012, 0x9b7bff));
}
} }
// ── Stats-Zeile ── // ── Stats-Zeile ──
const camInfo = (cameraPoses ?? []).map(cp => { const camInfo = (cameraPoses ?? []).map(cp => {
const rms = cp.rms_px != null ? `${cp.rms_px.toFixed(1)} px` : '?'; const rms = cp.rms_px != null ? `${cp.rms_px.toFixed(1)} px` : '?';
return `${cp.cameraId}: ${cp.usedMarkerIds?.length ?? 0} Marker, ${rms} RMS`; return `${cp.cameraId}: ${rms}`;
}).join(' │ '); }).join(' │ ');
const triInfo = nTriangulated > 0
? ` │ 3b: ${nTriangulated} trianguliert`
: (measuredMarkers === null ? ' │ 3b: ' : ' │ 3b: <2 Kameras');
document.getElementById('stats').textContent = document.getElementById('stats').textContent =
`Erkannt ${nDetected}/${boardMarkers.length}` + `Erkannt ${nDetected}/${boardMarkers.length}${triInfo}` +
(camInfo ? `${camInfo}` : ''); (camInfo ? ` RMS: ${camInfo}` : '');
} }
// ── Daten laden ─────────────────────────────────────────────────────────────── // ── Daten laden ───────────────────────────────────────────────────────────────
@@ -304,7 +329,7 @@ async function loadData() {
if (!data.runDir) { if (!data.runDir) {
statusEl.textContent = 'Kein Board-Run vorhanden.'; statusEl.textContent = 'Kein Board-Run vorhanden.';
document.getElementById('stats').textContent = ''; document.getElementById('stats').textContent = '';
clearGroup(gPaper); clearGroup(gMarkers); clearGroup(gCameras); clearGroup(gPaper); clearGroup(gMarkers); clearGroup(gMeasured); clearGroup(gCameras);
return; return;
} }
buildScene(data); buildScene(data);
@@ -316,8 +341,6 @@ async function loadData() {
loadData(); loadData();
document.getElementById('btnReload').addEventListener('click', loadData); document.getElementById('btnReload').addEventListener('click', loadData);
// Reload-Trigger vom Parent-Frame (nach Board-Run)
window.addEventListener('message', (e) => { window.addEventListener('message', (e) => {
if (e.data?.type === 'reload') loadData(); if (e.data?.type === 'reload') loadData();
}); });
@@ -325,9 +348,8 @@ window.addEventListener('message', (e) => {
// ── Resize & Render-Loop ────────────────────────────────────────────────────── // ── Resize & Render-Loop ──────────────────────────────────────────────────────
function onResize() { function onResize() {
const wrap = document.getElementById('canvas-wrap'); const wrap = document.getElementById('canvas-wrap');
const w = wrap.clientWidth, h = wrap.clientHeight; renderer.setSize(wrap.clientWidth, wrap.clientHeight);
renderer.setSize(w, h); cam.aspect = wrap.clientWidth / wrap.clientHeight;
cam.aspect = w / h;
cam.updateProjectionMatrix(); cam.updateProjectionMatrix();
} }
window.addEventListener('resize', onResize); window.addEventListener('resize', onResize);

View File

@@ -41,11 +41,11 @@ def load_cameras(eval_dir: str) -> Dict[str, dict]:
cams: Dict[str, dict] = {} cams: Dict[str, dict] = {}
for det_path in glob.glob(os.path.join(eval_dir, "*_aruco_detection.json")): for det_path in glob.glob(os.path.join(eval_dir, "*_aruco_detection.json")):
base = os.path.basename(det_path) base = os.path.basename(det_path)
m = re.match(r"render_([A-Za-z0-9]+)_aruco_detection\.json", base) m = re.match(r"(.+)_aruco_detection\.json", base)
if not m: if not m:
continue continue
cam_id = m.group(1) cam_id = m.group(1)
pose_path = os.path.join(eval_dir, f"render_{cam_id}_camera_pose.json") pose_path = os.path.join(eval_dir, f"{cam_id}_camera_pose.json")
if not os.path.exists(pose_path): if not os.path.exists(pose_path):
print(f"[WARN] no pose for camera {cam_id}, skipping") print(f"[WARN] no pose for camera {cam_id}, skipping")
continue continue

View File

@@ -411,6 +411,7 @@ const ROBOT_JSON = process.env.ROBOT_JSON
|| path.join(__dirname, '..', 'scripts', 'robot_1781069752019.json'); || path.join(__dirname, '..', 'scripts', 'robot_1781069752019.json');
const SCRIPT_1 = path.join(__dirname, '..', 'scripts', '1_detect_aruco_observations.py'); 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'); const SCRIPT_2 = path.join(__dirname, '..', 'scripts', '2_estimate_camera_from_observations.py');
const SCRIPT_3B = path.join(__dirname, '..', 'scripts', '3b_corner_marker_poses.py');
/** /**
* Führt ein Python-Script aus und leitet stdout/stderr zeilenweise an `send` weiter. * Führt ein Python-Script aus und leitet stdout/stderr zeilenweise an `send` weiter.
@@ -554,6 +555,24 @@ app.post('/api/board/run', async (req, res) => {
send({ type: 'log', text: '' }); send({ type: 'log', text: '' });
} }
// ── Script 3b: Marker-Triangulierung (benötigt ≥2 Kamera-Posen) ──
send({ type: 'log', text: '' });
send({ type: 'log', text: '─── 3b: Marker-Triangulierung ────────────────────────────' });
const runFiles3b = await fsPromises.readdir(runDir);
const numPoses = runFiles3b.filter(f => f.endsWith('_camera_pose.json')).length;
if (numPoses >= 2) {
send({ type: 'log', text: `▷ 3b_corner_marker_poses (${numPoses} Kamera-Posen)` });
const exit3b = await runScript([
SCRIPT_3B,
'--evalDir', runDir,
'--robot', ROBOT_JSON,
], send);
if (exit3b !== 0) send({ type: 'log', text: `❌ Script 3b Exit ${exit3b}` });
} else {
send({ type: 'log', text: `⚠ Nur ${numPoses} Kamera-Pose(n) vorhanden Script 3b braucht ≥2 Kameras für Triangulierung, wird übersprungen.` });
}
send({ type: 'log', text: '' });
send({ type: 'log', text: `✅ Board-Run abgeschlossen: ${ts}` }); send({ type: 'log', text: `✅ Board-Run abgeschlossen: ${ts}` });
send({ type: 'done', exitCode: 0, runDir: ts }); send({ type: 'done', exitCode: 0, runDir: ts });
if (!res.writableEnded) res.end(); if (!res.writableEnded) res.end();
@@ -633,7 +652,14 @@ app.get('/api/board/latest', async (req, res) => {
} }
} }
return res.json({ runDir: runName, robot, detections, cameraPoses }); // aruco_marker_poses.json (Ausgabe von 3b_corner_marker_poses.py)
let measuredMarkers = null;
try {
const raw = await fsPromises.readFile(path.join(runDir, 'aruco_marker_poses.json'), 'utf8');
measuredMarkers = JSON.parse(raw);
} catch {}
return res.json({ runDir: runName, robot, detections, cameraPoses, measuredMarkers });
} catch (err) { } catch (err) {
return res.status(500).json({ error: String(err) }); return res.status(500).json({ error: String(err) });
} }