From 4273caf1eed6059e3cce2d40cf1020721d519be5 Mon Sep 17 00:00:00 2001 From: Jules Date: Thu, 9 Apr 2026 01:49:48 +0200 Subject: [PATCH] Fix race condition gphoto2 : verrou camera sur preview et capture --- backend/camera.py | 120 +++++++++++++++++++++++----------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/backend/camera.py b/backend/camera.py index fc507a2..f8677ea 100644 --- a/backend/camera.py +++ b/backend/camera.py @@ -1,6 +1,7 @@ import io import logging import subprocess +import threading import time from datetime import datetime @@ -66,6 +67,7 @@ class Camera: self.webcam = None self.webcam_index = -1 self.preview_dslr_ok = True # True si le dernier preview DSLR a reussi + self._lock = threading.Lock() # Verrou pour eviter acces gphoto2 concurrent def connecter(self, source=None) -> bool: """Connecte la camera. source: 'gphoto2', 'webcam:0', 'webcam:2', etc.""" @@ -145,34 +147,33 @@ class Camera: def _capture_gphoto2(self, chemin_dest: Path) -> Path | None: from PIL import Image as PILImage - for attempt in range(3): - try: - chemin_camera = self.camera.capture(gp.GP_CAPTURE_IMAGE) - fichier_camera = gp.CameraFile() - self.camera.file_get( - chemin_camera.folder, chemin_camera.name, gp.GP_FILE_TYPE_NORMAL, fichier_camera - ) - # Sauvegarder le fichier brut temporairement - tmp_path = str(chemin_dest) + ".tmp" - fichier_camera.save(tmp_path) + with self._lock: + for attempt in range(3): + try: + chemin_camera = self.camera.capture(gp.GP_CAPTURE_IMAGE) + fichier_camera = gp.CameraFile() + self.camera.file_get( + chemin_camera.folder, chemin_camera.name, gp.GP_FILE_TYPE_NORMAL, fichier_camera + ) + tmp_path = str(chemin_dest) + ".tmp" + fichier_camera.save(tmp_path) - # Redimensionner (max 4000px de large, JPEG 92%) - img = PILImage.open(tmp_path) - if img.width > 4000: - ratio = 4000 / img.width - img = img.resize((4000, int(img.height * ratio)), PILImage.LANCZOS) - img.save(str(chemin_dest), "JPEG", quality=92) - Path(tmp_path).unlink(missing_ok=True) + img = PILImage.open(tmp_path) + if img.width > 4000: + ratio = 4000 / img.width + img = img.resize((4000, int(img.height * ratio)), PILImage.LANCZOS) + img.save(str(chemin_dest), "JPEG", quality=92) + Path(tmp_path).unlink(missing_ok=True) - log.info(f"Photo capturee (DSLR) : {chemin_dest} ({img.width}x{img.height})") - return chemin_dest - except gp.GPhoto2Error as e: - if "I/O in progress" in str(e) and attempt < 2: - log.warning(f"DSLR occupe, retry {attempt + 1}/3...") - time.sleep(0.5) - continue - log.error(f"Erreur capture DSLR : {e}") - return None + log.info(f"Photo capturee (DSLR) : {chemin_dest} ({img.width}x{img.height})") + return chemin_dest + except gp.GPhoto2Error as e: + if "I/O in progress" in str(e) and attempt < 2: + log.warning(f"DSLR occupe, retry {attempt + 1}/3...") + time.sleep(0.5) + continue + log.error(f"Erreur capture DSLR : {e}") + return None def _capture_webcam(self, chemin_dest: Path) -> Path | None: if not self.webcam or not self.webcam.isOpened(): @@ -203,47 +204,46 @@ class Camera: """Active le LiveView sur les Canon (nécessaire avant capture_preview).""" if self.mode != "gphoto2" or not self.connectee: return - try: - cfg = self.camera.get_config() - vf = cfg.get_child_by_name("viewfinder") - vf.set_value(1) - self.camera.set_config(cfg) - log.info("Viewfinder activé") - except Exception as e: - log.warning(f"Viewfinder non supporté : {e}") + with self._lock: + try: + cfg = self.camera.get_config() + vf = cfg.get_child_by_name("viewfinder") + vf.set_value(1) + self.camera.set_config(cfg) + log.info("Viewfinder activé") + except Exception as e: + log.warning(f"Viewfinder non supporté : {e}") def desactiver_viewfinder(self): """Désactive le LiveView.""" if self.mode != "gphoto2" or not self.connectee: return - try: - cfg = self.camera.get_config() - vf = cfg.get_child_by_name("viewfinder") - vf.set_value(0) - self.camera.set_config(cfg) - log.info("Viewfinder désactivé") - except Exception as e: - log.warning(f"Viewfinder non supporté : {e}") + with self._lock: + try: + cfg = self.camera.get_config() + vf = cfg.get_child_by_name("viewfinder") + vf.set_value(0) + self.camera.set_config(cfg) + log.info("Viewfinder désactivé") + except Exception as e: + log.warning(f"Viewfinder non supporté : {e}") def _preview_gphoto2(self) -> bytes | None: - try: - fichier = self.camera.capture_preview() - donnees = bytes(fichier.get_data_and_size()) - # Redimensionner pour alléger le WebSocket - img = cv2.imdecode( - np.frombuffer(donnees, dtype=np.uint8), - cv2.IMREAD_COLOR, - ) - if img is not None: - h, w = img.shape[:2] - if w > 960: - img = cv2.resize(img, (960, int(h * 960 / w)), interpolation=cv2.INTER_LINEAR) - _, buf = cv2.imencode(".jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 75]) - return buf.tobytes() - return donnees - except gp.GPhoto2Error as e: - log.error(f"Erreur preview DSLR : {e}") - return None + with self._lock: + try: + fichier = self.camera.capture_preview() + donnees = bytes(fichier.get_data_and_size()) + img = cv2.imdecode(np.frombuffer(donnees, dtype=np.uint8), cv2.IMREAD_COLOR) + if img is not None: + h, w = img.shape[:2] + if w > 960: + img = cv2.resize(img, (960, int(h * 960 / w)), interpolation=cv2.INTER_LINEAR) + _, buf = cv2.imencode(".jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 75]) + return buf.tobytes() + return donnees + except gp.GPhoto2Error as e: + log.error(f"Erreur preview DSLR : {e}") + return None def _preview_webcam(self) -> bytes | None: if not self.webcam or not self.webcam.isOpened():