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
+37
-12
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user