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:
Adriano Dal PastroandClaude Opus 4.7 committed 2026-05-21 12:18:24 +02:00
1 parent fcd921ff32
commit 1fa7d12ad7
8 files changed
+260 -20

No files matched your search

+37 -12
View File
@@ -162,16 +162,9 @@ async function refreshPose() {
// Aggiorna anche i campi delle pose (solo se non sono stati toccati)
if (s.home_pose) fillPoseFields('raHome', s.home_pose);
if (s.custom_pose) fillPoseFields('raCust', s.custom_pose);
if (s.offset_camera_mm) {
$('raOffX').value = s.offset_camera_mm.x;
$('raOffY').value = s.offset_camera_mm.y;
$('raOffZ').value = s.offset_camera_mm.z;
}
if (s.axis_signs) {
$('raSgnX').value = s.axis_signs.x;
$('raSgnY').value = s.axis_signs.y;
$('raSgnZ').value = s.axis_signs.z;
}
if (s.offset_camera_mm) fillIfUntouched('raOff', s.offset_camera_mm);
if (s.axis_signs) fillIfUntouched('raSgn', s.axis_signs);
if (s.camera_offset_wrist_mm) fillIfUntouched('raCamOff', s.camera_offset_wrist_mm);
} catch (e) {
// silenzioso: il polso può non essere disponibile
}
@@ -188,6 +181,21 @@ function fillPoseFields(prefix, pose) {
$(prefix + 'W').value = pose.rpy_deg[2];
}
function fillIfUntouched(prefix, xyz) {
for (const k of ['X','Y','Z']) {
const el = $(prefix + k);
if (!el || el.dataset.userTouched) continue;
el.value = xyz[k.toLowerCase()];
}
}
function clearUserTouched(prefix) {
for (const k of ['X','Y','Z','R','P','W']) {
const el = $(prefix + k);
if (el) delete el.dataset.userTouched;
}
}
function readPoseFields(prefix) {
return {
position: [
@@ -220,8 +228,16 @@ async function onSaveOffset() {
y: parseInt($('raSgnY').value, 10),
z: parseInt($('raSgnZ').value, 10),
},
camera_offset_wrist_mm: {
x: parseFloat($('raCamOffX').value),
y: parseFloat($('raCamOffY').value),
z: parseFloat($('raCamOffZ').value),
},
}),
});
clearUserTouched('raOff');
clearUserTouched('raSgn');
clearUserTouched('raCamOff');
alert('Offset salvato.');
} catch (e) {
alert(`Errore salvataggio: ${e.message}`);
@@ -234,11 +250,17 @@ async function onMove() {
const className = $('raWristClass').value;
const conf = parseFloat($('raWristConf').value) || null;
const dryRun = $('raMoveDryRun').checked;
if (!dryRun && !confirm('Dry-run OFF: eseguire move REALE verso il pezzo?')) return;
const force = $('raMoveForce').checked;
if (!dryRun) {
const msg = force
? 'Dry-run OFF + force ON (max_step bypassato): eseguire move REALE verso il pezzo?'
: 'Dry-run OFF: eseguire move REALE verso il pezzo?';
if (!confirm(msg)) return;
}
try {
const res = await jsonFetch('/api/right_arm/move/toward', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ class_name: className, conf, dry_run: dryRun }),
body: JSON.stringify({ class_name: className, conf, dry_run: dryRun, force }),
});
const d = res.delta_wrist_mm;
$('raMoveResult').classList.remove('dim');
@@ -335,6 +357,9 @@ function init() {
$('raWristAutoRefresh').addEventListener('change', startWristLoop);
markUserTouchedOnInput('raHome');
markUserTouchedOnInput('raCust');
markUserTouchedOnInput('raOff');
markUserTouchedOnInput('raSgn');
markUserTouchedOnInput('raCamOff');
populateClassDropdowns();
startFrontLoop();