diff --git a/pm2d/web/server.py b/pm2d/web/server.py index 893ada4..723bb02 100644 --- a/pm2d/web/server.py +++ b/pm2d/web/server.py @@ -9,6 +9,7 @@ Endpoint: """ from __future__ import annotations +import os import tempfile import time import uuid @@ -21,6 +22,30 @@ from fastapi.responses import HTMLResponse, Response from fastapi.staticfiles import StaticFiles from pydantic import BaseModel + +def _load_env(root: Path) -> None: + """Legge .env in root e popola os.environ (no override se già set).""" + f = root / ".env" + if not f.exists(): + return + for line in f.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + k, v = line.split("=", 1) + k = k.strip(); v = v.strip().strip('"').strip("'") + os.environ.setdefault(k, v) + + +# Root progetto (parent di pm2d/) +PROJECT_ROOT = Path(__file__).resolve().parents[2] +_load_env(PROJECT_ROOT) + +_images_dir_raw = os.environ.get("IMAGES_DIR", "Test") +IMAGES_DIR = Path(_images_dir_raw) +if not IMAGES_DIR.is_absolute(): + IMAGES_DIR = PROJECT_ROOT / IMAGES_DIR + from pm2d.line_matcher import LineShapeMatcher, Match from pm2d.auto_tune import auto_tune @@ -259,6 +284,39 @@ def index(): return HTMLResponse(html_path.read_text(encoding="utf-8")) +@app.get("/images") +def list_images(): + """Lista file immagine nella cartella configurata in IMAGES_DIR.""" + if not IMAGES_DIR.is_dir(): + return {"dir": str(IMAGES_DIR), "files": []} + exts = {".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff"} + files = sorted( + p.name for p in IMAGES_DIR.iterdir() + if p.is_file() and p.suffix.lower() in exts + ) + return {"dir": str(IMAGES_DIR), "files": files} + + +class LoadFolderReq(BaseModel): + filename: str + + +@app.post("/load_from_folder", response_model=UploadResp) +def load_from_folder(req: LoadFolderReq): + """Carica immagine dalla cartella IMAGES_DIR per nome file.""" + name = req.filename + if "/" in name or ".." in name: + raise HTTPException(400, "nome file non valido") + path = IMAGES_DIR / name + if not path.is_file(): + raise HTTPException(404, f"file non trovato: {name}") + img = cv2.imread(str(path), cv2.IMREAD_COLOR) + if img is None: + raise HTTPException(400, "immagine non leggibile") + iid = _store_image(img) + return UploadResp(id=iid, width=img.shape[1], height=img.shape[0]) + + @app.post("/upload", response_model=UploadResp) async def upload(file: UploadFile = File(...)): data = await file.read() diff --git a/pm2d/web/static/app.js b/pm2d/web/static/app.js index 3b2b06b..f170ca0 100644 --- a/pm2d/web/static/app.js +++ b/pm2d/web/static/app.js @@ -62,15 +62,35 @@ function readAdvancedOverrides() { return out; } -// ---------- Upload ---------- -async function uploadFile(file) { - const fd = new FormData(); - fd.append("file", file); - const r = await fetch("/upload", { method: "POST", body: fd }); - if (!r.ok) throw new Error("upload failed"); +// ---------- 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 populateSelect(selectEl, files) { + selectEl.innerHTML = ""; + const opt0 = document.createElement("option"); + opt0.value = ""; opt0.textContent = "-- seleziona --"; + selectEl.appendChild(opt0); + for (const f of files) { + const o = document.createElement("option"); + o.value = f; o.textContent = f; + selectEl.appendChild(o); + } +} + function loadImage(src) { return new Promise((res, rej) => { const img = new Image(); @@ -80,24 +100,35 @@ function loadImage(src) { }); } -async function onLoadModel(file) { +async function onSelectModel(filename) { + if (!filename) return; setStatus("Caricamento modello..."); - const meta = await uploadFile(file); - const img = await loadImage(`/image/${meta.id}/raw`); - state.model = { id: meta.id, w: meta.width, h: meta.height, img }; - state.roi = null; - setStatus(`Modello: ${file.name} ${meta.width}x${meta.height} — trascina ROI`); - renderModel(); + 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 onLoadScene(file) { +async function onSelectScene(filename) { + if (!filename) return; setStatus("Caricamento scena..."); - 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; - setStatus(`Scena: ${file.name} ${meta.width}x${meta.height}`); - renderScene(); + 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 ---------- @@ -274,15 +305,22 @@ function setStatus(s) { } // ---------- Init ---------- -window.addEventListener("DOMContentLoaded", () => { +window.addEventListener("DOMContentLoaded", async () => { buildAdvancedForm(); setupROI(); - document.getElementById("file-model").addEventListener("change", (e) => { - if (e.target.files[0]) onLoadModel(e.target.files[0]); - }); - document.getElementById("file-scene").addEventListener("change", (e) => { - if (e.target.files[0]) onLoadScene(e.target.files[0]); - }); + // Popola dropdown immagini da IMAGES_DIR + const {files, dir} = await fetchImagesList(); + const selM = document.getElementById("sel-model"); + const selS = document.getElementById("sel-scene"); + populateSelect(selM, files); + populateSelect(selS, files); + if (files.length === 0) { + setStatus(`Nessuna immagine in ${dir} (configura IMAGES_DIR in .env)`); + } else { + setStatus(`${files.length} immagini disponibili in ${dir}`); + } + selM.addEventListener("change", (e) => onSelectModel(e.target.value)); + selS.addEventListener("change", (e) => onSelectScene(e.target.value)); document.getElementById("btn-match").addEventListener("click", doMatch); const slider = document.getElementById("p-min-score"); slider.addEventListener("input", (e) => { diff --git a/pm2d/web/static/index.html b/pm2d/web/static/index.html index 364a4ef..f4c6547 100644 --- a/pm2d/web/static/index.html +++ b/pm2d/web/static/index.html @@ -9,14 +9,16 @@

Pattern Matching 2D

- - + + + + - Carica modello, disegna ROI, carica scena + Seleziona modello, disegna ROI, seleziona scena
diff --git a/pm2d/web/static/style.css b/pm2d/web/static/style.css index 41f5df1..7dba55d 100644 --- a/pm2d/web/static/style.css +++ b/pm2d/web/static/style.css @@ -26,6 +26,13 @@ header h1 { background: #0a7d3a; border-color: #0d9c48; color: #fff; font-weight: bold; } .btn-go:hover { background: #0d9c48; } +.tb-label { color: #b0b0b0; font-size: 12px; margin-left: 8px; } +.tb-select { + background: #2a2a2a; color: #dcdcdc; border: 1px solid #444; + padding: 5px 8px; border-radius: 3px; font-size: 13px; + min-width: 160px; +} +.tb-select:focus { outline: 1px solid #00c8ff; } #status { color: #00c8ff; margin-left: 12px; font-weight: 500; }