13 KiB
Kalibrierung – Arm-Marker
Stand: 2026-06-15
Ergänzung zudoc/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
{
"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
spinnicht 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:
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 falscherspin-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-RunsStatus= ✅ < 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:
# 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_mmist 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:
// 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):
// 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()):
// 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 |