// Pattern Matching 2D - frontend (UI semplificata operator-friendly) // Parametri avanzati (sezione collassabile) const ADV_PARAMS = [ ["num_features", "Num feature", "int", ""], ["weak_grad", "Weak grad", "float", ""], ["strong_grad", "Strong grad", "float", ""], ["spread_radius", "Spread radius", "int", ""], ["pyramid_levels", "Pyramid levels", "int", ""], ["verify_threshold", "Verify NCC thr", "float", 0.4], ["nms_radius", "NMS radius (0=auto)", "int", 0], ]; const PALETTE = [ "#00ff00", "#ffc800", "#ff6464", "#ffc800", "#c800ff", "#64ffc8", "#ff0000", "#00ffff", ]; const state = { model: null, scene: null, roi: null, drag: null, matches: [], annotatedImg: null, active_recipe: null, // V: ricetta caricata (string nome) o null }; // ---------- Forms ---------- function buildAdvancedForm() { const form = document.getElementById("adv-form"); form.innerHTML = ""; for (const [key, label, , def] of ADV_PARAMS) { const lbl = document.createElement("label"); lbl.textContent = label; const inp = document.createElement("input"); inp.id = `adv-${key}`; inp.type = "text"; inp.placeholder = "auto"; inp.value = def === "" ? "" : String(def); inp.addEventListener("keydown", (e) => { if (e.key === "Enter") doMatch(); }); form.appendChild(lbl); form.appendChild(inp); } } function readUserParams() { return { tipo: document.getElementById("p-tipo").value, simmetria: document.getElementById("p-simmetria").value, scala: document.getElementById("p-scala").value, precisione: document.getElementById("p-precisione").value, filtro_fp: document.getElementById("p-filtro-fp").value, penalita_scala: parseFloat( document.getElementById("p-penalita-scala").value), min_score: parseFloat(document.getElementById("p-min-score").value), max_matches: parseInt(document.getElementById("p-max-matches").value, 10), ...readHalconFlags(), }; } function readHalconFlags() { // Halcon-mode toggle: tutti i flag default-off, esposti via "Modalità Halcon" const $cb = (id) => document.getElementById(id)?.checked ?? false; const $num = (id, def) => { const v = parseFloat(document.getElementById(id)?.value); return Number.isFinite(v) ? v : def; }; const $int = (id, def) => { const v = parseInt(document.getElementById(id)?.value, 10); return Number.isFinite(v) ? v : def; }; const roiStr = document.getElementById("hc-search-roi")?.value.trim() ?? ""; let search_roi = null; if (roiStr) { const p = roiStr.split(/[ ,;]+/).map((x) => parseInt(x, 10)); if (p.length === 4 && p.every((v) => Number.isFinite(v))) search_roi = p; } return { use_polarity: $cb("hc-use-polarity"), use_gpu: $cb("hc-use-gpu"), use_soft_score: $cb("hc-soft-score"), subpixel_lm: $cb("hc-subpixel-lm"), refine_pose_joint: $cb("hc-refine-joint"), pyramid_propagate: $cb("hc-pyr-propagate"), min_recall: $num("hc-min-recall", 0), nms_iou_threshold: $num("hc-nms-iou", 0.3), greediness: $num("hc-greediness", 0), coarse_stride: $int("hc-coarse-stride", 1), search_roi: search_roi, }; } function readAdvancedOverrides() { const out = {}; for (const [key, , type] of ADV_PARAMS) { const v = document.getElementById(`adv-${key}`).value.trim(); if (v === "") continue; out[key] = type === "int" ? parseInt(v, 10) : parseFloat(v); } return out; } // ---------- Image loading from folder ---------- async function loadFromFolder(filename) { const r = await fetch("/load_from_folder", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ filename }), }); if (!r.ok) throw new Error(await r.text()); return await r.json(); } async function fetchImagesList() { const r = await fetch("/images"); if (!r.ok) return { files: [], dir: "" }; return await r.json(); } async function uploadToFolder(file) { const fd = new FormData(); fd.append("file", file); const r = await fetch("/upload_to_folder", { method: "POST", body: fd }); if (!r.ok) throw new Error(await r.text()); return await r.json(); } async function refreshPickers() { const {files, dir} = await fetchImagesList(); buildThumbPicker("picker-model", files, onSelectModel); buildThumbPicker("picker-scene", files, onSelectScene); return {files, dir}; } function buildThumbPicker(pickerId, files, onSelect) { const picker = document.getElementById(pickerId); const current = picker.querySelector(".picker-current"); const list = picker.querySelector(".picker-list"); const text = current.querySelector(".picker-text"); // Rimuovi eventuale vecchia thumbnail const oldImg = current.querySelector("img"); if (oldImg) oldImg.remove(); list.innerHTML = ""; files.forEach((f) => { const item = document.createElement("div"); item.className = "picker-item"; const img = document.createElement("img"); img.src = `/folder_image/${encodeURIComponent(f)}?w=120`; img.loading = "lazy"; const name = document.createElement("span"); name.className = "name"; name.textContent = f; item.appendChild(img); item.appendChild(name); item.addEventListener("click", () => { // Aggiorna la visual del "current" let thumb = current.querySelector("img"); if (!thumb) { thumb = document.createElement("img"); current.insertBefore(thumb, text); } thumb.src = `/folder_image/${encodeURIComponent(f)}?w=80`; text.textContent = f; picker.classList.remove("open"); onSelect(f); }); list.appendChild(item); }); current.onclick = () => { // Chiudi altri picker aperti document.querySelectorAll(".thumb-picker.open") .forEach((p) => { if (p !== picker) p.classList.remove("open"); }); picker.classList.toggle("open"); }; } // Close picker on outside click document.addEventListener("click", (e) => { if (!e.target.closest(".thumb-picker")) { document.querySelectorAll(".thumb-picker.open") .forEach((p) => p.classList.remove("open")); } }); function loadImage(src) { return new Promise((res, rej) => { const img = new Image(); img.onload = () => res(img); img.onerror = rej; img.src = src; }); } async function onSelectModel(filename) { if (!filename) return; setStatus("Caricamento modello..."); try { const meta = await loadFromFolder(filename); const img = await loadImage(`/image/${meta.id}/raw`); state.model = { id: meta.id, w: meta.width, h: meta.height, img }; state.roi = null; document.getElementById("roi-info").textContent = "ROI: (nessuna)"; setStatus(`Modello: ${filename} ${meta.width}x${meta.height} — trascina ROI`); renderModel(); } catch (e) { setStatus(`Errore modello: ${e.message}`); } } async function onSelectScene(filename) { if (!filename) return; setStatus("Caricamento scena..."); try { const meta = await loadFromFolder(filename); 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: ${filename} ${meta.width}x${meta.height}`); renderScene(); } catch (e) { setStatus(`Errore scena: ${e.message}`); } } // ---------- Rendering ---------- function fitToCanvas(img, cw, ch) { const sc = Math.min(cw / img.width, ch / img.height); const dw = img.width * sc, dh = img.height * sc; return { sc, ox: (cw - dw) / 2, oy: (ch - dh) / 2, 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); const img = state.annotatedImg || (state.scene && state.scene.img); if (!img) return; const fit = fitToCanvas(img, cnv.width, cnv.height); ctx.drawImage(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", () => { 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; 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(); }); } // ---------- Match action ---------- async function doMatchRecipe() { if (!state.scene) { setStatus("Carica scena"); return; } setStatus(`Match ricetta ${state.active_recipe}...`); const hc = readHalconFlags(); const body = { recipe: state.active_recipe, scene_id: state.scene.id, min_score: parseFloat(document.getElementById("p-min-score").value), max_matches: parseInt(document.getElementById("p-max-matches").value, 10), verify_threshold: 0.50, ...hc, }; const r = await fetch("/match_recipe", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!r.ok) { setStatus(`Errore: ${await r.text()}`); 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 = "—"; 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; renderDiag(data.diag, data.matches.length); setStatus(`${data.matches.length} match trovati (ricetta ${state.active_recipe})`); } async function doMatch() { // Path V: ricetta caricata → bypass training, solo find su scena if (state.active_recipe) { return doMatchRecipe(); } if (!state.model) { setStatus("Carica modello"); return; } if (!state.scene) { setStatus("Carica scena"); return; } if (!state.roi) { setStatus("Seleziona ROI sul modello"); return; } const user = readUserParams(); const adv = readAdvancedOverrides(); setStatus("Match in corso..."); // Se utente ha fornito override avanzati → usa /match (tecnico) // altrimenti /match_simple (operator mode) const hasAdv = Object.keys(adv).length > 0; const url = hasAdv ? "/match" : "/match_simple"; let body; if (hasAdv) { // Merge simple → tecnici base, poi overrides const SYM_MAP = {invariante:0, nessuna:360, bilaterale:180, rot_3:120, rot_4:90, rot_6:60, rot_8:45}; const SCALE_MAP = {fissa:[1,1,0.1], mini:[0.9,1.1,0.05], medio:[0.75,1.25,0.05], max:[0.5,1.5,0.05]}; const PREC_MAP = {veloce:10, normale:5, preciso:2}; // Allineato a FILTRO_FP_MAP server-side (server.py) const FP_MAP = {off:0, leggero:0.30, medio:0.50, forte:0.70}; const [smin, smax, sstep] = SCALE_MAP[user.scala]; // NB: SYM_MAP[invariante]=0 e' valido (zero rotazioni). Uso ?? per // distinguere "chiave mancante" da "valore zero": altrimenti 0 || 360 // collassa invariante a 360 = bug "simmetria non ha effetto". const angMax = SYM_MAP[user.simmetria] ?? 360; body = { model_id: state.model.id, scene_id: state.scene.id, roi: state.roi, angle_min: 0, angle_max: angMax, angle_step: PREC_MAP[user.precisione] ?? 5, scale_min: smin, scale_max: smax, scale_step: sstep, min_score: user.min_score, max_matches: user.max_matches, num_features: adv.num_features ?? 96, weak_grad: adv.weak_grad ?? 30, strong_grad: adv.strong_grad ?? 60, spread_radius: adv.spread_radius ?? 5, pyramid_levels: adv.pyramid_levels ?? 3, verify_threshold: adv.verify_threshold ?? (FP_MAP[user.filtro_fp] ?? 0.50), nms_radius: adv.nms_radius ?? 0, }; } else { body = { model_id: state.model.id, scene_id: state.scene.id, roi: state.roi, ...user, }; } const r = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!r.ok) { setStatus(`Errore: ${await r.text()}`); 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; renderDiag(data.diag, data.matches.length); setStatus(`${data.matches.length} match trovati${hasAdv ? " (avanzato)" : ""}`); } 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 ---------- // ---------- CC: Diagnostica match ---------- function renderDiag(diag, n_matches) { const el = document.getElementById("diag-content"); if (!diag) { el.innerHTML = 'Diagnostica non disponibile'; return; } const dropTotal = (diag.drop_ncc_low || 0) + (diag.drop_min_score_post_avg || 0) + (diag.drop_recall_low || 0) + (diag.drop_bbox_out_of_scene || 0) + (diag.drop_nms_iou || 0); // Hint contestuali se 0 match let hint = ""; if (n_matches === 0) { if (diag.n_after_pre_nms === 0) { hint = `