Files
TieMeasureFlow/scripts/calibrazione_pagina.py
T

219 lines
8.5 KiB
Python

"""Pagina HTML della calibrazione a scacchiera (servita da camera_service su /calibrazione, #83).
Cosa fa: una pagina sola, autonoma e offline (niente CDN, JS e CSS inline), in
italiano. Parla con le rotte dello stesso servizio (/camera/sources,
/camera/calibration, /camera/calibrate).
Dipende da: niente. E' una stringa, cosi' il pacchetto compilato non ha file dati.
"""
PAGINA = """<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Calibrazione camera</title>
<style>
body { font-family: system-ui, sans-serif; margin: 0; background: #f3f4f6; color: #1f2937; }
main { max-width: 960px; margin: 0 auto; padding: 16px; }
h1 { font-size: 1.4rem; margin: 0 0 12px; }
.scheda { background: #fff; border: 1px solid #d1d5db; border-radius: 8px; padding: 14px; margin-bottom: 14px; }
label { display: block; font-size: .85rem; margin-bottom: 4px; color: #4b5563; }
select, input[type=number] { padding: 6px 8px; font-size: 1rem; border: 1px solid #9ca3af; border-radius: 6px; }
.riga { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; }
button { padding: 8px 14px; font-size: 1rem; border: 0; border-radius: 6px; background: #2563eb; color: #fff; cursor: pointer; }
button.secondario { background: #6b7280; }
button.pericolo { background: #b91c1c; }
button:disabled { opacity: .5; cursor: default; }
.messaggio { margin-top: 10px; font-weight: 600; }
.errore { color: #b91c1c; }
.ok { color: #15803d; }
table { border-collapse: collapse; }
td { padding: 3px 14px 3px 0; }
td:first-child { color: #4b5563; }
img { max-width: 100%; border: 1px solid #d1d5db; border-radius: 6px; margin-top: 8px; }
.avviso { color: #92400e; }
</style>
</head>
<body>
<main>
<h1>Calibrazione camera con scacchiera</h1>
<div class="scheda">
<div class="riga">
<div><label for="sorgente">Sorgente</label><select id="sorgente"></select></div>
<div><label for="cols">Angoli interni in orizzontale</label><input id="cols" type="number" min="2" value="9"></div>
<div><label for="rows">Angoli interni in verticale</label><input id="rows" type="number" min="2" value="6"></div>
<div><label for="quadrato">Lato del quadrato (mm)</label><input id="quadrato" type="number" min="0.001" step="any" value="2"></div>
</div>
<p style="font-size:.85rem;color:#4b5563;margin:10px 0 0">
Gli angoli interni sono quelli dove si toccano quattro quadrati: una scacchiera di 10 x 7 quadrati ha 9 x 6 angoli.
Tenere la scacchiera piana e ferma, ben illuminata.</p>
</div>
<div class="scheda">
<div class="riga">
<button id="acquisisci">Acquisisci e calcola</button>
<div><label for="file">oppure un file immagine</label><input id="file" type="file" accept="image/*"></div>
<button id="carica" class="secondario">Carica immagine e calcola</button>
</div>
<div id="messaggio" class="messaggio"></div>
</div>
<div class="scheda" id="risultato" hidden>
<strong>Risultato del calcolo</strong>
<table id="tabella"></table>
<div id="avviso" class="avviso"></div>
<div class="riga" style="margin-top:10px">
<button id="salva">Salva per questa sorgente</button>
<span style="font-size:.85rem;color:#4b5563">Da camera, salvando si acquisisce e si calcola di nuovo: tenere la scacchiera ferma.</span>
</div>
<img id="overlay" alt="Angoli trovati sulla scacchiera">
</div>
<div class="scheda">
<strong>Calibrazione salvata</strong>
<div id="stato">-</div>
<div class="riga" style="margin-top:10px">
<button id="elimina" class="pericolo" disabled>Elimina</button>
</div>
</div>
</main>
<script>
"use strict";
var $ = function (id) { return document.getElementById(id); };
var ultimo = null; // ultimo calcolo, per «Salva»
function messaggio(testo, classe) {
var m = $("messaggio");
m.textContent = testo || "";
m.className = "messaggio " + (classe || "");
}
function numero(v, cifre) { return Number(v).toFixed(cifre); }
function righe(d) {
return [
["Scala", numero(d.px_per_mm, 4) + " px/mm (" + numero(d.mm_per_px, 5) + " mm/px)"],
["Errore di riproiezione (RMS)", numero(d.rms_px, 3) + " px"],
["Variazione di scala nell'area", numero(d.scale_spread_pct, 2) + " %"],
["Angoli trovati", d.n_corners + " (" + d.cols + " x " + d.rows + ")"],
["Lato del quadrato", d.square_mm + " mm"],
["Immagine", d.image_width + " x " + d.image_height + " px"]
];
}
function tabella(el, d) {
el.textContent = "";
righe(d).forEach(function (r) {
var tr = document.createElement("tr");
r.forEach(function (c) {
var td = document.createElement("td");
td.textContent = c;
tr.appendChild(td);
});
el.appendChild(tr);
});
}
async function dettaglio(risposta) {
try { var j = await risposta.json(); return j.detail || JSON.stringify(j); }
catch (e) { return "errore " + risposta.status; }
}
async function caricaSorgenti() {
var sel = $("sorgente");
try {
var r = await fetch("/camera/sources");
var j = await r.json();
(j.sources || []).forEach(function (s) {
var o = document.createElement("option");
o.value = s.name;
o.textContent = s.label || s.name;
sel.appendChild(o);
});
} catch (e) { messaggio("Elenco delle sorgenti non disponibile.", "errore"); }
if (!sel.options.length) messaggio("Nessuna sorgente camera trovata.", "errore");
aggiornaStato();
}
async function aggiornaStato() {
var sorgente = $("sorgente").value;
var stato = $("stato");
$("elimina").disabled = true;
if (!sorgente) { stato.textContent = "-"; return; }
var r = await fetch("/camera/calibration?source=" + encodeURIComponent(sorgente));
if (r.status === 404) { stato.textContent = "Sorgente non calibrata."; return; }
if (!r.ok) { stato.textContent = await dettaglio(r); return; }
var d = await r.json();
stato.textContent = "";
var t = document.createElement("table");
tabella(t, d);
var quando = document.createElement("div");
quando.textContent = "Calibrata il " + d.calibrated_at;
stato.appendChild(t);
stato.appendChild(quando);
$("elimina").disabled = false;
}
function parametri() {
return {
cols: $("cols").value, rows: $("rows").value, quadrato: $("quadrato").value
};
}
async function calcola(conFile, salva) {
var sorgente = $("sorgente").value;
if (!sorgente) { messaggio("Scegliere una sorgente.", "errore"); return; }
var p = parametri();
var f = new FormData();
f.append("source", sorgente);
f.append("cols", p.cols);
f.append("rows", p.rows);
f.append("square_mm", p.quadrato);
f.append("save", salva ? "true" : "false");
if (conFile) {
var file = $("file").files[0];
if (!file) { messaggio("Scegliere un file immagine.", "errore"); return; }
f.append("image", file);
}
messaggio("Calcolo in corso...", "");
["acquisisci", "carica", "salva"].forEach(function (i) { $(i).disabled = true; });
try {
var r = await fetch("/camera/calibrate", { method: "POST", body: f });
if (!r.ok) { messaggio(await dettaglio(r), "errore"); return; }
var d = await r.json();
ultimo = { conFile: conFile, dati: d };
tabella($("tabella"), d);
$("overlay").src = "data:image/png;base64," + d.overlay_png;
$("avviso").textContent = d.scale_spread_pct > 1
? "Attenzione: la scala varia di " + numero(d.scale_spread_pct, 2) + " % nell'area: scacchiera inclinata o ottica non telecentrica."
: "";
$("risultato").hidden = false;
messaggio(d.saved ? "Calibrazione salvata." : "Calcolo riuscito: controllare e salvare.", "ok");
if (d.saved) aggiornaStato();
} catch (e) {
messaggio("Servizio non raggiungibile.", "errore");
} finally {
["acquisisci", "carica", "salva"].forEach(function (i) { $(i).disabled = false; });
}
}
$("acquisisci").onclick = function () { calcola(false, false); };
$("carica").onclick = function () { calcola(true, false); };
// «Salva» rifa il calcolo con save=true sulla stessa origine (altra acquisizione se da camera)
$("salva").onclick = function () { calcola(ultimo ? ultimo.conFile : false, true); };
$("elimina").onclick = async function () {
var sorgente = $("sorgente").value;
if (!confirm("Eliminare la calibrazione salvata di " + sorgente + "?")) return;
var r = await fetch("/camera/calibration?source=" + encodeURIComponent(sorgente), { method: "DELETE" });
messaggio(r.ok ? "Calibrazione eliminata." : await dettaglio(r), r.ok ? "ok" : "errore");
aggiornaStato();
};
$("sorgente").onchange = aggiornaStato;
caricaSorgenti();
</script>
</body>
</html>
"""