acquisitore: immagine viva in Calibrazione e «Blocca immagine» per il calcolo (Refs #94)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -71,7 +71,7 @@ PAGINA = r"""<!doctype html>
|
|||||||
.mirino { position: absolute; inset: 0; pointer-events: none; /* #92: classe, vale per i due riquadri */
|
.mirino { position: absolute; inset: 0; pointer-events: none; /* #92: classe, vale per i due riquadri */
|
||||||
background: linear-gradient(#fbbf24, #fbbf24) center / 2px 100% no-repeat,
|
background: linear-gradient(#fbbf24, #fbbf24) center / 2px 100% no-repeat,
|
||||||
linear-gradient(#fbbf24, #fbbf24) center / 100% 2px no-repeat; opacity: .85; }
|
linear-gradient(#fbbf24, #fbbf24) center / 100% 2px no-repeat; opacity: .85; }
|
||||||
#vuoto, #vuotoCal { padding: 30px 16px; color: var(--secondario); text-align: center; }
|
#vuoto, #vuotoCal, #vuotoCalTab { padding: 30px 16px; color: var(--secondario); text-align: center; }
|
||||||
/* #92: due riquadri affiancati (grezza | calibrata); in colonna sotto i 900 px */
|
/* #92: due riquadri affiancati (grezza | calibrata); in colonna sotto i 900 px */
|
||||||
.riquadri { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; margin-top: 10px; }
|
.riquadri { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; margin-top: 10px; }
|
||||||
.riquadri.doppio { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
|
.riquadri.doppio { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
|
||||||
@@ -232,6 +232,18 @@ PAGINA = r"""<!doctype html>
|
|||||||
<section class="scheda" id="sezioneCalibrazione">
|
<section class="scheda" id="sezioneCalibrazione">
|
||||||
<h2>Calibrazione</h2>
|
<h2>Calibrazione</h2>
|
||||||
<div class="stato" style="margin:0 0 10px">Sorgente: <strong id="calSorgente">-</strong> — immagine grezza dalla tab Acquisizione</div>
|
<div class="stato" style="margin:0 0 10px">Sorgente: <strong id="calSorgente">-</strong> — immagine grezza dalla tab Acquisizione</div>
|
||||||
|
<!-- #94: immagine viva (stesso frame dell'anteprima) e «Blocca immagine» per calcolare su quella -->
|
||||||
|
<div class="palco" style="margin-top:0">
|
||||||
|
<div id="vuotoCalTab">Nessuna immagine: «Scatta» o avviare l'anteprima nella tab Acquisizione</div>
|
||||||
|
<div class="cornice" id="corniceCalTab" hidden>
|
||||||
|
<img id="immagineCalTab" alt="Immagine grezza della sorgente">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="riga" style="margin-top:8px">
|
||||||
|
<button id="scattaCal" class="secondario" type="button">Scatta</button>
|
||||||
|
<label style="margin:0 0 6px"><input id="bloccaImg" type="checkbox"> Blocca immagine</label>
|
||||||
|
</div>
|
||||||
|
<div id="msgBlocco" class="messaggio"></div>
|
||||||
<div class="riga" style="margin-top:10px">
|
<div class="riga" style="margin-top:10px">
|
||||||
<div><label for="quadrato">Lato dello scacco (mm)</label><input id="quadrato" type="text" inputmode="decimal" value="2" size="8"></div>
|
<div><label for="quadrato">Lato dello scacco (mm)</label><input id="quadrato" type="text" inputmode="decimal" value="2" size="8"></div>
|
||||||
<button id="acquisisci" title="Acquisisci e calibra">Calcola</button>
|
<button id="acquisisci" title="Acquisisci e calibra">Calcola</button>
|
||||||
@@ -239,7 +251,7 @@ PAGINA = r"""<!doctype html>
|
|||||||
<p class="nota">
|
<p class="nota">
|
||||||
Servono una scacchiera che riempie tutto il campo della camera, piana, ferma e ben illuminata, e il lato di uno scacco.
|
Servono una scacchiera che riempie tutto il campo della camera, piana, ferma e ben illuminata, e il lato di uno scacco.
|
||||||
La griglia si trova da sola; il risultato corregge anche la distorsione dell'ottica.
|
La griglia si trova da sola; il risultato corregge anche la distorsione dell'ottica.
|
||||||
Il frame è sempre quello grezzo della sorgente; l'anteprima resta sospesa finché questa tab è aperta.</p>
|
Il frame è sempre quello grezzo della sorgente; l'anteprima continua anche qui e si può bloccare l'immagine per calcolare su quella.</p>
|
||||||
<div id="messaggio" class="messaggio"></div>
|
<div id="messaggio" class="messaggio"></div>
|
||||||
|
|
||||||
<div id="risultato" hidden style="margin-top:10px">
|
<div id="risultato" hidden style="margin-top:10px">
|
||||||
@@ -431,6 +443,7 @@ function cambioSorgente(salva) {
|
|||||||
if (salva) memoria("tmf.acq.sorgente", $("sorgente").value);
|
if (salva) memoria("tmf.acq.sorgente", $("sorgente").value);
|
||||||
var era = A.attiva;
|
var era = A.attiva;
|
||||||
fermaAnteprima();
|
fermaAnteprima();
|
||||||
|
if (A.bloccata) rilasciaBlocco(); // #94: casella spenta e DELETE sulla sorgente vecchia (prima di svuotare i riquadri)
|
||||||
azzeraIndicatori();
|
azzeraIndicatori();
|
||||||
svuotaRiquadri(); // #92: il frame vecchio non e' della nuova sorgente
|
svuotaRiquadri(); // #92: il frame vecchio non e' della nuova sorgente
|
||||||
A.calAttiva = null; A.bloccoCal = false; // #92: stato calibrazione della nuova sorgente ancora da leggere
|
A.calAttiva = null; A.bloccoCal = false; // #92: stato calibrazione della nuova sorgente ancora da leggere
|
||||||
@@ -445,8 +458,8 @@ function cambioSorgente(salva) {
|
|||||||
|
|
||||||
// ---- Anteprima (#87)
|
// ---- Anteprima (#87)
|
||||||
|
|
||||||
// #91: ris = {w, h} dell'ultimo frame; #92: calAttiva = la sorgente ha una calibrazione attiva (null = non nota), bloccoCal = un 409 ha fermato le richieste calibrata; urlCal/seqCal = riquadro calibrata, riprendi = ciclo sospeso dalla tab Calibrazione, lato = ultimo lato_max
|
// #91: ris = {w, h} dell'ultimo frame; #92: calAttiva = la sorgente ha una calibrazione attiva (null = non nota), bloccoCal = un 409 ha fermato le richieste calibrata; urlCal/seqCal = riquadro calibrata, bloccata/sorgenteBloccata/urlBlocco/seqBlocco = #94 immagine bloccata per il calcolo (il riquadro della tab Calibrazione non segue piu' il ciclo), lato = ultimo lato_max
|
||||||
var A = { attiva: false, gen: 0, inVolo: false, url: null, urlCal: null, seqCal: 0, riprendi: false, lato: 1280, calAttiva: null, bloccoCal: false, nitMax: 0, ris: null };
|
var A = { attiva: false, gen: 0, inVolo: false, url: null, urlCal: null, seqCal: 0, bloccata: false, sorgenteBloccata: null, urlBlocco: null, seqBlocco: 0, lato: 1280, calAttiva: null, bloccoCal: false, nitMax: 0, ris: null };
|
||||||
|
|
||||||
function azzeraIndicatori() {
|
function azzeraIndicatori() {
|
||||||
A.nitMax = 0;
|
A.nitMax = 0;
|
||||||
@@ -467,9 +480,17 @@ function mostraImmagine(blob) {
|
|||||||
var vecchio = A.url;
|
var vecchio = A.url;
|
||||||
A.url = URL.createObjectURL(blob);
|
A.url = URL.createObjectURL(blob);
|
||||||
$("immagine").src = A.url;
|
$("immagine").src = A.url;
|
||||||
if (vecchio) URL.revokeObjectURL(vecchio); // niente memoria che cresce a ogni frame
|
if (vecchio && vecchio !== A.urlBlocco) URL.revokeObjectURL(vecchio); // niente memoria che cresce a ogni frame; #94: non quello bloccato
|
||||||
$("cornice").hidden = false;
|
$("cornice").hidden = false;
|
||||||
$("vuoto").hidden = true;
|
$("vuoto").hidden = true;
|
||||||
|
if (!A.bloccata) mostraInTabCal(A.url); // #94: lo stesso blob URL va anche nella tab Calibrazione, salvo immagine bloccata
|
||||||
|
}
|
||||||
|
|
||||||
|
// #94: riquadro «immagine viva» della tab Calibrazione (url null = vuoto)
|
||||||
|
function mostraInTabCal(url) {
|
||||||
|
$("immagineCalTab").src = url || "";
|
||||||
|
$("corniceCalTab").hidden = !url;
|
||||||
|
$("vuotoCalTab").hidden = !!url;
|
||||||
}
|
}
|
||||||
|
|
||||||
// #92: riquadro «Calibrata»: l'immagine raddrizzata oppure il motivo per cui non c'e' (404/409 del servizio)
|
// #92: riquadro «Calibrata»: l'immagine raddrizzata oppure il motivo per cui non c'e' (404/409 del servizio)
|
||||||
@@ -493,6 +514,7 @@ function svuotaRiquadri() {
|
|||||||
A.seqCal++; // una risposta calibrata ancora in volo non vale piu'
|
A.seqCal++; // una risposta calibrata ancora in volo non vale piu'
|
||||||
$("cornice").hidden = true;
|
$("cornice").hidden = true;
|
||||||
$("vuoto").hidden = false;
|
$("vuoto").hidden = false;
|
||||||
|
mostraInTabCal(null); // #94: svuotato anche il riquadro della tab Calibrazione
|
||||||
messaggioCalibrata("Premere «Avvia anteprima» o «Scatta».");
|
messaggioCalibrata("Premere «Avvia anteprima» o «Scatta».");
|
||||||
}
|
}
|
||||||
// #92: raddrizza l'ULTIMO frame grezzo tenuto dal servizio (nessuna lettura nuova); chiamata dopo ogni anteprima, dopo Carica/Scollega e alla casella
|
// #92: raddrizza l'ULTIMO frame grezzo tenuto dal servizio (nessuna lettura nuova); chiamata dopo ogni anteprima, dopo Carica/Scollega e alla casella
|
||||||
@@ -615,16 +637,64 @@ function fermaAnteprima() {
|
|||||||
// «Scatta»: ferma il ciclo e mostra un frame solo, alla risoluzione piena (fino a 4096 px)
|
// «Scatta»: ferma il ciclo e mostra un frame solo, alla risoluzione piena (fino a 4096 px)
|
||||||
async function scatta() {
|
async function scatta() {
|
||||||
fermaAnteprima();
|
fermaAnteprima();
|
||||||
$("scatta").disabled = true;
|
$("scatta").disabled = $("scattaCal").disabled = true; // #94: anche il «Scatta» della tab Calibrazione
|
||||||
dire("msgAnteprima", "Scatto in corso...", "");
|
dire("msgAnteprima", "Scatto in corso...", "");
|
||||||
while (A.inVolo) await pausa(50);
|
while (A.inVolo) await pausa(50);
|
||||||
await unaAnteprima(4096);
|
await unaAnteprima(4096);
|
||||||
$("scatta").disabled = false;
|
$("scatta").disabled = false;
|
||||||
|
$("scattaCal").disabled = A.bloccata; // #94: con l'immagine bloccata resta spento
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #94: «Blocca immagine»: il servizio tiene una copia del frame grezzo e «Calcola» usa quella
|
||||||
|
function sbloccaLocale() { // casella spenta, il riquadro della tab Calibrazione torna a seguire il ciclo
|
||||||
|
A.bloccata = false;
|
||||||
|
A.sorgenteBloccata = null;
|
||||||
|
A.seqBlocco++; // una risposta di blocco ancora in volo non vale piu'
|
||||||
|
var b = A.urlBlocco;
|
||||||
|
A.urlBlocco = null;
|
||||||
|
if (b && b !== A.url) URL.revokeObjectURL(b);
|
||||||
|
$("bloccaImg").checked = false;
|
||||||
|
$("scattaCal").disabled = $("scatta").disabled;
|
||||||
|
mostraInTabCal($("cornice").hidden ? null : A.url);
|
||||||
|
}
|
||||||
|
function rilasciaBlocco() { // DELETE idempotente: l'esito non conta
|
||||||
|
var s = A.sorgenteBloccata;
|
||||||
|
sbloccaLocale();
|
||||||
|
dire("msgBlocco", "");
|
||||||
|
if (s) fetch("/camera/blocca?source=" + encodeURIComponent(s), { method: "DELETE" }).catch(function () { /* niente da fare */ });
|
||||||
|
}
|
||||||
|
$("bloccaImg").onchange = async function () {
|
||||||
|
if (!this.checked) { rilasciaBlocco(); return; }
|
||||||
|
var sorgente = $("sorgente").value;
|
||||||
|
if (!sorgente || $("cornice").hidden) {
|
||||||
|
$("bloccaImg").checked = false;
|
||||||
|
dire("msgBlocco", "Nessuna immagine da bloccare: «Scatta» o avviare l'anteprima.", "errore");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
A.bloccata = true; // subito: il riquadro smette di seguire il ciclo
|
||||||
|
A.sorgenteBloccata = sorgente;
|
||||||
|
A.urlBlocco = A.url;
|
||||||
|
var n = ++A.seqBlocco;
|
||||||
|
$("scattaCal").disabled = true;
|
||||||
|
dire("msgBlocco", "Blocco in corso...", "");
|
||||||
|
try {
|
||||||
|
var r = await fetch("/camera/blocca", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ source: sorgente }) });
|
||||||
|
if (n !== A.seqBlocco) { // spenta o cambiata sorgente nel frattempo: la copia appena fatta si toglie
|
||||||
|
if (r.ok) fetch("/camera/blocca?source=" + encodeURIComponent(sorgente), { method: "DELETE" }).catch(function () { /* niente da fare */ });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!r.ok) { var d = await dettaglio(r); sbloccaLocale(); dire("msgBlocco", d, "errore"); return; }
|
||||||
|
var b = await r.json();
|
||||||
|
dire("msgBlocco", "Immagine bloccata " + b.larghezza + "×" + b.altezza + ": «Calcola» usa questa.", "ok");
|
||||||
|
} catch (e) {
|
||||||
|
if (n === A.seqBlocco) { sbloccaLocale(); dire("msgBlocco", "Servizio non raggiungibile.", "errore"); }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
$("avvia").onclick = avviaAnteprima;
|
$("avvia").onclick = avviaAnteprima;
|
||||||
$("ferma").onclick = fermaAnteprima;
|
$("ferma").onclick = fermaAnteprima;
|
||||||
$("scatta").onclick = scatta;
|
$("scatta").onclick = scatta;
|
||||||
|
$("scattaCal").onclick = scatta; // #94
|
||||||
$("azzeraMax").onclick = function () { A.nitMax = 0; $("nitidezzaMax").textContent = "-"; $("barraNitidezza").style.width = "0"; };
|
$("azzeraMax").onclick = function () { A.nitMax = 0; $("nitidezzaMax").textContent = "-"; $("barraNitidezza").style.width = "0"; };
|
||||||
// #92: il mirino vale per entrambi i riquadri; #93: la casella «Attiva calibrazione» apre il riquadro e raddrizza il frame gia' tenuto
|
// #92: il mirino vale per entrambi i riquadri; #93: la casella «Attiva calibrazione» apre il riquadro e raddrizza il frame gia' tenuto
|
||||||
$("mirinoCasella").onchange = function () { $("mirino").hidden = $("mirinoCal").hidden = !$("mirinoCasella").checked; };
|
$("mirinoCasella").onchange = function () { $("mirino").hidden = $("mirinoCal").hidden = !$("mirinoCasella").checked; };
|
||||||
@@ -863,9 +933,7 @@ function scegliTab(nome, salva) {
|
|||||||
$("tabCal").tabIndex = cal ? 0 : -1;
|
$("tabCal").tabIndex = cal ? 0 : -1;
|
||||||
$("pannelloAcq").hidden = cal;
|
$("pannelloAcq").hidden = cal;
|
||||||
$("pannelloCal").hidden = !cal;
|
$("pannelloCal").hidden = !cal;
|
||||||
// #92: nella tab Calibrazione il ciclo anteprima si sospende; tornando in Acquisizione riprende se era attivo
|
// #94: il ciclo anteprima non si sospende piu' aprendo la tab Calibrazione (come faceva #92)
|
||||||
if (cal && A.attiva) { A.riprendi = true; fermaAnteprima(); }
|
|
||||||
if (!cal && A.riprendi) { A.riprendi = false; avviaAnteprima(); }
|
|
||||||
if (!cal && salva) aggiornaCalibrata(); // il riquadro calibrata riflette la calibrazione attiva di adesso
|
if (!cal && salva) aggiornaCalibrata(); // il riquadro calibrata riflette la calibrazione attiva di adesso
|
||||||
if (salva) { memoria("tmf.acq.tab", nome); if (cal) caricaCalibrazioni(); } // l'elenco si rilegge entrando
|
if (salva) { memoria("tmf.acq.tab", nome); if (cal) caricaCalibrazioni(); } // l'elenco si rilegge entrando
|
||||||
}
|
}
|
||||||
@@ -1169,11 +1237,13 @@ async function calcola() {
|
|||||||
var f = new FormData();
|
var f = new FormData();
|
||||||
f.append("square_mm", String(lato)); // al servizio sempre con il punto
|
f.append("square_mm", String(lato)); // al servizio sempre con il punto
|
||||||
f.append("source", sorgente);
|
f.append("source", sorgente);
|
||||||
fermaAnteprima(); // #92: in questa tab il ciclo e' gia' sospeso; qui si aspetta anche l'eventuale richiesta ancora in volo
|
var bloccata = A.bloccata; // #94: immagine bloccata = calcolo sulla copia del servizio, l'anteprima puo' continuare
|
||||||
|
if (bloccata) f.append("bloccata", "true");
|
||||||
|
else fermaAnteprima(); // #92: si ferma il ciclo; sotto si aspetta anche l'eventuale richiesta ancora in volo
|
||||||
messaggio("Calcolo in corso...", "");
|
messaggio("Calcolo in corso...", "");
|
||||||
["acquisisci", "salva"].forEach(function (i) { $(i).disabled = true; });
|
["acquisisci", "salva"].forEach(function (i) { $(i).disabled = true; });
|
||||||
try {
|
try {
|
||||||
while (A.inVolo) await pausa(50);
|
while (!bloccata && A.inVolo) await pausa(50);
|
||||||
var r = await fetch("/camera/calibrate", { method: "POST", body: f });
|
var r = await fetch("/camera/calibrate", { method: "POST", body: f });
|
||||||
if (!r.ok) { messaggio(await dettaglio(r), "errore"); return; }
|
if (!r.ok) { messaggio(await dettaglio(r), "errore"); return; }
|
||||||
var d = await r.json();
|
var d = await r.json();
|
||||||
@@ -1181,7 +1251,7 @@ async function calcola() {
|
|||||||
tabella($("tabella"), d);
|
tabella($("tabella"), d);
|
||||||
$("overlay").src = "data:image/png;base64," + d.overlay_png;
|
$("overlay").src = "data:image/png;base64," + d.overlay_png;
|
||||||
$("avviso").textContent = avvisi(d);
|
$("avviso").textContent = avvisi(d);
|
||||||
$("origineCalcolo").textContent = "Origine: " + descriviOrigine(d.origine);
|
$("origineCalcolo").textContent = "Origine: " + (d.bloccata ? "immagine bloccata — " : "") + descriviOrigine(d.origine); // #94
|
||||||
$("risultato").hidden = false;
|
$("risultato").hidden = false;
|
||||||
annullaSovrascrivi();
|
annullaSovrascrivi();
|
||||||
dire("msgSalva", "");
|
dire("msgSalva", "");
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ def create_app(folder: Path | str | None = None,
|
|||||||
else _config_dir_default()) # #83
|
else _config_dir_default()) # #83
|
||||||
aperte: dict = {}
|
aperte: dict = {}
|
||||||
grezzi: dict = {} # #92: sorgente -> ultimo frame pieno GREZZO dell'anteprima (per anteprima/calibrata)
|
grezzi: dict = {} # #92: sorgente -> ultimo frame pieno GREZZO dell'anteprima (per anteprima/calibrata)
|
||||||
|
bloccati: dict = {} # #94: sorgente -> copia del frame grezzo pieno «bloccata» per il calcolo (stesso lock di grezzi)
|
||||||
cartella_immagini = archivio.cartella / "immagini" # #92: immagini caricate dalla pagina (creata al bisogno)
|
cartella_immagini = archivio.cartella / "immagini" # #92: immagini caricate dalla pagina (creata al bisogno)
|
||||||
lock = threading.Lock() # protegge SOLO i dizionari, mai un'apertura
|
lock = threading.Lock() # protegge SOLO i dizionari, mai un'apertura
|
||||||
lock_sorgenti: dict = {} # nome sorgente -> Lock che serializza apertura e grab
|
lock_sorgenti: dict = {} # nome sorgente -> Lock che serializza apertura e grab
|
||||||
@@ -223,6 +224,7 @@ def create_app(folder: Path | str | None = None,
|
|||||||
with lock:
|
with lock:
|
||||||
s = aperte.pop(nome, None)
|
s = aperte.pop(nome, None)
|
||||||
grezzi.pop(nome, None)
|
grezzi.pop(nome, None)
|
||||||
|
bloccati.pop(nome, None) # #94: tolta l'immagine/chiusa la sorgente, anche la copia bloccata
|
||||||
if s is not None:
|
if s is not None:
|
||||||
try:
|
try:
|
||||||
s.close()
|
s.close()
|
||||||
@@ -368,6 +370,7 @@ def create_app(folder: Path | str | None = None,
|
|||||||
rows: int | None = Form(None),
|
rows: int | None = Form(None),
|
||||||
image: UploadFile | None = File(None),
|
image: UploadFile | None = File(None),
|
||||||
save: bool = Form(False), # #91: ignorato, il calcolo non salva mai (si salva con nome)
|
save: bool = Form(False), # #91: ignorato, il calcolo non salva mai (si salva con nome)
|
||||||
|
bloccata: bool = Form(False), # #94: true = calcola sulla copia bloccata, senza grab
|
||||||
):
|
):
|
||||||
contenuto = await image.read() if image is not None else b""
|
contenuto = await image.read() if image is not None else b""
|
||||||
if contenuto:
|
if contenuto:
|
||||||
@@ -375,6 +378,13 @@ def create_app(folder: Path | str | None = None,
|
|||||||
if frame is None:
|
if frame is None:
|
||||||
raise HTTPException(422, "l'immagine caricata non è leggibile")
|
raise HTTPException(422, "l'immagine caricata non è leggibile")
|
||||||
origine = "file:" + (Path((image.filename or "").replace("\\", "/")).name or "immagine")
|
origine = "file:" + (Path((image.filename or "").replace("\\", "/")).name or "immagine")
|
||||||
|
elif bloccata: # #94: copia bloccata, nessun grab e nessun lock camera
|
||||||
|
nome = _sorgente(source)
|
||||||
|
with lock:
|
||||||
|
frame = bloccati.get(nome)
|
||||||
|
if frame is None:
|
||||||
|
raise HTTPException(409, "nessuna immagine bloccata su questa sorgente")
|
||||||
|
origine = nome
|
||||||
else:
|
else:
|
||||||
nome = _sorgente(source)
|
nome = _sorgente(source)
|
||||||
frame = await run_in_threadpool(_grab, nome, 5000) # come /camera/grab (404/503/504)
|
frame = await run_in_threadpool(_grab, nome, 5000) # come /camera/grab (404/503/504)
|
||||||
@@ -397,7 +407,8 @@ def create_app(folder: Path | str | None = None,
|
|||||||
calcoli[calcolo_id] = (corpo, mappa)
|
calcoli[calcolo_id] = (corpo, mappa)
|
||||||
while len(calcoli) > _MAX_CALCOLI:
|
while len(calcoli) > _MAX_CALCOLI:
|
||||||
calcoli.popitem(last=False)
|
calcoli.popitem(last=False)
|
||||||
return {**corpo, "calcolo_id": calcolo_id, "overlay_png": risultato["overlay_png"]}
|
return {**corpo, "calcolo_id": calcolo_id, "overlay_png": risultato["overlay_png"],
|
||||||
|
"bloccata": bool(bloccata and not contenuto)} # #94: solo nella risposta (non nel salvato): true se il frame e' la copia bloccata
|
||||||
|
|
||||||
# ---- #91: archivio unico di calibrazioni con nome
|
# ---- #91: archivio unico di calibrazioni con nome
|
||||||
|
|
||||||
@@ -531,6 +542,25 @@ def create_app(folder: Path | str | None = None,
|
|||||||
"X-Nitidezza": f"{nitidezza:.1f}", "X-Saturati-Pct": f"{saturati:.2f}",
|
"X-Nitidezza": f"{nitidezza:.1f}", "X-Saturati-Pct": f"{saturati:.2f}",
|
||||||
"X-Media": f"{media:.1f}"})
|
"X-Media": f"{media:.1f}"})
|
||||||
|
|
||||||
|
@app.post("/camera/blocca")
|
||||||
|
def camera_blocca(corpo: dict = Body(default=None)):
|
||||||
|
"""#94: copia l'ultimo frame grezzo della sorgente; /camera/calibrate con bloccata=true calcola su quella."""
|
||||||
|
corpo = corpo if isinstance(corpo, dict) else {}
|
||||||
|
nome = _sorgente(corpo.get("source"))
|
||||||
|
with lock:
|
||||||
|
frame = grezzi.get(nome)
|
||||||
|
if frame is None:
|
||||||
|
raise HTTPException(404, "nessun frame: avviare l'anteprima o scattare")
|
||||||
|
bloccati[nome] = frame.copy()
|
||||||
|
alt, larg = frame.shape[:2]
|
||||||
|
return {"source": nome, "larghezza": larg, "altezza": alt}
|
||||||
|
|
||||||
|
@app.delete("/camera/blocca", status_code=204)
|
||||||
|
def camera_sblocca(source: str = ""):
|
||||||
|
with lock: # #94: idempotente, 204 anche se non c'era nulla
|
||||||
|
bloccati.pop(source.strip(), None)
|
||||||
|
return Response(status_code=204)
|
||||||
|
|
||||||
@app.post("/camera/anteprima/calibrata")
|
@app.post("/camera/anteprima/calibrata")
|
||||||
def camera_anteprima_calibrata(corpo: dict = Body(default=None)):
|
def camera_anteprima_calibrata(corpo: dict = Body(default=None)):
|
||||||
"""#92: l'ultimo frame grezzo dell'anteprima, raddrizzato; NON legge un frame nuovo."""
|
"""#92: l'ultimo frame grezzo dell'anteprima, raddrizzato; NON legge un frame nuovo."""
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
"""Test di #94: «Blocca immagine» (scripts/camera_service.py) e riquadro vivo nella tab Calibrazione (PAGINA).
|
||||||
|
|
||||||
|
Uso: uv run --python 3.13 --extra vision-worker --extra dev \
|
||||||
|
--with-editable vendor/visionsuite/packages/vs-camera pytest scripts/tests -q
|
||||||
|
Sorgente finta che conta i grab: niente camera vera, niente ProgramData.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from test_calibrazione import _png, cartella, client, una_scacchiera # fixture e helper della calibrazione
|
||||||
|
from test_immagini_affiancate import _anteprima, _carica, _modulo
|
||||||
|
from test_pagina_acquisizione import PAGINA
|
||||||
|
|
||||||
|
__all__ = ["cartella", "client"] # le fixture importate restano usate
|
||||||
|
|
||||||
|
FINTA = "finta"
|
||||||
|
CAMPI = {"cols": "9", "rows": "6", "square_mm": "2"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def finta(client, monkeypatch):
|
||||||
|
"""Sorgente «finta»: il primo grab da' la scacchiera, i successivi un frame bianco (non calibrabile)."""
|
||||||
|
letture = []
|
||||||
|
scacchiera = una_scacchiera()
|
||||||
|
bianco = np.full(scacchiera.shape, 255, np.uint8)
|
||||||
|
|
||||||
|
class Finta:
|
||||||
|
def grab(self, timeout=1000):
|
||||||
|
letture.append(1)
|
||||||
|
return scacchiera.copy() if len(letture) == 1 else bianco.copy()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
monkeypatch.setitem(_modulo(client), "open_source", lambda *a, **k: Finta())
|
||||||
|
client.letture = letture
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
def _blocca(c, nome=FINTA):
|
||||||
|
return c.post("/camera/blocca", json={"source": nome})
|
||||||
|
|
||||||
|
|
||||||
|
def _calcola(c, **extra):
|
||||||
|
return c.post("/camera/calibrate", data={"source": FINTA, **CAMPI, **extra})
|
||||||
|
|
||||||
|
|
||||||
|
# ---- servizio
|
||||||
|
|
||||||
|
def test_blocca_senza_frame_404(finta):
|
||||||
|
r = _blocca(finta)
|
||||||
|
assert r.status_code == 404 and r.json()["detail"] == "nessun frame: avviare l'anteprima o scattare"
|
||||||
|
|
||||||
|
|
||||||
|
def test_blocca_senza_source_400(finta):
|
||||||
|
assert finta.post("/camera/blocca", json={}).status_code == 400
|
||||||
|
assert finta.post("/camera/blocca").status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_anteprima_poi_blocca_200_con_misure(finta):
|
||||||
|
assert _anteprima(finta, FINTA).status_code == 200
|
||||||
|
r = _blocca(finta)
|
||||||
|
alt, larg = una_scacchiera().shape[:2]
|
||||||
|
assert r.status_code == 200 and r.json() == {"source": FINTA, "larghezza": larg, "altezza": alt}
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrate_bloccata_usa_la_copia_senza_grab(finta):
|
||||||
|
assert _anteprima(finta, FINTA).status_code == 200 # frame 1: la scacchiera
|
||||||
|
assert _blocca(finta).status_code == 200
|
||||||
|
assert _anteprima(finta, FINTA).status_code == 200 # frame 2: bianco, il grezzo cambia ma la copia no
|
||||||
|
prima = len(finta.letture)
|
||||||
|
r = _calcola(finta, bloccata="true")
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
assert r.json()["bloccata"] is True and r.json()["origine"] == FINTA
|
||||||
|
assert len(finta.letture) == prima # nessun grab per il calcolo
|
||||||
|
# senza il blocco il calcolo legge un frame nuovo (bianco): non calibrabile
|
||||||
|
assert _calcola(finta).status_code == 422
|
||||||
|
assert len(finta.letture) == prima + 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrate_normale_dice_bloccata_false(client):
|
||||||
|
r = client.post("/camera/calibrate", data={"source": "camera:folder", **CAMPI})
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
assert r.json()["bloccata"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrate_bloccata_senza_copia_409(finta):
|
||||||
|
assert _anteprima(finta, FINTA).status_code == 200 # il grezzo c'e', ma non e' bloccato
|
||||||
|
r = _calcola(finta, bloccata="true")
|
||||||
|
assert r.status_code == 409 and r.json()["detail"] == "nessuna immagine bloccata su questa sorgente"
|
||||||
|
|
||||||
|
|
||||||
|
def test_sblocca_204_idempotente_e_toglie_la_copia(finta):
|
||||||
|
assert finta.delete("/camera/blocca", params={"source": FINTA}).status_code == 204 # niente da togliere
|
||||||
|
assert _anteprima(finta, FINTA).status_code == 200
|
||||||
|
assert _blocca(finta).status_code == 200
|
||||||
|
assert finta.delete("/camera/blocca", params={"source": FINTA}).status_code == 204
|
||||||
|
assert _calcola(finta, bloccata="true").status_code == 409
|
||||||
|
assert finta.delete("/camera/blocca", params={"source": FINTA}).status_code == 204
|
||||||
|
|
||||||
|
|
||||||
|
def test_rimuovere_l_immagine_toglie_la_copia_bloccata(client):
|
||||||
|
assert _carica(client, "foto.png", una_scacchiera()).status_code == 201
|
||||||
|
assert _anteprima(client, "foto.png").status_code == 200
|
||||||
|
assert _blocca(client, "foto.png").status_code == 200
|
||||||
|
assert client.delete("/camera/immagini", params={"nome": "foto.png"}).status_code == 204
|
||||||
|
assert _carica(client, "foto.png", una_scacchiera()).status_code == 201
|
||||||
|
r = client.post("/camera/calibrate", data={"source": "foto.png", "bloccata": "true", **CAMPI})
|
||||||
|
assert r.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
# ---- pagina (contenuto statico)
|
||||||
|
|
||||||
|
def test_pagina_riquadro_e_casella_nella_tab_calibrazione():
|
||||||
|
ids = set(re.findall(r'id="([^"]+)"', PAGINA))
|
||||||
|
assert {"immagineCalTab", "vuotoCalTab", "scattaCal", "bloccaImg", "msgBlocco"} <= ids
|
||||||
|
assert "Nessuna immagine: «Scatta» o avviare l'anteprima nella tab Acquisizione" in PAGINA
|
||||||
|
assert "Blocca immagine" in PAGINA
|
||||||
|
tab = PAGINA.split('id="pannelloCal"')[1].split('id="sezioneSalvate"')[0]
|
||||||
|
assert "immagineCalTab" in tab and "bloccaImg" in tab # nella sezione Calibrazione, non altrove
|
||||||
|
|
||||||
|
|
||||||
|
def test_pagina_scegli_tab_non_ferma_piu_il_ciclo():
|
||||||
|
assert "A.riprendi" not in PAGINA
|
||||||
|
scegli = PAGINA.split("function scegliTab")[1].split("$(\"tabAcq\").onclick")[0]
|
||||||
|
assert "fermaAnteprima" not in scegli and "avviaAnteprima" not in scegli
|
||||||
|
|
||||||
|
|
||||||
|
def test_pagina_calcola_manda_bloccata_e_non_ferma_il_ciclo():
|
||||||
|
calcola = PAGINA.split("async function calcola")[1].split("async function salvaNome")[0]
|
||||||
|
assert 'f.append("bloccata", "true")' in calcola
|
||||||
|
assert "if (bloccata)" in calcola and "else fermaAnteprima();" in calcola
|
||||||
|
assert "immagine bloccata" in calcola
|
||||||
|
assert "l'anteprima resta sospesa" not in PAGINA and "si può bloccare l'immagine" in PAGINA
|
||||||
|
|
||||||
|
|
||||||
|
def test_pagina_blocco_rotte_e_cambio_sorgente():
|
||||||
|
assert '"/camera/blocca"' in PAGINA and '"/camera/blocca?source="' in PAGINA
|
||||||
|
assert "method: \"DELETE\"" in PAGINA
|
||||||
|
cambio = PAGINA.split("function cambioSorgente")[1].split("// ---- Anteprima")[0]
|
||||||
|
assert "rilasciaBlocco()" in cambio
|
||||||
|
assert cambio.index("rilasciaBlocco()") < cambio.index("svuotaRiquadri()")
|
||||||
@@ -169,7 +169,7 @@ def test_c2_immagine_caricata_senza_salvare(client):
|
|||||||
files={"image": ("s.png", _png(una_scacchiera()), "image/png")})
|
files={"image": ("s.png", _png(una_scacchiera()), "image/png")})
|
||||||
assert r.status_code == 200, r.text
|
assert r.status_code == 200, r.text
|
||||||
corpo = r.json()
|
corpo = r.json()
|
||||||
assert CHIAVI_PLANARE | {"overlay_png", "calcolo_id"} == set(corpo) # #91: niente "saved", niente "source"
|
assert CHIAVI_PLANARE | {"overlay_png", "calcolo_id", "bloccata"} == set(corpo) # #94: + bloccata; #91: niente "saved", niente "source"
|
||||||
assert corpo["metodo"] == "planare" and corpo["distorsione"] is False # #84: con cols/rows nulla cambia
|
assert corpo["metodo"] == "planare" and corpo["distorsione"] is False # #84: con cols/rows nulla cambia
|
||||||
assert corpo["origine"] == "file:s.png"
|
assert corpo["origine"] == "file:s.png"
|
||||||
assert corpo["px_per_mm"] == pytest.approx(20.0, rel=0.005)
|
assert corpo["px_per_mm"] == pytest.approx(20.0, rel=0.005)
|
||||||
@@ -337,7 +337,7 @@ def test_c2_senza_cols_rows_e_densa(client_denso):
|
|||||||
r = client_denso.post("/camera/calibrate", data={"source": SORGENTE, "square_mm": "2"})
|
r = client_denso.post("/camera/calibrate", data={"source": SORGENTE, "square_mm": "2"})
|
||||||
assert r.status_code == 200, r.text
|
assert r.status_code == 200, r.text
|
||||||
corpo = r.json()
|
corpo = r.json()
|
||||||
assert CHIAVI_DENSA | {"overlay_png", "calcolo_id"} == set(corpo)
|
assert CHIAVI_DENSA | {"overlay_png", "calcolo_id", "bloccata"} == set(corpo)
|
||||||
assert corpo["metodo"] == "densa" and corpo["distorsione"] is True
|
assert corpo["metodo"] == "densa" and corpo["distorsione"] is True
|
||||||
assert (corpo["cols"], corpo["rows"]) == (15, 11)
|
assert (corpo["cols"], corpo["rows"]) == (15, 11)
|
||||||
assert corpo["px_per_mm"] == pytest.approx(1.0 / corpo["mm_per_px"])
|
assert corpo["px_per_mm"] == pytest.approx(1.0 / corpo["mm_per_px"])
|
||||||
|
|||||||
@@ -98,12 +98,11 @@ def test_pagina_stretta():
|
|||||||
assert "Lettura dei parametri..." in PAGINA
|
assert "Lettura dei parametri..." in PAGINA
|
||||||
|
|
||||||
|
|
||||||
def test_ciclo_sospeso_nella_tab_calibrazione():
|
def test_ciclo_non_sospeso_nella_tab_calibrazione():
|
||||||
# #92: entrando in Calibrazione il ciclo si ferma e poi riprende; Calcola aspetta le richieste in volo
|
# #94: il ciclo non si ferma piu' entrando in Calibrazione (prima #92: A.riprendi); Calcola senza blocco aspetta le richieste in volo
|
||||||
assert "A.riprendi = true; fermaAnteprima();" in PAGINA
|
assert "A.riprendi" not in PAGINA
|
||||||
assert "if (!cal && A.riprendi)" in PAGINA
|
|
||||||
calcola = PAGINA.split("async function calcola")[1].split("async function salvaNome")[0]
|
calcola = PAGINA.split("async function calcola")[1].split("async function salvaNome")[0]
|
||||||
assert "fermaAnteprima();" in calcola and "while (A.inVolo)" in calcola
|
assert "fermaAnteprima();" in calcola and "A.inVolo) await pausa(50)" in calcola
|
||||||
|
|
||||||
|
|
||||||
def test_calibrata_dopo_anteprima_e_dopo_carica():
|
def test_calibrata_dopo_anteprima_e_dopo_carica():
|
||||||
|
|||||||
Reference in New Issue
Block a user