feat(right_arm): force, camera↔polso offset, cluster filter sul centroide
Tre miglioramenti al move-toward dopo il primo uso live:
1. checkbox "force" accanto a "Muovi": bypassa max_step_m quando
l'utente sa che il delta richiesto e' grande ma corretto (es. primo
avvicinamento all'oggetto).
2. camera_offset_wrist_mm: il modello assumeva camera e TCP coincidenti
(sole sign flips). La camera Nuwa e' invece montata con traslazione
costante rispetto al polso. La formula diventa:
delta_wrist = sign * (centroid_cam - offset_cam) + t_wrist_cam
Esposto in config (default 0,0,0), GUI (3 input "Camera rispetto a
polso (mm)") e in /state. Il termine compensa il fatto che muovere
il polso porta la camera, non il TCP, sul punto di presa.
3. filter_nearest_cluster: il centroide era media di tutti i punti
SAM21 della mask. In presenza di sfondo lontano visibile attraverso
buchi della depth del primo piano la media veniva trascinata a
metri di distanza. Ora si tiene il cluster piu' vicino:
z_near = p5(Z)
keep = punti con Z <= z_near + cluster_depth_window_m
Default window 0.20 m, configurabile. La risposta di /detect/wrist
espone n_sam21_points, n_centroid_points e z_near/z_cutoff per
diagnosi.
Aggiornata anche la persistenza: /offset accetta camera_offset_wrist_mm.
GUI: i campi Offset/Sign/CamOff rispettano "user touched" (refresh non
li cancella mentre l'utente digita).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
fcd921ff32
commit
1fa7d12ad7
8 files changed
+260
-20
No files matched your search
@@ -15,6 +15,7 @@ from ..config import update_config
|
||||
from ..geometry import quaternion_xyzw_to_rpy_deg, rpy_deg_to_quaternion_xyzw
|
||||
from ..perception.detector import _normalize as _normalize_yolo
|
||||
from ..perception.sam21_bridge import Sam21Fallback, segment_with_sam21
|
||||
from ..right_arm.cluster import filter_nearest_cluster
|
||||
from ..right_arm.move import DeltaOversize, apply_z_floor, compute_delta_wrist
|
||||
from ..timing import Stopwatch
|
||||
from . import _deps
|
||||
@@ -40,6 +41,8 @@ def _state_payload(sw: Stopwatch) -> dict[str, Any]:
|
||||
"custom_pose": cfg.get("custom_pose"),
|
||||
"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"),
|
||||
"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"),
|
||||
"timings_ms": sw.as_dict(),
|
||||
@@ -158,9 +161,18 @@ def _detect_wrist_core(class_name: str | None, conf: float | None, sw: Stopwatch
|
||||
seg = segment_with_sam21(nuwa, _deps.sam21(), bbox, sw)
|
||||
except Sam21Fallback as e:
|
||||
raise HTTPException(status_code=422, detail=f"SAM21 fallback: {e}")
|
||||
pts = np.asarray(seg.points_cv_m, dtype=np.float64)
|
||||
pts_all = np.asarray(seg.points_cv_m, dtype=np.float64)
|
||||
if pts_all.shape[0] < 10:
|
||||
raise HTTPException(status_code=422, detail=f"too few sam21 points: {pts_all.shape[0]}")
|
||||
cfg = _deps.tab_right_arm_cfg()
|
||||
window_m = float(cfg.get("cluster_depth_window_m", 0.20))
|
||||
with sw.step("cluster_filter"):
|
||||
pts, cluster_diag = filter_nearest_cluster(pts_all, window_m=window_m)
|
||||
if pts.shape[0] < 10:
|
||||
raise HTTPException(status_code=422, detail=f"too few sam21 points: {pts.shape[0]}")
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=f"cluster troppo piccolo: {pts.shape[0]} punti dopo filtro "
|
||||
f"(z_near={cluster_diag['z_near_m']:.3f}m window={window_m:.3f}m)")
|
||||
with sw.step("centroid"):
|
||||
centroid = pts.mean(axis=0)
|
||||
distance = float(np.linalg.norm(centroid))
|
||||
@@ -169,7 +181,11 @@ def _detect_wrist_core(class_name: str | None, conf: float | None, sw: Stopwatch
|
||||
"bbox": list(bbox),
|
||||
"centroid_camera_m": [float(centroid[0]), float(centroid[1]), float(centroid[2])],
|
||||
"distance_m": distance,
|
||||
"n_sam21_points": int(pts.shape[0]),
|
||||
"n_sam21_points": int(pts_all.shape[0]),
|
||||
"n_centroid_points": int(pts.shape[0]),
|
||||
"cluster_window_m": window_m,
|
||||
"cluster_z_near_m": cluster_diag["z_near_m"],
|
||||
"cluster_z_cutoff_m": cluster_diag["z_cutoff_m"],
|
||||
}
|
||||
|
||||
|
||||
@@ -194,6 +210,7 @@ def move_toward(req: MoveTowardReq) -> dict[str, Any]:
|
||||
cfg = _deps.tab_right_arm_cfg()
|
||||
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})
|
||||
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
|
||||
@@ -201,10 +218,13 @@ def move_toward(req: MoveTowardReq) -> dict[str, Any]:
|
||||
det = _detect_wrist_core(req.class_name, req.conf, sw)
|
||||
centroid = np.asarray(det["centroid_camera_m"], dtype=np.float64)
|
||||
offset_m = np.array([off_mm["x"], off_mm["y"], off_mm["z"]], dtype=np.float64) / 1000.0
|
||||
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"]))
|
||||
|
||||
try:
|
||||
delta = compute_delta_wrist(centroid, offset_m, signs,
|
||||
camera_offset_wrist_m=cam_offset_wrist_m,
|
||||
max_step_m=max_step_m, force=req.force)
|
||||
except DeltaOversize as e:
|
||||
raise HTTPException(status_code=422, detail=str(e))
|
||||
@@ -246,6 +266,7 @@ def move_toward(req: MoveTowardReq) -> dict[str, Any]:
|
||||
"distance_m": det["distance_m"],
|
||||
"offset_camera_m": offset_m.tolist(),
|
||||
"axis_signs": list(signs),
|
||||
"camera_offset_wrist_m": cam_offset_wrist_m.tolist(),
|
||||
"delta_wrist_m": delta.tolist(),
|
||||
"delta_wrist_mm": (delta * 1000.0).tolist(),
|
||||
"z_clipped": bool(z_clipped),
|
||||
@@ -269,6 +290,7 @@ class PoseUpdateReq(BaseModel):
|
||||
class OffsetUpdateReq(BaseModel):
|
||||
move_offset_camera_mm: Optional[dict[str, float]] = None
|
||||
axis_sign_cam_to_wrist: Optional[dict[str, int]] = None
|
||||
camera_offset_wrist_mm: Optional[dict[str, float]] = None
|
||||
|
||||
|
||||
def _exec_named_pose(pose_key: str, dry_run: bool, sw: Stopwatch) -> dict[str, Any]:
|
||||
@@ -332,9 +354,15 @@ def put_offset(req: OffsetUpdateReq) -> dict[str, Any]:
|
||||
if v not in (-1, 1):
|
||||
raise HTTPException(status_code=422, detail=f"axis_sign_cam_to_wrist['{k}'] must be +/-1")
|
||||
patch["axis_sign_cam_to_wrist"] = {k: int(req.axis_sign_cam_to_wrist[k]) for k in ("x", "y", "z")}
|
||||
if req.camera_offset_wrist_mm is not None:
|
||||
for k in ("x", "y", "z"):
|
||||
if k not in req.camera_offset_wrist_mm:
|
||||
raise HTTPException(status_code=422, detail=f"camera_offset_wrist_mm missing '{k}'")
|
||||
patch["camera_offset_wrist_mm"] = {k: float(req.camera_offset_wrist_mm[k]) for k in ("x", "y", "z")}
|
||||
if not patch:
|
||||
raise HTTPException(status_code=422, detail="nothing to update")
|
||||
update_config({"tab_right_arm": patch})
|
||||
cfg = _deps.tab_right_arm_cfg()
|
||||
return {"move_offset_camera_mm": cfg.get("move_offset_camera_mm"),
|
||||
"axis_sign_cam_to_wrist": cfg.get("axis_sign_cam_to_wrist")}
|
||||
"axis_sign_cam_to_wrist": cfg.get("axis_sign_cam_to_wrist"),
|
||||
"camera_offset_wrist_mm": cfg.get("camera_offset_wrist_mm")}
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Filtri di pulizia della nuvola SAM21 prima del centroide."""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def filter_nearest_cluster(
|
||||
points_xyz_m: np.ndarray,
|
||||
window_m: float,
|
||||
z_near_percentile: float = 5.0,
|
||||
) -> tuple[np.ndarray, dict]:
|
||||
"""Tieni solo i punti del cluster più vicino alla camera lungo Z.
|
||||
|
||||
Assume frame OpenCV (Z>0 davanti). `z_near` = percentile robusto di Z
|
||||
(default p5) — proxy per "il valore Z più vicino" resistente a qualche
|
||||
outlier negativo. Si tengono i punti con `Z <= z_near + window_m`.
|
||||
|
||||
Utile contro nuvole bimodali tipo oggetto-vicino + background-lontano,
|
||||
dove la media e' dominata dal background. Niente magic-number su Z max
|
||||
assoluto: la soglia e' relativa al punto piu' vicino.
|
||||
|
||||
Ritorna (filtered, diag) con diag = {z_near_m, z_cutoff_m,
|
||||
n_input, n_kept}.
|
||||
"""
|
||||
pts = np.asarray(points_xyz_m, dtype=np.float64)
|
||||
n_in = int(pts.shape[0])
|
||||
if n_in == 0:
|
||||
return pts, {"z_near_m": None, "z_cutoff_m": None,
|
||||
"n_input": 0, "n_kept": 0}
|
||||
z = pts[:, 2]
|
||||
z_near = float(np.percentile(z, float(z_near_percentile)))
|
||||
z_cut = z_near + float(window_m)
|
||||
mask = z <= z_cut
|
||||
return pts[mask], {
|
||||
"z_near_m": z_near,
|
||||
"z_cutoff_m": z_cut,
|
||||
"n_input": n_in,
|
||||
"n_kept": int(mask.sum()),
|
||||
}
|
||||
@@ -17,13 +17,22 @@ def compute_delta_wrist(
|
||||
centroid_camera_m: np.ndarray,
|
||||
offset_camera_m: np.ndarray,
|
||||
axis_signs: tuple[int, int, int],
|
||||
camera_offset_wrist_m: np.ndarray | None = None,
|
||||
max_step_m: float | None = None,
|
||||
force: bool = False,
|
||||
) -> np.ndarray:
|
||||
"""Delta da applicare al polso (frame polso) per portare la camera sull'offset.
|
||||
"""Delta da applicare al polso (frame polso) per portare il TCP sull'offset
|
||||
di presa dall'oggetto osservato dalla camera.
|
||||
|
||||
Modello: si assume assi camera ≈ assi polso a meno dei segni.
|
||||
delta_wrist = sign * (centroid_camera - offset_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).
|
||||
|
||||
delta_wrist = sign * (centroid_camera - offset_camera) + 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.
|
||||
|
||||
Se `max_step_m` è settato e ||delta|| > max_step_m e `force` è False, solleva
|
||||
DeltaOversize.
|
||||
@@ -31,7 +40,9 @@ def compute_delta_wrist(
|
||||
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)
|
||||
delta = sign * (centroid - offset)
|
||||
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
|
||||
if max_step_m is not None and not force:
|
||||
n = float(np.linalg.norm(delta))
|
||||
if n > max_step_m:
|
||||
|
||||
Reference in new issue
Block a user