Files
appRobotHoming/doc/Kalibrierung_Marker.md
2026-06-15 09:23:21 +02:00

342 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 / ⚠ 520 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 |