Files
Shape_Model_2D/pm2d/web/static/app.js
Adriano a78884f950 feat(web): anteprima edge sul modello + tracker pulizia rumore + UCS baricentro
Pannello "🔬 Anteprima edge / pulizia rumore" sotto il canvas modello.
Permette tuning interattivo dei parametri di selezione edge per
togliere "sporcizie" (rumore di sfondo, edge spuri) prima di
trainare il matcher.

Server:
- POST /preview_edges: dato modello+ROI+param edge, ritorna immagine
  ROI con overlay:
  * heatmap magnitude gradient (sfondo)
  * verde scuro: pixel sopra hysteresis edge
  * cerchietti colorati per bin: feature scelte (palette 16 bin)
  * UCS rosso/verde sul baricentro feature (richiesta utente):
    asse X destra, Y giu' (image y-down)
  Ritorna anche stats: n_features, n_edge_strong, percentili magnitude,
  ucs_baricentro {cx, cy}

UI:
- Slider weak_grad/strong_grad/num_features/spacing + checkbox polarity
- Re-fetch debounced (200ms) ad ogni input → preview live
- Bottone "Applica ai parametri Avanzate": copia i valori scelti
  nei campi Avanzate del matcher principale
- Auto-fetch quando il pannello viene aperto

Use case: operatore vede SUBITO quali edge il matcher userebbe,
regola soglie per escludere rumore, applica e poi MATCH.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 10:48:58 +02:00

780 lines
28 KiB
JavaScript

// 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 ----------
// ---------- Edge preview (clean rumore) ----------
let _epDebounce = null;
let _epLastImg = null;
async function fetchEdgePreview() {
if (!state.model || !state.roi) {
document.getElementById("edge-preview-info").textContent =
"Disegna prima la ROI sul modello";
return;
}
const body = {
model_id: state.model.id,
roi: state.roi,
weak_grad: parseFloat(document.getElementById("ep-weak").value),
strong_grad: parseFloat(document.getElementById("ep-strong").value),
num_features: parseInt(document.getElementById("ep-nf").value, 10),
min_feature_spacing: parseInt(document.getElementById("ep-sp").value, 10),
use_polarity: document.getElementById("ep-pol").checked,
};
try {
const r = await fetch("/preview_edges", {
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();
_epLastImg = await loadImage(`/image/${j.preview_id}/raw?t=${Date.now()}`);
drawEdgePreview();
const ucs = j.ucs_baricentro
? ` | UCS=(${j.ucs_baricentro.cx},${j.ucs_baricentro.cy})`
: "";
document.getElementById("edge-preview-info").innerHTML =
`<b>${j.n_features}</b> feature scelte (di ${j.n_edge_after_hysteresis} edge totali)<br>` +
`mag: max=${j.mag_max.toFixed(0)} p50=${j.mag_p50.toFixed(0)} ` +
`p85=${j.mag_p85.toFixed(0)}${ucs}`;
} catch (e) {
document.getElementById("edge-preview-info").textContent =
`Errore preview: ${e.message}`;
}
}
function drawEdgePreview() {
const cnv = document.getElementById("c-edge-preview");
if (!_epLastImg) return;
const ctx = cnv.getContext("2d");
// Fit-contain
const r = Math.min(cnv.width / _epLastImg.width,
cnv.height / _epLastImg.height);
const w = _epLastImg.width * r;
const h = _epLastImg.height * r;
const ox = (cnv.width - w) / 2;
const oy = (cnv.height - h) / 2;
ctx.fillStyle = "#000"; ctx.fillRect(0, 0, cnv.width, cnv.height);
ctx.imageSmoothingEnabled = false;
ctx.drawImage(_epLastImg, ox, oy, w, h);
}
function scheduleEdgePreview() {
if (_epDebounce) clearTimeout(_epDebounce);
_epDebounce = setTimeout(fetchEdgePreview, 200);
}
function bindEdgePreviewControls() {
const slid = (id, valEl) => {
const el = document.getElementById(id);
const v = document.getElementById(valEl);
el.addEventListener("input", () => {
v.textContent = el.value;
scheduleEdgePreview();
});
};
slid("ep-weak", "ep-weak-v");
slid("ep-strong", "ep-strong-v");
slid("ep-nf", "ep-nf-v");
slid("ep-sp", "ep-sp-v");
document.getElementById("ep-pol").addEventListener("change",
scheduleEdgePreview);
// Auto-refresh quando il pannello viene aperto
document.getElementById("edge-preview-panel").addEventListener("toggle",
(e) => { if (e.target.open) fetchEdgePreview(); });
document.getElementById("btn-edge-apply").addEventListener("click", () => {
// Copia i valori correnti nei campi avanzati
const map = {
"ep-weak": "adv-weak_grad",
"ep-strong": "adv-strong_grad",
"ep-nf": "adv-num_features",
"ep-sp": "adv-min_feature_spacing",
};
for (const [src, dst] of Object.entries(map)) {
const dstEl = document.getElementById(dst);
if (dstEl) dstEl.value = document.getElementById(src).value;
}
// use_polarity: alla checkbox della modalita Halcon
const polCb = document.getElementById("hc-use-polarity");
if (polCb) polCb.checked = document.getElementById("ep-pol").checked;
// Apri pannello Avanzate per feedback
const advDetails = document.querySelectorAll("#col-params details");
advDetails.forEach((d) => { d.open = true; });
alert("Parametri edge applicati. Esegui MATCH per usare i valori scelti.");
});
}
// ---------- CC: Diagnostica match ----------
function renderDiag(diag, n_matches) {
const el = document.getElementById("diag-content");
if (!diag) {
el.innerHTML = '<em style="color:#888">Diagnostica non disponibile</em>';
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 = `<div style="color:#f88; margin-top:6px">⚠ Nessun candidato sopra soglia.
Prova: ↓ <b>min_score</b> o ↓ <b>top_thresh</b> (currently ${diag.top_thresh_used.toFixed(2)})</div>`;
} else if (diag.drop_ncc_low > 0 && dropTotal === diag.drop_ncc_low) {
hint = `<div style="color:#f88; margin-top:6px">⚠ ${diag.drop_ncc_low} candidati droppati da NCC.
Prova: ↓ <b>verify_threshold</b> (filtro_fp più leggero)</div>`;
} else if (diag.drop_min_score_post_avg > 0) {
hint = `<div style="color:#f88; margin-top:6px">⚠ ${diag.drop_min_score_post_avg} match sotto min_score post-NCC.
Prova: ↓ <b>min_score</b></div>`;
} else if (diag.drop_recall_low > 0) {
hint = `<div style="color:#f88; margin-top:6px">⚠ ${diag.drop_recall_low} match con recall < ${diag.min_recall_used}.
Prova: ↓ <b>min_recall</b></div>`;
} else if (diag.drop_bbox_out_of_scene > 0) {
hint = `<div style="color:#f88; margin-top:6px">⚠ ${diag.drop_bbox_out_of_scene} match con bbox fuori scena.
Centro derivato male: aumenta <b>min_score</b> o restringi <b>search_roi</b></div>`;
}
}
const flags = [];
if (diag.use_polarity) flags.push("polarity");
if (diag.use_soft_score) flags.push("soft");
if (diag.subpixel_lm) flags.push("subpix-LM");
el.innerHTML = `
<div><b>Pipeline pruning:</b></div>
<div>varianti: ${diag.n_variants_total} → top_eval=${diag.n_variants_top_evaluated}
→ top_pass=${diag.n_variants_top_passed} → full_eval=${diag.n_variants_full_evaluated}</div>
<div><b>Candidati:</b> raw=${diag.n_raw_candidates}
→ pre_nms=${diag.n_after_pre_nms} → final=${diag.n_final}</div>
<div><b>Drop reasons:</b> NCC=${diag.drop_ncc_low}, score=${diag.drop_min_score_post_avg},
recall=${diag.drop_recall_low}, bbox=${diag.drop_bbox_out_of_scene}, NMS=${diag.drop_nms_iou}</div>
<div><b>Soglie:</b> top=${diag.top_thresh_used.toFixed(2)},
min_score=${diag.min_score_used.toFixed(2)},
NCC=${diag.verify_threshold_used.toFixed(2)},
recall=${diag.min_recall_used.toFixed(2)}</div>
${flags.length ? `<div><b>Flag attivi:</b> ${flags.join(", ")}</div>` : ""}
${hint}
`;
// Auto-apri pannello se 0 match (segnala problema)
if (n_matches === 0) {
document.getElementById("diag-panel").open = true;
}
}
// ---------- 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: Recipe load/list/unload ----------
async function refreshRecipeList() {
try {
const r = await fetch("/recipes");
if (!r.ok) return;
const j = await r.json();
const sel = document.getElementById("hc-recipe-list");
const cur = sel.value;
sel.innerHTML = '<option value="">— ricette disponibili —</option>';
for (const f of j.files) {
const o = document.createElement("option");
o.value = f.name;
o.textContent = `${f.name} (${(f.size / 1024).toFixed(1)} KB)`;
sel.appendChild(o);
}
if (cur) sel.value = cur;
} catch (e) { /* silent */ }
}
async function loadRecipe() {
const sel = document.getElementById("hc-recipe-list");
const name = sel.value;
if (!name) {
alert("Seleziona una ricetta dalla lista.");
return;
}
try {
const r = await fetch(`/recipes/${encodeURIComponent(name)}/load`, {
method: "POST",
});
if (!r.ok) throw new Error(await r.text());
const j = await r.json();
state.active_recipe = j.name;
document.getElementById("recipe-status").textContent =
`Caricata: ${j.name}${j.n_variants} varianti, ` +
`${j.template_size[0]}x${j.template_size[1]} px` +
(j.use_polarity ? " (polarity)" : "");
document.getElementById("recipe-status").style.color = "#0c0";
document.getElementById("btn-unload-recipe").disabled = false;
} catch (e) {
alert(`Errore caricamento: ${e.message}`);
}
}
function unloadRecipe() {
state.active_recipe = null;
document.getElementById("recipe-status").textContent = "Nessuna ricetta caricata";
document.getElementById("recipe-status").style.color = "#888";
document.getElementById("btn-unload-recipe").disabled = true;
}
// ---------- 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`);
refreshRecipeList();
} 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);
document.getElementById("btn-load-recipe").addEventListener("click",
loadRecipe);
document.getElementById("btn-unload-recipe").addEventListener("click",
unloadRecipe);
refreshRecipeList();
bindEdgePreviewControls();
const slider = document.getElementById("p-min-score");
slider.addEventListener("input", (e) => {
document.getElementById("v-score").textContent =
parseFloat(e.target.value).toFixed(2);
});
renderModel();
renderScene();
});