331 lines
14 KiB
Markdown
331 lines
14 KiB
Markdown
# Kalibrierung – appRobotHoming
|
||
|
||
> Stand: 2026-06-15
|
||
> 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 → [5] Arm-Marker
|
||
```
|
||
|
||
| 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` | ✅ |
|
||
| [5] Arm-Marker | Marker | `links.*.markers[].{position,spin}` visuell prüfen und korrigieren | ✅ |
|
||
|
||
---
|
||
|
||
## [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.**
|
||
|
||
---
|
||
|
||
## [5] Arm-Marker — Spin-Verifikation und Korrektur
|
||
|
||
**Ziel:** Sicherstellen, dass die in `robot.json` eingetragenen Arm-Marker
|
||
(Position, Normale, Spin) mit dem realen Aufkleber übereinstimmen — insbesondere
|
||
die `spin`-Orientierung, die der Homing-Viewer korrekt darstellen muss.
|
||
|
||
**Ablauf:**
|
||
1. Arm-Marker physisch aufkleben und Position/Normale grob in `robot.json` eintragen
|
||
2. Homing-Run starten (Tab „Homing" oder `homing.html`)
|
||
3. Kalibrierung → Tab **„Marker"** öffnen:
|
||
- Tabelle zeigt alle Arm-Marker mit aktuellem `spin`-Wert
|
||
- Viewer zeigt 3D-Skeleton mit spin-orientiertem Marker-Quadrat und Orientierungszeiger (→ Ecke 0)
|
||
4. Visuelle Prüfung: zeigt die Viewer-Grafik dieselbe Orientierung wie der echte Aufkleber?
|
||
5. Falls nicht: Link und Marker-ID wählen → **−90° / +90° / 180°** klicken → Viewer lädt neu
|
||
6. Wiederholen bis Modell und Realität übereinstimmen
|
||
|
||
**Aktionen im Marker-Tab:**
|
||
- **Link-Dropdown + ID-Dropdown**: Marker auswählen
|
||
- **Spin-Buttons** (−90°, +90°, 180°): Spin in `robot.json` korrigieren und Viewer neu laden
|
||
- **Viewer** (boardViewer.html?mode=homing): zeigt Skeleton mit Marker-Quadraten und Orientierungszeigern
|
||
|
||
**Speichert:** `links.{Link}.markers[].spin` in `robot.json` (normalisiert auf [0, 360))
|
||
|
||
**Details und Roadmap:** [`doc/Kalibrierung_Marker.md`](Kalibrierung_Marker.md)
|
||
|
||
---
|
||
|
||
## 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)
|
||
...
|
||
```
|