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');
|
const graspCtx = graspCanvas.getContext('2d');
|
||||||
let displayedGrasp = null;
|
let displayedGrasp = null;
|
||||||
let graspRgbInFlight = false;
|
let graspRgbInFlight = false;
|
||||||
const cropCanvas = $('cropCanvas');
|
// (rimossi cropCanvas/scatter — ora c'è findResultCanvas)
|
||||||
const cropCtx = cropCanvas.getContext('2d');
|
|
||||||
const scatterXY = $('scatterXY');
|
|
||||||
const scatterXZ = $('scatterXZ');
|
|
||||||
let lastDetections = []; // all detections, sorted by confidence desc
|
let lastDetections = []; // all detections, sorted by confidence desc
|
||||||
let lastSelectedClass = null; // class name whose point cloud is displayed
|
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
|
let knownClasses = []; // names discovered from /api/perception/classes
|
||||||
|
|
||||||
// Stable per-class color palette (Okabe-Ito, colour-blind safe).
|
// 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() {
|
function refreshGraspRgb() {
|
||||||
if (graspRgbInFlight) return;
|
if (graspRgbInFlight) return;
|
||||||
graspRgbInFlight = true;
|
graspRgbInFlight = true;
|
||||||
@@ -223,7 +164,6 @@ function refreshGraspRgb() {
|
|||||||
displayedGrasp = img;
|
displayedGrasp = img;
|
||||||
graspRgbInFlight = false;
|
graspRgbInFlight = false;
|
||||||
drawGraspFrame();
|
drawGraspFrame();
|
||||||
drawCrop();
|
|
||||||
};
|
};
|
||||||
img.onerror = () => { graspRgbInFlight = false; };
|
img.onerror = () => { graspRgbInFlight = false; };
|
||||||
img.src = `/api/calibration/rgb.jpg?t=${Date.now()}`;
|
img.src = `/api/calibration/rgb.jpg?t=${Date.now()}`;
|
||||||
@@ -266,11 +206,9 @@ function applyScanResponse(r) {
|
|||||||
lastDetections = Array.isArray(r.detections) ? r.detections : [];
|
lastDetections = Array.isArray(r.detections) ? r.detections : [];
|
||||||
const sel = r.selected || null;
|
const sel = r.selected || null;
|
||||||
lastSelectedClass = sel ? sel.class : null;
|
lastSelectedClass = sel ? sel.class : null;
|
||||||
lastCropBbox = sel ? sel.bbox : null;
|
|
||||||
lastPoints = r.points_base || r.points_camera || null;
|
|
||||||
refreshObjectPickOptions(lastDetections);
|
refreshObjectPickOptions(lastDetections);
|
||||||
renderGraspLegend(lastDetections);
|
renderGraspLegend(lastDetections);
|
||||||
drawGraspFrame(); drawCrop(); drawScatters();
|
drawGraspFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
$('detectBtn').addEventListener('click', async () => {
|
$('detectBtn').addEventListener('click', async () => {
|
||||||
@@ -295,27 +233,6 @@ $('detectBtn').addEventListener('click', async () => {
|
|||||||
} catch (e) { setStatus(e.message, 'err'); }
|
} 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 ---
|
// --- continuous detection loop ---
|
||||||
const STATS_WINDOW = 5;
|
const STATS_WINDOW = 5;
|
||||||
const MISS_LIMIT = 5; // consecutive frames without detection before clearing the view
|
const MISS_LIMIT = 5; // consecutive frames without detection before clearing the view
|
||||||
@@ -327,12 +244,8 @@ let missStreak = 0;
|
|||||||
function clearDetectionState(reason) {
|
function clearDetectionState(reason) {
|
||||||
lastDetections = [];
|
lastDetections = [];
|
||||||
lastSelectedClass = null;
|
lastSelectedClass = null;
|
||||||
lastCropBbox = null;
|
|
||||||
lastPoints = null;
|
|
||||||
recentCentroids = [];
|
recentCentroids = [];
|
||||||
drawGraspFrame(); // redraws RGB without bbox overlay
|
drawGraspFrame(); // redraws RGB without bbox overlay
|
||||||
drawCrop(); // wipes crop thumbnail
|
|
||||||
drawScatters(); // wipes scatter plots
|
|
||||||
updateStats(); // resets table to dashes
|
updateStats(); // resets table to dashes
|
||||||
refreshObjectPickOptions([]);
|
refreshObjectPickOptions([]);
|
||||||
renderGraspLegend([]);
|
renderGraspLegend([]);
|
||||||
@@ -385,11 +298,9 @@ async function loopTick() {
|
|||||||
const frame = r.points_base ? 'base' : 'camera';
|
const frame = r.points_base ? 'base' : 'camera';
|
||||||
lastDetections = Array.isArray(r.detections) ? r.detections : [];
|
lastDetections = Array.isArray(r.detections) ? r.detections : [];
|
||||||
lastSelectedClass = r.selected ? r.selected.class : null;
|
lastSelectedClass = r.selected ? r.selected.class : null;
|
||||||
lastCropBbox = r.selected ? r.selected.bbox : null;
|
|
||||||
lastPoints = pts;
|
|
||||||
refreshObjectPickOptions(lastDetections);
|
refreshObjectPickOptions(lastDetections);
|
||||||
renderGraspLegend(lastDetections);
|
renderGraspLegend(lastDetections);
|
||||||
drawGraspFrame(); drawCrop(); drawScatters();
|
drawGraspFrame();
|
||||||
const c = centroidOf(pts);
|
const c = centroidOf(pts);
|
||||||
if (c) {
|
if (c) {
|
||||||
recentCentroids.push(c);
|
recentCentroids.push(c);
|
||||||
@@ -467,21 +378,6 @@ document.querySelectorAll('.tab').forEach(btn => btn.addEventListener('click', (
|
|||||||
if (btn.dataset.tab !== 'grasp' && loopTimer) stopLoop();
|
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 ---
|
// --- object pick select + legend + class discovery ---
|
||||||
|
|
||||||
// Build per-class summary: best (highest-confidence) detection and instance count.
|
// Build per-class summary: best (highest-confidence) detection and instance count.
|
||||||
@@ -575,6 +471,102 @@ $('graspObjectPick').addEventListener('change', () => {
|
|||||||
if (!loopTimer) $('detectBtn').click();
|
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 ====================
|
// ==================== EE ====================
|
||||||
async function refreshWrist() {
|
async function refreshWrist() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in new issue
Block a user