feat: calibration helper scripts + DANIILIDIS solver default + dual-robot README

- scripts/: explore_and_calibrate.py (probe envelope from actual + random sampling),
  extend_calibration.py (loop fino a n_used >= TARGET_GOOD), repeatability.py
  (diagnostica bias vs noise sul marker-finder)
- config.yaml: calibration.method TSAI → DANIILIDIS (sweep su 49 sample H1 sx
  mostra 4× meno residuo di TSAI per via del bias intrinsics-dipendente di
  marker-finder)
- README: dual-robot G1/H1, scripts dir, caveat noti (wrist passthrough,
  marker-finder intrinsics floor, H1 4 DoF, /api/arms/go_neutral non proxato)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal PastroandClaude Opus 4.7 committed 2026-05-08 18:34:59 +02:00
1 parent f6197488f9
commit 4981306062
5 files changed
+790 -16

No files matched your search

+104
View File
@@ -0,0 +1,104 @@
"""Test ripetibilità marker-finder: polso fermo, N detection consecutive,
calcola deviazione standard di tvec e rotazione (rvec). Dirige se bias o noise."""
from __future__ import annotations
import math
import statistics as stats
import time
from typing import Any
import requests
ORC = "http://localhost:8890"
H1 = "http://localhost:8767"
N_FRAMES = 30
SETTLE_S = 0.5 # tra detection
def post(url, body=None, t=10):
r = requests.post(url, json=body or {}, timeout=t)
return r.json()
def get(url, t=5):
return requests.get(url, timeout=t).json()
def go_home():
return post(f"{H1}/arms/go_neutral", {})
def detect():
return post(f"{ORC}/api/calibration/preview", {})
def main() -> int:
print("=== repeatability test (left, H1) — N=", N_FRAMES, flush=True)
print("[0] go_neutral + settle", flush=True)
print(go_home(), flush=True)
time.sleep(3.0)
pos = get(f"{ORC}/api/wrist")["left_wrist"]["position"]
print(f"polso fermo a {[round(v,4) for v in pos]}", flush=True)
tvecs: list[list[float]] = []
rvecs: list[list[float]] = []
quats: list[list[float]] = []
centers: list[list[float]] = []
timings: list[float] = []
found = 0
for i in range(N_FRAMES):
d = detect()
if not d.get("found"):
print(f" frame {i}: NOT FOUND", flush=True)
continue
found += 1
pose = d["pose_camera"]
tvecs.append(pose["tvec"])
rvecs.append(pose["rvec"])
quats.append(pose["quaternion_xyzw"])
centers.append(d["center_px"])
timings.append(d.get("timings_ms", {}).get("marker_detect", 0))
if i < 3 or i == N_FRAMES - 1:
print(f" frame {i}: tvec={[round(v,4) for v in pose['tvec']]}", flush=True)
time.sleep(SETTLE_S)
print(f"\nfound: {found}/{N_FRAMES}", flush=True)
if found < 5:
return 1
def std(xs):
return stats.pstdev(xs) if len(xs) > 1 else 0.0
def fmt(label, vals):
return f" {label}: mean=[{vals[0]:+.4f},{vals[1]:+.4f},{vals[2]:+.4f}] std=[{std([t[0] for t in tvecs])*1000:.2f},{std([t[1] for t in tvecs])*1000:.2f},{std([t[2] for t in tvecs])*1000:.2f}]mm"
mean_t = [sum(t[i] for t in tvecs) / len(tvecs) for i in range(3)]
std_t_mm = [std([t[i] for t in tvecs]) * 1000 for i in range(3)]
mean_r = [sum(r[i] for r in rvecs) / len(rvecs) for i in range(3)]
std_r_deg = [std([r[i] for r in rvecs]) * 180 / math.pi for i in range(3)]
mean_c = [sum(c[i] for c in centers) / len(centers) for i in range(2)]
std_c_px = [std([c[i] for c in centers]) for i in range(2)]
radial_t = math.sqrt(sum(s ** 2 for s in std_t_mm))
radial_r = math.sqrt(sum(s ** 2 for s in std_r_deg))
print("\n--- statistics su T_cam_marker (camera frame) ---", flush=True)
print(f"mean tvec : [{mean_t[0]:+.4f}, {mean_t[1]:+.4f}, {mean_t[2]:+.4f}] m", flush=True)
print(f"std tvec : [{std_t_mm[0]:.2f}, {std_t_mm[1]:.2f}, {std_t_mm[2]:.2f}] mm (radial {radial_t:.2f} mm)", flush=True)
print(f"std rvec : [{std_r_deg[0]:.3f}, {std_r_deg[1]:.3f}, {std_r_deg[2]:.3f}] deg (radial {radial_r:.3f} deg)", flush=True)
print(f"mean center px: [{mean_c[0]:.1f}, {mean_c[1]:.1f}] std: [{std_c_px[0]:.2f}, {std_c_px[1]:.2f}] px", flush=True)
print(f"detect latency mean: {sum(timings)/len(timings):.1f} ms", flush=True)
print("\n--- diagnosi ---", flush=True)
if radial_t < 5.0:
print(f"σ_trans={radial_t:.1f}mm < 5mm → noise basso, residuo dominato da BIAS (verosimilmente intrinsics).", flush=True)
elif radial_t > 20.0:
print(f"σ_trans={radial_t:.1f}mm > 20mm → noise alto, marker o lighting.", flush=True)
else:
print(f"σ_trans={radial_t:.1f}mm intermedio: noise + bias entrambi presenti.", flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())