ui: parametri user-friendly (tipo/simmetria/scala/precisione)
Nascosti i parametri tecnici (num_features, weak/strong_grad, spread, pyramid) incomprensibili per operatori. Sostituiti da scelte semantiche: - Tipo modello: intero | parziale - Simmetria: nessuna | bilaterale (180) | rotazionale 3/4/6/8x - Variazione scala: fissa | 10% | 25% | 50% - Precisione: veloce 10 | normale 5 | preciso 2 - Score minimo: slider - Max match: input Server: nuovo endpoint POST /match_simple. Deriva tecnici via _simple_to_technical(roi) che analizza la ROI: - weak/strong_grad da percentili - num_features da densita edge x tipo - pyramid_levels da min(h,w) ROI - spread_radius proporzionale Frontend: select + slider, sezione Avanzate collassabile per override. Test rings_and_nuts preset intero/nessuna/medio/normale: 3/3 ruote in 2.14s Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+94
-100
@@ -1,21 +1,14 @@
|
||||
// Pattern Matching 2D - frontend
|
||||
// Pattern Matching 2D - frontend (UI semplificata operator-friendly)
|
||||
|
||||
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],
|
||||
// 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 = [
|
||||
@@ -24,27 +17,22 @@ const PALETTE = [
|
||||
];
|
||||
|
||||
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
|
||||
model: null, scene: null, roi: null, drag: null,
|
||||
matches: [], annotatedImg: null,
|
||||
};
|
||||
|
||||
// ---------- Params form ----------
|
||||
function buildForm() {
|
||||
const form = document.getElementById("params-form");
|
||||
// ---------- Forms ----------
|
||||
function buildAdvancedForm() {
|
||||
const form = document.getElementById("adv-form");
|
||||
form.innerHTML = "";
|
||||
for (const [key, label, type, def] of PARAMS) {
|
||||
for (const [key, label, , def] of ADV_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.id = `adv-${key}`;
|
||||
inp.type = "text";
|
||||
inp.placeholder = "auto";
|
||||
inp.value = def === "" ? "" : String(def);
|
||||
inp.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") doMatch();
|
||||
});
|
||||
@@ -53,23 +41,27 @@ function buildForm() {
|
||||
}
|
||||
}
|
||||
|
||||
function readParams() {
|
||||
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,
|
||||
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 PARAMS) {
|
||||
const v = document.getElementById(`p-${key}`).value.trim();
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -103,8 +95,7 @@ async function onLoadScene(file) {
|
||||
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;
|
||||
state.matches = []; state.annotatedImg = null;
|
||||
setStatus(`Scena: ${file.name} ${meta.width}x${meta.height}`);
|
||||
renderScene();
|
||||
}
|
||||
@@ -112,11 +103,8 @@ async function onLoadScene(file) {
|
||||
// ---------- 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 };
|
||||
const dw = img.width * sc, dh = img.height * sc;
|
||||
return { sc, ox: (cw - dw) / 2, oy: (ch - dh) / 2, dw, dh };
|
||||
}
|
||||
|
||||
function renderModel() {
|
||||
@@ -127,26 +115,22 @@ function renderModel() {
|
||||
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;
|
||||
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.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.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),
|
||||
);
|
||||
Math.abs(state.drag.y1 - state.drag.y0));
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
}
|
||||
@@ -156,13 +140,10 @@ function renderScene() {
|
||||
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);
|
||||
}
|
||||
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 ----------
|
||||
@@ -185,12 +166,10 @@ function setupROI() {
|
||||
state.drag.x1 = p.x; state.drag.y1 = p.y;
|
||||
renderModel();
|
||||
});
|
||||
cnv.addEventListener("mouseup", (e) => {
|
||||
cnv.addEventListener("mouseup", () => {
|
||||
if (!state.drag || !state.model) return;
|
||||
const d = state.drag;
|
||||
state.drag = null;
|
||||
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);
|
||||
@@ -207,57 +186,68 @@ function setupROI() {
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Actions ----------
|
||||
// ---------- 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 params = readParams();
|
||||
const user = readUserParams();
|
||||
const adv = readAdvancedOverrides();
|
||||
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", {
|
||||
|
||||
// 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 = {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 [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 ?? 0.4,
|
||||
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) {
|
||||
const t = await r.text();
|
||||
setStatus(`Errore: ${t}`);
|
||||
return;
|
||||
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()}`,
|
||||
);
|
||||
`/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");
|
||||
setStatus(`${data.matches.length} match trovati${hasAdv ? " (avanzato)" : ""}`);
|
||||
}
|
||||
|
||||
function renderLegend() {
|
||||
@@ -285,7 +275,7 @@ function setStatus(s) {
|
||||
|
||||
// ---------- Init ----------
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
buildForm();
|
||||
buildAdvancedForm();
|
||||
setupROI();
|
||||
document.getElementById("file-model").addEventListener("change", (e) => {
|
||||
if (e.target.files[0]) onLoadModel(e.target.files[0]);
|
||||
@@ -294,7 +284,11 @@ window.addEventListener("DOMContentLoaded", () => {
|
||||
if (e.target.files[0]) onLoadScene(e.target.files[0]);
|
||||
});
|
||||
document.getElementById("btn-match").addEventListener("click", doMatch);
|
||||
document.getElementById("btn-tune").addEventListener("click", doAutoTune);
|
||||
const slider = document.getElementById("p-min-score");
|
||||
slider.addEventListener("input", (e) => {
|
||||
document.getElementById("v-score").textContent =
|
||||
parseFloat(e.target.value).toFixed(2);
|
||||
});
|
||||
renderModel();
|
||||
renderScene();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user