# API Integration — appRobotBodyTrack Der Service läuft als HTTP-Server auf Port 8446 und ist von jeder Sprache aus erreichbar. Alle Requests nutzen `multipart/form-data`. --- ## Endpunkte im Überblick | Endpunkt | Methode | Input | Output | |---|---|---|---| | `/v1/estimate` | POST | Bilder + Intrinsiken (+ optional robot.json) | Gelenkwinkel JSON | | `/v1/health` | GET | — | `{"status": "ok", "version": "1.0.0"}` | | `/v1/config` | GET | — | Aktiver `pose_estimation`-Block | --- ## Python (`requests`) ```python import requests BASE = "http://localhost:8446" # ── Health-Check ──────────────────────────────────────────────── resp = requests.get(f"{BASE}/v1/health") print(resp.json()) # {"status": "ok", "version": "1.0.0"} # ── Pose-Schätzung ─────────────────────────────────────────────── # Bilder und zugehörige Intrinsiken in der gleichen Reihenfolge übergeben. # Dateinamen müssen render_.png / render_.npz sein — # die ID (a, b, c, ...) verknüpft Bild und Intrinsik intern. camera_ids = ["a", "b", "c"] files = [] for cid in camera_ids: files.append(("images", (f"render_{cid}.png", open(f"render_{cid}.png", "rb"), "image/png"))) files.append(("intrinsics", (f"render_{cid}.npz", open(f"render_{cid}.npz", "rb"), "application/octet-stream"))) resp = requests.post(f"{BASE}/v1/estimate", files=files) resp.raise_for_status() result = resp.json() print(result["joints"]) # {"x": 50.2, "y": -2.1, "z": 94.8, "a": 20.1, "b": 59.9, "c": 9.0, "e": 3.0} print(result["confidence"]) # {"x": "high", "b": "low", ...} print(result["residual_rms"]) # 1.45 print(result["processing_ms"]) # 1240 ``` ### robot.json pro Request mitschicken (überschreibt Server-Konfig) ```python files.append(("robot_json", ("robot.json", open("robot.json", "rb"), "application/json"))) resp = requests.post(f"{BASE}/v1/estimate", files=files) ``` ### Fehlerbehandlung ```python resp = requests.post(f"{BASE}/v1/estimate", files=files) if resp.status_code == 400: print("Ungültige Eingabe:", resp.json()["detail"]) elif resp.status_code == 500: print("Pipeline-Fehler:", resp.json()["detail"]) else: resp.raise_for_status() joints = resp.json()["joints"] ``` ### Async mit `httpx` ```python import asyncio import httpx async def estimate(camera_ids: list[str]) -> dict: async with httpx.AsyncClient(base_url="http://localhost:8446") as client: files = [] for cid in camera_ids: files.append(("images", (f"render_{cid}.png", open(f"render_{cid}.png", "rb")))) files.append(("intrinsics", (f"render_{cid}.npz", open(f"render_{cid}.npz", "rb")))) resp = await client.post("/v1/estimate", files=files, timeout=60.0) resp.raise_for_status() return resp.json() result = asyncio.run(estimate(["a", "b", "c"])) ``` --- ## Node.js ### Native `fetch` + `FormData` (Node 18+, kein extra Paket) ```js import { readFileSync } from "fs"; const BASE = "http://localhost:8446"; const cameraIds = ["a", "b", "c"]; // ── Health-Check ──────────────────────────────────────────────── const health = await fetch(`${BASE}/v1/health`); console.log(await health.json()); // { status: 'ok', version: '1.0.0' } // ── Pose-Schätzung ─────────────────────────────────────────────── const form = new FormData(); for (const id of cameraIds) { form.append( "images", new Blob([readFileSync(`render_${id}.png`)], { type: "image/png" }), `render_${id}.png` ); form.append( "intrinsics", new Blob([readFileSync(`render_${id}.npz`)], { type: "application/octet-stream" }), `render_${id}.npz` ); } const resp = await fetch(`${BASE}/v1/estimate`, { method: "POST", body: form }); if (!resp.ok) { const err = await resp.json(); throw new Error(`Pipeline-Fehler ${resp.status}: ${err.detail}`); } const result = await resp.json(); console.log(result.joints); // { x: 50.2, y: -2.1, z: 94.8, a: 20.1, b: 59.9, c: 9.0, e: 3.0 } console.log(result.confidence); // { x: 'high', b: 'low', ... } ``` ### `axios` + `form-data` (Node 16 / CommonJS-Umgebungen) ```bash npm install axios form-data ``` ```js const axios = require("axios"); const FormData = require("form-data"); const fs = require("fs"); const BASE = "http://localhost:8446"; const cameraIds = ["a", "b", "c"]; const form = new FormData(); for (const id of cameraIds) { form.append("images", fs.createReadStream(`render_${id}.png`), `render_${id}.png`); form.append("intrinsics", fs.createReadStream(`render_${id}.npz`), `render_${id}.npz`); } const resp = await axios.post(`${BASE}/v1/estimate`, form, { headers: form.getHeaders(), timeout: 60_000, }); const { joints, confidence, residual_rms, n_markers, processing_ms } = resp.data; console.log(joints); ``` --- ## Response-Format ```json { "joints": { "x": 50.2, "y": -2.1, "z": 94.8, "a": 20.1, "b": 59.9, "c": 9.0, "e": 3.0 }, "confidence": { "x": "high", "y": "high", "z": "high", "a": "high", "b": "low", "c": "low", "e": "low" }, "residual_rms": 1.45, "n_markers": 56, "processing_ms": 1240 } ``` ### Felder | Feld | Typ | Einheit | Beschreibung | |---|---|---|---| | `joints.x` | float | mm | Linearachse X | | `joints.y` | float | mm | Linearachse Y | | `joints.z` | float | mm | Linearachse Z (Höhe) | | `joints.a` | float | ° | Drehgelenk A | | `joints.b` | float | ° | Drehgelenk B | | `joints.c` | float | ° | Drehgelenk C | | `joints.e` | float | ° | Fingergelenk E | | `confidence.*` | string | — | `high` / `medium` / `low` / `none` | | `residual_rms` | float | mm | RMS-Restfehler der Schätzung | | `n_markers` | int | — | Anzahl triangulierter Marker | | `processing_ms` | int | ms | Gesamtlaufzeit der Pipeline | ### Confidence-Stufen | Wert | Bedeutung | |---|---| | `high` | Gelenk gut durch mehrere Marker beobachtet | | `medium` | Gelenk beobachtet, aber mit eingeschränkter Geometrie | | `low` | Nur indirekt oder mit wenigen Markern beobachtet | | `none` | Gelenk nicht beobachtbar (z.B. alle Marker verdeckt) | ### HTTP-Fehlercodes | Code | Bedeutung | |---|---| | `400` | Eingabefehler (fehlende Dateien, falsche Namen, keine robot.json) | | `500` | Pipeline-Fehler (ArUco nicht gefunden, Triangulation fehlgeschlagen, …) | --- ## Dateinamens-Konvention Die Kamera-ID in Dateinamen verknüpft Bild und Intrinsik: ``` render_a.png ←→ render_a.npz # Kamera "a" render_b.png ←→ render_b.npz # Kamera "b" render_c.png ←→ render_c.npz # Kamera "c" ``` Die ID kann ein Buchstabe oder eine kurze alphanumerische Zeichenkette sein. Reihenfolge der `files`-Liste ist egal — die Zuordnung erfolgt über den Dateinamen.