// Pattern Matching 2D - frontend const PARAMS = [ ["angle_min", "Angolo min [°]", "float", 0], ["angle_max", "Angolo max [°]", "float", 360], ["angle_step", "Angolo step [°]", "float", 5], ["scale_min", "Scala min", "float", 1], ["scale_max", "Scala max", "float", 1], ["scale_step", "Scala step", "float", 0.1], ["min_score", "Score min [0..1]", "float", 0.55], ["max_matches", "Max match", "int", 25], ["nms_radius", "NMS radius (0=auto)", "int", 0], ["num_features", "Num feature", "int", 96], ["weak_grad", "Weak grad", "float", 30], ["strong_grad", "Strong grad", "float", 60], ["spread_radius", "Spread radius", "int", 5], ["pyramid_levels", "Pyramid levels", "int", 3], ["verify_threshold", "Verify NCC thr", "float", 0.4], ]; const PALETTE = [ "#00ff00", "#ffc800", "#ff6464", "#ffc800", "#c800ff", "#64ffc8", "#ff0000", "#00ffff", ]; const state = { model: null, // {id, w, h, img:HTMLImageElement, scale, ox, oy} scene: null, roi: null, // [x, y, w, h] in originale drag: null, // {x0,y0,x1,y1} in canvas coords matches: [], annotatedImg: null, // HTMLImageElement annotated }; // ---------- Params form ---------- function buildForm() { const form = document.getElementById("params-form"); form.innerHTML = ""; for (const [key, label, type, def] of PARAMS) { const lbl = document.createElement("label"); lbl.textContent = label; lbl.htmlFor = `p-${key}`; const inp = document.createElement("input"); inp.id = `p-${key}`; inp.name = key; inp.value = String(def); inp.type = "text"; inp.addEventListener("keydown", (e) => { if (e.key === "Enter") doMatch(); }); form.appendChild(lbl); form.appendChild(inp); } } function readParams() { const out = {}; for (const [key, , type] of PARAMS) { const v = document.getElementById(`p-${key}`).value.trim(); out[key] = type === "int" ? parseInt(v, 10) : parseFloat(v); } return out; } function setParams(values) { for (const [key] of PARAMS) { if (values[key] !== undefined) { document.getElementById(`p-${key}`).value = String(values[key]); } } } // ---------- Upload ---------- async function uploadFile(file) { const fd = new FormData(); fd.append("file", file); const r = await fetch("/upload", { method: "POST", body: fd }); if (!r.ok) throw new Error("upload failed"); return await r.json(); } function loadImage(src) { return new Promise((res, rej) => { const img = new Image(); img.onload = () => res(img); img.onerror = rej; img.src = src; }); } async function onLoadModel(file) { setStatus("Caricamento modello..."); const meta = await uploadFile(file); const img = await loadImage(`/image/${meta.id}/raw`); state.model = { id: meta.id, w: meta.width, h: meta.height, img }; state.roi = null; setStatus(`Modello: ${file.name} ${meta.width}x${meta.height} — trascina ROI`); renderModel(); } async function onLoadScene(file) { setStatus("Caricamento scena..."); const meta = await uploadFile(file); const img = await loadImage(`/image/${meta.id}/raw`); state.scene = { id: meta.id, w: meta.width, h: meta.height, img }; state.matches = []; state.annotatedImg = null; setStatus(`Scena: ${file.name} ${meta.width}x${meta.height}`); renderScene(); } // ---------- Rendering ---------- function fitToCanvas(img, cw, ch) { const sc = Math.min(cw / img.width, ch / img.height); const dw = img.width * sc; const dh = img.height * sc; const ox = (cw - dw) / 2; const oy = (ch - dh) / 2; return { sc, ox, oy, dw, dh }; } function renderModel() { const cnv = document.getElementById("c-model"); const ctx = cnv.getContext("2d"); ctx.fillStyle = "#141414"; ctx.fillRect(0, 0, cnv.width, cnv.height); if (!state.model) return; const fit = fitToCanvas(state.model.img, cnv.width, cnv.height); state.model.scale = fit.sc; state.model.ox = fit.ox; state.model.oy = fit.oy; ctx.drawImage(state.model.img, fit.ox, fit.oy, fit.dw, fit.dh); if (state.roi) { const [x, y, w, h] = state.roi; ctx.strokeStyle = "#00ff80"; ctx.lineWidth = 2; ctx.strokeRect(fit.ox + x * fit.sc, fit.oy + y * fit.sc, w * fit.sc, h * fit.sc); } if (state.drag) { ctx.strokeStyle = "#ffff00"; ctx.setLineDash([4, 2]); ctx.lineWidth = 2; ctx.strokeRect( Math.min(state.drag.x0, state.drag.x1), Math.min(state.drag.y0, state.drag.y1), Math.abs(state.drag.x1 - state.drag.x0), Math.abs(state.drag.y1 - state.drag.y0), ); ctx.setLineDash([]); } } function renderScene() { const cnv = document.getElementById("c-scene"); const ctx = cnv.getContext("2d"); ctx.fillStyle = "#141414"; ctx.fillRect(0, 0, cnv.width, cnv.height); if (state.annotatedImg) { const fit = fitToCanvas(state.annotatedImg, cnv.width, cnv.height); ctx.drawImage(state.annotatedImg, fit.ox, fit.oy, fit.dw, fit.dh); } else if (state.scene) { const fit = fitToCanvas(state.scene.img, cnv.width, cnv.height); ctx.drawImage(state.scene.img, fit.ox, fit.oy, fit.dw, fit.dh); } } // ---------- ROI drag ---------- function canvasPos(cnv, ev) { const r = cnv.getBoundingClientRect(); return { x: ev.clientX - r.left, y: ev.clientY - r.top }; } function setupROI() { const cnv = document.getElementById("c-model"); cnv.addEventListener("mousedown", (e) => { if (!state.model) return; const p = canvasPos(cnv, e); state.drag = { x0: p.x, y0: p.y, x1: p.x, y1: p.y }; renderModel(); }); cnv.addEventListener("mousemove", (e) => { if (!state.drag) return; const p = canvasPos(cnv, e); state.drag.x1 = p.x; state.drag.y1 = p.y; renderModel(); }); cnv.addEventListener("mouseup", (e) => { if (!state.drag || !state.model) return; const d = state.drag; state.drag = null; if (Math.abs(d.x1 - d.x0) < 4 || Math.abs(d.y1 - d.y0) < 4) return; // Canvas → originale const m = state.model; const ix0 = Math.round((Math.min(d.x0, d.x1) - m.ox) / m.scale); const iy0 = Math.round((Math.min(d.y0, d.y1) - m.oy) / m.scale); const iw = Math.round(Math.abs(d.x1 - d.x0) / m.scale); const ih = Math.round(Math.abs(d.y1 - d.y0) / m.scale); const cx0 = Math.max(0, Math.min(ix0, m.w - 1)); const cy0 = Math.max(0, Math.min(iy0, m.h - 1)); const cw = Math.max(1, Math.min(iw, m.w - cx0)); const ch = Math.max(1, Math.min(ih, m.h - cy0)); state.roi = [cx0, cy0, cw, ch]; document.getElementById("roi-info").textContent = `ROI: ${cw}x${ch} @ (${cx0}, ${cy0})`; renderModel(); }); } // ---------- Actions ---------- async function doMatch() { if (!state.model) { setStatus("Carica modello"); return; } if (!state.scene) { setStatus("Carica scena"); return; } if (!state.roi) { setStatus("Seleziona ROI sul modello"); return; } const params = readParams(); setStatus("Match in corso..."); const body = { model_id: state.model.id, scene_id: state.scene.id, roi: state.roi, ...params, }; const r = await fetch("/match", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!r.ok) { const t = await r.text(); setStatus(`Errore: ${t}`); return; } const data = await r.json(); state.matches = data.matches; state.annotatedImg = await loadImage( `/image/${data.annotated_id}/raw?t=${Date.now()}`, ); renderScene(); renderLegend(); document.getElementById("t-train").textContent = `${data.train_time.toFixed(2)}s`; document.getElementById("t-find").textContent = `${data.find_time.toFixed(2)}s`; document.getElementById("t-var").textContent = data.num_variants; document.getElementById("t-match").textContent = data.matches.length; setStatus(`${data.matches.length} match trovati`); } async function doAutoTune() { if (!state.model || !state.roi) { setStatus("Carica modello + seleziona ROI"); return; } const r = await fetch("/auto_tune", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model_id: state.model.id, roi: state.roi }), }); if (!r.ok) { setStatus("Errore auto-tune"); return; } const tune = await r.json(); setParams(tune); setStatus("Auto-tune applicato — premi MATCH"); } function renderLegend() { const el = document.getElementById("legend"); el.innerHTML = ""; state.matches.forEach((m, i) => { const div = document.createElement("div"); div.className = "legend-item"; const dot = document.createElement("span"); dot.className = "legend-dot"; dot.style.background = PALETTE[i % PALETTE.length]; div.appendChild(dot); const txt = document.createElement("span"); txt.textContent = `#${i+1} cx=${Math.round(m.cx)} cy=${Math.round(m.cy)} ` + `${m.angle_deg.toFixed(1)}° s=${m.scale.toFixed(2)} ` + `score=${m.score.toFixed(3)}`; div.appendChild(txt); el.appendChild(div); }); } function setStatus(s) { document.getElementById("status").textContent = s; } // ---------- Init ---------- window.addEventListener("DOMContentLoaded", () => { buildForm(); setupROI(); document.getElementById("file-model").addEventListener("change", (e) => { if (e.target.files[0]) onLoadModel(e.target.files[0]); }); document.getElementById("file-scene").addEventListener("change", (e) => { if (e.target.files[0]) onLoadScene(e.target.files[0]); }); document.getElementById("btn-match").addEventListener("click", doMatch); document.getElementById("btn-tune").addEventListener("click", doAutoTune); renderModel(); renderScene(); });