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:
1 parent
af8d4d6fc4
commit
33d66019ee
1 file changed
+99
-107
+99
-107
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user