feat: upload file nella cartella IMAGES_DIR
POST /upload_to_folder: sanitizza nome, valida estensione e contenuto via cv2.imdecode, auto-rename su collisione. Toolbar UI: bottone 'Carica file', dopo upload ricarica picker. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -329,6 +329,49 @@ def index():
|
|||||||
return HTMLResponse(html_path.read_text(encoding="utf-8"))
|
return HTMLResponse(html_path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/upload_to_folder")
|
||||||
|
async def upload_to_folder(file: UploadFile = File(...)):
|
||||||
|
"""Salva file caricato nella cartella IMAGES_DIR. Ritorna lista aggiornata."""
|
||||||
|
if not IMAGES_DIR.is_dir():
|
||||||
|
raise HTTPException(500, f"IMAGES_DIR non esiste: {IMAGES_DIR}")
|
||||||
|
# Sanitizza nome file (no traversal)
|
||||||
|
name = Path(file.filename or "upload.png").name
|
||||||
|
if not name:
|
||||||
|
raise HTTPException(400, "nome file vuoto")
|
||||||
|
ext = Path(name).suffix.lower()
|
||||||
|
allowed = {".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff"}
|
||||||
|
if ext not in allowed:
|
||||||
|
raise HTTPException(400, f"estensione non supportata: {ext}")
|
||||||
|
# Leggi contenuto e valida come immagine
|
||||||
|
data = await file.read()
|
||||||
|
arr = np.frombuffer(data, dtype=np.uint8)
|
||||||
|
img = cv2.imdecode(arr, cv2.IMREAD_COLOR)
|
||||||
|
if img is None:
|
||||||
|
raise HTTPException(400, "file non è un'immagine valida")
|
||||||
|
# Evita overwrite: se esiste, aggiungi suffisso numerico
|
||||||
|
target = IMAGES_DIR / name
|
||||||
|
if target.exists():
|
||||||
|
stem = target.stem; suffix = target.suffix
|
||||||
|
i = 1
|
||||||
|
while True:
|
||||||
|
alt = IMAGES_DIR / f"{stem}_{i}{suffix}"
|
||||||
|
if not alt.exists():
|
||||||
|
target = alt; break
|
||||||
|
i += 1
|
||||||
|
# Scrivi su disco
|
||||||
|
with open(target, "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
# Ritorna lista aggiornata
|
||||||
|
return {
|
||||||
|
"saved_as": target.name,
|
||||||
|
"dir": str(IMAGES_DIR),
|
||||||
|
"files": sorted(
|
||||||
|
p.name for p in IMAGES_DIR.iterdir()
|
||||||
|
if p.is_file() and p.suffix.lower() in allowed
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/folder_image/{filename}")
|
@app.get("/folder_image/{filename}")
|
||||||
def folder_image(filename: str, w: int = 120):
|
def folder_image(filename: str, w: int = 120):
|
||||||
"""Serve thumbnail PNG dell'immagine IMAGES_DIR (scalata a width w)."""
|
"""Serve thumbnail PNG dell'immagine IMAGES_DIR (scalata a width w)."""
|
||||||
|
|||||||
+34
-5
@@ -82,6 +82,21 @@ async function fetchImagesList() {
|
|||||||
return await r.json();
|
return await r.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function uploadToFolder(file) {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("file", file);
|
||||||
|
const r = await fetch("/upload_to_folder", { 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);
|
||||||
|
buildThumbPicker("picker-scene", files, onSelectScene);
|
||||||
|
return {files, dir};
|
||||||
|
}
|
||||||
|
|
||||||
function buildThumbPicker(pickerId, files, onSelect) {
|
function buildThumbPicker(pickerId, files, onSelect) {
|
||||||
const picker = document.getElementById(pickerId);
|
const picker = document.getElementById(pickerId);
|
||||||
const current = picker.querySelector(".picker-current");
|
const current = picker.querySelector(".picker-current");
|
||||||
@@ -351,14 +366,28 @@ window.addEventListener("DOMContentLoaded", async () => {
|
|||||||
buildAdvancedForm();
|
buildAdvancedForm();
|
||||||
setupROI();
|
setupROI();
|
||||||
// Popola picker immagini da IMAGES_DIR (con thumbnail)
|
// Popola picker immagini da IMAGES_DIR (con thumbnail)
|
||||||
const {files, dir} = await fetchImagesList();
|
const {files, dir} = await refreshPickers();
|
||||||
buildThumbPicker("picker-model", files, onSelectModel);
|
|
||||||
buildThumbPicker("picker-scene", files, onSelectScene);
|
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
setStatus(`Nessuna immagine in ${dir} (configura IMAGES_DIR in .env)`);
|
setStatus(`Nessuna immagine in ${dir} (carica file o configura IMAGES_DIR)`);
|
||||||
} else {
|
} else {
|
||||||
setStatus(`${files.length} immagini disponibili in ${dir}`);
|
setStatus(`${files.length} immagini in ${dir}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upload file nella folder
|
||||||
|
const upEl = document.getElementById("file-upload");
|
||||||
|
upEl.addEventListener("change", async (e) => {
|
||||||
|
const f = e.target.files[0];
|
||||||
|
if (!f) return;
|
||||||
|
setStatus(`Caricamento ${f.name} nella cartella...`);
|
||||||
|
try {
|
||||||
|
const res = await uploadToFolder(f);
|
||||||
|
await refreshPickers();
|
||||||
|
setStatus(`Salvato come ${res.saved_as} (${res.files.length} file totali)`);
|
||||||
|
} catch (err) {
|
||||||
|
setStatus(`Errore upload: ${err.message}`);
|
||||||
|
}
|
||||||
|
e.target.value = ""; // consente re-upload stesso file
|
||||||
|
});
|
||||||
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) => {
|
||||||
|
|||||||
@@ -26,6 +26,10 @@
|
|||||||
<div class="picker-list"></div>
|
<div class="picker-list"></div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-go" id="btn-match">▶ MATCH</button>
|
<button class="btn btn-go" id="btn-match">▶ MATCH</button>
|
||||||
|
<label class="btn" title="Carica nuovo file nella cartella immagini">
|
||||||
|
⬆ Carica file
|
||||||
|
<input type="file" id="file-upload" accept="image/*" hidden>
|
||||||
|
</label>
|
||||||
<span id="status">Seleziona modello, disegna ROI, seleziona scena</span>
|
<span id="status">Seleziona modello, disegna ROI, seleziona scena</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
Reference in New Issue
Block a user