From 33d66019ee1205c1d5e852c8577f777a89800de2 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Wed, 13 May 2026 10:12:46 +0200 Subject: [PATCH] =?UTF-8?q?feat(web):=20Cerca=20modello=20=E2=80=94=20mode?= =?UTF-8?q?l=20picker=20+=20find=5Fmodel=20overlay=20handlers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- web/app.js | 206 +++++++++++++++++++++++++---------------------------- 1 file changed, 99 insertions(+), 107 deletions(-) diff --git a/web/app.js b/web/app.js index 1ac8ccc..24c42dd 100644 --- a/web/app.js +++ b/web/app.js @@ -114,14 +114,9 @@ const graspCanvas = $('graspCanvas'); const graspCtx = graspCanvas.getContext('2d'); let displayedGrasp = null; let graspRgbInFlight = false; -const cropCanvas = $('cropCanvas'); -const cropCtx = cropCanvas.getContext('2d'); -const scatterXY = $('scatterXY'); -const scatterXZ = $('scatterXZ'); +// (rimossi cropCanvas/scatter — ora c'è findResultCanvas) let lastDetections = []; // all detections, sorted by confidence desc let lastSelectedClass = null; // class name whose point cloud is displayed -let lastCropBbox = null; -let lastPoints = null; // array of [x,y,z] let knownClasses = []; // names discovered from /api/perception/classes // Stable per-class color palette (Okabe-Ito, colour-blind safe). @@ -161,60 +156,6 @@ function drawGraspFrame() { }); } -function drawCrop() { - cropCtx.fillStyle = '#000'; - cropCtx.fillRect(0, 0, cropCanvas.width, cropCanvas.height); - if (!lastCropBbox || !displayedGrasp) return; - const { x1, y1, x2, y2 } = lastCropBbox; - const sw = Math.max(1, x2 - x1), sh = Math.max(1, y2 - y1); - const scale = Math.min(cropCanvas.width / sw, cropCanvas.height / sh); - const dw = sw * scale, dh = sh * scale; - const dx = (cropCanvas.width - dw) / 2, dy = (cropCanvas.height - dh) / 2; - cropCtx.drawImage(displayedGrasp, x1, y1, sw, sh, dx, dy, dw, dh); - cropCtx.strokeStyle = '#4da3ff'; - cropCtx.lineWidth = 2; - cropCtx.strokeRect(dx, dy, dw, dh); -} - -function drawScatter(canvas, pts, axU, axV, label) { - const ctx = canvas.getContext('2d'); - ctx.fillStyle = '#0d1014'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - if (!pts || pts.length === 0) { - ctx.fillStyle = '#666'; - ctx.font = '12px sans-serif'; - ctx.fillText('(no points)', 10, 20); - return; - } - let uMin = Infinity, uMax = -Infinity, vMin = Infinity, vMax = -Infinity; - for (const p of pts) { - if (p[axU] < uMin) uMin = p[axU]; if (p[axU] > uMax) uMax = p[axU]; - if (p[axV] < vMin) vMin = p[axV]; if (p[axV] > vMax) vMax = p[axV]; - } - const pad = 20; - const du = Math.max(1e-6, uMax - uMin), dv = Math.max(1e-6, vMax - vMin); - const s = Math.min((canvas.width - 2*pad) / du, (canvas.height - 2*pad) / dv); - // axes - ctx.strokeStyle = '#2a2f37'; - ctx.strokeRect(pad, pad, canvas.width - 2*pad, canvas.height - 2*pad); - // points - ctx.fillStyle = '#3ecf8e'; - for (const p of pts) { - const x = pad + (p[axU] - uMin) * s; - const y = canvas.height - pad - (p[axV] - vMin) * s; - ctx.fillRect(x - 1, y - 1, 2, 2); - } - ctx.fillStyle = '#9aa0a6'; - ctx.font = '11px ui-monospace, monospace'; - ctx.fillText(label, pad + 2, 14); - ctx.fillText(`u:[${uMin.toFixed(2)},${uMax.toFixed(2)}] v:[${vMin.toFixed(2)},${vMax.toFixed(2)}]`, pad, canvas.height - 4); -} - -function drawScatters() { - drawScatter(scatterXY, lastPoints, 0, 1, 'Top-down (X→, Y↑)'); - drawScatter(scatterXZ, lastPoints, 0, 2, 'Laterale (X→, Z↑)'); -} - function refreshGraspRgb() { if (graspRgbInFlight) return; graspRgbInFlight = true; @@ -223,7 +164,6 @@ function refreshGraspRgb() { displayedGrasp = img; graspRgbInFlight = false; drawGraspFrame(); - drawCrop(); }; img.onerror = () => { graspRgbInFlight = false; }; img.src = `/api/calibration/rgb.jpg?t=${Date.now()}`; @@ -266,11 +206,9 @@ function applyScanResponse(r) { lastDetections = Array.isArray(r.detections) ? r.detections : []; const sel = r.selected || null; lastSelectedClass = sel ? sel.class : null; - lastCropBbox = sel ? sel.bbox : null; - lastPoints = r.points_base || r.points_camera || null; refreshObjectPickOptions(lastDetections); renderGraspLegend(lastDetections); - drawGraspFrame(); drawCrop(); drawScatters(); + drawGraspFrame(); } $('detectBtn').addEventListener('click', async () => { @@ -295,27 +233,6 @@ $('detectBtn').addEventListener('click', async () => { } catch (e) { setStatus(e.message, 'err'); } }); -$('planBtn').addEventListener('click', async () => { - try { - setStatus('Pianifico…'); - const body = { conf: +$('graspConf').value || null, class_name: pickedClass() }; - const r = await api('POST', '/api/grasp/plan', body); - if (r.detection) { - // keep the multi-class view, but highlight the planned class - lastSelectedClass = r.detection.class || lastSelectedClass; - lastCropBbox = r.detection.bbox; - if (!lastDetections.some(d => d.class === r.detection.class)) { - lastDetections = [r.detection, ...lastDetections]; - } - } - $('graspOut').textContent = JSON.stringify(r, null, 2); - renderGraspLegend(lastDetections); - drawGraspFrame(); drawCrop(); - const src = r.source ? `source=${r.source}${r.sam21_fallback_reason ? ` (${r.sam21_fallback_reason})` : ''} · ` : ''; - setStatus(`Plan ok · ${src}n=${r.n_points ?? '?'} · ${fmtTimings(r.timings_ms)}`, 'ok'); - } catch (e) { setStatus(e.message, 'err'); } -}); - // --- continuous detection loop --- const STATS_WINDOW = 5; const MISS_LIMIT = 5; // consecutive frames without detection before clearing the view @@ -327,12 +244,8 @@ let missStreak = 0; function clearDetectionState(reason) { lastDetections = []; lastSelectedClass = null; - lastCropBbox = null; - lastPoints = null; recentCentroids = []; drawGraspFrame(); // redraws RGB without bbox overlay - drawCrop(); // wipes crop thumbnail - drawScatters(); // wipes scatter plots updateStats(); // resets table to dashes refreshObjectPickOptions([]); renderGraspLegend([]); @@ -385,11 +298,9 @@ async function loopTick() { const frame = r.points_base ? 'base' : 'camera'; lastDetections = Array.isArray(r.detections) ? r.detections : []; lastSelectedClass = r.selected ? r.selected.class : null; - lastCropBbox = r.selected ? r.selected.bbox : null; - lastPoints = pts; refreshObjectPickOptions(lastDetections); renderGraspLegend(lastDetections); - drawGraspFrame(); drawCrop(); drawScatters(); + drawGraspFrame(); const c = centroidOf(pts); if (c) { recentCentroids.push(c); @@ -467,21 +378,6 @@ document.querySelectorAll('.tab').forEach(btn => btn.addEventListener('click', ( if (btn.dataset.tab !== 'grasp' && loopTimer) stopLoop(); })); -$('executeBtn').addEventListener('click', async () => { - if (!$('graspDryRun').checked && !confirm('Eseguire realmente la presa sul robot?')) return; - try { - setStatus('Esecuzione…'); - const body = { - conf: +$('graspConf').value || null, - class_name: pickedClass(), - dry_run: $('graspDryRun').checked, - }; - const r = await api('POST', '/api/grasp/execute', body); - $('graspOut').textContent = JSON.stringify(r, null, 2); - setStatus(`Grasp eseguito · ${fmtTimings(r.timings_ms)}`, 'ok'); - } catch (e) { setStatus(e.message, 'err'); } -}); - // --- object pick select + legend + class discovery --- // Build per-class summary: best (highest-confidence) detection and instance count. @@ -575,6 +471,102 @@ $('graspObjectPick').addEventListener('change', () => { if (!loopTimer) $('detectBtn').click(); }); +// ==================== FIT MODELLO ==================== +const findCanvas = $('findResultCanvas'); +const findCtx = findCanvas.getContext('2d'); +let findResult = null; +let findRgbImg = null; + +async function loadRecipes() { + const sel = $('findModelPick'); + try { + const r = await api('GET', '/api/models/recipes'); + sel.innerHTML = ''; + (r.recipes || []).forEach(rec => { + const opt = document.createElement('option'); + opt.value = rec.id; + opt.textContent = `${rec.name} (${rec.symmetry_type})`; + sel.appendChild(opt); + }); + sel.disabled = (r.recipes || []).length === 0; + $('findBtn').disabled = !sel.value; + } catch (e) { + sel.innerHTML = ``; + } +} + +$('findModelPick').addEventListener('change', () => { + $('findBtn').disabled = !$('findModelPick').value; +}); + +$('findRecipesRefreshBtn').addEventListener('click', loadRecipes); + +$('findBtn').addEventListener('click', async () => { + const recipeId = $('findModelPick').value; + if (!recipeId) return; + $('findBtn').disabled = true; + $('findStatus').textContent = 'Cerco posa…'; + $('findStatus').className = 'status-line'; + try { + const body = { + recipe_id: recipeId, + conf: +$('graspConf').value || null, + class_name: pickedClass(), + }; + const r = await api('POST', '/api/grasp/find_model', body); + findResult = r; + findRgbImg = new Image(); + findRgbImg.onload = drawFindResult; + findRgbImg.src = 'data:image/jpeg;base64,' + r.rgb_jpeg_b64; + const fit = r.fit_ok + ? `OK · fitness=${(r.fitness ?? 0).toFixed(2)} · rmse=${(r.inlier_rmse_mm ?? 0).toFixed(2)}mm` + : `Fit fallito: ${r.fit_reason}`; + $('findStatus').textContent = `${fit} · ${fmtTimings(r.timings_ms)}`; + $('findStatus').className = 'status-line ' + (r.fit_ok ? 'ok' : 'warn'); + $('findDetails').textContent = JSON.stringify({ + detection: r.detection, + recipe: r.recipe, + n_accepted: r.n_accepted, + n_excluded: r.n_excluded, + fitness: r.fitness, + rmse_mm: r.inlier_rmse_mm, + timings: r.timings_ms, + }, null, 2); + } catch (e) { + $('findStatus').textContent = `errore: ${e.message}`; + $('findStatus').className = 'status-line err'; + } finally { + $('findBtn').disabled = !$('findModelPick').value; + } +}); + +['findShowModel', 'findShowAccepted', 'findShowExcluded'].forEach(id => + $(id).addEventListener('change', drawFindResult)); + +function drawFindResult() { + if (!findResult || !findRgbImg || !findRgbImg.complete) return; + const [W, H] = findResult.rgb_size; + if (findCanvas.width !== W) { findCanvas.width = W; findCanvas.height = H; } + findCtx.drawImage(findRgbImg, 0, 0); + const dot = (pts, color, r) => { + findCtx.fillStyle = color; + for (const [u, v] of pts) findCtx.fillRect(u - r, v - r, r*2+1, r*2+1); + }; + if ($('findShowExcluded').checked) dot(findResult.excluded_points_px, '#e74c3c', 1); + if ($('findShowAccepted').checked) dot(findResult.accepted_points_px, '#3ecf8e', 1); + if ($('findShowModel').checked) dot(findResult.model_points_px, '#4da3ff', 1); +} + +function loadRecipesIfNeeded() { + if ($('tab-grasp').classList.contains('active') && !$('findModelPick').dataset.loaded) { + loadRecipes(); + $('findModelPick').dataset.loaded = '1'; + } +} +document.querySelectorAll('.tab').forEach(t => + t.addEventListener('click', () => setTimeout(loadRecipesIfNeeded, 50))); +loadRecipesIfNeeded(); + // ==================== EE ==================== async function refreshWrist() { try {