- Preview thread : poll uniquement quand ecran capture actif, keepalive 30s sinon - Sequence boot Canon : power cycle relais si absent USB au demarrage - Veille auto 30min : Canon coupe par relais apres inactivite, reveil au preview_start - Stats diagnostic /api/canon/stats : keepalive, preview, captures, connexions - Escalade reconnexion adoucie : pas d'USB reset les 2 premieres tentatives - Hard reset relais 10s (resistance decharge 990ohm sur dummy battery 8V) - Gestion fin evenement : date_fin + bouton terminer + guard 403 impression - Compteur base sur min(papier, ruban) avec cap evenement optionnel - Pre-eclairage countdown : projecteurs ON 3s avant capture pour mesure expo - Canon _configurer_init() : ISO 800, drivemode Single, viewfinder, EXIF preserve - Admin : detail evenement, compteur consommables, galerie impression Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WyN9xFp84P9VaWT3Qe2DXu
115 lines
3.3 KiB
Python
115 lines
3.3 KiB
Python
"""Analyse de luminosité sur les visages détectés dans le preview DSLR.
|
|
|
|
Produit une note d'éclairage (0-100) et une recommandation (allumer/ok/sombre).
|
|
Échantillonne toutes les ~3 secondes pour ne pas charger le CPU.
|
|
"""
|
|
|
|
import logging
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import cv2
|
|
import numpy as np
|
|
|
|
log = logging.getLogger("photobooth.eclairage")
|
|
|
|
_CASCADE_CANDIDATES = [
|
|
"/usr/share/opencv4/haarcascades/haarcascade_frontalface_default.xml",
|
|
"/usr/share/opencv/haarcascades/haarcascade_frontalface_default.xml",
|
|
"/usr/local/share/opencv4/haarcascades/haarcascade_frontalface_default.xml",
|
|
]
|
|
try:
|
|
_CASCADE_PATH = cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
|
|
except AttributeError:
|
|
_CASCADE_PATH = next((p for p in _CASCADE_CANDIDATES if Path(p).exists()), _CASCADE_CANDIDATES[0])
|
|
_face_cascade = cv2.CascadeClassifier(_CASCADE_PATH)
|
|
|
|
SEUIL_SOMBRE = 45
|
|
SEUIL_CORRECT = 65
|
|
|
|
_dernier_analyse: float = 0
|
|
_INTERVALLE = 1.0
|
|
_dernier_resultat: dict | None = None
|
|
|
|
|
|
def analyser_frame(jpeg_data: bytes) -> dict | None:
|
|
"""Analyse une frame JPEG du preview.
|
|
|
|
Retourne None si l'intervalle n'est pas écoulé (throttle).
|
|
Sinon retourne:
|
|
{
|
|
"score": int 0-100,
|
|
"action": "allumer" | "attention" | "ok",
|
|
"visages": int,
|
|
"detail": str,
|
|
}
|
|
"""
|
|
global _dernier_analyse, _dernier_resultat
|
|
|
|
now = time.monotonic()
|
|
if now - _dernier_analyse < _INTERVALLE:
|
|
return None
|
|
_dernier_analyse = now
|
|
|
|
try:
|
|
arr = np.frombuffer(jpeg_data, dtype=np.uint8)
|
|
img = cv2.imdecode(arr, cv2.IMREAD_COLOR)
|
|
if img is None:
|
|
return None
|
|
|
|
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
|
faces = _face_cascade.detectMultiScale(
|
|
gray, scaleFactor=1.2, minNeighbors=4, minSize=(60, 60)
|
|
)
|
|
|
|
if len(faces) == 0:
|
|
_dernier_resultat = {
|
|
"score": -1,
|
|
"action": "no_face",
|
|
"visages": 0,
|
|
"detail": "Aucun visage détecté",
|
|
}
|
|
return _dernier_resultat
|
|
|
|
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
|
|
scores = []
|
|
|
|
for (x, y, w, h) in faces:
|
|
pad_x = int(w * 0.1)
|
|
pad_y = int(h * 0.1)
|
|
x1 = max(0, x + pad_x)
|
|
y1 = max(0, y + pad_y)
|
|
x2 = min(img.shape[1], x + w - pad_x)
|
|
y2 = min(img.shape[0], y + h - pad_y)
|
|
roi_v = hsv[y1:y2, x1:x2, 2]
|
|
score = int(np.mean(roi_v) / 2.55)
|
|
scores.append(score)
|
|
|
|
score_moyen = int(np.mean(scores))
|
|
|
|
if score_moyen < SEUIL_SOMBRE:
|
|
action = "allumer"
|
|
detail = f"Visages sombres ({score_moyen}/100)"
|
|
elif score_moyen < SEUIL_CORRECT:
|
|
action = "attention"
|
|
detail = f"Éclairage limite ({score_moyen}/100)"
|
|
else:
|
|
action = "ok"
|
|
detail = f"Éclairage correct ({score_moyen}/100)"
|
|
|
|
_dernier_resultat = {
|
|
"score": score_moyen,
|
|
"action": action,
|
|
"visages": len(faces),
|
|
"detail": detail,
|
|
}
|
|
return _dernier_resultat
|
|
|
|
except Exception as e:
|
|
log.debug(f"Erreur analyse éclairage : {e}")
|
|
return None
|
|
|
|
|
|
def dernier_resultat() -> dict | None:
|
|
return _dernier_resultat
|