Files
Shape_Model_2D/pm2d/web/static/app.js
T
Adriano cc7d035f66 feat: scale_penalty - score riflette dimensione oltre a forma
Shape matching e invariante scala per design: 3 ruote dentate di dim
diverse avevano tutte score 1.00 confondendo l operatore.

Parametro scale_penalty [0..1]: score_final = score * max(0, 1 - penalty * |scale - 1|)
UI dropdown 'Peso dimensione nel score' con preset 0 / 0.3 / 0.5 / 0.8.

Test rings con penalty 0.5: 1.00 -> 1.00, 0.95 -> 0.97, 0.80 -> 0.90.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:37:36 +02:00

371 lines
13 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,
};
// ---------- 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),
};
}
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();
}
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};
const FP_MAP = {off:0, leggero:0.20, medio:0.35, forte:0.50};
const [smin, smax, sstep] = SCALE_MAP[user.scala];
body = {
model_id: state.model.id, scene_id: state.scene.id, roi: state.roi,
angle_min: 0, angle_max: SYM_MAP[user.simmetria] || 360,
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.35),
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 ----------
window.addEventListener("DOMContentLoaded", async () => {
buildAdvancedForm();
setupROI();
// Popola picker immagini da IMAGES_DIR (con thumbnail)
const {files, dir} = await fetchImagesList();
buildThumbPicker("picker-model", files, onSelectModel);
buildThumbPicker("picker-scene", files, onSelectScene);
if (files.length === 0) {
setStatus(`Nessuna immagine in ${dir} (configura IMAGES_DIR in .env)`);
} else {
setStatus(`${files.length} immagini disponibili in ${dir}`);
}
document.getElementById("btn-match").addEventListener("click", doMatch);
const slider = document.getElementById("p-min-score");
slider.addEventListener("input", (e) => {
document.getElementById("v-score").textContent =
parseFloat(e.target.value).toFixed(2);
});
renderModel();
renderScene();
});