Files
Aivanov_scan_ogc/pipeline/zones_config.py
Dom 1255468676 feat(ui): calibration visuelle des zones via dessin à la souris
Nouveau module pipeline/zones_config.py : charge les zones d'extraction
depuis un fichier zones_config.json (coordonnées relatives 0-1), avec
fallback sur les constantes Python. Config partagée entre :
- pipeline/extract.py (crop colonne Recodage)
- pipeline/checkboxes.py (cases Accord/Désaccord)

Zones configurables aujourd'hui (page recueil) :
- codage_reco (crop zonal pour le second passage VLM)
- accord_checkbox / desaccord_checkbox (densité de pixels)

Mode "🔧 Calibration zones" ajouté dans pipeline/ui_overlay.py :
- Sélection d'un PDF de référence (idéalement bien cadré)
- Canvas interactif (streamlit-drawable-canvas) avec les zones
  existantes pré-dessinées en rouge
- Dessin/déplacement/redimensionnement à la souris
- Saisie d'un nom et description par zone
- Sauvegarde en JSON (ou OGC_ZONES_CONFIG si défini)

Permet au métier (Khalid) de recalibrer les zones sans toucher au code,
par exemple si le formulaire ATIH évolue ou si les scans sont d'un autre
établissement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 23:07:59 +02:00

91 lines
3.0 KiB
Python

"""Configuration des zones d'extraction éditable via l'overlay UI.
Les coordonnées sont relatives (0..1) dans l'image source. Elles sont chargées
au démarrage du pipeline et utilisées à la place des constantes en dur dans
`pipeline/prompts.py` et `pipeline/checkboxes.py` — avec fallback sur ces
constantes si la config n'est pas présente, pour ne pas casser l'existant.
Structure :
{
"recueil": {
"codage_reco": {"x1":0.77, "y1":0.330, "x2":0.97, "y2":0.490, "description":"..."},
"accord_checkbox": {"x1":..., "y1":..., "x2":..., "y2":..., "description":"..."},
"desaccord_checkbox":{...}
},
"concertation_2": {...}
}
Un fichier unique `zones_config.json` à la racine du projet, ou au chemin pointé
par la variable d'env `OGC_ZONES_CONFIG`.
"""
from __future__ import annotations
import json
import os
from pathlib import Path
DEFAULT_CONFIG_PATH = Path(
os.environ.get("OGC_ZONES_CONFIG", "zones_config.json")
)
# Zones par défaut, identiques aux constantes actuelles dans prompts.py et
# checkboxes.py. Sert de fallback et de "mise à jour initiale" quand le
# fichier n'existe pas encore.
DEFAULTS: dict = {
"recueil": {
"codage_reco": {
"x1": 0.77, "y1": 0.330, "x2": 0.97, "y2": 0.490,
"description": "Colonne Recodage (DP / DR / DAS) — exclut le bloc Actes",
},
"accord_checkbox": {
"x1": 0.588, "y1": 0.838, "x2": 0.622, "y2": 0.860,
"description": "Case à cocher 'Accord'",
},
"desaccord_checkbox": {
"x1": 0.588, "y1": 0.858, "x2": 0.622, "y2": 0.880,
"description": "Case à cocher 'Désaccord'",
},
},
}
def load_config(path: Path = DEFAULT_CONFIG_PATH) -> dict:
"""Charge la config JSON, ou retourne les defaults si absente."""
if not path.exists():
return _deep_copy(DEFAULTS)
try:
raw = json.loads(path.read_text(encoding="utf-8"))
except Exception:
return _deep_copy(DEFAULTS)
# Merge : les defaults sont une base, la config utilisateur vient par-dessus
merged = _deep_copy(DEFAULTS)
for page, zones in raw.items():
merged.setdefault(page, {}).update(zones)
return merged
def save_config(cfg: dict, path: Path = DEFAULT_CONFIG_PATH) -> Path:
path.write_text(json.dumps(cfg, ensure_ascii=False, indent=2), encoding="utf-8")
return path
def get_zone(page_type: str, zone_name: str,
config: dict | None = None) -> tuple[float, float, float, float] | None:
"""Récupère une zone depuis la config ou les defaults.
Retourne (x1, y1, x2, y2) ou None si inconnue.
"""
cfg = config or load_config()
z = cfg.get(page_type, {}).get(zone_name)
if not isinstance(z, dict):
return None
try:
return (float(z["x1"]), float(z["y1"]), float(z["x2"]), float(z["y2"]))
except (KeyError, ValueError, TypeError):
return None
def _deep_copy(d: dict) -> dict:
return json.loads(json.dumps(d))