7.0 KiB
7.0 KiB
API Integration — approbot-pipeline
Der Service läuft als HTTP-Server auf Port 8080 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)
import requests
BASE = "http://localhost:8080"
# ── 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_<id>.png / render_<id>.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)
files.append(("robot_json", ("robot.json", open("robot.json", "rb"), "application/json")))
resp = requests.post(f"{BASE}/v1/estimate", files=files)
Fehlerbehandlung
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
import asyncio
import httpx
async def estimate(camera_ids: list[str]) -> dict:
async with httpx.AsyncClient(base_url="http://localhost:8080") 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)
import { readFileSync } from "fs";
const BASE = "http://localhost:8080";
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)
npm install axios form-data
const axios = require("axios");
const FormData = require("form-data");
const fs = require("fs");
const BASE = "http://localhost:8080";
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
{
"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.