From edc4430c67d16ab4826fb50aca7f39c20d26e860 Mon Sep 17 00:00:00 2001 From: Adriano Date: Wed, 3 Jun 2026 12:31:16 +0200 Subject: [PATCH] feat(pose_3d): overlay tag su entrambe le camere (Nuwa rosso, D435i ciano) Co-Authored-By: Claude Sonnet 4.6 --- web/pose_3d.js | 73 +++++++++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 40 deletions(-) diff --git a/web/pose_3d.js b/web/pose_3d.js index 06f8d0e..1e40d14 100644 --- a/web/pose_3d.js +++ b/web/pose_3d.js @@ -227,8 +227,9 @@ const state = { position_m: null, // [x,y,z] in pelvis frame della crocetta corrente joints_at_place: null, // snapshot giunti al click (può essere null se G1 offline) }, - front_tags: { // overlay tag visibili dalla camera D435i frontale - group: null, // THREE.Group child del d435 parent (cloud-like transform) + tags: { // overlay tag per camera (gruppi THREE child del cloud node) + nuwa: { group: null }, + d435: { group: null }, }, }; @@ -1767,7 +1768,7 @@ async function acquireClouds() { saveCloudParamsToConfig(); // Nuova immagine acquisita: se l'overlay tag e' attivo ri-esegui la // detection una volta sola, sul frame appena acquisito. - if ($('p3ShowFrontTags')?.checked) pollFrontTags(); + if ($('p3ShowFrontTags')?.checked) pollTags(); } catch (e) { $('p3CloudStatus').textContent = `errore: ${e.message}`; $('p3CloudStatus').style.color = 'var(--danger)'; @@ -1795,6 +1796,8 @@ function clearClouds() { // camera in modalita' URDF link e in modalita' hand-eye. const FRONT_TAG_COLOR = 0x4dd0e1; +const NUWA_TAG_COLOR = 0xff3b30; // rosso: tag visti dalla camera Nuwa (polso) +const TAG_COLORS = { nuwa: NUWA_TAG_COLOR, d435: FRONT_TAG_COLOR }; const FRONT_TAG_LABEL_COLOR = '#4dd0e1'; // Label compatta auto-sized: canvas largo quanto il testo, font piccolo, @@ -1854,8 +1857,8 @@ function applyCloudLikeTransform(obj, camKey) { obj.scale.set(sv[0], sv[1], sv[2]); } -function clearFrontTagsGroup() { - const grp = state.front_tags.group; +function clearTagsGroup(camKey) { + const grp = state.tags[camKey].group; if (!grp) return; if (grp.parent) grp.parent.remove(grp); grp.traverse(o => { @@ -1865,64 +1868,54 @@ function clearFrontTagsGroup() { else o.material.dispose(); } }); - state.front_tags.group = null; + state.tags[camKey].group = null; } -function addOneFrontTag(grp, t) { +function addOneTag(grp, t, color) { const c = t.corners_3d_optical_m; if (!Array.isArray(c) || c.length !== 4) return; if (c.some(p => !p || p.some(v => !Number.isFinite(v)))) return; - - // LineLoop (5 verts = close) const v = new Float32Array(5 * 3); for (let i = 0; i < 4; i++) { v[3*i]=c[i][0]; v[3*i+1]=c[i][1]; v[3*i+2]=c[i][2]; } v[12] = c[0][0]; v[13] = c[0][1]; v[14] = c[0][2]; const geom = new THREE.BufferGeometry(); geom.setAttribute('position', new THREE.BufferAttribute(v, 3)); - const mat = new THREE.LineBasicMaterial({ color: FRONT_TAG_COLOR }); - grp.add(new THREE.Line(geom, mat)); - - // Label "id=X · NNmm · d=DDDmm" leggermente offset dal centro + grp.add(new THREE.Line(geom, new THREE.LineBasicMaterial({ color }))); const edge_s = Number.isFinite(t.edge_mm) ? `${t.edge_mm.toFixed(1)}mm` : '?'; const dist_s = Number.isFinite(t.distance_m) ? `${(t.distance_m * 1000).toFixed(0)}mm` : '?'; const sprite = makeCompactLabel(`id=${t.id} · ${edge_s} · d=${dist_s}`); const ctr = t.center_3d_optical_m; - if (ctr && ctr.every(Number.isFinite)) { - // offset in -Y ottico = sopra (Y down nella convenzione OpenCV) - sprite.position.set(ctr[0], ctr[1] - 0.04, ctr[2]); - } + if (ctr && ctr.every(Number.isFinite)) sprite.position.set(ctr[0], ctr[1] - 0.04, ctr[2]); grp.add(sprite); } -function renderFrontTags(tags) { - clearFrontTagsGroup(); +function renderTags(camKey, tags, color) { + clearTagsGroup(camKey); if (!tags || tags.length === 0) return; - const parent = getCloudParent('d435'); - if (!parent) { - console.warn('[pose_3d] front_tags: parent d435 non disponibile'); - return; - } + const parent = getCloudParent(camKey); + if (!parent) { console.warn(`[pose_3d] tags: parent ${camKey} non disponibile`); return; } const grp = new THREE.Group(); - grp.name = 'front_tags_group'; - applyCloudLikeTransform(grp, 'd435'); + grp.name = `${camKey}_tags_group`; + applyCloudLikeTransform(grp, camKey); parent.add(grp); - state.front_tags.group = grp; - for (const t of tags) addOneFrontTag(grp, t); + state.tags[camKey].group = grp; + for (const t of tags) addOneTag(grp, t, color); } -async function pollFrontTags() { +async function pollTags() { const ids = ($('p3FrontTagIds')?.value || '0-49').trim(); const status = $('p3FrontTagStatus'); try { - const r = await jsonFetch(`/api/pose_3d/front_tags?ids=${encodeURIComponent(ids)}`); - const tags = r.tags || []; - renderFrontTags(tags); - if (status) { - const ms = r.timings_ms?.total; - status.textContent = tags.length - ? `${tags.length} tag · ids: ${tags.map(t => t.id).join(',')}${ms != null ? ' · ' + ms.toFixed(0) + 'ms' : ''}` - : 'nessun tag visibile'; + const r = await jsonFetch(`/api/pose_3d/tags?ids=${encodeURIComponent(ids)}`); + const cams = r.cameras || {}; + const parts = []; + for (const camKey of ['nuwa', 'd435']) { + const c = cams[camKey] || {}; + renderTags(camKey, c.tags || [], TAG_COLORS[camKey]); + if (c.error) parts.push(`${camKey}: err`); + else parts.push(`${camKey}: ${(c.tags || []).length}`); } + if (status) status.textContent = parts.join(' · '); } catch (e) { if (status) status.textContent = `errore: ${e?.message || e}`; } @@ -1933,10 +1926,10 @@ function bindFrontTagFinder() { if (!cb) return; cb.addEventListener('change', () => { if (cb.checked) { - // Refresh immediato senza aspettare il prossimo tick di pollState - pollFrontTags(); + pollTags(); } else { - clearFrontTagsGroup(); + clearTagsGroup('nuwa'); + clearTagsGroup('d435'); const s = $('p3FrontTagStatus'); if (s) s.textContent = '—'; }