- 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>
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 = 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
|