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:
@@ -79,19 +79,31 @@ class Camera:
|
||||
|
||||
if source == "gphoto2" or source is None:
|
||||
if GPHOTO2_DISPONIBLE:
|
||||
cam = None
|
||||
try:
|
||||
self.camera = gp.Camera()
|
||||
self.camera.init()
|
||||
# Desactiver la mise en veille auto du DSLR (sinon il se deconnecte tout seul)
|
||||
cam = gp.Camera()
|
||||
try:
|
||||
cam.exit()
|
||||
except Exception:
|
||||
pass
|
||||
cam.init()
|
||||
self.camera = cam
|
||||
self._desactiver_veille_init()
|
||||
# Viewfinder activé AVANT de rendre la caméra visible au thread preview
|
||||
self._activer_viewfinder_init()
|
||||
self.connectee = True
|
||||
self.mode = "gphoto2"
|
||||
self.preview_dslr_ok = True
|
||||
log.info("Camera DSLR connectee (LiveView actif)")
|
||||
return True
|
||||
except gp.GPhoto2Error as e:
|
||||
except Exception as e:
|
||||
log.warning(f"Pas de DSLR : {e}")
|
||||
if cam is not None and cam is not self.camera:
|
||||
try:
|
||||
cam.exit()
|
||||
except Exception:
|
||||
pass
|
||||
del cam
|
||||
self.camera = None
|
||||
|
||||
# Pas de DSLR disponible : mode erreur, pas de fallback
|
||||
log.warning("Aucun DSLR disponible, camera non connectee")
|
||||
@@ -116,25 +128,30 @@ class Camera:
|
||||
|
||||
def deconnecter(self, timeout: float = 3.0):
|
||||
"""Deconnecte l'appareil photo avec timeout pour eviter le blocage PTP."""
|
||||
if self.camera and GPHOTO2_DISPONIBLE:
|
||||
cam_ref = self.camera
|
||||
ctx_ref = self.contexte
|
||||
if cam_ref and GPHOTO2_DISPONIBLE:
|
||||
def _exit():
|
||||
try:
|
||||
self.camera.exit()
|
||||
cam_ref.exit()
|
||||
except Exception:
|
||||
pass
|
||||
t = threading.Thread(target=_exit, daemon=True)
|
||||
t.start()
|
||||
t.join(timeout=timeout)
|
||||
if t.is_alive():
|
||||
log.warning("camera.exit() bloque — abandon (le FD sera libere au GC)")
|
||||
log.warning("camera.exit() bloque — force del camera/contexte pour liberer le FD USB")
|
||||
if self.webcam:
|
||||
self.webcam.release()
|
||||
self.camera = None
|
||||
self.contexte = None
|
||||
del cam_ref
|
||||
del ctx_ref
|
||||
self.webcam = None
|
||||
self.webcam_index = -1
|
||||
self.connectee = False
|
||||
self.mode = "simulation"
|
||||
self.preview_dslr_ok = False
|
||||
log.info("Camera deconnectee")
|
||||
|
||||
def capturer(self) -> Path | None:
|
||||
@@ -160,13 +177,16 @@ class Camera:
|
||||
for attempt in range(3):
|
||||
try:
|
||||
with self._gp_lock:
|
||||
# Désactiver LiveView : miroir abaissé → AF phase-détection Canon disponible
|
||||
if not self.camera:
|
||||
log.error("Camera None pendant capture")
|
||||
self.preview_dslr_ok = False
|
||||
return None
|
||||
try:
|
||||
cfg = self.camera.get_config()
|
||||
vf = cfg.get_child_by_name("viewfinder")
|
||||
vf.set_value(0)
|
||||
self.camera.set_config(cfg)
|
||||
time.sleep(0.2) # Laisser le miroir descendre
|
||||
time.sleep(0.2)
|
||||
except Exception:
|
||||
pass
|
||||
log.info(f"Déclenchement capture DSLR (tentative {attempt+1}/3)")
|
||||
@@ -177,8 +197,8 @@ class Camera:
|
||||
)
|
||||
tmp_path = str(chemin_dest) + ".tmp"
|
||||
fichier_camera.save(tmp_path)
|
||||
self.activer_viewfinder() # Miroir relevé immédiatement après la prise
|
||||
self._chauffer_liveview() # Absorbe la stabilisation LiveView ici (spinner visible), pas sur le countdown suivant
|
||||
self.activer_viewfinder()
|
||||
self._chauffer_liveview()
|
||||
from PIL import ImageOps as PILImageOps
|
||||
img = PILImageOps.exif_transpose(PILImage.open(tmp_path))
|
||||
if img.width > 4000:
|
||||
@@ -194,6 +214,11 @@ class Camera:
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
log.error(f"Erreur capture DSLR : {e}")
|
||||
self.preview_dslr_ok = False
|
||||
return None
|
||||
except Exception as e:
|
||||
log.error(f"Erreur fatale capture DSLR : {e}")
|
||||
self.preview_dslr_ok = False
|
||||
return None
|
||||
|
||||
def _chauffer_liveview(self, tentatives: int = 8, pause: float = 0.15):
|
||||
@@ -308,6 +333,8 @@ class Camera:
|
||||
def _preview_gphoto2(self) -> bytes | None:
|
||||
try:
|
||||
with self._gp_lock:
|
||||
if not self.camera:
|
||||
return None
|
||||
fichier = self.camera.capture_preview()
|
||||
donnees = bytes(fichier.get_data_and_size())
|
||||
img = cv2.imdecode(np.frombuffer(donnees, dtype=np.uint8), cv2.IMREAD_COLOR)
|
||||
@@ -318,7 +345,7 @@ class Camera:
|
||||
_, buf = cv2.imencode(".jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 75])
|
||||
return buf.tobytes()
|
||||
return donnees
|
||||
except gp.GPhoto2Error as e:
|
||||
except Exception as e:
|
||||
log.error(f"Erreur preview DSLR : {e}")
|
||||
return None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user