Anthropic Claude

This commit is contained in:
chk
2026-06-01 16:36:33 +02:00
parent 761919bb93
commit 6655fc6f02
49 changed files with 8379 additions and 7764 deletions

894
run/robot_viewer.html Normal file
View File

@@ -0,0 +1,894 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Robot FK Viewer</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0d0f13;
--panel: #161920;
--border: #2a2d35;
--text: #c8cdd8;
--accent: #4a9eff;
--muted: #555b6e;
--ok: #3ecf6b;
--warn: #f59e0b;
--err: #ff4f4f;
--panel-w: 300px;
}
body {
background: var(--bg);
color: var(--text);
font: 13px/1.5 'IBM Plex Mono', 'Cascadia Code', 'Courier New', monospace;
display: flex;
height: 100vh;
overflow: hidden;
}
/* ── sidebar ── */
#sidebar {
width: var(--panel-w);
flex-shrink: 0;
background: var(--panel);
border-right: 1px solid var(--border);
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0;
}
.section {
border-bottom: 1px solid var(--border);
padding: 12px 14px;
}
.section h3 {
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 8px;
}
/* file inputs */
.file-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.file-row label {
font-size: 11px;
color: var(--muted);
width: 58px;
flex-shrink: 0;
}
.file-row input[type=file] {
flex: 1;
font-size: 10px;
color: var(--text);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 3px;
padding: 3px 6px;
cursor: pointer;
}
.file-row input[type=file]::file-selector-button {
background: var(--border);
color: var(--text);
border: none;
padding: 2px 8px;
cursor: pointer;
font-family: inherit;
font-size: 10px;
border-radius: 2px;
margin-right: 6px;
}
/* joint sliders */
.slider-row {
display: grid;
grid-template-columns: 22px 1fr 48px;
align-items: center;
gap: 6px;
margin-bottom: 5px;
}
.slider-row span { color: var(--accent); font-size: 12px; }
input[type=range] {
-webkit-appearance: none;
height: 3px;
background: var(--border);
border-radius: 2px;
outline: none;
cursor: pointer;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px; height: 12px;
background: var(--accent);
border-radius: 50%;
}
.val-box {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 3px;
padding: 2px 4px;
font-size: 11px;
text-align: right;
color: var(--text);
width: 48px;
}
/* toggles */
.toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 5px;
}
.toggle-label { font-size: 11px; color: var(--text); }
.toggle {
position: relative; width: 36px; height: 18px;
}
.toggle input { display: none; }
.slider-track {
position: absolute; inset: 0;
background: var(--border);
border-radius: 9px;
cursor: pointer;
transition: background .2s;
}
.slider-track::after {
content: '';
position: absolute;
left: 2px; top: 2px;
width: 14px; height: 14px;
background: var(--muted);
border-radius: 50%;
transition: transform .2s, background .2s;
}
.toggle input:checked + .slider-track { background: var(--accent); }
.toggle input:checked + .slider-track::after {
transform: translateX(18px);
background: #fff;
}
/* stats */
#stats-content {
font-size: 11px;
line-height: 1.8;
}
.stat-line { display: flex; justify-content: space-between; }
.stat-val { color: var(--accent); }
.stat-ok { color: var(--ok); }
.stat-warn { color: var(--warn); }
.stat-err { color: var(--err); }
#status-bar {
font-size: 10px;
color: var(--muted);
padding: 8px 14px;
border-top: 1px solid var(--border);
margin-top: auto;
}
/* canvas */
#canvas-wrap {
flex: 1;
position: relative;
overflow: hidden;
}
canvas { display: block; width: 100%; height: 100%; }
</style>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.162.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.162.0/examples/jsm/"
}
}
</script>
</head>
<body>
<div id="sidebar">
<!-- files -->
<div class="section">
<h3>Data Files</h3>
<div class="file-row">
<label>robot.json</label>
<input type="file" id="fRobot" accept=".json">
</div>
<div class="file-row">
<label>aruco</label>
<input type="file" id="fAruco" accept=".json">
</div>
</div>
<!-- test poses -->
<div class="section" id="poseSection" style="display:none">
<h3>Test Poses</h3>
<select id="poseSelect" style="
width:100%; background:var(--bg); border:1px solid var(--border);
color:var(--text); font:inherit; font-size:11px; padding:4px 6px;
border-radius:3px; cursor:pointer; margin-bottom:4px;">
<option value="">— select pose —</option>
</select>
<div id="poseInfo" style="font-size:10px;color:var(--muted);min-height:14px;"></div>
</div>
<!-- joint sliders -->
<div class="section">
<h3>Joint Values</h3>
<div id="sliders"></div>
</div>
<!-- display toggles -->
<div class="section">
<h3>Display</h3>
<div class="toggle-row">
<span class="toggle-label">Skeleton</span>
<label class="toggle"><input type="checkbox" id="tSkeleton" checked><span class="slider-track"></span></label>
</div>
<div class="toggle-row">
<span class="toggle-label">Model markers</span>
<label class="toggle"><input type="checkbox" id="tModel" checked><span class="slider-track"></span></label>
</div>
<div class="toggle-row">
<span class="toggle-label">Model normals</span>
<label class="toggle"><input type="checkbox" id="tNormals"><span class="slider-track"></span></label>
</div>
<div class="toggle-row">
<span class="toggle-label">Observed markers</span>
<label class="toggle"><input type="checkbox" id="tObserved" checked><span class="slider-track"></span></label>
</div>
<div class="toggle-row">
<span class="toggle-label">Observed normals</span>
<label class="toggle"><input type="checkbox" id="tObsNormals"><span class="slider-track"></span></label>
</div>
<div class="toggle-row">
<span class="toggle-label">Error lines</span>
<label class="toggle"><input type="checkbox" id="tErrors" checked><span class="slider-track"></span></label>
</div>
<div class="toggle-row">
<span class="toggle-label">Board plane</span>
<label class="toggle"><input type="checkbox" id="tBoard" checked><span class="slider-track"></span></label>
</div>
</div>
<!-- stats -->
<div class="section">
<h3>Error Statistics</h3>
<div id="stats-content"><span style="color:var(--muted)">Load both files…</span></div>
</div>
<div id="status-bar">Drag to orbit · Scroll to zoom · Right-drag to pan</div>
</div>
<div id="canvas-wrap">
<canvas id="cv"></canvas>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// ═══════════════════════════════════════════════════════════════
// FK in plain JavaScript — mirrors robot_fk.py exactly
// ═══════════════════════════════════════════════════════════════
function norm(v) {
const n = Math.sqrt(v.reduce((s, x) => s + x * x, 0));
return n < 1e-12 ? v.map(() => 0) : v.map(x => x / n);
}
function mm3(A, B) { // 3×3 row-major mat-mat
const C = new Array(9).fill(0);
for (let i = 0; i < 3; i++)
for (let j = 0; j < 3; j++)
for (let k = 0; k < 3; k++)
C[i*3+j] += A[i*3+k] * B[k*3+j];
return C;
}
function rotAA(axis, deg) { // Rodrigues
const [x, y, z] = norm(axis);
const r = deg * Math.PI / 180, c = Math.cos(r), s = Math.sin(r), t = 1 - c;
return [t*x*x+c, t*x*y-s*z, t*x*z+s*y,
t*x*y+s*z, t*y*y+c, t*y*z-s*x,
t*x*z-s*y, t*y*z+s*x, t*z*z+c];
}
function rotXYZ(rx, ry, rz) {
return mm3(mm3(rotAA([0,0,1],rz), rotAA([0,1,0],ry)), rotAA([1,0,0],rx));
}
// 4×4 row-major
function makeT(R3, tx, ty, tz) {
return [R3[0],R3[1],R3[2],tx, R3[3],R3[4],R3[5],ty,
R3[6],R3[7],R3[8],tz, 0,0,0,1];
}
function I4() { return [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]; }
function mm4(A, B) {
const C = new Array(16).fill(0);
for (let i = 0; i < 4; i++)
for (let j = 0; j < 4; j++)
for (let k = 0; k < 4; k++)
C[i*4+j] += A[i*4+k] * B[k*4+j];
return C;
}
function applyT(T, p) {
return [T[0]*p[0]+T[1]*p[1]+T[2]*p[2]+T[3],
T[4]*p[0]+T[5]*p[1]+T[6]*p[2]+T[7],
T[8]*p[0]+T[9]*p[1]+T[10]*p[2]+T[11]];
}
function buildTopoOrder(links) {
const parent = Object.fromEntries(Object.entries(links).map(([n,d]) => [n, d.parent||null]));
const visited = new Set(), order = [];
const visit = n => {
if (visited.has(n)) return;
visited.add(n);
if (parent[n]) visit(parent[n]);
order.push(n);
};
Object.keys(links).forEach(visit);
return order;
}
function computeFK(robotData, joints) {
const links = robotData.links || {};
const state = {x:0,y:0,z:0,a:0,b:0,c:0,e:0, ...joints};
const T = {};
for (const lname of buildTopoOrder(links)) {
const ld = links[lname];
const par = ld.parent;
const Tp = T[par] || I4();
const mp = ld.mountPosition || [0,0,0];
const mr = ld.mountRotation || [0,0,0];
const Tm = makeT(rotXYZ(...mr), ...mp);
const jt = ld.jointToParent || {};
const jo = jt.origin || [0,0,0];
const jr = jt.rotation || [0,0,0];
const Tj = makeT(rotXYZ(...jr), ...jo);
const type = (jt.type||'').toLowerCase();
const axis = jt.axis || [1,0,0];
const v = state[jt.variable] || 0;
let Tmot = I4();
if (type === 'revolute') {
Tmot = makeT(rotAA(axis, v), 0, 0, 0);
} else if (type === 'linear') {
const [ax, ay, az] = norm(axis);
Tmot = makeT([1,0,0,0,1,0,0,0,1], ax*v, ay*v, az*v);
}
T[lname] = mm4(mm4(mm4(Tp, Tm), Tj), Tmot);
}
return T;
}
function markerWorld(T, link, local) {
return applyT(T[link] || I4(), local);
}
// ═══════════════════════════════════════════════════════════════
// Coordinate conversion robot-mm → Three.js scene (metres)
// robot: x=right, y=backward, z=up
// Three.js: x=right, y=up, z=toward viewer
// ═══════════════════════════════════════════════════════════════
const S = 1 / 1000;
function r2v(rx, ry, rz) { return new THREE.Vector3(rx*S, rz*S, -ry*S); }
function r2vArr(p) { return r2v(p[0], p[1], p[2]); }
// ═══════════════════════════════════════════════════════════════
// Three.js scene
// ═══════════════════════════════════════════════════════════════
const canvas = document.getElementById('cv');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(devicePixelRatio);
renderer.shadowMap.enabled = true;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 0.9;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0d0f13);
scene.fog = new THREE.FogExp2(0x0d0f13, 0.35);
const camera = new THREE.PerspectiveCamera(45, 1, 0.001, 20);
camera.position.set(0.35, 0.55, 1.1);
camera.lookAt(0.2, 0.05, 0);
const controls = new OrbitControls(camera, canvas);
controls.target.set(0.2, 0.05, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.08;
// Lighting
scene.add(new THREE.AmbientLight(0xffffff, 0.55));
const sun = new THREE.DirectionalLight(0xfff4e0, 1.4);
sun.position.set(-0.8, 1.2, 0.9);
sun.castShadow = true;
scene.add(sun);
const fill = new THREE.DirectionalLight(0xc0d8ff, 0.4);
fill.position.set(0.6, 0.3, -0.5);
scene.add(fill);
// Grid
const grid = new THREE.GridHelper(3, 30, 0x1e2230, 0x1a1e28);
grid.position.y = -0.028;
scene.add(grid);
// Axes helper (world origin)
const axes = new THREE.AxesHelper(0.1);
axes.position.set(0, 0, 0);
scene.add(axes);
// ─── geometry helpers ─────────────────────────────────────────
function makeCylinder(p1, p2, radius, color, opacity=1) {
const dir = p2.clone().sub(p1);
const len = dir.length();
if (len < 0.0001) return null;
const geo = new THREE.CylinderGeometry(radius, radius, len, 8, 1);
const mat = new THREE.MeshPhongMaterial({
color, transparent: opacity < 1, opacity,
shininess: 60
});
const m = new THREE.Mesh(geo, mat);
m.castShadow = true;
m.position.copy(p1.clone().add(p2).multiplyScalar(0.5));
m.quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0), dir.normalize());
return m;
}
function makeSphere(pos, radius, color) {
const geo = new THREE.SphereGeometry(radius, 12, 8);
const mat = new THREE.MeshPhongMaterial({ color, shininess: 80 });
const m = new THREE.Mesh(geo, mat);
m.position.copy(pos);
m.castShadow = true;
return m;
}
function transformDirByT(T, dir) {
return [
T[0]*dir[0] + T[1]*dir[1] + T[2]*dir[2],
T[4]*dir[0] + T[5]*dir[1] + T[6]*dir[2],
T[8]*dir[0] + T[9]*dir[1] + T[10]*dir[2],
];
}
function makeMarkerSquare(pos, normal, size, color) {
const geo = new THREE.BoxGeometry(size, size, size * 0.1);
const mat = new THREE.MeshPhongMaterial({
color,
shininess: 40
});
const m = new THREE.Mesh(geo, mat);
m.position.copy(pos);
// Fallback falls keine gültige Normale vorhanden
let nx = 0, ny = 0, nz = 1;
if (Array.isArray(normal) && normal.length >= 3) {
nx = Number(normal[0]) || 0;
ny = Number(normal[1]) || 0;
nz = Number(normal[2]) || 1;
} else if (normal instanceof THREE.Vector3) {
nx = normal.x;
ny = normal.y;
nz = normal.z;
}
const n = new THREE.Vector3(nx, ny, nz);
if (n.lengthSq() > 1e-12) {
n.normalize();
m.quaternion.setFromUnitVectors(
new THREE.Vector3(0, 0, 1),
n
);
}
return m;
}
function makeLine(p1, p2, color) {
const geo = new THREE.BufferGeometry().setFromPoints([p1, p2]);
const mat = new THREE.LineBasicMaterial({ color });
return new THREE.Line(geo, mat);
}
// ─── link colours ─────────────────────────────────────────────
const LINK_COLORS = {
Board: 0x8b6528,
Base: 0xc8c8c8,
Arm1: 0x3355cc,
Ellbow: 0xcccccc,
Arm2: 0xbbbbbb,
Hand: 0xaaaaaa,
Palm: 0x999999,
FingerA: 0xdddddd,
FingerB: 0xdddddd,
};
function linkColor(name) { return LINK_COLORS[name] ?? 0x888888; }
// ─── scene groups ─────────────────────────────────────────────
const gSkeleton = new THREE.Group();
const gModel = new THREE.Group();
const gNormals = new THREE.Group();
const gObserved = new THREE.Group();
const gObsNormals= new THREE.Group();
const gErrors = new THREE.Group();
const gBoard = new THREE.Group();
scene.add(gSkeleton, gModel, gNormals, gObserved, gObsNormals, gErrors, gBoard);
// ─── toggle wiring ────────────────────────────────────────────
const toggles = {
tSkeleton: gSkeleton,
tModel: gModel,
tNormals: gNormals,
tObserved: gObserved,
tObsNormals:gObsNormals,
tErrors: gErrors,
tBoard: gBoard,
};
for (const [id, grp] of Object.entries(toggles)) {
document.getElementById(id).addEventListener('change', e => {
grp.visible = e.target.checked;
});
}
// ═══════════════════════════════════════════════════════════════
// State
// ═══════════════════════════════════════════════════════════════
let robotData = null;
let arucoData = null;
const joints = { x:0, y:0, z:0, a:0, b:0, c:0, e:0 };
// ─── slider setup ─────────────────────────────────────────────
const JOINT_DEFS = [
{ key:'x', label:'x', min:-50, max:600, step:1, unit:'mm' },
{ key:'y', label:'y', min:-180, max:180, step:0.5,unit:'°' },
{ key:'z', label:'z', min:-180, max:180, step:0.5,unit:'°' },
{ key:'a', label:'a', min:-180, max:180, step:0.5,unit:'°' },
{ key:'b', label:'b', min:-180, max:180, step:0.5,unit:'°' },
{ key:'c', label:'c', min:-180, max:180, step:0.5,unit:'°' },
{ key:'e', label:'e', min:0, max:60, step:0.5,unit:'mm' },
];
const sliderEls = {}, valEls = {};
const slidersDiv = document.getElementById('sliders');
for (const d of JOINT_DEFS) {
const row = document.createElement('div');
row.className = 'slider-row';
const lbl = document.createElement('span');
lbl.textContent = d.key;
const sl = document.createElement('input');
sl.type = 'range';
sl.min = d.min; sl.max = d.max; sl.step = d.step; sl.value = 0;
const vb = document.createElement('input');
vb.type = 'text'; vb.className = 'val-box'; vb.value = '0';
vb.style.width = '58px';
sl.addEventListener('input', () => {
joints[d.key] = parseFloat(sl.value);
vb.value = parseFloat(sl.value).toFixed(d.step < 1 ? 1 : 0);
rebuild();
});
vb.addEventListener('change', () => {
const v = parseFloat(vb.value);
if (!isNaN(v)) {
joints[d.key] = v;
sl.value = v;
rebuild();
}
});
row.append(lbl, sl, vb);
slidersDiv.appendChild(row);
sliderEls[d.key] = sl;
valEls[d.key] = vb;
}
function setSlider(key, val) {
joints[key] = val;
sliderEls[key].value = val;
valEls[key].value = val.toFixed(JOINT_DEFS.find(d=>d.key===key)?.step < 1 ? 1 : 0);
}
// ─── file loading ──────────────────────────────────────────────
function readJSON(file) {
return new Promise((res, rej) => {
const r = new FileReader();
r.onload = e => { try { res(JSON.parse(e.target.result)); } catch(err) { rej(err); } };
r.onerror = rej;
r.readAsText(file);
});
}
document.getElementById('fRobot').addEventListener('change', async e => {
if (!e.target.files[0]) return;
robotData = await readJSON(e.target.files[0]);
// init sliders from defaultPosition if present
const dp = robotData.defaultPosition || {};
for (const k of Object.keys(joints)) {
if (dp[k] != null) setSlider(k, dp[k]);
}
populatePoses(robotData);
setStatus('robot.json loaded');
rebuild();
});
document.getElementById('fAruco').addEventListener('change', async e => {
if (!e.target.files[0]) return;
arucoData = await readJSON(e.target.files[0]);
setStatus('aruco/markers.json loaded');
rebuild();
});
// ═══════════════════════════════════════════════════════════════
// Test-pose dropdown
// ═══════════════════════════════════════════════════════════════
function populatePoses(data) {
const poses = data.robot_test_poses || {};
const keys = Object.keys(poses);
const section = document.getElementById('poseSection');
const sel = document.getElementById('poseSelect');
const info = document.getElementById('poseInfo');
if (keys.length === 0) { section.style.display = 'none'; return; }
section.style.display = '';
// Rebuild options
sel.innerHTML = '<option value="">— select pose —</option>';
keys.forEach(k => {
const o = document.createElement('option');
o.value = k; o.textContent = k;
sel.appendChild(o);
});
// Remove old listener by replacing element
const newSel = sel.cloneNode(true);
sel.parentNode.replaceChild(newSel, sel);
newSel.addEventListener('change', () => {
const k = newSel.value;
if (!k) { info.textContent = ''; return; }
const p = poses[k];
info.textContent = Object.entries(p).map(([a,b])=>`${a}:${b}`).join(' ');
for (const [key, val] of Object.entries(p)) {
if (key in joints) setSlider(key, Number(val));
}
rebuild();
});
}
// ─── clear groups ──────────────────────────────────────────────
function clearGroup(g) {
while (g.children.length) {
const c = g.children[0];
c.geometry?.dispose?.();
if (Array.isArray(c.material)) c.material.forEach(m => m.dispose?.());
else c.material?.dispose?.();
g.remove(c);
}
}
// ═══════════════════════════════════════════════════════════════
// Normal-arrow helper
// ═══════════════════════════════════════════════════════════════
function makeNormalArrow(posThreeJS, normalRobot, length, hexColor) {
// normalRobot is in robot coords → convert to Three.js direction
const dir = new THREE.Vector3(normalRobot[0], normalRobot[2], -normalRobot[1]).normalize();
const arrow = new THREE.ArrowHelper(
dir,
posThreeJS,
length,
hexColor,
length * 0.40, // cone head length
length * 0.20 // cone head width
);
return arrow;
}
// ─── rebuild scene ─────────────────────────────────────────────
function r2vDir(rx, ry, rz) {
return new THREE.Vector3(rx, rz, -ry).normalize();
}
function rebuild() {
clearGroup(gSkeleton);
clearGroup(gModel);
clearGroup(gNormals);
clearGroup(gObserved);
clearGroup(gObsNormals);
clearGroup(gErrors);
clearGroup(gBoard);
if (!robotData) return;
const T = computeFK(robotData, joints);
// ── board plane ──
const boardSize = robotData.links?.Board?.size || [1000, 200, 25];
{
const w = boardSize[0]*S, d = boardSize[1]*S, h = boardSize[2]*S;
const geo = new THREE.BoxGeometry(w, h, d);
const mat = new THREE.MeshPhongMaterial({ color:0x7a5520, transparent:true, opacity:0.5 });
const m = new THREE.Mesh(geo, mat);
m.position.set(w/2, -h/2, -d/2);
m.receiveShadow = true;
gBoard.add(m);
}
// ── skeleton ──
const links = robotData.links || {};
for (const [lname, ld] of Object.entries(links)) {
const sk = ld.skeleton;
if (!sk) continue;
const from = markerWorld(T, lname, sk.from || [0,0,0]);
const to = markerWorld(T, lname, sk.to || [0,0,0]);
const r = (sk.radius || 4) * S * 0.8;
const col = sk.color ? new THREE.Color(...sk.color) : new THREE.Color(linkColor(lname));
const cyl = makeCylinder(r2vArr(from), r2vArr(to), r, col);
if (cyl) gSkeleton.add(cyl);
}
// ── model markers + normals ──
const modelPositions = {};
const modelNormals = {};
for (const [lname, ld] of Object.entries(links)) {
const col = linkColor(lname);
for (const m of (ld.markers||[])) {
if (!m.position) continue;
const mid = m.id;
const wp = markerWorld(T, lname, m.position);
const nLocal = m.normal || [0,0,1];
const nWorld = transformDirByT(T[lname] || I4(), nLocal);
modelPositions[mid] = wp;
modelNormals[mid] = nWorld;
const sq = makeMarkerSquare(r2vArr(wp), r2vDir(...nWorld), 0.022, col);
gModel.add(sq);
// normal arrow (length = half a marker size = ~12.5mm → 0.0125m)
const arr = makeNormalArrow(r2vArr(wp), nWorld, 0.018, col);
gNormals.add(arr);
}
}
// ── observed markers + normals + error lines ──
const obs = {};
if (arucoData) {
for (const m of (arucoData.markers||[])) {
const mid = m.marker_id ?? m.id;
if (mid == null) continue;
let pos, nor = null;
if (m.position_mm) pos = m.position_mm;
else if (m.position_m) pos = m.position_m.map(v=>v*1000);
else continue;
// optional orientation from step-3 output or render markers.json
if (m.normal_world) nor = m.normal_world;
else if (m.normal) nor = m.normal;
obs[mid] = { pos, nor };
}
}
const errors = [];
for (const [midStr, {pos: opos, nor: oNor}] of Object.entries(obs)) {
const mid = parseInt(midStr);
const mpos = modelPositions[mid];
const op = r2vArr(opos);
const errMm = mpos
? Math.sqrt(opos.reduce((s,v,i) => s+(v-mpos[i])**2, 0))
: null;
const col = errMm == null ? 0x888888
: errMm < 5 ? 0x3ecf6b
: errMm < 15 ? 0xf59e0b
: 0xff4f4f;
gObserved.add(makeSphere(op, 0.006, col));
// observed normal arrow — orange if available
if (oNor) {
try {
const obsArrow = makeNormalArrow(op, oNor, 0.018, 0xffaa00);
gObsNormals.add(obsArrow);
} catch (e) {
console.warn('Invalid normal for marker', mid, oNor, e);
}
} else if (modelNormals[mid]) {
// fallback: show model normal at observed position (grey = no obs normal data)
const obsArrow = makeNormalArrow(op, modelNormals[mid], 0.014, 0x666666);
gObsNormals.add(obsArrow);
}
if (mpos) {
errors.push(errMm);
const mp = r2vArr(mpos);
gErrors.add(makeLine(mp, op, col));
}
}
// ── stats ──
updateStats(errors, Object.keys(obs).length, Object.keys(modelPositions).length);
}
// ─── statistics panel ──────────────────────────────────────────
function updateStats(errArr, nObs, nModel) {
const el = document.getElementById('stats-content');
if (errArr.length === 0) {
el.innerHTML = `<div class="stat-line"><span>Observed:</span><span class="stat-val">${nObs}</span></div>
<div class="stat-line"><span>Model:</span><span class="stat-val">${nModel}</span></div>
<div class="stat-line"><span style="color:var(--muted)">No matches found</span></div>`;
return;
}
const sorted = [...errArr].sort((a,b)=>a-b);
const mean = errArr.reduce((s,v)=>s+v,0) / errArr.length;
const rms = Math.sqrt(errArr.reduce((s,v)=>s+v*v,0) / errArr.length);
const p50 = sorted[Math.floor(sorted.length*0.5)];
const p90 = sorted[Math.floor(sorted.length*0.9)];
const max = sorted[sorted.length-1];
const cls = v => v < 5 ? 'stat-ok' : v < 15 ? 'stat-warn' : 'stat-err';
el.innerHTML = `
<div class="stat-line"><span>Observed:</span><span class="stat-val">${nObs}</span></div>
<div class="stat-line"><span>Matched:</span><span class="stat-val">${errArr.length}</span></div>
<div class="stat-line"><span>Mean error:</span><span class="${cls(mean)}">${mean.toFixed(1)} mm</span></div>
<div class="stat-line"><span>RMS error:</span><span class="${cls(rms)}">${rms.toFixed(1)} mm</span></div>
<div class="stat-line"><span>Median:</span><span class="${cls(p50)}">${p50.toFixed(1)} mm</span></div>
<div class="stat-line"><span>p90:</span><span class="${cls(p90)}">${p90.toFixed(1)} mm</span></div>
<div class="stat-line"><span>Max:</span><span class="${cls(max)}">${max.toFixed(1)} mm</span></div>
<div style="margin-top:6px;font-size:10px;color:var(--muted)">
🟢 &lt;5mm &nbsp; 🟡 515mm &nbsp; 🔴 &gt;15mm</div>`;
}
function setStatus(msg) {
document.getElementById('status-bar').textContent = msg + ' — Drag to orbit · Scroll to zoom';
}
// ═══════════════════════════════════════════════════════════════
// Render loop
// ═══════════════════════════════════════════════════════════════
function onResize() {
const w = canvas.parentElement.clientWidth;
const h = canvas.parentElement.clientHeight;
renderer.setSize(w, h);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
window.addEventListener('resize', onResize);
onResize();
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>

View File

@@ -95,6 +95,6 @@ echo [STEP 4] Robotics Pose calculation of angles and joint positions
python3 "..\pipeline\4_robotState_estimation_v6.py" ^
"%OUT_DIR%\aruco_positions_optimized.json" ^
"%OUT_DIR%\aruco_positions_initial.json" ^
-robot "%ROBOT_JSON%" ^
-out "%OUT_DIR%\robot_state.json"

100
run/run_pipeline_v8.bat Normal file
View File

@@ -0,0 +1,100 @@
@echo off
setlocal EnableDelayedExpansion
REM 3_pipeline_multiview.bat
REM Multi-camera ArUco detection and pose estimation pipeline
REM Parametr e.g.
REM run_pipeline.bat ../data/simulation/Scene4
REM Wenn kein Argument übergeben wurde
if "%1"=="" (
echo.
echo [INFO] Aufruf fehlt!
echo Beispiel: .\run_pipeline.bat ../data/simulation/Scene4
echo.
exit /b
)
set "IMAGES=%~1"
REM trailing slash entfernen
if "%IMAGES:~-1%"=="\" set "IMAGES=%IMAGES:~0,-1%"
if "%IMAGES:~-1%"=="/" set "IMAGES=%IMAGES:~0,-1%"
set ROBOT_JSON=..\data\robot\robot.json
set BASE_OUT_DIR=C:\Users\kech\SynologyDrive\2026-AppServer-AppRobot\appRobotRendering\data\evaluations
REM ✅ richtigen Namen extrahieren
for %%I in ("%IMAGES%") do set "SCENE_NAME=%%~nxI"
set OUT_DIR=%BASE_OUT_DIR%\%SCENE_NAME%
echo.
echo [STEP 1] Detect ArUco markers from all cameras in the folder %IMAGES%
for %%F in ("%IMAGES%\*.png" "%IMAGES%\*.PNG" "%IMAGES%\*.jpg" "%IMAGES%\*.jpeg" "%IMAGES%\*.JPG" "%IMAGES%\*.JPEG") do (
REM Dateiname ohne Pfad und ohne .png
set "NAME=%%~nF"
echo Bearbeite: !NAME!
REM Split bei "_" → nehme 2. Teil (die ID)
for /f "tokens=2 delims=_" %%A in ("%%~nF") do (
set "CAMID=%%A"
REM Takes files and detected arucos output to render_c_aruco_detection.json
python3 ../pipeline/1_detect_aruco_observations.py ^
-i "%%F" ^
-npz "%IMAGES%\render_a.npz" ^
-outDir %OUT_DIR% ^
-robot %ROBOT_JSON% ^
-cameraId !CAMID!
)
)
echo.
echo [STEP 2] Estimate camera poses from detections
for %%F in ("%OUT_DIR%\*_aruco_detection.json") do (
echo Bearbeite: %%F
python3 ../pipeline/2_estimate_camera_from_observations.py ^
-i "%%F" ^
-robot %ROBOT_JSON% ^
-outDir %OUT_DIR%
)
echo.
echo [STEP 3] Triangulate marker positions from multi-view observations
REM Alle detection files sammeln
for %%F in ("%OUT_DIR%\*_aruco_detection.json") do (
set DET_ARGS=!DET_ARGS! -det "%%F"
)
REM Alle pose files sammeln
for %%F in ("%OUT_DIR%\*_camera_pose.json") do (
set POSE_ARGS=!POSE_ARGS! -pose "%%F"
)
python3 "..\pipeline\3_multiview_bundle_adjustment_v4.py" ^
-robot "%ROBOT_JSON%" ^
-lambdaWeight 100.0 ^
!DET_ARGS! ^
!POSE_ARGS!
echo.
echo [STEP 4] Robotics Pose calculation of angles and joint positions
python3 "..\pipeline\4_v8_4a_base_slider.py" ^
"%OUT_DIR%\aruco_positions_initial.json" ^
-robot "%ROBOT_JSON%" ^
-out "%OUT_DIR%\robot_state.json"