Callibration Mathe
This commit is contained in:
@@ -1,127 +0,0 @@
|
|||||||
# Bestimmung der Y-Rotationsachse aus Marker-Positionen
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
|
|
||||||
Wenn der Roboter um seine Y-Achse rotiert, bewegt sich jeder erkannte Marker auf einem **Kreisbogen** im 3D-Raum. Ziel ist es, Lage und Richtung dieser Rotationsachse aus den beobachteten Marker-Positionen (x, y, z) zu mehreren Zeitstempeln zu berechnen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Reichen zwei Positionen?
|
|
||||||
|
|
||||||
**Nein.** Zwei Punkte P₁, P₂ desselben Markers definieren nur eine Strecke. Die Rotationsachse muss durch die senkrechte Mittelebene dieser Strecke laufen – aber *wo* genau in dieser Ebene und in welcher Richtung bleibt unbestimmt. Das System ist unterbestimmt.
|
|
||||||
|
|
||||||
| Beobachtungen | Bestimmbar? | Begründung |
|
|
||||||
|---|---|---|
|
|
||||||
| 1 Marker, 2 Positionen | Nein | Achse liegt irgendwo auf einer Halbebene |
|
|
||||||
| 1 Marker, 3 Positionen | **Ja** | Eindeutiger Umkreis → eindeutige Achse |
|
|
||||||
| N Marker, je ≥ 3 Positionen | Ja, überbestimmt | Least-Squares, robuster gegenüber Messrauschen |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Mathematik: 1 Marker, 3 Positionen
|
|
||||||
|
|
||||||
Gegeben: P₁, P₂, P₃ ∈ ℝ³ (derselbe Marker zu drei verschiedenen Rotationswinkeln).
|
|
||||||
|
|
||||||
Da die Rotation starr ist, liegen P₁, P₂, P₃ auf einem **Kreis**, dessen Ebene senkrecht zur Rotationsachse steht.
|
|
||||||
|
|
||||||
### Schritt 1 – Achsenrichtung (Normalenvektor der Kreisebene)
|
|
||||||
|
|
||||||
```
|
|
||||||
v₁ = P₂ − P₁
|
|
||||||
v₂ = P₃ − P₁
|
|
||||||
n = (v₁ × v₂) / |v₁ × v₂| ← Einheitsvektor entlang der Rotationsachse
|
|
||||||
```
|
|
||||||
|
|
||||||
### Schritt 2 – Mittelpunkt des Kreises (Umkreismittelpunkt)
|
|
||||||
|
|
||||||
Der Umkreismittelpunkt C liegt in der Ebene der drei Punkte und ist von allen drei Punkten gleich weit entfernt. Er ist ein Punkt **auf der Rotationsachse**.
|
|
||||||
|
|
||||||
Berechnung über Baryzentrischen Koordinaten:
|
|
||||||
|
|
||||||
```
|
|
||||||
a² = |P₂ − P₃|²
|
|
||||||
b² = |P₁ − P₃|²
|
|
||||||
c² = |P₁ − P₂|²
|
|
||||||
|
|
||||||
w₁ = a²·(b² + c² − a²)
|
|
||||||
w₂ = b²·(a² + c² − b²)
|
|
||||||
w₃ = c²·(a² + b² − c²)
|
|
||||||
|
|
||||||
C = (w₁·P₁ + w₂·P₂ + w₃·P₃) / (w₁ + w₂ + w₃)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Schritt 3 – Rotationsachse
|
|
||||||
|
|
||||||
```
|
|
||||||
Achse: r(t) = C + t·n, t ∈ ℝ
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Mathematik: N Marker, je ≥ 3 Positionen (Least Squares)
|
|
||||||
|
|
||||||
Jeder Marker i liefert:
|
|
||||||
- einen Schätzwert **Cᵢ** (Umkreismittelpunkt, Punkt auf der Achse)
|
|
||||||
- einen Schätzwert **nᵢ** (Normalenvektor, Achsenrichtung)
|
|
||||||
|
|
||||||
### Achsenrichtung (gemittelt / PCA)
|
|
||||||
|
|
||||||
Wegen möglichem Vorzeichenambiguität alle nᵢ auf dasselbe Halbraum ausrichten, dann:
|
|
||||||
|
|
||||||
```
|
|
||||||
n̄ = mean(nᵢ) / |mean(nᵢ)|
|
|
||||||
```
|
|
||||||
|
|
||||||
Robuster: **PCA** über die Matrix der nᵢ-Vektoren → erster Hauptkomponentenvektor.
|
|
||||||
|
|
||||||
### Achsenposition (Least Squares)
|
|
||||||
|
|
||||||
Minimiere die quadratischen Abstände aller Umkreismittelpunkte Cᵢ zur gesuchten Geraden `r(t) = A + t·n̄`:
|
|
||||||
|
|
||||||
```
|
|
||||||
Abstand² von Cᵢ zur Achse = |Cᵢ − A|² − ((Cᵢ − A)·n̄)²
|
|
||||||
```
|
|
||||||
|
|
||||||
Ableitung null setzen liefert:
|
|
||||||
|
|
||||||
```
|
|
||||||
A = C̄ − (C̄·n̄)·n̄ + (C̄·n̄)·n̄ = C̄ (Referenzpunkt = Schwerpunkt der Cᵢ)
|
|
||||||
```
|
|
||||||
|
|
||||||
d. h., der **Schwerpunkt der Umkreismittelpunkte** ist der optimale Referenzpunkt auf der Achse. Die Achsenrichtung n̄ bleibt unverändert.
|
|
||||||
|
|
||||||
**Verbleibender Fehler** (Residuum pro Marker):
|
|
||||||
|
|
||||||
```
|
|
||||||
εᵢ = |(Cᵢ − C̄) − ((Cᵢ − C̄)·n̄)·n̄|
|
|
||||||
```
|
|
||||||
|
|
||||||
Große εᵢ deuten auf einen fehlerhaften Marker oder eine nicht-rotatorische Bewegungskomponente hin.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Praktische Empfehlung
|
|
||||||
|
|
||||||
- **Mindestens 3 Zeitstempel** pro Marker, verteilt über einen Winkelbereich von ≥ 30° (sonst ist der Kreis numerisch schlecht konditioniert).
|
|
||||||
- **3–4 Marker** mit je 3 Positionen sind ausreichend für eine stabile Schätzung.
|
|
||||||
- Die Rotationswinkel müssen nicht bekannt sein – nur die 3D-Koordinaten der Marker.
|
|
||||||
- Bei sehr kleinen Winkeln (< 10°) ist die Bestimmung numerisch instabil; größere Drehungen bevorzugen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Zusammenfassung
|
|
||||||
|
|
||||||
```
|
|
||||||
Eingabe: Marker-Positionen P[marker][timestamp] ∈ ℝ³
|
|
||||||
(mind. 3 Timestamps pro Marker, mind. 1 Marker)
|
|
||||||
|
|
||||||
Pro Marker:
|
|
||||||
1. n_i = normalize((P2-P1) × (P3-P1)) ← Achsenrichtung
|
|
||||||
2. C_i = Umkreismittelpunkt(P1, P2, P3) ← Punkt auf Achse
|
|
||||||
|
|
||||||
Kombination:
|
|
||||||
3. n̄ = normalize(mean(n_i)) ← beste Achsenrichtung
|
|
||||||
4. C̄ = mean(C_i) ← bester Referenzpunkt
|
|
||||||
|
|
||||||
Ergebnis: Rotationsachse r(t) = C̄ + t·n̄
|
|
||||||
```
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
# Calibration Roadmap – appRobotHoming
|
|
||||||
|
|
||||||
> Stand: 2026-06-10
|
|
||||||
> Ziel: Vor dem Homing muss das System einmalig (oder nach mechanischen Änderungen) kalibriert werden.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Übersicht der Kalibrierungsschritte
|
|
||||||
|
|
||||||
```
|
|
||||||
[1] Camera NPZ → [2] Board → [3] Robot X-Axis → [4] Arm1 / Arm2
|
|
||||||
```
|
|
||||||
|
|
||||||
Jede Stufe baut auf der vorherigen auf. Die Ergebnisse werden als Dateien gespeichert und vom Homing-Prozess geladen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1] Camera NPZ – Kamerakalibrierung
|
|
||||||
|
|
||||||
**Ziel:** Intrinsische Kameraparameter (Brennweite, Verzerrungskoeffizienten, Kameramatrix) für jede Kamera ermitteln und als `.npz`-Datei speichern.
|
|
||||||
|
|
||||||
**Vorgehen (noch offen):**
|
|
||||||
- Schachbrettmuster / ChArUco-Board aus verschiedenen Winkeln fotografieren
|
|
||||||
- OpenCV `calibrateCamera()` ausführen
|
|
||||||
- Ergebnis speichern: `cam0_calib.npz`, `cam1_calib.npz`, `cam2_calib.npz`
|
|
||||||
|
|
||||||
**Aktionen auf der Seite (geplant):**
|
|
||||||
- Fotos aufnehmen (mehrere Posen)
|
|
||||||
- Kalibrierung berechnen
|
|
||||||
- `.npz`-Datei herunterladen / auf Server speichern
|
|
||||||
- Reprojektionsfehler anzeigen
|
|
||||||
|
|
||||||
**Offene Fragen:**
|
|
||||||
- Welches Muster wird verwendet (Schachbrett vs. ChArUco)?
|
|
||||||
- Automatische Erfassung oder manueller Upload der Bilder?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [2] Board – Referenz-Board-Kalibrierung
|
|
||||||
|
|
||||||
**Ziel:** Die extrinsische Position des Marker-Boards im Kamera-Koordinatensystem bestimmen.
|
|
||||||
|
|
||||||
**Vorgehen (noch offen):**
|
|
||||||
- Board in definierter Position aufstellen
|
|
||||||
- Foto aufnehmen, Marker erkennen (`aruco.detectMarkers`)
|
|
||||||
- `solvePnP` → Rotations- und Translationsvektor berechnen
|
|
||||||
- Transformation Board → Kamera speichern
|
|
||||||
|
|
||||||
**Aktionen auf der Seite (geplant):**
|
|
||||||
- Kamerabild anzeigen mit erkannten Markern
|
|
||||||
- Pose berechnen und anzeigen
|
|
||||||
- Kalibrierungsdatei speichern
|
|
||||||
|
|
||||||
**Offene Fragen:**
|
|
||||||
- Feste Board-Position oder Referenzpunkte einmessen?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [3] Robot X-Axis – Achsenrichtung kalibrieren
|
|
||||||
|
|
||||||
**Ziel:** Die X-Achse des Roboters im Weltkoordinatensystem verorten (Ausrichtung und Nullpunkt).
|
|
||||||
|
|
||||||
**Vorgehen (noch offen):**
|
|
||||||
- Roboter an bekannte X-Positionen fahren (z. B. X=0 und X=max)
|
|
||||||
- Kamera beobachtet den Endeffektor / Marker am Roboter
|
|
||||||
- Achsvektor aus zwei Messpunkten berechnen
|
|
||||||
- Ergebnis speichern
|
|
||||||
|
|
||||||
**Aktionen auf der Seite (geplant):**
|
|
||||||
- Roboter zu Referenzposition 1 fahren → Foto → Marker-Position merken
|
|
||||||
- Roboter zu Referenzposition 2 fahren → Foto → Marker-Position merken
|
|
||||||
- Achsvektor berechnen und anzeigen
|
|
||||||
- Speichern
|
|
||||||
|
|
||||||
**Offene Fragen:**
|
|
||||||
- Wie viele Referenzpunkte werden benötigt?
|
|
||||||
- Wird die Z-Achse separat kalibriert?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [4] Arm1 / Arm2 – Gelenk-Kalibrierung
|
|
||||||
|
|
||||||
**Ziel:** Nullposition und Kinematikparameter von Arm1 und Arm2 einmessen.
|
|
||||||
|
|
||||||
**Vorgehen (noch offen):**
|
|
||||||
- Arm in mechanische Nullposition fahren (physischer Anschlag oder Markierung)
|
|
||||||
- Kamera prüft die tatsächliche Arm-Pose
|
|
||||||
- Offset zwischen Soll und Ist berechnen und speichern
|
|
||||||
|
|
||||||
**Aktionen auf der Seite (geplant):**
|
|
||||||
- Arm1 auf Nullposition → Foto → Winkel ablesen
|
|
||||||
- Arm2 auf Nullposition → Foto → Winkel ablesen
|
|
||||||
- Offset-Korrektur berechnen und speichern
|
|
||||||
|
|
||||||
**Offene Fragen:**
|
|
||||||
- Separater Marker pro Armgelenk?
|
|
||||||
- Kalibrierung bei jedem Start oder nur nach Umbau?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Dateistruktur (geplant)
|
|
||||||
|
|
||||||
```
|
|
||||||
calibration/
|
|
||||||
cam0_calib.npz
|
|
||||||
cam1_calib.npz
|
|
||||||
cam2_calib.npz
|
|
||||||
board_pose.json
|
|
||||||
robot_xaxis.json
|
|
||||||
arm_offsets.json
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
| Schritt | Status | Anmerkung |
|
|
||||||
|------------------|--------------|----------------------------------|
|
|
||||||
| Camera NPZ | offen | Konzept unklar |
|
|
||||||
| Board | offen | Konzept unklar |
|
|
||||||
| Robot X-Axis | offen | Konzept unklar |
|
|
||||||
| Arm1 / Arm2 | offen | Konzept unklar |
|
|
||||||
| Calibration-UI | in Arbeit | HTML-Seite angelegt |
|
|
||||||
@@ -1,23 +1,22 @@
|
|||||||
# Homing Roadmap – appRobotHoming
|
# Homing – appRobotHoming
|
||||||
|
|
||||||
> Stand: 2026-06-13
|
> Stand: 2026-06-14
|
||||||
> Ziel: Aus einem einzigen Kamera-Snapshot die aktuellen Gelenkwinkel/-positionen
|
> Homing läuft bei **jedem Einschalten** — schnell, vollautomatisch, ohne mechanische Endschalter.
|
||||||
> des Roboters bestimmen und an den Controller senden.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Was ist Homing?
|
## Was ist Homing?
|
||||||
|
|
||||||
Homing = der Roboter weiss **nicht**, wo er ist.
|
Der Roboter weiss beim Einschalten nicht, wo er steht.
|
||||||
Die Kameras schauen auf das Board + die ArUco-Marker am Roboter und berechnen
|
Die Kameras schauen auf die ArUco-Marker am Roboter und berechnen daraus
|
||||||
daraus die vollständige Pose aller Gelenke — ohne mechanische Endschalter.
|
die vollständige Pose aller Gelenke.
|
||||||
|
|
||||||
Homing läuft bei **jedem** Einschalten ab: schnell, robust, vollautomatisch.
|
**Homing** (dieser Prozess): bei jedem Einschalten, automatisch.
|
||||||
Kalibrierung hingegen läuft nur nach mechanischen Änderungen (≈ einmalig).
|
**Kalibrierung** (`doc/Kalibrierung.md`): nur nach mechanischen Änderungen.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Kinematik-Kette (aus `robot.json → links`)
|
## Kinematik-Kette
|
||||||
|
|
||||||
```
|
```
|
||||||
Board (ROOT, fest) ← Referenz aller Kameras
|
Board (ROOT, fest) ← Referenz aller Kameras
|
||||||
@@ -31,376 +30,121 @@ Board (ROOT, fest) ← Referenz aller Kameras
|
|||||||
└── FingerA/B linear e axis=±[1,0,0] ← Greifer (symmetrisch)
|
└── FingerA/B linear e axis=±[1,0,0] ← Greifer (symmetrisch)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Resultat-State:** `{ x_mm, y_deg, z_deg, a_deg, b_deg, c_deg, e_mm }`
|
**Ergebnis-State:** `{ x_mm, y_deg, z_deg, a_deg, b_deg, c_deg, e_mm }`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Voraussetzungen (Kalibrierung)
|
## Voraussetzungen
|
||||||
|
|
||||||
| Was | Mechanismus | Status |
|
Homing setzt eine abgeschlossene Kalibrierung voraus:
|
||||||
|-----|-------------|--------|
|
|
||||||
| Kamera-Intrinsik (NPZ) | `calibration.html` → Tab Camera NPZ | ✅ fertig |
|
|
||||||
| Board-Marker-Positionen | `calibration.html` → Tab Board | ✅ fertig |
|
|
||||||
| X-Achsen-Richtung | `calibration.html` → Tab Robot X-Axis | ✅ fertig |
|
|
||||||
| **Arm1 Joint-Origin Y/Z** | `calibration.html` → Tab Arm1 → Button „Joint-Origin Y/Z übernehmen" | ✅ **Button vorhanden, ausführbar** |
|
|
||||||
| Arm-Marker in robot.json | Manuell eintragen (links.Arm1/Ellbow/Arm2/Hand.markers) | 🔶 Nutzer trägt ein |
|
|
||||||
|
|
||||||
> **Kalibrierung gilt als abgeschlossen** sobald der Arm1-Button geklickt und
|
| Was | Status |
|
||||||
> die Arm-Marker eingetragen sind.
|
|-----|--------|
|
||||||
|
| Kamera-Intrinsik (NPZ) | ✅ |
|
||||||
|
| Board-Marker-Positionen | ✅ |
|
||||||
|
| X-Achsen-Richtung | ✅ |
|
||||||
|
| Arm1 Joint-Origin Y/Z | ✅ Button vorhanden und ausführbar |
|
||||||
|
| Arm-Marker in robot.json | 🔶 Nutzer trägt ein (`links.Arm1/Ellbow/Arm2/Hand.markers`) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## robot.json – Ladestrategie
|
## Ablauf
|
||||||
|
|
||||||
### Aktuell: lokale Datei
|
|
||||||
```
|
```
|
||||||
|
Foto alle Kameras
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
1_detect_aruco_observations.py (pro Kamera, mit NPZ)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
2_estimate_camera_from_observations.py (pro Kamera)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
3b_corner_marker_poses.py (einmal, benötigt ≥2 Kamera-Posen)
|
||||||
|
│ → aruco_marker_poses.json
|
||||||
|
▼
|
||||||
|
X-Position aus Marker-Positionen schätzen
|
||||||
|
│ → x_mm (Durchschnitt x der Nicht-Board-Marker)
|
||||||
|
▼
|
||||||
|
4b_revolute_angle.py --link Arm1 --x-mm {x_mm}
|
||||||
|
│ → state_Arm1.json (accumulated_state)
|
||||||
|
▼
|
||||||
|
4b_revolute_angle.py --link Ellbow --from-state state_Arm1.json
|
||||||
|
│ → state_Ellbow.json
|
||||||
|
▼
|
||||||
|
4b_revolute_angle.py --link Arm2 --from-state state_Ellbow.json
|
||||||
|
│ → state_Arm2.json
|
||||||
|
▼
|
||||||
|
4b_revolute_angle.py --link Hand --from-state state_Arm2.json
|
||||||
|
│ → state_Hand.json ← accumulated_state enthält x,y,z,a,b,c,e
|
||||||
|
▼
|
||||||
|
POST ROBOT_URL/api/state
|
||||||
|
```
|
||||||
|
|
||||||
|
**Schritte 1–3b** sind dieselbe Board-Pipeline wie in der Kalibrierung.
|
||||||
|
Sie sind in `runBoardPipeline()` (`server/server.js`) als gemeinsame Funktion ausgelagert.
|
||||||
|
|
||||||
|
**4b-Schleife**: sequenziell von root nach tip; jedes Script bekommt den Zustand des
|
||||||
|
vorherigen Schritts über `--from-state`. Der erste Aufruf erhält die geschätzte
|
||||||
|
X-Slider-Position über `--x-mm`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementierung
|
||||||
|
|
||||||
|
| Komponente | Datei | Beschreibung |
|
||||||
|
|-----------|-------|--------------|
|
||||||
|
| Board-Pipeline | `server/server.js` → `runBoardPipeline(runDir, send)` | Foto + Scripts 1, 2, 3b; von Board-Run und Homing genutzt |
|
||||||
|
| X-Schätzung | `server/homingOrchestrator.js` → `estimateXFromMarkers()` | Mittelwert x der Nicht-Board-Marker aus `aruco_marker_poses.json` |
|
||||||
|
| Homing-Orchestrator | `server/homingOrchestrator.js` → `runHoming()` | Kompletter Ablauf als SSE-Stream |
|
||||||
|
| Backend-Route | `POST /api/homing/run` | SSE-Stream, startet `runHoming()` |
|
||||||
|
| State senden | `POST /api/homing/send-state` | Weiterleitung an `ROBOT_URL/api/state` |
|
||||||
|
| Run-Daten | `GET /api/homing/run-data?run=ts` | Debug-Bilder (base64) + finalState |
|
||||||
|
| Frontend | `public/index.html` + `public/client.js` | Homing-Buttons, Fortschrittsbalken, Tree View |
|
||||||
|
|
||||||
|
**Lauf-Verzeichnisse:** `data/homing/{timestamp}/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SSE-Event-Typen
|
||||||
|
|
||||||
|
Das Backend streamt während des Homing-Laufs folgende Events:
|
||||||
|
|
||||||
|
| `type` | Felder | Bedeutung |
|
||||||
|
|--------|--------|-----------|
|
||||||
|
| `log` | `text` | Zeile aus Script-Ausgabe |
|
||||||
|
| `step` | `step`, `total`, `text` | Fortschritt (1–6) |
|
||||||
|
| `analysis` | `key`, `value` | Zwischenergebnis (x_mm, state_Arm1, …) |
|
||||||
|
| `error` | `text` | Fehler (Script-Exit ≠ 0 o.ä.) |
|
||||||
|
| `done` | `exitCode`, `state?`, `runDir` | Abschluss; `state` nur bei Erfolg |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## robot.json — Ladestrategie
|
||||||
|
|
||||||
|
**Aktuell:** Lokale Datei
|
||||||
|
```javascript
|
||||||
ROBOT_JSON = process.env.ROBOT_JSON || 'scripts/robot_1781069752019.json'
|
ROBOT_JSON = process.env.ROBOT_JSON || 'scripts/robot_1781069752019.json'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Geplant: vom Driver per API
|
**Geplant** (wenn Driver `GET ROBOT_URL/api/robot/config` implementiert):
|
||||||
Der Driver-Service (ROBOT_URL) kennt die aktuelle Roboter-Konfiguration.
|
|
||||||
Lademechanismus (bereits implementiertes Muster aus `ROBOT_URL`/`BODYTRACKER_URL`):
|
|
||||||
|
|
||||||
```
|
|
||||||
GET ROBOT_URL/api/robot/config → robot.json Inhalt
|
|
||||||
```
|
|
||||||
|
|
||||||
**Implementierung (Backend `server.js`):**
|
|
||||||
```javascript
|
```javascript
|
||||||
async function loadRobotConfig() {
|
async function loadRobotConfig() {
|
||||||
if (ROBOT_URL) {
|
if (ROBOT_URL) {
|
||||||
// Vom Driver holen
|
|
||||||
const res = await fetch(new URL('/api/robot/config', ROBOT_URL));
|
const res = await fetch(new URL('/api/robot/config', ROBOT_URL));
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
// Fallback: lokale Datei
|
|
||||||
return JSON.parse(await fs.readFile(ROBOT_JSON, 'utf8'));
|
return JSON.parse(await fs.readFile(ROBOT_JSON, 'utf8'));
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
Auswirkung: nur `ROBOT_JSON`-Variable ändern — alle Scripts bekommen automatisch
|
||||||
**Konsequenz für Homing:** Das Homing-Script bekommt robot.json als
|
die aktuelle Konfiguration.
|
||||||
temporäre Datei (bereits vorhandenes Muster: `ROBOT_JSON` als Pfad an Python).
|
|
||||||
Falls ROBOT_URL konfiguriert: zuerst fetch → temp-Datei schreiben → Script aufrufen.
|
|
||||||
|
|
||||||
**Priorität:** Kann nach dem restlichen Homing implementiert werden.
|
|
||||||
Solange ROBOT_URL nicht konfiguriert, läuft alles mit der lokalen Datei.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## X-Position (Slider) – Bestimmung
|
## Offene Punkte
|
||||||
|
|
||||||
Die Slider-Position `x` wird **nicht** manuell eingegeben, sondern aus den
|
- [ ] **Arm-Marker eintragen** (Nutzer): `links.Arm1/Ellbow/Arm2/Hand.markers` in `robot.json`
|
||||||
triangulierten Marker-Positionen berechnet (nach Schritt 3b).
|
- [ ] **Erstmals testen**: Homing-Run mit echtem Roboter und eingetragenen Markern durchführen
|
||||||
|
- [ ] **X-Schätzung verfeinern** (optional): `estimateXFromMarkers()` könnte gelenk-spezifischere Logik nutzen statt einfachem Mittelwert
|
||||||
**Ansatz:** Die absolute X-Position eines bekannten Arm-Markers im
|
- [ ] **robot.json via Driver-API** (optional): wenn Driver `GET ROBOT_URL/api/robot/config` bereitstellt
|
||||||
Board-Koordinatensystem enthält direkt die Slider-Information —
|
|
||||||
alle anderen Gelenke sind rotatorisch und verschieben den Marker nicht
|
|
||||||
entlang der X-Achse des Boards.
|
|
||||||
|
|
||||||
Alternativ: Schwerpunkt der Board-nahen A0-Marker projiziert auf die X-Achse
|
|
||||||
(robust, braucht keine Arm-Marker).
|
|
||||||
|
|
||||||
**In `4b_revolute_angle.py`:** `--x-mm` wird aus `aruco_marker_poses.json`
|
|
||||||
berechnet und als erstes Argument übergeben. Alle weiteren 4b-Aufrufe
|
|
||||||
nutzen `--from-state` des vorherigen Schritts.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Homing-Ablauf (Script-Kette)
|
|
||||||
|
|
||||||
```
|
|
||||||
[Foto] → [1_detect] → [2_camera] → [3b_poses]
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
[4b Arm1]
|
|
||||||
[4b Ellbow]
|
|
||||||
[4b Arm2]
|
|
||||||
[4b Hand]
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
State-JSON
|
|
||||||
{x,y,z,a,b,c,e}
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
POST ROBOT_URL/api/state
|
|
||||||
```
|
|
||||||
|
|
||||||
**Strategie:** `4b_revolute_angle.py` sequenziell, Link für Link von root nach tip.
|
|
||||||
X-Position (`--x-mm`) wird aus den triangulierten Board-Marker-Positionen bestimmt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementierungsplan: Homing-UI
|
|
||||||
|
|
||||||
### ⚠ Wichtig: Schritte 1–3b existieren bereits
|
|
||||||
|
|
||||||
Die Kette **Foto → 1_detect → 2_camera → 3b_poses** ist bereits vollständig
|
|
||||||
implementiert und produktiv in `POST /api/board/run` (`server/server.js`,
|
|
||||||
ca. Zeile 520–606). Sie erzeugt `<runDir>/aruco_marker_poses.json`.
|
|
||||||
|
|
||||||
**Die echten, funktionierenden Aufrufe (NICHT neu erfinden):**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Pro Kamera geloopt (jede Kamera hat eigene NPZ):
|
|
||||||
runScript([SCRIPT_1, '-i', imgPath, '-npz', npzPath, '-robot', ROBOT_JSON,
|
|
||||||
'-cameraId', camId, '-outDir', runDir, '--saveDebugImage']);
|
|
||||||
|
|
||||||
runScript([SCRIPT_2, '-i', detJson, '-robot', ROBOT_JSON, '-outDir', runDir]);
|
|
||||||
|
|
||||||
// Einmal, nach allen Kameras (braucht ≥2 _camera_pose.json):
|
|
||||||
runScript([SCRIPT_3B, '--evalDir', runDir, '--robot', ROBOT_JSON]);
|
|
||||||
// → schreibt <runDir>/aruco_marker_poses.json
|
|
||||||
```
|
|
||||||
|
|
||||||
**Empfehlung:** Die Snapshot+1+2+3b-Logik aus `/api/board/run` in eine
|
|
||||||
gemeinsame Funktion `runBoardPipeline(runDir, send)` auslagern. Homing ruft
|
|
||||||
sie auf und hängt nur den 4b-Teil an. So gibt es keine Duplikate und keine
|
|
||||||
abweichenden Argumente.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 1 — Backend-Route `POST /api/homing/run`
|
|
||||||
|
|
||||||
**Datei:** `server/server.js` (neue Route) + `server/homingOrchestrator.js` (neue Datei)
|
|
||||||
|
|
||||||
**Ablauf als SSE-Stream:**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// server/homingOrchestrator.js
|
|
||||||
export async function runHoming({ robotJsonPath, send }) {
|
|
||||||
|
|
||||||
// 1–3b: bestehende Board-Pipeline wiederverwenden
|
|
||||||
// (Foto + 1_detect + 2_camera + 3b_poses, pro Kamera geloopt)
|
|
||||||
send({ type: 'step', step: 1, text: 'Snapshot + Marker-Triangulation …' });
|
|
||||||
const runDir = await runBoardPipeline(robotJsonPath, send); // aus server.js ausgelagert
|
|
||||||
const arucoJson = path.join(runDir, 'aruco_marker_poses.json');
|
|
||||||
|
|
||||||
// 4. X-Position aus triangulierten Markern bestimmen
|
|
||||||
const xMm = estimateXFromMarkers(arucoJson); // siehe Abschnitt „X-Position"
|
|
||||||
|
|
||||||
// 4b. Gelenkwinkel sequenziell, Link für Link
|
|
||||||
const links = ['Arm1', 'Ellbow', 'Arm2', 'Hand'];
|
|
||||||
let fromState = null;
|
|
||||||
for (const link of links) {
|
|
||||||
send({ type: 'step', text: `Gelenkwinkel ${link} …` });
|
|
||||||
const args = [SCRIPT_4B,
|
|
||||||
'--robot', robotJsonPath, '--aruco', arucoJson,
|
|
||||||
'--link', link,
|
|
||||||
'--output', path.join(runDir, `state_${link}.json`),
|
|
||||||
];
|
|
||||||
if (fromState) args.push('--from-state', fromState);
|
|
||||||
else args.push('--x-mm', String(xMm));
|
|
||||||
const exit = await runScript(args, send);
|
|
||||||
if (exit !== 0) { send({ type: 'error', text: `4b ${link} Exit ${exit}` }); return; }
|
|
||||||
fromState = path.join(runDir, `state_${link}.json`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ergebnis: letztes state_*.json enthält den vollständigen accumulated_state
|
|
||||||
const finalState = JSON.parse(fs.readFileSync(fromState, 'utf8'));
|
|
||||||
send({ type: 'done', state: finalState.accumulated_state, runDir });
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Hinweis:** `runScript()` gibt den Exit-Code zurück (nicht den Pfad).
|
|
||||||
> Der State-Pfad wird separat gemerkt (`fromState`).
|
|
||||||
|
|
||||||
**Route:**
|
|
||||||
```javascript
|
|
||||||
app.post('/api/homing/run', async (req, res) => {
|
|
||||||
res.setHeader('Content-Type', 'text/event-stream');
|
|
||||||
const send = (data) => res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
||||||
try {
|
|
||||||
await runHoming({ robotJsonPath: ROBOT_JSON, send });
|
|
||||||
} catch (err) {
|
|
||||||
send({ type: 'error', text: String(err) });
|
|
||||||
}
|
|
||||||
res.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/api/homing/send-state', async (req, res) => {
|
|
||||||
// Sendet { x, y, z, a, b, c, e } an ROBOT_URL/api/state
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 2 — Frontend `public/homing.html`
|
|
||||||
|
|
||||||
Neue Seite (zugänglich von `index.html` via Link-Button, wie `calibration.html`).
|
|
||||||
|
|
||||||
**Sektionen (identisches Muster wie `index.html`):**
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────────────────────────────────────────────┐
|
|
||||||
│ AKTIONEN │
|
|
||||||
│ [📷 Foto & Homing berechnen] │
|
|
||||||
│ [✅ An Roboter senden] (disabled bis Ergebnis) │
|
|
||||||
│ Status-Badge: ○ Warte ● Läuft ✓ Fertig ✗ Fehler │
|
|
||||||
├──────────────────────────────────────────────────────┤
|
|
||||||
│ AUSGABE / LOG │
|
|
||||||
│ Schritt-für-Schritt Log aller Scripts (SSE-Stream) │
|
|
||||||
│ Fortschritt: ──── Schritt 3/6 ──── │
|
|
||||||
├──────────────────────────────────────────────────────┤
|
|
||||||
│ ANALYSIS & REASONING │
|
|
||||||
│ Zwischenergebnisse je Script als JSON │
|
|
||||||
│ { camera_reprojection_px, arm1_std_deg, … } │
|
|
||||||
├──────────────────┬───────────────────────────────────┤
|
|
||||||
│ RESULT RAW JSON │ RESULT TREE VIEW │
|
|
||||||
│ { │ x (Slider): 180.0 mm │
|
|
||||||
│ "x": 180.0, │ y (Arm1): +23.4° │
|
|
||||||
│ "y": 23.4, │ z (Ellbow): -12.1° │
|
|
||||||
│ ... │ a (Arm2): +5.0° │
|
|
||||||
│ } │ b (Hand): 0.0° │
|
|
||||||
│ │ c (Palm): 0.0° │
|
|
||||||
│ │ e (Greifer): 0.0 mm │
|
|
||||||
├──────────────────┴───────────────────────────────────┤
|
|
||||||
│ SNAPSHOT CSV (Marker-Tabelle) │
|
|
||||||
│ ID │ Link │ x mm │ y mm │ z mm │ Residual │ │
|
|
||||||
│ 218 │ Arm2 │ 229.1 │ 118.5 │ 48.3 │ 2.1 mm │ │
|
|
||||||
├──────────────────────────────────────────────────────┤
|
|
||||||
│ SNAPSHOTS (annotierte Kamerabilder) │
|
|
||||||
│ [cam0] [cam1] [cam2] │
|
|
||||||
└──────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Schlüssel-Implementierungsdetails:**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// homing.js (client)
|
|
||||||
|
|
||||||
// SSE-Stream vom Backend empfangen
|
|
||||||
async function runHoming() {
|
|
||||||
const response = await fetch('/api/homing/run', { method: 'POST' });
|
|
||||||
await readSseStream(response, appendLog, (evt) => {
|
|
||||||
if (evt.type === 'step') { updateProgress(evt); }
|
|
||||||
if (evt.type === 'analysis') { showAnalysis(evt.data); }
|
|
||||||
if (evt.type === 'done') {
|
|
||||||
showResult(evt.state);
|
|
||||||
enableSendButton(evt.state);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ergebnis an Roboter senden
|
|
||||||
async function sendToRobot(state) {
|
|
||||||
await fetch('/api/homing/send-state', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ state }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 3 — robot.json via Driver-API
|
|
||||||
|
|
||||||
**Voraussetzung:** ROBOT_URL ist konfiguriert und der Driver hat `GET /api/robot/config`.
|
|
||||||
|
|
||||||
**Implementierung in `server.js`:**
|
|
||||||
```javascript
|
|
||||||
// Beim Start oder on-demand: robot.json vom Driver laden
|
|
||||||
async function fetchRobotConfig() {
|
|
||||||
if (!ROBOT_URL) return; // lokale Datei reicht
|
|
||||||
const res = await fetch(new URL('/api/robot/config', ROBOT_URL));
|
|
||||||
if (!res.ok) return; // Fallback auf lokale Datei
|
|
||||||
const data = await res.json();
|
|
||||||
// Temporär in data/robot/robot_live.json cachen
|
|
||||||
await fs.writeFile(ROBOT_JSON_LIVE, JSON.stringify(data, null, 2));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Auswirkung:** Nur `ROBOT_JSON` Variable ändern — alle Scripts bekommen
|
|
||||||
automatisch die aktuelle Konfiguration.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## To-Do / Fortschritt
|
|
||||||
|
|
||||||
### Voraussetzungen (Kalibrierung abschliessen)
|
|
||||||
|
|
||||||
- [x] Kamera-Intrinsik (NPZ) kalibriert
|
|
||||||
- [x] Board-Marker-Positionen kalibriert
|
|
||||||
- [x] X-Achsen-Richtung kalibriert
|
|
||||||
- [x] Arm1 Joint-Origin Y/Z — Button in `calibration_arm.html` ausführbar
|
|
||||||
- [ ] **Arm-Marker eintragen** (Nutzer): `links.Arm1.markers`, `links.Ellbow.markers`, `links.Arm2.markers`, `links.Hand.markers` in `scripts/robot_*.json`
|
|
||||||
- [ ] Arm1 Joint-Origin Y/Z Button klicken + in robot.json gespeichert
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 0 — Refactor: Board-Pipeline auslagern
|
|
||||||
|
|
||||||
- [x] Funktion `runBoardPipeline(runDir, send)` in `server/server.js` extrahieren
|
|
||||||
- [x] Bestehende Logik aus `POST /api/board/run` in Funktion verschieben
|
|
||||||
- [x] `POST /api/board/run` ruft `runBoardPipeline()` auf (Verhalten unverändert)
|
|
||||||
- [ ] Test: Board-Run funktioniert weiterhin identisch
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 1 — Backend `POST /api/homing/run`
|
|
||||||
|
|
||||||
- [x] Konstante `SCRIPT_4B` in `server/server.js` ergänzen
|
|
||||||
- [x] `server/homingOrchestrator.js` erstellen
|
|
||||||
- [x] `estimateXFromMarkers(arucoJsonPath)` implementieren
|
|
||||||
- [x] `runHoming({ robotJsonPath, homingDir, send, runScript, runBoardPipeline, SCRIPT_4B })` implementieren
|
|
||||||
- [x] `runBoardPipeline()` aufrufen → `aruco_marker_poses.json`
|
|
||||||
- [x] X-Position berechnen
|
|
||||||
- [x] 4b-Schleife: Arm1 → Ellbow → Arm2 → Hand (sequenziell, `--from-state`)
|
|
||||||
- [x] `send({ type: 'done', state: accumulated_state, runDir })`
|
|
||||||
- [x] Route `POST /api/homing/run` in `server/server.js` (SSE-Stream)
|
|
||||||
- [x] Route `POST /api/homing/send-state` in `server/server.js`
|
|
||||||
- [x] Route `GET /api/homing/run-data` (Bilder + State für Frontend)
|
|
||||||
- [x] `ROBOT_URL` Konstante in `server/server.js` ergänzen
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 2 — Frontend `public/homing.html`
|
|
||||||
|
|
||||||
- [x] Datei `public/homing.html` erstellen
|
|
||||||
- [x] Sektion **Aktionen**: Button „Foto & Homing berechnen", Button „An Roboter senden"
|
|
||||||
- [x] Sektion **Ausgabe / Log**: SSE-Stream-Ausgabe, Schritt-Fortschritt
|
|
||||||
- [x] Sektion **Analysis & Reasoning**: Zwischenergebnisse je Script als JSON
|
|
||||||
- [x] Sektion **Result Raw JSON** + **Result Tree View**: `{ x, y, z, a, b, c, e }`
|
|
||||||
- [x] Sektion **Snapshot CSV**: Marker-Tabelle (ID, Link, x, y, z)
|
|
||||||
- [x] Sektion **Snapshots**: annotierte Kamerabilder
|
|
||||||
- [x] `public/homing.js` erstellen
|
|
||||||
- [x] `runHoming()`: POST + SSE-Stream lesen, Log befüllen, Fortschrittsbalken
|
|
||||||
- [x] `showResult(state)`: Tree View + Raw JSON befüllen, Send-Button aktivieren
|
|
||||||
- [x] `sendToRobot(state)`: POST `/api/homing/send-state`
|
|
||||||
- [x] `loadRunData(runDir)`: Debug-Bilder nach dem Run laden
|
|
||||||
- [x] Link-Button von `public/index.html` zu `homing.html` ergänzen
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 3 — State an Roboter senden
|
|
||||||
|
|
||||||
- [ ] Route `POST /api/homing/send-state` in `server/server.js` registrieren
|
|
||||||
- [ ] Body: `{ state: { x, y, z, a, b, c, e } }`
|
|
||||||
- [ ] Weiterleitung an `ROBOT_URL/api/state` (analog zu `robotActions.js`)
|
|
||||||
- [ ] Fehler wenn `ROBOT_URL` nicht konfiguriert: JSON-Fehler zurückgeben
|
|
||||||
- [ ] Frontend: Fehler-Feedback wenn kein ROBOT_URL
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 4 — robot.json via Driver-API *(nach allem anderen)*
|
|
||||||
|
|
||||||
- [ ] `loadRobotConfig()` Funktion in `server/server.js`
|
|
||||||
- [ ] Wenn `ROBOT_URL` gesetzt: `GET ROBOT_URL/api/robot/config` → temp-Datei cachen
|
|
||||||
- [ ] Fallback: lokale Datei (Verhalten unverändert)
|
|
||||||
- [ ] Homing und Board-Run nutzen `loadRobotConfig()` statt direkt `ROBOT_JSON`
|
|
||||||
- [ ] *(Voraussetzung: Driver implementiert `GET /api/robot/config`)*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Status-Übersicht
|
|
||||||
|
|
||||||
| Bereich | Status |
|
|
||||||
|---------|--------|
|
|
||||||
| Python-Scripts (1, 2, 3b, 4b) | ✅ vorhanden |
|
|
||||||
| Kalibrierung (Kamera, Board, X-Achse) | ✅ fertig |
|
|
||||||
| Arm1 Joint-Origin Button | ✅ ausführbar |
|
|
||||||
| Arm-Marker in robot.json | 🔶 Nutzer |
|
|
||||||
| Phase 0 – runBoardPipeline() | ✅ fertig |
|
|
||||||
| Phase 1 – /api/homing/run | ✅ fertig |
|
|
||||||
| Phase 2 – homing.html | ✅ fertig |
|
|
||||||
| Phase 3 – /api/homing/send-state | ✅ Route implementiert |
|
|
||||||
| Phase 4 – robot.json via Driver-API | ⏳ später |
|
|
||||||
|
|||||||
303
doc/Kalibrierung.md
Normal file
303
doc/Kalibrierung.md
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
# Kalibrierung – appRobotHoming
|
||||||
|
|
||||||
|
> Stand: 2026-06-14
|
||||||
|
> Einmaliger Vorgang — nur nach mechanischen Änderungen wiederholen.
|
||||||
|
> Jede Stufe baut auf der vorherigen auf.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
```
|
||||||
|
[1] Camera NPZ → [2] Board → [3] X-Achse → [4] Arm1 Y-Achse
|
||||||
|
```
|
||||||
|
|
||||||
|
| Schritt | UI-Tab | Ergebnis | Status |
|
||||||
|
|---------|--------|----------|--------|
|
||||||
|
| [1] Camera NPZ | Camera NPZ | `data/calibration/{session}/{cam}_calibration.npz` | ✅ |
|
||||||
|
| [2] Board | Board | `links.Board.markers[].position` in `robot.json` | ✅ |
|
||||||
|
| [3] X-Achse | Robot X Axis | Alle Marker-Positionen in `robot.json` rotiert | ✅ |
|
||||||
|
| [4] Arm1 Y-Achse | Arm1 – Y | `links.Arm1.jointToParent.origin[1,2]` in `robot.json` | ✅ |
|
||||||
|
| Arm-Marker eintragen | — | `links.Arm1/Ellbow/Arm2/Hand.markers` | 🔶 Nutzer |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1] Camera NPZ — Kameraparameter
|
||||||
|
|
||||||
|
**Ziel:** Intrinsische Parameter (Brennweite, Verzerrungskoeffizienten, Kameramatrix)
|
||||||
|
für jede Kamera als `.npz`-Datei speichern.
|
||||||
|
|
||||||
|
**Ablauf:**
|
||||||
|
1. ChArUco-Board aus verschiedenen Winkeln fotografieren (mehrere Posen)
|
||||||
|
2. OpenCV `calibrateCamera()` berechnet Kameraparameter
|
||||||
|
3. Ergebnis als `.npz` speichern und an Webcam-Service übertragen
|
||||||
|
|
||||||
|
**Speichert:** `data/calibration/{session}/{cam}_calibration.npz`
|
||||||
|
|
||||||
|
**Wird verwendet von:** Script `1_detect_aruco_observations.py` beim Board-Run und Homing
|
||||||
|
(Argument `-npz`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [2] Board — Referenz-Marker-Positionen
|
||||||
|
|
||||||
|
**Ziel:** Absolute 3D-Positionen aller Board-ArUco-Marker im Welt-Koordinatensystem
|
||||||
|
bestimmen und in `robot.json` speichern.
|
||||||
|
|
||||||
|
**Ablauf:**
|
||||||
|
1. Foto mit allen Kameras (Snapshot)
|
||||||
|
2. Script `1_detect_aruco_observations.py` → `{cam}_aruco_detection.json`
|
||||||
|
3. Script `2_estimate_camera_from_observations.py` → `{cam}_camera_pose.json`
|
||||||
|
4. Script `3b_corner_marker_poses.py` → `aruco_marker_poses.json` (Triangulierung)
|
||||||
|
5. Positionen in `robot.json` unter `links.Board.markers` eintragen / bestätigen
|
||||||
|
|
||||||
|
**Aktionen im Board-Tab:**
|
||||||
|
- **Board erkennen**: startet den vollständigen Foto+Script-Durchlauf (SSE-Stream)
|
||||||
|
- **Marker zuordnen** (Z-Bereich, Set, Link): Marker manuell kategorisieren
|
||||||
|
- **Sets justieren** (Kabsch 2D+Z): Zwei Marker-Sets aufeinander ausrichten
|
||||||
|
- **Marker ID zuordnen / entfernen**: einzelne Marker-Korrekturen
|
||||||
|
|
||||||
|
**Speichert:** `links.Board.markers` in `robot.json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [3] X-Achse — Schieberichtung des Sliders
|
||||||
|
|
||||||
|
**Ziel:** X-Achse des Roboters (Slider-Richtung `Base → Arm1`) im Board-Koordinatensystem
|
||||||
|
ausrichten, sodass `[1, 0, 0]` der realen Bewegungsrichtung entspricht.
|
||||||
|
|
||||||
|
**Ablauf:**
|
||||||
|
1. Roboter auf Position A fahren → Board erkennen
|
||||||
|
2. Roboter auf Position B fahren (mind. 20 mm Unterschied) → Board erkennen
|
||||||
|
3. Board-Viewer berechnet den Richtungsvektor der Markerbewegungen
|
||||||
|
4. „X-Achse übernehmen": alle Marker-Positionen in `robot.json` werden rotiert,
|
||||||
|
sodass die gemessene Richtung zur neuen X-Achse `[1, 0, 0]` wird
|
||||||
|
|
||||||
|
**Implementierung:** `editRobot.js` → `adoptXAxis()` — rotiert alle `links.*.markers[].position`
|
||||||
|
um den A0-Schwerpunkt als Ursprung.
|
||||||
|
|
||||||
|
**Speichert:** alle `links.*.markers[].position` in `robot.json` (rotiert)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [4] Arm1 — Y-Rotationsachse (Schultergelenk)
|
||||||
|
|
||||||
|
**Ziel:** Lage und Richtung der Rotationsachse zwischen `Base` und `Arm1` bestimmen
|
||||||
|
und als `jointToParent.origin` in `robot.json` speichern.
|
||||||
|
|
||||||
|
**Ablauf:**
|
||||||
|
1. Board erkennen mit Arm in **Position A**
|
||||||
|
2. Arm1 um ≥ 15° drehen
|
||||||
|
3. Board erkennen (**Position B**)
|
||||||
|
4. Arm1 nochmals ≥ 15° drehen
|
||||||
|
5. Board erkennen (**Position C**)
|
||||||
|
6. Board-Viewer berechnet automatisch die Rotationsachse (magenta Linie im 3D-Viewer)
|
||||||
|
7. Aktion **„Joint-Origin Y/Z übernehmen"**: speichert Y/Z des Achspunkts in `robot.json`
|
||||||
|
|
||||||
|
**Optional — Aktion „Fixe Marker → Link Base":**
|
||||||
|
Marker mit Bewegung < 10 mm über alle drei Positionen sind physisch am Basis-Körper
|
||||||
|
befestigt. Diese werden in `links.Base.markers` eingetragen.
|
||||||
|
|
||||||
|
**Speichert:**
|
||||||
|
- `links.Arm1.jointToParent.origin[1]` (Y) und `[2]` (Z) in `robot.json`
|
||||||
|
- Optional: `links.Base.markers` ergänzt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Mathematik: Bestimmung der Rotationsachse
|
||||||
|
|
||||||
|
Bei einer Rotation um die Y-Achse bewegt sich jeder erkannte Marker auf einem
|
||||||
|
**Kreisbogen** im 3D-Raum. Aus den beobachteten Marker-Positionen zu mehreren
|
||||||
|
Zeitstempeln lassen sich Lage und Richtung der Rotationsachse berechnen.
|
||||||
|
|
||||||
|
#### Wie viele Beobachtungen sind nötig?
|
||||||
|
|
||||||
|
Entscheidend ist nicht „2 oder 3 Positionen" allein, sondern die Kombination aus
|
||||||
|
Anzahl Marker und Anzahl Positionen:
|
||||||
|
|
||||||
|
| Beobachtungen | Bestimmbar? | Begründung |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 Marker, 2 Positionen | **Nein** | Achse liegt *irgendwo* in der Mittelsenkrechten-Ebene der Sehne; Richtung und Lage bleiben unterbestimmt |
|
||||||
|
| **2 Marker, je 2 Positionen** | **Ja** | **Verfahren A**: Richtung aus d₁ × d₂, Lage aus Schnitt zweier Mittelsenkrechten-Ebenen |
|
||||||
|
| 1 Marker, 3 Positionen | **Ja** | **Verfahren B**: drei Punkte definieren einen eindeutigen Umkreis → eindeutige Achse |
|
||||||
|
| N Marker, je ≥ 2 bzw. ≥ 3 Positionen | Ja, überbestimmt | Least-Squares, robust gegenüber Messrauschen, erlaubt Fehlerabschätzung |
|
||||||
|
|
||||||
|
Die häufige Kurzbehauptung „zwei Positionen reichen nicht" gilt nur für **einen
|
||||||
|
einzigen** Marker. Sobald **zwei** (nicht-entartete) Marker vorliegen, genügen
|
||||||
|
zwei Positionen — siehe Verfahren A.
|
||||||
|
|
||||||
|
> Die aktuelle Implementierung (`public/yAxisCompute.js`) verwendet **Verfahren B**.
|
||||||
|
|
||||||
|
#### Verfahren A — Zwei Marker, je zwei Positionen
|
||||||
|
|
||||||
|
Gegeben: M₁ₐ, M₁ᵦ (Marker 1 zu Zeit a, b) und M₂ₐ, M₂ᵦ (Marker 2 zu Zeit a, b).
|
||||||
|
Beide Marker sitzen am selben starren Körper, der zwischen a und b um die gesuchte
|
||||||
|
Achse rotiert. Verschiebungsvektoren (Sehnen der Kreisbögen):
|
||||||
|
|
||||||
|
```
|
||||||
|
d₁ = M₁ᵦ − M₁ₐ
|
||||||
|
d₂ = M₂ᵦ − M₂ₐ
|
||||||
|
```
|
||||||
|
|
||||||
|
**Schritt 1 – Achsenrichtung.** Jeder Punkt bewegt sich in einer Ebene **senkrecht**
|
||||||
|
zur Achse; die Sehne liegt in dieser Ebene, also `d₁ ⊥ n` und `d₂ ⊥ n`:
|
||||||
|
|
||||||
|
```
|
||||||
|
n = (d₁ × d₂) / |d₁ × d₂| ← Einheitsvektor entlang der Rotationsachse
|
||||||
|
```
|
||||||
|
|
||||||
|
**Schritt 2 – Achsenlage.** Der Kreismittelpunkt eines Markers ist von Start- und
|
||||||
|
Endposition gleich weit entfernt, liegt also in der **Mittelsenkrechten-Ebene** der
|
||||||
|
Sehne. Da zusätzlich `n ⊥ dᵢ`, liegt die **gesamte Achse** in dieser Ebene.
|
||||||
|
|
||||||
|
```
|
||||||
|
Ebene 1: d₁ · (x − P₁) = 0, P₁ = (M₁ₐ + M₁ᵦ) / 2
|
||||||
|
Ebene 2: d₂ · (x − P₂) = 0, P₂ = (M₂ₐ + M₂ᵦ) / 2
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Achse ist die **Schnittgerade** beider Ebenen mit Richtung `n`. Ein Punkt A auf
|
||||||
|
ihr (Ansatz A = α·d₁ + β·d₂, Komponente entlang n zu 0 gesetzt):
|
||||||
|
|
||||||
|
```
|
||||||
|
c₁ = d₁ · P₁, c₂ = d₂ · P₂
|
||||||
|
D = |d₁|²·|d₂|² − (d₁ · d₂)² (= |d₁ × d₂|²)
|
||||||
|
|
||||||
|
α = (c₁·|d₂|² − c₂·(d₁ · d₂)) / D
|
||||||
|
β = (c₂·|d₁|² − c₁·(d₁ · d₂)) / D
|
||||||
|
|
||||||
|
A = α·d₁ + β·d₂ ← Referenzpunkt auf der Achse
|
||||||
|
```
|
||||||
|
|
||||||
|
Ergebnis: `r(t) = A + t·n`.
|
||||||
|
|
||||||
|
**Entartung:** `D = 0` ⟺ `d₁ ∥ d₂` ⟺ beide Marker liegen **mit der Achse in einer
|
||||||
|
gemeinsamen Ebene**. Dann sind die Mittelsenkrechten-Ebenen parallel und schneiden
|
||||||
|
sich nicht eindeutig. Erkennung über `|d₁ × d₂| / (|d₁|·|d₂|) ≈ 0`; auch ein Marker
|
||||||
|
direkt auf der Achse liefert `dᵢ ≈ 0`. → anderen Marker wählen oder Verfahren B.
|
||||||
|
|
||||||
|
#### Verfahren B — Ein Marker, drei Positionen *(implementiert)*
|
||||||
|
|
||||||
|
Gegeben P₁, P₂, P₃ desselben Markers zu drei Drehwinkeln. Die drei Punkte liegen
|
||||||
|
auf einem Kreis, dessen Ebene senkrecht zur Achse steht.
|
||||||
|
|
||||||
|
**Schritt 1 — Achsenrichtung** (Normalenvektor der Kreisebene):
|
||||||
|
```
|
||||||
|
v₁ = P₂ − P₁
|
||||||
|
v₂ = P₃ − P₁
|
||||||
|
n = normalize(v₁ × v₂)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Schritt 2 — Umkreismittelpunkt** (Punkt auf der Achse), über baryzentrische Gewichte:
|
||||||
|
```
|
||||||
|
a² = |P₂ − P₃|², b² = |P₁ − P₃|², c² = |P₁ − P₂|²
|
||||||
|
|
||||||
|
w₁ = a²·(b² + c² − a²)
|
||||||
|
w₂ = b²·(a² + c² − b²)
|
||||||
|
w₃ = c²·(a² + b² − c²)
|
||||||
|
|
||||||
|
C = (w₁·P₁ + w₂·P₂ + w₃·P₃) / (w₁ + w₂ + w₃)
|
||||||
|
```
|
||||||
|
|
||||||
|
Ergebnis: `r(t) = C + t·n`. **Entartung:** P₁,P₂,P₃ kollinear (zu kleiner Drehwinkel)
|
||||||
|
→ `|v₁ × v₂| ≈ 0`, Umkreis numerisch schlecht bestimmt.
|
||||||
|
|
||||||
|
#### Kombination über N Marker (Least Squares)
|
||||||
|
|
||||||
|
Beide Verfahren lassen sich über mehrere Marker mitteln — robuster und mit
|
||||||
|
Fehlerabschätzung.
|
||||||
|
|
||||||
|
**Achsenrichtung gemeinsam.** Aus `dᵢ ⊥ n` (bzw. Ebenen-Normalen) minimiert die beste
|
||||||
|
Richtung `Σ (dᵢ · n)²` unter `|n| = 1`:
|
||||||
|
```
|
||||||
|
M = Σ dᵢ · dᵢᵀ (3×3-Matrix)
|
||||||
|
n = Eigenvektor von M zum kleinsten Eigenwert
|
||||||
|
```
|
||||||
|
(Verallgemeinert das Kreuzprodukt. Verfahren B alternativ: alle Normalen nᵢ aufs
|
||||||
|
gleiche Halbraum ausrichten und mitteln.)
|
||||||
|
|
||||||
|
**Achsenlage gemeinsam.** Für Verfahren B (Umkreismittelpunkte Cᵢ) ergibt sich der
|
||||||
|
einfache Schwerpunkt:
|
||||||
|
```
|
||||||
|
axisDir = normalize( mean(nᵢ) ) ← Vorzeichen vorher angleichen
|
||||||
|
axisPoint = mean(Cᵢ) ← Schwerpunkt der Umkreismittelpunkte
|
||||||
|
```
|
||||||
|
|
||||||
|
**Residuum pro Marker** (Verfahren B, Fehler- / Ausreißer-Erkennung):
|
||||||
|
```
|
||||||
|
εᵢ = |(Cᵢ − C̄) − ((Cᵢ − C̄)·n̄)·n̄|
|
||||||
|
```
|
||||||
|
Große εᵢ deuten auf einen fehlerhaften Marker oder eine nicht-rotatorische
|
||||||
|
Bewegungskomponente hin.
|
||||||
|
|
||||||
|
Die Y/Z-Koordinaten von `axisPoint` geben an, wo die Rotationsachse durch die
|
||||||
|
YZ-Ebene geht — das ist der gesuchte `jointToParent.origin`.
|
||||||
|
|
||||||
|
#### Genauigkeit & Verfahrenswahl
|
||||||
|
|
||||||
|
Beide Verfahren sind im **rauschfreien** Fall exakt; die Unterschiede liegen in
|
||||||
|
Robustheit und Aufwand:
|
||||||
|
|
||||||
|
| Kriterium | Verfahren A (2 Marker, 2 Bilder) | Verfahren B (1 Marker, 3 Bilder) |
|
||||||
|
|---|---|---|
|
||||||
|
| Mindest-Aufnahmen | 2 Bilder | 3 Bilder |
|
||||||
|
| Mindest-Marker | 2 (gut separiert) | 1 |
|
||||||
|
| Eigenständige Fehler-Schätzung | nur über mehrere Marker | pro Marker (Residuum εᵢ) |
|
||||||
|
| Kritische Entartung | d₁ ∥ d₂ (Marker koplanar mit Achse) | P₁,P₂,P₃ kollinear (kleiner Winkel) |
|
||||||
|
| Empfindlichkeit | hoch, wenn Sehnen fast parallel oder Drehwinkel klein | hoch, wenn Drehwinkel klein (Bogen fast gerade) |
|
||||||
|
|
||||||
|
**Beide** brauchen einen ausreichend großen Drehwinkel: kleine Winkel liefern kurze
|
||||||
|
Sehnen bzw. fast gerade Bögen, in denen das Messrauschen dominiert.
|
||||||
|
|
||||||
|
- **Verfahren A** ist schneller (zwei statt drei Aufnahmen), sinnvoll bei ≥ 2 gut
|
||||||
|
getrennten Markern.
|
||||||
|
- **Verfahren B** ist robuster für die **Fehlerrechnung**: jeder Marker liefert
|
||||||
|
unabhängig eine vollständige Achsen-Schätzung, das Residuum εᵢ erlaubt
|
||||||
|
Ausreißer-Erkennung, keine Entartung zwischen Markern. Für belastbare Genauigkeit
|
||||||
|
daher vorzuziehen — und der Grund, weshalb die Implementierung Verfahren B nutzt.
|
||||||
|
|
||||||
|
**Praktische Werte (beide Verfahren):**
|
||||||
|
- Drehwinkel zwischen den Positionen ≥ 15° (besser ≥ 30°), sonst numerisch instabil.
|
||||||
|
- 3–4 Marker mit guter räumlicher Verteilung um die Achse.
|
||||||
|
- Die Rotationswinkel müssen **nicht** bekannt sein – nur die 3D-Koordinaten.
|
||||||
|
|
||||||
|
**Marker-Filter (`min_movement_mm = 10`):**
|
||||||
|
Marker, die sich zwischen A/B/C weniger als 10 mm bewegen, sind nicht am rotierenden
|
||||||
|
Teil befestigt und werden aus der Achsenberechnung ausgeschlossen. Ihre Position A
|
||||||
|
wird für die optionale Base-Link-Zuweisung gespeichert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Implementierungsnachweis
|
||||||
|
|
||||||
|
Die Implementierung in `public/yAxisCompute.js` setzt **Verfahren B** um:
|
||||||
|
|
||||||
|
| Schritt | Implementierung | Verifikation |
|
||||||
|
|---------|----------------|--------------|
|
||||||
|
| Normalenvektor | `yAxisCompute.js` Z. 51–61 | Kreuzprodukt v₁×v₂, normiert ✓ |
|
||||||
|
| Umkreismittelpunkt | `yAxisCompute.js` Z. 63–76 | Baryzentrische Gewichte ✓ |
|
||||||
|
| Mittelung Normalen | `yAxisCompute.js` Z. 153–162 | Vorzeichen-Alignment + mean + renormieren ✓ |
|
||||||
|
| Mittelung Achspunkte | `yAxisCompute.js` Z. 164–167 | Schwerpunkt der Cᵢ ✓ |
|
||||||
|
| Origin speichern | `calibration.js` → `setOriginBtn` sendet `axisPoint[1,2]` | `editRobot.js` schreibt `origin[1,2]` ✓ |
|
||||||
|
|
||||||
|
**Ergebnis: Implementierung entspricht der beschriebenen Mathematik (Verfahren B) vollständig.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dateistruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
data/
|
||||||
|
calibration/
|
||||||
|
{session}/
|
||||||
|
meta.json
|
||||||
|
{cam}_calibration.npz ← Kameraparameter
|
||||||
|
{cam}_aruco_detection.json ← Marker-Erkennung
|
||||||
|
{cam}_camera_pose.json ← Kamera-Pose
|
||||||
|
scripts/
|
||||||
|
robot_1781069752019.json ← robot.json (Haupt-Konfiguration)
|
||||||
|
links.Board.markers ← Board-Marker-Positionen
|
||||||
|
links.Base.markers ← fixe Basis-Marker (nach Y-Kalibrierung)
|
||||||
|
links.Arm1.jointToParent.origin ← Schultergelenk-Position (nach Y-Kalibrierung)
|
||||||
|
links.Arm1.markers ← Arm1-Marker (Nutzer eingetragen)
|
||||||
|
...
|
||||||
|
```
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
title="Erst Homing ausführen">
|
title="Erst Homing ausführen">
|
||||||
✅ An Roboter senden
|
✅ An Roboter senden
|
||||||
</button>
|
</button>
|
||||||
<span id="homing-status" class="status-badge open">○ Warte</span>
|
<span id="homing-status" class="status-badge idle">○ Warte</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fortschrittsbalken -->
|
<!-- Fortschrittsbalken -->
|
||||||
|
|||||||
@@ -237,8 +237,9 @@ textarea {
|
|||||||
background: #1e293b;
|
background: #1e293b;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
}
|
}
|
||||||
|
.status-badge.idle { color: #60a5fa; }
|
||||||
|
.status-badge.wip { color: #93c5fd; }
|
||||||
.status-badge.open { color: #f59e0b; }
|
.status-badge.open { color: #f59e0b; }
|
||||||
.status-badge.wip { color: #60a5fa; }
|
|
||||||
.status-badge.done { color: #34d399; background: #064e3b; }
|
.status-badge.done { color: #34d399; background: #064e3b; }
|
||||||
|
|
||||||
/* ===== HOMING FORTSCHRITTSBALKEN ===== */
|
/* ===== HOMING FORTSCHRITTSBALKEN ===== */
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
|
|
||||||
const n = cross.map(c => c / crossLen);
|
const n = cross.map(c => c / crossLen);
|
||||||
|
|
||||||
// Baryzentrische Gewichte → Umkreismittelpunkt (doc/04_y_achse.md)
|
// Baryzentrische Gewichte → Umkreismittelpunkt (doc/Kalibrierung.md → [4] Arm1, Verfahren B)
|
||||||
const a2 = dist2(P2, P3), b2 = dist2(P1, P3), c2 = dist2(P1, P2);
|
const a2 = dist2(P2, P3), b2 = dist2(P1, P3), c2 = dist2(P1, P2);
|
||||||
const w1 = a2 * (b2 + c2 - a2);
|
const w1 = a2 * (b2 + c2 - a2);
|
||||||
const w2 = b2 * (a2 + c2 - b2);
|
const w2 = b2 * (a2 + c2 - b2);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Gegeben drei Messungen (Pos A, B, C) – in denen dieselben fremd-Marker
|
|||||||
- Referenzpunkt auf der Achse (axisPoint_mm)
|
- Referenzpunkt auf der Achse (axisPoint_mm)
|
||||||
- Residuen pro Punkt (Qualitätsmass)
|
- Residuen pro Punkt (Qualitätsmass)
|
||||||
|
|
||||||
Methode (doc/04_y_achse.md):
|
Methode (doc/Kalibrierung.md → [4] Arm1, Verfahren B):
|
||||||
Jeder Marker bewegt sich auf einem Kreisbogen. Die Ebene des Kreises
|
Jeder Marker bewegt sich auf einem Kreisbogen. Die Ebene des Kreises
|
||||||
steht senkrecht zur Rotationsachse → Normalenvektor = Achsenrichtung.
|
steht senkrecht zur Rotationsachse → Normalenvektor = Achsenrichtung.
|
||||||
Der Umkreismittelpunkt des Dreiecks P1-P2-P3 liegt auf der Achse.
|
Der Umkreismittelpunkt des Dreiecks P1-P2-P3 liegt auf der Achse.
|
||||||
|
|||||||
Reference in New Issue
Block a user