Specchiatura orizzontale/verticale per sorgente nel servizio camera, salvata su disco. Refs #110

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 10:38:49 +01:00
parent b2e3867370
commit 2c2d601495
2 changed files with 255 additions and 3 deletions
+87 -3
View File
@@ -61,7 +61,17 @@ Acquisizione (#87): anteprima, esposizione/guadagno, settaggi di default:
POST /camera/parametri/salva json {source} -> {source, salvati, file}
scrive <config-dir>/settaggi/<nome>-<sha1>.json; alla prima apertura
della camera nel servizio il file, se c'e', viene ricaricato.
Le calibrazioni stanno in <config-dir>/calibrazioni, le attive in attive.json (default
#110: specchiatura per sorgente, in software (cv2.flip) su OGNI frame letto dalla
sorgente, prima di raddrizzatura/calibrazione (anteprima, grab, calibrazione da camera,
copia bloccata); non tocca l'immagine caricata a mano in /camera/calibrate.
GET /camera/orientamento?source=S -> {source, orizzontale, verticale} (default false/false);
400 se manca source
PUT /camera/orientamento json {source, orizzontale: bool, verticale: bool} -> come GET +
"avviso" (stringa o null: la calibrazione attiva ha una mappa fatta con l'orientamento
di prima e va rifatta). 400 se source manca o i due campi non sono bool. Salva subito
su disco; scarta i frame tenuti (anteprima/blocca).
Le calibrazioni stanno in <config-dir>/calibrazioni, le attive in attive.json, l'orientamento
per sorgente in orientamento.json (#110) (default
%ProgramData%/Tielogic/TieMeasureFlow/acquisitore su Windows, ./acquisitore altrove).
Serve perche' vs-gui ascolta solo su 127.0.0.1 e il container `client` non lo
@@ -87,6 +97,7 @@ camera a chiunque raggiunga la porta: solo su rete isolata.
from __future__ import annotations
import argparse
import json # #110: orientamento.json
import logging
import math # #95: mm/px finito nella calibrazione manuale
import os
@@ -167,6 +178,36 @@ def create_app(folder: Path | str | None = None,
lock_sorgenti: dict = {} # nome sorgente -> Lock che serializza apertura e grab
calcoli: OrderedDict = OrderedDict() # #91: calcolo_id -> (dati, mappa); gli ultimi _MAX_CALCOLI
lock_calcoli = threading.Lock()
file_orientamento = archivio.cartella / "orientamento.json" # #110: {sorgente: {orizzontale, verticale}}
def _carica_orientamenti() -> dict:
"""#110: orientamenti salvati; file assente o rotto -> {} (mai eccezione)."""
if not file_orientamento.is_file():
return {}
try:
dati = json.loads(file_orientamento.read_text(encoding="utf-8"))
if not isinstance(dati, dict):
raise ValueError("non e' un oggetto JSON")
return {n: {"orizzontale": v.get("orizzontale") is True, "verticale": v.get("verticale") is True}
for n, v in dati.items() if isinstance(n, str) and isinstance(v, dict)}
except (OSError, ValueError) as e:
_log.warning("orientamento non caricato (%s): %s", file_orientamento, e)
return {}
orientamenti: dict = _carica_orientamenti() # #110: sorgente -> {"orizzontale": bool, "verticale": bool}; protetto da `lock`
def _specchia(nome: str, frame):
"""#110: applica l'orientamento della sorgente al frame letto; nessuno -> lo stesso oggetto, senza copia."""
with lock:
o = orientamenti.get(nome) or {}
orizz, vert = o.get("orizzontale", False), o.get("verticale", False)
if orizz and vert:
return cv2.flip(frame, -1) # entrambi = rotazione di 180 gradi
if orizz:
return cv2.flip(frame, 1)
if vert:
return cv2.flip(frame, 0)
return frame
def _config() -> dict:
if folder is None:
@@ -317,7 +358,7 @@ def create_app(folder: Path | str | None = None,
def _grab(nome: str, timeout: int):
"""Un frame dalla sorgente (apre e tiene aperta); errori camera -> HTTP."""
return _con_sorgente(nome, lambda s: s.grab(timeout)) # #87: corpo in _con_sorgente
return _con_sorgente(nome, lambda s: _specchia(nome, s.grab(timeout))) # #87: corpo in _con_sorgente; #110: specchiato prima di tutto il resto
@app.post("/camera/grab")
def camera_grab(corpo: dict = Body(default=None)):
@@ -544,7 +585,7 @@ def create_app(folder: Path | str | None = None,
if isinstance(timeout, bool) or not isinstance(timeout, int) or not 1 <= timeout <= 10000:
raise HTTPException(400, "«timeout_ms» deve essere un intero fra 1 e 10000")
def leggi(s): # #92: il frame grezzo si tiene sotto il lock della sorgente
f = s.grab(timeout)
f = _specchia(nome, s.grab(timeout)) # #110: specchiato PRIMA di tenerlo, cosi' blocca/calibrata lo ricevono gia' specchiato
with lock:
grezzi[nome] = f.copy()
return f
@@ -686,6 +727,49 @@ def create_app(folder: Path | str | None = None,
return _con_sorgente(nome, salva)
# ---- #110: specchiatura orizzontale/verticale per sorgente
def _orientamento_di(nome: str) -> dict:
with lock:
o = orientamenti.get(nome) or {}
return {"source": nome, "orizzontale": bool(o.get("orizzontale")), "verticale": bool(o.get("verticale"))}
@app.get("/camera/orientamento")
def orientamento_leggi(source: str = ""):
return _orientamento_di(_sorgente(source))
@app.put("/camera/orientamento")
def orientamento_scrivi(corpo: dict = Body(default=None)):
corpo = corpo if isinstance(corpo, dict) else {}
nome = _sorgente(corpo.get("source"))
orizz, vert = corpo.get("orizzontale"), corpo.get("verticale")
if not isinstance(orizz, bool) or not isinstance(vert, bool):
raise HTTPException(400, "«orizzontale» e «verticale» devono essere true o false")
with lock:
prima = orientamenti.get(nome) or {}
cambiato = (bool(prima.get("orizzontale")), bool(prima.get("verticale"))) != (orizz, vert)
if orizz or vert:
orientamenti[nome] = {"orizzontale": orizz, "verticale": vert}
else:
orientamenti.pop(nome, None) # tutti false: la sorgente esce dal file
# scrittura atomica di tutto il dizionario (tmp + replace)
try:
file_orientamento.parent.mkdir(parents=True, exist_ok=True)
tmp = file_orientamento.with_name(file_orientamento.name + ".tmp")
tmp.write_text(json.dumps(orientamenti, ensure_ascii=False, indent=1), encoding="utf-8")
os.replace(tmp, file_orientamento)
except OSError as e:
_log.warning("orientamento non salvato (%s): %s", file_orientamento, e)
raise HTTPException(500, "salvataggio dell'orientamento non riuscito")
# i frame tenuti hanno l'orientamento di prima
grezzi.pop(nome, None)
bloccati.pop(nome, None)
avviso = None
if cambiato and archivio.leggi_mappa(nome) is not None:
avviso = (f"la calibrazione attiva «{archivio.attiva_di(nome)}» corregge la distorsione "
"con l'orientamento di prima: va rifatta")
return {**_orientamento_di(nome), "avviso": avviso}
@app.get("/calibrazione", response_class=HTMLResponse)
@app.get("/acquisizione", response_class=HTMLResponse) # #87: stessa pagina
def calibrazione_pagina():