feat: selezione immagini da cartella IMAGES_DIR via .env
- .env con IMAGES_DIR=Test - server: _load_env legge .env senza dip extra - GET /images lista file, POST /load_from_folder carica per nome - frontend: file picker sostituiti con 2 select popolati all avvio Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ Endpoint:
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
@@ -21,6 +22,30 @@ from fastapi.responses import HTMLResponse, Response
|
|||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from pydantic import BaseModel
|
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.line_matcher import LineShapeMatcher, Match
|
||||||
from pm2d.auto_tune import auto_tune
|
from pm2d.auto_tune import auto_tune
|
||||||
|
|
||||||
@@ -259,6 +284,39 @@ def index():
|
|||||||
return HTMLResponse(html_path.read_text(encoding="utf-8"))
|
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)
|
@app.post("/upload", response_model=UploadResp)
|
||||||
async def upload(file: UploadFile = File(...)):
|
async def upload(file: UploadFile = File(...)):
|
||||||
data = await file.read()
|
data = await file.read()
|
||||||
|
|||||||
+57
-19
@@ -62,15 +62,35 @@ function readAdvancedOverrides() {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Upload ----------
|
// ---------- Image loading from folder ----------
|
||||||
async function uploadFile(file) {
|
async function loadFromFolder(filename) {
|
||||||
const fd = new FormData();
|
const r = await fetch("/load_from_folder", {
|
||||||
fd.append("file", file);
|
method: "POST",
|
||||||
const r = await fetch("/upload", { method: "POST", body: fd });
|
headers: { "Content-Type": "application/json" },
|
||||||
if (!r.ok) throw new Error("upload failed");
|
body: JSON.stringify({ filename }),
|
||||||
|
});
|
||||||
|
if (!r.ok) throw new Error(await r.text());
|
||||||
return await r.json();
|
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) {
|
function loadImage(src) {
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
const img = new Image();
|
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...");
|
setStatus("Caricamento modello...");
|
||||||
const meta = await uploadFile(file);
|
try {
|
||||||
|
const meta = await loadFromFolder(filename);
|
||||||
const img = await loadImage(`/image/${meta.id}/raw`);
|
const img = await loadImage(`/image/${meta.id}/raw`);
|
||||||
state.model = { id: meta.id, w: meta.width, h: meta.height, img };
|
state.model = { id: meta.id, w: meta.width, h: meta.height, img };
|
||||||
state.roi = null;
|
state.roi = null;
|
||||||
setStatus(`Modello: ${file.name} ${meta.width}x${meta.height} — trascina ROI`);
|
document.getElementById("roi-info").textContent = "ROI: (nessuna)";
|
||||||
|
setStatus(`Modello: ${filename} ${meta.width}x${meta.height} — trascina ROI`);
|
||||||
renderModel();
|
renderModel();
|
||||||
|
} catch (e) {
|
||||||
|
setStatus(`Errore modello: ${e.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onLoadScene(file) {
|
async function onSelectScene(filename) {
|
||||||
|
if (!filename) return;
|
||||||
setStatus("Caricamento scena...");
|
setStatus("Caricamento scena...");
|
||||||
const meta = await uploadFile(file);
|
try {
|
||||||
|
const meta = await loadFromFolder(filename);
|
||||||
const img = await loadImage(`/image/${meta.id}/raw`);
|
const img = await loadImage(`/image/${meta.id}/raw`);
|
||||||
state.scene = { id: meta.id, w: meta.width, h: meta.height, img };
|
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}`);
|
setStatus(`Scena: ${filename} ${meta.width}x${meta.height}`);
|
||||||
renderScene();
|
renderScene();
|
||||||
|
} catch (e) {
|
||||||
|
setStatus(`Errore scena: ${e.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Rendering ----------
|
// ---------- Rendering ----------
|
||||||
@@ -274,15 +305,22 @@ function setStatus(s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Init ----------
|
// ---------- Init ----------
|
||||||
window.addEventListener("DOMContentLoaded", () => {
|
window.addEventListener("DOMContentLoaded", async () => {
|
||||||
buildAdvancedForm();
|
buildAdvancedForm();
|
||||||
setupROI();
|
setupROI();
|
||||||
document.getElementById("file-model").addEventListener("change", (e) => {
|
// Popola dropdown immagini da IMAGES_DIR
|
||||||
if (e.target.files[0]) onLoadModel(e.target.files[0]);
|
const {files, dir} = await fetchImagesList();
|
||||||
});
|
const selM = document.getElementById("sel-model");
|
||||||
document.getElementById("file-scene").addEventListener("change", (e) => {
|
const selS = document.getElementById("sel-scene");
|
||||||
if (e.target.files[0]) onLoadScene(e.target.files[0]);
|
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);
|
document.getElementById("btn-match").addEventListener("click", doMatch);
|
||||||
const slider = document.getElementById("p-min-score");
|
const slider = document.getElementById("p-min-score");
|
||||||
slider.addEventListener("input", (e) => {
|
slider.addEventListener("input", (e) => {
|
||||||
|
|||||||
@@ -9,14 +9,16 @@
|
|||||||
<header>
|
<header>
|
||||||
<h1>Pattern Matching 2D</h1>
|
<h1>Pattern Matching 2D</h1>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<label class="btn">📂 Modello
|
<label class="tb-label">Modello:</label>
|
||||||
<input type="file" id="file-model" accept="image/*" hidden>
|
<select id="sel-model" class="tb-select">
|
||||||
</label>
|
<option value="">-- seleziona --</option>
|
||||||
<label class="btn">📂 Scena
|
</select>
|
||||||
<input type="file" id="file-scene" accept="image/*" hidden>
|
<label class="tb-label">Scena:</label>
|
||||||
</label>
|
<select id="sel-scene" class="tb-select">
|
||||||
|
<option value="">-- seleziona --</option>
|
||||||
|
</select>
|
||||||
<button class="btn btn-go" id="btn-match">▶ MATCH</button>
|
<button class="btn btn-go" id="btn-match">▶ MATCH</button>
|
||||||
<span id="status">Carica modello, disegna ROI, carica scena</span>
|
<span id="status">Seleziona modello, disegna ROI, seleziona scena</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ header h1 {
|
|||||||
background: #0a7d3a; border-color: #0d9c48; color: #fff; font-weight: bold;
|
background: #0a7d3a; border-color: #0d9c48; color: #fff; font-weight: bold;
|
||||||
}
|
}
|
||||||
.btn-go:hover { background: #0d9c48; }
|
.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 {
|
#status {
|
||||||
color: #00c8ff; margin-left: 12px; font-weight: 500;
|
color: #00c8ff; margin-left: 12px; font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user