# Kalibrierung – Arm-Marker > Stand: 2026-06-15 > Ergänzung zu `doc/Kalibrierung.md` → Schritt „Arm-Marker eintragen und verifizieren" > Dient als Programmier-Roadmap für den UI-Tab „Marker" und die Verifikations-Pipeline. --- ## Was ist ein Arm-Marker? Ein ArUco-Aufkleber, der auf einem beweglichen Roboter-Glied (Arm1, Ellbow, Arm2, …) befestigt ist. Arm-Marker unterscheiden sich von Board-Markern: | | Board-Marker | Arm-Marker | |--|--|--| | Position | fest, kalibriert | bewegt sich mit dem Gelenk | | Zweck | Referenz für Kamera-Pose | Gelenk-Winkel-Schätzung (Homing) | | Position in robot.json | `links.Board.markers` | `links.{Link}.markers` | | Koordinatensystem | Welt (Board) | lokal im Link-Frame | --- ## Marker-Daten in robot.json ```jsonc { "links": { "Arm1": { "markers": [ { "id": 198, // ArUco-ID (eindeutig) "name": "aruco_198", // optional, lesbar "position": [0, -160, 35], // Mittelpunkt im lokalen Link-Frame [mm] "normal": [0, 0, 1], // Normale der Marker-Fläche (Link-Frame) "size": 25, // Kantenlänge mm "spin": 0 // Drehung der Marker-Grafik um die Normale [°] } ] } } } ``` ### Felder | Feld | Typ | Bedeutung | |------|-----|-----------| | `id` | int | ArUco-ID — muss mit gedrucktem Marker übereinstimmen | | `position` | `[x,y,z]` mm | Mittelpunkt im **lokalen Link-Frame** (nicht Welt) | | `normal` | `[nx,ny,nz]` | Flächennormale des Markers im Link-Frame; `[0,0,1]` = Marker schaut in +Z | | `size` | mm | Kantenlänge des ArUco-Quadrats | | `spin` | ° | Drehung der Aufkleber-Grafik um die Normale — 0 / 90 / 180 / 270 | ### Spin-Semantik Ein ArUco-Aufkleber kann in 4 Lagen aufgeklebt werden. Physisch ist das egal — der Detektor findet die ID unabhängig von der Orientierung, und auch die gemessene Mittelpunkt-Position ist spin-unabhängig. `spin` beschreibt nur die visuelle Darstellung im 3D-Viewer, damit die angezeigte Marker-Grafik mit dem echten Aufkleber übereinstimmt. Das hilft beim visuellen Abgleich: wenn die Grafik im Viewer verdreht zum Aufkleber steht, ist spin falsch. > **In der aktuellen Homing-Pipeline (3b, 4b) wird `spin` nicht verwendet.** > Relevant wird es, wenn der Viewer spin korrekt darstellt (→ offenes Todo unten). --- ## Typischer Workflow ``` 1. ArUco-Aufkleber auf Roboter-Glied kleben │ ▼ 2. Position messen / schätzen → in robot.json eintragen (position = Mittelpunkt im lokalen Link-Frame, normal = Flächen-Richtung) │ ▼ 3. Homing-Run starten (homing.html) │ ▼ 4. Kalibrierung → Tab "Marker" öffnen │ · Tabelle zeigt alle Marker und ihren aktuellen spin │ · Viewer zeigt Modell-Marker (Vierecke) + beobachtete Punkte (Kugeln) │ · Fehler-Linien: Modell-Marker → beobachteter Punkt ▼ 5. Prüfen: · Linie kurz → Position stimmt gut · Linie lang oder in falscher Richtung → position in robot.json korrigieren · Viewer-Grafik verdreht → spin korrigieren (+90 / -90 / 180) │ ▼ 6. Spin / Normal korrigieren → Viewer lädt neu → erneut prüfen ``` --- ## UI-Tab „Marker" (implementiert 2026-06-15) ### Datei: `public/calibration_marker.html` Drei Abschnitte: | Abschnitt | Inhalt | |-----------|--------| | Aktuelle Marker | Tabelle aller Arm-Marker (Link, ID, Name, Position, Normal, Size, **Spin**) aus robot.json | | Aktionen | Link-Dropdown → Marker-ID-Dropdown → Spin-Buttons (−90°, +90°, 180°) | | Viewer | `boardViewer.html?mode=homing` — Modell + letzter Homing-Run | ### JS: `initMarker()` in `public/calibration.js` | Funktion | Beschreibung | |----------|-------------| | `loadRobot()` | Holt `/api/robot`, rendert Tabelle, befüllt ID-Dropdown | | `renderTable(robot)` | Tabelle aller Arm-Marker mit farbig hervorgehobenem Spin | | `updateMarkerDropdown()` | Link-Dropdown-Wechsel → ID-Dropdown neu befüllen | | `applySpin(delta)` | `POST /api/robot/set-arm-marker-spin` → Viewer `reload`-Message | ### Backend: `POST /api/robot/set-arm-marker-spin` ``` Body: { linkName: string, markerId: number, spin: number } Return: { changed, linkName, markerId, oldSpin, newSpin } ``` Implementiert in `server/server.js` → delegiert an `setArmMarkerSpin()` in `server/editRobot.js`. --- ## Offene Punkte (Programmier-Roadmap) ### [P1] Spin-Rendering im boardViewer **Status:** ✅ implementiert (2026-06-15) **Datei:** `public/boardViewer.html` → `buildSkeletonFK()`, Abschnitt „4. Arm-Marker" `spin` wird als zusätzliche Rotation um die Marker-Normale (in Welt-Koordinaten) auf das orientierte Quadrat angewendet via `premultiply`: ```javascript const normalW = new THREE.Vector3(nx, nz, -ny).transformDirection(childFrame).normalize(); const markerMesh = makeMarkerSquareOriented(posWorld, normalW, markerSizeM, col); const spinRad = ((m.spin ?? 0) * Math.PI) / 180; if (Math.abs(spinRad) > 1e-6) { markerMesh.quaternion.premultiply( new THREE.Quaternion().setFromAxisAngle(normalW, spinRad) ); } ``` `premultiply(Q_spin)` setzt `quaternion = Q_spin * Q_normal` → zuerst Normal-Alignment, dann Spin-Rotation in Welt-Koordinaten um `normalW`. --- ### [P2] Marker-Position verifizieren: Positions-Residuum **Status:** ❌ noch nicht implementiert Nach einem Homing-Run kennen wir für jeden erkannten Arm-Marker: - `model_pos_world` = Modell-Mittelpunkt im Welt-Frame (via FK) - `obs_pos_world` = triangulierter beobachteter Mittelpunkt Das **Positions-Residuum** `|model_pos_world − obs_pos_world|` zeigt, wie gut die eingetragene `position` (Mittelpunkt im lokalen Frame) stimmt. > **⚠ Wichtig: Spin-Fehler sind damit NICHT erkennbar.** > Der Mittelpunkt eines Markers ist spin-unabhängig — egal wie ein Aufkleber gedreht > ist, das Zentrum bleibt dasselbe. Ein falscher `spin`-Wert erzeugt daher kein > Positions-Residuum. Spin-Fehler erfordern → P3 (visuell) oder P4 (automatisch). **Erweiterung im Marker-Tab:** Tabelle um Spalten ergänzen: - `Δ mm` = Positions-Residuum des letzten Homing-Runs - `Status` = ✅ < 5 mm / ⚠ 5–20 mm / ❌ > 20 mm **Datenquelle:** `/api/homing/run-data?run={ts}` → `finalState` + `measuredMarkers` --- ### [P3] Python: Ecken-Position in aruco_marker_poses.json ausgeben **Status:** ❌ noch nicht implementiert **Datei:** `scripts/3b_corner_marker_poses.py` Voraussetzung für P3b. Aktuell enthält `aruco_marker_poses.json` pro Marker nur `position_mm` (Mittelpunkt) und `normal`. Die Spin-Orientierung geht verloren. `3b` trianguliert den Mittelpunkt aus den Ecken der Kamera-Beobachtungen. Dieselbe Triangulierung auf **Ecke 0** anwenden und zusätzlich ausgeben: ```python # Zusätzliches Feld pro Marker in aruco_marker_poses.json: "corner0_mm": [x, y, z] # triangulierte Welt-Position von Ecke 0 ``` Ecke 0 = top-left in OpenCVs ArUco-Konvention (Index 0 in `corners[0]`). Zusammen mit `position_mm` (Mittelpunkt) definiert `corner0_mm` eindeutig die Orientierung des Markers in 3D. > Der Mittelpunkt bleibt spin-unabhängig. `corner0_mm` ist das einzige Feld, > das den Spin kodiert. --- ### [P3b] boardViewer: Orientierungszeiger zeichnen **Status:** ✅ Modell-Seite implementiert (2026-06-15) · Beobachtungs-Seite offen (→ P3) **Datei:** `public/boardViewer.html` → `buildSkeletonFK()` **Voraussetzungen:** P1 ✅, P3 (corner0_mm) für Beobachtungs-Zeiger noch offen Für jeden Marker werden zwei kurze Linien vom Mittelpunkt zu Ecke 0 gezeichnet — eine für das Modell, eine für die Beobachtung. Der Winkel zwischen beiden = Spin-Fehler. **Modell-Zeiger** (implementiert — nutzt `markerMesh.quaternion` direkt): `markerMesh.quaternion` kodiert bereits `Q_spin * Q_normal`, daher reicht: ```javascript // Richtung zur Ecke 0: (1,1,0) normalisiert im lokalen Marker-Frame, // transformiert durch die bereits berechnete Mesh-Rotation (Q_normal ∘ Q_spin) const ptrDir = new THREE.Vector3(1, 1, 0).normalize().applyQuaternion(markerMesh.quaternion); const corner0W = posWorld.clone().add(ptrDir.multiplyScalar(markerSizeM * Math.SQRT1_2)); gArmMarkers.add(makeLine(posWorld, corner0W, col, 0.9)); // Zeiger (Link-Farbe) gArmMarkers.add(makeSphere(corner0W, 0.0008, col)); // Ecke 0 (Punkt) ``` `Math.SQRT1_2 = 1/√2` weil der Abstand Mittelpunkt→Ecke bei einem Quadrat mit Kantenlänge `size` genau `size/√2` beträgt (`(size/2)·√2 = size/√2`). **Beobachtungs-Zeiger** (aus `corner0_mm` in `aruco_marker_poses.json`, sobald Python das Feld liefert → P3): ```javascript // obs = beobachteter Marker aus _measuredMarkers if (obs.corner0_mm) { const corner0Obs = r2vArr(obs.corner0_mm); // robot→Three.js gArmMarkers.add(makeLine(obsPosW, corner0Obs, 0xffffff, 0.6)); // Beobachtungs-Zeiger (dünn) gArmMarkers.add(makeSphere(corner0Obs, 0.0006, 0xffffff)); // Beobachtungs-Ecke } ``` **Ergebnis im Viewer:** ``` Modell-Mittelpunkt ●——▶ Modell-Ecke 0 (Link-Farbe, voll) Obs-Mittelpunkt ●··▶ Obs-Ecke 0 (weiß, dünn) ``` Zeigen beide Zeiger in dieselbe Richtung → spin korrekt. 90°-Unterschied → spin um 90° falsch → +90° oder −90° klicken. --- ### [P3c] Homing-Check direkt im Marker-Tab starten **Status:** ❌ noch nicht implementiert Button „Homing-Check starten" im Marker-Tab triggert die vollständige Homing-Pipeline (`POST /api/homing/run`) und zeigt: - SSE-Log im Tab-internen Textfeld - Fortschritt im Viewer via `postMessage({ type: 'homing-state', state })` Kein struktureller Unterschied zu `homing.html` — Code-Duplizierung vermeiden durch Extraktion eines gemeinsamen `runHomingStream(sendFn, frameFn)` aus `client.js`. > Für Spin-Verifikation ist P3c nötig, damit frische Beobachtungsdaten im Viewer landen. > Ohne P3 (corner0_mm) sieht man trotzdem nur Mittelpunkt-Fehlerlinien, keine Zeiger. --- ### [P4] Spin automatisch berechnen und korrigieren **Status:** ❌ noch nicht implementiert **Voraussetzungen:** P3 (corner0_mm), P3b (Zeiger im Viewer) Sobald `corner0_mm` aus Python vorliegt und der Viewer die Zeiger anzeigt (P3b), kann der tatsächliche Spin-Wert rechnerisch bestimmt werden — ohne manuelles Raten. **Berechnung im Browser** (in `initMarker()` oder `buildSkeletonFK()`): ```javascript // Modell-Richtung zu Ecke 0 bei spin=0 (im Welt-Frame, via FK): const modelCorner0Dir = /* corner0World − posWorld, normalisiert */; // Beobachtete Richtung zu Ecke 0: const obsCorner0Dir = r2vArr(obs.corner0_mm).sub(obsPosW).normalize(); // Winkel zwischen beiden (um die Marker-Normale): const cross = new THREE.Vector3().crossVectors(modelCorner0Dir, obsCorner0Dir); const sinA = cross.dot(normalWorld); // Vorzeichen = Drehrichtung const cosA = modelCorner0Dir.dot(obsCorner0Dir); const deltaDeg = Math.round(Math.atan2(sinA, cosA) * 180 / Math.PI / 90) * 90; // → rundet auf nächste 90°: 0 / 90 / -90 / 180 ``` **UI-Erweiterung im Marker-Tab:** - Tabelle bekommt Spalte „Gemessener Spin" und „Soll-Spin (robot.json)" - Unterschied → Badge „⚠ +90°" → Klick übernimmt die Korrektur direkt --- ### [P5] Marker-Position aus Homing übernehmen **Status:** ❌ offen Wenn ein Arm-Marker nur grob eingemessen wurde, kann die triangulierte Welt-Position aus dem Homing-Run dazu genutzt werden, die `position` in robot.json zu verfeinern: ``` position_local = inverse_FK(obs_world, current_state) ``` Setzt voraus, dass der Gelenk-Winkel für diesen Link bereits korrekt bestimmt wurde. Iterativ einsetzbar: grobe Startposition → erster Homing → verfeinerte Position → … --- ## Abhängigkeits-Kette Spin-Verifikation ``` P1 boardViewer rendert spin korrekt → Modell-Viereck zeigt echte Orientierung P3 3b gibt corner0_mm aus → Beobachtungs-Orientierung verfügbar P3b boardViewer zeichnet Orientierungszeiger → Spin-Fehler sichtbar als Winkel P3c Marker-Tab: Homing-Check-Button → frische Daten ohne Tab-Wechsel P4 JS berechnet Δspin, schlägt Korrektur vor → kein manuelles Raten mehr ``` --- ## Dateiübersicht | Datei | Rolle | |-------|-------| | `public/calibration.html` | Tab-Button „Marker" | | `public/calibration_marker.html` | Panel-HTML (Tabelle, Aktionen, Viewer) | | `public/calibration.js` → `initMarker()` | Frontend-Logik des Tabs | | `server/server.js` → `POST /api/robot/set-arm-marker-spin` | Spin-Endpoint ✅ | | `server/editRobot.js` → `setArmMarkerSpin()` | robot.json schreiben ✅ | | `public/boardViewer.html` → `buildSkeletonFK()` | Spin-Rendering (→ P1) + Zeiger (→ P3b) | | `scripts/3b_corner_marker_poses.py` | corner0_mm ausgeben (→ P3) | | `scripts/robot_1781069752019.json` → `links.*.markers` | Marker-Daten |