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
|
||||
> Ziel: Aus einem einzigen Kamera-Snapshot die aktuellen Gelenkwinkel/-positionen
|
||||
> des Roboters bestimmen und an den Controller senden.
|
||||
> Stand: 2026-06-14
|
||||
> Homing läuft bei **jedem Einschalten** — schnell, vollautomatisch, ohne mechanische Endschalter.
|
||||
|
||||
---
|
||||
|
||||
## Was ist Homing?
|
||||
|
||||
Homing = der Roboter weiss **nicht**, wo er ist.
|
||||
Die Kameras schauen auf das Board + die ArUco-Marker am Roboter und berechnen
|
||||
daraus die vollständige Pose aller Gelenke — ohne mechanische Endschalter.
|
||||
Der Roboter weiss beim Einschalten nicht, wo er steht.
|
||||
Die Kameras schauen auf die ArUco-Marker am Roboter und berechnen daraus
|
||||
die vollständige Pose aller Gelenke.
|
||||
|
||||
Homing läuft bei **jedem** Einschalten ab: schnell, robust, vollautomatisch.
|
||||
Kalibrierung hingegen läuft nur nach mechanischen Änderungen (≈ einmalig).
|
||||
**Homing** (dieser Prozess): bei jedem Einschalten, automatisch.
|
||||
**Kalibrierung** (`doc/Kalibrierung.md`): nur nach mechanischen Änderungen.
|
||||
|
||||
---
|
||||
|
||||
## Kinematik-Kette (aus `robot.json → links`)
|
||||
## Kinematik-Kette
|
||||
|
||||
```
|
||||
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)
|
||||
```
|
||||
|
||||
**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 |
|
||||
|-----|-------------|--------|
|
||||
| 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 |
|
||||
Homing setzt eine abgeschlossene Kalibrierung voraus:
|
||||
|
||||
> **Kalibrierung gilt als abgeschlossen** sobald der Arm1-Button geklickt und
|
||||
> die Arm-Marker eingetragen sind.
|
||||
| Was | Status |
|
||||
|-----|--------|
|
||||
| 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'
|
||||
```
|
||||
|
||||
### Geplant: vom Driver per API
|
||||
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`):**
|
||||
**Geplant** (wenn Driver `GET ROBOT_URL/api/robot/config` implementiert):
|
||||
```javascript
|
||||
async function loadRobotConfig() {
|
||||
if (ROBOT_URL) {
|
||||
// Vom Driver holen
|
||||
const res = await fetch(new URL('/api/robot/config', ROBOT_URL));
|
||||
return res.json();
|
||||
}
|
||||
// Fallback: lokale Datei
|
||||
return JSON.parse(await fs.readFile(ROBOT_JSON, 'utf8'));
|
||||
}
|
||||
```
|
||||
|
||||
**Konsequenz für Homing:** Das Homing-Script bekommt robot.json als
|
||||
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.
|
||||
Auswirkung: nur `ROBOT_JSON`-Variable ändern — alle Scripts bekommen automatisch
|
||||
die aktuelle Konfiguration.
|
||||
|
||||
---
|
||||
|
||||
## X-Position (Slider) – Bestimmung
|
||||
## Offene Punkte
|
||||
|
||||
Die Slider-Position `x` wird **nicht** manuell eingegeben, sondern aus den
|
||||
triangulierten Marker-Positionen berechnet (nach Schritt 3b).
|
||||
|
||||
**Ansatz:** Die absolute X-Position eines bekannten Arm-Markers im
|
||||
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 |
|
||||
- [ ] **Arm-Marker eintragen** (Nutzer): `links.Arm1/Ellbow/Arm2/Hand.markers` in `robot.json`
|
||||
- [ ] **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
|
||||
- [ ] **robot.json via Driver-API** (optional): wenn Driver `GET ROBOT_URL/api/robot/config` bereitstellt
|
||||
|
||||
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">
|
||||
✅ An Roboter senden
|
||||
</button>
|
||||
<span id="homing-status" class="status-badge open">○ Warte</span>
|
||||
<span id="homing-status" class="status-badge idle">○ Warte</span>
|
||||
</div>
|
||||
|
||||
<!-- Fortschrittsbalken -->
|
||||
|
||||
@@ -237,8 +237,9 @@ textarea {
|
||||
background: #1e293b;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.status-badge.idle { color: #60a5fa; }
|
||||
.status-badge.wip { color: #93c5fd; }
|
||||
.status-badge.open { color: #f59e0b; }
|
||||
.status-badge.wip { color: #60a5fa; }
|
||||
.status-badge.done { color: #34d399; background: #064e3b; }
|
||||
|
||||
/* ===== HOMING FORTSCHRITTSBALKEN ===== */
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
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 w1 = a2 * (b2 + c2 - a2);
|
||||
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)
|
||||
- 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
|
||||
steht senkrecht zur Rotationsachse → Normalenvektor = Achsenrichtung.
|
||||
Der Umkreismittelpunkt des Dreiecks P1-P2-P3 liegt auf der Achse.
|
||||
|
||||
Reference in New Issue
Block a user