Robustesse DSLR + éclairage visage + fix pellicule cadre imposé
- camera.py: fix fuite FD USB sur échec reconnexion (gp.Camera non libéré), cleanup PTP session avant init, catch toutes exceptions preview/capture, deconnecter() avec timeout thread + force del pour libérer le port USB - main.py: thread preview anti-crash (outer try/except), reconnexion DSLR même si preview actif, notification camera_erreur après échec capture, intégration module éclairage (analyse luminosité visage via WS) - printer.py: mode pellicule (-2up) ignoré par le cadre imposé événement - eclairage.py: nouveau module analyse luminosité visage (Haar + HSV) - camera.js: indicateur éclairage temps réel (score/100 + visages) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
114
backend/eclairage.py
Normal file
114
backend/eclairage.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""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 = 3.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
|
||||
Reference in New Issue
Block a user