feat(right_arm): perm assi cam->polso, precise=true default, verify_tag UI
Lavoro accumulato nella sessione 2026-05-21 prima del brainstorming
della tab Pose 3D:
- compute_delta_wrist generalizzato con cam_to_wrist_perm: la mappa
cam->polso non e' solo sign-flip ma una permutazione (default
identita' per backward compat). DeltaOversize ora espone X/Y/Z
nella stringa d'errore. Test aggiornati.
- G1Client.wrist_absolute / wrist_relative: precise=True di default
(era False). Stessa cosa per wrist_routes.AbsoluteReq Pydantic
e per agent.tools move_wrist_*. Movimenti reali ora chiedono
controllo accurato salvo override esplicito.
- Tab Braccio DX:
- /verify_tag endpoint: snapshot wrist FK + tag id=1 dalla front
camera. Per verifica relativa moveto vs marker (no calibrazione).
- GUI: pannello "Verifica tag id=1" + campi camera_offset_wrist_mm.
- markUserTouchedOnInput esteso a raOff/raSgn/raCamOff.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8d40ebaf89
commit
57954b5c8d
8 files changed
+238
-23
No files matched your search
@@ -121,7 +121,7 @@ def _h_move_wrist_absolute(args: dict[str, Any]) -> dict[str, Any]:
|
||||
return _err("position deve avere 3 float (m), quaternion 4 float (xyzw)")
|
||||
dry_run = bool(args.get("dry_run", True))
|
||||
timeout_s = float(args.get("timeout_s", 5.0))
|
||||
precise = bool(args.get("precise", False))
|
||||
precise = bool(args.get("precise", True))
|
||||
result = _deps.g1(dry_run=dry_run).wrist_absolute(
|
||||
side=side, position=position, quaternion=quaternion,
|
||||
wait=True, timeout_s=timeout_s, precise=precise,
|
||||
@@ -144,7 +144,7 @@ def _h_move_wrist_relative(args: dict[str, Any]) -> dict[str, Any]:
|
||||
return _err("delta_quaternion deve avere 4 float (xyzw) o essere null")
|
||||
dry_run = bool(args.get("dry_run", True))
|
||||
timeout_s = float(args.get("timeout_s", 5.0))
|
||||
precise = bool(args.get("precise", False))
|
||||
precise = bool(args.get("precise", True))
|
||||
result = _deps.g1(dry_run=dry_run).wrist_relative(
|
||||
side=side,
|
||||
delta_position=list(dp) if dp is not None else None,
|
||||
@@ -516,8 +516,8 @@ TOOL_DEFINITIONS: list[dict[str, Any]] = [
|
||||
"quaternion": {"type": "array", "items": {"type": "number"}, "minItems": 4, "maxItems": 4,
|
||||
"description": "x,y,z,w"},
|
||||
"dry_run": {"type": "boolean", "default": True},
|
||||
"precise": {"type": "boolean", "default": False,
|
||||
"description": "true = controllo accurato (più lento)"},
|
||||
"precise": {"type": "boolean", "default": True,
|
||||
"description": "true = controllo accurato (più lento, default)"},
|
||||
"timeout_s": {"type": "number", "default": 5.0},
|
||||
},
|
||||
},
|
||||
@@ -540,7 +540,7 @@ TOOL_DEFINITIONS: list[dict[str, Any]] = [
|
||||
"delta_position": {"type": "array", "items": {"type": "number"}, "minItems": 3, "maxItems": 3},
|
||||
"delta_quaternion": {"type": "array", "items": {"type": "number"}, "minItems": 4, "maxItems": 4},
|
||||
"dry_run": {"type": "boolean", "default": True},
|
||||
"precise": {"type": "boolean", "default": False},
|
||||
"precise": {"type": "boolean", "default": True},
|
||||
"timeout_s": {"type": "number", "default": 5.0},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -42,6 +42,7 @@ def _state_payload(sw: Stopwatch) -> dict[str, Any]:
|
||||
"offset_camera_mm": cfg.get("move_offset_camera_mm"),
|
||||
"axis_signs": cfg.get("axis_sign_cam_to_wrist"),
|
||||
"camera_offset_wrist_mm": cfg.get("camera_offset_wrist_mm"),
|
||||
"cam_to_wrist_perm": cfg.get("cam_to_wrist_perm"),
|
||||
"cluster_depth_window_m": cfg.get("cluster_depth_window_m"),
|
||||
"max_step_m": cfg.get("max_step_m"),
|
||||
"z_floor_m": cfg.get("z_floor_m"),
|
||||
@@ -211,6 +212,7 @@ def move_toward(req: MoveTowardReq) -> dict[str, Any]:
|
||||
off_mm = cfg.get("move_offset_camera_mm", {"x": 0, "y": 0, "z": 0})
|
||||
sign = cfg.get("axis_sign_cam_to_wrist", {"x": 1, "y": 1, "z": 1})
|
||||
cam_off_mm = cfg.get("camera_offset_wrist_mm", {"x": 0, "y": 0, "z": 0})
|
||||
perm_cfg = cfg.get("cam_to_wrist_perm", ["x", "y", "z"])
|
||||
max_step_m = float(cfg.get("max_step_m", 0.15))
|
||||
z_floor = cfg.get("z_floor_m")
|
||||
z_floor_m = float(z_floor) if z_floor is not None else None
|
||||
@@ -221,9 +223,16 @@ def move_toward(req: MoveTowardReq) -> dict[str, Any]:
|
||||
cam_offset_wrist_m = np.array(
|
||||
[cam_off_mm["x"], cam_off_mm["y"], cam_off_mm["z"]], dtype=np.float64) / 1000.0
|
||||
signs = (int(sign["x"]), int(sign["y"]), int(sign["z"]))
|
||||
_AXIS_IDX = {"x": 0, "y": 1, "z": 2}
|
||||
try:
|
||||
perm = tuple(_AXIS_IDX[str(p).lower()] for p in perm_cfg)
|
||||
except KeyError as e:
|
||||
raise HTTPException(status_code=422,
|
||||
detail=f"cam_to_wrist_perm: asse sconosciuto {e}")
|
||||
|
||||
try:
|
||||
delta = compute_delta_wrist(centroid, offset_m, signs,
|
||||
cam_to_wrist_perm=perm,
|
||||
camera_offset_wrist_m=cam_offset_wrist_m,
|
||||
max_step_m=max_step_m, force=req.force)
|
||||
except DeltaOversize as e:
|
||||
@@ -340,6 +349,67 @@ def put_custom_pose(req: PoseUpdateReq) -> dict[str, Any]:
|
||||
return {"custom_pose": _deps.tab_right_arm_cfg().get("custom_pose")}
|
||||
|
||||
|
||||
def _front_cam_marker_intrinsics() -> dict[str, Any]:
|
||||
"""Estrae le intrinsics RGB della front camera nel formato marker-finder
|
||||
(camera_matrix + dist_coeffs). marker-finder ha intrinsics 'baked-in'
|
||||
della D435i che non combaciano con la Nuwa: passarle per-call evita
|
||||
errori di scala sistematici sulla posa del tag."""
|
||||
intr = _deps.front_camera().intrinsics()
|
||||
rgb = (intr or {}).get("rgb") or {}
|
||||
K = [[float(rgb.get("fx", 0)), 0.0, float(rgb.get("cx", 0))],
|
||||
[0.0, float(rgb.get("fy", 0)), float(rgb.get("cy", 0))],
|
||||
[0.0, 0.0, 1.0]]
|
||||
dist = rgb.get("distortion") or {}
|
||||
d = [float(dist.get("k1", 0.0)), float(dist.get("k2", 0.0)),
|
||||
float(dist.get("p1", 0.0)), float(dist.get("p2", 0.0)),
|
||||
float(dist.get("k3", 0.0))]
|
||||
return {"camera_matrix": K, "dist_coeffs": d}
|
||||
|
||||
|
||||
@router.post("/verify_tag")
|
||||
def post_verify_tag() -> dict[str, Any]:
|
||||
"""Snapshot per verifica relativa: posa polso (G1 FK) + posa tag (marker
|
||||
finder sulla front camera). Da chiamare ripetutamente con il polso in
|
||||
posizioni diverse; il client confronta i moduli dei delta consecutivi.
|
||||
|
||||
Niente calibrazione assoluta richiesta: il tag e' rigidamente attaccato
|
||||
al polso, quindi ||Δtag|| dovrebbe approssimare ||Δwrist|| (a meno di
|
||||
qualche % di errore intrinsics)."""
|
||||
sw = Stopwatch()
|
||||
cfg = _deps.tab_right_arm_cfg()
|
||||
vm = cfg.get("verify_marker") or {"id": 1, "size_mm": 40.0}
|
||||
try:
|
||||
with sw.step("g1_wrist"):
|
||||
wrist = _deps.g1().wrist()["right_wrist"]
|
||||
except G1Unavailable as e:
|
||||
raise HTTPException(status_code=502, detail=f"G1 unavailable: {e}")
|
||||
try:
|
||||
with sw.step("front_intr"):
|
||||
intr = _front_cam_marker_intrinsics()
|
||||
with sw.step("marker_detect"):
|
||||
image_url = _deps.front_camera().rgb_url()
|
||||
res = _deps.marker().detect_url(
|
||||
markers=[{"id": int(vm["id"]), "size_mm": float(vm["size_mm"])}],
|
||||
image_url=image_url,
|
||||
intrinsics=intr,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=502, detail=f"marker-finder: {e}")
|
||||
found = (res or {}).get("found") or []
|
||||
if not found:
|
||||
raise HTTPException(status_code=404,
|
||||
detail=f"tag id={vm['id']} non rilevato dalla front camera")
|
||||
tag = found[0]
|
||||
return {
|
||||
"marker_id": int(vm["id"]),
|
||||
"wrist_position_m": list(wrist["position"]),
|
||||
"wrist_quaternion": list(wrist["quaternion"]),
|
||||
"tag_position_m_in_front_cam": list(tag["position_m"]),
|
||||
"tag_quaternion": list(tag.get("quaternion_xyzw") or []),
|
||||
"timings_ms": sw.as_dict(),
|
||||
}
|
||||
|
||||
|
||||
@router.put("/offset")
|
||||
def put_offset(req: OffsetUpdateReq) -> dict[str, Any]:
|
||||
patch: dict[str, Any] = {}
|
||||
|
||||
@@ -19,7 +19,7 @@ class AbsoluteReq(BaseModel):
|
||||
quaternion: list[float] = Field(..., min_length=4, max_length=4)
|
||||
wait: bool = True
|
||||
timeout_s: float = 5.0
|
||||
precise: bool = False
|
||||
precise: bool = True
|
||||
dry_run: bool = False
|
||||
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ class G1Client:
|
||||
# ----- mutating -----
|
||||
|
||||
def wrist_absolute(self, side: str, position: list[float], quaternion: list[float],
|
||||
wait: bool = True, timeout_s: float = 5.0, precise: bool = False) -> dict[str, Any]:
|
||||
wait: bool = True, timeout_s: float = 5.0, precise: bool = True) -> dict[str, Any]:
|
||||
body = {
|
||||
"side": side, "position": position, "quaternion": quaternion,
|
||||
"wait": wait, "timeout_s": timeout_s, "precise": precise,
|
||||
@@ -70,7 +70,7 @@ class G1Client:
|
||||
|
||||
def wrist_relative(self, side: str, delta_position: list[float] | None = None,
|
||||
delta_quaternion: list[float] | None = None, wait: bool = True,
|
||||
timeout_s: float = 5.0, precise: bool = False) -> dict[str, Any]:
|
||||
timeout_s: float = 5.0, precise: bool = True) -> dict[str, Any]:
|
||||
# `precise` is forwarded but the bridge silently ignores it on
|
||||
# /wrist/relative if not supported — safer to always send it.
|
||||
body: dict[str, Any] = {"side": side, "wait": wait, "timeout_s": timeout_s,
|
||||
|
||||
@@ -5,11 +5,23 @@ import numpy as np
|
||||
|
||||
|
||||
class DeltaOversize(ValueError):
|
||||
"""Sollevata quando ||delta|| supera max_step_m e force=False."""
|
||||
"""Sollevata quando ||delta|| supera max_step_m e force=False.
|
||||
|
||||
def __init__(self, delta_norm_m: float, max_step_m: float):
|
||||
super().__init__(f"delta {delta_norm_m*1000:.1f}mm > max {max_step_m*1000:.1f}mm")
|
||||
self.delta_norm_m = float(delta_norm_m)
|
||||
Espone la scomposizione X/Y/Z del delta richiesto sia come attributo
|
||||
`delta_m` sia nel messaggio, cosi' chi riceve l'errore (GUI o CLI) puo'
|
||||
vedere subito su quali assi sta lavorando senza dover rifare il calcolo
|
||||
con force=true."""
|
||||
|
||||
def __init__(self, delta_m: np.ndarray, max_step_m: float):
|
||||
delta = np.asarray(delta_m, dtype=np.float64).reshape(3)
|
||||
n = float(np.linalg.norm(delta))
|
||||
mm = delta * 1000.0
|
||||
super().__init__(
|
||||
f"delta {n*1000:.1f}mm > max {max_step_m*1000:.1f}mm "
|
||||
f"[X={mm[0]:.1f} Y={mm[1]:.1f} Z={mm[2]:.1f}]"
|
||||
)
|
||||
self.delta_m = delta
|
||||
self.delta_norm_m = n
|
||||
self.max_step_m = float(max_step_m)
|
||||
|
||||
|
||||
@@ -17,6 +29,7 @@ def compute_delta_wrist(
|
||||
centroid_camera_m: np.ndarray,
|
||||
offset_camera_m: np.ndarray,
|
||||
axis_signs: tuple[int, int, int],
|
||||
cam_to_wrist_perm: tuple[int, int, int] = (0, 1, 2),
|
||||
camera_offset_wrist_m: np.ndarray | None = None,
|
||||
max_step_m: float | None = None,
|
||||
force: bool = False,
|
||||
@@ -24,29 +37,36 @@ def compute_delta_wrist(
|
||||
"""Delta da applicare al polso (frame polso) per portare il TCP sull'offset
|
||||
di presa dall'oggetto osservato dalla camera.
|
||||
|
||||
Modello: assi camera ≈ assi polso a meno dei segni; la camera è montata sul
|
||||
polso con traslazione costante `camera_offset_wrist_m` (origine polso →
|
||||
origine camera, espressa nel frame polso).
|
||||
Modello: la camera e' montata sul polso con orientamento che e' una
|
||||
permutazione (+/-) degli assi del polso. `cam_to_wrist_perm = (i_x, i_y, i_z)`
|
||||
indica da quale componente del frame camera proviene ciascun asse polso:
|
||||
`wrist_X = sign_X * cam[i_x]`, etc. `axis_signs` applica il segno DOPO la
|
||||
permutazione. La traslazione costante `camera_offset_wrist_m` e' l'origine
|
||||
della camera nel frame polso.
|
||||
|
||||
delta_wrist = sign * (centroid_camera - offset_camera) + t_wrist_cam
|
||||
delta_wrist = sign * (centroid_cam[perm] - offset_cam[perm]) + t_wrist_cam
|
||||
|
||||
Il termine `+ t_wrist_cam` compensa il fatto che camera e TCP non coincidono:
|
||||
spostare il polso di `sign * (centroid - offset)` porterebbe la CAMERA sul
|
||||
punto desiderato, lasciando il TCP indietro di t_wrist_cam.
|
||||
Esempio (Nuwa eye-in-hand che guarda avanti come la mano, polso destro G1):
|
||||
cam_to_wrist_perm = (2, 0, 1) # wrist_X<-cam_Z, _Y<-cam_X, _Z<-cam_Y
|
||||
axis_signs = (+1, -1, -1)
|
||||
|
||||
Se `max_step_m` è settato e ||delta|| > max_step_m e `force` è False, solleva
|
||||
DeltaOversize.
|
||||
Default `perm = (0, 1, 2)`: nessuna permutazione, modello vecchio sign-only.
|
||||
Se `max_step_m` e' settato e ||delta|| > max_step_m e `force` e' False,
|
||||
solleva DeltaOversize.
|
||||
"""
|
||||
centroid = np.asarray(centroid_camera_m, dtype=np.float64).reshape(3)
|
||||
offset = np.asarray(offset_camera_m, dtype=np.float64).reshape(3)
|
||||
sign = np.asarray(axis_signs, dtype=np.float64).reshape(3)
|
||||
perm = np.asarray(cam_to_wrist_perm, dtype=np.intp).reshape(3)
|
||||
if not (np.all((perm >= 0) & (perm <= 2)) and len(set(perm.tolist())) == 3):
|
||||
raise ValueError(f"cam_to_wrist_perm must be a permutation of (0,1,2), got {perm.tolist()}")
|
||||
t_wc = (np.zeros(3, dtype=np.float64) if camera_offset_wrist_m is None
|
||||
else np.asarray(camera_offset_wrist_m, dtype=np.float64).reshape(3))
|
||||
delta = sign * (centroid - offset) + t_wc
|
||||
delta = sign * (centroid[perm] - offset[perm]) + t_wc
|
||||
if max_step_m is not None and not force:
|
||||
n = float(np.linalg.norm(delta))
|
||||
if n > max_step_m:
|
||||
raise DeltaOversize(n, max_step_m)
|
||||
raise DeltaOversize(delta, max_step_m)
|
||||
return delta
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user