"""Client for the marker-finder service (port 8082). Marker-finder accepts OpenCV intrinsics (`camera_matrix` 3x3 + `dist_coeffs`) either as a per-request `intrinsics` form field on `/detect`, or as a stored default via `POST /intrinsics`. We push the live Nuwa intrinsics at startup (see main.py) so every subsequent `/detect` produces correct poses with no extra work, and we still expose a per-call override for unusual cases. """ from __future__ import annotations import json from typing import Any import httpx class MarkerClient: def __init__(self, base_url: str, timeout_s: float = 5.0): self.base_url = base_url.rstrip("/") self.timeout_s = timeout_s def detect_url(self, markers: list[dict[str, Any]], image_url: str, intrinsics: dict[str, Any] | None = None) -> dict[str, Any]: """Returns {'found': [...], 'not_found': [...]}. Each found item includes 'corners' (Nx2), 'id', 'size_mm', 'position_m', 'rotation_matrix', 'quaternion_xyzw'. Pose is computed by marker-finder with the active default intrinsics (or the per-request override). Args: intrinsics: optional per-call override, OpenCV format `{"camera_matrix": [[fx,0,cx],[0,fy,cy],[0,0,1]], "dist_coeffs": [k1,k2,p1,p2,k3]}` (mostly unused now that we push the Nuwa defaults at startup; kept for tools that want to detect against a custom camera). """ data = {"markers": json.dumps(markers), "image_url": image_url} if intrinsics is not None: data["intrinsics"] = json.dumps(intrinsics) with httpx.Client(timeout=self.timeout_s) as c: r = c.post(f"{self.base_url}/detect", data=data) r.raise_for_status() return r.json() def get_intrinsics(self) -> dict[str, Any]: """Currently-active default intrinsics on marker-finder.""" with httpx.Client(timeout=self.timeout_s) as c: r = c.get(f"{self.base_url}/intrinsics") r.raise_for_status() return r.json() def set_intrinsics(self, camera_matrix: list[list[float]], dist_coeffs: list[float], persist: bool = False) -> dict[str, Any]: """Replace marker-finder's default intrinsics. `persist=True` writes them into marker-finder's config.yaml so they survive a restart. We default to in-memory only — presa-mano repushes them at every startup, so persisting would just create config drift. """ body = {"camera_matrix": camera_matrix, "dist_coeffs": dist_coeffs} with httpx.Client(timeout=self.timeout_s) as c: r = c.post(f"{self.base_url}/intrinsics", params={"persist": "true" if persist else "false"}, json=body) r.raise_for_status() return r.json()