// 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, }; // ---------- 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 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 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; 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 ---------- // ---------- Auto-tune (Halcon-style) ---------- async function doAutoTune() { if (!state.model || !state.roi) { alert("Seleziona modello e disegna ROI prima di Auto-tune."); return; } const status = document.getElementById("status"); status.textContent = "Analisi ROI in corso..."; try { 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) throw new Error(await r.text()); const t = await r.json(); // Applica ai campi avanzati (override automatico) for (const [key] of ADV_PARAMS) { const el = document.getElementById(`adv-${key}`); if (el && t[key] !== undefined) el.value = String(t[key]); } // Espandi la sezione Avanzate per mostrare i valori applicati const advDetails = document.querySelector("#col-params details:last-of-type"); if (advDetails) advDetails.open = true; // Feedback diagnostico const lines = [ `weak/strong: ${t.weak_grad} / ${t.strong_grad}`, `feature: ${t.num_features}, piramide: ${t.pyramid_levels}`, `angle: [${t.angle_min}..${t.angle_max}]@${t.angle_step}°`, ]; if (t._symmetry_order > 1) { lines.push(`simmetria rotaz. ${t._symmetry_order}x (conf ${t._symmetry_conf})`); } if (t._self_score !== undefined) { lines.push(`self-validation: ${t._validation}`); } status.textContent = `Auto-tune OK — ${lines[0]}`; alert("Auto-tune completato:\n\n" + lines.join("\n")); } catch (e) { status.textContent = `Auto-tune errore: ${e.message}`; alert(`Errore auto-tune: ${e.message}`); } } // ---------- V: Save recipe ---------- async function saveRecipe() { if (!state.model || !state.roi) { alert("Seleziona modello e disegna ROI prima di salvare la ricetta."); return; } const name = document.getElementById("hc-recipe-name").value.trim(); if (!name) { alert("Inserisci un nome per la ricetta."); return; } const user = readUserParams(); const body = { model_id: state.model.id, scene_id: state.scene?.id || state.model.id, roi: state.roi, tipo: user.tipo, simmetria: user.simmetria, scala: user.scala, precisione: user.precisione, use_polarity: user.use_polarity, use_gpu: user.use_gpu, name: name, }; try { const r = await fetch("/recipes", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!r.ok) throw new Error(await r.text()); const j = await r.json(); alert(`Ricetta salvata: ${j.name}\n${j.n_variants} varianti, ${j.size} bytes`); } catch (e) { alert(`Errore salvataggio: ${e.message}`); } } window.addEventListener("DOMContentLoaded", async () => { buildAdvancedForm(); setupROI(); // Popola picker immagini da IMAGES_DIR (con thumbnail) const {files, dir} = await refreshPickers(); if (files.length === 0) { setStatus(`Nessuna immagine in ${dir} (carica file o configura IMAGES_DIR)`); } else { setStatus(`${files.length} immagini in ${dir}`); } // Upload file nella folder const upEl = document.getElementById("file-upload"); upEl.addEventListener("change", async (e) => { const f = e.target.files[0]; if (!f) return; setStatus(`Caricamento ${f.name} nella cartella...`); try { const res = await uploadToFolder(f); await refreshPickers(); setStatus(`Salvato come ${res.saved_as} (${res.files.length} file totali)`); } catch (err) { setStatus(`Errore upload: ${err.message}`); } e.target.value = ""; // consente re-upload stesso file }); document.getElementById("btn-match").addEventListener("click", doMatch); document.getElementById("btn-autotune").addEventListener("click", doAutoTune); document.getElementById("btn-save-recipe").addEventListener("click", saveRecipe); const slider = document.getElementById("p-min-score"); slider.addEventListener("input", (e) => { document.getElementById("v-score").textContent = parseFloat(e.target.value).toFixed(2); }); renderModel(); renderScene(); });