feat(web): Cerca modello — model picker + find_model overlay handlers

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-13 10:12:46 +02:00
1 parent af8d4d6fc4
commit 33d66019ee
1 file changed
+99 -107
+99 -107
View File
@@ -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 = '<option value="">— seleziona modello —</option>';
(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 = `<option value="">errore: ${e.message}</option>`;
}
}
$('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 {