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

13 KiB
Raw Blame History

Kalibrierung Arm-Marker

Stand: 2026-06-15
Ergänzung zu doc/Kalibrierung.md → Schritt „Arm-Marker eintragen und verifizieren"
Dient als Programmier-Roadmap für den UI-Tab „Marker" und die Verifikations-Pipeline.


Was ist ein Arm-Marker?

Ein ArUco-Aufkleber, der auf einem beweglichen Roboter-Glied (Arm1, Ellbow, Arm2, …) befestigt ist. Arm-Marker unterscheiden sich von Board-Markern:

Board-Marker Arm-Marker
Position fest, kalibriert bewegt sich mit dem Gelenk
Zweck Referenz für Kamera-Pose Gelenk-Winkel-Schätzung (Homing)
Position in robot.json links.Board.markers links.{Link}.markers
Koordinatensystem Welt (Board) lokal im Link-Frame

Marker-Daten in robot.json

{
  "links": {
    "Arm1": {
      "markers": [
        {
          "id":       198,          // ArUco-ID (eindeutig)
          "name":     "aruco_198",  // optional, lesbar
          "position": [0, -160, 35],  // Mittelpunkt im lokalen Link-Frame [mm]
          "normal":   [0, 0, 1],    // Normale der Marker-Fläche (Link-Frame)
          "size":     25,           // Kantenlänge mm
          "spin":     0             // Drehung der Marker-Grafik um die Normale [°]
        }
      ]
    }
  }
}

Felder

Feld Typ Bedeutung
id int ArUco-ID — muss mit gedrucktem Marker übereinstimmen
position [x,y,z] mm Mittelpunkt im lokalen Link-Frame (nicht Welt)
normal [nx,ny,nz] Flächennormale des Markers im Link-Frame; [0,0,1] = Marker schaut in +Z
size mm Kantenlänge des ArUco-Quadrats
spin ° Drehung der Aufkleber-Grafik um die Normale — 0 / 90 / 180 / 270

Spin-Semantik

Ein ArUco-Aufkleber kann in 4 Lagen aufgeklebt werden. Physisch ist das egal — der Detektor findet die ID unabhängig von der Orientierung, und auch die gemessene Mittelpunkt-Position ist spin-unabhängig.

spin beschreibt nur die visuelle Darstellung im 3D-Viewer, damit die angezeigte Marker-Grafik mit dem echten Aufkleber übereinstimmt. Das hilft beim visuellen Abgleich: wenn die Grafik im Viewer verdreht zum Aufkleber steht, ist spin falsch.

In der aktuellen Homing-Pipeline (3b, 4b) wird spin nicht verwendet.
Relevant wird es, wenn der Viewer spin korrekt darstellt (→ offenes Todo unten).


Typischer Workflow

1. ArUco-Aufkleber auf Roboter-Glied kleben
         │
         ▼
2. Position messen / schätzen → in robot.json eintragen
   (position = Mittelpunkt im lokalen Link-Frame, normal = Flächen-Richtung)
         │
         ▼
3. Homing-Run starten (homing.html)
         │
         ▼
4. Kalibrierung → Tab "Marker" öffnen
         │  · Tabelle zeigt alle Marker und ihren aktuellen spin
         │  · Viewer zeigt Modell-Marker (Vierecke) + beobachtete Punkte (Kugeln)
         │  · Fehler-Linien: Modell-Marker → beobachteter Punkt
         ▼
5. Prüfen:
   · Linie kurz → Position stimmt gut
   · Linie lang oder in falscher Richtung → position in robot.json korrigieren
   · Viewer-Grafik verdreht → spin korrigieren (+90 / -90 / 180)
         │
         ▼
6. Spin / Normal korrigieren → Viewer lädt neu → erneut prüfen

UI-Tab „Marker" (implementiert 2026-06-15)

Datei: public/calibration_marker.html

Drei Abschnitte:

Abschnitt Inhalt
Aktuelle Marker Tabelle aller Arm-Marker (Link, ID, Name, Position, Normal, Size, Spin) aus robot.json
Aktionen Link-Dropdown → Marker-ID-Dropdown → Spin-Buttons (90°, +90°, 180°)
Viewer boardViewer.html?mode=homing — Modell + letzter Homing-Run

JS: initMarker() in public/calibration.js

Funktion Beschreibung
loadRobot() Holt /api/robot, rendert Tabelle, befüllt ID-Dropdown
renderTable(robot) Tabelle aller Arm-Marker mit farbig hervorgehobenem Spin
updateMarkerDropdown() Link-Dropdown-Wechsel → ID-Dropdown neu befüllen
applySpin(delta) POST /api/robot/set-arm-marker-spin → Viewer reload-Message

Backend: POST /api/robot/set-arm-marker-spin

Body:   { linkName: string, markerId: number, spin: number }
Return: { changed, linkName, markerId, oldSpin, newSpin }

Implementiert in server/server.js → delegiert an setArmMarkerSpin() in server/editRobot.js.


Offene Punkte (Programmier-Roadmap)

[P1] Spin-Rendering im boardViewer

Status: implementiert (2026-06-15)
Datei: public/boardViewer.htmlbuildSkeletonFK(), Abschnitt „4. Arm-Marker"

spin wird als zusätzliche Rotation um die Marker-Normale (in Welt-Koordinaten) auf das orientierte Quadrat angewendet via premultiply:

const normalW    = new THREE.Vector3(nx, nz, -ny).transformDirection(childFrame).normalize();
const markerMesh = makeMarkerSquareOriented(posWorld, normalW, markerSizeM, col);
const spinRad    = ((m.spin ?? 0) * Math.PI) / 180;
if (Math.abs(spinRad) > 1e-6) {
  markerMesh.quaternion.premultiply(
    new THREE.Quaternion().setFromAxisAngle(normalW, spinRad)
  );
}

premultiply(Q_spin) setzt quaternion = Q_spin * Q_normal → zuerst Normal-Alignment, dann Spin-Rotation in Welt-Koordinaten um normalW.


[P2] Marker-Position verifizieren: Positions-Residuum

Status: noch nicht implementiert

Nach einem Homing-Run kennen wir für jeden erkannten Arm-Marker:

  • model_pos_world = Modell-Mittelpunkt im Welt-Frame (via FK)
  • obs_pos_world = triangulierter beobachteter Mittelpunkt

Das Positions-Residuum |model_pos_world obs_pos_world| zeigt, wie gut die eingetragene position (Mittelpunkt im lokalen Frame) stimmt.

⚠ Wichtig: Spin-Fehler sind damit NICHT erkennbar.
Der Mittelpunkt eines Markers ist spin-unabhängig — egal wie ein Aufkleber gedreht ist, das Zentrum bleibt dasselbe. Ein falscher spin-Wert erzeugt daher kein Positions-Residuum. Spin-Fehler erfordern → P3 (visuell) oder P4 (automatisch).

Erweiterung im Marker-Tab: Tabelle um Spalten ergänzen:

  • Δ mm = Positions-Residuum des letzten Homing-Runs
  • Status = < 5 mm / ⚠ 520 mm / > 20 mm

Datenquelle: /api/homing/run-data?run={ts}finalState + measuredMarkers


[P3] Python: Ecken-Position in aruco_marker_poses.json ausgeben

Status: noch nicht implementiert
Datei: scripts/3b_corner_marker_poses.py

Voraussetzung für P3b. Aktuell enthält aruco_marker_poses.json pro Marker nur position_mm (Mittelpunkt) und normal. Die Spin-Orientierung geht verloren.

3b trianguliert den Mittelpunkt aus den Ecken der Kamera-Beobachtungen. Dieselbe Triangulierung auf Ecke 0 anwenden und zusätzlich ausgeben:

# Zusätzliches Feld pro Marker in aruco_marker_poses.json:
"corner0_mm": [x, y, z]   # triangulierte Welt-Position von Ecke 0

Ecke 0 = top-left in OpenCVs ArUco-Konvention (Index 0 in corners[0]). Zusammen mit position_mm (Mittelpunkt) definiert corner0_mm eindeutig die Orientierung des Markers in 3D.

Der Mittelpunkt bleibt spin-unabhängig. corner0_mm ist das einzige Feld, das den Spin kodiert.


[P3b] boardViewer: Orientierungszeiger zeichnen

Status: Modell-Seite implementiert (2026-06-15) · Beobachtungs-Seite offen (→ P3)
Datei: public/boardViewer.htmlbuildSkeletonFK()
Voraussetzungen: P1 , P3 (corner0_mm) für Beobachtungs-Zeiger noch offen

Für jeden Marker werden zwei kurze Linien vom Mittelpunkt zu Ecke 0 gezeichnet — eine für das Modell, eine für die Beobachtung. Der Winkel zwischen beiden = Spin-Fehler.

Modell-Zeiger (implementiert — nutzt markerMesh.quaternion direkt):

markerMesh.quaternion kodiert bereits Q_spin * Q_normal, daher reicht:

// Richtung zur Ecke 0: (1,1,0) normalisiert im lokalen Marker-Frame,
// transformiert durch die bereits berechnete Mesh-Rotation (Q_normal ∘ Q_spin)
const ptrDir   = new THREE.Vector3(1, 1, 0).normalize().applyQuaternion(markerMesh.quaternion);
const corner0W = posWorld.clone().add(ptrDir.multiplyScalar(markerSizeM * Math.SQRT1_2));
gArmMarkers.add(makeLine(posWorld, corner0W, col, 0.9));   // Zeiger (Link-Farbe)
gArmMarkers.add(makeSphere(corner0W, 0.0008, col));         // Ecke 0 (Punkt)

Math.SQRT1_2 = 1/√2 weil der Abstand Mittelpunkt→Ecke bei einem Quadrat mit Kantenlänge size genau size/√2 beträgt ((size/2)·√2 = size/√2).

Beobachtungs-Zeiger (aus corner0_mm in aruco_marker_poses.json, sobald Python das Feld liefert → P3):

// obs = beobachteter Marker aus _measuredMarkers
if (obs.corner0_mm) {
  const corner0Obs = r2vArr(obs.corner0_mm);                          // robot→Three.js
  gArmMarkers.add(makeLine(obsPosW, corner0Obs, 0xffffff, 0.6));     // Beobachtungs-Zeiger (dünn)
  gArmMarkers.add(makeSphere(corner0Obs, 0.0006, 0xffffff));         // Beobachtungs-Ecke
}

Ergebnis im Viewer:

  Modell-Mittelpunkt ●——▶ Modell-Ecke 0      (Link-Farbe, voll)
  Obs-Mittelpunkt    ●··▶ Obs-Ecke 0         (weiß, dünn)

Zeigen beide Zeiger in dieselbe Richtung → spin korrekt.
90°-Unterschied → spin um 90° falsch → +90° oder 90° klicken.


[P3c] Homing-Check direkt im Marker-Tab starten

Status: noch nicht implementiert

Button „Homing-Check starten" im Marker-Tab triggert die vollständige Homing-Pipeline (POST /api/homing/run) und zeigt:

  • SSE-Log im Tab-internen Textfeld
  • Fortschritt im Viewer via postMessage({ type: 'homing-state', state })

Kein struktureller Unterschied zu homing.html — Code-Duplizierung vermeiden durch Extraktion eines gemeinsamen runHomingStream(sendFn, frameFn) aus client.js.

Für Spin-Verifikation ist P3c nötig, damit frische Beobachtungsdaten im Viewer landen. Ohne P3 (corner0_mm) sieht man trotzdem nur Mittelpunkt-Fehlerlinien, keine Zeiger.


[P4] Spin automatisch berechnen und korrigieren

Status: noch nicht implementiert
Voraussetzungen: P3 (corner0_mm), P3b (Zeiger im Viewer)

Sobald corner0_mm aus Python vorliegt und der Viewer die Zeiger anzeigt (P3b), kann der tatsächliche Spin-Wert rechnerisch bestimmt werden — ohne manuelles Raten.

Berechnung im Browser (in initMarker() oder buildSkeletonFK()):

// Modell-Richtung zu Ecke 0 bei spin=0 (im Welt-Frame, via FK):
const modelCorner0Dir = /* corner0World  posWorld, normalisiert */;

// Beobachtete Richtung zu Ecke 0:
const obsCorner0Dir = r2vArr(obs.corner0_mm).sub(obsPosW).normalize();

// Winkel zwischen beiden (um die Marker-Normale):
const cross = new THREE.Vector3().crossVectors(modelCorner0Dir, obsCorner0Dir);
const sinA  = cross.dot(normalWorld);   // Vorzeichen = Drehrichtung
const cosA  = modelCorner0Dir.dot(obsCorner0Dir);
const deltaDeg = Math.round(Math.atan2(sinA, cosA) * 180 / Math.PI / 90) * 90;
// → rundet auf nächste 90°: 0 / 90 / -90 / 180

UI-Erweiterung im Marker-Tab:

  • Tabelle bekommt Spalte „Gemessener Spin" und „Soll-Spin (robot.json)"
  • Unterschied → Badge „⚠ +90°" → Klick übernimmt die Korrektur direkt

[P5] Marker-Position aus Homing übernehmen

Status: offen

Wenn ein Arm-Marker nur grob eingemessen wurde, kann die triangulierte Welt-Position aus dem Homing-Run dazu genutzt werden, die position in robot.json zu verfeinern:

position_local = inverse_FK(obs_world, current_state)

Setzt voraus, dass der Gelenk-Winkel für diesen Link bereits korrekt bestimmt wurde. Iterativ einsetzbar: grobe Startposition → erster Homing → verfeinerte Position → …


Abhängigkeits-Kette Spin-Verifikation

P1  boardViewer rendert spin korrekt          → Modell-Viereck zeigt echte Orientierung
P3  3b gibt corner0_mm aus                    → Beobachtungs-Orientierung verfügbar
P3b boardViewer zeichnet Orientierungszeiger  → Spin-Fehler sichtbar als Winkel
P3c Marker-Tab: Homing-Check-Button           → frische Daten ohne Tab-Wechsel
P4  JS berechnet Δspin, schlägt Korrektur vor → kein manuelles Raten mehr

Dateiübersicht

Datei Rolle
public/calibration.html Tab-Button „Marker"
public/calibration_marker.html Panel-HTML (Tabelle, Aktionen, Viewer)
public/calibration.jsinitMarker() Frontend-Logik des Tabs
server/server.jsPOST /api/robot/set-arm-marker-spin Spin-Endpoint
server/editRobot.jssetArmMarkerSpin() robot.json schreiben
public/boardViewer.htmlbuildSkeletonFK() Spin-Rendering (→ P1) + Zeiger (→ P3b)
scripts/3b_corner_marker_poses.py corner0_mm ausgeben (→ P3)
scripts/robot_1781069752019.jsonlinks.*.markers Marker-Daten