- pm2d/dxf.py: rasterizzazione DXF -> template (ezdxf, flattening entita', scala/centratura, render edge antialiased) - POST /upload_dxf: carica CAD come modello (size 128..2048) - roi_poly su /match, /match_simple e POST /recipes: train con mask cv2.fillPoly (validazione 400 su poligoni degeneri), cache key inclusa - UI: upload .dxf, modalita' ROI poligonale su canvas (click=vertice, dblclick=chiudi, reset), bottone Esporta JSON dei risultati - .gitea/workflows/ci.yml: uv sync + ruff + pytest su push/PR Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+180
-1
@@ -20,6 +20,10 @@ const state = {
|
||||
model: null, scene: null, roi: null, drag: null,
|
||||
matches: [], annotatedImg: null,
|
||||
active_recipe: null, // V: ricetta caricata (string nome) o null
|
||||
// ROI poligonale: vertici [x, y] in coordinate immagine modello
|
||||
polyMode: false, polyPts: [], polyClosed: false,
|
||||
// Export JSON: ultimo match completo (params + risposta)
|
||||
lastMatch: null,
|
||||
};
|
||||
|
||||
// ---------- Forms ----------
|
||||
@@ -148,6 +152,15 @@ async function uploadToFolder(file) {
|
||||
return await r.json();
|
||||
}
|
||||
|
||||
async function uploadDxf(file) {
|
||||
// DXF: rasterizzato server-side in template grayscale (vedi pm2d/dxf.py)
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
const r = await fetch("/upload_dxf", { 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);
|
||||
@@ -222,6 +235,7 @@ async function onSelectModel(filename) {
|
||||
const img = await loadImage(`/image/${meta.id}/raw`);
|
||||
state.model = { id: meta.id, w: meta.width, h: meta.height, img };
|
||||
state.roi = null;
|
||||
state.polyPts = []; state.polyClosed = false; // B: scarta poligono stale
|
||||
document.getElementById("roi-info").textContent = "ROI: (nessuna)";
|
||||
setStatus(`Modello: ${filename} ${meta.width}x${meta.height} — trascina ROI`);
|
||||
renderModel();
|
||||
@@ -262,12 +276,36 @@ function renderModel() {
|
||||
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) {
|
||||
if (state.roi && !state.polyMode) {
|
||||
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);
|
||||
}
|
||||
// ROI poligonale: path aperto giallo, chiuso verde con fill semitrasparente
|
||||
if (state.polyMode && state.polyPts.length > 0) {
|
||||
ctx.beginPath();
|
||||
state.polyPts.forEach(([px, py], i) => {
|
||||
const cx = fit.ox + px * fit.sc;
|
||||
const cy = fit.oy + py * fit.sc;
|
||||
if (i === 0) ctx.moveTo(cx, cy); else ctx.lineTo(cx, cy);
|
||||
});
|
||||
if (state.polyClosed) {
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = "rgba(0, 255, 128, 0.18)";
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = "#00ff80";
|
||||
} else {
|
||||
ctx.strokeStyle = "#ffff00";
|
||||
}
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
// Vertici come quadratini
|
||||
ctx.fillStyle = state.polyClosed ? "#00ff80" : "#ffff00";
|
||||
for (const [px, py] of state.polyPts) {
|
||||
ctx.fillRect(fit.ox + px * fit.sc - 2, fit.oy + py * fit.sc - 2, 4, 4);
|
||||
}
|
||||
}
|
||||
if (state.drag) {
|
||||
ctx.strokeStyle = "#ffff00";
|
||||
ctx.setLineDash([4, 2]); ctx.lineWidth = 2;
|
||||
@@ -301,10 +339,35 @@ function setupROI() {
|
||||
const cnv = document.getElementById("c-model");
|
||||
cnv.addEventListener("mousedown", (e) => {
|
||||
if (!state.model) return;
|
||||
if (state.polyMode) return; // poly mode: gestito da click/dblclick
|
||||
const p = canvasPos(cnv, e);
|
||||
state.drag = { x0: p.x, y0: p.y, x1: p.x, y1: p.y };
|
||||
renderModel();
|
||||
});
|
||||
// ROI poligonale: click aggiunge vertice, doppio click chiude
|
||||
cnv.addEventListener("click", (e) => {
|
||||
if (!state.model || !state.polyMode || state.polyClosed) return;
|
||||
const m = state.model;
|
||||
const p = canvasPos(cnv, e);
|
||||
const ix = (p.x - m.ox) / m.scale;
|
||||
const iy = (p.y - m.oy) / m.scale;
|
||||
if (ix < 0 || iy < 0 || ix > m.w || iy > m.h) return; // fuori immagine
|
||||
const last = state.polyPts[state.polyPts.length - 1];
|
||||
// Dedup: il dblclick genera anche 2 click ravvicinati
|
||||
if (last && Math.hypot(ix - last[0], iy - last[1]) < 3) return;
|
||||
state.polyPts.push([
|
||||
Math.max(0, Math.min(Math.round(ix), m.w - 1)),
|
||||
Math.max(0, Math.min(Math.round(iy), m.h - 1)),
|
||||
]);
|
||||
document.getElementById("roi-info").textContent =
|
||||
`Poligono: ${state.polyPts.length} vertici (doppio click o "Chiudi" per chiudere)`;
|
||||
renderModel();
|
||||
});
|
||||
cnv.addEventListener("dblclick", (e) => {
|
||||
if (!state.polyMode) return;
|
||||
e.preventDefault();
|
||||
closePoly();
|
||||
});
|
||||
cnv.addEventListener("mousemove", (e) => {
|
||||
if (!state.drag) return;
|
||||
const p = canvasPos(cnv, e);
|
||||
@@ -331,6 +394,41 @@ function setupROI() {
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- ROI poligonale ----------
|
||||
function closePoly() {
|
||||
if (!state.polyMode || state.polyClosed) return;
|
||||
if (state.polyPts.length < 3) {
|
||||
setStatus("Servono almeno 3 vertici per chiudere il poligono");
|
||||
return;
|
||||
}
|
||||
state.polyClosed = true;
|
||||
// ROI = bounding box del poligono (il server riceve anche roi_poly)
|
||||
const xs = state.polyPts.map((p) => p[0]);
|
||||
const ys = state.polyPts.map((p) => p[1]);
|
||||
const x0 = Math.min(...xs), y0 = Math.min(...ys);
|
||||
const w = Math.max(...xs) - x0, h = Math.max(...ys) - y0;
|
||||
state.roi = [x0, y0, Math.max(1, w), Math.max(1, h)];
|
||||
document.getElementById("roi-info").textContent =
|
||||
`Poligono: ${state.polyPts.length} vertici, bbox ${w}x${h} @ (${x0}, ${y0})`;
|
||||
renderModel();
|
||||
}
|
||||
|
||||
function resetPoly() {
|
||||
state.polyPts = [];
|
||||
state.polyClosed = false;
|
||||
state.roi = null;
|
||||
document.getElementById("roi-info").textContent = state.polyMode
|
||||
? "Poligono: clicca sul modello per aggiungere vertici"
|
||||
: "ROI: (nessuna)";
|
||||
renderModel();
|
||||
}
|
||||
|
||||
function getRoiPoly() {
|
||||
// Poligono valido solo se in modalità poly e chiuso
|
||||
return (state.polyMode && state.polyClosed && state.polyPts.length >= 3)
|
||||
? state.polyPts : null;
|
||||
}
|
||||
|
||||
// ---------- Match action ----------
|
||||
async function doMatchRecipe() {
|
||||
if (!state.scene) { setStatus("Carica scena"); return; }
|
||||
@@ -352,6 +450,12 @@ async function doMatchRecipe() {
|
||||
if (!r.ok) { setStatus(`Errore: ${await r.text()}`); return; }
|
||||
const data = await r.json();
|
||||
state.matches = data.matches;
|
||||
// C: salva tutto per "Esporta JSON"
|
||||
state.lastMatch = {
|
||||
endpoint: "/match_recipe", params: body, response: data,
|
||||
image_id: state.scene.id,
|
||||
};
|
||||
document.getElementById("btn-export-json").disabled = false;
|
||||
state.annotatedImg = await loadImage(
|
||||
`/image/${data.annotated_id}/raw?t=${Date.now()}`);
|
||||
renderScene();
|
||||
@@ -371,7 +475,11 @@ async function doMatch() {
|
||||
}
|
||||
if (!state.model) { setStatus("Carica modello"); return; }
|
||||
if (!state.scene) { setStatus("Carica scena"); return; }
|
||||
if (state.polyMode && !state.polyClosed) {
|
||||
setStatus("Chiudi il poligono (doppio click o bottone Chiudi)"); return;
|
||||
}
|
||||
if (!state.roi) { setStatus("Seleziona ROI sul modello"); return; }
|
||||
const roiPoly = getRoiPoly();
|
||||
const user = readUserParams();
|
||||
const adv = readAdvancedOverrides();
|
||||
setStatus("Match in corso...");
|
||||
@@ -397,6 +505,7 @@ async function doMatch() {
|
||||
const angMax = SYM_MAP[user.simmetria] ?? 360;
|
||||
body = {
|
||||
model_id: state.model.id, scene_id: state.scene.id, roi: state.roi,
|
||||
roi_poly: roiPoly,
|
||||
angle_min: 0, angle_max: angMax,
|
||||
angle_step: PREC_MAP[user.precisione] ?? 5,
|
||||
scale_min: smin, scale_max: smax, scale_step: sstep,
|
||||
@@ -412,6 +521,7 @@ async function doMatch() {
|
||||
} else {
|
||||
body = {
|
||||
model_id: state.model.id, scene_id: state.scene.id, roi: state.roi,
|
||||
roi_poly: roiPoly,
|
||||
...user,
|
||||
};
|
||||
}
|
||||
@@ -426,6 +536,12 @@ async function doMatch() {
|
||||
}
|
||||
const data = await r.json();
|
||||
state.matches = data.matches;
|
||||
// C: salva tutto per "Esporta JSON"
|
||||
state.lastMatch = {
|
||||
endpoint: url, params: body, response: data,
|
||||
image_id: state.scene.id,
|
||||
};
|
||||
document.getElementById("btn-export-json").disabled = false;
|
||||
state.annotatedImg = await loadImage(
|
||||
`/image/${data.annotated_id}/raw?t=${Date.now()}`);
|
||||
renderScene();
|
||||
@@ -461,6 +577,38 @@ function setStatus(s) {
|
||||
document.getElementById("status").textContent = s;
|
||||
}
|
||||
|
||||
// ---------- C: Export JSON risultati ----------
|
||||
function exportMatchJSON() {
|
||||
if (!state.lastMatch) {
|
||||
alert("Nessun match da esportare: esegui prima un MATCH.");
|
||||
return;
|
||||
}
|
||||
const lm = state.lastMatch;
|
||||
const payload = {
|
||||
timestamp: new Date().toISOString(),
|
||||
image_id: lm.image_id,
|
||||
endpoint: lm.endpoint,
|
||||
params: lm.params,
|
||||
matches: lm.response.matches.map((m) => ({
|
||||
cx: m.cx, cy: m.cy, angle_deg: m.angle_deg,
|
||||
scale: m.scale, score: m.score, bbox: m.bbox_poly,
|
||||
})),
|
||||
train_time: lm.response.train_time,
|
||||
find_time: lm.response.find_time,
|
||||
num_variants: lm.response.num_variants,
|
||||
};
|
||||
const blob = new Blob([JSON.stringify(payload, null, 2)],
|
||||
{ type: "application/json" });
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
a.download = `pm2d_match_${ts}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(a.href);
|
||||
}
|
||||
|
||||
// ---------- Init ----------
|
||||
// ---------- Edge preview (clean rumore) ----------
|
||||
let _epDebounce = null;
|
||||
@@ -734,6 +882,7 @@ async function saveRecipe() {
|
||||
model_id: state.model.id,
|
||||
scene_id: state.scene?.id || state.model.id,
|
||||
roi: state.roi,
|
||||
roi_poly: getRoiPoly(),
|
||||
tipo: user.tipo,
|
||||
simmetria: user.simmetria,
|
||||
scala: user.scala,
|
||||
@@ -777,6 +926,24 @@ window.addEventListener("DOMContentLoaded", async () => {
|
||||
upEl.addEventListener("change", async (e) => {
|
||||
const f = e.target.files[0];
|
||||
if (!f) return;
|
||||
// A: file DXF → rasterizza server-side e usa direttamente come modello
|
||||
if (f.name.toLowerCase().endsWith(".dxf")) {
|
||||
setStatus(`Rasterizzazione DXF ${f.name}...`);
|
||||
try {
|
||||
const meta = await uploadDxf(f);
|
||||
const img = await loadImage(`/image/${meta.id}/raw`);
|
||||
state.model = { id: meta.id, w: meta.width, h: meta.height, img };
|
||||
state.roi = null;
|
||||
resetPoly();
|
||||
setStatus(`DXF ${f.name} rasterizzato ` +
|
||||
`${meta.width}x${meta.height} — disegna ROI sul modello`);
|
||||
renderModel();
|
||||
} catch (err) {
|
||||
setStatus(`Errore DXF: ${err.message}`);
|
||||
}
|
||||
e.target.value = "";
|
||||
return;
|
||||
}
|
||||
setStatus(`Caricamento ${f.name} nella cartella...`);
|
||||
try {
|
||||
const res = await uploadToFolder(f);
|
||||
@@ -789,6 +956,18 @@ window.addEventListener("DOMContentLoaded", async () => {
|
||||
});
|
||||
document.getElementById("btn-match").addEventListener("click", doMatch);
|
||||
document.getElementById("btn-autotune").addEventListener("click", doAutoTune);
|
||||
// B: ROI poligonale (toggle + chiudi + reset)
|
||||
document.getElementById("roi-poly-toggle").addEventListener("change", (e) => {
|
||||
state.polyMode = e.target.checked;
|
||||
document.getElementById("btn-poly-close").disabled = !state.polyMode;
|
||||
document.getElementById("btn-poly-reset").disabled = !state.polyMode;
|
||||
resetPoly();
|
||||
});
|
||||
document.getElementById("btn-poly-close").addEventListener("click", closePoly);
|
||||
document.getElementById("btn-poly-reset").addEventListener("click", resetPoly);
|
||||
// C: export JSON ultimo match
|
||||
document.getElementById("btn-export-json").addEventListener("click",
|
||||
exportMatchJSON);
|
||||
document.getElementById("btn-save-recipe").addEventListener("click",
|
||||
saveRecipe);
|
||||
document.getElementById("btn-load-recipe").addEventListener("click",
|
||||
|
||||
Reference in New Issue
Block a user